2 * Copyright 2000-2018 Vaadin Ltd.
4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
5 * use this file except in compliance with the License. You may obtain a copy of
8 * http://www.apache.org/licenses/LICENSE-2.0
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13 * License for the specific language governing permissions and limitations under
16 package com.vaadin.v7.client.widget.grid.selection;
18 import java.util.Collection;
19 import java.util.HashSet;
21 import com.google.gwt.animation.client.AnimationScheduler;
22 import com.google.gwt.animation.client.AnimationScheduler.AnimationCallback;
23 import com.google.gwt.animation.client.AnimationScheduler.AnimationHandle;
24 import com.google.gwt.core.client.GWT;
25 import com.google.gwt.dom.client.BrowserEvents;
26 import com.google.gwt.dom.client.Element;
27 import com.google.gwt.dom.client.NativeEvent;
28 import com.google.gwt.dom.client.TableElement;
29 import com.google.gwt.dom.client.TableRowElement;
30 import com.google.gwt.dom.client.TableSectionElement;
31 import com.google.gwt.event.dom.client.ClickEvent;
32 import com.google.gwt.event.dom.client.ClickHandler;
33 import com.google.gwt.event.dom.client.MouseDownEvent;
34 import com.google.gwt.event.dom.client.MouseDownHandler;
35 import com.google.gwt.event.dom.client.TouchStartEvent;
36 import com.google.gwt.event.dom.client.TouchStartHandler;
37 import com.google.gwt.event.shared.HandlerRegistration;
38 import com.google.gwt.user.client.Event;
39 import com.google.gwt.user.client.Event.NativePreviewEvent;
40 import com.google.gwt.user.client.Event.NativePreviewHandler;
41 import com.google.gwt.user.client.ui.CheckBox;
42 import com.vaadin.client.WidgetUtil;
43 import com.vaadin.v7.client.renderers.ClickableRenderer;
44 import com.vaadin.v7.client.widget.grid.CellReference;
45 import com.vaadin.v7.client.widget.grid.RendererCellReference;
46 import com.vaadin.v7.client.widget.grid.events.GridEnabledEvent;
47 import com.vaadin.v7.client.widget.grid.events.GridEnabledHandler;
48 import com.vaadin.v7.client.widget.grid.selection.SelectionModel.Multi.Batched;
49 import com.vaadin.v7.client.widgets.Escalator.AbstractRowContainer;
50 import com.vaadin.v7.client.widgets.Grid;
53 * Renderer showing multi selection check boxes.
57 * the type of the associated grid
60 public class MultiSelectionRenderer<T>
61 extends ClickableRenderer<Boolean, CheckBox> {
63 private static final String SELECTION_CHECKBOX_CLASSNAME = "-selection-checkbox";
65 /** The size of the autoscroll area, both top and bottom. */
66 private static final int SCROLL_AREA_GRADIENT_PX = 100;
68 /** The maximum number of pixels per second to autoscroll. */
69 private static final int SCROLL_TOP_SPEED_PX_SEC = 500;
72 * The minimum area where the grid doesn't scroll while the pointer is
75 private static final int MIN_NO_AUTOSCROLL_AREA_PX = 50;
78 * Handler for MouseDown and TouchStart events for selection checkboxes.
82 private final class CheckBoxEventHandler implements MouseDownHandler,
83 TouchStartHandler, ClickHandler, GridEnabledHandler {
84 private final CheckBox checkBox;
88 * checkbox widget for this handler
90 private CheckBoxEventHandler(CheckBox checkBox) {
91 this.checkBox = checkBox;
95 public void onMouseDown(MouseDownEvent event) {
96 if (checkBox.isEnabled()) {
97 if (event.getNativeButton() == NativeEvent.BUTTON_LEFT) {
98 startDragSelect(event.getNativeEvent(),
99 checkBox.getElement());
105 public void onTouchStart(TouchStartEvent event) {
106 if (checkBox.isEnabled()) {
107 startDragSelect(event.getNativeEvent(), checkBox.getElement());
112 public void onClick(ClickEvent event) {
113 // Clicking is already handled with MultiSelectionRenderer
114 event.preventDefault();
115 event.stopPropagation();
119 public void onEnabled(boolean enabled) {
120 checkBox.setEnabled(enabled);
125 * This class's main objective is to listen when to stop autoscrolling, and
126 * make sure everything stops accordingly.
128 private class TouchEventHandler implements NativePreviewHandler {
130 public void onPreviewNativeEvent(final NativePreviewEvent event) {
131 switch (event.getTypeInt()) {
132 case Event.ONTOUCHSTART: {
133 if (event.getNativeEvent().getTouches().length() == 1) {
135 * Something has dropped a touchend/touchcancel and the
136 * scroller is most probably running amok. Let's cancel it
137 * and pretend that everything's going as expected
139 * Because this is a preview, this code is run before the
140 * event handler in MultiSelectionRenderer.onBrowserEvent.
141 * Therefore, we can simply kill everything and let that
142 * method restart things as they should.
144 autoScrollHandler.stop();
147 * Related TODO: investigate why iOS seems to ignore a
148 * touchend/touchcancel when frames are dropped, and/or if
149 * something can be done about that.
155 case Event.ONTOUCHMOVE:
159 case Event.ONTOUCHEND:
160 case Event.ONTOUCHCANCEL:
162 * Remember: targetElement is always where touchstart started,
163 * not where the finger is pointing currently.
165 final Element targetElement = Element
166 .as(event.getNativeEvent().getEventTarget());
167 if (isInFirstColumn(targetElement)) {
168 removeNativeHandler();
175 private boolean isInFirstColumn(final Element element) {
176 if (element == null) {
179 final Element tbody = getTbodyElement();
181 if (tbody == null || !tbody.isOrHasChild(element)) {
186 * The null-parent in the while clause is in the case where element
187 * is an immediate tr child in the tbody. Should never happen in
188 * internal code, but hey...
190 Element cursor = element;
191 while (cursor.getParentElement() != null
192 && cursor.getParentElement().getParentElement() != tbody) {
193 cursor = cursor.getParentElement();
196 final Element tr = cursor.getParentElement();
197 return tr.getFirstChildElement().equals(cursor);
202 * This class's responsibility is to
204 * <li>scroll the table while a pointer is kept in a scrolling zone and
205 * <li>select rows whenever a pointer is "activated" on a selection cell
208 * <em>Techical note:</em> This class is an AnimationCallback because we
209 * need a timer: when the finger is kept in place while the grid scrolls, we
210 * still need to be able to make new selections. So, instead of relying on
211 * events (which won't be fired, since the pointer isn't necessarily
212 * moving), we do this check on each frame while the pointer is "active"
213 * (mouse is pressed, finger is on screen).
215 private class AutoScrollerAndSelector implements AnimationCallback {
218 * If the acceleration gradient area is smaller than this, autoscrolling
219 * will be disabled (it becomes too quick to accelerate to be usable).
221 private static final int GRADIENT_MIN_THRESHOLD_PX = 10;
224 * The speed at which the gradient area recovers, once scrolling in that
225 * direction has started.
227 private static final int SCROLL_AREA_REBOUND_PX_PER_SEC = 1;
228 private static final double SCROLL_AREA_REBOUND_PX_PER_MS = SCROLL_AREA_REBOUND_PX_PER_SEC
232 * The lowest y-coordinate on the {@link Event#getClientY() client} from
233 * where we need to start scrolling towards the top.
235 private int topBound = -1;
238 * The highest y-coordinate on the {@link Event#getClientY() client}
239 * from where we need to scrolling towards the bottom.
241 private int bottomBound = -1;
244 * <code>true</code> if the pointer is selecting, <code>false</code> if
245 * the pointer is deselecting.
247 private final boolean selectionPaint;
250 * The area where the selection acceleration takes place. If <
251 * {@link #GRADIENT_MIN_THRESHOLD_PX}, autoscrolling is disabled
253 private final int gradientArea;
256 * The number of pixels per seconds we currently are scrolling (negative
257 * is towards the top, positive is towards the bottom).
259 private double scrollSpeed = 0;
261 private double prevTimestamp = 0;
264 * This field stores fractions of pixels to scroll, to make sure that
265 * we're able to scroll less than one px per frame.
267 private double pixelsToScroll = 0.0d;
269 /** Should this animator be running. */
270 private boolean running = false;
272 /** The handle in which this instance is running. */
273 private AnimationHandle handle;
275 /** The pointer's pageX coordinate of the first click. */
276 private int initialPageX = -1;
278 /** The pointer's pageY coordinate. */
281 /** The logical index of the row that was most recently modified. */
282 private int lastModifiedLogicalRow = -1;
284 /** @see #doScrollAreaChecks(int) */
285 private int finalTopBound;
287 /** @see #doScrollAreaChecks(int) */
288 private int finalBottomBound;
290 private boolean scrollAreaShouldRebound = false;
292 private final int bodyAbsoluteTop;
293 private final int bodyAbsoluteBottom;
295 public AutoScrollerAndSelector(final int topBound,
296 final int bottomBound, final int gradientArea,
297 final boolean selectionPaint) {
298 finalTopBound = topBound;
299 finalBottomBound = bottomBound;
300 this.gradientArea = gradientArea;
301 this.selectionPaint = selectionPaint;
303 bodyAbsoluteTop = getBodyClientTop();
304 bodyAbsoluteBottom = getBodyClientBottom();
308 public void execute(final double timestamp) {
309 final double timeDiff = timestamp - prevTimestamp;
310 prevTimestamp = timestamp;
312 reboundScrollArea(timeDiff);
314 pixelsToScroll += scrollSpeed * (timeDiff / 1000.0d);
315 final int intPixelsToScroll = (int) pixelsToScroll;
316 pixelsToScroll -= intPixelsToScroll;
318 if (intPixelsToScroll != 0) {
319 grid.setScrollTop(grid.getScrollTop() + intPixelsToScroll);
322 int constrainedPageY = Math.max(bodyAbsoluteTop,
323 Math.min(bodyAbsoluteBottom, pageY));
324 int logicalRow = getLogicalRowIndex(grid, WidgetUtil
325 .getElementFromPoint(initialPageX, constrainedPageY));
327 int incrementOrDecrement = (logicalRow > lastModifiedLogicalRow) ? 1
331 * Both pageY and initialPageX have their initialized (and
332 * unupdated) values while the cursor hasn't moved since the first
333 * invocation. This will lead to logicalRow being -1, until the
334 * pointer has been moved.
336 while (logicalRow != -1 && lastModifiedLogicalRow != logicalRow) {
337 lastModifiedLogicalRow += incrementOrDecrement;
338 setSelected(lastModifiedLogicalRow, selectionPaint);
345 * If the scroll are has been offset by the pointer starting out there,
348 private void reboundScrollArea(double timeDiff) {
349 if (!scrollAreaShouldRebound) {
353 int reboundPx = (int) Math
354 .ceil(SCROLL_AREA_REBOUND_PX_PER_MS * timeDiff);
355 if (topBound < finalTopBound) {
356 topBound += reboundPx;
357 topBound = Math.min(topBound, finalTopBound);
358 updateScrollSpeed(pageY);
359 } else if (bottomBound > finalBottomBound) {
360 bottomBound -= reboundPx;
361 bottomBound = Math.max(bottomBound, finalBottomBound);
362 updateScrollSpeed(pageY);
366 private void updateScrollSpeed(final int pointerPageY) {
369 if (pointerPageY < topBound) {
370 final double distance = pointerPageY - topBound;
371 ratio = Math.max(-1, distance / gradientArea);
372 } else if (pointerPageY > bottomBound) {
373 final double distance = pointerPageY - bottomBound;
374 ratio = Math.min(1, distance / gradientArea);
379 scrollSpeed = ratio * SCROLL_TOP_SPEED_PX_SEC;
382 public void start(int logicalRowIndex) {
384 setSelected(logicalRowIndex, selectionPaint);
385 lastModifiedLogicalRow = logicalRowIndex;
392 if (handle != null) {
398 private void reschedule() {
399 if (running && gradientArea >= GRADIENT_MIN_THRESHOLD_PX) {
400 handle = AnimationScheduler.get().requestAnimationFrame(this,
405 public void updatePointerCoords(int pageX, int pageY) {
406 doScrollAreaChecks(pageY);
407 updateScrollSpeed(pageY);
410 if (initialPageX == -1) {
411 initialPageX = pageX;
416 * This method checks whether the first pointer event started in an area
417 * that would start scrolling immediately, and does some actions
420 * If it is, that scroll area will be offset "beyond" the pointer (above
421 * if pointer is towards the top, otherwise below).
423 * <span style="font-size:smaller">*) This behavior will change in
424 * future patches (henrik paul 2.7.2014)</span>
426 private void doScrollAreaChecks(int pageY) {
428 * The first run makes sure that neither scroll position is
429 * underneath the finger, but offset to either direction from
430 * underneath the pointer.
432 if (topBound == -1) {
433 topBound = Math.min(finalTopBound, pageY);
434 bottomBound = Math.max(finalBottomBound, pageY);
437 * Subsequent runs make sure that the scroll area grows (but doesn't
438 * shrink) with the finger, but no further than the final bound.
440 int oldTopBound = topBound;
441 if (topBound < finalTopBound) {
442 topBound = Math.max(topBound,
443 Math.min(finalTopBound, pageY));
446 int oldBottomBound = bottomBound;
447 if (bottomBound > finalBottomBound) {
448 bottomBound = Math.min(bottomBound,
449 Math.max(finalBottomBound, pageY));
452 final boolean topDidNotMove = oldTopBound == topBound;
453 final boolean bottomDidNotMove = oldBottomBound == bottomBound;
454 final boolean wasVerticalMovement = pageY != this.pageY;
455 scrollAreaShouldRebound = (topDidNotMove && bottomDidNotMove
456 && wasVerticalMovement);
462 * This class makes sure that pointer movemenets are registered and
463 * delegated to the autoscroller so that it can:
465 * <li>modify the speed in which we autoscroll.
466 * <li>"paint" a new row with the selection.
468 * Essentially, when a pointer is pressed on the selection column, a native
469 * preview handler is registered (so that selection gestures can happen
470 * outside of the selection column). The handler itself makes sure that it's
471 * detached when the pointer is "lifted".
473 private class AutoScrollHandler {
474 private AutoScrollerAndSelector autoScroller;
476 /** The registration info for {@link #scrollPreviewHandler} */
477 private HandlerRegistration handlerRegistration;
479 private final NativePreviewHandler scrollPreviewHandler = new NativePreviewHandler() {
481 public void onPreviewNativeEvent(final NativePreviewEvent event) {
482 if (autoScroller == null) {
487 final NativeEvent nativeEvent = event.getNativeEvent();
490 switch (event.getTypeInt()) {
491 case Event.ONMOUSEMOVE:
492 case Event.ONTOUCHMOVE:
493 pageY = WidgetUtil.getTouchOrMouseClientY(nativeEvent);
494 pageX = WidgetUtil.getTouchOrMouseClientX(nativeEvent);
495 autoScroller.updatePointerCoords(pageX, pageY);
497 case Event.ONMOUSEUP:
498 case Event.ONTOUCHEND:
499 case Event.ONTOUCHCANCEL:
507 * The top bound, as calculated from the {@link Event#getClientY()
508 * client} coordinates.
510 private int topBound = -1;
513 * The bottom bound, as calculated from the {@link Event#getClientY()
514 * client} coordinates.
516 private int bottomBound = -1;
518 /** The size of the autoscroll acceleration area. */
519 private int gradientArea;
521 public void start(int logicalRowIndex) {
523 SelectionModel<T> model = grid.getSelectionModel();
524 if (model instanceof Batched) {
525 Batched<?> batchedModel = (Batched<?>) model;
526 batchedModel.startBatchSelect();
530 * bounds are updated whenever the autoscroll cycle starts, to make
531 * sure that the widget hasn't changed in size, moved around, or
534 updateScrollBounds();
536 assert handlerRegistration == null : "handlerRegistration was not null";
537 assert autoScroller == null : "autoScroller was not null";
538 handlerRegistration = Event
539 .addNativePreviewHandler(scrollPreviewHandler);
541 autoScroller = new AutoScrollerAndSelector(topBound, bottomBound,
542 gradientArea, !isSelected(logicalRowIndex));
543 autoScroller.start(logicalRowIndex);
546 private void updateScrollBounds() {
547 final int topBorder = getBodyClientTop();
548 final int bottomBorder = getBodyClientBottom();
550 topBound = topBorder + SCROLL_AREA_GRADIENT_PX;
551 bottomBound = bottomBorder - SCROLL_AREA_GRADIENT_PX;
552 gradientArea = SCROLL_AREA_GRADIENT_PX;
554 // modify bounds if they're too tightly packed
555 if (bottomBound - topBound < MIN_NO_AUTOSCROLL_AREA_PX) {
556 int adjustment = MIN_NO_AUTOSCROLL_AREA_PX
557 - (bottomBound - topBound);
558 topBound -= adjustment / 2;
559 bottomBound += adjustment / 2;
560 gradientArea -= adjustment / 2;
565 if (handlerRegistration != null) {
566 handlerRegistration.removeHandler();
567 handlerRegistration = null;
570 if (autoScroller != null) {
575 SelectionModel<T> model = grid.getSelectionModel();
576 if (model instanceof Batched) {
577 Batched<?> batchedModel = (Batched<?>) model;
578 batchedModel.commitBatchSelect();
581 removeNativeHandler();
585 private final Grid<T> grid;
586 private HandlerRegistration nativePreviewHandlerRegistration;
588 private final AutoScrollHandler autoScrollHandler = new AutoScrollHandler();
590 public MultiSelectionRenderer(final Grid<T> grid) {
595 public void destroy() {
596 if (nativePreviewHandlerRegistration != null) {
597 removeNativeHandler();
602 public CheckBox createWidget() {
603 final CheckBox checkBox = GWT.create(CheckBox.class);
604 checkBox.setStylePrimaryName(
605 grid.getStylePrimaryName() + SELECTION_CHECKBOX_CLASSNAME);
607 CheckBoxEventHandler handler = new CheckBoxEventHandler(checkBox);
610 checkBox.sinkBitlessEvent(BrowserEvents.MOUSEDOWN);
611 checkBox.sinkBitlessEvent(BrowserEvents.TOUCHSTART);
612 checkBox.sinkBitlessEvent(BrowserEvents.CLICK);
615 checkBox.addMouseDownHandler(handler);
616 checkBox.addTouchStartHandler(handler);
617 checkBox.addClickHandler(handler);
618 grid.addHandler(handler, GridEnabledEvent.TYPE);
620 checkBox.setEnabled(grid.isEnabled());
626 public void render(final RendererCellReference cell, final Boolean data,
628 checkBox.setValue(data, false);
629 checkBox.setEnabled(grid.isEnabled() && !grid.isEditorActive()
630 && grid.isUserSelectionAllowed());
634 public Collection<String> getConsumedEvents() {
635 final HashSet<String> events = new HashSet<String>();
638 * this column's first interest is only to attach a NativePreventHandler
639 * that does all the magic. These events are the beginning of that
642 events.add(BrowserEvents.MOUSEDOWN);
643 events.add(BrowserEvents.TOUCHSTART);
649 public boolean onBrowserEvent(final CellReference<?> cell,
650 final NativeEvent event) {
651 if (BrowserEvents.TOUCHSTART.equals(event.getType())
652 || (BrowserEvents.MOUSEDOWN.equals(event.getType())
653 && event.getButton() == NativeEvent.BUTTON_LEFT)) {
654 startDragSelect(event, Element.as(event.getEventTarget()));
660 private void startDragSelect(NativeEvent event, final Element target) {
661 injectNativeHandler();
662 int logicalRowIndex = getLogicalRowIndex(grid, target);
663 autoScrollHandler.start(logicalRowIndex);
664 event.preventDefault();
665 event.stopPropagation();
668 private void injectNativeHandler() {
669 removeNativeHandler();
670 nativePreviewHandlerRegistration = Event
671 .addNativePreviewHandler(new TouchEventHandler());
674 private void removeNativeHandler() {
675 if (nativePreviewHandlerRegistration != null) {
676 nativePreviewHandlerRegistration.removeHandler();
677 nativePreviewHandlerRegistration = null;
681 private int getLogicalRowIndex(Grid<T> grid, final Element target) {
682 if (target == null) {
687 * We can't simply go backwards until we find a <tr> first element,
688 * because of the table-in-table scenario. We need to, unfortunately, go
689 * up from our known root.
691 final Element tbody = getTbodyElement();
692 Element tr = tbody.getFirstChildElement();
694 if (tr.isOrHasChild(target)) {
695 final Element td = tr.getFirstChildElement();
696 assert td != null : "Cell has disappeared";
698 final Element checkbox = td.getFirstChildElement();
699 assert checkbox != null : "Checkbox has disappeared";
701 return ((AbstractRowContainer) grid.getEscalator().getBody())
702 .getLogicalRowIndex((TableRowElement) tr);
704 tr = tr.getNextSiblingElement();
709 private TableElement getTableElement() {
710 final Element root = grid.getElement();
711 final Element tablewrapper = Element.as(root.getChild(2));
712 if (tablewrapper != null) {
713 return TableElement.as(tablewrapper.getFirstChildElement());
719 private TableSectionElement getTbodyElement() {
720 TableElement table = getTableElement();
722 return table.getTBodies().getItem(0);
728 private TableSectionElement getTheadElement() {
729 TableElement table = getTableElement();
731 return table.getTHead();
737 private TableSectionElement getTfootElement() {
738 TableElement table = getTableElement();
740 return table.getTFoot();
746 /** Get the "top" of an element in relation to "client" coordinates. */
747 private int getClientTop(final Element e) {
748 return e.getAbsoluteTop();
751 private int getBodyClientBottom() {
752 return getClientTop(getTfootElement()) - 1;
755 private int getBodyClientTop() {
756 // Off by one pixel miscalculation. possibly border related.
757 return getClientTop(grid.getElement())
758 + getTheadElement().getOffsetHeight() + 1;
761 protected boolean isSelected(final int logicalRow) {
762 return grid.isSelected(grid.getDataSource().getRow(logicalRow));
765 protected void setSelected(final int logicalRow, final boolean select) {
766 if (!grid.isUserSelectionAllowed()) {
770 T row = grid.getDataSource().getRow(logicalRow);