com.vaadin.v7.client.widget.grid.selection.MultiSelectionRenderer Maven / Gradle / Ivy
Show all versions of vaadin-compatibility-client Show documentation
/*
* Copyright (C) 2000-2024 Vaadin Ltd
*
* This program is available under Vaadin Commercial License and Service Terms.
*
* See for the full
* license.
*/
package com.vaadin.v7.client.widget.grid.selection;
import java.util.Collection;
import java.util.HashSet;
import com.google.gwt.animation.client.AnimationScheduler;
import com.google.gwt.animation.client.AnimationScheduler.AnimationCallback;
import com.google.gwt.animation.client.AnimationScheduler.AnimationHandle;
import com.google.gwt.core.client.GWT;
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.TableRowElement;
import com.google.gwt.dom.client.TableSectionElement;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.event.dom.client.MouseDownEvent;
import com.google.gwt.event.dom.client.MouseDownHandler;
import com.google.gwt.event.dom.client.TouchStartEvent;
import com.google.gwt.event.dom.client.TouchStartHandler;
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.CheckBox;
import com.vaadin.client.WidgetUtil;
import com.vaadin.v7.client.renderers.ClickableRenderer;
import com.vaadin.v7.client.widget.grid.CellReference;
import com.vaadin.v7.client.widget.grid.RendererCellReference;
import com.vaadin.v7.client.widget.grid.events.GridEnabledEvent;
import com.vaadin.v7.client.widget.grid.events.GridEnabledHandler;
import com.vaadin.v7.client.widget.grid.selection.SelectionModel.Multi.Batched;
import com.vaadin.v7.client.widgets.Escalator.AbstractRowContainer;
import com.vaadin.v7.client.widgets.Grid;
/**
* Renderer showing multi selection check boxes.
*
* @author Vaadin Ltd
* @param
* the type of the associated grid
* @since 7.4
*/
public class MultiSelectionRenderer
extends ClickableRenderer {
private static final String SELECTION_CHECKBOX_CLASSNAME = "-selection-checkbox";
/** The size of the autoscroll area, both top and bottom. */
private static final int SCROLL_AREA_GRADIENT_PX = 100;
/** 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;
/**
* Handler for MouseDown and TouchStart events for selection checkboxes.
*
* @since 7.5
*/
private final class CheckBoxEventHandler implements MouseDownHandler,
TouchStartHandler, ClickHandler, GridEnabledHandler {
private final CheckBox checkBox;
/**
* @param checkBox
* checkbox widget for this handler
*/
private CheckBoxEventHandler(CheckBox checkBox) {
this.checkBox = checkBox;
}
@Override
public void onMouseDown(MouseDownEvent event) {
if (checkBox.isEnabled()) {
if (event.getNativeButton() == NativeEvent.BUTTON_LEFT) {
startDragSelect(event.getNativeEvent(),
checkBox.getElement());
}
}
}
@Override
public void onTouchStart(TouchStartEvent event) {
if (checkBox.isEnabled()) {
startDragSelect(event.getNativeEvent(), checkBox.getElement());
}
}
@Override
public void onClick(ClickEvent event) {
// Clicking is already handled with MultiSelectionRenderer
event.preventDefault();
event.stopPropagation();
}
@Override
public void onEnabled(boolean enabled) {
checkBox.setEnabled(enabled);
}
}
/**
* 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) {
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 the
* event handler in MultiSelectionRenderer.onBrowserEvent.
* Therefore, we can simply kill everything and let that
* method restart things as they should.
*/
autoScrollHandler.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:
/*
* Remember: targetElement is always where touchstart started,
* not where the finger is pointing currently.
*/
final Element targetElement = Element
.as(event.getNativeEvent().getEventTarget());
if (isInFirstColumn(targetElement)) {
removeNativeHandler();
event.cancel();
}
break;
}
}
private boolean isInFirstColumn(final Element element) {
if (element == null) {
return false;
}
final Element tbody = getTbodyElement();
if (tbody == null || !tbody.isOrHasChild(element)) {
return false;
}
/*
* The null-parent in the while clause is in the case where element
* is an immediate tr child in the tbody. Should never happen in
* internal code, but hey...
*/
Element cursor = element;
while (cursor.getParentElement() != null
&& cursor.getParentElement().getParentElement() != tbody) {
cursor = cursor.getParentElement();
}
final Element tr = cursor.getParentElement();
return tr.getFirstChildElement().equals(cursor);
}
}
/**
* This class's responsibility is to
*
* - scroll the table while a pointer is kept in a scrolling zone and
*
- select rows whenever a pointer is "activated" on a selection cell
*
*
* Techical note: This class is an AnimationCallback because we
* need a timer: when the finger is kept in place while the grid scrolls, we
* still need to be able to make new selections. So, instead of relying on
* events (which won't be fired, since the pointer isn't necessarily
* moving), we do this check on each frame while the pointer is "active"
* (mouse is pressed, finger is on screen).
*/
private class AutoScrollerAndSelector 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-coordinate on the {@link Event#getClientY() client} from
* where we need to start scrolling towards the top.
*/
private int topBound = -1;
/**
* The highest y-coordinate on the {@link Event#getClientY() client}
* from where we need to scrolling towards the bottom.
*/
private int bottomBound = -1;
/**
* true
if the pointer is selecting, false
if
* the pointer is deselecting.
*/
private final boolean selectionPaint;
/**
* 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, positive is towards the bottom).
*/
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 pageX coordinate of the first click. */
private int initialPageX = -1;
/** The pointer's pageY coordinate. */
private int pageY;
/** The logical index of the row that was most recently modified. */
private int lastModifiedLogicalRow = -1;
/** @see #doScrollAreaChecks(int) */
private int finalTopBound;
/** @see #doScrollAreaChecks(int) */
private int finalBottomBound;
private boolean scrollAreaShouldRebound = false;
private final int bodyAbsoluteTop;
private final int bodyAbsoluteBottom;
public AutoScrollerAndSelector(final int topBound,
final int bottomBound, final int gradientArea,
final boolean selectionPaint) {
finalTopBound = topBound;
finalBottomBound = bottomBound;
this.gradientArea = gradientArea;
this.selectionPaint = selectionPaint;
bodyAbsoluteTop = getBodyClientTop();
bodyAbsoluteBottom = getBodyClientBottom();
}
@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) {
grid.setScrollTop(grid.getScrollTop() + intPixelsToScroll);
}
int constrainedPageY = Math.max(bodyAbsoluteTop,
Math.min(bodyAbsoluteBottom, pageY));
int logicalRow = getLogicalRowIndex(grid, WidgetUtil
.getElementFromPoint(initialPageX, constrainedPageY));
int incrementOrDecrement = (logicalRow > lastModifiedLogicalRow) ? 1
: -1;
/*
* Both pageY and initialPageX have their initialized (and
* unupdated) values while the cursor hasn't moved since the first
* invocation. This will lead to logicalRow being -1, until the
* pointer has been moved.
*/
while (logicalRow != -1 && lastModifiedLogicalRow != logicalRow) {
lastModifiedLogicalRow += incrementOrDecrement;
setSelected(lastModifiedLogicalRow, selectionPaint);
}
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 (topBound < finalTopBound) {
topBound += reboundPx;
topBound = Math.min(topBound, finalTopBound);
updateScrollSpeed(pageY);
} else if (bottomBound > finalBottomBound) {
bottomBound -= reboundPx;
bottomBound = Math.max(bottomBound, finalBottomBound);
updateScrollSpeed(pageY);
}
}
private void updateScrollSpeed(final int pointerPageY) {
final double ratio;
if (pointerPageY < topBound) {
final double distance = pointerPageY - topBound;
ratio = Math.max(-1, distance / gradientArea);
} else if (pointerPageY > bottomBound) {
final double distance = pointerPageY - bottomBound;
ratio = Math.min(1, distance / gradientArea);
} else {
ratio = 0;
}
scrollSpeed = ratio * SCROLL_TOP_SPEED_PX_SEC;
}
public void start(int logicalRowIndex) {
running = true;
setSelected(logicalRowIndex, selectionPaint);
lastModifiedLogicalRow = logicalRowIndex;
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) {
doScrollAreaChecks(pageY);
updateScrollSpeed(pageY);
this.pageY = pageY;
if (initialPageX == -1) {
initialPageX = pageX;
}
}
/**
* This method checks whether the first pointer event started in an area
* that would start scrolling immediately, and does some actions
* accordingly.
*
* If it is, that scroll area will be offset "beyond" the pointer (above
* if pointer is towards the top, otherwise below).
*
* *) This behavior will change in
* future patches (henrik paul 2.7.2014)
*/
private void doScrollAreaChecks(int pageY) {
/*
* The first run makes sure that neither scroll position is
* underneath the finger, but offset to either direction from
* underneath the pointer.
*/
if (topBound == -1) {
topBound = Math.min(finalTopBound, pageY);
bottomBound = Math.max(finalBottomBound, pageY);
} else {
/*
* Subsequent runs make sure that the scroll area grows (but
* doesn't shrink) with the finger, but no further than the
* final bound.
*/
int oldTopBound = topBound;
if (topBound < finalTopBound) {
topBound = Math.max(topBound,
Math.min(finalTopBound, pageY));
}
int oldBottomBound = bottomBound;
if (bottomBound > finalBottomBound) {
bottomBound = Math.min(bottomBound,
Math.max(finalBottomBound, pageY));
}
final boolean topDidNotMove = oldTopBound == topBound;
final boolean bottomDidNotMove = oldBottomBound == bottomBound;
final boolean wasVerticalMovement = pageY != this.pageY;
scrollAreaShouldRebound = (topDidNotMove && bottomDidNotMove
&& wasVerticalMovement);
}
}
}
/**
* This class makes sure that pointer movemenets are registered and
* delegated to the autoscroller so that it can:
*
* - modify the speed in which we autoscroll.
*
- "paint" a new row with the selection.
*
* Essentially, when a pointer is pressed on the selection column, 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 class AutoScrollHandler {
private AutoScrollerAndSelector autoScroller;
/** The registration info for {@link #scrollPreviewHandler} */
private HandlerRegistration handlerRegistration;
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 top bound, as calculated from the {@link Event#getClientY()
* client} coordinates.
*/
private int topBound = -1;
/**
* The bottom bound, as calculated from the {@link Event#getClientY()
* client} coordinates.
*/
private int bottomBound = -1;
/** The size of the autoscroll acceleration area. */
private int gradientArea;
public void start(int logicalRowIndex) {
SelectionModel model = grid.getSelectionModel();
if (model instanceof Batched) {
Batched> batchedModel = (Batched>) model;
batchedModel.startBatchSelect();
}
/*
* 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 AutoScrollerAndSelector(topBound, bottomBound,
gradientArea, !isSelected(logicalRowIndex));
autoScroller.start(logicalRowIndex);
}
private void updateScrollBounds() {
final int topBorder = getBodyClientTop();
final int bottomBorder = getBodyClientBottom();
topBound = topBorder + SCROLL_AREA_GRADIENT_PX;
bottomBound = bottomBorder - SCROLL_AREA_GRADIENT_PX;
gradientArea = SCROLL_AREA_GRADIENT_PX;
// modify bounds if they're too tightly packed
if (bottomBound - topBound < MIN_NO_AUTOSCROLL_AREA_PX) {
int adjustment = MIN_NO_AUTOSCROLL_AREA_PX
- (bottomBound - topBound);
topBound -= adjustment / 2;
bottomBound += adjustment / 2;
gradientArea -= adjustment / 2;
}
}
public void stop() {
if (handlerRegistration != null) {
handlerRegistration.removeHandler();
handlerRegistration = null;
}
if (autoScroller != null) {
autoScroller.stop();
autoScroller = null;
}
SelectionModel model = grid.getSelectionModel();
if (model instanceof Batched) {
Batched> batchedModel = (Batched>) model;
batchedModel.commitBatchSelect();
}
removeNativeHandler();
}
}
private final Grid grid;
private HandlerRegistration nativePreviewHandlerRegistration;
private final AutoScrollHandler autoScrollHandler = new AutoScrollHandler();
public MultiSelectionRenderer(final Grid grid) {
this.grid = grid;
}
@Override
public void destroy() {
if (nativePreviewHandlerRegistration != null) {
removeNativeHandler();
}
}
@Override
public CheckBox createWidget() {
final CheckBox checkBox = GWT.create(CheckBox.class);
checkBox.setStylePrimaryName(
grid.getStylePrimaryName() + SELECTION_CHECKBOX_CLASSNAME);
CheckBoxEventHandler handler = new CheckBoxEventHandler(checkBox);
// Sink events
checkBox.sinkBitlessEvent(BrowserEvents.MOUSEDOWN);
checkBox.sinkBitlessEvent(BrowserEvents.TOUCHSTART);
checkBox.sinkBitlessEvent(BrowserEvents.CLICK);
// Add handlers
checkBox.addMouseDownHandler(handler);
checkBox.addTouchStartHandler(handler);
checkBox.addClickHandler(handler);
grid.addHandler(handler, GridEnabledEvent.TYPE);
checkBox.setEnabled(grid.isEnabled());
return checkBox;
}
@Override
public void render(final RendererCellReference cell, final Boolean data,
CheckBox checkBox) {
checkBox.setValue(data, false);
checkBox.setEnabled(grid.isEnabled() && !grid.isEditorActive()
&& grid.isUserSelectionAllowed());
}
@Override
public Collection getConsumedEvents() {
final HashSet events = new HashSet();
/*
* this column's first interest is only to attach a NativePreventHandler
* that does all the magic. These events are the beginning of that
* cycle.
*/
events.add(BrowserEvents.MOUSEDOWN);
events.add(BrowserEvents.TOUCHSTART);
return events;
}
@Override
public boolean onBrowserEvent(final CellReference> cell,
final NativeEvent event) {
if (BrowserEvents.TOUCHSTART.equals(event.getType())
|| (BrowserEvents.MOUSEDOWN.equals(event.getType())
&& event.getButton() == NativeEvent.BUTTON_LEFT)) {
startDragSelect(event, Element.as(event.getEventTarget()));
return true;
}
return false;
}
private void startDragSelect(NativeEvent event, final Element target) {
injectNativeHandler();
int logicalRowIndex = getLogicalRowIndex(grid, target);
autoScrollHandler.start(logicalRowIndex);
event.preventDefault();
event.stopPropagation();
}
private void injectNativeHandler() {
removeNativeHandler();
nativePreviewHandlerRegistration = Event
.addNativePreviewHandler(new TouchEventHandler());
}
private void removeNativeHandler() {
if (nativePreviewHandlerRegistration != null) {
nativePreviewHandlerRegistration.removeHandler();
nativePreviewHandlerRegistration = null;
}
}
private int getLogicalRowIndex(Grid grid, final Element target) {
if (target == null) {
return -1;
}
/*
* We can't simply go backwards until we find a first element,
* because of the table-in-table scenario. We need to, unfortunately, go
* up from our known root.
*/
final Element tbody = getTbodyElement();
Element tr = tbody.getFirstChildElement();
while (tr != null) {
if (tr.isOrHasChild(target)) {
final Element td = tr.getFirstChildElement();
assert td != null : "Cell has disappeared";
final Element checkbox = td.getFirstChildElement();
assert checkbox != null : "Checkbox has disappeared";
return ((AbstractRowContainer) grid.getEscalator().getBody())
.getLogicalRowIndex((TableRowElement) tr);
}
tr = tr.getNextSiblingElement();
}
return -1;
}
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. */
private int getClientTop(final Element e) {
return e.getAbsoluteTop();
}
private int getBodyClientBottom() {
return getClientTop(getTfootElement()) - 1;
}
private int getBodyClientTop() {
// Off by one pixel miscalculation. possibly border related.
return getClientTop(grid.getElement())
+ getTheadElement().getOffsetHeight() + 1;
}
protected boolean isSelected(final int logicalRow) {
return grid.isSelected(grid.getDataSource().getRow(logicalRow));
}
protected void setSelected(final int logicalRow, final boolean select) {
if (!grid.isUserSelectionAllowed()) {
return;
}
T row = grid.getDataSource().getRow(logicalRow);
if (select) {
grid.select(row);
} else {
grid.deselect(row);
}
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy