]> source.dussan.org Git - vaadin-framework.git/blob
cae999c7f16d9d3b4b440b4b5d3db9c1d28d980b
[vaadin-framework.git] /
1 /*
2  * Copyright 2000-2018 Vaadin Ltd.
3  *
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
6  * the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
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
14  * the License.
15  */
16 package com.vaadin.v7.client.widget.grid.selection;
17
18 import java.util.Collection;
19 import java.util.HashSet;
20
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;
51
52 /**
53  * Renderer showing multi selection check boxes.
54  *
55  * @author Vaadin Ltd
56  * @param <T>
57  *            the type of the associated grid
58  * @since 7.4
59  */
60 public class MultiSelectionRenderer<T>
61         extends ClickableRenderer<Boolean, CheckBox> {
62
63     private static final String SELECTION_CHECKBOX_CLASSNAME = "-selection-checkbox";
64
65     /** The size of the autoscroll area, both top and bottom. */
66     private static final int SCROLL_AREA_GRADIENT_PX = 100;
67
68     /** The maximum number of pixels per second to autoscroll. */
69     private static final int SCROLL_TOP_SPEED_PX_SEC = 500;
70
71     /**
72      * The minimum area where the grid doesn't scroll while the pointer is
73      * pressed.
74      */
75     private static final int MIN_NO_AUTOSCROLL_AREA_PX = 50;
76
77     /**
78      * Handler for MouseDown and TouchStart events for selection checkboxes.
79      *
80      * @since 7.5
81      */
82     private final class CheckBoxEventHandler implements MouseDownHandler,
83             TouchStartHandler, ClickHandler, GridEnabledHandler {
84         private final CheckBox checkBox;
85
86         /**
87          * @param checkBox
88          *            checkbox widget for this handler
89          */
90         private CheckBoxEventHandler(CheckBox checkBox) {
91             this.checkBox = checkBox;
92         }
93
94         @Override
95         public void onMouseDown(MouseDownEvent event) {
96             if (checkBox.isEnabled()) {
97                 if (event.getNativeButton() == NativeEvent.BUTTON_LEFT) {
98                     startDragSelect(event.getNativeEvent(),
99                             checkBox.getElement());
100                 }
101             }
102         }
103
104         @Override
105         public void onTouchStart(TouchStartEvent event) {
106             if (checkBox.isEnabled()) {
107                 startDragSelect(event.getNativeEvent(), checkBox.getElement());
108             }
109         }
110
111         @Override
112         public void onClick(ClickEvent event) {
113             // Clicking is already handled with MultiSelectionRenderer
114             event.preventDefault();
115             event.stopPropagation();
116         }
117
118         @Override
119         public void onEnabled(boolean enabled) {
120             checkBox.setEnabled(enabled);
121         }
122     }
123
124     /**
125      * This class's main objective is to listen when to stop autoscrolling, and
126      * make sure everything stops accordingly.
127      */
128     private class TouchEventHandler implements NativePreviewHandler {
129         @Override
130         public void onPreviewNativeEvent(final NativePreviewEvent event) {
131             switch (event.getTypeInt()) {
132             case Event.ONTOUCHSTART: {
133                 if (event.getNativeEvent().getTouches().length() == 1) {
134                     /*
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
138                      *
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.
143                      */
144                     autoScrollHandler.stop();
145
146                     /*
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.
150                      */
151                 }
152                 break;
153             }
154
155             case Event.ONTOUCHMOVE:
156                 event.cancel();
157                 break;
158
159             case Event.ONTOUCHEND:
160             case Event.ONTOUCHCANCEL:
161                 /*
162                  * Remember: targetElement is always where touchstart started,
163                  * not where the finger is pointing currently.
164                  */
165                 final Element targetElement = Element
166                         .as(event.getNativeEvent().getEventTarget());
167                 if (isInFirstColumn(targetElement)) {
168                     removeNativeHandler();
169                     event.cancel();
170                 }
171                 break;
172             }
173         }
174
175         private boolean isInFirstColumn(final Element element) {
176             if (element == null) {
177                 return false;
178             }
179             final Element tbody = getTbodyElement();
180
181             if (tbody == null || !tbody.isOrHasChild(element)) {
182                 return false;
183             }
184
185             /*
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...
189              */
190             Element cursor = element;
191             while (cursor.getParentElement() != null
192                     && cursor.getParentElement().getParentElement() != tbody) {
193                 cursor = cursor.getParentElement();
194             }
195
196             final Element tr = cursor.getParentElement();
197             return tr.getFirstChildElement().equals(cursor);
198         }
199     }
200
201     /**
202      * This class's responsibility is to
203      * <ul>
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
206      * </ul>
207      * <p>
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).
214      */
215     private class AutoScrollerAndSelector implements AnimationCallback {
216
217         /**
218          * If the acceleration gradient area is smaller than this, autoscrolling
219          * will be disabled (it becomes too quick to accelerate to be usable).
220          */
221         private static final int GRADIENT_MIN_THRESHOLD_PX = 10;
222
223         /**
224          * The speed at which the gradient area recovers, once scrolling in that
225          * direction has started.
226          */
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
229                 / 1000.0d;
230
231         /**
232          * The lowest y-coordinate on the {@link Event#getClientY() client} from
233          * where we need to start scrolling towards the top.
234          */
235         private int topBound = -1;
236
237         /**
238          * The highest y-coordinate on the {@link Event#getClientY() client}
239          * from where we need to scrolling towards the bottom.
240          */
241         private int bottomBound = -1;
242
243         /**
244          * <code>true</code> if the pointer is selecting, <code>false</code> if
245          * the pointer is deselecting.
246          */
247         private final boolean selectionPaint;
248
249         /**
250          * The area where the selection acceleration takes place. If &lt;
251          * {@link #GRADIENT_MIN_THRESHOLD_PX}, autoscrolling is disabled
252          */
253         private final int gradientArea;
254
255         /**
256          * The number of pixels per seconds we currently are scrolling (negative
257          * is towards the top, positive is towards the bottom).
258          */
259         private double scrollSpeed = 0;
260
261         private double prevTimestamp = 0;
262
263         /**
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.
266          */
267         private double pixelsToScroll = 0.0d;
268
269         /** Should this animator be running. */
270         private boolean running = false;
271
272         /** The handle in which this instance is running. */
273         private AnimationHandle handle;
274
275         /** The pointer's pageX coordinate of the first click. */
276         private int initialPageX = -1;
277
278         /** The pointer's pageY coordinate. */
279         private int pageY;
280
281         /** The logical index of the row that was most recently modified. */
282         private int lastModifiedLogicalRow = -1;
283
284         /** @see #doScrollAreaChecks(int) */
285         private int finalTopBound;
286
287         /** @see #doScrollAreaChecks(int) */
288         private int finalBottomBound;
289
290         private boolean scrollAreaShouldRebound = false;
291
292         private final int bodyAbsoluteTop;
293         private final int bodyAbsoluteBottom;
294
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;
302
303             bodyAbsoluteTop = getBodyClientTop();
304             bodyAbsoluteBottom = getBodyClientBottom();
305         }
306
307         @Override
308         public void execute(final double timestamp) {
309             final double timeDiff = timestamp - prevTimestamp;
310             prevTimestamp = timestamp;
311
312             reboundScrollArea(timeDiff);
313
314             pixelsToScroll += scrollSpeed * (timeDiff / 1000.0d);
315             final int intPixelsToScroll = (int) pixelsToScroll;
316             pixelsToScroll -= intPixelsToScroll;
317
318             if (intPixelsToScroll != 0) {
319                 grid.setScrollTop(grid.getScrollTop() + intPixelsToScroll);
320             }
321
322             int constrainedPageY = Math.max(bodyAbsoluteTop,
323                     Math.min(bodyAbsoluteBottom, pageY));
324             int logicalRow = getLogicalRowIndex(grid, WidgetUtil
325                     .getElementFromPoint(initialPageX, constrainedPageY));
326
327             int incrementOrDecrement = (logicalRow > lastModifiedLogicalRow) ? 1
328                     : -1;
329
330             /*
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.
335              */
336             while (logicalRow != -1 && lastModifiedLogicalRow != logicalRow) {
337                 lastModifiedLogicalRow += incrementOrDecrement;
338                 setSelected(lastModifiedLogicalRow, selectionPaint);
339             }
340
341             reschedule();
342         }
343
344         /**
345          * If the scroll are has been offset by the pointer starting out there,
346          * move it back a bit
347          */
348         private void reboundScrollArea(double timeDiff) {
349             if (!scrollAreaShouldRebound) {
350                 return;
351             }
352
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);
363             }
364         }
365
366         private void updateScrollSpeed(final int pointerPageY) {
367
368             final double ratio;
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);
375             } else {
376                 ratio = 0;
377             }
378
379             scrollSpeed = ratio * SCROLL_TOP_SPEED_PX_SEC;
380         }
381
382         public void start(int logicalRowIndex) {
383             running = true;
384             setSelected(logicalRowIndex, selectionPaint);
385             lastModifiedLogicalRow = logicalRowIndex;
386             reschedule();
387         }
388
389         public void stop() {
390             running = false;
391
392             if (handle != null) {
393                 handle.cancel();
394                 handle = null;
395             }
396         }
397
398         private void reschedule() {
399             if (running && gradientArea >= GRADIENT_MIN_THRESHOLD_PX) {
400                 handle = AnimationScheduler.get().requestAnimationFrame(this,
401                         grid.getElement());
402             }
403         }
404
405         public void updatePointerCoords(int pageX, int pageY) {
406             doScrollAreaChecks(pageY);
407             updateScrollSpeed(pageY);
408             this.pageY = pageY;
409
410             if (initialPageX == -1) {
411                 initialPageX = pageX;
412             }
413         }
414
415         /**
416          * This method checks whether the first pointer event started in an area
417          * that would start scrolling immediately, and does some actions
418          * accordingly.
419          * <p>
420          * If it is, that scroll area will be offset "beyond" the pointer (above
421          * if pointer is towards the top, otherwise below).
422          * <p>
423          * <span style="font-size:smaller">*) This behavior will change in
424          * future patches (henrik paul 2.7.2014)</span>
425          */
426         private void doScrollAreaChecks(int pageY) {
427             /*
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.
431              */
432             if (topBound == -1) {
433                 topBound = Math.min(finalTopBound, pageY);
434                 bottomBound = Math.max(finalBottomBound, pageY);
435             } else {
436                 /*
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.
439                  */
440                 int oldTopBound = topBound;
441                 if (topBound < finalTopBound) {
442                     topBound = Math.max(topBound,
443                             Math.min(finalTopBound, pageY));
444                 }
445
446                 int oldBottomBound = bottomBound;
447                 if (bottomBound > finalBottomBound) {
448                     bottomBound = Math.min(bottomBound,
449                             Math.max(finalBottomBound, pageY));
450                 }
451
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);
457             }
458         }
459     }
460
461     /**
462      * This class makes sure that pointer movemenets are registered and
463      * delegated to the autoscroller so that it can:
464      * <ul>
465      * <li>modify the speed in which we autoscroll.
466      * <li>"paint" a new row with the selection.
467      * </ul>
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".
472      */
473     private class AutoScrollHandler {
474         private AutoScrollerAndSelector autoScroller;
475
476         /** The registration info for {@link #scrollPreviewHandler} */
477         private HandlerRegistration handlerRegistration;
478
479         private final NativePreviewHandler scrollPreviewHandler = new NativePreviewHandler() {
480             @Override
481             public void onPreviewNativeEvent(final NativePreviewEvent event) {
482                 if (autoScroller == null) {
483                     stop();
484                     return;
485                 }
486
487                 final NativeEvent nativeEvent = event.getNativeEvent();
488                 int pageY = 0;
489                 int pageX = 0;
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);
496                     break;
497                 case Event.ONMOUSEUP:
498                 case Event.ONTOUCHEND:
499                 case Event.ONTOUCHCANCEL:
500                     stop();
501                     break;
502                 }
503             }
504         };
505
506         /**
507          * The top bound, as calculated from the {@link Event#getClientY()
508          * client} coordinates.
509          */
510         private int topBound = -1;
511
512         /**
513          * The bottom bound, as calculated from the {@link Event#getClientY()
514          * client} coordinates.
515          */
516         private int bottomBound = -1;
517
518         /** The size of the autoscroll acceleration area. */
519         private int gradientArea;
520
521         public void start(int logicalRowIndex) {
522
523             SelectionModel<T> model = grid.getSelectionModel();
524             if (model instanceof Batched) {
525                 Batched<?> batchedModel = (Batched<?>) model;
526                 batchedModel.startBatchSelect();
527             }
528
529             /*
530              * bounds are updated whenever the autoscroll cycle starts, to make
531              * sure that the widget hasn't changed in size, moved around, or
532              * whatnot.
533              */
534             updateScrollBounds();
535
536             assert handlerRegistration == null : "handlerRegistration was not null";
537             assert autoScroller == null : "autoScroller was not null";
538             handlerRegistration = Event
539                     .addNativePreviewHandler(scrollPreviewHandler);
540
541             autoScroller = new AutoScrollerAndSelector(topBound, bottomBound,
542                     gradientArea, !isSelected(logicalRowIndex));
543             autoScroller.start(logicalRowIndex);
544         }
545
546         private void updateScrollBounds() {
547             final int topBorder = getBodyClientTop();
548             final int bottomBorder = getBodyClientBottom();
549
550             topBound = topBorder + SCROLL_AREA_GRADIENT_PX;
551             bottomBound = bottomBorder - SCROLL_AREA_GRADIENT_PX;
552             gradientArea = SCROLL_AREA_GRADIENT_PX;
553
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;
561             }
562         }
563
564         public void stop() {
565             if (handlerRegistration != null) {
566                 handlerRegistration.removeHandler();
567                 handlerRegistration = null;
568             }
569
570             if (autoScroller != null) {
571                 autoScroller.stop();
572                 autoScroller = null;
573             }
574
575             SelectionModel<T> model = grid.getSelectionModel();
576             if (model instanceof Batched) {
577                 Batched<?> batchedModel = (Batched<?>) model;
578                 batchedModel.commitBatchSelect();
579             }
580
581             removeNativeHandler();
582         }
583     }
584
585     private final Grid<T> grid;
586     private HandlerRegistration nativePreviewHandlerRegistration;
587
588     private final AutoScrollHandler autoScrollHandler = new AutoScrollHandler();
589
590     public MultiSelectionRenderer(final Grid<T> grid) {
591         this.grid = grid;
592     }
593
594     @Override
595     public void destroy() {
596         if (nativePreviewHandlerRegistration != null) {
597             removeNativeHandler();
598         }
599     }
600
601     @Override
602     public CheckBox createWidget() {
603         final CheckBox checkBox = GWT.create(CheckBox.class);
604         checkBox.setStylePrimaryName(
605                 grid.getStylePrimaryName() + SELECTION_CHECKBOX_CLASSNAME);
606
607         CheckBoxEventHandler handler = new CheckBoxEventHandler(checkBox);
608
609         // Sink events
610         checkBox.sinkBitlessEvent(BrowserEvents.MOUSEDOWN);
611         checkBox.sinkBitlessEvent(BrowserEvents.TOUCHSTART);
612         checkBox.sinkBitlessEvent(BrowserEvents.CLICK);
613
614         // Add handlers
615         checkBox.addMouseDownHandler(handler);
616         checkBox.addTouchStartHandler(handler);
617         checkBox.addClickHandler(handler);
618         grid.addHandler(handler, GridEnabledEvent.TYPE);
619
620         checkBox.setEnabled(grid.isEnabled());
621
622         return checkBox;
623     }
624
625     @Override
626     public void render(final RendererCellReference cell, final Boolean data,
627             CheckBox checkBox) {
628         checkBox.setValue(data, false);
629         checkBox.setEnabled(grid.isEnabled() && !grid.isEditorActive()
630                 && grid.isUserSelectionAllowed());
631     }
632
633     @Override
634     public Collection<String> getConsumedEvents() {
635         final HashSet<String> events = new HashSet<String>();
636
637         /*
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
640          * cycle.
641          */
642         events.add(BrowserEvents.MOUSEDOWN);
643         events.add(BrowserEvents.TOUCHSTART);
644
645         return events;
646     }
647
648     @Override
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()));
655             return true;
656         }
657         return false;
658     }
659
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();
666     }
667
668     private void injectNativeHandler() {
669         removeNativeHandler();
670         nativePreviewHandlerRegistration = Event
671                 .addNativePreviewHandler(new TouchEventHandler());
672     }
673
674     private void removeNativeHandler() {
675         if (nativePreviewHandlerRegistration != null) {
676             nativePreviewHandlerRegistration.removeHandler();
677             nativePreviewHandlerRegistration = null;
678         }
679     }
680
681     private int getLogicalRowIndex(Grid<T> grid, final Element target) {
682         if (target == null) {
683             return -1;
684         }
685
686         /*
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.
690          */
691         final Element tbody = getTbodyElement();
692         Element tr = tbody.getFirstChildElement();
693         while (tr != null) {
694             if (tr.isOrHasChild(target)) {
695                 final Element td = tr.getFirstChildElement();
696                 assert td != null : "Cell has disappeared";
697
698                 final Element checkbox = td.getFirstChildElement();
699                 assert checkbox != null : "Checkbox has disappeared";
700
701                 return ((AbstractRowContainer) grid.getEscalator().getBody())
702                         .getLogicalRowIndex((TableRowElement) tr);
703             }
704             tr = tr.getNextSiblingElement();
705         }
706         return -1;
707     }
708
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());
714         } else {
715             return null;
716         }
717     }
718
719     private TableSectionElement getTbodyElement() {
720         TableElement table = getTableElement();
721         if (table != null) {
722             return table.getTBodies().getItem(0);
723         } else {
724             return null;
725         }
726     }
727
728     private TableSectionElement getTheadElement() {
729         TableElement table = getTableElement();
730         if (table != null) {
731             return table.getTHead();
732         } else {
733             return null;
734         }
735     }
736
737     private TableSectionElement getTfootElement() {
738         TableElement table = getTableElement();
739         if (table != null) {
740             return table.getTFoot();
741         } else {
742             return null;
743         }
744     }
745
746     /** Get the "top" of an element in relation to "client" coordinates. */
747     private int getClientTop(final Element e) {
748         return e.getAbsoluteTop();
749     }
750
751     private int getBodyClientBottom() {
752         return getClientTop(getTfootElement()) - 1;
753     }
754
755     private int getBodyClientTop() {
756         // Off by one pixel miscalculation. possibly border related.
757         return getClientTop(grid.getElement())
758                 + getTheadElement().getOffsetHeight() + 1;
759     }
760
761     protected boolean isSelected(final int logicalRow) {
762         return grid.isSelected(grid.getDataSource().getRow(logicalRow));
763     }
764
765     protected void setSelected(final int logicalRow, final boolean select) {
766         if (!grid.isUserSelectionAllowed()) {
767             return;
768         }
769
770         T row = grid.getDataSource().getRow(logicalRow);
771         if (select) {
772             grid.select(row);
773         } else {
774             grid.deselect(row);
775         }
776     }
777 }