diff options
Diffstat (limited to 'client')
9 files changed, 1938 insertions, 47 deletions
diff --git a/client/src/com/vaadin/client/connectors/GridConnector.java b/client/src/com/vaadin/client/connectors/GridConnector.java index 55f07ecf85..495bcd0411 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,26 @@ public class GridConnector extends AbstractHasComponentsConnector implements } } + private ColumnReorderHandler<JsonObject> columnReorderHandler = new ColumnReorderHandler<JsonObject>() { + + @Override + public void onColumnReorder(ColumnReorderEvent<JsonObject> event) { + if (!columnsUpdatedFromState) { + List<Column<?, JsonObject>> columns = getWidget().getColumns(); + final List<String> newColumnOrder = new ArrayList<String>(); + for (Column<?, JsonObject> column : columns) { + if (column instanceof CustomGridColumn) { + newColumnOrder.add(((CustomGridColumn) column).id); + } // the other case would be the multi selection column + } + getRpcProxy(GridServerRpc.class).columnsReordered( + newColumnOrder, columnOrder); + columnOrder = newColumnOrder; + getState().columnOrder = newColumnOrder; + } + } + }; + /** * Maps a generated column id to a grid column instance */ @@ -370,13 +392,22 @@ public class GridConnector extends AbstractHasComponentsConnector implements private List<String> columnOrder = new ArrayList<String>(); /** - * updateFromState is set to true when {@link #updateSelectionFromState()} - * makes changes to selection. This flag tells the - * {@code internalSelectionChangeHandler} to not send same data straight - * back to server. Said listener sets it back to false when handling that - * event. + * {@link #selectionUpdatedFromState} is set to true when + * {@link #updateSelectionFromState()} makes changes to selection. This flag + * tells the {@code internalSelectionChangeHandler} to not send same data + * straight back to server. Said listener sets it back to false when + * handling that event. + */ + private boolean selectionUpdatedFromState; + + /** + * {@link #columnsUpdatedFromState} is set to true when + * {@link #updateColumnOrderFromState(List)} is updating the column order + * for the widget. This flag tells the {@link #columnReorderHandler} to not + * send same data straight back to server. After updates, listener sets the + * value back to false. */ - private boolean updatedFromState = false; + private boolean columnsUpdatedFromState; private RpcDataSource dataSource; @@ -386,7 +417,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 +429,7 @@ public class GridConnector extends AbstractHasComponentsConnector implements getRpcProxy(GridServerRpc.class).select( new ArrayList<String>(selectedKeys)); } else { - updatedFromState = false; + selectionUpdatedFromState = false; } } }; @@ -501,6 +532,9 @@ public class GridConnector extends AbstractHasComponentsConnector implements }); getWidget().setEditorHandler(new CustomEditorHandler()); + + getWidget().addColumnReorderHandler(columnReorderHandler); + getLayoutManager().registerDependency(this, getWidget().getElement()); layout(); } @@ -594,7 +628,9 @@ public class GridConnector extends AbstractHasComponentsConnector implements columns[i] = columnIdToColumn.get(id); i++; } + columnsUpdatedFromState = true; getWidget().setColumnOrder(columns); + columnsUpdatedFromState = false; columnOrder = stateColumnOrder; } @@ -891,7 +927,7 @@ public class GridConnector extends AbstractHasComponentsConnector implements if (changed) { // At least for now there's no way to send the selected and/or // deselected row data. Some data is only stored as keys - updatedFromState = true; + selectionUpdatedFromState = true; getWidget().fireEvent( new SelectionEvent<JsonObject>(getWidget(), (List<JsonObject>) null, null, false)); 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..0710606818 --- /dev/null +++ b/client/src/com/vaadin/client/ui/dd/DragAndDropHandler.java @@ -0,0 +1,248 @@ +/* + * 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. + * <p> + * The showing of the dragged element, drag hints and reacting to drop/cancel is + * delegated to {@link DragAndDropCallback} implementation. + * + * @since + * @author Vaadin Ltd + */ +public class DragAndDropHandler { + + /** + * Callback interface for drag and drop. + */ + public interface DragAndDropCallback { + /** + * Called when the drag has started. The drag can be canceled by + * returning {@code false}. + * + * @param startEvent + * the original event that started the drag + * @return {@code true} if the drag is OK to start, {@code false} to + * cancel + */ + boolean onDragStart(NativeEvent startEvent); + + /** + * Called on drag. + * + * @param event + * the event related to the drag + */ + void onDragUpdate(NativePreviewEvent event); + + /** + * Called after the has ended on a drop or cancel. + */ + void onDragEnd(); + + /** + * Called when the drag has ended on a drop. + */ + void onDrop(); + + /** + * Called when the drag has been canceled. + */ + void onDragCancel(); + } + + private HandlerRegistration dragStartNativePreviewHandlerRegistration; + private HandlerRegistration dragHandlerRegistration; + + private boolean dragging; + + private DragAndDropCallback callback; + + private final NativePreviewHandler dragHandler = new NativePreviewHandler() { + + @Override + public void onPreviewNativeEvent(NativePreviewEvent event) { + if (dragging) { + final int typeInt = event.getTypeInt(); + switch (typeInt) { + case Event.ONKEYDOWN: + int keyCode = event.getNativeEvent().getKeyCode(); + if (keyCode == KeyCodes.KEY_ESCAPE) { + // end drag if ESC is hit + cancelDrag(event); + } + break; + case Event.ONMOUSEMOVE: + case Event.ONTOUCHMOVE: + callback.onDragUpdate(event); + // prevent text selection on IE + event.getNativeEvent().preventDefault(); + break; + case Event.ONTOUCHCANCEL: + cancelDrag(event); + break; + case Event.ONTOUCHEND: + /* Avoid simulated event on drag end */ + event.getNativeEvent().preventDefault(); + //$FALL-THROUGH$ + case Event.ONMOUSEUP: + callback.onDragUpdate(event); + callback.onDrop(); + stopDrag(); + event.cancel(); + break; + default: + break; + } + } else { + stopDrag(); + } + } + + }; + + 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(dragStartingEvent, event, callback); + } + break; + default: + // on any other events, clean up this preview + // listener + removeNativePreviewHandlerRegistration(); + break; + } + } + }); + } + + private void startDrag(NativeEvent startEvent, + NativePreviewEvent triggerEvent, DragAndDropCallback callback) { + if (callback.onDragStart(startEvent)) { + dragging = true; + // just capture something to prevent text selection in IE + Event.setCapture(RootPanel.getBodyElement()); + this.callback = callback; + dragHandlerRegistration = Event + .addNativePreviewHandler(dragHandler); + callback.onDragUpdate(triggerEvent); + } + } + + private void stopDrag() { + dragging = false; + if (dragHandlerRegistration != null) { + dragHandlerRegistration.removeHandler(); + dragHandlerRegistration = null; + } + Event.releaseCapture(RootPanel.getBodyElement()); + if (callback != null) { + callback.onDragEnd(); + callback = null; + } + } + + private void cancelDrag(NativePreviewEvent event) { + callback.onDragCancel(); + callback.onDragEnd(); + stopDrag(); + event.cancel(); + event.getNativeEvent().preventDefault(); + } + + private void removeNativePreviewHandlerRegistration() { + if (dragStartNativePreviewHandlerRegistration != null) { + dragStartNativePreviewHandlerRegistration.removeHandler(); + dragStartNativePreviewHandlerRegistration = null; + } + } +} diff --git a/client/src/com/vaadin/client/widget/grid/AutoScroller.java b/client/src/com/vaadin/client/widget/grid/AutoScroller.java new file mode 100644 index 0000000000..f7c80df623 --- /dev/null +++ b/client/src/com/vaadin/client/widget/grid/AutoScroller.java @@ -0,0 +1,698 @@ +/* + * 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. + */ + public interface AutoScrollerCallback { + + /** + * Triggered when doing automatic scrolling. + * <p> + * Because the auto scroller currently only supports scrolling in one + * axis, this method is used for both vertical and horizontal scrolling. + * + * @param scrollDiff + * the amount of pixels that have been auto scrolled since + * last call + */ + void onAutoScroll(int scrollDiff); + + /** + * Triggered when the grid scroll has reached the minimum scroll + * position. Depending on the scroll axis, either scrollLeft or + * scrollTop is 0. + */ + void onAutoScrollReachedMin(); + + /** + * Triggered when the grid scroll has reached the max scroll position. + * Depending on the scroll axis, either scrollLeft or scrollTop is at + * its maximum value. + */ + void onAutoScrollReachedMax(); + } + + public enum ScrollAxis { + VERTICAL, HORIZONTAL + }; + + /** The maximum number of pixels per second to autoscroll. */ + private static final int SCROLL_TOP_SPEED_PX_SEC = 500; + + /** + * The minimum area where the grid doesn't scroll while the pointer is + * pressed. + */ + private static final int MIN_NO_AUTOSCROLL_AREA_PX = 50; + + /** The size of the autoscroll area, both top/left and bottom/right. */ + private int scrollAreaPX = 100; + + /** + * This class's main objective is to listen when to stop autoscrolling, and + * make sure everything stops accordingly. + */ + private class TouchEventHandler implements NativePreviewHandler { + @Override + public void onPreviewNativeEvent(final NativePreviewEvent event) { + /* + * Remember: targetElement is always where touchstart started, not + * where the finger is pointing currently. + */ + switch (event.getTypeInt()) { + case Event.ONTOUCHSTART: { + if (event.getNativeEvent().getTouches().length() == 1) { + /* + * Something has dropped a touchend/touchcancel and the + * scroller is most probably running amok. Let's cancel it + * and pretend that everything's going as expected + * + * Because this is a preview, this code is run before start + * event can be passed to the start(...) method. + */ + stop(); + + /* + * Related TODO: investigate why iOS seems to ignore a + * touchend/touchcancel when frames are dropped, and/or if + * something can be done about that. + */ + } + break; + } + + case Event.ONTOUCHMOVE: + event.cancel(); + break; + + case Event.ONTOUCHEND: + case Event.ONTOUCHCANCEL: + // TODO investigate if this works as desired + stop(); + break; + } + } + + } + + /** + * This class's responsibility is to scroll the table while a pointer is + * kept in a scrolling zone. + * <p> + * <em>Techical note:</em> This class is an AnimationCallback because we + * need a timer: when the finger is kept in place while the grid scrolls, we + * still need to be able to make new selections. So, instead of relying on + * events (which won't be fired, since the pointer isn't necessarily + * moving), we do this check on each frame while the pointer is "active" + * (mouse is pressed, finger is on screen). + */ + private class AutoScrollingFrame implements AnimationCallback { + + /** + * If the acceleration gradient area is smaller than this, autoscrolling + * will be disabled (it becomes too quick to accelerate to be usable). + */ + private static final int GRADIENT_MIN_THRESHOLD_PX = 10; + + /** + * The speed at which the gradient area recovers, once scrolling in that + * direction has started. + */ + private static final int SCROLL_AREA_REBOUND_PX_PER_SEC = 1; + private static final double SCROLL_AREA_REBOUND_PX_PER_MS = SCROLL_AREA_REBOUND_PX_PER_SEC / 1000.0d; + + /** + * The lowest y/x-coordinate on the {@link Event#getClientY() client-y} + * or {@link Event#getClientX() client-x} from where we need to start + * scrolling towards the top/left. + */ + private int startBound = -1; + + /** + * The highest y/x-coordinate on the {@link Event#getClientY() client-y} + * or {@link Event#getClientX() client-x} from where we need to + * scrolling towards the bottom. + */ + private int endBound = -1; + + /** + * The area where the selection acceleration takes place. If < + * {@link #GRADIENT_MIN_THRESHOLD_PX}, autoscrolling is disabled + */ + private final int gradientArea; + + /** + * The number of pixels per seconds we currently are scrolling (negative + * is towards the top/left, positive is towards the bottom/right). + */ + private double scrollSpeed = 0; + + private double prevTimestamp = 0; + + /** + * This field stores fractions of pixels to scroll, to make sure that + * we're able to scroll less than one px per frame. + */ + private double pixelsToScroll = 0.0d; + + /** Should this animator be running. */ + private boolean running = false; + + /** The handle in which this instance is running. */ + private AnimationHandle handle; + + /** + * The pointer's pageY (VERTICAL) / pageX (HORIZONTAL) coordinate + * depending on scrolling axis. + */ + private int scrollingAxisPageCoordinate; + + /** @see #doScrollAreaChecks(int) */ + private int finalStartBound; + + /** @see #doScrollAreaChecks(int) */ + private int finalEndBound; + + private boolean scrollAreaShouldRebound = false; + + public AutoScrollingFrame(final int startBound, final int endBound, + final int gradientArea) { + finalStartBound = startBound; + finalEndBound = endBound; + this.gradientArea = gradientArea; + } + + @Override + public void execute(final double timestamp) { + final double timeDiff = timestamp - prevTimestamp; + prevTimestamp = timestamp; + + reboundScrollArea(timeDiff); + + pixelsToScroll += scrollSpeed * (timeDiff / 1000.0d); + final int intPixelsToScroll = (int) pixelsToScroll; + pixelsToScroll -= intPixelsToScroll; + + if (intPixelsToScroll != 0) { + double scrollPos; + double maxScrollPos; + double newScrollPos; + if (scrollDirection == ScrollAxis.VERTICAL) { + scrollPos = grid.getScrollTop(); + maxScrollPos = getMaxScrollTop(); + } else { + scrollPos = grid.getScrollLeft(); + maxScrollPos = getMaxScrollLeft(); + } + if (intPixelsToScroll > 0 && scrollPos < maxScrollPos + || intPixelsToScroll < 0 && scrollPos > 0) { + newScrollPos = scrollPos + intPixelsToScroll; + if (scrollDirection == ScrollAxis.VERTICAL) { + grid.setScrollTop(newScrollPos); + } else { + grid.setScrollLeft(newScrollPos); + } + callback.onAutoScroll(intPixelsToScroll); + if (newScrollPos <= 0) { + callback.onAutoScrollReachedMin(); + } else if (newScrollPos >= maxScrollPos) { + callback.onAutoScrollReachedMax(); + } + } + } + + reschedule(); + } + + /** + * If the scroll are has been offset by the pointer starting out there, + * move it back a bit + */ + private void reboundScrollArea(double timeDiff) { + if (!scrollAreaShouldRebound) { + return; + } + + int reboundPx = (int) Math.ceil(SCROLL_AREA_REBOUND_PX_PER_MS + * timeDiff); + if (startBound < finalStartBound) { + startBound += reboundPx; + startBound = Math.min(startBound, finalStartBound); + updateScrollSpeed(scrollingAxisPageCoordinate); + } else if (endBound > finalEndBound) { + endBound -= reboundPx; + endBound = Math.max(endBound, finalEndBound); + updateScrollSpeed(scrollingAxisPageCoordinate); + } + } + + private void updateScrollSpeed(final int pointerPageCordinate) { + + final double ratio; + if (pointerPageCordinate < startBound) { + final double distance = pointerPageCordinate - startBound; + ratio = Math.max(-1, distance / gradientArea); + } + + else if (pointerPageCordinate > endBound) { + final double distance = pointerPageCordinate - endBound; + ratio = Math.min(1, distance / gradientArea); + } + + else { + ratio = 0; + } + + scrollSpeed = ratio * SCROLL_TOP_SPEED_PX_SEC; + } + + public void start() { + running = true; + reschedule(); + } + + public void stop() { + running = false; + + if (handle != null) { + handle.cancel(); + handle = null; + } + } + + private void reschedule() { + if (running && gradientArea >= GRADIENT_MIN_THRESHOLD_PX) { + handle = AnimationScheduler.get().requestAnimationFrame(this, + grid.getElement()); + } + } + + public void updatePointerCoords(int pageX, int pageY) { + final int pageCordinate; + if (scrollDirection == ScrollAxis.VERTICAL) { + pageCordinate = pageY; + } else { + pageCordinate = pageX; + } + doScrollAreaChecks(pageCordinate); + updateScrollSpeed(pageCordinate); + scrollingAxisPageCoordinate = pageCordinate; + } + + /** + * This method checks whether the first pointer event started in an area + * that would start scrolling immediately, and does some actions + * accordingly. + * <p> + * If it is, that scroll area will be offset "beyond" the pointer (above + * if pointer is towards the top/left, otherwise below/right). + */ + private void doScrollAreaChecks(int pageCordinate) { + /* + * The first run makes sure that neither scroll position is + * underneath the finger, but offset to either direction from + * underneath the pointer. + */ + if (startBound == -1) { + startBound = Math.min(finalStartBound, pageCordinate); + endBound = Math.max(finalEndBound, pageCordinate); + } + + /* + * Subsequent runs make sure that the scroll area grows (but doesn't + * shrink) with the finger, but no further than the final bound. + */ + else { + int oldTopBound = startBound; + if (startBound < finalStartBound) { + startBound = Math.max(startBound, + Math.min(finalStartBound, pageCordinate)); + } + + int oldBottomBound = endBound; + if (endBound > finalEndBound) { + endBound = Math.min(endBound, + Math.max(finalEndBound, pageCordinate)); + } + + final boolean startDidNotMove = oldTopBound == startBound; + final boolean endDidNotMove = oldBottomBound == endBound; + final boolean wasMovement = pageCordinate != scrollingAxisPageCoordinate; + scrollAreaShouldRebound = (startDidNotMove && endDidNotMove && wasMovement); + } + } + } + + /** + * This handler makes sure that pointer movements are handled. + * <p> + * Essentially, a native preview handler is registered (so that selection + * gestures can happen outside of the selection column). The handler itself + * makes sure that it's detached when the pointer is "lifted". + */ + private final NativePreviewHandler scrollPreviewHandler = new NativePreviewHandler() { + @Override + public void onPreviewNativeEvent(final NativePreviewEvent event) { + if (autoScroller == null) { + stop(); + return; + } + + final NativeEvent nativeEvent = event.getNativeEvent(); + int pageY = 0; + int pageX = 0; + switch (event.getTypeInt()) { + case Event.ONMOUSEMOVE: + case Event.ONTOUCHMOVE: + pageY = WidgetUtil.getTouchOrMouseClientY(nativeEvent); + pageX = WidgetUtil.getTouchOrMouseClientX(nativeEvent); + autoScroller.updatePointerCoords(pageX, pageY); + break; + case Event.ONMOUSEUP: + case Event.ONTOUCHEND: + case Event.ONTOUCHCANCEL: + stop(); + break; + } + } + }; + /** The registration info for {@link #scrollPreviewHandler} */ + private HandlerRegistration handlerRegistration; + + /** + * The top/left bound, as calculated from the {@link Event#getClientY() + * client-y} or {@link Event#getClientX() client-x} coordinates. + */ + private double startingBound = -1; + + /** + * The bottom/right bound, as calculated from the {@link Event#getClientY() + * client-y} or or {@link Event#getClientX() client-x} coordinates. + */ + private int endingBound = -1; + + /** The size of the autoscroll acceleration area. */ + private int gradientArea; + + private Grid<?> grid; + + private HandlerRegistration nativePreviewHandlerRegistration; + + private ScrollAxis scrollDirection; + + private AutoScrollingFrame autoScroller; + + private AutoScrollerCallback callback; + + /** + * Creates a new instance for scrolling the given grid. + * + * @param grid + * the grid to auto scroll + */ + public AutoScroller(Grid<?> grid) { + this.grid = grid; + } + + /** + * Starts the automatic scrolling detection. 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. + * <p> + * 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. + * <p> + * 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(); + startBorder += getFrozenColumnsWidth(); + + final int scrollCompensation = getScrollCompensation(); + startingBound = scrollCompensation + startBorder + scrollAreaPX; + endingBound = scrollCompensation + endBorder - scrollAreaPX; + gradientArea = scrollAreaPX; + + // modify bounds if they're too tightly packed + if (endingBound - startingBound < MIN_NO_AUTOSCROLL_AREA_PX) { + double adjustment = MIN_NO_AUTOSCROLL_AREA_PX + - (endingBound - startingBound); + startingBound -= adjustment / 2; + endingBound += adjustment / 2; + gradientArea -= adjustment / 2; + } + } + + private int getScrollCompensation() { + Element cursor = grid.getElement(); + int scroll = 0; + while (cursor != null) { + scroll -= scrollDirection == ScrollAxis.VERTICAL ? cursor + .getScrollTop() : cursor.getScrollLeft(); + cursor = cursor.getParentElement(); + } + + return scroll; + } + + private void injectNativeHandler() { + removeNativeHandler(); + nativePreviewHandlerRegistration = Event + .addNativePreviewHandler(new TouchEventHandler()); + } + + private void removeNativeHandler() { + if (nativePreviewHandlerRegistration != null) { + nativePreviewHandlerRegistration.removeHandler(); + nativePreviewHandlerRegistration = null; + } + } + + private TableElement getTableElement() { + final Element root = grid.getElement(); + final Element tablewrapper = Element.as(root.getChild(2)); + if (tablewrapper != null) { + return TableElement.as(tablewrapper.getFirstChildElement()); + } else { + return null; + } + } + + private TableSectionElement getTbodyElement() { + TableElement table = getTableElement(); + if (table != null) { + return table.getTBodies().getItem(0); + } else { + return null; + } + } + + private TableSectionElement getTheadElement() { + TableElement table = getTableElement(); + if (table != null) { + return table.getTHead(); + } else { + return null; + } + } + + private TableSectionElement getTfootElement() { + TableElement table = getTableElement(); + if (table != null) { + return table.getTFoot(); + } else { + return null; + } + } + + /** Get the "top" of an element in relation to "client" coordinates. */ + @SuppressWarnings("static-method") + private int getClientTop(final Element e) { + Element cursor = e; + int top = 0; + while (cursor != null) { + top += cursor.getOffsetTop(); + cursor = cursor.getOffsetParent(); + } + return top; + } + + /** Get the "left" of an element in relation to "client" coordinates. */ + @SuppressWarnings("static-method") + private int getClientLeft(final Element e) { + Element cursor = e; + int left = 0; + while (cursor != null) { + left += cursor.getOffsetLeft(); + cursor = cursor.getOffsetParent(); + } + return left; + } + + private int getBodyClientEnd() { + if (scrollDirection == ScrollAxis.VERTICAL) { + return getClientTop(getTfootElement()) - 1; + } else { + TableSectionElement tbodyElement = getTbodyElement(); + return getClientLeft(tbodyElement) + tbodyElement.getOffsetWidth() + - 1; + } + + } + + private int getBodyClientStart() { + if (scrollDirection == ScrollAxis.VERTICAL) { + return getClientTop(grid.getElement()) + + getTheadElement().getOffsetHeight(); + } else { + return getClientLeft(getTbodyElement()); + } + } + + private double getFrozenColumnsWidth() { + double value = getMultiSelectColumnWidth(); + for (int i = 0; i < grid.getFrozenColumnCount(); i++) { + value += grid.getColumn(i).getWidthActual(); + } + return value; + } + + private double getMultiSelectColumnWidth() { + if (grid.getFrozenColumnCount() >= 0 + && grid.getSelectionModel().getSelectionColumnRenderer() != null) { + // frozen checkbox column is present + return getTheadElement().getFirstChildElement() + .getFirstChildElement().getOffsetWidth(); + } + return 0.0; + } + + private double getMaxScrollLeft() { + return grid.getScrollWidth() + - (getTableElement().getParentElement().getOffsetWidth() - getFrozenColumnsWidth()); + } + + private double getMaxScrollTop() { + return grid.getScrollHeight() - getTfootElement().getOffsetHeight() + - getTheadElement().getOffsetHeight(); + } +} diff --git a/client/src/com/vaadin/client/widget/grid/events/ColumnReorderEvent.java b/client/src/com/vaadin/client/widget/grid/events/ColumnReorderEvent.java new file mode 100644 index 0000000000..c72da0c48e --- /dev/null +++ b/client/src/com/vaadin/client/widget/grid/events/ColumnReorderEvent.java @@ -0,0 +1,51 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.client.widget.grid.events; + +import com.google.gwt.event.shared.GwtEvent; + +/** + * An event for notifying that the columns in the Grid have been reordered. + * + * @param <T> + * The row type of the grid. The row type is the POJO type from where + * the data is retrieved into the column cells. + * @since + * @author Vaadin Ltd + */ +public class ColumnReorderEvent<T> extends GwtEvent<ColumnReorderHandler<T>> { + + /** + * Handler type. + */ + private final static Type<ColumnReorderHandler<?>> TYPE = new Type<ColumnReorderHandler<?>>(); + + public static final Type<ColumnReorderHandler<?>> getType() { + return TYPE; + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + @Override + public Type<ColumnReorderHandler<T>> getAssociatedType() { + return (Type) TYPE; + } + + @Override + protected void dispatch(ColumnReorderHandler<T> handler) { + handler.onColumnReorder(this); + } + +} diff --git a/client/src/com/vaadin/client/widget/grid/events/ColumnReorderHandler.java b/client/src/com/vaadin/client/widget/grid/events/ColumnReorderHandler.java new file mode 100644 index 0000000000..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 <T> + * The row type of the grid. The row type is the POJO type from where + * the data is retrieved into the column cells. + * @since + * @author Vaadin Ltd + */ +public interface ColumnReorderHandler<T> 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<T> event); +} diff --git a/client/src/com/vaadin/client/widget/grid/events/ColumnVisibilityChangeEvent.java b/client/src/com/vaadin/client/widget/grid/events/ColumnVisibilityChangeEvent.java new file mode 100644 index 0000000000..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 <T> + * The row type of the grid. The row type is the POJO type from where + * the data is retrieved into the column cells. + * @since + * @author Vaadin Ltd + */ +public class ColumnVisibilityChangeEvent<T> extends + GwtEvent<ColumnVisibilityChangeHandler<T>> { + + private final static Type<ColumnVisibilityChangeHandler<?>> TYPE = new Type<ColumnVisibilityChangeHandler<?>>(); + + public static final Type<ColumnVisibilityChangeHandler<?>> getType() { + return TYPE; + } + + private final Column<?, T> column; + + private final boolean userOriginated; + + private final boolean hidden; + + public ColumnVisibilityChangeEvent(Column<?, T> column, boolean hidden, + boolean userOriginated) { + this.column = column; + this.hidden = hidden; + this.userOriginated = userOriginated; + } + + /** + * Returns the column where the visibility change occurred. + * + * @return the column where the visibility change occurred. + */ + public Column<?, T> getColumn() { + return column; + } + + /** + * Is the column hidden or visible. + * + * @return <code>true</code> if the column was hidden <code>false</code> if + * it was set visible + */ + public boolean isHidden() { + return hidden; + } + + /** + * Is the visibility change triggered by user. + * + * @return <code>true</code> if the change was triggered by user, + * <code>false</code> if not + */ + public boolean isUserOriginated() { + return userOriginated; + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + @Override + public com.google.gwt.event.shared.GwtEvent.Type<ColumnVisibilityChangeHandler<T>> getAssociatedType() { + return (Type) TYPE; + } + + @Override + protected void dispatch(ColumnVisibilityChangeHandler<T> handler) { + handler.onVisibilityChange(this); + } + +} diff --git a/client/src/com/vaadin/client/widget/grid/events/ColumnVisibilityChangeHandler.java b/client/src/com/vaadin/client/widget/grid/events/ColumnVisibilityChangeHandler.java new file mode 100644 index 0000000000..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<T> The row type of the grid. The row type is the POJO type from where + * the data is retrieved into the column cells. + * @since + * @author Vaadin Ltd + */ +public interface ColumnVisibilityChangeHandler<T> extends EventHandler { + + /** + * A column visibility change event, fired by Grid when a column in the Grid + * has changed visibility. + * + * @param event + * column visibility change event + */ + public void onVisibilityChange(ColumnVisibilityChangeEvent<T> event); +} diff --git a/client/src/com/vaadin/client/widgets/Escalator.java b/client/src/com/vaadin/client/widgets/Escalator.java index 679b452fde..828d9ca29e 100644 --- a/client/src/com/vaadin/client/widgets/Escalator.java +++ b/client/src/com/vaadin/client/widgets/Escalator.java @@ -1383,10 +1383,10 @@ public class Escalator extends Widget implements RequiresResize, * first time. */ Map<Integer, Double> colWidths = new HashMap<Integer, Double>(); - Double width = Double - .valueOf(ColumnConfigurationImpl.Column.DEFAULT_COLUMN_WIDTH_PX); for (int i = 0; i < getColumnConfiguration() .getColumnCount(); i++) { + Double width = Double.valueOf(getColumnConfiguration() + .getColumnWidth(i)); Integer col = Integer.valueOf(i); colWidths.put(col, width); } @@ -5786,6 +5786,28 @@ public class Escalator extends Widget implements RequiresResize, } /** + * 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 * the given scroll destination. diff --git a/client/src/com/vaadin/client/widgets/Grid.java b/client/src/com/vaadin/client/widgets/Grid.java index b3906591c0..ea9525f157 100644 --- a/client/src/com/vaadin/client/widgets/Grid.java +++ b/client/src/com/vaadin/client/widgets/Grid.java @@ -25,7 +25,9 @@ import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Map.Entry; import java.util.Set; +import java.util.TreeMap; import java.util.logging.Level; import java.util.logging.Logger; @@ -36,6 +38,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.NativeEvent; import com.google.gwt.dom.client.Node; import com.google.gwt.dom.client.Style; import com.google.gwt.dom.client.Style.Unit; @@ -54,6 +57,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; @@ -70,6 +75,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; @@ -81,6 +88,9 @@ import com.vaadin.client.widget.escalator.RowVisibilityChangeHandler; import com.vaadin.client.widget.escalator.ScrollbarBundle.Direction; import com.vaadin.client.widget.escalator.Spacer; import com.vaadin.client.widget.escalator.SpacerUpdater; +import com.vaadin.client.widget.grid.AutoScroller; +import com.vaadin.client.widget.grid.AutoScroller.AutoScrollerCallback; +import com.vaadin.client.widget.grid.AutoScroller.ScrollAxis; import com.vaadin.client.widget.grid.CellReference; import com.vaadin.client.widget.grid.CellStyleGenerator; import com.vaadin.client.widget.grid.DataAvailableEvent; @@ -99,6 +109,10 @@ import com.vaadin.client.widget.grid.events.BodyDoubleClickHandler; import com.vaadin.client.widget.grid.events.BodyKeyDownHandler; import com.vaadin.client.widget.grid.events.BodyKeyPressHandler; import com.vaadin.client.widget.grid.events.BodyKeyUpHandler; +import com.vaadin.client.widget.grid.events.ColumnReorderEvent; +import com.vaadin.client.widget.grid.events.ColumnReorderHandler; +import com.vaadin.client.widget.grid.events.ColumnVisibilityChangeEvent; +import com.vaadin.client.widget.grid.events.ColumnVisibilityChangeHandler; import com.vaadin.client.widget.grid.events.FooterClickHandler; import com.vaadin.client.widget.grid.events.FooterDoubleClickHandler; import com.vaadin.client.widget.grid.events.FooterKeyDownHandler; @@ -133,6 +147,8 @@ import com.vaadin.client.widget.grid.sort.SortOrder; import com.vaadin.client.widgets.Escalator.AbstractRowContainer; import com.vaadin.client.widgets.Escalator.SubPartArguments; import com.vaadin.client.widgets.Grid.Editor.State; +import com.vaadin.client.widgets.Grid.StaticSection.StaticCell; +import com.vaadin.client.widgets.Grid.StaticSection.StaticRow; import com.vaadin.shared.data.sort.SortDirection; import com.vaadin.shared.ui.grid.GridConstants; import com.vaadin.shared.ui.grid.GridStaticCellType; @@ -411,6 +427,16 @@ public class Grid<T> extends ResizeComposite implements } /** + * Returns <code>true</code> 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 * * @param columns @@ -2132,6 +2158,8 @@ public class Grid<T> extends ResizeComposite implements boolean insertionIsAboveFocusedCell = (added.getStart() <= rowWithFocus); if (bodyHasFocus && insertionIsAboveFocusedCell) { rowWithFocus += added.length(); + rowWithFocus = Math.min(rowWithFocus, escalator.getBody() + .getRowCount() - 1); refreshRow(rowWithFocus); } } @@ -2159,10 +2187,12 @@ public class Grid<T> extends ResizeComposite implements rowWithFocus = removed.getStart() - 1; } else { if (escalator.getHeader().getRowCount() > 0) { - rowWithFocus = lastFocusedHeaderRow; + rowWithFocus = Math.min(lastFocusedHeaderRow, escalator + .getHeader().getRowCount() - 1); containerWithFocus = escalator.getHeader(); } else if (escalator.getFooter().getRowCount() > 0) { - rowWithFocus = lastFocusedFooterRow; + rowWithFocus = Math.min(lastFocusedFooterRow, escalator + .getFooter().getRowCount() - 1); containerWithFocus = escalator.getFooter(); } } @@ -2466,7 +2496,7 @@ public class Grid<T> extends ResizeComposite implements private boolean columnsAreGuaranteedToBeWiderThanGrid() { double freeSpace = escalator.getInnerWidth(); - for (Column<?, ?> column : getColumns()) { + for (Column<?, ?> column : getVisibleColumns()) { if (column.getWidth() >= 0) { freeSpace -= column.getWidth(); } else if (column.getMinimumWidth() >= 0) { @@ -2482,7 +2512,7 @@ public class Grid<T> extends ResizeComposite implements /* Step 1: Apply all column widths as they are. */ Map<Integer, Double> selfWidths = new LinkedHashMap<Integer, Double>(); - List<Column<?, T>> columns = getColumns(); + List<Column<?, T>> columns = getVisibleColumns(); for (int index = 0; index < columns.size(); index++) { selfWidths.put(index, columns.get(index).getWidth()); } @@ -2523,6 +2553,7 @@ public class Grid<T> extends ResizeComposite implements final Set<Column<?, T>> columnsToExpand = new HashSet<Column<?, T>>(); List<Column<?, T>> nonFixedColumns = new ArrayList<Column<?, T>>(); Map<Integer, Double> columnSizes = new HashMap<Integer, Double>(); + final List<Column<?, T>> visibleColumns = getVisibleColumns(); /* * Set all fixed widths and also calculate the size-to-fit widths @@ -2531,7 +2562,7 @@ public class Grid<T> extends ResizeComposite implements * This way we know with how many pixels we have left to expand the * rest. */ - for (Column<?, T> column : getColumns()) { + for (Column<?, T> column : visibleColumns) { final double widthAsIs = column.getWidth(); final boolean isFixedWidth = widthAsIs >= 0; final double widthFixed = Math.max(widthAsIs, @@ -2540,11 +2571,11 @@ public class Grid<T> 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); } } @@ -2561,7 +2592,7 @@ public class Grid<T> extends ResizeComposite implements columnsToExpand.add(column); } reservedPixels += newWidth; - columnSizes.put(indexOfColumn(column), newWidth); + columnSizes.put(visibleColumns.indexOf(column), newWidth); } /* @@ -2589,8 +2620,8 @@ public class Grid<T> extends ResizeComposite implements final Column<?, T> column = i.next(); final int expandRatio = getExpandRatio(column, defaultExpandRatios); - final double autoWidth = columnSizes - .get(indexOfColumn(column)); + final int columnIndex = visibleColumns.indexOf(column); + final double autoWidth = columnSizes.get(columnIndex); final double maxWidth = getMaxWidth(column); double expandedWidth = autoWidth + widthPerRatio * expandRatio; @@ -2600,7 +2631,7 @@ public class Grid<T> extends ResizeComposite implements totalRatios -= expandRatio; aColumnHasMaxedOut = true; pixelsToDistribute -= maxWidth - autoWidth; - columnSizes.put(indexOfColumn(column), maxWidth); + columnSizes.put(columnIndex, maxWidth); } } } while (aColumnHasMaxedOut); @@ -2636,13 +2667,14 @@ public class Grid<T> extends ResizeComposite implements for (Column<?, T> column : columnsToExpand) { final int expandRatio = getExpandRatio(column, defaultExpandRatios); - final double autoWidth = columnSizes.get(indexOfColumn(column)); + final int columnIndex = visibleColumns.indexOf(column); + final double autoWidth = columnSizes.get(columnIndex); double totalWidth = autoWidth + widthPerRatio * expandRatio; if (leftOver > 0) { totalWidth += 1; leftOver--; } - columnSizes.put(indexOfColumn(column), totalWidth); + columnSizes.put(columnIndex, totalWidth); totalRatios -= expandRatio; } @@ -2663,7 +2695,7 @@ public class Grid<T> extends ResizeComposite implements * remove those pixels from other columns */ double pixelsToRemoveFromOtherColumns = 0; - for (Column<?, T> column : getColumns()) { + for (Column<?, T> column : visibleColumns) { /* * We can't iterate over columnsToExpand, even though that * would be convenient. This is because some column without @@ -2672,11 +2704,11 @@ public class Grid<T> extends ResizeComposite implements */ double minWidth = getMinWidth(column); - double currentWidth = columnSizes - .get(indexOfColumn(column)); + final int columnIndex = visibleColumns.indexOf(column); + double currentWidth = columnSizes.get(columnIndex); boolean hasAutoWidth = column.getWidth() < 0; if (hasAutoWidth && currentWidth < minWidth) { - columnSizes.put(indexOfColumn(column), minWidth); + columnSizes.put(columnIndex, minWidth); pixelsToRemoveFromOtherColumns += (minWidth - currentWidth); minWidthsCausedReflows = true; @@ -2702,7 +2734,7 @@ public class Grid<T> extends ResizeComposite implements for (Column<?, T> column : columnsToExpand) { final double pixelsToRemove = pixelsToRemovePerRatio * getExpandRatio(column, defaultExpandRatios); - int colIndex = indexOfColumn(column); + int colIndex = visibleColumns.indexOf(column); columnSizes.put(colIndex, columnSizes.get(colIndex) - pixelsToRemove); } @@ -2934,11 +2966,424 @@ public class Grid<T> extends ResizeComposite implements private boolean enabled = true; + private DetailsGenerator detailsGenerator = DetailsGenerator.NULL; private GridSpacerUpdater gridSpacerUpdater = new GridSpacerUpdater(); /** A set keeping track of the indices of all currently open details */ private Set<Integer> visibleDetails = new HashSet<Integer>(); + private boolean columnReorderingAllowed; + + private DragAndDropHandler dndHandler = new DragAndDropHandler(); + + private AutoScroller autoScroller = new AutoScroller(this); + + private DragAndDropCallback headerCellDndCallback = new DragAndDropCallback() { + + private final AutoScrollerCallback autoScrollerCallback = new AutoScrollerCallback() { + + @Override + public void onAutoScroll(int scrollDiff) { + autoScrollX = scrollDiff; + onDragUpdate(null); + } + + @Override + public void onAutoScrollReachedMin() { + // make sure the drop marker is visible on the left + autoScrollX = 0; + updateDragDropMarker(clientX); + } + + @Override + public void onAutoScrollReachedMax() { + // make sure the drop marker is visible on the right + autoScrollX = 0; + updateDragDropMarker(clientX); + } + }; + /** + * Elements for displaying the dragged column(s) and drop marker + * properly + */ + private Element table; + private Element tableHeader; + /** Marks the column drop location */ + private Element dropMarker; + /** A copy of the dragged column(s), moves with cursor. */ + private Element dragElement; + /** Tracks index of the column whose left side the drop would occur */ + private int latestColumnDropIndex; + /** + * Map of possible drop positions for the column and the corresponding + * column index. + */ + private final TreeMap<Double, Integer> possibleDropPositions = new TreeMap<Double, Integer>(); + /** + * Makes sure that drag cancel doesn't cause anything unwanted like sort + */ + private HandlerRegistration columnSortPreventRegistration; + + private int clientX; + + /** How much the grid is being auto scrolled while dragging. */ + private int autoScrollX; + + private void initHeaderDragElementDOM() { + if (table == null) { + tableHeader = DOM.createTHead(); + dropMarker = DOM.createDiv(); + tableHeader.appendChild(dropMarker); + table = DOM.createTable(); + table.appendChild(tableHeader); + table.setClassName("header-drag-table"); + } + // update the style names on each run in case primary name has been + // modified + tableHeader.setClassName(escalator.getHeader().getElement() + .getClassName()); + dropMarker.setClassName(getStylePrimaryName() + "-drop-marker"); + int topOffset = 0; + for (int i = 0; i < eventCell.getRowIndex(); i++) { + topOffset += escalator.getHeader().getRowElement(i) + .getFirstChildElement().getOffsetHeight(); + } + tableHeader.getStyle().setTop(topOffset, Unit.PX); + + getElement().appendChild(table); + } + + @Override + public void onDragUpdate(NativePreviewEvent event) { + if (event != null) { + clientX = WidgetUtil.getTouchOrMouseClientX(event + .getNativeEvent()); + autoScrollX = 0; + } + resolveDragElementHorizontalPosition(clientX); + updateDragDropMarker(clientX); + } + + private void updateDragDropMarker(final int clientX) { + final double scrollLeft = getScrollLeft(); + final double cursorXCoordinate = clientX + - escalator.getHeader().getElement().getAbsoluteLeft(); + final Entry<Double, Integer> cellEdgeOnRight = possibleDropPositions + .ceilingEntry(cursorXCoordinate); + final Entry<Double, Integer> cellEdgeOnLeft = possibleDropPositions + .floorEntry(cursorXCoordinate); + final double diffToRightEdge = cellEdgeOnRight == null ? Double.MAX_VALUE + : cellEdgeOnRight.getKey() - cursorXCoordinate; + final double diffToLeftEdge = cellEdgeOnLeft == null ? Double.MAX_VALUE + : cursorXCoordinate - cellEdgeOnLeft.getKey(); + + double dropMarkerLeft = 0 - scrollLeft; + if (diffToRightEdge > diffToLeftEdge) { + latestColumnDropIndex = cellEdgeOnLeft.getValue(); + dropMarkerLeft += cellEdgeOnLeft.getKey(); + } else { + latestColumnDropIndex = cellEdgeOnRight.getValue(); + dropMarkerLeft += cellEdgeOnRight.getKey(); + } + + dropMarkerLeft += autoScrollX; + + final double frozenColumnsWidth = getFrozenColumnsWidth(); + if (dropMarkerLeft < frozenColumnsWidth + || dropMarkerLeft > escalator.getHeader().getElement() + .getOffsetWidth() || dropMarkerLeft < 0) { + dropMarkerLeft = -10000000; + } + dropMarker.getStyle().setLeft(dropMarkerLeft, Unit.PX); + } + + private void resolveDragElementHorizontalPosition(final int clientX) { + double left = clientX - table.getAbsoluteLeft(); + final double frozenColumnsWidth = getFrozenColumnsWidth(); + if (left < frozenColumnsWidth) { + left = (int) frozenColumnsWidth; + } + + // do not show the drag element beyond a spanned header cell + // limitation + final Double leftBound = possibleDropPositions.firstKey(); + final Double rightBound = possibleDropPositions.lastKey(); + double scrollLeft = getScrollLeft(); + if (left + scrollLeft < leftBound) { + left = leftBound - scrollLeft + autoScrollX; + } else if (left + scrollLeft > rightBound) { + left = rightBound - scrollLeft + autoScrollX; + } + + // do not show the drag element beyond the grid + left = Math.max(0, Math.min(left, table.getClientWidth())); + + left -= dragElement.getClientWidth() / 2; + dragElement.getStyle().setLeft(left, Unit.PX); + } + + @Override + public boolean onDragStart(NativeEvent startingEvent) { + calculatePossibleDropPositions(); + + if (possibleDropPositions.isEmpty()) { + return false; + } + + initHeaderDragElementDOM(); + // needs to clone focus and sorting indicators too (UX) + dragElement = DOM.clone(eventCell.getElement(), true); + dragElement.getStyle().clearWidth(); + dropMarker.getStyle().setProperty("height", + dragElement.getStyle().getHeight()); + tableHeader.appendChild(dragElement); + // mark the column being dragged for styling + eventCell.getElement().addClassName("dragged"); + // mark the floating cell, for styling & testing + dragElement.addClassName("dragged-column-header"); + + // start the auto scroll handler + autoScroller.setScrollAreaPX(60); + autoScroller.start(startingEvent, ScrollAxis.HORIZONTAL, + autoScrollerCallback); + return true; + } + + @Override + public void onDragEnd() { + table.removeFromParent(); + dragElement.removeFromParent(); + eventCell.getElement().removeClassName("dragged"); + } + + @Override + public void onDrop() { + final int draggedColumnIndex = eventCell.getColumnIndex(); + final int colspan = header.getRow(eventCell.getRowIndex()) + .getCell(eventCell.getColumn()).getColspan(); + if (latestColumnDropIndex != draggedColumnIndex + && latestColumnDropIndex != (draggedColumnIndex + colspan)) { + List<Column<?, T>> columns = getColumns(); + List<Column<?, T>> reordered = new ArrayList<Column<?, T>>(); + if (draggedColumnIndex < latestColumnDropIndex) { + reordered.addAll(columns.subList(0, draggedColumnIndex)); + reordered.addAll(columns.subList(draggedColumnIndex + + colspan, latestColumnDropIndex)); + reordered.addAll(columns.subList(draggedColumnIndex, + draggedColumnIndex + colspan)); + reordered.addAll(columns.subList(latestColumnDropIndex, + columns.size())); + } else { + reordered.addAll(columns.subList(0, latestColumnDropIndex)); + reordered.addAll(columns.subList(draggedColumnIndex, + draggedColumnIndex + colspan)); + reordered.addAll(columns.subList(latestColumnDropIndex, + draggedColumnIndex)); + reordered.addAll(columns.subList(draggedColumnIndex + + colspan, columns.size())); + } + reordered.remove(selectionColumn); // since setColumnOrder will + // add it anyway! + + Column<?, T>[] array = reordered.toArray(new Column[reordered + .size()]); + setColumnOrder(array); + transferCellFocusOnDrop(); + } // else no reordering + } + + private void transferCellFocusOnDrop() { + final Cell focusedCell = cellFocusHandler.getFocusedCell(); + if (focusedCell != null) { + final int focusedCellColumnIndex = focusedCell.getColumn(); + final int focusedRowIndex = focusedCell.getRow(); + final int draggedColumnIndex = eventCell.getColumnIndex(); + // transfer focus if it was effected by the new column order + final RowContainer rowContainer = escalator + .findRowContainer(focusedCell.getElement()); + if (focusedCellColumnIndex == draggedColumnIndex) { + // move with the dragged column + final int adjustedDropIndex = latestColumnDropIndex > draggedColumnIndex ? latestColumnDropIndex - 1 + : latestColumnDropIndex; + cellFocusHandler.setCellFocus(focusedRowIndex, + adjustedDropIndex, 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 + // mouse was released on top of the dragged cell + if (columnSortPreventRegistration == null) { + columnSortPreventRegistration = Event + .addNativePreviewHandler(new NativePreviewHandler() { + + @Override + public void onPreviewNativeEvent( + NativePreviewEvent event) { + if (event.getTypeInt() == Event.ONCLICK) { + event.cancel(); + event.getNativeEvent().preventDefault(); + columnSortPreventRegistration + .removeHandler(); + columnSortPreventRegistration = null; + } + } + }); + } + autoScroller.stop(); + } + + private double getFrozenColumnsWidth() { + double value = getMultiSelectColumnWidth(); + for (int i = 0; i < getFrozenColumnCount(); i++) { + value += getColumn(i).getWidthActual(); + } + return value; + } + + private double getMultiSelectColumnWidth() { + if (getSelectionModel().getSelectionColumnRenderer() != null) { + // frozen checkbox column is present, it is always the first + // column + return escalator.getHeader().getElement() + .getFirstChildElement().getFirstChildElement() + .getOffsetWidth(); + } + return 0.0; + } + + /** + * Returns the amount of frozen columns. The selection column is always + * considered frozen, since it can't be moved. + */ + private int getSelectionAndFrozenColumnCount() { + // no matter if selection column is frozen or not, it is considered + // frozen for column dnd reorder + if (getSelectionModel().getSelectionColumnRenderer() != null) { + return Math.max(0, getFrozenColumnCount()) + 1; + } else { + return Math.max(0, getFrozenColumnCount()); + } + } + + @SuppressWarnings("boxing") + private void calculatePossibleDropPositions() { + possibleDropPositions.clear(); + + final int 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 intersects with a spanned cell in any other + * header or footer row, then the drag is limited inside that + * spanned cell. The same rules apply: the cell can't be dropped + * inside another spanned cell. The left and right bounds keep track + * of the edges of the most limiting spanned cell. + */ + int leftBound = -1; + int rightBound = getColumnCount() + 1; + + final HashSet<Integer> unavailableColumnDropIndices = new HashSet<Integer>(); + final List<StaticRow<?>> rows = new ArrayList<StaticRow<?>>(); + rows.addAll(header.getRows()); + rows.addAll(footer.getRows()); + for (StaticRow<?> row : rows) { + if (!row.hasSpannedCells()) { + continue; + } + final boolean isDraggedCellRow = row.equals(draggedCellRow); + for (int cellColumnIndex = frozenColumns; cellColumnIndex < getColumnCount(); cellColumnIndex++) { + StaticCell cell = row.getCell(getColumn(cellColumnIndex)); + int colspan = cell.getColspan(); + if (colspan <= 1) { + continue; + } + final int cellColumnRightIndex = cellColumnIndex + colspan; + final Range cellRange = Range.between(cellColumnIndex, + cellColumnRightIndex); + final boolean intersects = draggedCellRange + .intersects(cellRange); + if (intersects && !isDraggedCellRow) { + // if the currently iterated cell is inside or same as + // the dragged cell, then it doesn't restrict the drag + if (cellRange.isSubsetOf(draggedCellRange)) { + cellColumnIndex = cellColumnRightIndex - 1; + continue; + } + /* + * if the dragged cell is a spanned cell and it crosses + * with the currently iterated cell without sharing + * either start or end then not possible to drag the + * cell. + */ + if (!draggedCellRange.isSubsetOf(cellRange)) { + return; + } + // the spanned cell overlaps the dragged cell (but is + // not the dragged cell) + if (cellColumnIndex <= draggedCellIndex + && cellColumnIndex > leftBound) { + leftBound = cellColumnIndex; + } + if (cellColumnRightIndex < rightBound) { + rightBound = cellColumnRightIndex; + } + cellColumnIndex = cellColumnRightIndex - 1; + } + + else { // can't drop inside a spanned cell, or this is the + // dragged cell + while (colspan > 1) { + cellColumnIndex++; + colspan--; + unavailableColumnDropIndices.add(cellColumnIndex); + } + } + } + } + + if (leftBound == (rightBound - 1)) { + return; + } + + double position = getFrozenColumnsWidth(); + // iterate column indices and add possible drop positions + for (int i = frozenColumns; i < getColumnCount(); i++) { + 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()); + } + } + + }; + /** * Enumeration for easy setting of selection mode. */ @@ -3041,6 +3486,10 @@ public class Grid<T> extends ResizeComposite implements private boolean editable = true; + private boolean hidden = false; + + private boolean hideable = false; + private String headerCaption = ""; private double minimumWidthPx = GridConstants.DEFAULT_MIN_WIDTH; @@ -3211,6 +3660,9 @@ public class Grid<T> extends ResizeComposite implements * This action is done "finally", once the current execution loop * returns. This is done to reduce overhead of unintentionally always * recalculate all columns, when modifying several columns at once. + * <p> + * If the column is currently {@link #isHidden() hidden}, then this set + * width has effect only once the column has been made visible again. * * @param pixels * the width in pixels or negative for auto sizing @@ -3218,14 +3670,17 @@ public class Grid<T> extends ResizeComposite implements public Column<C, T> setWidth(double pixels) { if (!WidgetUtil.pixelValuesEqual(widthUser, pixels)) { widthUser = pixels; - scheduleColumnWidthRecalculator(); + if (!isHidden()) { + scheduleColumnWidthRecalculator(); + } } return this; } void doSetWidth(double pixels) { + assert !isHidden() : "applying width for a hidden column"; if (grid != null) { - int index = grid.columns.indexOf(this); + int index = grid.getVisibleColumns().indexOf(this); ColumnConfiguration conf = grid.escalator .getColumnConfiguration(); conf.setColumnWidth(index, pixels); @@ -3237,6 +3692,9 @@ public class Grid<T> extends ResizeComposite implements * <p> * <em>Note:</em> If a negative value was given to * {@link #setWidth(double)}, that same negative value is returned here. + * <p> + * <em>Note:</em> Returns the value, even if the column is currently + * {@link #isHidden() hidden}. * * @return pixel width of the column, or a negative number if the column * width has been automatically calculated. @@ -3251,13 +3709,18 @@ public class Grid<T> extends ResizeComposite implements * Returns the effective pixel width of the column. * <p> * This differs from {@link #getWidth()} only when the column has been - * automatically resized. + * automatically resized, or when the column is currently + * {@link #isHidden() hidden}, when the value is 0. * * @return pixel width of the column. */ public double getWidthActual() { + if (isHidden()) { + return 0; + } return grid.escalator.getColumnConfiguration() - .getColumnWidthActual(grid.columns.indexOf(this)); + .getColumnWidthActual( + grid.getVisibleColumns().indexOf(this)); } void reapplyWidth() { @@ -3294,6 +3757,75 @@ public class Grid<T> extends ResizeComposite implements return sortable; } + /** + * Hides or shows the column. By default columns are visible before + * explicitly hiding them. + * + * @since + * @param hidden + * <code>true</code> to hide the column, <code>false</code> + * 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(); + this.grid.fireEvent(new ColumnVisibilityChangeEvent<T>(this, + hidden, false)); + } + } + + /** + * Is this column hidden. Default is {@code false}. + * + * @since + * @return <code>true</code> if the column is currently hidden, + * <code>false</code> otherwise + */ + public boolean isHidden() { + return hidden; + } + + /** + * Set whether it is possible for the user to hide this column or not. + * Default is {@code false}. + * <p> + * <em>Note:</em> it is still possible to hide the column + * programmatically using {@link #setHidden(boolean)}. + * + * @since + * @param hideable + * <code>true</code> if the user can hide this column, + * <code>false</code> 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}. + * <p> + * <em>Note:</em> the column can be programmatically hidden using + * {@link #setHidden(boolean)} regardless of the returned value. + * + * @since + * @return <code>true</code> if the user can hide the column, + * <code>false</code> if not + */ + public boolean isHideable() { + return hideable; + } + @Override public String toString() { String details = ""; @@ -4239,7 +4771,8 @@ public class Grid<T> extends ResizeComposite implements int columnIndex = columns.indexOf(column); // Remove from column configuration - escalator.getColumnConfiguration().removeColumns(columnIndex, 1); + escalator.getColumnConfiguration().removeColumns( + getVisibleColumns().indexOf(column), 1); updateFrozenColumns(); @@ -4254,6 +4787,8 @@ public class Grid<T> extends ResizeComposite implements /** * Returns the amount of columns in the grid. + * <p> + * <em>NOTE:</em> this includes the hidden columns in the count. * * @return The number of columns in the grid */ @@ -4262,7 +4797,9 @@ public class Grid<T> extends ResizeComposite implements } /** - * Returns a list of columns in the grid. + * Returns a list columns in the grid, including hidden columns. + * <p> + * For currently visible columns, use {@link #getVisibleColumns()}. * * @return A unmodifiable list of the columns in the grid */ @@ -4272,6 +4809,24 @@ public class Grid<T> extends ResizeComposite implements } /** + * Returns a list of the currently visible columns in the grid. + * <p> + * No {@link Column#isHidden() hidden} columns included. + * + * @since + * @return A unmodifiable list of the currently visible columns in the grid + */ + public List<Column<?, T>> getVisibleColumns() { + ArrayList<Column<?, T>> visible = new ArrayList<Column<?, T>>(); + for (Column<?, T> c : columns) { + if (!c.isHidden()) { + visible.add(c); + } + } + return Collections.unmodifiableList(visible); + } + + /** * Returns a column by its index in the grid. * * @param index @@ -4288,17 +4843,6 @@ public class Grid<T> extends ResizeComposite implements } /** - * Returns current index of given column - * - * @param column - * column in grid - * @return column index, or <code>-1</code> if not in this Grid - */ - protected int indexOfColumn(Column<?, T> column) { - return columns.indexOf(column); - } - - /** * Returns the header section of this grid. The default header contains a * single row displaying the column captions. * @@ -4667,8 +5211,12 @@ public class Grid<T> extends ResizeComposite implements if (newSize > oldSize) { body.insertRows(oldSize, newSize - oldSize); + cellFocusHandler.rowsAddedToBody(Range.withLength(oldSize, + newSize - oldSize)); } else if (newSize < oldSize) { body.removeRows(newSize, oldSize - newSize); + cellFocusHandler.rowsRemovedFromBody(Range.withLength( + newSize, oldSize - newSize)); } if (newSize > 0) { @@ -4676,6 +5224,10 @@ public class Grid<T> extends ResizeComposite implements Range visibleRowRange = escalator.getVisibleRowRange(); dataSource.ensureAvailability(visibleRowRange.getStart(), visibleRowRange.length()); + } else { + // We won't expect any data more data updates, so just make + // the bookkeeping happy + dataAvailable(0, 0); } assert body.getRowCount() == newSize; @@ -4735,7 +5287,7 @@ public class Grid<T> extends ResizeComposite implements + getColumnCount() + ")"); } - this.frozenColumnCount = numberOfColumns; + frozenColumnCount = numberOfColumns; updateFrozenColumns(); } @@ -4877,6 +5429,17 @@ public class Grid<T> extends ResizeComposite implements } /** + * 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 * * @return the number of pixels this grid is scrolled to the right @@ -4885,6 +5448,26 @@ public class Grid<T> 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()); } @@ -5035,6 +5618,10 @@ public class Grid<T> extends ResizeComposite implements if (!isElementInChildWidget(e)) { + if (handleHeaderCellDragStartEvent(event, container)) { + return; + } + // Sorting through header Click / KeyUp if (handleHeaderDefaultRowEvent(event, container)) { return; @@ -5196,6 +5783,31 @@ public class Grid<T> extends ResizeComposite implements return true; } + private boolean handleHeaderCellDragStartEvent(Event event, + RowContainer container) { + if (!isColumnReorderingAllowed()) { + return false; + } + if (container != escalator.getHeader()) { + return false; + } + if (eventCell.getColumnIndex() < escalator.getColumnConfiguration() + .getFrozenColumnCount()) { + return false; + } + + if (event.getTypeInt() == Event.ONMOUSEDOWN + && event.getButton() == NativeEvent.BUTTON_LEFT + || event.getTypeInt() == Event.ONTOUCHSTART) { + dndHandler.onDragStartOnDraggableElement(event, + headerCellDndCallback); + event.preventDefault(); + event.stopPropagation(); + return true; + } + return false; + } + private Point rowEventTouchStartingPoint; private CellStyleGenerator<T> cellStyleGenerator; private RowStyleGenerator<T> rowStyleGenerator; @@ -5878,6 +6490,34 @@ public class Grid<T> extends ResizeComposite implements } /** + * Register a column reorder handler to this Grid. The event for this + * handler is fired when the Grid's columns are reordered. + * + * @since + * @param handler + * the handler for the event + * @return the registration for the event + */ + public HandlerRegistration addColumnReorderHandler( + ColumnReorderHandler<T> handler) { + return addHandler(handler, ColumnReorderEvent.getType()); + } + + /** + * Register a column visibility change handler to this Grid. The event for + * this handler is fired when the Grid's columns change visibility. + * + * @since + * @param handler + * the handler for the event + * @return the registration for the event + */ + public HandlerRegistration addColumnVisibilityChangeHandler( + ColumnVisibilityChangeHandler<T> handler) { + return addHandler(handler, ColumnVisibilityChangeEvent.getType()); + } + + /** * Apply sorting to data source. */ private void sort(boolean userOriginated) { @@ -5929,6 +6569,27 @@ public class Grid<T> extends ResizeComposite implements } /** + * Returns whether columns can be reordered with drag and drop. + * + * @since + * @return <code>true</code> if columns can be reordered, false otherwise + */ + public boolean isColumnReorderingAllowed() { + return columnReorderingAllowed; + } + + /** + * Sets whether column reordering with drag and drop is allowed or not. + * + * @since + * @param columnReorderingAllowed + * specifies whether column reordering is allowed + */ + public void setColumnReorderingAllowed(boolean columnReorderingAllowed) { + this.columnReorderingAllowed = columnReorderingAllowed; + } + + /** * Sets a new column order for the grid. All columns which are not ordered * here will remain in the order they were before as the last columns of * grid. @@ -5979,6 +6640,8 @@ public class Grid<T> extends ResizeComposite implements for (FooterRow row : footer.getRows()) { row.calculateColspans(); } + + fireEvent(new ColumnReorderEvent<T>()); } /** |