View Javadoc
1   /*
2    * Copyright 2000-2014 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  
17  package com.vaadin.client.ui;
18  
19  import java.util.ArrayList;
20  import java.util.Collection;
21  import java.util.HashMap;
22  import java.util.HashSet;
23  import java.util.Iterator;
24  import java.util.LinkedList;
25  import java.util.List;
26  import java.util.Map;
27  import java.util.Set;
28  import java.util.logging.Level;
29  import java.util.logging.Logger;
30  
31  import com.google.gwt.core.client.JavaScriptObject;
32  import com.google.gwt.core.client.Scheduler;
33  import com.google.gwt.core.client.Scheduler.ScheduledCommand;
34  import com.google.gwt.dom.client.Document;
35  import com.google.gwt.dom.client.Element;
36  import com.google.gwt.dom.client.NativeEvent;
37  import com.google.gwt.dom.client.Node;
38  import com.google.gwt.dom.client.NodeList;
39  import com.google.gwt.dom.client.Style;
40  import com.google.gwt.dom.client.Style.Display;
41  import com.google.gwt.dom.client.Style.Overflow;
42  import com.google.gwt.dom.client.Style.Position;
43  import com.google.gwt.dom.client.Style.TextAlign;
44  import com.google.gwt.dom.client.Style.Unit;
45  import com.google.gwt.dom.client.Style.Visibility;
46  import com.google.gwt.dom.client.TableCellElement;
47  import com.google.gwt.dom.client.TableRowElement;
48  import com.google.gwt.dom.client.TableSectionElement;
49  import com.google.gwt.dom.client.Touch;
50  import com.google.gwt.event.dom.client.BlurEvent;
51  import com.google.gwt.event.dom.client.BlurHandler;
52  import com.google.gwt.event.dom.client.ContextMenuEvent;
53  import com.google.gwt.event.dom.client.ContextMenuHandler;
54  import com.google.gwt.event.dom.client.FocusEvent;
55  import com.google.gwt.event.dom.client.FocusHandler;
56  import com.google.gwt.event.dom.client.KeyCodes;
57  import com.google.gwt.event.dom.client.KeyDownEvent;
58  import com.google.gwt.event.dom.client.KeyDownHandler;
59  import com.google.gwt.event.dom.client.KeyPressEvent;
60  import com.google.gwt.event.dom.client.KeyPressHandler;
61  import com.google.gwt.event.dom.client.KeyUpEvent;
62  import com.google.gwt.event.dom.client.KeyUpHandler;
63  import com.google.gwt.event.dom.client.ScrollEvent;
64  import com.google.gwt.event.dom.client.ScrollHandler;
65  import com.google.gwt.event.logical.shared.CloseEvent;
66  import com.google.gwt.event.logical.shared.CloseHandler;
67  import com.google.gwt.event.shared.HandlerRegistration;
68  import com.google.gwt.regexp.shared.MatchResult;
69  import com.google.gwt.regexp.shared.RegExp;
70  import com.google.gwt.user.client.Command;
71  import com.google.gwt.user.client.DOM;
72  import com.google.gwt.user.client.Event;
73  import com.google.gwt.user.client.Event.NativePreviewEvent;
74  import com.google.gwt.user.client.Event.NativePreviewHandler;
75  import com.google.gwt.user.client.Timer;
76  import com.google.gwt.user.client.Window;
77  import com.google.gwt.user.client.ui.FlowPanel;
78  import com.google.gwt.user.client.ui.HasWidgets;
79  import com.google.gwt.user.client.ui.Panel;
80  import com.google.gwt.user.client.ui.PopupPanel;
81  import com.google.gwt.user.client.ui.RootPanel;
82  import com.google.gwt.user.client.ui.UIObject;
83  import com.google.gwt.user.client.ui.Widget;
84  import com.vaadin.client.ApplicationConnection;
85  import com.vaadin.client.BrowserInfo;
86  import com.vaadin.client.ComponentConnector;
87  import com.vaadin.client.ConnectorMap;
88  import com.vaadin.client.DeferredWorker;
89  import com.vaadin.client.Focusable;
90  import com.vaadin.client.MouseEventDetailsBuilder;
91  import com.vaadin.client.StyleConstants;
92  import com.vaadin.client.TooltipInfo;
93  import com.vaadin.client.UIDL;
94  import com.vaadin.client.Util;
95  import com.vaadin.client.VConsole;
96  import com.vaadin.client.VTooltip;
97  import com.vaadin.client.WidgetUtil;
98  import com.vaadin.client.ui.VScrollTablePatched.VScrollTableBody.VScrollTableRow;
99  import com.vaadin.client.ui.dd.DDUtil;
100 import com.vaadin.client.ui.dd.VAbstractDropHandler;
101 import com.vaadin.client.ui.dd.VAcceptCallback;
102 import com.vaadin.client.ui.dd.VDragAndDropManager;
103 import com.vaadin.client.ui.dd.VDragEvent;
104 import com.vaadin.client.ui.dd.VHasDropHandler;
105 import com.vaadin.client.ui.dd.VTransferable;
106 import com.vaadin.shared.AbstractComponentState;
107 import com.vaadin.shared.MouseEventDetails;
108 import com.vaadin.shared.ui.dd.VerticalDropLocation;
109 import com.vaadin.shared.ui.table.TableConstants;
110 
111 /**
112  * VScrollTable
113  * 
114  * VScrollTable is a FlowPanel having two widgets in it: * TableHead component *
115  * ScrollPanel
116  * 
117  * TableHead contains table's header and widgets + logic for resizing,
118  * reordering and hiding columns.
119  * 
120  * ScrollPanel contains VScrollTableBody object which handles content. To save
121  * some bandwidth and to improve clients responsiveness with loads of data, in
122  * VScrollTableBody all rows are not necessary rendered. There are "spacers" in
123  * VScrollTableBody to use the exact same space as non-rendered rows would use.
124  * This way we can use seamlessly traditional scrollbars and scrolling to fetch
125  * more rows instead of "paging".
126  * 
127  * In VScrollTable we listen to scroll events. On horizontal scrolling we also
128  * update TableHeads scroll position which has its scrollbars hidden. On
129  * vertical scroll events we will check if we are reaching the end of area where
130  * we have rows rendered and
131  * 
132  * TODO implement unregistering for child components in Cells
133  */
134 public class VScrollTablePatched extends FlowPanel implements HasWidgets,
135         ScrollHandler, VHasDropHandler, FocusHandler, BlurHandler, Focusable,
136         ActionOwner, SubPartAware, DeferredWorker {
137 
138     protected TableHead createTableHead() {
139         return new TableHead();
140     }
141 
142     protected HeaderCell createHeaderCell(String colId, String headerText) {
143         return new HeaderCell(colId, headerText);
144     }
145 
146     public boolean isVisibleInHierarchy() {
147         boolean isVisible = isVisible();
148         Widget current = getParent();
149         while (isVisible && current != null) {
150             isVisible &= current.isVisible();
151             current = current.getParent();
152         }
153         return isVisible;
154     }
155 
156     /**
157      * Simple interface for parts of the table capable of owning a context menu.
158      * 
159      * @since 7.2
160      * @author Vaadin Ltd
161      */
162     private interface ContextMenuOwner {
163         public void showContextMenu(Event event);
164     }
165 
166     /**
167      * Handles showing context menu on "long press" from a touch screen.
168      * 
169      * @since 7.2
170      * @author Vaadin Ltd
171      */
172     private class TouchContextProvider {
173         private static final int TOUCH_CONTEXT_MENU_TIMEOUT = 500;
174         private Timer contextTouchTimeout;
175 
176         private Event touchStart;
177         private int touchStartY;
178         private int touchStartX;
179 
180         private ContextMenuOwner target;
181 
182         /**
183          * Initializes a handler for a certain context menu owner.
184          * 
185          * @param target
186          *            the owner of the context menu
187          */
188         public TouchContextProvider(ContextMenuOwner target) {
189             this.target = target;
190         }
191 
192         /**
193          * Cancels the current context touch timeout.
194          */
195         public void cancel() {
196             if (contextTouchTimeout != null) {
197                 contextTouchTimeout.cancel();
198                 contextTouchTimeout = null;
199             }
200             touchStart = null;
201         }
202 
203         /**
204          * A function to handle touch context events in a table.
205          * 
206          * @param event
207          *            browser event to handle
208          */
209         public void handleTouchEvent(final Event event) {
210             int type = event.getTypeInt();
211 
212             switch (type) {
213             case Event.ONCONTEXTMENU:
214                 target.showContextMenu(event);
215                 break;
216             case Event.ONTOUCHSTART:
217                 // save position to fields, touches in events are same
218                 // instance during the operation.
219                 touchStart = event;
220 
221                 Touch touch = event.getChangedTouches().get(0);
222                 touchStartX = touch.getClientX();
223                 touchStartY = touch.getClientY();
224 
225                 if (contextTouchTimeout == null) {
226                     contextTouchTimeout = new Timer() {
227 
228                         @Override
229                         public void run() {
230                             if (touchStart != null) {
231                                 // Open the context menu if finger
232                                 // is held in place long enough.
233                                 target.showContextMenu(touchStart);
234                                 event.preventDefault();
235                                 touchStart = null;
236                             }
237                         }
238                     };
239                 }
240                 contextTouchTimeout.schedule(TOUCH_CONTEXT_MENU_TIMEOUT);
241                 break;
242             case Event.ONTOUCHCANCEL:
243             case Event.ONTOUCHEND:
244                 cancel();
245                 break;
246             case Event.ONTOUCHMOVE:
247                 if (isSignificantMove(event)) {
248                     // Moved finger before the context menu timer
249                     // expired, so let the browser handle the event.
250                     cancel();
251                 }
252             }
253         }
254 
255         /**
256          * Calculates how many pixels away the user's finger has traveled. This
257          * reduces the chance of small non-intentional movements from canceling
258          * the long press detection.
259          * 
260          * @param event
261          *            the Event for which to check the move distance
262          * @return true if this is considered an intentional move by the user
263          */
264         protected boolean isSignificantMove(Event event) {
265             if (touchStart == null) {
266                 // no touch start
267                 return false;
268             }
269 
270             // Calculate the distance between touch start and the current touch
271             // position
272             Touch touch = event.getChangedTouches().get(0);
273             int deltaX = touch.getClientX() - touchStartX;
274             int deltaY = touch.getClientY() - touchStartY;
275             int delta = deltaX * deltaX + deltaY * deltaY;
276 
277             // Compare to the square of the significant move threshold to remove
278             // the need for a square root
279             if (delta > TouchScrollDelegate.SIGNIFICANT_MOVE_THRESHOLD
280                     * TouchScrollDelegate.SIGNIFICANT_MOVE_THRESHOLD) {
281                 return true;
282             }
283             return false;
284         }
285     }
286 
287     public static final String STYLENAME = "v-table";
288 
289     public enum SelectMode {
290         NONE(0), SINGLE(1), MULTI(2);
291         private int id;
292 
293         private SelectMode(int id) {
294             this.id = id;
295         }
296 
297         public int getId() {
298             return id;
299         }
300     }
301 
302     private static final String ROW_HEADER_COLUMN_KEY = "0";
303 
304     private static final double CACHE_RATE_DEFAULT = 2;
305 
306     /**
307      * The default multi select mode where simple left clicks only selects one
308      * item, CTRL+left click selects multiple items and SHIFT-left click selects
309      * a range of items.
310      */
311     private static final int MULTISELECT_MODE_DEFAULT = 0;
312 
313     /**
314      * The simple multiselect mode is what the table used to have before
315      * ctrl/shift selections were added. That is that when this is set clicking
316      * on an item selects/deselects the item and no ctrl/shift selections are
317      * available.
318      */
319     private static final int MULTISELECT_MODE_SIMPLE = 1;
320 
321     /**
322      * multiple of pagelength which component will cache when requesting more
323      * rows
324      */
325     private double cache_rate = CACHE_RATE_DEFAULT;
326     /**
327      * fraction of pageLenght which can be scrolled without making new request
328      */
329     private double cache_react_rate = 0.75 * cache_rate;
330 
331     public static final char ALIGN_CENTER = 'c';
332     public static final char ALIGN_LEFT = 'b';
333     public static final char ALIGN_RIGHT = 'e';
334     private static final int CHARCODE_SPACE = 32;
335     private int firstRowInViewPort = 0;
336     private int pageLength = 15;
337     private int lastRequestedFirstvisible = 0; // to detect "serverside scroll"
338     private int firstvisibleOnLastPage = -1; // To detect if the first visible
339                                              // is on the last page
340 
341     /** For internal use only. May be removed or replaced in the future. */
342     public boolean showRowHeaders = false;
343 
344     private String[] columnOrder;
345 
346     protected ApplicationConnection client;
347 
348     /** For internal use only. May be removed or replaced in the future. */
349     public String paintableId;
350 
351     /** For internal use only. May be removed or replaced in the future. */
352     public boolean immediate;
353 
354     private boolean updatedReqRows = true;
355 
356     private boolean nullSelectionAllowed = true;
357 
358     private SelectMode selectMode = SelectMode.NONE;
359 
360     public final HashSet<String> selectedRowKeys = new HashSet<String>();
361 
362     /*
363      * When scrolling and selecting at the same time, the selections are not in
364      * sync with the server while retrieving new rows (until key is released).
365      */
366     private HashSet<Object> unSyncedselectionsBeforeRowFetch;
367 
368     /*
369      * These are used when jumping between pages when pressing Home and End
370      */
371 
372     /** For internal use only. May be removed or replaced in the future. */
373     public boolean selectLastItemInNextRender = false;
374     /** For internal use only. May be removed or replaced in the future. */
375     public boolean selectFirstItemInNextRender = false;
376     /** For internal use only. May be removed or replaced in the future. */
377     public boolean focusFirstItemInNextRender = false;
378     /** For internal use only. May be removed or replaced in the future. */
379     public boolean focusLastItemInNextRender = false;
380 
381     /**
382      * The currently focused row.
383      * <p>
384      * For internal use only. May be removed or replaced in the future.
385      */
386     public VScrollTableRow focusedRow;
387 
388     /**
389      * Helper to store selection range start in when using the keyboard
390      * <p>
391      * For internal use only. May be removed or replaced in the future.
392      */
393     public VScrollTableRow selectionRangeStart;
394 
395     /**
396      * Flag for notifying when the selection has changed and should be sent to
397      * the server
398      * <p>
399      * For internal use only. May be removed or replaced in the future.
400      */
401     public boolean selectionChanged = false;
402 
403     /*
404      * The speed (in pixels) which the scrolling scrolls vertically/horizontally
405      */
406     private int scrollingVelocity = 10;
407 
408     private Timer scrollingVelocityTimer = null;
409 
410     /** For internal use only. May be removed or replaced in the future. */
411     public String[] bodyActionKeys;
412 
413     private boolean enableDebug = false;
414 
415     private static final boolean hasNativeTouchScrolling = BrowserInfo.get()
416             .isTouchDevice()
417             && !BrowserInfo.get().requiresTouchScrollDelegate();
418 
419     private Set<String> noncollapsibleColumns;
420 
421     /**
422      * The last known row height used to preserve the height of a table with
423      * custom row heights and a fixed page length after removing the last row
424      * from the table.
425      * 
426      * A new VScrollTableBody instance is created every time the number of rows
427      * changes causing {@link VScrollTableBody#rowHeight} to be discarded and
428      * the height recalculated by {@link VScrollTableBody#getRowHeight(boolean)}
429      * to avoid some rounding problems, e.g. round(2 * 19.8) / 2 = 20 but
430      * round(3 * 19.8) / 3 = 19.66.
431      */
432     private double lastKnownRowHeight = Double.NaN;
433 
434     /**
435      * Remember scroll position when getting detached to properly scroll back to
436      * the location that there is data for if getting attached again.
437      */
438     private int detachedScrollPosition = 0;
439 
440     /**
441      * Represents a select range of rows
442      */
443     private class SelectionRange {
444         private VScrollTableRow startRow;
445         private final int length;
446 
447         /**
448          * Constuctor.
449          */
450         public SelectionRange(VScrollTableRow row1, VScrollTableRow row2) {
451             VScrollTableRow endRow;
452             if (row2.isBefore(row1)) {
453                 startRow = row2;
454                 endRow = row1;
455             } else {
456                 startRow = row1;
457                 endRow = row2;
458             }
459             length = endRow.getIndex() - startRow.getIndex() + 1;
460         }
461 
462         public SelectionRange(VScrollTableRow row, int length) {
463             startRow = row;
464             this.length = length;
465         }
466 
467         /*
468          * (non-Javadoc)
469          * 
470          * @see java.lang.Object#toString()
471          */
472 
473         @Override
474         public String toString() {
475             return startRow.getKey() + "-" + length;
476         }
477 
478         private boolean inRange(VScrollTableRow row) {
479             return row.getIndex() >= startRow.getIndex()
480                     && row.getIndex() < startRow.getIndex() + length;
481         }
482 
483         public Collection<SelectionRange> split(VScrollTableRow row) {
484             assert row.isAttached();
485             ArrayList<SelectionRange> ranges = new ArrayList<SelectionRange>(2);
486 
487             int endOfFirstRange = row.getIndex() - 1;
488             if (endOfFirstRange >= startRow.getIndex()) {
489                 // create range of first part unless its length is < 1
490                 ranges.add(new SelectionRange(startRow, endOfFirstRange
491                         - startRow.getIndex() + 1));
492             }
493             int startOfSecondRange = row.getIndex() + 1;
494             if (getEndIndex() >= startOfSecondRange) {
495                 // create range of second part unless its length is < 1
496                 VScrollTableRow startOfRange = scrollBody
497                         .getRowByRowIndex(startOfSecondRange);
498                 if (startOfRange != null) {
499                     ranges.add(new SelectionRange(startOfRange, getEndIndex()
500                             - startOfSecondRange + 1));
501                 }
502             }
503             return ranges;
504         }
505 
506         private int getEndIndex() {
507             return startRow.getIndex() + length - 1;
508         }
509 
510     }
511 
512     private final HashSet<SelectionRange> selectedRowRanges = new HashSet<SelectionRange>();
513 
514     /** For internal use only. May be removed or replaced in the future. */
515     public boolean initializedAndAttached = false;
516 
517     /**
518      * Flag to indicate if a column width recalculation is needed due update.
519      * <p>
520      * For internal use only. May be removed or replaced in the future.
521      */
522     public boolean headerChangedDuringUpdate = false;
523 
524     /** For internal use only. May be removed or replaced in the future. */
525     public final TableHead tHead = createTableHead();
526 
527     /** For internal use only. May be removed or replaced in the future. */
528     public final TableFooter tFoot = new TableFooter();
529 
530     /** Handles context menu for table body */
531     private ContextMenuOwner contextMenuOwner = new ContextMenuOwner() {
532 
533         @Override
534         public void showContextMenu(Event event) {
535             int left = WidgetUtil.getTouchOrMouseClientX(event);
536             int top = WidgetUtil.getTouchOrMouseClientY(event);
537             boolean menuShown = handleBodyContextMenu(left, top);
538             if (menuShown) {
539                 event.stopPropagation();
540                 event.preventDefault();
541             }
542         }
543     };
544 
545     /** Handles touch events to display a context menu for table body */
546     private TouchContextProvider touchContextProvider = new TouchContextProvider(
547             contextMenuOwner);
548 
549     /**
550      * For internal use only. May be removed or replaced in the future.
551      * 
552      * Overwrites onBrowserEvent function on FocusableScrollPanel to give event
553      * access to touchContextProvider. Has to be public to give TableConnector
554      * access to the scrollBodyPanel field.
555      * 
556      * @since 7.2
557      * @author Vaadin Ltd
558      */
559     public class FocusableScrollContextPanel extends FocusableScrollPanel {
560         @Override
561         public void onBrowserEvent(Event event) {
562             super.onBrowserEvent(event);
563             touchContextProvider.handleTouchEvent(event);
564         };
565 
566         public FocusableScrollContextPanel(boolean useFakeFocusElement) {
567             super(useFakeFocusElement);
568         }
569     }
570 
571     /** For internal use only. May be removed or replaced in the future. */
572     public final FocusableScrollContextPanel scrollBodyPanel = new FocusableScrollContextPanel(
573             true);
574 
575     private KeyPressHandler navKeyPressHandler = new KeyPressHandler() {
576 
577         @Override
578         public void onKeyPress(KeyPressEvent keyPressEvent) {
579             // This is used for Firefox only, since Firefox auto-repeat
580             // works correctly only if we use a key press handler, other
581             // browsers handle it correctly when using a key down handler
582             if (!BrowserInfo.get().isGecko()) {
583                 return;
584             }
585 
586             NativeEvent event = keyPressEvent.getNativeEvent();
587             if (!enabled) {
588                 // Cancel default keyboard events on a disabled Table
589                 // (prevents scrolling)
590                 event.preventDefault();
591             } else if (hasFocus) {
592                 // Key code in Firefox/onKeyPress is present only for
593                 // special keys, otherwise 0 is returned
594                 int keyCode = event.getKeyCode();
595                 if (keyCode == 0 && event.getCharCode() == ' ') {
596                     // Provide a keyCode for space to be compatible with
597                     // FireFox keypress event
598                     keyCode = CHARCODE_SPACE;
599                 }
600 
601                 if (handleNavigation(keyCode,
602                         event.getCtrlKey() || event.getMetaKey(),
603                         event.getShiftKey())) {
604                     event.preventDefault();
605                 }
606 
607                 startScrollingVelocityTimer();
608             }
609         }
610 
611     };
612 
613     private KeyUpHandler navKeyUpHandler = new KeyUpHandler() {
614 
615         @Override
616         public void onKeyUp(KeyUpEvent keyUpEvent) {
617             NativeEvent event = keyUpEvent.getNativeEvent();
618             int keyCode = event.getKeyCode();
619 
620             if (!isFocusable()) {
621                 cancelScrollingVelocityTimer();
622             } else if (isNavigationKey(keyCode)) {
623                 if (keyCode == getNavigationDownKey()
624                         || keyCode == getNavigationUpKey()) {
625                     /*
626                      * in multiselect mode the server may still have value from
627                      * previous page. Clear it unless doing multiselection or
628                      * just moving focus.
629                      */
630                     if (!event.getShiftKey() && !event.getCtrlKey()) {
631                         instructServerToForgetPreviousSelections();
632                     }
633                     sendSelectedRows();
634                 }
635                 cancelScrollingVelocityTimer();
636                 navKeyDown = false;
637             }
638         }
639     };
640 
641     private KeyDownHandler navKeyDownHandler = new KeyDownHandler() {
642 
643         @Override
644         public void onKeyDown(KeyDownEvent keyDownEvent) {
645             NativeEvent event = keyDownEvent.getNativeEvent();
646             // This is not used for Firefox
647             if (BrowserInfo.get().isGecko()) {
648                 return;
649             }
650 
651             if (!enabled) {
652                 // Cancel default keyboard events on a disabled Table
653                 // (prevents scrolling)
654                 event.preventDefault();
655             } else if (hasFocus) {
656                 if (handleNavigation(event.getKeyCode(), event.getCtrlKey()
657                         || event.getMetaKey(), event.getShiftKey())) {
658                     navKeyDown = true;
659                     event.preventDefault();
660                 }
661 
662                 startScrollingVelocityTimer();
663             }
664         }
665     };
666 
667     /** For internal use only. May be removed or replaced in the future. */
668     public int totalRows;
669 
670     private Set<String> collapsedColumns;
671 
672     /** For internal use only. May be removed or replaced in the future. */
673     public final RowRequestHandler rowRequestHandler;
674 
675     /** For internal use only. May be removed or replaced in the future. */
676     public VScrollTableBody scrollBody;
677 
678     private int firstvisible = 0;
679     private boolean sortAscending;
680     private String sortColumn;
681     private String oldSortColumn;
682     private boolean columnReordering;
683 
684     /**
685      * This map contains captions and icon urls for actions like: * "33_c" ->
686      * "Edit" * "33_i" -> "http://dom.com/edit.png"
687      */
688     private final HashMap<Object, String> actionMap = new HashMap<Object, String>();
689     private String[] visibleColOrder;
690     private boolean initialContentReceived = false;
691     private Element scrollPositionElement;
692 
693     /** For internal use only. May be removed or replaced in the future. */
694     public boolean enabled;
695 
696     /** For internal use only. May be removed or replaced in the future. */
697     public boolean showColHeaders;
698 
699     /** For internal use only. May be removed or replaced in the future. */
700     public boolean showColFooters;
701 
702     /** flag to indicate that table body has changed */
703     private boolean isNewBody = true;
704 
705     /**
706      * Read from the "recalcWidths" -attribute. When it is true, the table will
707      * recalculate the widths for columns - desirable in some cases. For #1983,
708      * marked experimental. See also variable <code>refreshContentWidths</code>
709      * in method {@link TableHead#updateCellsFromUIDL(UIDL)}.
710      * <p>
711      * For internal use only. May be removed or replaced in the future.
712      */
713     public boolean recalcWidths = false;
714 
715     /** For internal use only. May be removed or replaced in the future. */
716     public boolean rendering = false;
717 
718     private boolean hasFocus = false;
719     private int dragmode;
720 
721     protected int multiselectmode;
722 
723     /** For internal use only. May be removed or replaced in the future. */
724     public int tabIndex;
725 
726     private TouchScrollDelegate touchScrollDelegate;
727 
728     /** For internal use only. May be removed or replaced in the future. */
729     public int lastRenderedHeight;
730 
731     /**
732      * Values (serverCacheFirst+serverCacheLast) sent by server that tells which
733      * rows (indexes) are in the server side cache (page buffer). -1 means
734      * unknown. The server side cache row MUST MATCH the client side cache rows.
735      * 
736      * If the client side cache contains additional rows with e.g. buttons, it
737      * will cause out of sync when such a button is pressed.
738      * 
739      * If the server side cache contains additional rows with e.g. buttons,
740      * scrolling in the client will cause empty buttons to be rendered
741      * (cached=true request for non-existing components)
742      * 
743      * For internal use only. May be removed or replaced in the future.
744      */
745     public int serverCacheFirst = -1;
746     public int serverCacheLast = -1;
747 
748     /**
749      * In several cases TreeTable depends on the scrollBody.lastRendered being
750      * 'out of sync' while the update is being done. In those cases the sanity
751      * check must be performed afterwards.
752      */
753     public boolean postponeSanityCheckForLastRendered;
754 
755     /** For internal use only. May be removed or replaced in the future. */
756     public boolean sizeNeedsInit = true;
757 
758     /**
759      * Used to recall the position of an open context menu if we need to close
760      * and reopen it during a row update.
761      * <p>
762      * For internal use only. May be removed or replaced in the future.
763      */
764     public class ContextMenuDetails implements CloseHandler<PopupPanel> {
765         public String rowKey;
766         public int left;
767         public int top;
768         HandlerRegistration closeRegistration;
769 
770         public ContextMenuDetails(VContextMenu menu, String rowKey, int left,
771                 int top) {
772             this.rowKey = rowKey;
773             this.left = left;
774             this.top = top;
775             closeRegistration = menu.addCloseHandler(this);
776         }
777 
778         @Override
779         public void onClose(CloseEvent<PopupPanel> event) {
780             contextMenu = null;
781             closeRegistration.removeHandler();
782         }
783     }
784 
785     /** For internal use only. May be removed or replaced in the future. */
786     public ContextMenuDetails contextMenu = null;
787 
788     private boolean hadScrollBars = false;
789 
790     private HandlerRegistration addCloseHandler;
791 
792     /**
793      * Changes to manage mouseDown and mouseUp
794      */
795     /**
796      * The element where the last mouse down event was registered.
797      */
798     private Element lastMouseDownTarget;
799 
800     /**
801      * Set to true by {@link #mouseUpPreviewHandler} if it gets a mouseup at the
802      * same element as {@link #lastMouseDownTarget}.
803      */
804     private boolean mouseUpPreviewMatched = false;
805 
806     private HandlerRegistration mouseUpEventPreviewRegistration;
807 
808     /**
809      * Previews events after a mousedown to detect where the following mouseup
810      * hits.
811      */
812     private final NativePreviewHandler mouseUpPreviewHandler = new NativePreviewHandler() {
813 
814         @Override
815         public void onPreviewNativeEvent(NativePreviewEvent event) {
816             if (event.getTypeInt() == Event.ONMOUSEUP) {
817                 mouseUpEventPreviewRegistration.removeHandler();
818 
819                 // Event's reported target not always correct if event
820                 // capture is in use
821                 Element elementUnderMouse = WidgetUtil
822                         .getElementUnderMouse(event.getNativeEvent());
823                 if (lastMouseDownTarget != null
824                         && lastMouseDownTarget.isOrHasChild(elementUnderMouse)) {
825                     mouseUpPreviewMatched = true;
826                 } else {
827                     getLogger().log(
828                             Level.FINEST,
829                             "Ignoring mouseup from " + elementUnderMouse
830                                     + " when mousedown was on "
831                                     + lastMouseDownTarget);
832                 }
833             }
834         }
835     };
836 
837     public VScrollTablePatched() {
838         setMultiSelectMode(MULTISELECT_MODE_DEFAULT);
839 
840         scrollBodyPanel.addFocusHandler(this);
841         scrollBodyPanel.addBlurHandler(this);
842 
843         scrollBodyPanel.addScrollHandler(this);
844 
845         /*
846          * Firefox auto-repeat works correctly only if we use a key press
847          * handler, other browsers handle it correctly when using a key down
848          * handler
849          */
850         if (BrowserInfo.get().isGecko()) {
851             scrollBodyPanel.addKeyPressHandler(navKeyPressHandler);
852         } else {
853             scrollBodyPanel.addKeyDownHandler(navKeyDownHandler);
854         }
855         scrollBodyPanel.addKeyUpHandler(navKeyUpHandler);
856 
857         scrollBodyPanel.sinkEvents(Event.TOUCHEVENTS | Event.ONCONTEXTMENU);
858 
859         setStyleName(STYLENAME);
860 
861         add(tHead);
862         add(scrollBodyPanel);
863         add(tFoot);
864 
865         rowRequestHandler = new RowRequestHandler();
866     }
867 
868     @Override
869     public void setStyleName(String style) {
870         updateStyleNames(style, false);
871     }
872 
873     @Override
874     public void setStylePrimaryName(String style) {
875         updateStyleNames(style, true);
876     }
877 
878     private void updateStyleNames(String newStyle, boolean isPrimary) {
879         scrollBodyPanel
880                 .removeStyleName(getStylePrimaryName() + "-body-wrapper");
881         scrollBodyPanel.removeStyleName(getStylePrimaryName() + "-body");
882 
883         if (scrollBody != null) {
884             scrollBody.removeStyleName(getStylePrimaryName()
885                     + "-body-noselection");
886         }
887 
888         if (isPrimary) {
889             super.setStylePrimaryName(newStyle);
890         } else {
891             super.setStyleName(newStyle);
892         }
893 
894         scrollBodyPanel.addStyleName(getStylePrimaryName() + "-body-wrapper");
895         scrollBodyPanel.addStyleName(getStylePrimaryName() + "-body");
896 
897         tHead.updateStyleNames(getStylePrimaryName());
898         tFoot.updateStyleNames(getStylePrimaryName());
899 
900         if (scrollBody != null) {
901             scrollBody.updateStyleNames(getStylePrimaryName());
902         }
903     }
904 
905     public void init(ApplicationConnection client) {
906         this.client = client;
907         // Add a handler to clear saved context menu details when the menu
908         // closes. See #8526.
909         addCloseHandler = client.getContextMenu().addCloseHandler(
910                 new CloseHandler<PopupPanel>() {
911 
912                     @Override
913                     public void onClose(CloseEvent<PopupPanel> event) {
914                         contextMenu = null;
915                     }
916                 });
917     }
918 
919     /**
920      * Handles a context menu event on table body.
921      * 
922      * @param left
923      *            left position of the context menu
924      * @param top
925      *            top position of the context menu
926      * @return true if a context menu was shown, otherwise false
927      */
928     private boolean handleBodyContextMenu(int left, int top) {
929         if (enabled && bodyActionKeys != null) {
930             top += Window.getScrollTop();
931             left += Window.getScrollLeft();
932             client.getContextMenu().showAt(this, left, top);
933             return true;
934         }
935         return false;
936     }
937 
938     /**
939      * Fires a column resize event which sends the resize information to the
940      * server.
941      * 
942      * @param columnId
943      *            The columnId of the column which was resized
944      * @param originalWidth
945      *            The width in pixels of the column before the resize event
946      * @param newWidth
947      *            The width in pixels of the column after the resize event
948      */
949     private void fireColumnResizeEvent(String columnId, int originalWidth,
950             int newWidth) {
951         client.updateVariable(paintableId, "columnResizeEventColumn", columnId,
952                 false);
953         client.updateVariable(paintableId, "columnResizeEventPrev",
954                 originalWidth, false);
955         client.updateVariable(paintableId, "columnResizeEventCurr", newWidth,
956                 immediate);
957 
958     }
959 
960     /**
961      * Non-immediate variable update of column widths for a collection of
962      * columns.
963      * 
964      * @param columns
965      *            the columns to trigger the events for.
966      */
967     private void sendColumnWidthUpdates(Collection<HeaderCell> columns) {
968         String[] newSizes = new String[columns.size()];
969         int ix = 0;
970         for (HeaderCell cell : columns) {
971             newSizes[ix++] = cell.getColKey() + ":" + cell.getWidth();
972         }
973         client.updateVariable(paintableId, "columnWidthUpdates", newSizes,
974                 false);
975     }
976 
977     /**
978      * Moves the focus one step down
979      * 
980      * @return Returns true if succeeded
981      */
982     private boolean moveFocusDown() {
983         return moveFocusDown(0);
984     }
985 
986     /**
987      * Moves the focus down by 1+offset rows
988      * 
989      * @return Returns true if succeeded, else false if the selection could not
990      *         be move downwards
991      */
992     private boolean moveFocusDown(int offset) {
993         if (isSelectable()) {
994             if (focusedRow == null && scrollBody.iterator().hasNext()) {
995                 // FIXME should focus first visible from top, not first rendered
996                 // ??
997                 return setRowFocus((VScrollTableRow) scrollBody.iterator()
998                         .next());
999             } else {
1000                 VScrollTableRow next = getNextRow(focusedRow, offset);
1001                 if (next != null) {
1002                     return setRowFocus(next);
1003                 }
1004             }
1005         }
1006 
1007         return false;
1008     }
1009 
1010     /**
1011      * Moves the selection one step up
1012      * 
1013      * @return Returns true if succeeded
1014      */
1015     private boolean moveFocusUp() {
1016         return moveFocusUp(0);
1017     }
1018 
1019     /**
1020      * Moves the focus row upwards
1021      * 
1022      * @return Returns true if succeeded, else false if the selection could not
1023      *         be move upwards
1024      * 
1025      */
1026     private boolean moveFocusUp(int offset) {
1027         if (isSelectable()) {
1028             if (focusedRow == null && scrollBody.iterator().hasNext()) {
1029                 // FIXME logic is exactly the same as in moveFocusDown, should
1030                 // be the opposite??
1031                 return setRowFocus((VScrollTableRow) scrollBody.iterator()
1032                         .next());
1033             } else {
1034                 VScrollTableRow prev = getPreviousRow(focusedRow, offset);
1035                 if (prev != null) {
1036                     return setRowFocus(prev);
1037                 } else {
1038                     VConsole.log("no previous available");
1039                 }
1040             }
1041         }
1042 
1043         return false;
1044     }
1045 
1046     /**
1047      * Selects a row where the current selection head is
1048      * 
1049      * @param ctrlSelect
1050      *            Is the selection a ctrl+selection
1051      * @param shiftSelect
1052      *            Is the selection a shift+selection
1053      * @return Returns truw
1054      */
1055     private void selectFocusedRow(boolean ctrlSelect, boolean shiftSelect) {
1056         if (focusedRow != null) {
1057             // Arrows moves the selection and clears previous selections
1058             if (isSelectable() && !ctrlSelect && !shiftSelect) {
1059                 deselectAll();
1060                 focusedRow.toggleSelection();
1061                 selectionRangeStart = focusedRow;
1062             } else if (isSelectable() && ctrlSelect && !shiftSelect) {
1063                 // Ctrl+arrows moves selection head
1064                 selectionRangeStart = focusedRow;
1065                 // No selection, only selection head is moved
1066             } else if (isMultiSelectModeAny() && !ctrlSelect && shiftSelect) {
1067                 // Shift+arrows selection selects a range
1068                 focusedRow.toggleShiftSelection(shiftSelect);
1069             }
1070         }
1071     }
1072 
1073     /**
1074      * Sends the selection to the server if changed since the last update/visit.
1075      */
1076     protected void sendSelectedRows() {
1077         sendSelectedRows(immediate);
1078     }
1079 
1080     private void updateFirstVisibleAndSendSelectedRows() {
1081         updateFirstVisibleRow();
1082         sendSelectedRows(immediate);
1083     }
1084 
1085     /**
1086      * Sends the selection to the server if it has been changed since the last
1087      * update/visit.
1088      * 
1089      * @param immediately
1090      *            set to true to immediately send the rows
1091      */
1092     protected void sendSelectedRows(boolean immediately) {
1093         // Don't send anything if selection has not changed
1094         if (!selectionChanged) {
1095             return;
1096         }
1097 
1098         // Reset selection changed flag
1099         selectionChanged = false;
1100 
1101         // Note: changing the immediateness of this might require changes to
1102         // "clickEvent" immediateness also.
1103         if (isMultiSelectModeDefault()) {
1104             // Convert ranges to a set of strings
1105             Set<String> ranges = new HashSet<String>();
1106             for (SelectionRange range : selectedRowRanges) {
1107                 ranges.add(range.toString());
1108             }
1109 
1110             // Send the selected row ranges
1111             client.updateVariable(paintableId, "selectedRanges",
1112                     ranges.toArray(new String[selectedRowRanges.size()]), false);
1113             selectedRowRanges.clear();
1114 
1115             // clean selectedRowKeys so that they don't contain excess values
1116             for (Iterator<String> iterator = selectedRowKeys.iterator(); iterator
1117                     .hasNext();) {
1118                 String key = iterator.next();
1119                 VScrollTableRow renderedRowByKey = getRenderedRowByKey(key);
1120                 if (renderedRowByKey != null) {
1121                     for (SelectionRange range : selectedRowRanges) {
1122                         if (range.inRange(renderedRowByKey)) {
1123                             iterator.remove();
1124                         }
1125                     }
1126                 } else {
1127                     // orphaned selected key, must be in a range, ignore
1128                     iterator.remove();
1129                 }
1130 
1131             }
1132         }
1133 
1134         // Send the selected rows
1135         client.updateVariable(paintableId, "selected",
1136                 selectedRowKeys.toArray(new String[selectedRowKeys.size()]),
1137                 false);
1138 
1139         if (immediately) {
1140             selectionSynchronizer.schedule(500);
1141         }
1142     }
1143 
1144     private Timer selectionSynchronizer = new Timer() {
1145         @Override
1146         public void run() {
1147             client.sendPendingVariableChanges();
1148     }
1149     };
1150 
1151     /**
1152      * Get the key that moves the selection head upwards. By default it is the
1153      * up arrow key but by overriding this you can change the key to whatever
1154      * you want.
1155      * 
1156      * @return The keycode of the key
1157      */
1158     protected int getNavigationUpKey() {
1159         return KeyCodes.KEY_UP;
1160     }
1161 
1162     /**
1163      * Get the key that moves the selection head downwards. By default it is the
1164      * down arrow key but by overriding this you can change the key to whatever
1165      * you want.
1166      * 
1167      * @return The keycode of the key
1168      */
1169     protected int getNavigationDownKey() {
1170         return KeyCodes.KEY_DOWN;
1171     }
1172 
1173     /**
1174      * Get the key that scrolls to the left in the table. By default it is the
1175      * left arrow key but by overriding this you can change the key to whatever
1176      * you want.
1177      * 
1178      * @return The keycode of the key
1179      */
1180     protected int getNavigationLeftKey() {
1181         return KeyCodes.KEY_LEFT;
1182     }
1183 
1184     /**
1185      * Get the key that scroll to the right on the table. By default it is the
1186      * right arrow key but by overriding this you can change the key to whatever
1187      * you want.
1188      * 
1189      * @return The keycode of the key
1190      */
1191     protected int getNavigationRightKey() {
1192         return KeyCodes.KEY_RIGHT;
1193     }
1194 
1195     /**
1196      * Get the key that selects an item in the table. By default it is the space
1197      * bar key but by overriding this you can change the key to whatever you
1198      * want.
1199      * 
1200      * @return
1201      */
1202     protected int getNavigationSelectKey() {
1203         return CHARCODE_SPACE;
1204     }
1205 
1206     /**
1207      * Get the key the moves the selection one page up in the table. By default
1208      * this is the Page Up key but by overriding this you can change the key to
1209      * whatever you want.
1210      * 
1211      * @return
1212      */
1213     protected int getNavigationPageUpKey() {
1214         return KeyCodes.KEY_PAGEUP;
1215     }
1216 
1217     /**
1218      * Get the key the moves the selection one page down in the table. By
1219      * default this is the Page Down key but by overriding this you can change
1220      * the key to whatever you want.
1221      * 
1222      * @return
1223      */
1224     protected int getNavigationPageDownKey() {
1225         return KeyCodes.KEY_PAGEDOWN;
1226     }
1227 
1228     /**
1229      * Get the key the moves the selection to the beginning of the table. By
1230      * default this is the Home key but by overriding this you can change the
1231      * key to whatever you want.
1232      * 
1233      * @return
1234      */
1235     protected int getNavigationStartKey() {
1236         return KeyCodes.KEY_HOME;
1237     }
1238 
1239     /**
1240      * Get the key the moves the selection to the end of the table. By default
1241      * this is the End key but by overriding this you can change the key to
1242      * whatever you want.
1243      * 
1244      * @return
1245      */
1246     protected int getNavigationEndKey() {
1247         return KeyCodes.KEY_END;
1248     }
1249 
1250     /** For internal use only. May be removed or replaced in the future. */
1251     public void initializeRows(UIDL uidl, UIDL rowData) {
1252         if (scrollBody != null) {
1253             scrollBody.removeFromParent();
1254         }
1255 
1256         // Without this call the scroll position is messed up in IE even after
1257         // the lazy scroller has set the scroll position to the first visible
1258         // item
1259         int pos = scrollBodyPanel.getScrollPosition();
1260 
1261         // Reset first row in view port so client requests correct last row.
1262         if (pos == 0) {
1263             firstRowInViewPort = 0;
1264         }
1265 
1266         scrollBody = createScrollBody();
1267 
1268         scrollBody.renderInitialRows(rowData, uidl.getIntAttribute("firstrow"),
1269                 uidl.getIntAttribute("rows"));
1270         scrollBodyPanel.add(scrollBody);
1271 
1272         // New body starts scrolled to the left, make sure the header and footer
1273         // are also scrolled to the left
1274         tHead.setHorizontalScrollPosition(0);
1275         tFoot.setHorizontalScrollPosition(0);
1276 
1277         initialContentReceived = true;
1278         sizeNeedsInit = true;
1279         scrollBody.restoreRowVisibility();
1280 
1281         // scroll to selection
1282         if (scrollBodyPanel != null) {
1283             VScrollTableRow row;
1284             if (selectedRowKeys.size() == 1) {
1285                 String selectedRowKey = selectedRowKeys.iterator().next();
1286                 row = getRenderedRowByKey(selectedRowKey);
1287             } else {
1288                 row = (VScrollTableRow) scrollBody.iterator().next();
1289             }
1290             lazyRevertFocusToRow(row);
1291         }
1292     }
1293 
1294     /** For internal use only. May be removed or replaced in the future. */
1295     public void updateColumnProperties(UIDL uidl) {
1296         updateColumnOrder(uidl);
1297 
1298         updateCollapsedColumns(uidl);
1299 
1300         UIDL vc = uidl.getChildByTagName("visiblecolumns");
1301         if (vc != null) {
1302             tHead.updateCellsFromUIDL(vc);
1303             tFoot.updateCellsFromUIDL(vc);
1304         }
1305 
1306         updateHeader(uidl.getStringArrayAttribute("vcolorder"));
1307         updateFooter(uidl.getStringArrayAttribute("vcolorder"));
1308         if (uidl.hasVariable("noncollapsiblecolumns")) {
1309             noncollapsibleColumns = uidl
1310                     .getStringArrayVariableAsSet("noncollapsiblecolumns");
1311         }
1312     }
1313 
1314     private void updateCollapsedColumns(UIDL uidl) {
1315         if (uidl.hasVariable("collapsedcolumns")) {
1316             tHead.setColumnCollapsingAllowed(true);
1317             collapsedColumns = uidl
1318                     .getStringArrayVariableAsSet("collapsedcolumns");
1319         } else {
1320             tHead.setColumnCollapsingAllowed(false);
1321         }
1322     }
1323 
1324     private void updateColumnOrder(UIDL uidl) {
1325         if (uidl.hasVariable("columnorder")) {
1326             columnReordering = true;
1327             columnOrder = uidl.getStringArrayVariable("columnorder");
1328         } else {
1329             columnReordering = false;
1330             columnOrder = null;
1331         }
1332     }
1333 
1334     /** For internal use only. May be removed or replaced in the future. */
1335     public boolean selectSelectedRows(UIDL uidl) {
1336         boolean keyboardSelectionOverRowFetchInProgress = false;
1337 
1338         if (uidl.hasVariable("selected")) {
1339             final Set<String> selectedKeys = uidl
1340                     .getStringArrayVariableAsSet("selected");
1341             removeUnselectedRowKeys(selectedKeys);
1342 
1343             if (scrollBody != null) {
1344                 Iterator<Widget> iterator = scrollBody.iterator();
1345                 while (iterator.hasNext()) {
1346                     /*
1347                      * Make the focus reflect to the server side state unless we
1348                      * are currently selecting multiple rows with keyboard.
1349                      */
1350                     VScrollTableRow row = (VScrollTableRow) iterator.next();
1351                     boolean selected = selectedKeys.contains(row.getKey());
1352                     if (!selected
1353                             && unSyncedselectionsBeforeRowFetch != null
1354                             && unSyncedselectionsBeforeRowFetch.contains(row
1355                                     .getKey())) {
1356                         selected = true;
1357                         keyboardSelectionOverRowFetchInProgress = true;
1358                     }
1359                     if (selected && selectedKeys.size() == 1) {
1360                         /*
1361                          * If a single item is selected, move focus to the
1362                          * selected row. (#10522)
1363                          */
1364                         setRowFocus(row);
1365                     }
1366 
1367                     if (selected != row.isSelected()) {
1368                         row.toggleSelection();
1369 
1370                         if (!isSingleSelectMode() && !selected) {
1371                             // Update selection range in case a row is
1372                             // unselected from the middle of a range - #8076
1373                             removeRowFromUnsentSelectionRanges(row);
1374                         }
1375                     }
1376                 }
1377 
1378             }
1379         }
1380         unSyncedselectionsBeforeRowFetch = null;
1381         return keyboardSelectionOverRowFetchInProgress;
1382     }
1383 
1384     private void removeUnselectedRowKeys(final Set<String> selectedKeys) {
1385         List<String> unselectedKeys = new ArrayList<String>(0);
1386         for (String key : selectedRowKeys) {
1387             if (!selectedKeys.contains(key)) {
1388                 unselectedKeys.add(key);
1389             }
1390         }
1391         selectedRowKeys.removeAll(unselectedKeys);
1392     }
1393 
1394     /** For internal use only. May be removed or replaced in the future. */
1395     public void updateSortingProperties(UIDL uidl) {
1396         oldSortColumn = sortColumn;
1397         if (uidl.hasVariable("sortascending")) {
1398             sortAscending = uidl.getBooleanVariable("sortascending");
1399             sortColumn = uidl.getStringVariable("sortcolumn");
1400         }
1401     }
1402 
1403     /** For internal use only. May be removed or replaced in the future. */
1404     public void resizeSortedColumnForSortIndicator() {
1405         // Force recalculation of the captionContainer element inside the header
1406         // cell to accomodate for the size of the sort arrow.
1407         HeaderCell sortedHeader = tHead.getHeaderCell(sortColumn);
1408         if (sortedHeader != null) {
1409             // Mark header as sorted now. Any earlier marking would lead to
1410             // columns with wrong sizes
1411             sortedHeader.setSorted(true);
1412             tHead.resizeCaptionContainer(sortedHeader);
1413         }
1414         // Also recalculate the width of the captionContainer element in the
1415         // previously sorted header, since this now has more room.
1416         HeaderCell oldSortedHeader = tHead.getHeaderCell(oldSortColumn);
1417         if (oldSortedHeader != null) {
1418             tHead.resizeCaptionContainer(oldSortedHeader);
1419         }
1420     }
1421 
1422     private boolean lazyScrollerIsActive;
1423 
1424     private void disableLazyScroller() {
1425         lazyScrollerIsActive = false;
1426         scrollBodyPanel.getElement().getStyle().clearOverflowX();
1427         scrollBodyPanel.getElement().getStyle().clearOverflowY();
1428     }
1429 
1430     private void enableLazyScroller() {
1431         Scheduler.get().scheduleDeferred(lazyScroller);
1432         lazyScrollerIsActive = true;
1433         // prevent scrolling to jump in IE11
1434         scrollBodyPanel.getElement().getStyle().setOverflowX(Overflow.HIDDEN);
1435         scrollBodyPanel.getElement().getStyle().setOverflowY(Overflow.HIDDEN);
1436     }
1437 
1438     private boolean isLazyScrollerActive() {
1439         return lazyScrollerIsActive;
1440     }
1441 
1442     private ScheduledCommand lazyScroller = new ScheduledCommand() {
1443 
1444         @Override
1445         public void execute() {
1446             if (firstvisible >= 0) {
1447                 firstRowInViewPort = firstvisible;
1448                 if (firstvisibleOnLastPage > -1) {
1449                     scrollBodyPanel
1450                             .setScrollPosition(measureRowHeightOffset(firstvisibleOnLastPage));
1451                 } else {
1452                     scrollBodyPanel
1453                             .setScrollPosition(measureRowHeightOffset(firstvisible));
1454                 }
1455             }
1456             disableLazyScroller();
1457         }
1458     };
1459 
1460     /** For internal use only. May be removed or replaced in the future. */
1461     public void updateFirstVisibleAndScrollIfNeeded(UIDL uidl) {
1462         firstvisible = uidl.hasVariable("firstvisible") ? uidl
1463                 .getIntVariable("firstvisible") : 0;
1464         firstvisibleOnLastPage = uidl.hasVariable("firstvisibleonlastpage") ? uidl
1465                 .getIntVariable("firstvisibleonlastpage") : -1;
1466         if (firstvisible != lastRequestedFirstvisible && scrollBody != null) {
1467 
1468             // Update lastRequestedFirstvisible right away here
1469             // (don't rely on update in the timer which could be cancelled).
1470             lastRequestedFirstvisible = firstRowInViewPort;
1471 
1472             // Only scroll if the first visible changes from the server side.
1473             // Else we might unintentionally scroll even when the scroll
1474             // position has not changed.
1475             enableLazyScroller();
1476         }
1477     }
1478 
1479     protected int measureRowHeightOffset(int rowIx) {
1480         return (int) (rowIx * scrollBody.getRowHeight());
1481     }
1482 
1483     /** For internal use only. May be removed or replaced in the future. */
1484     public void updatePageLength(UIDL uidl) {
1485         int oldPageLength = pageLength;
1486         if (uidl.hasAttribute("pagelength")) {
1487             pageLength = uidl.getIntAttribute("pagelength");
1488         } else {
1489             // pagelenght is "0" meaning scrolling is turned off
1490             pageLength = totalRows;
1491         }
1492 
1493         if (oldPageLength != pageLength && initializedAndAttached) {
1494             // page length changed, need to update size
1495             sizeNeedsInit = true;
1496         }
1497     }
1498 
1499     /** For internal use only. May be removed or replaced in the future. */
1500     public void updateSelectionProperties(UIDL uidl,
1501             AbstractComponentState state, boolean readOnly) {
1502         setMultiSelectMode(uidl.hasAttribute("multiselectmode") ? uidl
1503                 .getIntAttribute("multiselectmode") : MULTISELECT_MODE_DEFAULT);
1504 
1505         nullSelectionAllowed = uidl.hasAttribute("nsa") ? uidl
1506                 .getBooleanAttribute("nsa") : true;
1507 
1508         if (uidl.hasAttribute("selectmode")) {
1509             if (readOnly) {
1510                 selectMode = SelectMode.NONE;
1511             } else if (uidl.getStringAttribute("selectmode").equals("multi")) {
1512                 selectMode = SelectMode.MULTI;
1513             } else if (uidl.getStringAttribute("selectmode").equals("single")) {
1514                 selectMode = SelectMode.SINGLE;
1515             } else {
1516                 selectMode = SelectMode.NONE;
1517             }
1518         }
1519     }
1520 
1521     /** For internal use only. May be removed or replaced in the future. */
1522     public void updateDragMode(UIDL uidl) {
1523         dragmode = uidl.hasAttribute("dragmode") ? uidl
1524                 .getIntAttribute("dragmode") : 0;
1525         if (BrowserInfo.get().isIE()) {
1526             if (dragmode > 0) {
1527                 getElement().setPropertyJSO("onselectstart",
1528                         getPreventTextSelectionIEHack());
1529             } else {
1530                 getElement().setPropertyJSO("onselectstart", null);
1531             }
1532         }
1533     }
1534 
1535     /** For internal use only. May be removed or replaced in the future. */
1536     public void updateTotalRows(UIDL uidl) {
1537         int newTotalRows = uidl.getIntAttribute("totalrows");
1538         if (newTotalRows != getTotalRows()) {
1539             if (scrollBody != null) {
1540                 if (getTotalRows() == 0) {
1541                     tHead.clear();
1542                     tFoot.clear();
1543                 }
1544                 initializedAndAttached = false;
1545                 initialContentReceived = false;
1546                 isNewBody = true;
1547             }
1548             setTotalRows(newTotalRows);
1549         }
1550     }
1551 
1552     protected void setTotalRows(int newTotalRows) {
1553         totalRows = newTotalRows;
1554     }
1555 
1556     public int getTotalRows() {
1557         return totalRows;
1558     }
1559 
1560     /**
1561      * Returns the extra space that is given to the header column when column
1562      * width is determined by header text.
1563      * 
1564      * @return extra space in pixels
1565      */
1566     private int getHeaderPadding() {
1567         return scrollBody.getCellExtraWidth();
1568     }
1569 
1570     /**
1571      * This method exists for the needs of {@link VTreeTable} only. Not part of
1572      * the official API, <b>extend at your own risk</b>. May be removed or
1573      * replaced in the future.
1574      * 
1575      * @return index of TreeTable's hierarchy column, or -1 if not applicable
1576      */
1577     protected int getHierarchyColumnIndex() {
1578         return -1;
1579     }
1580 
1581     /**
1582      * For internal use only. May be removed or replaced in the future.
1583      */
1584     public void updateMaxIndent() {
1585         int oldIndent = scrollBody.getMaxIndent();
1586         scrollBody.calculateMaxIndent();
1587         if (oldIndent != scrollBody.getMaxIndent()) {
1588             // indent updated, headers might need adjusting
1589             triggerLazyColumnAdjustment(true);
1590         }
1591     }
1592 
1593     /** For internal use only. May be removed or replaced in the future. */
1594     public void focusRowFromBody() {
1595         if (selectedRowKeys.size() == 1) {
1596             // try to focus a row currently selected and in viewport
1597             String selectedRowKey = selectedRowKeys.iterator().next();
1598             if (selectedRowKey != null) {
1599                 VScrollTableRow renderedRow = getRenderedRowByKey(selectedRowKey);
1600                 if (renderedRow == null || !renderedRow.isInViewPort()) {
1601                     setRowFocus(scrollBody.getRowByRowIndex(firstRowInViewPort));
1602                 } else {
1603                     setRowFocus(renderedRow);
1604                 }
1605             }
1606         } else {
1607             // multiselect mode
1608             setRowFocus(scrollBody.getRowByRowIndex(firstRowInViewPort));
1609         }
1610     }
1611 
1612     protected VScrollTableBody createScrollBody() {
1613         return new VScrollTableBody();
1614     }
1615 
1616     /**
1617      * Selects the last row visible in the table
1618      * <p>
1619      * For internal use only. May be removed or replaced in the future.
1620      * 
1621      * @param focusOnly
1622      *            Should the focus only be moved to the last row
1623      */
1624     public void selectLastRenderedRowInViewPort(boolean focusOnly) {
1625         int index = firstRowInViewPort + getFullyVisibleRowCount();
1626         VScrollTableRow lastRowInViewport = scrollBody.getRowByRowIndex(index);
1627         if (lastRowInViewport == null) {
1628             // this should not happen in normal situations (white space at the
1629             // end of viewport). Select the last rendered as a fallback.
1630             lastRowInViewport = scrollBody.getRowByRowIndex(scrollBody
1631                     .getLastRendered());
1632             if (lastRowInViewport == null) {
1633                 return; // empty table
1634             }
1635         }
1636         setRowFocus(lastRowInViewport);
1637         if (!focusOnly) {
1638             selectFocusedRow(false, multiselectPending);
1639             sendSelectedRows();
1640         }
1641     }
1642 
1643     /**
1644      * Selects the first row visible in the table
1645      * <p>
1646      * For internal use only. May be removed or replaced in the future.
1647      * 
1648      * @param focusOnly
1649      *            Should the focus only be moved to the first row
1650      */
1651     public void selectFirstRenderedRowInViewPort(boolean focusOnly) {
1652         int index = firstRowInViewPort;
1653         VScrollTableRow firstInViewport = scrollBody.getRowByRowIndex(index);
1654         if (firstInViewport == null) {
1655             // this should not happen in normal situations
1656             return;
1657         }
1658         setRowFocus(firstInViewport);
1659         if (!focusOnly) {
1660             selectFocusedRow(false, multiselectPending);
1661             sendSelectedRows();
1662         }
1663     }
1664 
1665     /** For internal use only. May be removed or replaced in the future. */
1666     public void setCacheRateFromUIDL(UIDL uidl) {
1667         setCacheRate(uidl.hasAttribute("cr") ? uidl.getDoubleAttribute("cr")
1668                 : CACHE_RATE_DEFAULT);
1669     }
1670 
1671     private void setCacheRate(double d) {
1672         if (cache_rate != d) {
1673             cache_rate = d;
1674             cache_react_rate = 0.75 * d;
1675         }
1676     }
1677 
1678     /** For internal use only. May be removed or replaced in the future. */
1679     public void updateActionMap(UIDL mainUidl) {
1680         UIDL actionsUidl = mainUidl.getChildByTagName("actions");
1681         if (actionsUidl == null) {
1682             return;
1683         }
1684 
1685         final Iterator<?> it = actionsUidl.getChildIterator();
1686         while (it.hasNext()) {
1687             final UIDL action = (UIDL) it.next();
1688             final String key = action.getStringAttribute("key");
1689             final String caption = action.getStringAttribute("caption");
1690             actionMap.put(key + "_c", caption);
1691             if (action.hasAttribute("icon")) {
1692                 // TODO need some uri handling ??
1693                 actionMap.put(key + "_i", client.translateVaadinUri(action
1694                         .getStringAttribute("icon")));
1695             } else {
1696                 actionMap.remove(key + "_i");
1697             }
1698         }
1699 
1700     }
1701 
1702     public String getActionCaption(String actionKey) {
1703         return actionMap.get(actionKey + "_c");
1704     }
1705 
1706     public String getActionIcon(String actionKey) {
1707         return actionMap.get(actionKey + "_i");
1708     }
1709 
1710     private void updateHeader(String[] strings) {
1711         if (strings == null) {
1712             return;
1713         }
1714 
1715         int visibleCols = strings.length;
1716         int colIndex = 0;
1717         if (showRowHeaders) {
1718             tHead.enableColumn(ROW_HEADER_COLUMN_KEY, colIndex);
1719             visibleCols++;
1720             visibleColOrder = new String[visibleCols];
1721             visibleColOrder[colIndex] = ROW_HEADER_COLUMN_KEY;
1722             colIndex++;
1723         } else {
1724             visibleColOrder = new String[visibleCols];
1725             tHead.removeCell(ROW_HEADER_COLUMN_KEY);
1726         }
1727 
1728         int i;
1729         for (i = 0; i < strings.length; i++) {
1730             final String cid = strings[i];
1731             visibleColOrder[colIndex] = cid;
1732             tHead.enableColumn(cid, colIndex);
1733             colIndex++;
1734         }
1735 
1736         tHead.setVisible(showColHeaders);
1737         setContainerHeight();
1738 
1739     }
1740 
1741     /**
1742      * Updates footers.
1743      * <p>
1744      * Update headers whould be called before this method is called!
1745      * </p>
1746      * 
1747      * @param strings
1748      */
1749     private void updateFooter(String[] strings) {
1750         if (strings == null) {
1751             return;
1752         }
1753 
1754         // Add dummy column if row headers are present
1755         int colIndex = 0;
1756         if (showRowHeaders) {
1757             tFoot.enableColumn(ROW_HEADER_COLUMN_KEY, colIndex);
1758             colIndex++;
1759         } else {
1760             tFoot.removeCell(ROW_HEADER_COLUMN_KEY);
1761         }
1762 
1763         int i;
1764         for (i = 0; i < strings.length; i++) {
1765             final String cid = strings[i];
1766             tFoot.enableColumn(cid, colIndex);
1767             colIndex++;
1768         }
1769 
1770         tFoot.setVisible(showColFooters);
1771     }
1772 
1773     /**
1774      * For internal use only. May be removed or replaced in the future.
1775      * 
1776      * @param uidl
1777      *            which contains row data
1778      * @param firstRow
1779      *            first row in data set
1780      * @param reqRows
1781      *            amount of rows in data set
1782      */
1783     public void updateBody(UIDL uidl, int firstRow, int reqRows) {
1784         int oldIndent = scrollBody.getMaxIndent();
1785         if (uidl == null || reqRows < 1) {
1786             // container is empty, remove possibly existing rows
1787             if (firstRow <= 0) {
1788                 postponeSanityCheckForLastRendered = true;
1789                 while (scrollBody.getLastRendered() > scrollBody
1790                         .getFirstRendered()) {
1791                     scrollBody.unlinkRow(false);
1792                 }
1793                 postponeSanityCheckForLastRendered = false;
1794                 scrollBody.unlinkRow(false);
1795             }
1796             return;
1797         }
1798 
1799         scrollBody.renderRows(uidl, firstRow, reqRows);
1800 
1801         discardRowsOutsideCacheWindow();
1802         scrollBody.calculateMaxIndent();
1803         if (oldIndent != scrollBody.getMaxIndent()) {
1804             // indent updated, headers might need adjusting
1805             headerChangedDuringUpdate = true;
1806         }
1807     }
1808 
1809     /** For internal use only. May be removed or replaced in the future. */
1810     public void updateRowsInBody(UIDL partialRowUpdates) {
1811         if (partialRowUpdates == null) {
1812             return;
1813         }
1814         int firstRowIx = partialRowUpdates.getIntAttribute("firsturowix");
1815         int count = partialRowUpdates.getIntAttribute("numurows");
1816         scrollBody.unlinkRows(firstRowIx, count);
1817         scrollBody.insertRows(partialRowUpdates, firstRowIx, count);
1818     }
1819 
1820     /**
1821      * Updates the internal cache by unlinking rows that fall outside of the
1822      * caching window.
1823      */
1824     protected void discardRowsOutsideCacheWindow() {
1825         int firstRowToKeep = (int) (firstRowInViewPort - pageLength
1826                 * cache_rate);
1827         int lastRowToKeep = (int) (firstRowInViewPort + pageLength + pageLength
1828                 * cache_rate);
1829         // sanity checks:
1830         if (firstRowToKeep < 0) {
1831             firstRowToKeep = 0;
1832         }
1833         if (lastRowToKeep > totalRows) {
1834             lastRowToKeep = totalRows - 1;
1835         }
1836         debug("Client side calculated cache rows to keep: " + firstRowToKeep
1837                 + "-" + lastRowToKeep);
1838 
1839         if (serverCacheFirst != -1) {
1840             firstRowToKeep = serverCacheFirst;
1841             lastRowToKeep = serverCacheLast;
1842             debug("Server cache rows that override: " + serverCacheFirst + "-"
1843                     + serverCacheLast);
1844             if (firstRowToKeep < scrollBody.getFirstRendered()
1845                     || lastRowToKeep > scrollBody.getLastRendered()) {
1846                 debug("*** Server wants us to keep " + serverCacheFirst + "-"
1847                         + serverCacheLast + " but we only have rows "
1848                         + scrollBody.getFirstRendered() + "-"
1849                         + scrollBody.getLastRendered() + " rendered!");
1850             }
1851         }
1852         discardRowsOutsideOf(firstRowToKeep, lastRowToKeep);
1853 
1854         scrollBody.fixSpacers();
1855 
1856         scrollBody.restoreRowVisibility();
1857     }
1858 
1859     private void discardRowsOutsideOf(int optimalFirstRow, int optimalLastRow) {
1860         /*
1861          * firstDiscarded and lastDiscarded are only calculated for debug
1862          * purposes
1863          */
1864         int firstDiscarded = -1, lastDiscarded = -1;
1865         boolean cont = true;
1866         while (cont && scrollBody.getLastRendered() > optimalFirstRow
1867                 && scrollBody.getFirstRendered() < optimalFirstRow) {
1868             if (firstDiscarded == -1) {
1869                 firstDiscarded = scrollBody.getFirstRendered();
1870             }
1871 
1872             // removing row from start
1873             cont = scrollBody.unlinkRow(true);
1874         }
1875         if (firstDiscarded != -1) {
1876             lastDiscarded = scrollBody.getFirstRendered() - 1;
1877             debug("Discarded rows " + firstDiscarded + "-" + lastDiscarded);
1878         }
1879         firstDiscarded = lastDiscarded = -1;
1880 
1881         cont = true;
1882         while (cont && scrollBody.getLastRendered() > optimalLastRow) {
1883             if (lastDiscarded == -1) {
1884                 lastDiscarded = scrollBody.getLastRendered();
1885             }
1886 
1887             // removing row from the end
1888             cont = scrollBody.unlinkRow(false);
1889         }
1890         if (lastDiscarded != -1) {
1891             firstDiscarded = scrollBody.getLastRendered() + 1;
1892             debug("Discarded rows " + firstDiscarded + "-" + lastDiscarded);
1893         }
1894 
1895         debug("Now in cache: " + scrollBody.getFirstRendered() + "-"
1896                 + scrollBody.getLastRendered());
1897     }
1898 
1899     /**
1900      * Inserts rows in the table body or removes them from the table body based
1901      * on the commands in the UIDL.
1902      * <p>
1903      * For internal use only. May be removed or replaced in the future.
1904      * 
1905      * @param partialRowAdditions
1906      *            the UIDL containing row updates.
1907      */
1908     public void addAndRemoveRows(UIDL partialRowAdditions) {
1909         if (partialRowAdditions == null) {
1910             return;
1911         }
1912         if (partialRowAdditions.hasAttribute("hide")) {
1913             scrollBody.unlinkAndReindexRows(
1914                     partialRowAdditions.getIntAttribute("firstprowix"),
1915                     partialRowAdditions.getIntAttribute("numprows"));
1916             scrollBody.ensureCacheFilled();
1917         } else {
1918             if (partialRowAdditions.hasAttribute("delbelow")) {
1919                 scrollBody.insertRowsDeleteBelow(partialRowAdditions,
1920                         partialRowAdditions.getIntAttribute("firstprowix"),
1921                         partialRowAdditions.getIntAttribute("numprows"));
1922             } else {
1923                 scrollBody.insertAndReindexRows(partialRowAdditions,
1924                         partialRowAdditions.getIntAttribute("firstprowix"),
1925                         partialRowAdditions.getIntAttribute("numprows"));
1926             }
1927         }
1928 
1929         discardRowsOutsideCacheWindow();
1930     }
1931 
1932     /**
1933      * Gives correct column index for given column key ("cid" in UIDL).
1934      * 
1935      * @param colKey
1936      * @return column index of visible columns, -1 if column not visible
1937      */
1938     private int getColIndexByKey(String colKey) {
1939         // return 0 if asked for rowHeaders
1940         if (ROW_HEADER_COLUMN_KEY.equals(colKey)) {
1941             return 0;
1942         }
1943         for (int i = 0; i < visibleColOrder.length; i++) {
1944             if (visibleColOrder[i].equals(colKey)) {
1945                 return i;
1946             }
1947         }
1948         return -1;
1949     }
1950 
1951     protected boolean isMultiSelectModeSimple() {
1952         return selectMode == SelectMode.MULTI
1953                 && multiselectmode == MULTISELECT_MODE_SIMPLE;
1954     }
1955 
1956     protected boolean isSingleSelectMode() {
1957         return selectMode == SelectMode.SINGLE;
1958     }
1959 
1960     protected boolean isMultiSelectModeAny() {
1961         return selectMode == SelectMode.MULTI;
1962     }
1963 
1964     protected boolean isMultiSelectModeDefault() {
1965         return selectMode == SelectMode.MULTI
1966                 && multiselectmode == MULTISELECT_MODE_DEFAULT;
1967     }
1968 
1969     protected void setMultiSelectMode(int multiselectmode) {
1970         if (BrowserInfo.get().isTouchDevice()) {
1971             // Always use the simple mode for touch devices that do not have
1972             // shift/ctrl keys
1973             this.multiselectmode = MULTISELECT_MODE_SIMPLE;
1974         } else {
1975             this.multiselectmode = multiselectmode;
1976         }
1977 
1978     }
1979 
1980     /** For internal use only. May be removed or replaced in the future. */
1981     public boolean isSelectable() {
1982         return selectMode.getId() > SelectMode.NONE.getId();
1983     }
1984 
1985     private boolean isCollapsedColumn(String colKey) {
1986         if (collapsedColumns == null) {
1987             return false;
1988         }
1989         if (collapsedColumns.contains(colKey)) {
1990             return true;
1991         }
1992         return false;
1993     }
1994 
1995     private String getColKeyByIndex(int index) {
1996         return tHead.getHeaderCell(index).getColKey();
1997     }
1998 
1999     /**
2000      * Note: not part of the official API, extend at your own risk. May be
2001      * removed or replaced in the future.
2002      * 
2003      * Sets the indicated column's width for headers and scrollBody alike.
2004      * 
2005      * @param colIndex
2006      *            index of the modified column
2007      * @param w
2008      *            new width (may be subject to modifications if doesn't meet
2009      *            minimum requirements)
2010      * @param isDefinedWidth
2011      *            disables expand ratio if set true
2012      */
2013     protected void setColWidth(int colIndex, int w, boolean isDefinedWidth) {
2014         final HeaderCell hcell = tHead.getHeaderCell(colIndex);
2015 
2016         // Make sure that the column grows to accommodate the sort indicator if
2017         // necessary.
2018         // get min width with no indent or padding
2019         int minWidth = hcell.getMinWidth(false, false);
2020         if (w < minWidth) {
2021             w = minWidth;
2022         }
2023 
2024         // Set header column width WITHOUT INDENT
2025         hcell.setWidth(w, isDefinedWidth);
2026 
2027         // Set footer column width likewise
2028         FooterCell fcell = tFoot.getFooterCell(colIndex);
2029         fcell.setWidth(w, isDefinedWidth);
2030 
2031         // Ensure indicators have been taken into account
2032         tHead.resizeCaptionContainer(hcell);
2033 
2034         // Make sure that the body column grows to accommodate the indent if
2035         // necessary.
2036         // get min width with indent, no padding
2037         minWidth = hcell.getMinWidth(true, false);
2038         if (w < minWidth) {
2039             w = minWidth;
2040         }
2041 
2042         // Set body column width
2043         scrollBody.setColWidth(colIndex, w);
2044     }
2045 
2046     private int getColWidth(String colKey) {
2047         return tHead.getHeaderCell(colKey).getWidthWithIndent();
2048     }
2049 
2050     /**
2051      * Get a rendered row by its key
2052      * 
2053      * @param key
2054      *            The key to search with
2055      * @return
2056      */
2057     public VScrollTableRow getRenderedRowByKey(String key) {
2058         if (scrollBody != null) {
2059             final Iterator<Widget> it = scrollBody.iterator();
2060             VScrollTableRow r = null;
2061             while (it.hasNext()) {
2062                 r = (VScrollTableRow) it.next();
2063                 if (r.getKey().equals(key)) {
2064                     return r;
2065                 }
2066             }
2067         }
2068         return null;
2069     }
2070 
2071     /**
2072      * Returns the next row to the given row
2073      * 
2074      * @param row
2075      *            The row to calculate from
2076      * 
2077      * @return The next row or null if no row exists
2078      */
2079     private VScrollTableRow getNextRow(VScrollTableRow row, int offset) {
2080         final Iterator<Widget> it = scrollBody.iterator();
2081         VScrollTableRow r = null;
2082         while (it.hasNext()) {
2083             r = (VScrollTableRow) it.next();
2084             if (r == row) {
2085                 r = null;
2086                 while (offset >= 0 && it.hasNext()) {
2087                     r = (VScrollTableRow) it.next();
2088                     offset--;
2089                 }
2090                 return r;
2091             }
2092         }
2093 
2094         return null;
2095     }
2096 
2097     /**
2098      * Returns the previous row from the given row
2099      * 
2100      * @param row
2101      *            The row to calculate from
2102      * @return The previous row or null if no row exists
2103      */
2104     private VScrollTableRow getPreviousRow(VScrollTableRow row, int offset) {
2105         final Iterator<Widget> it = scrollBody.iterator();
2106         final Iterator<Widget> offsetIt = scrollBody.iterator();
2107         VScrollTableRow r = null;
2108         VScrollTableRow prev = null;
2109         while (it.hasNext()) {
2110             r = (VScrollTableRow) it.next();
2111             if (offset < 0) {
2112                 prev = (VScrollTableRow) offsetIt.next();
2113             }
2114             if (r == row) {
2115                 return prev;
2116             }
2117             offset--;
2118         }
2119 
2120         return null;
2121     }
2122 
2123     protected void reOrderColumn(String columnKey, int newIndex) {
2124 
2125         final int oldIndex = getColIndexByKey(columnKey);
2126 
2127         // Change header order
2128         tHead.moveCell(oldIndex, newIndex);
2129 
2130         // Change body order
2131         scrollBody.moveCol(oldIndex, newIndex);
2132 
2133         // Change footer order
2134         tFoot.moveCell(oldIndex, newIndex);
2135 
2136         /*
2137          * Build new columnOrder and update it to server Note that columnOrder
2138          * also contains collapsed columns so we cannot directly build it from
2139          * cells vector Loop the old columnOrder and append in order to new
2140          * array unless on moved columnKey. On new index also put the moved key
2141          * i == index on columnOrder, j == index on newOrder
2142          */
2143         final String oldKeyOnNewIndex = visibleColOrder[newIndex];
2144         if (showRowHeaders) {
2145             newIndex--; // columnOrder don't have rowHeader
2146         }
2147         // add back hidden rows,
2148         for (int i = 0; i < columnOrder.length; i++) {
2149             if (columnOrder[i].equals(oldKeyOnNewIndex)) {
2150                 break; // break loop at target
2151             }
2152             if (isCollapsedColumn(columnOrder[i])) {
2153                 newIndex++;
2154             }
2155         }
2156         // finally we can build the new columnOrder for server
2157         final String[] newOrder = new String[columnOrder.length];
2158         for (int i = 0, j = 0; j < newOrder.length; i++) {
2159             if (j == newIndex) {
2160                 newOrder[j] = columnKey;
2161                 j++;
2162             }
2163             if (i == columnOrder.length) {
2164                 break;
2165             }
2166             if (columnOrder[i].equals(columnKey)) {
2167                 continue;
2168             }
2169             newOrder[j] = columnOrder[i];
2170             j++;
2171         }
2172         columnOrder = newOrder;
2173         // also update visibleColumnOrder
2174         int i = showRowHeaders ? 1 : 0;
2175         for (int j = 0; j < newOrder.length; j++) {
2176             final String cid = newOrder[j];
2177             if (!isCollapsedColumn(cid)) {
2178                 visibleColOrder[i++] = cid;
2179             }
2180         }
2181         client.updateVariable(paintableId, "columnorder", columnOrder, false);
2182         if (client.hasEventListeners(this,
2183                 TableConstants.COLUMN_REORDER_EVENT_ID)) {
2184             client.sendPendingVariableChanges();
2185         }
2186     }
2187 
2188     @Override
2189     protected void onDetach() {
2190         detachedScrollPosition = scrollBodyPanel.getScrollPosition();
2191         rowRequestHandler.cancel();
2192         super.onDetach();
2193         // ensure that scrollPosElement will be detached
2194         if (scrollPositionElement != null) {
2195             final Element parent = DOM.getParent(scrollPositionElement);
2196             if (parent != null) {
2197                 DOM.removeChild(parent, scrollPositionElement);
2198             }
2199         }
2200     }
2201 
2202     @Override
2203     public void onAttach() {
2204         super.onAttach();
2205         // scrollBodyPanel.setScrollPosition(detachedScrollPosition);
2206     }
2207 
2208     /**
2209      * Run only once when component is attached and received its initial
2210      * content. This function:
2211      * 
2212      * * Syncs headers and bodys "natural widths and saves the values.
2213      * 
2214      * * Sets proper width and height
2215      * 
2216      * * Makes deferred request to get some cache rows
2217      * 
2218      * For internal use only. May be removed or replaced in the future.
2219      */
2220     public void sizeInit() {
2221         if (!isVisibleInHierarchy()) {
2222             return;
2223         }
2224 
2225         sizeNeedsInit = false;
2226 
2227         scrollBody.setContainerHeight();
2228 
2229         /*
2230          * We will use browsers table rendering algorithm to find proper column
2231          * widths. If content and header take less space than available, we will
2232          * divide extra space relatively to each column which has not width set.
2233          * 
2234          * Overflow pixels are added to last column.
2235          */
2236 
2237         Iterator<Widget> headCells = tHead.iterator();
2238         Iterator<Widget> footCells = tFoot.iterator();
2239         int i = 0;
2240         int totalExplicitColumnsWidths = 0;
2241         int total = 0;
2242         float expandRatioDivider = 0;
2243 
2244         final int[] widths = new int[tHead.visibleCells.size()];
2245 
2246         tHead.enableBrowserIntelligence();
2247         tFoot.enableBrowserIntelligence();
2248 
2249         int hierarchyColumnIndent = scrollBody != null ? scrollBody
2250                 .getMaxIndent() : 0;
2251         HeaderCell hierarchyHeaderWithExpandRatio = null;
2252 
2253         // first loop: collect natural widths
2254         while (headCells.hasNext()) {
2255             final HeaderCell hCell = (HeaderCell) headCells.next();
2256             final FooterCell fCell = (FooterCell) footCells.next();
2257             boolean needsIndent = hierarchyColumnIndent > 0
2258                     && hCell.isHierarchyColumn();
2259             hCell.saveNaturalColumnWidthIfNotSaved(i);
2260             fCell.saveNaturalColumnWidthIfNotSaved(i);
2261             int w = hCell.getWidth();
2262             if (hCell.isDefinedWidth()) {
2263                 // server has defined column width explicitly
2264                 if (needsIndent && w < hierarchyColumnIndent) {
2265                     // hierarchy indent overrides explicitly set width
2266                     w = hierarchyColumnIndent;
2267                 }
2268                 totalExplicitColumnsWidths += w;
2269             } else {
2270                 if (hCell.getExpandRatio() > 0) {
2271                     expandRatioDivider += hCell.getExpandRatio();
2272                     w = 0;
2273                     if (needsIndent && w < hierarchyColumnIndent) {
2274                         hierarchyHeaderWithExpandRatio = hCell;
2275                         // don't add to widths here, because will be included in
2276                         // the expand ratio space if there's enough of it
2277                     }
2278                 } else {
2279                     // get and store greater of header width and column width,
2280                     // and store it as a minimum natural column width (these
2281                     // already contain the indent if any)
2282                     int headerWidth = hCell.getNaturalColumnWidth(i);
2283                     int footerWidth = fCell.getNaturalColumnWidth(i);
2284                     w = headerWidth > footerWidth ? headerWidth : footerWidth;
2285                 }
2286                 if (w != 0) {
2287                     hCell.setNaturalMinimumColumnWidth(w);
2288                     fCell.setNaturalMinimumColumnWidth(w);
2289                 }
2290             }
2291             widths[i] = w;
2292             total += w;
2293             i++;
2294         }
2295         if (hierarchyHeaderWithExpandRatio != null) {
2296             total += hierarchyColumnIndent;
2297         }
2298 
2299         tHead.disableBrowserIntelligence();
2300         tFoot.disableBrowserIntelligence();
2301 
2302         boolean willHaveScrollbarz = willHaveScrollbars();
2303 
2304         // fix "natural" width if width not set
2305         if (isDynamicWidth()) {
2306             int w = total;
2307             w += scrollBody.getCellExtraWidth() * visibleColOrder.length;
2308             if (willHaveScrollbarz) {
2309                 w += WidgetUtil.getNativeScrollbarSize();
2310             }
2311             setContentWidth(w);
2312         }
2313 
2314         int availW = scrollBody.getAvailableWidth();
2315         if (BrowserInfo.get().isIE()) {
2316             // Hey IE, are you really sure about this?
2317             availW = scrollBody.getAvailableWidth();
2318         }
2319         availW -= scrollBody.getCellExtraWidth() * visibleColOrder.length;
2320 
2321         if (willHaveScrollbarz) {
2322             availW -= WidgetUtil.getNativeScrollbarSize();
2323         }
2324 
2325         // TODO refactor this code to be the same as in resize timer
2326 
2327         if (availW > total) {
2328             // natural size is smaller than available space
2329             int extraSpace = availW - total;
2330             if (hierarchyHeaderWithExpandRatio != null) {
2331                 /*
2332                  * add the indent's space back to ensure each column gets an
2333                  * even share according to the expand ratios (note: if the
2334                  * allocated space isn't enough for the hierarchy column it
2335                  * shall be treated like a defined width column and the indent
2336                  * space gets removed from the extra space again)
2337                  */
2338                 extraSpace += hierarchyColumnIndent;
2339             }
2340             final int totalWidthR = total - totalExplicitColumnsWidths;
2341             int checksum = 0;
2342 
2343             if (extraSpace == 1) {
2344                 // We cannot divide one single pixel so we give it the first
2345                 // undefined column
2346                 // no need to worry about indent here
2347                 headCells = tHead.iterator();
2348                 i = 0;
2349                 checksum = availW;
2350                 while (headCells.hasNext()) {
2351                     HeaderCell hc = (HeaderCell) headCells.next();
2352                     if (!hc.isDefinedWidth()) {
2353                         widths[i]++;
2354                         break;
2355                     }
2356                     i++;
2357                 }
2358 
2359             } else if (expandRatioDivider > 0) {
2360                 boolean setIndentToHierarchyHeader = false;
2361                 if (hierarchyHeaderWithExpandRatio != null) {
2362                     // ensure first that the hierarchyColumn gets at least the
2363                     // space allocated for indent
2364                     final int newSpace = Math
2365                             .round((extraSpace * (hierarchyHeaderWithExpandRatio
2366                                     .getExpandRatio() / expandRatioDivider)));
2367                     if (newSpace < hierarchyColumnIndent) {
2368                         // not enough space for indent, remove indent from the
2369                         // extraSpace again and handle hierarchy column's header
2370                         // separately
2371                         setIndentToHierarchyHeader = true;
2372                         extraSpace -= hierarchyColumnIndent;
2373                     }
2374                 }
2375 
2376                 // visible columns have some active expand ratios, excess
2377                 // space is divided according to them
2378                 headCells = tHead.iterator();
2379                 i = 0;
2380                 while (headCells.hasNext()) {
2381                     HeaderCell hCell = (HeaderCell) headCells.next();
2382                     if (hCell.getExpandRatio() > 0) {
2383                         int w = widths[i];
2384                         if (setIndentToHierarchyHeader
2385                                 && hierarchyHeaderWithExpandRatio.equals(hCell)) {
2386                             // hierarchy column's header is no longer part of
2387                             // the expansion divide and only gets indent
2388                             w += hierarchyColumnIndent;
2389                         } else {
2390                             final int newSpace = Math
2391                                     .round((extraSpace * (hCell
2392                                             .getExpandRatio() / expandRatioDivider)));
2393                             w += newSpace;
2394                         }
2395                         widths[i] = w;
2396                     }
2397                     checksum += widths[i];
2398                     i++;
2399                 }
2400             } else if (totalWidthR > 0) {
2401                 // no expand ratios defined, we will share extra space
2402                 // relatively to "natural widths" among those without
2403                 // explicit width
2404                 // no need to worry about indent here, it's already included
2405                 headCells = tHead.iterator();
2406                 i = 0;
2407                 while (headCells.hasNext()) {
2408                     HeaderCell hCell = (HeaderCell) headCells.next();
2409                     if (!hCell.isDefinedWidth()) {
2410                         int w = widths[i];
2411                         final int newSpace = Math.round((float) extraSpace
2412                                 * (float) w / totalWidthR);
2413                         w += newSpace;
2414                         widths[i] = w;
2415                     }
2416                     checksum += widths[i];
2417                     i++;
2418                 }
2419             }
2420 
2421             if (extraSpace > 0 && checksum != availW) {
2422                 /*
2423                  * There might be in some cases a rounding error of 1px when
2424                  * extra space is divided so if there is one then we give the
2425                  * first undefined column 1 more pixel
2426                  */
2427                 headCells = tHead.iterator();
2428                 i = 0;
2429                 while (headCells.hasNext()) {
2430                     HeaderCell hc = (HeaderCell) headCells.next();
2431                     if (!hc.isDefinedWidth()) {
2432                         widths[i] += availW - checksum;
2433                         break;
2434                     }
2435                     i++;
2436                 }
2437             }
2438 
2439         } else {
2440             // body's size will be more than available and scrollbar will appear
2441         }
2442 
2443         // last loop: set possibly modified values or reset if new tBody
2444         i = 0;
2445         headCells = tHead.iterator();
2446         while (headCells.hasNext()) {
2447             final HeaderCell hCell = (HeaderCell) headCells.next();
2448             // if (isNewBody || hCell.getWidth() == -1) {
2449                 final int w = widths[i];
2450                 setColWidth(i, w, false);
2451             // }
2452             i++;
2453         }
2454 
2455         initializedAndAttached = true;
2456 
2457         updatePageLength();
2458 
2459         /*
2460          * Fix "natural" height if height is not set. This must be after width
2461          * fixing so the components' widths have been adjusted.
2462          */
2463         if (isDynamicHeight()) {
2464             /*
2465              * We must force an update of the row height as this point as it
2466              * might have been (incorrectly) calculated earlier
2467              */
2468 
2469             /*
2470              * TreeTable updates stuff in a funky order, so we must set the
2471              * height as zero here before doing the real update to make it
2472              * realize that there is no content,
2473              */
2474             if (pageLength == totalRows && pageLength == 0) {
2475                 scrollBody.setHeight("0px");
2476             }
2477 
2478             int bodyHeight;
2479             if (pageLength == totalRows) {
2480                 /*
2481                  * A hack to support variable height rows when paging is off.
2482                  * Generally this is not supported by scrolltable. We want to
2483                  * show all rows so the bodyHeight should be equal to the table
2484                  * height.
2485                  */
2486                 // int bodyHeight = scrollBody.getOffsetHeight();
2487                 bodyHeight = scrollBody.getRequiredHeight();
2488             } else {
2489                 bodyHeight = (int) Math.round(scrollBody.getRowHeight(true)
2490                         * pageLength);
2491             }
2492             boolean needsSpaceForHorizontalSrollbar = (total > availW);
2493             if (needsSpaceForHorizontalSrollbar) {
2494                 bodyHeight += WidgetUtil.getNativeScrollbarSize();
2495             }
2496             scrollBodyPanel.setHeight(bodyHeight + "px");
2497             WidgetUtil.runWebkitOverflowAutoFix(scrollBodyPanel.getElement());
2498         }
2499 
2500         isNewBody = false;
2501 
2502         if (firstvisible > 0) {
2503             enableLazyScroller();
2504         }
2505 
2506         if (enabled) {
2507             // Do we need cache rows
2508             if (scrollBody.getLastRendered() + 1 < firstRowInViewPort
2509                     + pageLength + (int) cache_react_rate * pageLength) {
2510                 if (totalRows - 1 > scrollBody.getLastRendered()) {
2511                     // fetch cache rows
2512                     int firstInNewSet = scrollBody.getLastRendered() + 1;
2513                     int lastInNewSet = (int) (firstRowInViewPort + pageLength + cache_rate
2514                             * pageLength);
2515                     if (lastInNewSet > totalRows - 1) {
2516                         lastInNewSet = totalRows - 1;
2517                     }
2518                     rowRequestHandler.triggerRowFetch(firstInNewSet,
2519                             lastInNewSet - firstInNewSet + 1, 1);
2520                 }
2521             }
2522         }
2523 
2524         /*
2525          * Ensures the column alignments are correct at initial loading. <br/>
2526          * (child components widths are correct)
2527          */
2528         WidgetUtil.runWebkitOverflowAutoFixDeferred(scrollBodyPanel
2529                 .getElement());
2530 
2531         hadScrollBars = willHaveScrollbarz;
2532     }
2533 
2534     /**
2535      * Note: this method is not part of official API although declared as
2536      * protected. Extend at your own risk.
2537      * 
2538      * @return true if content area will have scrollbars visible.
2539      */
2540     protected boolean willHaveScrollbars() {
2541         if (isDynamicHeight()) {
2542             if (pageLength < totalRows) {
2543                 return true;
2544             }
2545         } else {
2546             int fakeheight = (int) Math.round(scrollBody.getRowHeight()
2547                     * totalRows);
2548             int availableHeight = scrollBodyPanel.getElement().getPropertyInt(
2549                     "clientHeight");
2550             if (fakeheight > availableHeight) {
2551                 return true;
2552             }
2553         }
2554         return false;
2555     }
2556 
2557     private void announceScrollPosition() {
2558         if (scrollPositionElement == null) {
2559             scrollPositionElement = DOM.createDiv();
2560             scrollPositionElement.setClassName(getStylePrimaryName()
2561                     + "-scrollposition");
2562             scrollPositionElement.getStyle().setPosition(Position.ABSOLUTE);
2563             scrollPositionElement.getStyle().setDisplay(Display.NONE);
2564             getElement().appendChild(scrollPositionElement);
2565         }
2566 
2567         Style style = scrollPositionElement.getStyle();
2568         style.setMarginLeft(getElement().getOffsetWidth() / 2 - 80, Unit.PX);
2569         style.setMarginTop(-scrollBodyPanel.getOffsetHeight(), Unit.PX);
2570 
2571         // indexes go from 1-totalRows, as rowheaders in index-mode indicate
2572         int last = (firstRowInViewPort + pageLength);
2573         if (last > totalRows) {
2574             last = totalRows;
2575         }
2576         scrollPositionElement.setInnerHTML("<span>" + (firstRowInViewPort + 1)
2577                 + " &ndash; " + (last) + "..." + "</span>");
2578         style.setDisplay(Display.BLOCK);
2579     }
2580 
2581     /** For internal use only. May be removed or replaced in the future. */
2582     public void hideScrollPositionAnnotation() {
2583         if (scrollPositionElement != null) {
2584             scrollPositionElement.getStyle().setDisplay(Display.NONE);
2585         }
2586     }
2587 
2588     /** For internal use only. May be removed or replaced in the future. */
2589     public boolean isScrollPositionVisible() {
2590         return scrollPositionElement != null
2591                 && !scrollPositionElement.getStyle().getDisplay()
2592                         .equals(Display.NONE.toString());
2593     }
2594 
2595     /** For internal use only. May be removed or replaced in the future. */
2596     public class RowRequestHandler extends Timer {
2597 
2598         private int reqFirstRow = 0;
2599         private int reqRows = 0;
2600         private boolean isRequestHandlerRunning = false;
2601 
2602         public void triggerRowFetch(int first, int rows) {
2603             setReqFirstRow(first);
2604             setReqRows(rows);
2605             deferRowFetch();
2606         }
2607 
2608         public void triggerRowFetch(int first, int rows, int delay) {
2609             setReqFirstRow(first);
2610             setReqRows(rows);
2611             deferRowFetch(delay);
2612         }
2613 
2614         public void deferRowFetch() {
2615             deferRowFetch(250);
2616         }
2617 
2618         public boolean isRequestHandlerRunning() {
2619             return isRequestHandlerRunning;
2620         }
2621 
2622         public void deferRowFetch(int msec) {
2623             isRequestHandlerRunning = true;
2624             if (reqRows > 0 && reqFirstRow < totalRows) {
2625                 schedule(msec);
2626 
2627                 // tell scroll position to user if currently "visible" rows are
2628                 // not rendered
2629                 if (totalRows > pageLength
2630                         && ((firstRowInViewPort + pageLength > scrollBody
2631                                 .getLastRendered()) || (firstRowInViewPort < scrollBody
2632                                 .getFirstRendered()))) {
2633                     announceScrollPosition();
2634                 } else {
2635                     hideScrollPositionAnnotation();
2636                 }
2637             }
2638         }
2639 
2640         public int getReqFirstRow() {
2641             return reqFirstRow;
2642         }
2643 
2644         public void setReqFirstRow(int reqFirstRow) {
2645             if (reqFirstRow < 0) {
2646                 this.reqFirstRow = 0;
2647             } else if (reqFirstRow >= totalRows) {
2648                 this.reqFirstRow = totalRows - 1;
2649             } else {
2650                 this.reqFirstRow = reqFirstRow;
2651             }
2652         }
2653 
2654         public void setReqRows(int reqRows) {
2655             if (reqRows < 0) {
2656                 this.reqRows = 0;
2657             } else if (reqFirstRow + reqRows > totalRows) {
2658                 this.reqRows = totalRows - reqFirstRow;
2659             } else {
2660                 this.reqRows = reqRows;
2661             }
2662         }
2663 
2664         @Override
2665         public void run() {
2666 
2667             if (client.hasActiveRequest() || navKeyDown) {
2668                 // if client connection is busy, don't bother loading it more
2669                 VConsole.log("Postponed rowfetch");
2670                 schedule(250);
2671             } else if (allRenderedRowsAreNew() && !updatedReqRows) {
2672 
2673                 /*
2674                  * If all rows are new, there might have been a server-side call
2675                  * to Table.setCurrentPageFirstItemIndex(int) In this case,
2676                  * scrolling event takes way too late, and all the rows from
2677                  * previous viewport to this one were requested.
2678                  * 
2679                  * This should prevent requesting unneeded rows by updating
2680                  * reqFirstRow and reqRows before needing them. See (#14135)
2681                  */
2682 
2683                 setReqFirstRow((firstRowInViewPort - (int) (pageLength * cache_rate)));
2684                 int last = firstRowInViewPort + (int) (cache_rate * pageLength)
2685                         + pageLength - 1;
2686                 if (last >= totalRows) {
2687                     last = totalRows - 1;
2688                 }
2689                 setReqRows(last - getReqFirstRow() + 1);
2690                 updatedReqRows = true;
2691                 schedule(250);
2692 
2693             } else {
2694 
2695                 int firstRendered = scrollBody.getFirstRendered();
2696                 int lastRendered = scrollBody.getLastRendered();
2697                 if (lastRendered > totalRows) {
2698                     lastRendered = totalRows - 1;
2699                 }
2700                 boolean rendered = firstRendered >= 0 && lastRendered >= 0;
2701 
2702                 int firstToBeRendered = firstRendered;
2703 
2704                 if (reqFirstRow < firstToBeRendered) {
2705                     firstToBeRendered = reqFirstRow;
2706                 } else if (firstRowInViewPort - (int) (cache_rate * pageLength) > firstToBeRendered) {
2707                     firstToBeRendered = firstRowInViewPort
2708                             - (int) (cache_rate * pageLength);
2709                     if (firstToBeRendered < 0) {
2710                         firstToBeRendered = 0;
2711                     }
2712                 } else if (rendered && firstRendered + 1 < reqFirstRow
2713                         && lastRendered + 1 < reqFirstRow) {
2714                     // requested rows must fall within the requested rendering
2715                     // area
2716                     firstToBeRendered = reqFirstRow;
2717                 }
2718                 if (firstToBeRendered + reqRows < firstRendered) {
2719                     // must increase the required row count accordingly,
2720                     // otherwise may leave a gap and the rows beyond will get
2721                     // removed
2722                     setReqRows(firstRendered - firstToBeRendered);
2723                 }
2724 
2725                 int lastToBeRendered = lastRendered;
2726                 int lastReqRow = reqFirstRow + reqRows - 1;
2727 
2728                 if (lastReqRow > lastToBeRendered) {
2729                     lastToBeRendered = lastReqRow;
2730                 } else if (firstRowInViewPort + pageLength + pageLength
2731                         * cache_rate < lastToBeRendered) {
2732                     lastToBeRendered = (firstRowInViewPort + pageLength + (int) (pageLength * cache_rate));
2733                     if (lastToBeRendered >= totalRows) {
2734                         lastToBeRendered = totalRows - 1;
2735                     }
2736                     // due Safari 3.1 bug (see #2607), verify reqrows, original
2737                     // problem unknown, but this should catch the issue
2738                     if (lastReqRow > lastToBeRendered) {
2739                         setReqRows(lastToBeRendered - reqFirstRow);
2740                     }
2741                 } else if (rendered && lastRendered - 1 > lastReqRow
2742                         && firstRendered - 1 > lastReqRow) {
2743                     // requested rows must fall within the requested rendering
2744                     // area
2745                     lastToBeRendered = lastReqRow;
2746                 }
2747 
2748                 if (lastToBeRendered > totalRows) {
2749                     lastToBeRendered = totalRows - 1;
2750                 }
2751                 if (reqFirstRow < firstToBeRendered
2752                         || (reqFirstRow > firstToBeRendered && (reqFirstRow < firstRendered || reqFirstRow > lastRendered + 1))) {
2753                     setReqFirstRow(firstToBeRendered);
2754                 }
2755                 if (lastRendered < lastToBeRendered
2756                         && lastRendered + reqRows < lastToBeRendered) {
2757                     // must increase the required row count accordingly,
2758                     // otherwise may leave a gap and the rows after will get
2759                     // removed
2760                     setReqRows(lastToBeRendered - lastRendered);
2761                 } else if (lastToBeRendered >= firstRendered
2762                         && reqFirstRow + reqRows < firstRendered) {
2763                     setReqRows(lastToBeRendered - lastRendered);
2764                 }
2765 
2766                 client.updateVariable(paintableId, "firstToBeRendered",
2767                         firstToBeRendered, false);
2768                 client.updateVariable(paintableId, "lastToBeRendered",
2769                         lastToBeRendered, false);
2770 
2771                 // don't request server to update page first index in case it
2772                 // has not been changed
2773                 if (firstRowInViewPort != firstvisible) {
2774                     // remember which firstvisible we requested, in case the
2775                     // server has a differing opinion
2776                     lastRequestedFirstvisible = firstRowInViewPort;
2777                     client.updateVariable(paintableId, "firstvisible",
2778                             firstRowInViewPort, false);
2779                 }
2780 
2781                 client.updateVariable(paintableId, "reqfirstrow", reqFirstRow,
2782                         false);
2783                 client.updateVariable(paintableId, "reqrows", reqRows, true);
2784 
2785                 if (selectionChanged) {
2786                     unSyncedselectionsBeforeRowFetch = new HashSet<Object>(
2787                             selectedRowKeys);
2788                 }
2789                 isRequestHandlerRunning = false;
2790             }
2791         }
2792 
2793         /**
2794          * Sends request to refresh content at this position.
2795          */
2796         public void refreshContent() {
2797             isRequestHandlerRunning = true;
2798             int first = (int) (firstRowInViewPort - pageLength * cache_rate);
2799             int reqRows = (int) (2 * pageLength * cache_rate + pageLength);
2800             if (first < 0) {
2801                 reqRows = reqRows + first;
2802                 first = 0;
2803             }
2804             setReqFirstRow(first);
2805             setReqRows(reqRows);
2806             run();
2807         }
2808     }
2809 
2810     public class HeaderCell extends Widget {
2811 
2812         Element td = DOM.createTD();
2813 
2814         protected Element captionContainer = DOM.createDiv();
2815 
2816         Element sortIndicator = DOM.createDiv();
2817 
2818         Element colResizeWidget = DOM.createDiv();
2819 
2820         Element floatingCopyOfHeaderCell;
2821 
2822         private boolean sortable = false;
2823         private final String cid;
2824 
2825         private boolean dragging;
2826         private Integer currentDragX = null; // is used to resolve #14796
2827 
2828         private int dragStartX;
2829         private int colIndex;
2830         private int originalWidth;
2831 
2832         private boolean isResizing;
2833 
2834         private int headerX;
2835 
2836         private boolean moved;
2837 
2838         private int closestSlot;
2839 
2840         private int width = -1;
2841 
2842         private int naturalWidth = -1;
2843 
2844         private char align = ALIGN_LEFT;
2845 
2846         boolean definedWidth = false;
2847 
2848         private float expandRatio = 0;
2849 
2850         private boolean sorted;
2851 
2852         public void setSortable(boolean b) {
2853             sortable = b;
2854         }
2855 
2856         /**
2857          * Makes room for the sorting indicator in case the column that the
2858          * header cell belongs to is sorted. This is done by resizing the width
2859          * of the caption container element by the correct amount
2860          */
2861         public void resizeCaptionContainer(int rightSpacing) {
2862             int captionContainerWidth = width
2863                     - colResizeWidget.getOffsetWidth() - rightSpacing;
2864 
2865             if (td.getClassName().contains("-asc")
2866                     || td.getClassName().contains("-desc")) {
2867                 // Leave room for the sort indicator
2868                 captionContainerWidth -= sortIndicator.getOffsetWidth();
2869             }
2870 
2871             if (captionContainerWidth < 0) {
2872                 rightSpacing += captionContainerWidth;
2873                 captionContainerWidth = 0;
2874             }
2875 
2876             captionContainer.getStyle().setPropertyPx("width",
2877                     captionContainerWidth);
2878 
2879             // Apply/Remove spacing if defined
2880             if (rightSpacing > 0) {
2881                 colResizeWidget.getStyle().setMarginLeft(rightSpacing, Unit.PX);
2882             } else {
2883                 colResizeWidget.getStyle().clearMarginLeft();
2884             }
2885         }
2886 
2887         public void setNaturalMinimumColumnWidth(int w) {
2888             naturalWidth = w;
2889         }
2890 
2891         public HeaderCell(String colId, String headerText) {
2892             cid = colId;
2893 
2894             setText(headerText);
2895 
2896             td.appendChild(colResizeWidget);
2897 
2898             // ensure no clipping initially (problem on column additions)
2899             captionContainer.getStyle().setOverflow(Overflow.VISIBLE);
2900 
2901             td.appendChild(sortIndicator);
2902             td.appendChild(captionContainer);
2903 
2904             DOM.sinkEvents(td, Event.MOUSEEVENTS | Event.ONDBLCLICK
2905                     | Event.ONCONTEXTMENU | Event.TOUCHEVENTS);
2906 
2907             setElement(td);
2908 
2909             setAlign(ALIGN_LEFT);
2910         }
2911 
2912         protected void updateStyleNames(String primaryStyleName) {
2913             colResizeWidget.setClassName(primaryStyleName + "-resizer");
2914             sortIndicator.setClassName(primaryStyleName + "-sort-indicator");
2915             captionContainer.setClassName(primaryStyleName
2916                     + "-caption-container");
2917             if (sorted) {
2918                 if (sortAscending) {
2919                     setStyleName(primaryStyleName + "-header-cell-asc");
2920                 } else {
2921                     setStyleName(primaryStyleName + "-header-cell-desc");
2922                 }
2923             } else {
2924                 setStyleName(primaryStyleName + "-header-cell");
2925             }
2926 
2927             final String ALIGN_PREFIX = primaryStyleName
2928                     + "-caption-container-align-";
2929 
2930             switch (align) {
2931             case ALIGN_CENTER:
2932                 captionContainer.addClassName(ALIGN_PREFIX + "center");
2933                 break;
2934             case ALIGN_RIGHT:
2935                 captionContainer.addClassName(ALIGN_PREFIX + "right");
2936                 break;
2937             default:
2938                 captionContainer.addClassName(ALIGN_PREFIX + "left");
2939                 break;
2940             }
2941 
2942         }
2943 
2944         public void disableAutoWidthCalculation() {
2945             definedWidth = true;
2946             expandRatio = 0;
2947         }
2948 
2949         /**
2950          * Sets width to the header cell. This width should not include any
2951          * possible indent modifications that are present in
2952          * {@link VScrollTableBody#getMaxIndent()}.
2953          * 
2954          * @param w
2955          *            required width of the cell sans indentations
2956          * @param ensureDefinedWidth
2957          *            disables expand ratio if required
2958          */
2959         public void setWidth(int w, boolean ensureDefinedWidth) {
2960             if (ensureDefinedWidth) {
2961                 definedWidth = true;
2962                 // on column resize expand ratio becomes zero
2963                 expandRatio = 0;
2964             }
2965             if (width == -1) {
2966                 // go to default mode, clip content if necessary
2967                 captionContainer.getStyle().clearOverflow();
2968             }
2969             width = w;
2970             if (w == -1) {
2971                 captionContainer.getStyle().clearWidth();
2972                 setWidth("");
2973             } else {
2974                 tHead.resizeCaptionContainer(this);
2975 
2976                 /*
2977                  * if we already have tBody, set the header width properly, if
2978                  * not defer it. IE will fail with complex float in table header
2979                  * unless TD width is not explicitly set.
2980                  */
2981                 if (scrollBody != null) {
2982                     int maxIndent = scrollBody.getMaxIndent();
2983                     if (w < maxIndent && isHierarchyColumn()) {
2984                         w = maxIndent;
2985                     }
2986                     int tdWidth = w + scrollBody.getCellExtraWidth();
2987                     setWidth(tdWidth + "px");
2988                 } else {
2989                     Scheduler.get().scheduleDeferred(new Command() {
2990 
2991                         @Override
2992                         public void execute() {
2993                             int maxIndent = scrollBody.getMaxIndent();
2994                             int tdWidth = width;
2995                             if (tdWidth < maxIndent && isHierarchyColumn()) {
2996                                 tdWidth = maxIndent;
2997                             }
2998                             tdWidth += scrollBody.getCellExtraWidth();
2999                             setWidth(tdWidth + "px");
3000                         }
3001                     });
3002                 }
3003             }
3004         }
3005 
3006         public void setUndefinedWidth() {
3007             definedWidth = false;
3008             if (!isResizing) {
3009                 setWidth(-1, false);
3010             }
3011         }
3012 
3013         private void setUndefinedWidthFlagOnly() {
3014             definedWidth = false;
3015         }
3016 
3017         /**
3018          * Detects if width is fixed by developer on server side or resized to
3019          * current width by user.
3020          * 
3021          * @return true if defined, false if "natural" width
3022          */
3023         public boolean isDefinedWidth() {
3024             return definedWidth && width >= 0;
3025         }
3026 
3027         /**
3028          * This method exists for the needs of {@link VTreeTable} only.
3029          * 
3030          * Returns the pixels width of the header cell. This includes the
3031          * indent, if applicable.
3032          * 
3033          * @return The width in pixels
3034          */
3035         protected int getWidthWithIndent() {
3036             if (scrollBody != null && isHierarchyColumn()) {
3037                 int maxIndent = scrollBody.getMaxIndent();
3038                 if (maxIndent > width) {
3039                     return maxIndent;
3040                 }
3041             }
3042             return width;
3043         }
3044 
3045         /**
3046          * Returns the pixels width of the header cell.
3047          * 
3048          * @return The width in pixels
3049          */
3050         public int getWidth() {
3051             return width;
3052         }
3053 
3054         /**
3055          * This method exists for the needs of {@link VTreeTable} only.
3056          * 
3057          * @return <code>true</code> if this is hierarcyColumn's header cell,
3058          *         <code>false</code> otherwise
3059          */
3060         private boolean isHierarchyColumn() {
3061             int hierarchyColumnIndex = getHierarchyColumnIndex();
3062             return hierarchyColumnIndex >= 0
3063                     && tHead.visibleCells.indexOf(this) == hierarchyColumnIndex;
3064         }
3065 
3066         public void setText(String headerText) {
3067             DOM.setInnerHTML(captionContainer, headerText);
3068         }
3069 
3070         public String getColKey() {
3071             return cid;
3072         }
3073 
3074         protected void setSorted(boolean sorted) {
3075             this.sorted = sorted;
3076             updateStyleNames(VScrollTablePatched.this.getStylePrimaryName());
3077         }
3078 
3079         /**
3080          * Handle column reordering.
3081          */
3082 
3083         @Override
3084         public void onBrowserEvent(Event event) {
3085             if (enabled && event != null) {
3086                 if (isResizing
3087                         || event.getEventTarget().cast() == colResizeWidget) {
3088                     if (dragging
3089                             && (event.getTypeInt() == Event.ONMOUSEUP || event
3090                                     .getTypeInt() == Event.ONTOUCHEND)) {
3091                         // Handle releasing column header on spacer #5318
3092                         handleCaptionEvent(event);
3093                     } else {
3094                         onResizeEvent(event);
3095                     }
3096                 } else {
3097                     /*
3098                      * Ensure focus before handling caption event. Otherwise
3099                      * variables changed from caption event may be before
3100                      * variables from other components that fire variables when
3101                      * they lose focus.
3102                      */
3103                     if (event.getTypeInt() == Event.ONMOUSEDOWN
3104                             || event.getTypeInt() == Event.ONTOUCHSTART) {
3105                         scrollBodyPanel.setFocus(true);
3106                     }
3107                     handleCaptionEvent(event);
3108                     boolean stopPropagation = true;
3109                     if (event.getTypeInt() == Event.ONCONTEXTMENU
3110                             && !client.hasEventListeners(VScrollTablePatched.this,
3111                                     TableConstants.HEADER_CLICK_EVENT_ID)) {
3112                         // Prevent showing the browser's context menu only when
3113                         // there is a header click listener.
3114                         stopPropagation = false;
3115                     }
3116                     if (stopPropagation) {
3117                         event.stopPropagation();
3118                         event.preventDefault();
3119                     }
3120                 }
3121             }
3122         }
3123 
3124         private void createFloatingCopy() {
3125             floatingCopyOfHeaderCell = DOM.createDiv();
3126             DOM.setInnerHTML(floatingCopyOfHeaderCell, DOM.getInnerHTML(td));
3127             floatingCopyOfHeaderCell = DOM
3128                     .getChild(floatingCopyOfHeaderCell, 2);
3129             // #12714 the shown "ghost element" should be inside
3130             // v-overlay-container, and it should contain the same styles as the
3131             // table to enable theming (except v-table & v-widget).
3132             String stylePrimaryName = VScrollTablePatched.this.getStylePrimaryName();
3133             StringBuilder sb = new StringBuilder();
3134             for (String s : VScrollTablePatched.this.getStyleName().split(" ")) {
3135                 if (!s.equals(StyleConstants.UI_WIDGET)) {
3136                     sb.append(s);
3137                     if (s.equals(stylePrimaryName)) {
3138                         sb.append("-header-drag ");
3139                     } else {
3140                         sb.append(" ");
3141                     }
3142                 }
3143             }
3144             floatingCopyOfHeaderCell.setClassName(sb.toString().trim());
3145             // otherwise might wrap or be cut if narrow column
3146             floatingCopyOfHeaderCell.getStyle().setProperty("width", "auto");
3147             updateFloatingCopysPosition(DOM.getAbsoluteLeft(td),
3148                     DOM.getAbsoluteTop(td));
3149             DOM.appendChild(VOverlay.getOverlayContainer(client),
3150                     floatingCopyOfHeaderCell);
3151         }
3152 
3153         private void updateFloatingCopysPosition(int x, int y) {
3154             x -= DOM.getElementPropertyInt(floatingCopyOfHeaderCell,
3155                     "offsetWidth") / 2;
3156             floatingCopyOfHeaderCell.getStyle().setLeft(x, Unit.PX);
3157             if (y > 0) {
3158                 floatingCopyOfHeaderCell.getStyle().setTop(y + 7, Unit.PX);
3159             }
3160         }
3161 
3162         private void hideFloatingCopy() {
3163             floatingCopyOfHeaderCell.removeFromParent();
3164             floatingCopyOfHeaderCell = null;
3165         }
3166 
3167         /**
3168          * Fires a header click event after the user has clicked a column header
3169          * cell
3170          * 
3171          * @param event
3172          *            The click event
3173          */
3174         private void fireHeaderClickedEvent(Event event) {
3175             if (client.hasEventListeners(VScrollTablePatched.this,
3176                     TableConstants.HEADER_CLICK_EVENT_ID)) {
3177                 MouseEventDetails details = MouseEventDetailsBuilder
3178                         .buildMouseEventDetails(event);
3179                 client.updateVariable(paintableId, "headerClickEvent",
3180                         details.toString(), false);
3181                 client.updateVariable(paintableId, "headerClickCID", cid, true);
3182             }
3183         }
3184 
3185         protected void handleCaptionEvent(Event event) {
3186             switch (DOM.eventGetType(event)) {
3187             case Event.ONTOUCHSTART:
3188             case Event.ONMOUSEDOWN:
3189                 if (columnReordering
3190                         && WidgetUtil.isTouchEventOrLeftMouseButton(event)) {
3191                     if (event.getTypeInt() == Event.ONTOUCHSTART) {
3192                         /*
3193                          * prevent using this event in e.g. scrolling
3194                          */
3195                         event.stopPropagation();
3196                     }
3197                     dragging = true;
3198                     currentDragX = WidgetUtil.getTouchOrMouseClientX(event);
3199                     moved = false;
3200                     colIndex = getColIndexByKey(cid);
3201                     DOM.setCapture(getElement());
3202                     headerX = tHead.getAbsoluteLeft();
3203                     event.preventDefault(); // prevent selecting text &&
3204                                             // generated touch events
3205                 }
3206                 break;
3207             case Event.ONMOUSEUP:
3208             case Event.ONTOUCHEND:
3209             case Event.ONTOUCHCANCEL:
3210                 if (columnReordering
3211                         && WidgetUtil.isTouchEventOrLeftMouseButton(event)) {
3212                     dragging = false;
3213                     currentDragX = null;
3214                     DOM.releaseCapture(getElement());
3215 
3216                     if (WidgetUtil.isTouchEvent(event)) {
3217                         /*
3218                          * Prevent using in e.g. scrolling and prevent generated
3219                          * events.
3220                          */
3221                         event.preventDefault();
3222                         event.stopPropagation();
3223                     }
3224                     if (moved) {
3225                         hideFloatingCopy();
3226                         tHead.removeSlotFocus();
3227                         if (closestSlot != colIndex
3228                                 && closestSlot != (colIndex + 1)) {
3229                             if (closestSlot > colIndex) {
3230                                 reOrderColumn(cid, closestSlot - 1);
3231                             } else {
3232                                 reOrderColumn(cid, closestSlot);
3233                             }
3234                         }
3235                         moved = false;
3236                         break;
3237                     }
3238                 }
3239 
3240                 if (!moved) {
3241                     // mouse event was a click to header -> sort column
3242                     if (sortable
3243                             && WidgetUtil.isTouchEventOrLeftMouseButton(event)) {
3244                         if (sortColumn.equals(cid)) {
3245                             // just toggle order
3246                             client.updateVariable(paintableId, "sortascending",
3247                                     !sortAscending, false);
3248                         } else {
3249                             // set table sorted by this column
3250                             client.updateVariable(paintableId, "sortcolumn",
3251                                     cid, false);
3252                         }
3253                         // get also cache columns at the same request
3254                         scrollBodyPanel.setScrollPosition(0);
3255                         firstvisible = 0;
3256                         rowRequestHandler.setReqFirstRow(0);
3257                         rowRequestHandler.setReqRows((int) (2 * pageLength
3258                                 * cache_rate + pageLength));
3259                         rowRequestHandler.deferRowFetch(); // some validation +
3260                                                            // defer 250ms
3261                         rowRequestHandler.cancel(); // instead of waiting
3262                         rowRequestHandler.run(); // run immediately
3263                     }
3264                     fireHeaderClickedEvent(event);
3265                     if (WidgetUtil.isTouchEvent(event)) {
3266                         /*
3267                          * Prevent using in e.g. scrolling and prevent generated
3268                          * events.
3269                          */
3270                         event.preventDefault();
3271                         event.stopPropagation();
3272                     }
3273                     break;
3274                 }
3275                 break;
3276             case Event.ONDBLCLICK:
3277                 fireHeaderClickedEvent(event);
3278                 break;
3279             case Event.ONTOUCHMOVE:
3280             case Event.ONMOUSEMOVE:
3281                 // only start the drag if the mouse / touch has moved a minimum
3282                 // distance in x-axis (the same idea as in #13381)
3283                 int currentX = WidgetUtil.getTouchOrMouseClientX(event);
3284 
3285                 if (currentDragX == null
3286                         || Math.abs(currentDragX - currentX) > VDragAndDropManager.MINIMUM_DISTANCE_TO_START_DRAG) {
3287                     if (dragging
3288                             && WidgetUtil.isTouchEventOrLeftMouseButton(event)) {
3289                         if (event.getTypeInt() == Event.ONTOUCHMOVE) {
3290                             /*
3291                              * prevent using this event in e.g. scrolling
3292                              */
3293                             event.stopPropagation();
3294                         }
3295                         if (!moved) {
3296                             createFloatingCopy();
3297                             moved = true;
3298                         }
3299 
3300                         final int clientX = WidgetUtil
3301                                 .getTouchOrMouseClientX(event);
3302                         final int x = clientX
3303                                 + tHead.hTableWrapper.getScrollLeft();
3304                         int slotX = headerX;
3305                         closestSlot = colIndex;
3306                         int closestDistance = -1;
3307                         int start = 0;
3308                         if (showRowHeaders) {
3309                             start++;
3310                         }
3311                         final int visibleCellCount = tHead
3312                                 .getVisibleCellCount();
3313                         for (int i = start; i <= visibleCellCount; i++) {
3314                             if (i > 0) {
3315                                 final String colKey = getColKeyByIndex(i - 1);
3316                                 // getColWidth only returns the internal width
3317                                 // without padding, not the offset width of the
3318                                 // whole td (#10890)
3319                                 slotX += getColWidth(colKey)
3320                                         + scrollBody.getCellExtraWidth();
3321                             }
3322                             final int dist = Math.abs(x - slotX);
3323                             if (closestDistance == -1 || dist < closestDistance) {
3324                                 closestDistance = dist;
3325                                 closestSlot = i;
3326                             }
3327                         }
3328                         tHead.focusSlot(closestSlot);
3329 
3330                         updateFloatingCopysPosition(clientX, -1);
3331                     }
3332                 }
3333                 break;
3334             default:
3335                 break;
3336             }
3337         }
3338 
3339         private void onResizeEvent(Event event) {
3340             switch (DOM.eventGetType(event)) {
3341             case Event.ONMOUSEDOWN:
3342                 if (!WidgetUtil.isTouchEventOrLeftMouseButton(event)) {
3343                     return;
3344                 }
3345                 isResizing = true;
3346                 DOM.setCapture(getElement());
3347                 dragStartX = DOM.eventGetClientX(event);
3348                 colIndex = getColIndexByKey(cid);
3349                 originalWidth = getWidthWithIndent();
3350                 DOM.eventPreventDefault(event);
3351                 break;
3352             case Event.ONMOUSEUP:
3353                 if (!WidgetUtil.isTouchEventOrLeftMouseButton(event)) {
3354                     return;
3355                 }
3356                 isResizing = false;
3357                 DOM.releaseCapture(getElement());
3358                 tHead.disableAutoColumnWidthCalculation(this);
3359 
3360                 // Ensure last header cell is taking into account possible
3361                 // column selector
3362                 HeaderCell lastCell = tHead.getHeaderCell(tHead
3363                         .getVisibleCellCount() - 1);
3364                 tHead.resizeCaptionContainer(lastCell);
3365                 triggerLazyColumnAdjustment(true);
3366 
3367                 fireColumnResizeEvent(cid, originalWidth, getColWidth(cid));
3368                 break;
3369             case Event.ONMOUSEMOVE:
3370                 if (!WidgetUtil.isTouchEventOrLeftMouseButton(event)) {
3371                     return;
3372                 }
3373                 if (isResizing) {
3374                     final int deltaX = DOM.eventGetClientX(event) - dragStartX;
3375                     if (deltaX == 0) {
3376                         return;
3377                     }
3378                     tHead.disableAutoColumnWidthCalculation(this);
3379 
3380                     int newWidth = originalWidth + deltaX;
3381                     // get min width with indent, no padding
3382                     int minWidth = getMinWidth(true, false);
3383                     if (newWidth < minWidth) {
3384                         // already includes indent if any
3385                         newWidth = minWidth;
3386                     }
3387                     setColWidth(colIndex, newWidth, true);
3388                     triggerLazyColumnAdjustment(false);
3389                     forceRealignColumnHeaders();
3390                 }
3391                 break;
3392             default:
3393                 break;
3394             }
3395         }
3396 
3397         /**
3398          * Returns the smallest possible cell width in pixels.
3399          * 
3400          * @param includeIndent
3401          *            - width should include hierarchy column indent if
3402          *            applicable (VTreeTable only)
3403          * @param includeCellExtraWidth
3404          *            - width should include paddings etc.
3405          * @return
3406          */
3407         private int getMinWidth(boolean includeIndent,
3408                 boolean includeCellExtraWidth) {
3409             int minWidth = sortIndicator.getOffsetWidth();
3410             if (scrollBody != null) {
3411                 // check the need for indent before adding paddings etc.
3412                 if (includeIndent && isHierarchyColumn()) {
3413                     int maxIndent = scrollBody.getMaxIndent();
3414                     if (minWidth < maxIndent) {
3415                         minWidth = maxIndent;
3416                     }
3417                 }
3418                 if (includeCellExtraWidth) {
3419                     minWidth += scrollBody.getCellExtraWidth();
3420                 }
3421             }
3422             return minWidth;
3423         }
3424 
3425         public int getMinWidth() {
3426             // get min width with padding, no indent
3427             return getMinWidth(false, true);
3428         }
3429 
3430         public String getCaption() {
3431             return DOM.getInnerText(captionContainer);
3432         }
3433 
3434         public boolean isEnabled() {
3435             return getParent() != null;
3436         }
3437 
3438         public void setAlign(char c) {
3439             align = c;
3440             updateStyleNames(VScrollTablePatched.this.getStylePrimaryName());
3441         }
3442 
3443         public char getAlign() {
3444             return align;
3445         }
3446 
3447         /**
3448          * Saves natural column width if it hasn't been saved already.
3449          * 
3450          * @param columnIndex
3451          * @since 7.3.9
3452          */
3453         protected void saveNaturalColumnWidthIfNotSaved(int columnIndex) {
3454             if (naturalWidth < 0) {
3455                 // This is recently revealed column. Try to detect a proper
3456                 // value (greater of header and data columns)
3457 
3458                 int hw = captionContainer.getOffsetWidth() + getHeaderPadding();
3459                 if (BrowserInfo.get().isGecko()) {
3460                     hw += sortIndicator.getOffsetWidth();
3461                 }
3462                 if (columnIndex < 0) {
3463                     columnIndex = 0;
3464                     for (Iterator<Widget> it = tHead.iterator(); it.hasNext(); columnIndex++) {
3465                         if (it.next() == this) {
3466                             break;
3467                         }
3468                     }
3469                 }
3470                 final int cw = scrollBody.getColWidth(columnIndex);
3471                 naturalWidth = (hw > cw ? hw : cw);
3472             }
3473         }
3474 
3475         /**
3476          * Detects the natural minimum width for the column of this header cell.
3477          * If column is resized by user or the width is defined by server the
3478          * actual width is returned. Else the natural min width is returned.
3479          * 
3480          * @param columnIndex
3481          *            column index hint, if -1 (unknown) it will be detected
3482          * 
3483          * @return
3484          */
3485         public int getNaturalColumnWidth(int columnIndex) {
3486             final int iw = columnIndex == getHierarchyColumnIndex() ? scrollBody
3487                     .getMaxIndent() : 0;
3488             saveNaturalColumnWidthIfNotSaved(columnIndex);
3489             if (isDefinedWidth()) {
3490                 if (iw > width) {
3491                     return iw;
3492                 }
3493                 return width;
3494             } else {
3495                 if (iw > naturalWidth) {
3496                     // indent is temporary value, naturalWidth shouldn't be
3497                     // updated
3498                     return iw;
3499                 } else {
3500                     return naturalWidth;
3501                 }
3502             }
3503         }
3504 
3505         public void setExpandRatio(float floatAttribute) {
3506             if (floatAttribute != expandRatio) {
3507                 triggerLazyColumnAdjustment(false);
3508             }
3509             expandRatio = floatAttribute;
3510         }
3511 
3512         public float getExpandRatio() {
3513             return expandRatio;
3514         }
3515 
3516         public boolean isSorted() {
3517             return sorted;
3518         }
3519     }
3520 
3521     /**
3522      * HeaderCell that is header cell for row headers.
3523      * 
3524      * Reordering disabled and clicking on it resets sorting.
3525      */
3526     public class RowHeadersHeaderCell extends HeaderCell {
3527 
3528         RowHeadersHeaderCell() {
3529             super(ROW_HEADER_COLUMN_KEY, "");
3530             updateStyleNames(VScrollTablePatched.this.getStylePrimaryName());
3531         }
3532 
3533         @Override
3534         protected void updateStyleNames(String primaryStyleName) {
3535             super.updateStyleNames(primaryStyleName);
3536             setStyleName(primaryStyleName + "-header-cell-rowheader");
3537         }
3538 
3539         @Override
3540         protected void handleCaptionEvent(Event event) {
3541             // NOP: RowHeaders cannot be reordered
3542             // TODO It'd be nice to reset sorting here
3543         }
3544     }
3545 
3546     public class TableHead extends Panel implements ActionOwner {
3547 
3548         private static final int WRAPPER_WIDTH = 900000;
3549 
3550         ArrayList<Widget> visibleCells = new ArrayList<Widget>();
3551 
3552         HashMap<String, HeaderCell> availableCells = new HashMap<String, HeaderCell>();
3553 
3554         protected Element div = DOM.createDiv();
3555         Element hTableWrapper = DOM.createDiv();
3556         Element hTableContainer = DOM.createDiv();
3557         Element table = DOM.createTable();
3558         Element headerTableBody = DOM.createTBody();
3559         Element tr = DOM.createTR();
3560 
3561         private final Element columnSelector = DOM.createDiv();
3562 
3563         private int focusedSlot = -1;
3564 
3565         public TableHead() {
3566             if (BrowserInfo.get().isIE()) {
3567                 table.setPropertyInt("cellSpacing", 0);
3568             }
3569 
3570             hTableWrapper.getStyle().setOverflow(Overflow.HIDDEN);
3571             columnSelector.getStyle().setDisplay(Display.NONE);
3572 
3573             DOM.appendChild(table, headerTableBody);
3574             DOM.appendChild(headerTableBody, tr);
3575             DOM.appendChild(hTableContainer, table);
3576             DOM.appendChild(hTableWrapper, hTableContainer);
3577             DOM.appendChild(div, hTableWrapper);
3578             DOM.appendChild(div, columnSelector);
3579             setElement(div);
3580 
3581             DOM.sinkEvents(columnSelector, Event.ONCLICK);
3582 
3583             availableCells.put(ROW_HEADER_COLUMN_KEY,
3584                     new RowHeadersHeaderCell());
3585         }
3586 
3587         protected void updateStyleNames(String primaryStyleName) {
3588             hTableWrapper.setClassName(primaryStyleName + "-header");
3589             columnSelector.setClassName(primaryStyleName + "-column-selector");
3590             setStyleName(primaryStyleName + "-header-wrap");
3591             for (HeaderCell c : availableCells.values()) {
3592                 c.updateStyleNames(primaryStyleName);
3593             }
3594         }
3595 
3596         public void resizeCaptionContainer(HeaderCell cell) {
3597             HeaderCell lastcell = getHeaderCell(visibleCells.size() - 1);
3598             int columnSelectorOffset = columnSelector.getOffsetWidth();
3599 
3600             if (cell == lastcell && columnSelectorOffset > 0
3601                     && !hasVerticalScrollbar()) {
3602 
3603                 // Measure column widths
3604                 int columnTotalWidth = 0;
3605                 for (Widget w : visibleCells) {
3606                     int cellExtraWidth = w.getOffsetWidth();
3607                     if (scrollBody != null
3608                             && visibleCells.indexOf(w) == getHierarchyColumnIndex()
3609                             && cellExtraWidth < scrollBody.getMaxIndent()) {
3610                         // indent must be taken into consideration even if it
3611                         // hasn't been applied yet
3612                         columnTotalWidth += scrollBody.getMaxIndent();
3613                     } else {
3614                         columnTotalWidth += cellExtraWidth;
3615                     }
3616                 }
3617 
3618                 int divOffset = div.getOffsetWidth();
3619                 if (columnTotalWidth >= divOffset - columnSelectorOffset) {
3620                     /*
3621                      * Ensure column caption is visible when placed under the
3622                      * column selector widget by shifting and resizing the
3623                      * caption.
3624                      */
3625                     int offset = 0;
3626                     int diff = divOffset - columnTotalWidth;
3627                     if (diff < columnSelectorOffset && diff > 0) {
3628                         /*
3629                          * If the difference is less than the column selectors
3630                          * width then just offset by the difference
3631                          */
3632                         offset = columnSelectorOffset - diff;
3633                     } else {
3634                         // Else offset by the whole column selector
3635                         offset = columnSelectorOffset;
3636                     }
3637                     lastcell.resizeCaptionContainer(offset);
3638                 } else {
3639                     cell.resizeCaptionContainer(0);
3640                 }
3641             } else {
3642                 cell.resizeCaptionContainer(0);
3643             }
3644         }
3645 
3646         @Override
3647         public void clear() {
3648             for (String cid : availableCells.keySet()) {
3649                 removeCell(cid);
3650             }
3651             availableCells.clear();
3652             availableCells.put(ROW_HEADER_COLUMN_KEY,
3653                     new RowHeadersHeaderCell());
3654         }
3655 
3656         public void updateCellsFromUIDL(UIDL uidl) {
3657             Iterator<?> it = uidl.getChildIterator();
3658             HashSet<String> updated = new HashSet<String>();
3659             boolean refreshContentWidths = initializedAndAttached
3660                     && hadScrollBars != willHaveScrollbars();
3661             while (it.hasNext()) {
3662                 final UIDL col = (UIDL) it.next();
3663                 final String cid = col.getStringAttribute("cid");
3664                 updated.add(cid);
3665 
3666                 String caption = buildCaptionHtmlSnippet(col);
3667                 HeaderCell c = getHeaderCell(cid);
3668                 if (c == null) {
3669                     c = createHeaderCell(cid, caption);
3670                     availableCells.put(cid, c);
3671                     if (initializedAndAttached) {
3672                         // we will need a column width recalculation
3673                         initializedAndAttached = false;
3674                         initialContentReceived = false;
3675                         isNewBody = true;
3676                     }
3677                 } else {
3678                     c.setText(caption);
3679                 }
3680 
3681                 c.setSorted(false);
3682                 if (col.hasAttribute("sortable")) {
3683                     c.setSortable(true);
3684                 } else {
3685                     c.setSortable(false);
3686                 }
3687 
3688                 if (col.hasAttribute("align")) {
3689                     c.setAlign(col.getStringAttribute("align").charAt(0));
3690                 } else {
3691                     c.setAlign(ALIGN_LEFT);
3692 
3693                 }
3694                 if (col.hasAttribute("width") && !c.isResizing) {
3695                     // Make sure to accomodate for the sort indicator if
3696                     // necessary.
3697                     int width = col.getIntAttribute("width");
3698                     int widthWithoutAddedIndent = width;
3699 
3700                     // get min width with indent, no padding
3701                     int minWidth = c.getMinWidth(true, false);
3702                     if (width < minWidth) {
3703                         width = minWidth;
3704                     }
3705                     if (scrollBody != null && width != c.getWidthWithIndent()) {
3706                         // Do a more thorough update if a column is resized from
3707                         // the server *after* the header has been properly
3708                         // initialized
3709                         final int newWidth = width;
3710                         Scheduler.get().scheduleFinally(new ScheduledCommand() {
3711 
3712                             @Override
3713                             public void execute() {
3714                                 final int colIx = getColIndexByKey(cid);
3715                                 setColWidth(colIx, newWidth, true);
3716                             }
3717                         });
3718                         refreshContentWidths = true;
3719                     } else {
3720                         // get min width with no indent or padding
3721                         minWidth = c.getMinWidth(false, false);
3722                         if (widthWithoutAddedIndent < minWidth) {
3723                             widthWithoutAddedIndent = minWidth;
3724                         }
3725                         // save min width without indent
3726                         c.setWidth(widthWithoutAddedIndent, true);
3727                     }
3728                 } else if (col.hasAttribute("er")) {
3729                     c.setExpandRatio(col.getFloatAttribute("er"));
3730                     c.setUndefinedWidthFlagOnly();
3731                 } else if (recalcWidths) {
3732                     c.setUndefinedWidth();
3733 
3734                 } else {
3735                     boolean hadExpandRatio = c.getExpandRatio() > 0;
3736                     boolean hadDefinedWidth = c.isDefinedWidth();
3737                     if (hadExpandRatio || hadDefinedWidth) {
3738                         // Someone has removed a expand width or the defined
3739                         // width on the server side (setting it to -1), make the
3740                         // column undefined again and measure columns again.
3741                         c.setUndefinedWidth();
3742                         c.setExpandRatio(0);
3743                         refreshContentWidths = true;
3744                     }
3745                 }
3746 
3747                 if (col.hasAttribute("collapsed")) {
3748                     // ensure header is properly removed from parent (case when
3749                     // collapsing happens via servers side api)
3750                     if (c.isAttached()) {
3751                         c.removeFromParent();
3752                         headerChangedDuringUpdate = true;
3753                     }
3754                 }
3755             }
3756 
3757             if (refreshContentWidths) {
3758                 // Recalculate the column sizings if any column has changed
3759                 Scheduler.get().scheduleFinally(new ScheduledCommand() {
3760 
3761                     @Override
3762                     public void execute() {
3763                         triggerLazyColumnAdjustment(true);
3764                     }
3765                 });
3766             }
3767 
3768             // check for orphaned header cells
3769             for (Iterator<String> cit = availableCells.keySet().iterator(); cit
3770                     .hasNext();) {
3771                 String cid = cit.next();
3772                 if (!updated.contains(cid)) {
3773                     removeCell(cid);
3774                     cit.remove();
3775                     // we will need a column width recalculation, since columns
3776                     // with expand ratios should expand to fill the void.
3777                     initializedAndAttached = false;
3778                     initialContentReceived = false;
3779                     isNewBody = true;
3780                 }
3781             }
3782         }
3783 
3784         public void enableColumn(String cid, int index) {
3785             final HeaderCell c = getHeaderCell(cid);
3786             if (!c.isEnabled() || getHeaderCell(index) != c) {
3787                 setHeaderCell(index, c);
3788                 if (initializedAndAttached) {
3789                     headerChangedDuringUpdate = true;
3790                 }
3791             }
3792         }
3793 
3794         public int getVisibleCellCount() {
3795             return visibleCells.size();
3796         }
3797 
3798         public void setHorizontalScrollPosition(int scrollLeft) {
3799             hTableWrapper.setScrollLeft(scrollLeft);
3800         }
3801 
3802         public void setColumnCollapsingAllowed(boolean cc) {
3803             if (cc) {
3804                 columnSelector.getStyle().setDisplay(Display.BLOCK);
3805             } else {
3806                 columnSelector.getStyle().setDisplay(Display.NONE);
3807             }
3808         }
3809 
3810         public void disableBrowserIntelligence() {
3811             hTableContainer.getStyle().setWidth(WRAPPER_WIDTH, Unit.PX);
3812         }
3813 
3814         public void enableBrowserIntelligence() {
3815             hTableContainer.getStyle().clearWidth();
3816         }
3817 
3818         public void setHeaderCell(int index, HeaderCell cell) {
3819             if (cell.isEnabled()) {
3820                 // we're moving the cell
3821                 DOM.removeChild(tr, cell.getElement());
3822                 orphan(cell);
3823                 visibleCells.remove(cell);
3824             }
3825             if (index < visibleCells.size()) {
3826                 // insert to right slot
3827                 DOM.insertChild(tr, cell.getElement(), index);
3828                 adopt(cell);
3829                 visibleCells.add(index, cell);
3830             } else if (index == visibleCells.size()) {
3831                 // simply append
3832                 DOM.appendChild(tr, cell.getElement());
3833                 adopt(cell);
3834                 visibleCells.add(cell);
3835             } else {
3836                 throw new RuntimeException(
3837                         "Header cells must be appended in order");
3838             }
3839         }
3840 
3841         public HeaderCell getHeaderCell(int index) {
3842             if (index >= 0 && index < visibleCells.size()) {
3843                 return (HeaderCell) visibleCells.get(index);
3844             } else {
3845                 return null;
3846             }
3847         }
3848 
3849         /**
3850          * Get's HeaderCell by it's column Key.
3851          * 
3852          * Note that this returns HeaderCell even if it is currently collapsed.
3853          * 
3854          * @param cid
3855          *            Column key of accessed HeaderCell
3856          * @return HeaderCell
3857          */
3858         public HeaderCell getHeaderCell(String cid) {
3859             return availableCells.get(cid);
3860         }
3861 
3862         public void moveCell(int oldIndex, int newIndex) {
3863             final HeaderCell hCell = getHeaderCell(oldIndex);
3864             final Element cell = hCell.getElement();
3865 
3866             visibleCells.remove(oldIndex);
3867             DOM.removeChild(tr, cell);
3868 
3869             DOM.insertChild(tr, cell, newIndex);
3870             visibleCells.add(newIndex, hCell);
3871         }
3872 
3873         @Override
3874         public Iterator<Widget> iterator() {
3875             return visibleCells.iterator();
3876         }
3877 
3878         @Override
3879         public boolean remove(Widget w) {
3880             if (visibleCells.contains(w)) {
3881                 visibleCells.remove(w);
3882                 orphan(w);
3883                 DOM.removeChild(DOM.getParent(w.getElement()), w.getElement());
3884                 return true;
3885             }
3886             return false;
3887         }
3888 
3889         public void removeCell(String colKey) {
3890             final HeaderCell c = getHeaderCell(colKey);
3891             remove(c);
3892         }
3893 
3894         private void focusSlot(int index) {
3895             removeSlotFocus();
3896             if (index > 0) {
3897                 Element child = tr.getChild(index - 1).getFirstChild().cast();
3898                 child.setClassName(VScrollTablePatched.this.getStylePrimaryName()
3899                         + "-resizer");
3900                 child.addClassName(VScrollTablePatched.this.getStylePrimaryName()
3901                         + "-focus-slot-right");
3902             } else {
3903                 Element child = tr.getChild(index).getFirstChild().cast();
3904                 child.setClassName(VScrollTablePatched.this.getStylePrimaryName()
3905                         + "-resizer");
3906                 child.addClassName(VScrollTablePatched.this.getStylePrimaryName()
3907                         + "-focus-slot-left");
3908             }
3909             focusedSlot = index;
3910         }
3911 
3912         private void removeSlotFocus() {
3913             if (focusedSlot < 0) {
3914                 return;
3915             }
3916             if (focusedSlot == 0) {
3917                 Element child = tr.getChild(focusedSlot).getFirstChild().cast();
3918                 child.setClassName(VScrollTablePatched.this.getStylePrimaryName()
3919                         + "-resizer");
3920             } else if (focusedSlot > 0) {
3921                 Element child = tr.getChild(focusedSlot - 1).getFirstChild()
3922                         .cast();
3923                 child.setClassName(VScrollTablePatched.this.getStylePrimaryName()
3924                         + "-resizer");
3925             }
3926             focusedSlot = -1;
3927         }
3928 
3929         @Override
3930         public void onBrowserEvent(Event event) {
3931             if (enabled) {
3932                 if (event.getEventTarget().cast() == columnSelector) {
3933                     final int left = DOM.getAbsoluteLeft(columnSelector);
3934                     final int top = DOM.getAbsoluteTop(columnSelector)
3935                             + DOM.getElementPropertyInt(columnSelector,
3936                                     "offsetHeight");
3937                     client.getContextMenu().showAt(this, left, top);
3938                 }
3939             }
3940         }
3941 
3942         @Override
3943         protected void onDetach() {
3944             super.onDetach();
3945             if (client != null) {
3946                 client.getContextMenu().ensureHidden(this);
3947             }
3948         }
3949 
3950         class VisibleColumnAction extends Action {
3951 
3952             String colKey;
3953             private boolean collapsed;
3954             private boolean noncollapsible = false;
3955             private VScrollTableRow currentlyFocusedRow;
3956 
3957             public VisibleColumnAction(String colKey) {
3958                 super(VScrollTablePatched.TableHead.this);
3959                 this.colKey = colKey;
3960                 caption = tHead.getHeaderCell(colKey).getCaption();
3961                 currentlyFocusedRow = focusedRow;
3962             }
3963 
3964             @Override
3965             public void execute() {
3966                 if (noncollapsible) {
3967                     return;
3968                 }
3969                 client.getContextMenu().hide();
3970                 // toggle selected column
3971                 if (collapsedColumns.contains(colKey)) {
3972                     collapsedColumns.remove(colKey);
3973                 } else {
3974                     tHead.removeCell(colKey);
3975                     collapsedColumns.add(colKey);
3976                     triggerLazyColumnAdjustment(true);
3977                 }
3978 
3979                 // update variable to server
3980                 client.updateVariable(paintableId, "collapsedcolumns",
3981                         collapsedColumns.toArray(new String[collapsedColumns
3982                                 .size()]), false);
3983                 // let rowRequestHandler determine proper rows
3984                 rowRequestHandler.refreshContent();
3985                 lazyRevertFocusToRow(currentlyFocusedRow);
3986             }
3987 
3988             public void setCollapsed(boolean b) {
3989                 collapsed = b;
3990             }
3991 
3992             public void setNoncollapsible(boolean b) {
3993                 noncollapsible = b;
3994             }
3995 
3996             /**
3997              * Override default method to distinguish on/off columns
3998              */
3999 
4000             @Override
4001             public String getHTML() {
4002                 final StringBuffer buf = new StringBuffer();
4003                 buf.append("<span class=\"");
4004                 if (collapsed) {
4005                     buf.append("v-off");
4006                 } else {
4007                     buf.append("v-on");
4008                 }
4009                 if (noncollapsible) {
4010                     buf.append(" v-disabled");
4011                 }
4012                 buf.append("\">");
4013 
4014                 buf.append(super.getHTML());
4015                 buf.append("</span>");
4016 
4017                 return buf.toString();
4018             }
4019 
4020         }
4021 
4022         /*
4023          * Returns columns as Action array for column select popup
4024          */
4025 
4026         @Override
4027         public Action[] getActions() {
4028             Object[] cols;
4029             if (columnReordering && columnOrder != null) {
4030                 cols = columnOrder;
4031             } else {
4032                 // if columnReordering is disabled, we need different way to get
4033                 // all available columns
4034                 cols = visibleColOrder;
4035                 cols = new Object[visibleColOrder.length
4036                         + collapsedColumns.size()];
4037                 int i;
4038                 for (i = 0; i < visibleColOrder.length; i++) {
4039                     cols[i] = visibleColOrder[i];
4040                 }
4041                 for (final Iterator<String> it = collapsedColumns.iterator(); it
4042                         .hasNext();) {
4043                     cols[i++] = it.next();
4044                 }
4045             }
4046             final Action[] actions = new Action[cols.length];
4047 
4048             for (int i = 0; i < cols.length; i++) {
4049                 final String cid = (String) cols[i];
4050                 final HeaderCell c = getHeaderCell(cid);
4051                 final VisibleColumnAction a = new VisibleColumnAction(
4052                         c.getColKey());
4053                 a.setCaption(c.getCaption());
4054                 if (!c.isEnabled()) {
4055                     a.setCollapsed(true);
4056                 }
4057                 if (noncollapsibleColumns.contains(cid)) {
4058                     a.setNoncollapsible(true);
4059                 }
4060                 actions[i] = a;
4061             }
4062             return actions;
4063         }
4064 
4065         @Override
4066         public ApplicationConnection getClient() {
4067             return client;
4068         }
4069 
4070         @Override
4071         public String getPaintableId() {
4072             return paintableId;
4073         }
4074 
4075         /**
4076          * Returns column alignments for visible columns
4077          */
4078         public char[] getColumnAlignments() {
4079             final Iterator<Widget> it = visibleCells.iterator();
4080             final char[] aligns = new char[visibleCells.size()];
4081             int colIndex = 0;
4082             while (it.hasNext()) {
4083                 aligns[colIndex++] = ((HeaderCell) it.next()).getAlign();
4084             }
4085             return aligns;
4086         }
4087 
4088         /**
4089          * Disables the automatic calculation of all column widths by forcing
4090          * the widths to be "defined" thus turning off expand ratios and such.
4091          */
4092         public void disableAutoColumnWidthCalculation(HeaderCell source) {
4093             for (HeaderCell cell : availableCells.values()) {
4094                 cell.disableAutoWidthCalculation();
4095             }
4096             // fire column resize events for all columns but the source of the
4097             // resize action, since an event will fire separately for this.
4098             ArrayList<HeaderCell> columns = new ArrayList<HeaderCell>(
4099                     availableCells.values());
4100             columns.remove(source);
4101             sendColumnWidthUpdates(columns);
4102             forceRealignColumnHeaders();
4103         }
4104     }
4105 
4106     /**
4107      * A cell in the footer
4108      */
4109     public class FooterCell extends Widget {
4110         private final Element td = DOM.createTD();
4111         private final Element captionContainer = DOM.createDiv();
4112         private char align = ALIGN_LEFT;
4113         private int width = -1;
4114         private float expandRatio = 0;
4115         private final String cid;
4116         boolean definedWidth = false;
4117         private int naturalWidth = -1;
4118 
4119         public FooterCell(String colId, String headerText) {
4120             cid = colId;
4121 
4122             setText(headerText);
4123 
4124             // ensure no clipping initially (problem on column additions)
4125             captionContainer.getStyle().setOverflow(Overflow.VISIBLE);
4126 
4127             DOM.sinkEvents(captionContainer, Event.MOUSEEVENTS);
4128 
4129             DOM.appendChild(td, captionContainer);
4130 
4131             DOM.sinkEvents(td, Event.MOUSEEVENTS | Event.ONDBLCLICK
4132                     | Event.ONCONTEXTMENU);
4133 
4134             setElement(td);
4135 
4136             updateStyleNames(VScrollTablePatched.this.getStylePrimaryName());
4137         }
4138 
4139         protected void updateStyleNames(String primaryStyleName) {
4140             captionContainer.setClassName(primaryStyleName
4141                     + "-footer-container");
4142         }
4143 
4144         /**
4145          * Sets the text of the footer
4146          * 
4147          * @param footerText
4148          *            The text in the footer
4149          */
4150         public void setText(String footerText) {
4151             if (footerText == null || footerText.equals("")) {
4152                 footerText = "&nbsp;";
4153             }
4154 
4155             DOM.setInnerHTML(captionContainer, footerText);
4156         }
4157 
4158         /**
4159          * Set alignment of the text in the cell
4160          * 
4161          * @param c
4162          *            The alignment which can be ALIGN_CENTER, ALIGN_LEFT,
4163          *            ALIGN_RIGHT
4164          */
4165         public void setAlign(char c) {
4166             if (align != c) {
4167                 switch (c) {
4168                 case ALIGN_CENTER:
4169                     captionContainer.getStyle().setTextAlign(TextAlign.CENTER);
4170                     break;
4171                 case ALIGN_RIGHT:
4172                     captionContainer.getStyle().setTextAlign(TextAlign.RIGHT);
4173                     break;
4174                 default:
4175                     captionContainer.getStyle().setTextAlign(TextAlign.LEFT);
4176                     break;
4177                 }
4178             }
4179             align = c;
4180         }
4181 
4182         /**
4183          * Get the alignment of the text int the cell
4184          * 
4185          * @return Returns either ALIGN_CENTER, ALIGN_LEFT or ALIGN_RIGHT
4186          */
4187         public char getAlign() {
4188             return align;
4189         }
4190 
4191         /**
4192          * Sets the width of the cell. This width should not include any
4193          * possible indent modifications that are present in
4194          * {@link VScrollTableBody#getMaxIndent()}.
4195          * 
4196          * @param w
4197          *            The width of the cell
4198          * @param ensureDefinedWidth
4199          *            Ensures that the given width is not recalculated
4200          */
4201         public void setWidth(int w, boolean ensureDefinedWidth) {
4202 
4203             if (ensureDefinedWidth) {
4204                 definedWidth = true;
4205                 // on column resize expand ratio becomes zero
4206                 expandRatio = 0;
4207             }
4208             if (width == w) {
4209                 return;
4210             }
4211             if (width == -1) {
4212                 // go to default mode, clip content if necessary
4213                 captionContainer.getStyle().clearOverflow();
4214             }
4215             width = w;
4216             if (w == -1) {
4217                 captionContainer.getStyle().clearWidth();
4218                 setWidth("");
4219             } else {
4220                 /*
4221                  * Reduce width with one pixel for the right border since the
4222                  * footers does not have any spacers between them.
4223                  */
4224                 final int borderWidths = 1;
4225 
4226                 // Set the container width (check for negative value)
4227                 captionContainer.getStyle().setPropertyPx("width",
4228                         Math.max(w - borderWidths, 0));
4229 
4230                 /*
4231                  * if we already have tBody, set the header width properly, if
4232                  * not defer it. IE will fail with complex float in table header
4233                  * unless TD width is not explicitly set.
4234                  */
4235                 if (scrollBody != null) {
4236                     int maxIndent = scrollBody.getMaxIndent();
4237                     if (w < maxIndent
4238                             && tFoot.visibleCells.indexOf(this) == getHierarchyColumnIndex()) {
4239                         // ensure there's room for the indent
4240                         w = maxIndent;
4241                     }
4242                     int tdWidth = w + scrollBody.getCellExtraWidth()
4243                             - borderWidths;
4244                     setWidth(Math.max(tdWidth, 0) + "px");
4245                 } else {
4246                     Scheduler.get().scheduleDeferred(new Command() {
4247 
4248                         @Override
4249                         public void execute() {
4250                             int tdWidth = width;
4251                             int maxIndent = scrollBody.getMaxIndent();
4252                             if (tdWidth < maxIndent
4253                                     && tFoot.visibleCells.indexOf(this) == getHierarchyColumnIndex()) {
4254                                 // ensure there's room for the indent
4255                                 tdWidth = maxIndent;
4256                             }
4257                             tdWidth += scrollBody.getCellExtraWidth()
4258                                     - borderWidths;
4259                             setWidth(Math.max(tdWidth, 0) + "px");
4260                         }
4261                     });
4262                 }
4263             }
4264         }
4265 
4266         /**
4267          * Sets the width to undefined
4268          */
4269         public void setUndefinedWidth() {
4270             definedWidth = false;
4271             setWidth(-1, false);
4272         }
4273 
4274         /**
4275          * Detects if width is fixed by developer on server side or resized to
4276          * current width by user.
4277          * 
4278          * @return true if defined, false if "natural" width
4279          */
4280         public boolean isDefinedWidth() {
4281             return definedWidth && width >= 0;
4282         }
4283 
4284         /**
4285          * Returns the pixels width of the footer cell.
4286          * 
4287          * @return The width in pixels
4288          */
4289         public int getWidth() {
4290             return width;
4291         }
4292 
4293         /**
4294          * Sets the expand ratio of the cell
4295          * 
4296          * @param floatAttribute
4297          *            The expand ratio
4298          */
4299         public void setExpandRatio(float floatAttribute) {
4300             expandRatio = floatAttribute;
4301         }
4302 
4303         /**
4304          * Returns the expand ratio of the cell
4305          * 
4306          * @return The expand ratio
4307          */
4308         public float getExpandRatio() {
4309             return expandRatio;
4310         }
4311 
4312         /**
4313          * Is the cell enabled?
4314          * 
4315          * @return True if enabled else False
4316          */
4317         public boolean isEnabled() {
4318             return getParent() != null;
4319         }
4320 
4321         /**
4322          * Handle column clicking
4323          */
4324 
4325         @Override
4326         public void onBrowserEvent(Event event) {
4327             if (enabled && event != null) {
4328                 handleCaptionEvent(event);
4329 
4330                 if (DOM.eventGetType(event) == Event.ONMOUSEUP) {
4331                     scrollBodyPanel.setFocus(true);
4332                 }
4333                 boolean stopPropagation = true;
4334                 if (event.getTypeInt() == Event.ONCONTEXTMENU
4335                         && !client.hasEventListeners(VScrollTablePatched.this,
4336                                 TableConstants.FOOTER_CLICK_EVENT_ID)) {
4337                     // Show browser context menu if a footer click listener is
4338                     // not present
4339                     stopPropagation = false;
4340                 }
4341                 if (stopPropagation) {
4342                     event.stopPropagation();
4343                     event.preventDefault();
4344                 }
4345             }
4346         }
4347 
4348         /**
4349          * Handles a event on the captions
4350          * 
4351          * @param event
4352          *            The event to handle
4353          */
4354         protected void handleCaptionEvent(Event event) {
4355             if (event.getTypeInt() == Event.ONMOUSEUP
4356                     || event.getTypeInt() == Event.ONDBLCLICK) {
4357                 fireFooterClickedEvent(event);
4358             }
4359         }
4360 
4361         /**
4362          * Fires a footer click event after the user has clicked a column footer
4363          * cell
4364          * 
4365          * @param event
4366          *            The click event
4367          */
4368         private void fireFooterClickedEvent(Event event) {
4369             if (client.hasEventListeners(VScrollTablePatched.this,
4370                     TableConstants.FOOTER_CLICK_EVENT_ID)) {
4371                 MouseEventDetails details = MouseEventDetailsBuilder
4372                         .buildMouseEventDetails(event);
4373                 client.updateVariable(paintableId, "footerClickEvent",
4374                         details.toString(), false);
4375                 client.updateVariable(paintableId, "footerClickCID", cid, true);
4376             }
4377         }
4378 
4379         /**
4380          * Returns the column key of the column
4381          * 
4382          * @return The column key
4383          */
4384         public String getColKey() {
4385             return cid;
4386         }
4387 
4388         /**
4389          * Saves natural column width if it hasn't been saved already.
4390          * 
4391          * @param columnIndex
4392          * @since 7.3.9
4393          */
4394         protected void saveNaturalColumnWidthIfNotSaved(int columnIndex) {
4395             if (naturalWidth < 0) {
4396                 // This is recently revealed column. Try to detect a proper
4397                 // value (greater of header and data cols)
4398 
4399                 final int hw = ((Element) getElement().getLastChild())
4400                         .getOffsetWidth() + getHeaderPadding();
4401                 if (columnIndex < 0) {
4402                     columnIndex = 0;
4403                     for (Iterator<Widget> it = tHead.iterator(); it.hasNext(); columnIndex++) {
4404                         if (it.next() == this) {
4405                             break;
4406                         }
4407                     }
4408                 }
4409                 final int cw = scrollBody.getColWidth(columnIndex);
4410                 naturalWidth = (hw > cw ? hw : cw);
4411             }
4412         }
4413 
4414         /**
4415          * Detects the natural minimum width for the column of this header cell.
4416          * If column is resized by user or the width is defined by server the
4417          * actual width is returned. Else the natural min width is returned.
4418          * 
4419          * @param columnIndex
4420          *            column index hint, if -1 (unknown) it will be detected
4421          * 
4422          * @return
4423          */
4424         public int getNaturalColumnWidth(int columnIndex) {
4425             final int iw = columnIndex == getHierarchyColumnIndex() ? scrollBody
4426                     .getMaxIndent() : 0;
4427             saveNaturalColumnWidthIfNotSaved(columnIndex);
4428             if (isDefinedWidth()) {
4429                 if (iw > width) {
4430                     return iw;
4431                 }
4432                 return width;
4433             } else {
4434                 if (iw > naturalWidth) {
4435                     return iw;
4436                 } else {
4437                     return naturalWidth;
4438                 }
4439             }
4440         }
4441 
4442         public void setNaturalMinimumColumnWidth(int w) {
4443             naturalWidth = w;
4444         }
4445     }
4446 
4447     /**
4448      * HeaderCell that is header cell for row headers.
4449      * 
4450      * Reordering disabled and clicking on it resets sorting.
4451      */
4452     public class RowHeadersFooterCell extends FooterCell {
4453 
4454         RowHeadersFooterCell() {
4455             super(ROW_HEADER_COLUMN_KEY, "");
4456         }
4457 
4458         @Override
4459         protected void handleCaptionEvent(Event event) {
4460             // NOP: RowHeaders cannot be reordered
4461             // TODO It'd be nice to reset sorting here
4462         }
4463     }
4464 
4465     /**
4466      * The footer of the table which can be seen in the bottom of the Table.
4467      */
4468     public class TableFooter extends Panel {
4469 
4470         private static final int WRAPPER_WIDTH = 900000;
4471 
4472         ArrayList<Widget> visibleCells = new ArrayList<Widget>();
4473         HashMap<String, FooterCell> availableCells = new HashMap<String, FooterCell>();
4474 
4475         Element div = DOM.createDiv();
4476         Element hTableWrapper = DOM.createDiv();
4477         Element hTableContainer = DOM.createDiv();
4478         Element table = DOM.createTable();
4479         Element headerTableBody = DOM.createTBody();
4480         Element tr = DOM.createTR();
4481 
4482         public TableFooter() {
4483 
4484             hTableWrapper.getStyle().setOverflow(Overflow.HIDDEN);
4485 
4486             DOM.appendChild(table, headerTableBody);
4487             DOM.appendChild(headerTableBody, tr);
4488             DOM.appendChild(hTableContainer, table);
4489             DOM.appendChild(hTableWrapper, hTableContainer);
4490             DOM.appendChild(div, hTableWrapper);
4491             setElement(div);
4492 
4493             availableCells.put(ROW_HEADER_COLUMN_KEY,
4494                     new RowHeadersFooterCell());
4495 
4496             updateStyleNames(VScrollTablePatched.this.getStylePrimaryName());
4497         }
4498 
4499         protected void updateStyleNames(String primaryStyleName) {
4500             hTableWrapper.setClassName(primaryStyleName + "-footer");
4501             setStyleName(primaryStyleName + "-footer-wrap");
4502             for (FooterCell c : availableCells.values()) {
4503                 c.updateStyleNames(primaryStyleName);
4504             }
4505         }
4506 
4507         @Override
4508         public void clear() {
4509             for (String cid : availableCells.keySet()) {
4510                 removeCell(cid);
4511             }
4512             availableCells.clear();
4513             availableCells.put(ROW_HEADER_COLUMN_KEY,
4514                     new RowHeadersFooterCell());
4515         }
4516 
4517         /*
4518          * (non-Javadoc)
4519          * 
4520          * @see
4521          * com.google.gwt.user.client.ui.Panel#remove(com.google.gwt.user.client
4522          * .ui.Widget)
4523          */
4524 
4525         @Override
4526         public boolean remove(Widget w) {
4527             if (visibleCells.contains(w)) {
4528                 visibleCells.remove(w);
4529                 orphan(w);
4530                 DOM.removeChild(DOM.getParent(w.getElement()), w.getElement());
4531                 return true;
4532             }
4533             return false;
4534         }
4535 
4536         /*
4537          * (non-Javadoc)
4538          * 
4539          * @see com.google.gwt.user.client.ui.HasWidgets#iterator()
4540          */
4541 
4542         @Override
4543         public Iterator<Widget> iterator() {
4544             return visibleCells.iterator();
4545         }
4546 
4547         /**
4548          * Gets a footer cell which represents the given columnId
4549          * 
4550          * @param cid
4551          *            The columnId
4552          * 
4553          * @return The cell
4554          */
4555         public FooterCell getFooterCell(String cid) {
4556             return availableCells.get(cid);
4557         }
4558 
4559         /**
4560          * Gets a footer cell by using a column index
4561          * 
4562          * @param index
4563          *            The index of the column
4564          * @return The Cell
4565          */
4566         public FooterCell getFooterCell(int index) {
4567             if (index < visibleCells.size()) {
4568                 return (FooterCell) visibleCells.get(index);
4569             } else {
4570                 return null;
4571             }
4572         }
4573 
4574         /**
4575          * Updates the cells contents when updateUIDL request is received
4576          * 
4577          * @param uidl
4578          *            The UIDL
4579          */
4580         public void updateCellsFromUIDL(UIDL uidl) {
4581             Iterator<?> columnIterator = uidl.getChildIterator();
4582             HashSet<String> updated = new HashSet<String>();
4583             while (columnIterator.hasNext()) {
4584                 final UIDL col = (UIDL) columnIterator.next();
4585                 final String cid = col.getStringAttribute("cid");
4586                 updated.add(cid);
4587 
4588                 String caption = col.hasAttribute("fcaption") ? col
4589                         .getStringAttribute("fcaption") : "";
4590                 FooterCell c = getFooterCell(cid);
4591                 if (c == null) {
4592                     c = new FooterCell(cid, caption);
4593                     availableCells.put(cid, c);
4594                     if (initializedAndAttached) {
4595                         // we will need a column width recalculation
4596                         initializedAndAttached = false;
4597                         initialContentReceived = false;
4598                         isNewBody = true;
4599                     }
4600                 } else {
4601                     c.setText(caption);
4602                 }
4603 
4604                 if (col.hasAttribute("align")) {
4605                     c.setAlign(col.getStringAttribute("align").charAt(0));
4606                 } else {
4607                     c.setAlign(ALIGN_LEFT);
4608 
4609                 }
4610                 if (col.hasAttribute("width")) {
4611                     if (scrollBody == null || isNewBody) {
4612                         // Already updated by setColWidth called from
4613                         // TableHeads.updateCellsFromUIDL in case of a server
4614                         // side resize
4615                         final int width = col.getIntAttribute("width");
4616                         c.setWidth(width, true);
4617                     }
4618                 } else if (recalcWidths) {
4619                     c.setUndefinedWidth();
4620                 }
4621                 if (col.hasAttribute("er")) {
4622                     c.setExpandRatio(col.getFloatAttribute("er"));
4623                 }
4624                 if (col.hasAttribute("collapsed")) {
4625                     // ensure header is properly removed from parent (case when
4626                     // collapsing happens via servers side api)
4627                     if (c.isAttached()) {
4628                         c.removeFromParent();
4629                         headerChangedDuringUpdate = true;
4630                     }
4631                 }
4632             }
4633 
4634             // check for orphaned header cells
4635             for (Iterator<String> cit = availableCells.keySet().iterator(); cit
4636                     .hasNext();) {
4637                 String cid = cit.next();
4638                 if (!updated.contains(cid)) {
4639                     removeCell(cid);
4640                     cit.remove();
4641                 }
4642             }
4643         }
4644 
4645         /**
4646          * Set a footer cell for a specified column index
4647          * 
4648          * @param index
4649          *            The index
4650          * @param cell
4651          *            The footer cell
4652          */
4653         public void setFooterCell(int index, FooterCell cell) {
4654             if (cell.isEnabled()) {
4655                 // we're moving the cell
4656                 DOM.removeChild(tr, cell.getElement());
4657                 orphan(cell);
4658                 visibleCells.remove(cell);
4659             }
4660             if (index < visibleCells.size()) {
4661                 // insert to right slot
4662                 DOM.insertChild(tr, cell.getElement(), index);
4663                 adopt(cell);
4664                 visibleCells.add(index, cell);
4665             } else if (index == visibleCells.size()) {
4666                 // simply append
4667                 DOM.appendChild(tr, cell.getElement());
4668                 adopt(cell);
4669                 visibleCells.add(cell);
4670             } else {
4671                 throw new RuntimeException(
4672                         "Header cells must be appended in order");
4673             }
4674         }
4675 
4676         /**
4677          * Remove a cell by using the columnId
4678          * 
4679          * @param colKey
4680          *            The columnId to remove
4681          */
4682         public void removeCell(String colKey) {
4683             final FooterCell c = getFooterCell(colKey);
4684             remove(c);
4685         }
4686 
4687         /**
4688          * Enable a column (Sets the footer cell)
4689          * 
4690          * @param cid
4691          *            The columnId
4692          * @param index
4693          *            The index of the column
4694          */
4695         public void enableColumn(String cid, int index) {
4696             final FooterCell c = getFooterCell(cid);
4697             if (!c.isEnabled() || getFooterCell(index) != c) {
4698                 setFooterCell(index, c);
4699                 if (initializedAndAttached) {
4700                     headerChangedDuringUpdate = true;
4701                 }
4702             }
4703         }
4704 
4705         /**
4706          * Disable browser measurement of the table width
4707          */
4708         public void disableBrowserIntelligence() {
4709             hTableContainer.getStyle().setWidth(WRAPPER_WIDTH, Unit.PX);
4710         }
4711 
4712         /**
4713          * Enable browser measurement of the table width
4714          */
4715         public void enableBrowserIntelligence() {
4716             hTableContainer.getStyle().clearWidth();
4717         }
4718 
4719         /**
4720          * Set the horizontal position in the cell in the footer. This is done
4721          * when a horizontal scrollbar is present.
4722          * 
4723          * @param scrollLeft
4724          *            The value of the leftScroll
4725          */
4726         public void setHorizontalScrollPosition(int scrollLeft) {
4727             hTableWrapper.setScrollLeft(scrollLeft);
4728         }
4729 
4730         /**
4731          * Swap cells when the column are dragged
4732          * 
4733          * @param oldIndex
4734          *            The old index of the cell
4735          * @param newIndex
4736          *            The new index of the cell
4737          */
4738         public void moveCell(int oldIndex, int newIndex) {
4739             final FooterCell hCell = getFooterCell(oldIndex);
4740             final Element cell = hCell.getElement();
4741 
4742             visibleCells.remove(oldIndex);
4743             DOM.removeChild(tr, cell);
4744 
4745             DOM.insertChild(tr, cell, newIndex);
4746             visibleCells.add(newIndex, hCell);
4747         }
4748     }
4749 
4750     /**
4751      * This Panel can only contain VScrollTableRow type of widgets. This
4752      * "simulates" very large table, keeping spacers which take room of
4753      * unrendered rows.
4754      * 
4755      */
4756     public class VScrollTableBody extends Panel {
4757         protected VScrollTableRow createScrollTableRow(UIDL uidl, char[] aligns) {
4758             return new VScrollTableRow(uidl, aligns);
4759         }
4760 
4761         protected VScrollTableRow createScrollTableRow() {
4762             return new VScrollTableRow();
4763         }
4764 
4765         public static final int DEFAULT_ROW_HEIGHT = 24;
4766 
4767         private double rowHeight = -1;
4768 
4769         public final LinkedList<Widget> renderedRows = new LinkedList<Widget>();
4770 
4771         /**
4772          * Due some optimizations row height measuring is deferred and initial
4773          * set of rows is rendered detached. Flag set on when table body has
4774          * been attached in dom and rowheight has been measured.
4775          */
4776         private boolean tBodyMeasurementsDone = false;
4777 
4778         Element preSpacer = DOM.createDiv();
4779         Element postSpacer = DOM.createDiv();
4780 
4781         Element container = DOM.createDiv();
4782 
4783         TableSectionElement tBodyElement = Document.get().createTBodyElement();
4784         Element table = DOM.createTable();
4785 
4786         private int firstRendered;
4787         private int lastRendered;
4788 
4789         private char[] aligns;
4790 
4791         protected VScrollTableBody() {
4792             constructDOM();
4793             setElement(container);
4794         }
4795 
4796         public void setLastRendered(int lastRendered) {
4797             if (totalRows >= 0 && lastRendered > totalRows) {
4798                 VConsole.log("setLastRendered: " + this.lastRendered + " -> "
4799                         + lastRendered);
4800                 this.lastRendered = totalRows - 1;
4801             } else {
4802                 this.lastRendered = lastRendered;
4803             }
4804         }
4805 
4806         public int getLastRendered() {
4807 
4808             return lastRendered;
4809         }
4810 
4811         public int getFirstRendered() {
4812 
4813             return firstRendered;
4814         }
4815 
4816         public VScrollTableRow getRowByRowIndex(int indexInTable) {
4817             int internalIndex = indexInTable - firstRendered;
4818             if (internalIndex >= 0 && internalIndex < renderedRows.size()) {
4819                 return (VScrollTableRow) renderedRows.get(internalIndex);
4820             } else {
4821                 return null;
4822             }
4823         }
4824 
4825         /**
4826          * @return the height of scrollable body, subpixels ceiled.
4827          */
4828         public int getRequiredHeight() {
4829             return preSpacer.getOffsetHeight() + postSpacer.getOffsetHeight()
4830                     + WidgetUtil.getRequiredHeight(table);
4831         }
4832 
4833         private void constructDOM() {
4834             if (BrowserInfo.get().isIE()) {
4835                 table.setPropertyInt("cellSpacing", 0);
4836             }
4837 
4838             table.appendChild(tBodyElement);
4839             DOM.appendChild(container, preSpacer);
4840             DOM.appendChild(container, table);
4841             DOM.appendChild(container, postSpacer);
4842             if (BrowserInfo.get().requiresTouchScrollDelegate()) {
4843                 NodeList<Node> childNodes = container.getChildNodes();
4844                 for (int i = 0; i < childNodes.getLength(); i++) {
4845                     Element item = (Element) childNodes.getItem(i);
4846                     item.getStyle().setProperty("webkitTransform",
4847                             "translate3d(0,0,0)");
4848                 }
4849             }
4850             updateStyleNames(VScrollTablePatched.this.getStylePrimaryName());
4851         }
4852 
4853         protected void updateStyleNames(String primaryStyleName) {
4854             table.setClassName(primaryStyleName + "-table");
4855             preSpacer.setClassName(primaryStyleName + "-row-spacer");
4856             postSpacer.setClassName(primaryStyleName + "-row-spacer");
4857             for (Widget w : renderedRows) {
4858                 VScrollTableRow row = (VScrollTableRow) w;
4859                 row.updateStyleNames(primaryStyleName);
4860             }
4861         }
4862 
4863         public int getAvailableWidth() {
4864             int availW = scrollBodyPanel.getOffsetWidth() - getBorderWidth();
4865             return availW;
4866         }
4867 
4868         public void renderInitialRows(UIDL rowData, int firstIndex, int rows) {
4869             firstRendered = firstIndex;
4870             setLastRendered(firstIndex + rows - 1);
4871             final Iterator<?> it = rowData.getChildIterator();
4872             aligns = tHead.getColumnAlignments();
4873             while (it.hasNext()) {
4874                 final VScrollTableRow row = createRow((UIDL) it.next(), aligns);
4875                 addRow(row);
4876             }
4877             if (isAttached()) {
4878                 fixSpacers();
4879             }
4880         }
4881 
4882         public void renderRows(UIDL rowData, int firstIndex, int rows) {
4883             // FIXME REVIEW
4884             aligns = tHead.getColumnAlignments();
4885             final Iterator<?> it = rowData.getChildIterator();
4886             if (firstIndex == lastRendered + 1) {
4887                 while (it.hasNext()) {
4888                     final VScrollTableRow row = prepareRow((UIDL) it.next());
4889                     addRow(row);
4890                     setLastRendered(lastRendered + 1);
4891                 }
4892                 fixSpacers();
4893             } else if (firstIndex + rows == firstRendered) {
4894                 final VScrollTableRow[] rowArray = new VScrollTableRow[rows];
4895                 int i = rows;
4896 
4897                 while (it.hasNext()) {
4898                     i--;
4899                     rowArray[i] = prepareRow((UIDL) it.next());
4900                 }
4901 
4902                 for (i = 0; i < rows; i++) {
4903                     addRowBeforeFirstRendered(rowArray[i]);
4904                     firstRendered--;
4905                 }
4906             } else {
4907                 // completely new set of rows
4908 
4909                 // there can't be sanity checks for last rendered within this
4910                 // while loop regardless of what has been set previously, so
4911                 // change it temporarily to true and then return the original
4912                 // value
4913                 boolean temp = postponeSanityCheckForLastRendered;
4914                 postponeSanityCheckForLastRendered = true;
4915                 while (lastRendered + 1 > firstRendered) {
4916                     unlinkRow(false);
4917                 }
4918                 postponeSanityCheckForLastRendered = temp;
4919                 VScrollTableRow row = prepareRow((UIDL) it.next());
4920                 firstRendered = firstIndex;
4921                 setLastRendered(firstIndex - 1);
4922                 addRow(row);
4923                 setLastRendered(lastRendered + 1);
4924                 setContainerHeight();
4925                 fixSpacers();
4926 
4927                 while (it.hasNext()) {
4928                     addRow(prepareRow((UIDL) it.next()));
4929                     setLastRendered(lastRendered + 1);
4930                 }
4931 
4932                 fixSpacers();
4933             }
4934 
4935             // this may be a new set of rows due content change,
4936             // ensure we have proper cache rows
4937             ensureCacheFilled();
4938         }
4939 
4940         /**
4941          * Ensure we have the correct set of rows on client side, e.g. if the
4942          * content on the server side has changed, or the client scroll position
4943          * has changed since the last request.
4944          */
4945         protected void ensureCacheFilled() {
4946 
4947             /**
4948              * Fixes cache issue #13576 where unnecessary rows are fetched
4949              */
4950             if (isLazyScrollerActive()) {
4951                 return;
4952             }
4953 
4954             int reactFirstRow = (int) (firstRowInViewPort - pageLength
4955                     * cache_react_rate);
4956             int reactLastRow = (int) (firstRowInViewPort + pageLength + pageLength
4957                     * cache_react_rate);
4958             if (reactFirstRow < 0) {
4959                 reactFirstRow = 0;
4960             }
4961             if (reactLastRow >= totalRows) {
4962                 reactLastRow = totalRows - 1;
4963             }
4964             if (lastRendered < reactFirstRow || firstRendered > reactLastRow) {
4965                 /*
4966                  * #8040 - scroll position is completely changed since the
4967                  * latest request, so request a new set of rows.
4968                  * 
4969                  * TODO: We should probably check whether the fetched rows match
4970                  * the current scroll position right when they arrive, so as to
4971                  * not waste time rendering a set of rows that will never be
4972                  * visible...
4973                  */
4974                 rowRequestHandler.triggerRowFetch(reactFirstRow, reactLastRow
4975                         - reactFirstRow + 1, 1);
4976             } else if (lastRendered < reactLastRow) {
4977                 // get some cache rows below visible area
4978                 rowRequestHandler.triggerRowFetch(lastRendered + 1,
4979                         reactLastRow - lastRendered, 1);
4980             } else if (firstRendered > reactFirstRow) {
4981                 /*
4982                  * Branch for fetching cache above visible area.
4983                  * 
4984                  * If cache needed for both before and after visible area, this
4985                  * will be rendered after-cache is received and rendered. So in
4986                  * some rare situations the table may make two cache visits to
4987                  * server.
4988                  */
4989                 rowRequestHandler.triggerRowFetch(reactFirstRow, firstRendered
4990                         - reactFirstRow, 1);
4991             }
4992         }
4993 
4994         /**
4995          * Inserts rows as provided in the rowData starting at firstIndex.
4996          * 
4997          * @param rowData
4998          * @param firstIndex
4999          * @param rows
5000          *            the number of rows
5001          * @return a list of the rows added.
5002          */
5003         protected List<VScrollTableRow> insertRows(UIDL rowData,
5004                 int firstIndex, int rows) {
5005             aligns = tHead.getColumnAlignments();
5006             final Iterator<?> it = rowData.getChildIterator();
5007             List<VScrollTableRow> insertedRows = new ArrayList<VScrollTableRow>();
5008 
5009             if (firstIndex == lastRendered + 1) {
5010                 while (it.hasNext()) {
5011                     final VScrollTableRow row = prepareRow((UIDL) it.next());
5012                     addRow(row);
5013                     insertedRows.add(row);
5014                     if (postponeSanityCheckForLastRendered) {
5015                         lastRendered++;
5016                     } else {
5017                         setLastRendered(lastRendered + 1);
5018                     }
5019                 }
5020                 fixSpacers();
5021             } else if (firstIndex + rows == firstRendered) {
5022                 final VScrollTableRow[] rowArray = new VScrollTableRow[rows];
5023                 int i = rows;
5024                 while (it.hasNext()) {
5025                     i--;
5026                     rowArray[i] = prepareRow((UIDL) it.next());
5027                 }
5028                 for (i = 0; i < rows; i++) {
5029                     addRowBeforeFirstRendered(rowArray[i]);
5030                     insertedRows.add(rowArray[i]);
5031                     firstRendered--;
5032                 }
5033             } else {
5034                 // insert in the middle
5035                 int ix = firstIndex;
5036                 while (it.hasNext()) {
5037                     VScrollTableRow row = prepareRow((UIDL) it.next());
5038                     insertRowAt(row, ix);
5039                     insertedRows.add(row);
5040                     if (postponeSanityCheckForLastRendered) {
5041                         lastRendered++;
5042                     } else {
5043                         setLastRendered(lastRendered + 1);
5044                     }
5045                     ix++;
5046                 }
5047                 fixSpacers();
5048             }
5049             return insertedRows;
5050         }
5051 
5052         protected List<VScrollTableRow> insertAndReindexRows(UIDL rowData,
5053                 int firstIndex, int rows) {
5054             List<VScrollTableRow> inserted = insertRows(rowData, firstIndex,
5055                     rows);
5056             int actualIxOfFirstRowAfterInserted = firstIndex + rows
5057                     - firstRendered;
5058             for (int ix = actualIxOfFirstRowAfterInserted; ix < renderedRows
5059                     .size(); ix++) {
5060                 VScrollTableRow r = (VScrollTableRow) renderedRows.get(ix);
5061                 r.setIndex(r.getIndex() + rows);
5062             }
5063             setContainerHeight();
5064             return inserted;
5065         }
5066 
5067         protected void insertRowsDeleteBelow(UIDL rowData, int firstIndex,
5068                 int rows) {
5069             unlinkAllRowsStartingAt(firstIndex);
5070             insertRows(rowData, firstIndex, rows);
5071             setContainerHeight();
5072         }
5073 
5074         /**
5075          * This method is used to instantiate new rows for this table. It
5076          * automatically sets correct widths to rows cells and assigns correct
5077          * client reference for child widgets.
5078          * 
5079          * This method can be called only after table has been initialized
5080          * 
5081          * @param uidl
5082          */
5083         private VScrollTableRow prepareRow(UIDL uidl) {
5084             final VScrollTableRow row = createRow(uidl, aligns);
5085             row.initCellWidths();
5086             return row;
5087         }
5088 
5089         protected VScrollTableRow createRow(UIDL uidl, char[] aligns2) {
5090             if (uidl.hasAttribute("gen_html")) {
5091                 // This is a generated row.
5092                 return new VScrollTableGeneratedRow(uidl, aligns2);
5093             }
5094             return createScrollTableRow(uidl, aligns2);
5095         }
5096 
5097         private void addRowBeforeFirstRendered(VScrollTableRow row) {
5098             row.setIndex(firstRendered - 1);
5099             if (row.isSelected()) {
5100                 row.addStyleName("v-selected");
5101             }
5102             tBodyElement.insertBefore(row.getElement(),
5103                     tBodyElement.getFirstChild());
5104             adopt(row);
5105             renderedRows.add(0, row);
5106         }
5107 
5108         private void addRow(VScrollTableRow row) {
5109             row.setIndex(firstRendered + renderedRows.size());
5110             if (row.isSelected()) {
5111                 row.addStyleName("v-selected");
5112             }
5113             tBodyElement.appendChild(row.getElement());
5114             // Add to renderedRows before adopt so iterator() will return also
5115             // this row if called in an attach handler (#9264)
5116             renderedRows.add(row);
5117             adopt(row);
5118         }
5119 
5120         private void insertRowAt(VScrollTableRow row, int index) {
5121             row.setIndex(index);
5122             if (row.isSelected()) {
5123                 row.addStyleName("v-selected");
5124             }
5125             if (index > 0) {
5126                 VScrollTableRow sibling = getRowByRowIndex(index - 1);
5127                 tBodyElement
5128                         .insertAfter(row.getElement(), sibling.getElement());
5129             } else {
5130                 VScrollTableRow sibling = getRowByRowIndex(index);
5131                 tBodyElement.insertBefore(row.getElement(),
5132                         sibling.getElement());
5133             }
5134             adopt(row);
5135             int actualIx = index - firstRendered;
5136             renderedRows.add(actualIx, row);
5137         }
5138 
5139         @Override
5140         public Iterator<Widget> iterator() {
5141             return renderedRows.iterator();
5142         }
5143 
5144         /**
5145          * @return false if couldn't remove row
5146          */
5147         protected boolean unlinkRow(boolean fromBeginning) {
5148             if (lastRendered - firstRendered < 0) {
5149                 return false;
5150             }
5151             int actualIx;
5152             if (fromBeginning) {
5153                 actualIx = 0;
5154                 firstRendered++;
5155             } else {
5156                 actualIx = renderedRows.size() - 1;
5157                 if (postponeSanityCheckForLastRendered) {
5158                     --lastRendered;
5159                 } else {
5160                     setLastRendered(lastRendered - 1);
5161                 }
5162             }
5163             if (actualIx >= 0) {
5164                 unlinkRowAtActualIndex(actualIx);
5165                 fixSpacers();
5166                 return true;
5167             }
5168             return false;
5169         }
5170 
5171         protected void unlinkRows(int firstIndex, int count) {
5172             if (count < 1) {
5173                 return;
5174             }
5175             if (firstRendered > firstIndex
5176                     && firstRendered < firstIndex + count) {
5177                 count = count - (firstRendered - firstIndex);
5178                 firstIndex = firstRendered;
5179             }
5180             int lastIndex = firstIndex + count - 1;
5181             if (lastRendered < lastIndex) {
5182                 lastIndex = lastRendered;
5183             }
5184             for (int ix = lastIndex; ix >= firstIndex; ix--) {
5185                 unlinkRowAtActualIndex(actualIndex(ix));
5186                 if (postponeSanityCheckForLastRendered) {
5187                     // partialUpdate handles sanity check later
5188                     lastRendered--;
5189                 } else {
5190                     setLastRendered(lastRendered - 1);
5191                 }
5192             }
5193             fixSpacers();
5194         }
5195 
5196         protected void unlinkAndReindexRows(int firstIndex, int count) {
5197             unlinkRows(firstIndex, count);
5198             int actualFirstIx = firstIndex - firstRendered;
5199             for (int ix = actualFirstIx; ix < renderedRows.size(); ix++) {
5200                 VScrollTableRow r = (VScrollTableRow) renderedRows.get(ix);
5201                 r.setIndex(r.getIndex() - count);
5202             }
5203             setContainerHeight();
5204         }
5205 
5206         protected void unlinkAllRowsStartingAt(int index) {
5207             if (firstRendered > index) {
5208                 index = firstRendered;
5209             }
5210             for (int ix = renderedRows.size() - 1; ix >= index; ix--) {
5211                 unlinkRowAtActualIndex(actualIndex(ix));
5212                 setLastRendered(lastRendered - 1);
5213             }
5214             fixSpacers();
5215         }
5216 
5217         private int actualIndex(int index) {
5218             return index - firstRendered;
5219         }
5220 
5221         private void unlinkRowAtActualIndex(int index) {
5222             final VScrollTableRow toBeRemoved = (VScrollTableRow) renderedRows
5223                     .get(index);
5224             tBodyElement.removeChild(toBeRemoved.getElement());
5225             orphan(toBeRemoved);
5226             renderedRows.remove(index);
5227         }
5228 
5229         @Override
5230         public boolean remove(Widget w) {
5231             throw new UnsupportedOperationException();
5232         }
5233 
5234         /**
5235          * Fix container blocks height according to totalRows to avoid
5236          * "bouncing" when scrolling
5237          */
5238         private void setContainerHeight() {
5239             fixSpacers();
5240             container.getStyle().setHeight(measureRowHeightOffset(totalRows),
5241                     Unit.PX);
5242         }
5243 
5244         private void fixSpacers() {
5245             int prepx = measureRowHeightOffset(firstRendered);
5246             if (prepx < 0) {
5247                 prepx = 0;
5248             }
5249             preSpacer.getStyle().setPropertyPx("height", prepx);
5250             int postpx;
5251             if (pageLength == 0 && totalRows == pageLength) {
5252                 /*
5253                  * TreeTable depends on having lastRendered out of sync in some
5254                  * situations, which makes this method miss the special
5255                  * situation in which one row worth of post spacer to be added
5256                  * if there are no rows in the table. #9203
5257                  */
5258                 postpx = measureRowHeightOffset(1);
5259             } else {
5260                 postpx = measureRowHeightOffset(totalRows - 1)
5261                         - measureRowHeightOffset(lastRendered);
5262             }
5263 
5264             if (postpx < 0) {
5265                 postpx = 0;
5266             }
5267             postSpacer.getStyle().setPropertyPx("height", postpx);
5268         }
5269 
5270         public double getRowHeight() {
5271             return getRowHeight(false);
5272         }
5273 
5274         public double getRowHeight(boolean forceUpdate) {
5275             if (tBodyMeasurementsDone && !forceUpdate) {
5276                 return rowHeight;
5277             } else {
5278                 if (tBodyElement.getRows().getLength() > 0 && getTableHeight() > 0) {
5279                     int tableHeight = getTableHeight();
5280                     int rowCount = tBodyElement.getRows().getLength();
5281                     rowHeight = tableHeight / (double) rowCount;
5282                 } else {
5283                     // Special cases if we can't just measure the current rows
5284                     if (!Double.isNaN(lastKnownRowHeight)) {
5285                         // Use previous value if available
5286                         if (BrowserInfo.get().isIE()) {
5287                             /*
5288                              * IE needs to reflow the table element at this
5289                              * point to work correctly (e.g.
5290                              * com.vaadin.tests.components.table.
5291                              * ContainerSizeChange) - the other code paths
5292                              * already trigger reflows, but here it must be done
5293                              * explicitly.
5294                              */
5295                             getTableHeight();
5296                         }
5297                         rowHeight = lastKnownRowHeight;
5298                     } else if (isAttached()) {
5299                         // measure row height by adding a dummy row
5300                         VScrollTableRow scrollTableRow = createScrollTableRow();
5301                         tBodyElement.appendChild(scrollTableRow.getElement());
5302                         getRowHeight(forceUpdate);
5303                         tBodyElement.removeChild(scrollTableRow.getElement());
5304                     } else {
5305                         // TODO investigate if this can never happen anymore
5306                         return DEFAULT_ROW_HEIGHT;
5307                     }
5308                 }
5309                 lastKnownRowHeight = rowHeight;
5310                 tBodyMeasurementsDone = true;
5311                 return rowHeight;
5312             }
5313         }
5314 
5315         public int getTableHeight() {
5316             return table.getOffsetHeight();
5317         }
5318 
5319         /**
5320          * Returns the width available for column content.
5321          * 
5322          * @param columnIndex
5323          * @return
5324          */
5325         public int getColWidth(int columnIndex) {
5326             if (tBodyMeasurementsDone) {
5327                 if (renderedRows.isEmpty()) {
5328                     // no rows yet rendered
5329                     return 0;
5330                 }
5331                 for (Widget row : renderedRows) {
5332                     if (!(row instanceof VScrollTableGeneratedRow)) {
5333                         TableRowElement tr = row.getElement().cast();
5334                         Element wrapperdiv = tr.getCells().getItem(columnIndex)
5335                                 .getFirstChildElement().cast();
5336                         return wrapperdiv.getOffsetWidth();
5337                     }
5338                 }
5339                 return 0;
5340             } else {
5341                 return 0;
5342             }
5343         }
5344 
5345         /**
5346          * Sets the content width of a column.
5347          * 
5348          * Due IE limitation, we must set the width to a wrapper elements inside
5349          * table cells (with overflow hidden, which does not work on td
5350          * elements).
5351          * 
5352          * To get this work properly crossplatform, we will also set the width
5353          * of td.
5354          * 
5355          * @param colIndex
5356          * @param w
5357          */
5358         public void setColWidth(int colIndex, int w) {
5359             for (Widget row : renderedRows) {
5360                 ((VScrollTableRow) row).setCellWidth(colIndex, w);
5361             }
5362         }
5363 
5364         private int cellExtraWidth = -1;
5365 
5366         /**
5367          * Method to return the space used for cell paddings + border.
5368          */
5369         private int getCellExtraWidth() {
5370             if (!isVisibleInHierarchy()) {
5371                 return 0;
5372             }
5373             if (cellExtraWidth < 0) {
5374                 detectExtrawidth();
5375             }
5376             return cellExtraWidth;
5377         }
5378 
5379         /**
5380          * This method exists for the needs of {@link VTreeTable} only. May be
5381          * removed or replaced in the future.</br> </br> Returns the maximum
5382          * indent of the hierarcyColumn, if applicable.
5383          * 
5384          * @see {@link VScrollTable#getHierarchyColumnIndex()}
5385          * 
5386          * @return maximum indent in pixels
5387          */
5388         protected int getMaxIndent() {
5389             return 0;
5390         }
5391 
5392         /**
5393          * This method exists for the needs of {@link VTreeTable} only. May be
5394          * removed or replaced in the future.</br> </br> Calculates the maximum
5395          * indent of the hierarcyColumn, if applicable.
5396          */
5397         protected void calculateMaxIndent() {
5398             // NOP
5399         }
5400 
5401         private void detectExtrawidth() {
5402             NodeList<TableRowElement> rows = tBodyElement.getRows();
5403             if (rows.getLength() == 0) {
5404                 /* need to temporary add empty row and detect */
5405                 VScrollTableRow scrollTableRow = createScrollTableRow();
5406                 scrollTableRow.updateStyleNames(VScrollTablePatched.this
5407                         .getStylePrimaryName());
5408                 tBodyElement.appendChild(scrollTableRow.getElement());
5409                 detectExtrawidth();
5410                 tBodyElement.removeChild(scrollTableRow.getElement());
5411             } else {
5412                 boolean noCells = false;
5413                 TableRowElement item = rows.getItem(0);
5414                 TableCellElement firstTD = item.getCells().getItem(0);
5415                 if (firstTD == null) {
5416                     // content is currently empty, we need to add a fake cell
5417                     // for measuring
5418                     noCells = true;
5419                     VScrollTableRow next = (VScrollTableRow) iterator().next();
5420                     boolean sorted = tHead.getHeaderCell(0) != null ? tHead
5421                             .getHeaderCell(0).isSorted() : false;
5422                     next.addCell(null, "", ALIGN_LEFT, "", true, sorted);
5423                     firstTD = item.getCells().getItem(0);
5424                 }
5425                 com.google.gwt.dom.client.Element wrapper = firstTD
5426                         .getFirstChildElement();
5427                 cellExtraWidth = firstTD.getOffsetWidth()
5428                         - wrapper.getOffsetWidth();
5429                 if (noCells) {
5430                     firstTD.getParentElement().removeChild(firstTD);
5431                 }
5432             }
5433         }
5434 
5435         public void moveCol(int oldIndex, int newIndex) {
5436 
5437             // loop all rows and move given index to its new place
5438             final Iterator<?> rows = iterator();
5439             while (rows.hasNext()) {
5440                 final VScrollTableRow row = (VScrollTableRow) rows.next();
5441 
5442                 final Element td = DOM.getChild(row.getElement(), oldIndex);
5443                 if (td != null) {
5444                     DOM.removeChild(row.getElement(), td);
5445 
5446                     DOM.insertChild(row.getElement(), td, newIndex);
5447                 }
5448             }
5449 
5450         }
5451 
5452         /**
5453          * Restore row visibility which is set to "none" when the row is
5454          * rendered (due a performance optimization).
5455          */
5456         private void restoreRowVisibility() {
5457             for (Widget row : renderedRows) {
5458                 row.getElement().getStyle().setProperty("visibility", "");
5459             }
5460         }
5461 
5462         public int indexOf(Widget row) {
5463             int relIx = -1;
5464             for (int ix = 0; ix < renderedRows.size(); ix++) {
5465                 if (renderedRows.get(ix) == row) {
5466                     relIx = ix;
5467                     break;
5468                 }
5469             }
5470             if (relIx >= 0) {
5471                 return firstRendered + relIx;
5472             }
5473             return -1;
5474         }
5475 
5476         public class VScrollTableRow extends Panel implements ActionOwner,
5477                 ContextMenuOwner {
5478 
5479             private static final int TOUCHSCROLL_TIMEOUT = 500;
5480             private static final int DRAGMODE_MULTIROW = 2;
5481             protected ArrayList<Widget> childWidgets = new ArrayList<Widget>();
5482             private boolean selected = false;
5483             protected final int rowKey;
5484 
5485             private String[] actionKeys = null;
5486             private final TableRowElement rowElement;
5487             private int index;
5488             private Event touchStart;
5489 
5490             private static final int TOUCH_CONTEXT_MENU_TIMEOUT = 500;
5491             private Timer contextTouchTimeout;
5492             private Timer dragTouchTimeout;
5493             private int touchStartY;
5494             private int touchStartX;
5495 
5496             private TouchContextProvider touchContextProvider = new TouchContextProvider(
5497                     this);
5498 
5499             private TooltipInfo tooltipInfo = null;
5500             private Map<TableCellElement, TooltipInfo> cellToolTips = new HashMap<TableCellElement, TooltipInfo>();
5501             private boolean isDragging = false;
5502             protected String rowStyle = null;
5503             protected boolean applyZeroWidthFix = true;
5504 
5505             private VScrollTableRow(int rowKey) {
5506                 this.rowKey = rowKey;
5507                 rowElement = Document.get().createTRElement();
5508                 setElement(rowElement);
5509                 DOM.sinkEvents(getElement(), Event.MOUSEEVENTS
5510                         | Event.TOUCHEVENTS | Event.ONDBLCLICK
5511                         | Event.ONCONTEXTMENU | VTooltip.TOOLTIP_EVENTS);
5512             }
5513 
5514             public VScrollTableRow(UIDL uidl, char[] aligns) {
5515                 this(uidl.getIntAttribute("key"));
5516 
5517                 /*
5518                  * Rendering the rows as hidden improves Firefox and Safari
5519                  * performance drastically.
5520                  */
5521                 getElement().getStyle().setProperty("visibility", "hidden");
5522 
5523                 rowStyle = uidl.getStringAttribute("rowstyle");
5524                 updateStyleNames(VScrollTablePatched.this.getStylePrimaryName());
5525 
5526                 String rowDescription = uidl.getStringAttribute("rowdescr");
5527                 if (rowDescription != null && !rowDescription.equals("")) {
5528                     tooltipInfo = new TooltipInfo(rowDescription, null, this);
5529                 } else {
5530                     tooltipInfo = null;
5531                 }
5532 
5533                 tHead.getColumnAlignments();
5534                 int col = 0;
5535                 int visibleColumnIndex = -1;
5536 
5537                 // row header
5538                 if (showRowHeaders) {
5539                     boolean sorted = tHead.getHeaderCell(col).isSorted();
5540                     addCell(uidl, buildCaptionHtmlSnippet(uidl), aligns[col++],
5541                             "rowheader", true, sorted);
5542                     visibleColumnIndex++;
5543                 }
5544 
5545                 if (uidl.hasAttribute("al")) {
5546                     actionKeys = uidl.getStringArrayAttribute("al");
5547                 }
5548 
5549                 addCellsFromUIDL(uidl, aligns, col, visibleColumnIndex);
5550 
5551                 if (uidl.hasAttribute("selected") && !isSelected()) {
5552                     toggleSelection();
5553                 }
5554             }
5555 
5556             protected void updateStyleNames(String primaryStyleName) {
5557 
5558                 if (getStylePrimaryName().contains("odd")) {
5559                     setStyleName(primaryStyleName + "-row-odd");
5560                 } else {
5561                     setStyleName(primaryStyleName + "-row");
5562                 }
5563 
5564                 if (rowStyle != null) {
5565                     addStyleName(primaryStyleName + "-row-" + rowStyle);
5566                 }
5567 
5568                 for (int i = 0; i < rowElement.getChildCount(); i++) {
5569                     TableCellElement cell = (TableCellElement) rowElement
5570                             .getChild(i);
5571                     updateCellStyleNames(cell, primaryStyleName);
5572                 }
5573             }
5574 
5575             public TooltipInfo getTooltipInfo() {
5576                 return tooltipInfo;
5577             }
5578 
5579             /**
5580              * Add a dummy row, used for measurements if Table is empty.
5581              */
5582             public VScrollTableRow() {
5583                 this(0);
5584                 addCell(null, "_", 'b', "", true, false);
5585             }
5586 
5587             protected void initCellWidths() {
5588                 final int cells = tHead.getVisibleCellCount();
5589                 for (int i = 0; i < cells; i++) {
5590                     int w = VScrollTablePatched.this.getColWidth(getColKeyByIndex(i));
5591                     if (w < 0) {
5592                         w = 0;
5593                     }
5594                     setCellWidth(i, w);
5595                 }
5596             }
5597 
5598             protected void setCellWidth(int cellIx, int width) {
5599                 final Element cell = DOM.getChild(getElement(), cellIx);
5600                 Style wrapperStyle = cell.getFirstChildElement().getStyle();
5601                 int wrapperWidth = width;
5602                 if (BrowserInfo.get().isWebkit()
5603                         || BrowserInfo.get().isOpera10()) {
5604                     /*
5605                      * Some versions of Webkit and Opera ignore the width
5606                      * definition of zero width table cells. Instead, use 1px
5607                      * and compensate with a negative margin.
5608                      */
5609                     if (applyZeroWidthFix && width == 0) {
5610                         wrapperWidth = 1;
5611                         wrapperStyle.setMarginRight(-1, Unit.PX);
5612                     } else {
5613                         wrapperStyle.clearMarginRight();
5614                     }
5615                 }
5616                 wrapperStyle.setPropertyPx("width", wrapperWidth);
5617                 cell.getStyle().setPropertyPx("width", width);
5618             }
5619 
5620             protected void addCellsFromUIDL(UIDL uidl, char[] aligns, int col,
5621                     int visibleColumnIndex) {
5622                 final Iterator<?> cells = uidl.getChildIterator();
5623                 while (cells.hasNext()) {
5624                     final Object cell = cells.next();
5625                     visibleColumnIndex++;
5626 
5627                     String columnId = visibleColOrder[visibleColumnIndex];
5628 
5629                     String style = "";
5630                     if (uidl.hasAttribute("style-" + columnId)) {
5631                         style = uidl.getStringAttribute("style-" + columnId);
5632                     }
5633 
5634                     String description = null;
5635                     if (uidl.hasAttribute("descr-" + columnId)) {
5636                         description = uidl.getStringAttribute("descr-"
5637                                 + columnId);
5638                     }
5639 
5640                     boolean sorted = tHead.getHeaderCell(col).isSorted();
5641                     if (cell instanceof String) {
5642                         addCell(uidl, cell.toString(), aligns[col++], style,
5643                                 isRenderHtmlInCells(), sorted, description);
5644                     } else {
5645                         final ComponentConnector cellContent = client
5646                                 .getPaintable((UIDL) cell);
5647 
5648                         addCell(uidl, cellContent.getWidget(), aligns[col++],
5649                                 style, sorted, description);
5650                     }
5651                 }
5652             }
5653 
5654             /**
5655              * Overriding this and returning true causes all text cells to be
5656              * rendered as HTML.
5657              * 
5658              * @return always returns false in the default implementation
5659              */
5660             protected boolean isRenderHtmlInCells() {
5661                 return false;
5662             }
5663 
5664             /**
5665              * Detects whether row is visible in tables viewport.
5666              * 
5667              * @return
5668              */
5669             public boolean isInViewPort() {
5670                 int absoluteTop = getAbsoluteTop();
5671                 int absoluteBottom = absoluteTop + getOffsetHeight();
5672                 int viewPortTop = scrollBodyPanel.getAbsoluteTop();
5673                 int viewPortBottom = viewPortTop
5674                         + scrollBodyPanel.getOffsetHeight();
5675                 return absoluteBottom > viewPortTop
5676                         && absoluteTop < viewPortBottom;
5677             }
5678 
5679             /**
5680              * Makes a check based on indexes whether the row is before the
5681              * compared row.
5682              * 
5683              * @param row1
5684              * @return true if this rows index is smaller than in the row1
5685              */
5686             public boolean isBefore(VScrollTableRow row1) {
5687                 return getIndex() < row1.getIndex();
5688             }
5689 
5690             /**
5691              * Sets the index of the row in the whole table. Currently used just
5692              * to set even/odd classname
5693              * 
5694              * @param indexInWholeTable
5695              */
5696             private void setIndex(int indexInWholeTable) {
5697                 index = indexInWholeTable;
5698                 boolean isOdd = indexInWholeTable % 2 == 0;
5699                 // Inverted logic to be backwards compatible with earlier 6.4.
5700                 // It is very strange because rows 1,3,5 are considered "even"
5701                 // and 2,4,6 "odd".
5702                 //
5703                 // First remove any old styles so that both styles aren't
5704                 // applied when indexes are updated.
5705                 String primaryStyleName = getStylePrimaryName();
5706                 if (primaryStyleName != null && !primaryStyleName.equals("")) {
5707                     removeStyleName(getStylePrimaryName());
5708                 }
5709                 if (!isOdd) {
5710                     addStyleName(VScrollTablePatched.this.getStylePrimaryName()
5711                             + "-row-odd");
5712                 } else {
5713                     addStyleName(VScrollTablePatched.this.getStylePrimaryName()
5714                             + "-row");
5715                 }
5716             }
5717 
5718             public int getIndex() {
5719                 return index;
5720             }
5721 
5722             @Override
5723             protected void onDetach() {
5724                 super.onDetach();
5725                 client.getContextMenu().ensureHidden(this);
5726             }
5727 
5728             public String getKey() {
5729                 return String.valueOf(rowKey);
5730             }
5731 
5732             public void addCell(UIDL rowUidl, String text, char align,
5733                     String style, boolean textIsHTML, boolean sorted) {
5734                 addCell(rowUidl, text, align, style, textIsHTML, sorted, null);
5735             }
5736 
5737             public void addCell(UIDL rowUidl, String text, char align,
5738                     String style, boolean textIsHTML, boolean sorted,
5739                     String description) {
5740                 // String only content is optimized by not using Label widget
5741                 final TableCellElement td = DOM.createTD().cast();
5742                 initCellWithText(text, align, style, textIsHTML, sorted,
5743                         description, td);
5744             }
5745 
5746             protected void initCellWithText(String text, char align,
5747                     String style, boolean textIsHTML, boolean sorted,
5748                     String description, final TableCellElement td) {
5749                 final Element container = DOM.createDiv();
5750                 container.setClassName(VScrollTablePatched.this.getStylePrimaryName()
5751                         + "-cell-wrapper");
5752 
5753                 td.setClassName(VScrollTablePatched.this.getStylePrimaryName()
5754                         + "-cell-content");
5755 
5756                 if (style != null && !style.equals("")) {
5757                     td.addClassName(VScrollTablePatched.this.getStylePrimaryName()
5758                             + "-cell-content-" + style);
5759                 }
5760 
5761                 if (sorted) {
5762                     td.addClassName(VScrollTablePatched.this.getStylePrimaryName()
5763                             + "-cell-content-sorted");
5764                 }
5765 
5766                 if (textIsHTML) {
5767                     container.setInnerHTML(text);
5768                 } else {
5769                     container.setInnerText(text);
5770                 }
5771                 setAlign(align, container);
5772                 setTooltip(td, description);
5773 
5774                 td.appendChild(container);
5775                 getElement().appendChild(td);
5776             }
5777 
5778             protected void updateCellStyleNames(TableCellElement td,
5779                     String primaryStyleName) {
5780                 Element container = td.getFirstChild().cast();
5781                 container.setClassName(primaryStyleName + "-cell-wrapper");
5782 
5783                 /*
5784                  * Replace old primary style name with new one
5785                  */
5786                 String className = td.getClassName();
5787                 String oldPrimaryName = className.split("-cell-content")[0];
5788                 td.setClassName(className.replaceAll(oldPrimaryName,
5789                         primaryStyleName));
5790             }
5791 
5792             public void addCell(UIDL rowUidl, Widget w, char align,
5793                     String style, boolean sorted, String description) {
5794                 final TableCellElement td = DOM.createTD().cast();
5795                 initCellWithWidget(w, align, style, sorted, td);
5796                 setTooltip(td, description);
5797             }
5798 
5799             private void setTooltip(TableCellElement td, String description) {
5800                 if (description != null && !description.equals("")) {
5801                     TooltipInfo info = new TooltipInfo(description, null, this);
5802                     cellToolTips.put(td, info);
5803                 } else {
5804                     cellToolTips.remove(td);
5805                 }
5806 
5807             }
5808 
5809             private void setAlign(char align, final Element container) {
5810                 switch (align) {
5811                 case ALIGN_CENTER:
5812                     container.getStyle().setProperty("textAlign", "center");
5813                     break;
5814                 case ALIGN_LEFT:
5815                     container.getStyle().setProperty("textAlign", "left");
5816                     break;
5817                 case ALIGN_RIGHT:
5818                 default:
5819                     container.getStyle().setProperty("textAlign", "right");
5820                     break;
5821                 }
5822             }
5823 
5824             protected void initCellWithWidget(Widget w, char align,
5825                     String style, boolean sorted, final TableCellElement td) {
5826                 final Element container = DOM.createDiv();
5827                 String className = VScrollTablePatched.this.getStylePrimaryName()
5828                         + "-cell-content";
5829                 if (style != null && !style.equals("")) {
5830                     className += " " + VScrollTablePatched.this.getStylePrimaryName()
5831                             + "-cell-content-" + style;
5832                 }
5833                 if (sorted) {
5834                     className += " " + VScrollTablePatched.this.getStylePrimaryName()
5835                             + "-cell-content-sorted";
5836                 }
5837                 td.setClassName(className);
5838                 container.setClassName(VScrollTablePatched.this.getStylePrimaryName()
5839                         + "-cell-wrapper");
5840                 setAlign(align, container);
5841                 td.appendChild(container);
5842                 getElement().appendChild(td);
5843                 // ensure widget not attached to another element (possible tBody
5844                 // change)
5845                 w.removeFromParent();
5846                 container.appendChild(w.getElement());
5847                 adopt(w);
5848                 childWidgets.add(w);
5849             }
5850 
5851             @Override
5852             public Iterator<Widget> iterator() {
5853                 return childWidgets.iterator();
5854             }
5855 
5856             @Override
5857             public boolean remove(Widget w) {
5858                 if (childWidgets.contains(w)) {
5859                     orphan(w);
5860                     DOM.removeChild(DOM.getParent(w.getElement()),
5861                             w.getElement());
5862                     childWidgets.remove(w);
5863                     return true;
5864                 } else {
5865                     return false;
5866                 }
5867             }
5868 
5869             /**
5870              * If there are registered click listeners, sends a click event and
5871              * returns true. Otherwise, does nothing and returns false.
5872              * 
5873              * @param event
5874              * @param targetTdOrTr
5875              * @param immediate
5876              *            Whether the event is sent immediately
5877              * @return Whether a click event was sent
5878              */
5879             private boolean handleClickEvent(Event event, Element targetTdOrTr,
5880                     boolean immediate) {
5881                 if (!client.hasEventListeners(VScrollTablePatched.this,
5882                         TableConstants.ITEM_CLICK_EVENT_ID)) {
5883                     // Don't send an event if nobody is listening
5884                     return false;
5885                 }
5886 
5887                 // This row was clicked
5888                 client.updateVariable(paintableId, "clickedKey", "" + rowKey,
5889                         false);
5890 
5891                 if (getElement() == targetTdOrTr.getParentElement()) {
5892                     // A specific column was clicked
5893                     int childIndex = DOM.getChildIndex(getElement(),
5894                             targetTdOrTr);
5895                     String colKey = null;
5896                     colKey = tHead.getHeaderCell(childIndex).getColKey();
5897                     client.updateVariable(paintableId, "clickedColKey", colKey,
5898                             false);
5899                 }
5900 
5901                 MouseEventDetails details = MouseEventDetailsBuilder
5902                         .buildMouseEventDetails(event);
5903 
5904                 client.updateVariable(paintableId, "clickEvent",
5905                         details.toString(), immediate);
5906 
5907                 return true;
5908             }
5909 
5910             public TooltipInfo getTooltip(
5911                     com.google.gwt.dom.client.Element target) {
5912 
5913                 TooltipInfo info = null;
5914                 final Element targetTdOrTr = getTdOrTr(target);
5915                 if (targetTdOrTr != null
5916                         && "td".equals(targetTdOrTr.getTagName().toLowerCase())) {
5917                     TableCellElement td = (TableCellElement) targetTdOrTr
5918                             .cast();
5919                     info = cellToolTips.get(td);
5920                 }
5921 
5922                 if (info == null) {
5923                     info = tooltipInfo;
5924                 }
5925 
5926                 return info;
5927             }
5928 
5929             private Element getTdOrTr(Element target) {
5930                 Element thisTrElement = getElement();
5931                 if (target == thisTrElement) {
5932                     // This was a on the TR element
5933                     return target;
5934                 }
5935 
5936                 // Iterate upwards until we find the TR element
5937                 Element element = target;
5938                 while (element != null
5939                         && element.getParentElement() != thisTrElement) {
5940                     element = element.getParentElement();
5941                 }
5942                 return element;
5943             }
5944 
5945             /**
5946              * Special handler for touch devices that support native scrolling
5947              * 
5948              * @return Whether the event was handled by this method.
5949              */
5950             private boolean handleTouchEvent(final Event event) {
5951 
5952                 boolean touchEventHandled = false;
5953 
5954                 if (enabled && hasNativeTouchScrolling) {
5955                     touchContextProvider.handleTouchEvent(event);
5956 
5957                     final Element targetTdOrTr = getEventTargetTdOrTr(event);
5958                     final int type = event.getTypeInt();
5959 
5960                     switch (type) {
5961                     case Event.ONTOUCHSTART:
5962                         touchEventHandled = true;
5963                         touchStart = event;
5964                         isDragging = false;
5965                         Touch touch = event.getChangedTouches().get(0);
5966                         // save position to fields, touches in events are same
5967                         // instance during the operation.
5968                         touchStartX = touch.getClientX();
5969                         touchStartY = touch.getClientY();
5970 
5971                         if (dragmode != 0) {
5972                             if (dragTouchTimeout == null) {
5973                                 dragTouchTimeout = new Timer() {
5974 
5975                                     @Override
5976                                     public void run() {
5977                                         if (touchStart != null) {
5978                                             // Start a drag if a finger is held
5979                                             // in place long enough, then moved
5980                                             isDragging = true;
5981                                         }
5982                                     }
5983                                 };
5984                             }
5985                             dragTouchTimeout.schedule(TOUCHSCROLL_TIMEOUT);
5986                         }
5987 
5988                         if (actionKeys != null) {
5989                             if (contextTouchTimeout == null) {
5990                                 contextTouchTimeout = new Timer() {
5991 
5992                                     @Override
5993                                     public void run() {
5994                                         if (touchStart != null) {
5995                                             // Open the context menu if finger
5996                                             // is held in place long enough.
5997                                             showContextMenu(touchStart);
5998                                             event.preventDefault();
5999                                             touchStart = null;
6000                                         }
6001                                     }
6002                                 };
6003                             }
6004                             contextTouchTimeout
6005                                     .schedule(TOUCH_CONTEXT_MENU_TIMEOUT);
6006                             event.stopPropagation();
6007                         }
6008                         break;
6009                     case Event.ONTOUCHMOVE:
6010                         touchEventHandled = true;
6011                         if (isSignificantMove(event)) {
6012                             if (contextTouchTimeout != null) {
6013                                 // Moved finger before the context menu timer
6014                                 // expired, so let the browser handle this as a
6015                                 // scroll.
6016                                 contextTouchTimeout.cancel();
6017                                 contextTouchTimeout = null;
6018                             }
6019                             if (!isDragging && dragTouchTimeout != null) {
6020                                 // Moved finger before the drag timer expired,
6021                                 // so let the browser handle this as a scroll.
6022                                 dragTouchTimeout.cancel();
6023                                 dragTouchTimeout = null;
6024                             }
6025 
6026                             if (dragmode != 0 && touchStart != null
6027                                     && isDragging) {
6028                                 event.preventDefault();
6029                                 event.stopPropagation();
6030                                 startRowDrag(touchStart, type, targetTdOrTr);
6031                             }
6032                             touchStart = null;
6033                         }
6034                         break;
6035                     case Event.ONTOUCHEND:
6036                     case Event.ONTOUCHCANCEL:
6037                         touchEventHandled = true;
6038                         if (contextTouchTimeout != null) {
6039                             contextTouchTimeout.cancel();
6040                         }
6041                         if (dragTouchTimeout != null) {
6042                             dragTouchTimeout.cancel();
6043                         }
6044                         if (touchStart != null) {
6045                             if (!BrowserInfo.get().isAndroid()) {
6046                                 event.preventDefault();
6047                                 WidgetUtil.simulateClickFromTouchEvent(
6048                                         touchStart, this);
6049                             }
6050                             event.stopPropagation();
6051                             touchStart = null;
6052                         }
6053                         isDragging = false;
6054                         break;
6055                     }
6056                 }
6057                 return touchEventHandled;
6058             }
6059 
6060             /*
6061              * React on click that occur on content cells only
6062              */
6063 
6064             @Override
6065             public void onBrowserEvent(final Event event) {
6066 
6067                 final boolean touchEventHandled = handleTouchEvent(event);
6068 
6069                 if (enabled && !touchEventHandled) {
6070                     final int type = event.getTypeInt();
6071                     final Element targetTdOrTr = getEventTargetTdOrTr(event);
6072                     if (type == Event.ONCONTEXTMENU) {
6073                         showContextMenu(event);
6074                         if (enabled
6075                                 && (actionKeys != null || client
6076                                         .hasEventListeners(
6077                                                 VScrollTablePatched.this,
6078                                                 TableConstants.ITEM_CLICK_EVENT_ID))) {
6079                             /*
6080                              * Prevent browser context menu only if there are
6081                              * action handlers or item click listeners
6082                              * registered
6083                              */
6084                             event.stopPropagation();
6085                             event.preventDefault();
6086                         }
6087                         return;
6088                     }
6089 
6090                     boolean targetCellOrRowFound = targetTdOrTr != null;
6091 
6092                     switch (type) {
6093                     case Event.ONDBLCLICK:
6094                         if (targetCellOrRowFound) {
6095                             handleClickEvent(event, targetTdOrTr, true);
6096                         }
6097                         break;
6098                     case Event.ONMOUSEUP:
6099                         /*
6100                          * Only fire a click if the mouseup hits the same
6101                          * element as the corresponding mousedown. This is first
6102                          * checked in the event preview but we can't fire the
6103                          * event there as the event might get canceled before it
6104                          * gets here.
6105                          */
6106                         if (mouseUpPreviewMatched
6107                                 && lastMouseDownTarget != null
6108                                 && lastMouseDownTarget == getElementTdOrTr(WidgetUtil
6109                                         .getElementUnderMouse(event))) {
6110                             // "Click" with left, right or middle button
6111 
6112                             if (targetCellOrRowFound) {
6113                                 /*
6114                                  * Queue here, send at the same time as the
6115                                  * corresponding value change event - see #7127
6116                                  */
6117                                 boolean clickEventSent = handleClickEvent(
6118                                         event, targetTdOrTr, false);
6119 
6120                                 if (event.getButton() == Event.BUTTON_LEFT
6121                                         && isSelectable()) {
6122 
6123                                     // Ctrl+Shift click
6124                                     if ((event.getCtrlKey() || event
6125                                             .getMetaKey())
6126                                             && event.getShiftKey()
6127                                             && isMultiSelectModeDefault()) {
6128                                         toggleShiftSelection(false);
6129                                         setRowFocus(this);
6130 
6131                                         // Ctrl click
6132                                     } else if ((event.getCtrlKey() || event
6133                                             .getMetaKey())
6134                                             && isMultiSelectModeDefault()) {
6135                                         boolean wasSelected = isSelected();
6136                                         toggleSelection();
6137                                         setRowFocus(this);
6138                                         /*
6139                                          * next possible range select must start
6140                                          * on this row
6141                                          */
6142                                         selectionRangeStart = this;
6143                                         if (wasSelected) {
6144                                             removeRowFromUnsentSelectionRanges(this);
6145                                         }
6146 
6147                                     } else if ((event.getCtrlKey() || event
6148                                             .getMetaKey())
6149                                             && isSingleSelectMode()) {
6150                                         // Ctrl (or meta) click (Single
6151                                         // selection)
6152                                         if (!isSelected()
6153                                                 || (isSelected() && nullSelectionAllowed)) {
6154 
6155                                             if (!isSelected()) {
6156                                                 deselectAll();
6157                                             }
6158 
6159                                             toggleSelection();
6160                                             setRowFocus(this);
6161                                         }
6162 
6163                                     } else if (event.getShiftKey()
6164                                             && isMultiSelectModeDefault()) {
6165                                         // Shift click
6166                                         toggleShiftSelection(true);
6167 
6168                                     } else {
6169                                         // click
6170                                         boolean currentlyJustThisRowSelected = selectedRowKeys
6171                                                 .size() == 1
6172                                                 && selectedRowKeys
6173                                                         .contains(getKey());
6174 
6175                                         if (!currentlyJustThisRowSelected) {
6176                                             if (isSingleSelectMode()
6177                                                     || isMultiSelectModeDefault()) {
6178                                                 /*
6179                                                  * For default multi select mode
6180                                                  * (ctrl/shift) and for single
6181                                                  * select mode we need to clear
6182                                                  * the previous selection before
6183                                                  * selecting a new one when the
6184                                                  * user clicks on a row. Only in
6185                                                  * multiselect/simple mode the
6186                                                  * old selection should remain
6187                                                  * after a normal click.
6188                                                  */
6189                                                 deselectAll();
6190                                             }
6191                                             toggleSelection();
6192                                         } else if ((isSingleSelectMode() || isMultiSelectModeSimple())
6193                                                 && nullSelectionAllowed) {
6194                                             toggleSelection();
6195                                         }/*
6196                                           * else NOP to avoid excessive server
6197                                           * visits (selection is removed with
6198                                           * CTRL/META click)
6199                                           */
6200 
6201                                         selectionRangeStart = this;
6202                                         setRowFocus(this);
6203                                     }
6204 
6205                                     // Remove IE text selection hack
6206                                     if (BrowserInfo.get().isIE()) {
6207                                         ((Element) event.getEventTarget()
6208                                                 .cast()).setPropertyJSO(
6209                                                 "onselectstart", null);
6210                                     }
6211                                     // Queue value change
6212                                     sendSelectedRows(false);
6213                                 }
6214                                 /*
6215                                  * Send queued click and value change events if
6216                                  * any If a click event is sent, send value
6217                                  * change with it regardless of the immediate
6218                                  * flag, see #7127
6219                                  */
6220                                 if (immediate || clickEventSent) {
6221                                     client.sendPendingVariableChanges();
6222                                 }
6223                             }
6224                         }
6225                         mouseUpPreviewMatched = false;
6226                         lastMouseDownTarget = null;
6227                         break;
6228                     case Event.ONTOUCHEND:
6229                     case Event.ONTOUCHCANCEL:
6230                         if (touchStart != null) {
6231                             /*
6232                              * Touch has not been handled as neither context or
6233                              * drag start, handle it as a click.
6234                              */
6235                             WidgetUtil.simulateClickFromTouchEvent(touchStart,
6236                                     this);
6237                             touchStart = null;
6238                         }
6239                         touchContextProvider.cancel();
6240                         break;
6241                     case Event.ONTOUCHMOVE:
6242                         if (isSignificantMove(event)) {
6243                             /*
6244                              * TODO figure out scroll delegate don't eat events
6245                              * if row is selected. Null check for active
6246                              * delegate is as a workaround.
6247                              */
6248                             if (dragmode != 0
6249                                     && touchStart != null
6250                                     && (TouchScrollDelegate
6251                                             .getActiveScrollDelegate() == null)) {
6252                                 startRowDrag(touchStart, type, targetTdOrTr);
6253                             }
6254                             touchContextProvider.cancel();
6255                             /*
6256                              * Avoid clicks and drags by clearing touch start
6257                              * flag.
6258                              */
6259                             touchStart = null;
6260                         }
6261 
6262                         break;
6263                     case Event.ONTOUCHSTART:
6264                         touchStart = event;
6265                         Touch touch = event.getChangedTouches().get(0);
6266                         // save position to fields, touches in events are same
6267                         // instance during the operation.
6268                         touchStartX = touch.getClientX();
6269                         touchStartY = touch.getClientY();
6270                         /*
6271                          * Prevent simulated mouse events.
6272                          */
6273                         touchStart.preventDefault();
6274                         if (dragmode != 0 || actionKeys != null) {
6275                             new Timer() {
6276 
6277                                 @Override
6278                                 public void run() {
6279                                     TouchScrollDelegate activeScrollDelegate = TouchScrollDelegate
6280                                             .getActiveScrollDelegate();
6281                                     /*
6282                                      * If there's a scroll delegate, check if
6283                                      * we're actually scrolling and handle it.
6284                                      * If no delegate, do nothing here and let
6285                                      * the row handle potential drag'n'drop or
6286                                      * context menu.
6287                                      */
6288                                     if (activeScrollDelegate != null) {
6289                                         if (activeScrollDelegate.isMoved()) {
6290                                             /*
6291                                              * Prevent the row from handling
6292                                              * touch move/end events (the
6293                                              * delegate handles those) and from
6294                                              * doing drag'n'drop or opening a
6295                                              * context menu.
6296                                              */
6297                                             touchStart = null;
6298                                         } else {
6299                                             /*
6300                                              * Scrolling hasn't started, so
6301                                              * cancel delegate and let the row
6302                                              * handle potential drag'n'drop or
6303                                              * context menu.
6304                                              */
6305                                             activeScrollDelegate
6306                                                     .stopScrolling();
6307                                         }
6308                                     }
6309                                 }
6310                             }.schedule(TOUCHSCROLL_TIMEOUT);
6311 
6312                             if (contextTouchTimeout == null
6313                                     && actionKeys != null) {
6314                                 contextTouchTimeout = new Timer() {
6315 
6316                                     @Override
6317                                     public void run() {
6318                                         if (touchStart != null) {
6319                                             showContextMenu(touchStart);
6320                                             touchStart = null;
6321                                         }
6322                                     }
6323                                 };
6324                             }
6325                             if (contextTouchTimeout != null) {
6326                                 contextTouchTimeout.cancel();
6327                                 contextTouchTimeout
6328                                         .schedule(TOUCH_CONTEXT_MENU_TIMEOUT);
6329                             }
6330                         }
6331                         break;
6332                     case Event.ONMOUSEDOWN:
6333                         /*
6334                          * When getting a mousedown event, we must detect where
6335                          * the corresponding mouseup event if it's on a
6336                          * different part of the page.
6337                          */
6338                         lastMouseDownTarget = getElementTdOrTr(WidgetUtil
6339                                 .getElementUnderMouse(event));
6340                         mouseUpPreviewMatched = false;
6341                         mouseUpEventPreviewRegistration = Event
6342                                 .addNativePreviewHandler(mouseUpPreviewHandler);
6343 
6344                         if (targetCellOrRowFound) {
6345                             setRowFocus(this);
6346                             ensureFocus();
6347                             if (dragmode != 0
6348                                     && (event.getButton() == NativeEvent.BUTTON_LEFT)) {
6349                                 startRowDrag(event, type, targetTdOrTr);
6350 
6351                             } else if (event.getCtrlKey()
6352                                     || event.getShiftKey()
6353                                     || event.getMetaKey()
6354                                     && isMultiSelectModeDefault()) {
6355 
6356                                 // Prevent default text selection in Firefox
6357                                 event.preventDefault();
6358 
6359                                 // Prevent default text selection in IE
6360                                 if (BrowserInfo.get().isIE()) {
6361                                     ((Element) event.getEventTarget().cast())
6362                                             .setPropertyJSO(
6363                                                     "onselectstart",
6364                                                     getPreventTextSelectionIEHack());
6365                                 }
6366 
6367                                 event.stopPropagation();
6368                             }
6369                         }
6370                         break;
6371                     case Event.ONMOUSEOUT:
6372                         break;
6373                     default:
6374                         break;
6375                     }
6376                 }
6377                 super.onBrowserEvent(event);
6378             }
6379 
6380             private boolean isSignificantMove(Event event) {
6381                 if (touchStart == null) {
6382                     // no touch start
6383                     return false;
6384                 }
6385                 /*
6386                  * TODO calculate based on real distance instead of separate
6387                  * axis checks
6388                  */
6389                 Touch touch = event.getChangedTouches().get(0);
6390                 if (Math.abs(touch.getClientX() - touchStartX) > TouchScrollDelegate.SIGNIFICANT_MOVE_THRESHOLD) {
6391                     return true;
6392                 }
6393                 if (Math.abs(touch.getClientY() - touchStartY) > TouchScrollDelegate.SIGNIFICANT_MOVE_THRESHOLD) {
6394                     return true;
6395                 }
6396                 return false;
6397             }
6398 
6399             /**
6400              * Checks if the row represented by the row key has been selected
6401              * 
6402              * @param key
6403              *            The generated row key
6404              */
6405             private boolean rowKeyIsSelected(int rowKey) {
6406                 // Check single selections
6407                 if (selectedRowKeys.contains("" + rowKey)) {
6408                     return true;
6409                 }
6410 
6411                 // Check range selections
6412                 for (SelectionRange r : selectedRowRanges) {
6413                     if (r.inRange(getRenderedRowByKey("" + rowKey))) {
6414                         return true;
6415                     }
6416                 }
6417                 return false;
6418             }
6419 
6420             protected void startRowDrag(Event event, final int type,
6421                     Element targetTdOrTr) {
6422                 VTransferable transferable = new VTransferable();
6423                 transferable.setDragSource(ConnectorMap.get(client)
6424                         .getConnector(VScrollTablePatched.this));
6425                 transferable.setData("itemId", "" + rowKey);
6426                 NodeList<TableCellElement> cells = rowElement.getCells();
6427                 for (int i = 0; i < cells.getLength(); i++) {
6428                     if (cells.getItem(i).isOrHasChild(targetTdOrTr)) {
6429                         HeaderCell headerCell = tHead.getHeaderCell(i);
6430                         transferable.setData("propertyId", headerCell.cid);
6431                         break;
6432                     }
6433                 }
6434 
6435                 VDragEvent ev = VDragAndDropManager.get().startDrag(
6436                         transferable, event, true);
6437                 if (dragmode == DRAGMODE_MULTIROW && isMultiSelectModeAny()
6438                         && rowKeyIsSelected(rowKey)) {
6439 
6440                     // Create a drag image of ALL rows
6441                     ev.createDragImage(scrollBody.tBodyElement, true);
6442 
6443                     // Hide rows which are not selected
6444                     Element dragImage = ev.getDragImage();
6445                     int i = 0;
6446                     for (Iterator<Widget> iterator = scrollBody.iterator(); iterator
6447                             .hasNext();) {
6448                         VScrollTableRow next = (VScrollTableRow) iterator
6449                                 .next();
6450 
6451                         Element child = (Element) dragImage.getChild(i++);
6452 
6453                         if (!rowKeyIsSelected(next.rowKey)) {
6454                             child.getStyle().setVisibility(Visibility.HIDDEN);
6455                         }
6456                     }
6457                 } else {
6458                     ev.createDragImage(getElement(), true);
6459                 }
6460                 if (type == Event.ONMOUSEDOWN) {
6461                     event.preventDefault();
6462                 }
6463                 event.stopPropagation();
6464             }
6465 
6466             /**
6467              * Finds the TD that the event interacts with. Returns null if the
6468              * target of the event should not be handled. If the event target is
6469              * the row directly this method returns the TR element instead of
6470              * the TD.
6471              * 
6472              * @param event
6473              * @return TD or TR element that the event targets (the actual event
6474              *         target is this element or a child of it)
6475              */
6476             private Element getEventTargetTdOrTr(Event event) {
6477                 final Element eventTarget = event.getEventTarget().cast();
6478                 return getElementTdOrTr(eventTarget);
6479             }
6480 
6481             private Element getElementTdOrTr(Element element) {
6482 
6483                 Widget widget = WidgetUtil.findWidget(element, null);
6484 
6485                 if (widget != this) {
6486                     /*
6487                      * This is a workaround to make Labels, read only TextFields
6488                      * and Embedded in a Table clickable (see #2688). It is
6489                      * really not a fix as it does not work with a custom read
6490                      * only components (not extending VLabel/VEmbedded).
6491                      */
6492                     while (widget != null && widget.getParent() != this) {
6493                         widget = widget.getParent();
6494                     }
6495 
6496                     if (!(widget instanceof VLabel)
6497                             && !(widget instanceof VEmbedded)
6498                             && !(widget instanceof VTextField && ((VTextField) widget)
6499                                     .isReadOnly())) {
6500                         return null;
6501                     }
6502                 }
6503                 return getTdOrTr(element);
6504             }
6505 
6506             @Override
6507             public void showContextMenu(Event event) {
6508                 if (enabled && actionKeys != null) {
6509                     // Show context menu if there are registered action handlers
6510                     int left = WidgetUtil.getTouchOrMouseClientX(event)
6511                             + Window.getScrollLeft();
6512                     int top = WidgetUtil.getTouchOrMouseClientY(event)
6513                             + Window.getScrollTop();
6514                     showContextMenu(left, top);
6515                 }
6516             }
6517 
6518             public void showContextMenu(int left, int top) {
6519                 VContextMenu menu = client.getContextMenu();
6520                 contextMenu = new ContextMenuDetails(menu, getKey(), left, top);
6521                 menu.showAt(this, left, top);
6522             }
6523 
6524             /**
6525              * Has the row been selected?
6526              * 
6527              * @return Returns true if selected, else false
6528              */
6529             public boolean isSelected() {
6530                 return selected;
6531             }
6532 
6533             /**
6534              * Toggle the selection of the row
6535              */
6536             public void toggleSelection() {
6537                 selected = !selected;
6538                 selectionChanged = true;
6539                 if (selected) {
6540                     selectedRowKeys.add(String.valueOf(rowKey));
6541                     addStyleName("v-selected");
6542                 } else {
6543                     removeStyleName("v-selected");
6544                     selectedRowKeys.remove(String.valueOf(rowKey));
6545                 }
6546             }
6547 
6548             /**
6549              * Is called when a user clicks an item when holding SHIFT key down.
6550              * This will select a new range from the last focused row
6551              * 
6552              * @param deselectPrevious
6553              *            Should the previous selected range be deselected
6554              */
6555             private void toggleShiftSelection(boolean deselectPrevious) {
6556 
6557                 /*
6558                  * Ensures that we are in multiselect mode and that we have a
6559                  * previous selection which was not a deselection
6560                  */
6561                 if (isSingleSelectMode()) {
6562                     // No previous selection found
6563                     deselectAll();
6564                     toggleSelection();
6565                     return;
6566                 }
6567 
6568                 // Set the selectable range
6569                 VScrollTableRow endRow = this;
6570                 VScrollTableRow startRow = selectionRangeStart;
6571                 if (startRow == null) {
6572                     startRow = focusedRow;
6573                     selectionRangeStart = focusedRow;
6574                     // If start row is null then we have a multipage selection
6575                     // from above
6576                     if (startRow == null) {
6577                         startRow = (VScrollTableRow) scrollBody.iterator()
6578                                 .next();
6579                         setRowFocus(endRow);
6580                     }
6581                 } else if (!startRow.isSelected()) {
6582                     // The start row is no longer selected (probably removed)
6583                     // and so we select from above
6584                     startRow = (VScrollTableRow) scrollBody.iterator().next();
6585                     setRowFocus(endRow);
6586                 }
6587 
6588                 // Deselect previous items if so desired
6589                 if (deselectPrevious) {
6590                     deselectAll();
6591                 }
6592 
6593                 // we'll ensure GUI state from top down even though selection
6594                 // was the opposite way
6595                 if (!startRow.isBefore(endRow)) {
6596                     VScrollTableRow tmp = startRow;
6597                     startRow = endRow;
6598                     endRow = tmp;
6599                 }
6600                 SelectionRange range = new SelectionRange(startRow, endRow);
6601 
6602                 for (Widget w : scrollBody) {
6603                     VScrollTableRow row = (VScrollTableRow) w;
6604                     if (range.inRange(row)) {
6605                         if (!row.isSelected()) {
6606                             row.toggleSelection();
6607                         }
6608                         selectedRowKeys.add(row.getKey());
6609                     }
6610                 }
6611 
6612                 // Add range
6613                 if (startRow != endRow) {
6614                     selectedRowRanges.add(range);
6615                 }
6616             }
6617 
6618             /*
6619              * (non-Javadoc)
6620              * 
6621              * @see com.vaadin.client.ui.IActionOwner#getActions ()
6622              */
6623 
6624             @Override
6625             public Action[] getActions() {
6626                 if (actionKeys == null) {
6627                     return new Action[] {};
6628                 }
6629                 final Action[] actions = new Action[actionKeys.length];
6630                 for (int i = 0; i < actions.length; i++) {
6631                     final String actionKey = actionKeys[i];
6632                     final TreeAction a = new TreeAction(this,
6633                             String.valueOf(rowKey), actionKey) {
6634 
6635                         @Override
6636                         public void execute() {
6637                             super.execute();
6638                             lazyRevertFocusToRow(VScrollTableRow.this);
6639                         }
6640                     };
6641                     a.setCaption(getActionCaption(actionKey));
6642                     a.setIconUrl(getActionIcon(actionKey));
6643                     actions[i] = a;
6644                 }
6645                 return actions;
6646             }
6647 
6648             @Override
6649             public ApplicationConnection getClient() {
6650                 return client;
6651             }
6652 
6653             @Override
6654             public String getPaintableId() {
6655                 return paintableId;
6656             }
6657 
6658             private int getColIndexOf(Widget child) {
6659                 com.google.gwt.dom.client.Element widgetCell = child
6660                         .getElement().getParentElement().getParentElement();
6661                 NodeList<TableCellElement> cells = rowElement.getCells();
6662                 for (int i = 0; i < cells.getLength(); i++) {
6663                     if (cells.getItem(i) == widgetCell) {
6664                         return i;
6665                     }
6666                 }
6667                 return -1;
6668             }
6669 
6670             public Widget getWidgetForPaintable() {
6671                 return this;
6672             }
6673         }
6674 
6675         protected class VScrollTableGeneratedRow extends VScrollTableRow {
6676 
6677             private boolean spanColumns;
6678             private boolean htmlContentAllowed;
6679 
6680             public VScrollTableGeneratedRow(UIDL uidl, char[] aligns) {
6681                 super(uidl, aligns);
6682                 addStyleName("v-table-generated-row");
6683             }
6684 
6685             public boolean isSpanColumns() {
6686                 return spanColumns;
6687             }
6688 
6689             @Override
6690             protected void initCellWidths() {
6691                 if (spanColumns) {
6692                     setSpannedColumnWidthAfterDOMFullyInited();
6693                 } else {
6694                     super.initCellWidths();
6695                 }
6696             }
6697 
6698             private void setSpannedColumnWidthAfterDOMFullyInited() {
6699                 // Defer setting width on spanned columns to make sure that
6700                 // they are added to the DOM before trying to calculate
6701                 // widths.
6702                 Scheduler.get().scheduleDeferred(new ScheduledCommand() {
6703 
6704                     @Override
6705                     public void execute() {
6706                         if (showRowHeaders) {
6707                             setCellWidth(0, tHead.getHeaderCell(0)
6708                                     .getWidthWithIndent());
6709                             calcAndSetSpanWidthOnCell(1);
6710                         } else {
6711                             calcAndSetSpanWidthOnCell(0);
6712                         }
6713                     }
6714                 });
6715             }
6716 
6717             @Override
6718             protected boolean isRenderHtmlInCells() {
6719                 return htmlContentAllowed;
6720             }
6721 
6722             @Override
6723             protected void addCellsFromUIDL(UIDL uidl, char[] aligns, int col,
6724                     int visibleColumnIndex) {
6725                 htmlContentAllowed = uidl.getBooleanAttribute("gen_html");
6726                 spanColumns = uidl.getBooleanAttribute("gen_span");
6727 
6728                 final Iterator<?> cells = uidl.getChildIterator();
6729                 if (spanColumns) {
6730                     int colCount = uidl.getChildCount();
6731                     if (cells.hasNext()) {
6732                         final Object cell = cells.next();
6733                         if (cell instanceof String) {
6734                             addSpannedCell(uidl, cell.toString(), aligns[0],
6735                                     "", htmlContentAllowed, false, null,
6736                                     colCount);
6737                         } else {
6738                             addSpannedCell(uidl, (Widget) cell, aligns[0], "",
6739                                     false, colCount);
6740                         }
6741                     }
6742                 } else {
6743                     super.addCellsFromUIDL(uidl, aligns, col,
6744                             visibleColumnIndex);
6745                 }
6746             }
6747 
6748             private void addSpannedCell(UIDL rowUidl, Widget w, char align,
6749                     String style, boolean sorted, int colCount) {
6750                 TableCellElement td = DOM.createTD().cast();
6751                 td.setColSpan(colCount);
6752                 initCellWithWidget(w, align, style, sorted, td);
6753             }
6754 
6755             private void addSpannedCell(UIDL rowUidl, String text, char align,
6756                     String style, boolean textIsHTML, boolean sorted,
6757                     String description, int colCount) {
6758                 // String only content is optimized by not using Label widget
6759                 final TableCellElement td = DOM.createTD().cast();
6760                 td.setColSpan(colCount);
6761                 initCellWithText(text, align, style, textIsHTML, sorted,
6762                         description, td);
6763             }
6764 
6765             @Override
6766             protected void setCellWidth(int cellIx, int width) {
6767                 if (isSpanColumns()) {
6768                     if (showRowHeaders) {
6769                         if (cellIx == 0) {
6770                             super.setCellWidth(0, width);
6771                         } else {
6772                             // We need to recalculate the spanning TDs width for
6773                             // every cellIx in order to support column resizing.
6774                             calcAndSetSpanWidthOnCell(1);
6775                         }
6776                     } else {
6777                         // Same as above.
6778                         calcAndSetSpanWidthOnCell(0);
6779                     }
6780                 } else {
6781                     super.setCellWidth(cellIx, width);
6782                 }
6783             }
6784 
6785             private void calcAndSetSpanWidthOnCell(final int cellIx) {
6786                 int spanWidth = 0;
6787                 for (int ix = (showRowHeaders ? 1 : 0); ix < tHead
6788                         .getVisibleCellCount(); ix++) {
6789                     spanWidth += tHead.getHeaderCell(ix).getOffsetWidth();
6790                 }
6791                 WidgetUtil.setWidthExcludingPaddingAndBorder(
6792                         (Element) getElement().getChild(cellIx), spanWidth, 13,
6793                         false);
6794             }
6795         }
6796 
6797         /**
6798          * Ensure the component has a focus.
6799          * 
6800          * TODO the current implementation simply always calls focus for the
6801          * component. In case the Table at some point implements focus/blur
6802          * listeners, this method needs to be evolved to conditionally call
6803          * focus only if not currently focused.
6804          */
6805         protected void ensureFocus() {
6806             if (!hasFocus) {
6807                 scrollBodyPanel.setFocus(true);
6808             }
6809 
6810         }
6811 
6812     }
6813 
6814     /**
6815      * Deselects all items
6816      */
6817     public void deselectAll() {
6818         for (Widget w : scrollBody) {
6819             VScrollTableRow row = (VScrollTableRow) w;
6820             if (row.isSelected()) {
6821                 row.toggleSelection();
6822             }
6823         }
6824         // still ensure all selects are removed from (not necessary rendered)
6825         selectedRowKeys.clear();
6826         selectedRowRanges.clear();
6827         // also notify server that it clears all previous selections (the client
6828         // side does not know about the invisible ones)
6829         instructServerToForgetPreviousSelections();
6830     }
6831 
6832     /**
6833      * Used in multiselect mode when the client side knows that all selections
6834      * are in the next request.
6835      */
6836     private void instructServerToForgetPreviousSelections() {
6837         client.updateVariable(paintableId, "clearSelections", true, false);
6838     }
6839 
6840     /**
6841      * Determines the pagelength when the table height is fixed.
6842      */
6843     public void updatePageLength() {
6844         // Only update if visible and enabled
6845         if (!isVisibleInHierarchy() || !enabled) {
6846             return;
6847         }
6848 
6849         if (scrollBody == null) {
6850             return;
6851         }
6852 
6853         if (isDynamicHeight()) {
6854             return;
6855         }
6856 
6857         int rowHeight = (int) Math.round(scrollBody.getRowHeight());
6858         int bodyH = scrollBodyPanel.getOffsetHeight();
6859         int rowsAtOnce = bodyH / rowHeight;
6860         boolean anotherPartlyVisible = ((bodyH % rowHeight) != 0);
6861         if (anotherPartlyVisible) {
6862             rowsAtOnce++;
6863         }
6864         if (pageLength != rowsAtOnce) {
6865             pageLength = rowsAtOnce;
6866             client.updateVariable(paintableId, "pagelength", pageLength, false);
6867 
6868             if (!rendering) {
6869                 int currentlyVisible = scrollBody.getLastRendered()
6870                         - scrollBody.getFirstRendered();
6871                 if (currentlyVisible < pageLength
6872                         && currentlyVisible < totalRows) {
6873                     // shake scrollpanel to fill empty space
6874                     scrollBodyPanel.setScrollPosition(scrollTop + 1);
6875                     scrollBodyPanel.setScrollPosition(scrollTop - 1);
6876                 }
6877 
6878                 sizeNeedsInit = true;
6879             }
6880         }
6881 
6882     }
6883 
6884     /** For internal use only. May be removed or replaced in the future. */
6885     public void updateWidth() {
6886         if (!isVisibleInHierarchy()) {
6887             /*
6888              * Do not update size when the table is hidden as all column widths
6889              * will be set to zero and they won't be recalculated when the table
6890              * is set visible again (until the size changes again)
6891              */
6892             return;
6893         }
6894 
6895         if (!isDynamicWidth()) {
6896             int innerPixels = getOffsetWidth() - getBorderWidth();
6897             if (innerPixels < 0) {
6898                 innerPixels = 0;
6899             }
6900             setContentWidth(innerPixels);
6901 
6902             // readjust undefined width columns
6903             triggerLazyColumnAdjustment(false);
6904 
6905         } else {
6906 
6907             sizeNeedsInit = true;
6908 
6909             // readjust undefined width columns
6910             triggerLazyColumnAdjustment(false);
6911         }
6912 
6913         /*
6914          * setting width may affect wheter the component has scrollbars -> needs
6915          * scrolling or not
6916          */
6917         setProperTabIndex();
6918     }
6919 
6920     private static final int LAZY_COLUMN_ADJUST_TIMEOUT = 300;
6921 
6922     private final Timer lazyAdjustColumnWidths = new Timer() {
6923 
6924         private int formerWidth;
6925         private boolean formerScrolling;
6926 
6927         /**
6928          * Check for column widths, and available width, to see if we can fix
6929          * column widths "optimally". Doing this lazily to avoid expensive
6930          * calculation when resizing is not yet finished.
6931          */
6932 
6933         @Override
6934         public void run() {
6935             if (scrollBody == null || !isVisibleInHierarchy()) {
6936                 // Try again later if we get here before scrollBody has been
6937                 // initalized
6938                 triggerLazyColumnAdjustment(false);
6939                 return;
6940             }
6941 
6942             // MGNLUI-962 We don't want to recalculate column widths if table body's width itself hasn't changed.
6943             int width = scrollBody.getAvailableWidth();
6944             boolean scrolling = willHaveScrollbars();
6945             if (width != formerWidth || scrolling != formerScrolling) {
6946                 formerWidth = width;
6947                 formerScrolling = scrolling;
6948             } else {
6949                 return;
6950             }
6951 
6952             Iterator<Widget> headCells = tHead.iterator();
6953             int usedMinimumWidth = 0;
6954             int totalExplicitColumnsWidths = 0;
6955             float expandRatioDivider = 0;
6956             int colIndex = 0;
6957 
6958             int hierarchyColumnIndent = scrollBody.getMaxIndent();
6959             int hierarchyColumnIndex = getHierarchyColumnIndex();
6960             HeaderCell hierarchyHeaderInNeedOfFurtherHandling = null;
6961 
6962             while (headCells.hasNext()) {
6963                 final HeaderCell hCell = (HeaderCell) headCells.next();
6964                 boolean hasIndent = hierarchyColumnIndent > 0
6965                         && hCell.isHierarchyColumn();
6966                 if (hCell.isDefinedWidth()) {
6967                     // get width without indent to find out whether adjustments
6968                     // are needed (requires special handling further ahead)
6969                     int w = hCell.getWidth();
6970                     if (hasIndent && w < hierarchyColumnIndent) {
6971                         // enforce indent if necessary
6972                         w = hierarchyColumnIndent;
6973                         hierarchyHeaderInNeedOfFurtherHandling = hCell;
6974                     }
6975                     totalExplicitColumnsWidths += w;
6976                     usedMinimumWidth += w;
6977                 } else {
6978                     // natural width already includes indent if any
6979                     int naturalColumnWidth = hCell
6980                             .getNaturalColumnWidth(colIndex);
6981                     /*
6982                      * TODO If there is extra width, expand ratios are for
6983                      * additional extra widths, not for absolute column widths.
6984                      * Should be fixed in sizeInit(), too.
6985                      */
6986                     if (hCell.getExpandRatio() > 0) {
6987                         naturalColumnWidth = 0;
6988                     }
6989                     usedMinimumWidth += naturalColumnWidth;
6990                     expandRatioDivider += hCell.getExpandRatio();
6991                     if (hasIndent) {
6992                         hierarchyHeaderInNeedOfFurtherHandling = hCell;
6993                     }
6994                 }
6995                 colIndex++;
6996             }
6997 
6998             int availW = scrollBody.getAvailableWidth();
6999             // Hey IE, are you really sure about this?
7000             availW = scrollBody.getAvailableWidth();
7001             int visibleCellCount = tHead.getVisibleCellCount();
7002             int totalExtraWidth = scrollBody.getCellExtraWidth()
7003                     * visibleCellCount;
7004             if (willHaveScrollbars()) {
7005                 totalExtraWidth += WidgetUtil.getNativeScrollbarSize();
7006                 // if there will be vertical scrollbar, let's enable it
7007                 scrollBodyPanel.getElement().getStyle().clearOverflowY();
7008             } else {
7009                 // if there is no need for vertical scrollbar, let's disable it
7010                 // this is necessary since sometimes the browsers insist showing
7011                 // the scrollbar even if the content would fit perfectly
7012                 scrollBodyPanel.getElement().getStyle()
7013                         .setOverflowY(Overflow.HIDDEN);
7014             }
7015 
7016             availW -= totalExtraWidth;
7017             int forceScrollBodyWidth = -1;
7018 
7019             int extraSpace = availW - usedMinimumWidth;
7020             if (extraSpace < 0) {
7021                 if (getTotalRows() == 0) {
7022                     /*
7023                      * Too wide header combined with no rows in the table.
7024                      * 
7025                      * No horizontal scrollbars would be displayed because
7026                      * there's no rows that grows too wide causing the
7027                      * scrollBody container div to overflow. Must explicitely
7028                      * force a width to a scrollbar. (see #9187)
7029                      */
7030                     forceScrollBodyWidth = usedMinimumWidth + totalExtraWidth;
7031                 }
7032                 extraSpace = 0;
7033                 // if there will be horizontal scrollbar, let's enable it
7034                 scrollBodyPanel.getElement().getStyle().clearOverflowX();
7035             } else {
7036                 // if there is no need for horizontal scrollbar, let's disable
7037                 // it
7038                 // this is necessary since sometimes the browsers insist showing
7039                 // the scrollbar even if the content would fit perfectly
7040                 scrollBodyPanel.getElement().getStyle()
7041                         .setOverflowX(Overflow.HIDDEN);
7042             }
7043 
7044             if (forceScrollBodyWidth > 0) {
7045                 scrollBody.container.getStyle().setWidth(forceScrollBodyWidth,
7046                         Unit.PX);
7047             } else {
7048                 // Clear width that might have been set to force horizontal
7049                 // scrolling if there are no rows
7050                 scrollBody.container.getStyle().clearWidth();
7051             }
7052 
7053             int totalUndefinedNaturalWidths = usedMinimumWidth
7054                     - totalExplicitColumnsWidths;
7055 
7056             if (hierarchyHeaderInNeedOfFurtherHandling != null
7057                     && !hierarchyHeaderInNeedOfFurtherHandling.isDefinedWidth()) {
7058                 // ensure the cell gets enough space for the indent
7059                 int w = hierarchyHeaderInNeedOfFurtherHandling
7060                         .getNaturalColumnWidth(hierarchyColumnIndex);
7061                 int newSpace = Math.round(w + (float) extraSpace * (float) w
7062                         / totalUndefinedNaturalWidths);
7063                 if (newSpace >= hierarchyColumnIndent) {
7064                     // no special handling required
7065                     hierarchyHeaderInNeedOfFurtherHandling = null;
7066                 } else {
7067                     // treat as a defined width column of indent's width
7068                     totalExplicitColumnsWidths += hierarchyColumnIndent;
7069                     usedMinimumWidth -= w - hierarchyColumnIndent;
7070                     totalUndefinedNaturalWidths = usedMinimumWidth
7071                             - totalExplicitColumnsWidths;
7072                     expandRatioDivider += hierarchyHeaderInNeedOfFurtherHandling
7073                             .getExpandRatio();
7074                     extraSpace = Math.max(availW - usedMinimumWidth, 0);
7075                 }
7076             }
7077 
7078             // we have some space that can be divided optimally
7079             HeaderCell hCell;
7080             colIndex = 0;
7081             headCells = tHead.iterator();
7082             int checksum = 0;
7083             while (headCells.hasNext()) {
7084                 hCell = (HeaderCell) headCells.next();
7085                 if (hCell.isResizing) {
7086                     continue;
7087                 }
7088                 if (!hCell.isDefinedWidth()) {
7089                     int w = hCell.getNaturalColumnWidth(colIndex);
7090                     int newSpace;
7091                     if (expandRatioDivider > 0) {
7092                         // divide excess space by expand ratios
7093                         if (hCell.getExpandRatio() > 0) {
7094                             w = 0;
7095                         }
7096                         newSpace = Math.round((w + extraSpace
7097                                 * hCell.getExpandRatio() / expandRatioDivider));
7098                     } else {
7099                         if (hierarchyHeaderInNeedOfFurtherHandling == hCell) {
7100                             // still exists, so needs exactly the indent's width
7101                             newSpace = hierarchyColumnIndent;
7102                         } else if (totalUndefinedNaturalWidths != 0) {
7103                             // divide relatively to natural column widths
7104                             newSpace = Math.round(w + (float) extraSpace
7105                                     * (float) w / totalUndefinedNaturalWidths);
7106                         } else {
7107                             newSpace = w;
7108                         }
7109                     }
7110                     checksum += newSpace;
7111                     setColWidth(colIndex, newSpace, false);
7112 
7113                 } else {
7114                     if (hierarchyHeaderInNeedOfFurtherHandling == hCell) {
7115                         // defined with enforced into indent width
7116                         checksum += hierarchyColumnIndent;
7117                         setColWidth(colIndex, hierarchyColumnIndent, false);
7118                     } else {
7119                         int cellWidth = hCell.getWidthWithIndent();
7120                         checksum += cellWidth;
7121                         if (hCell.isHierarchyColumn()) {
7122                             // update in case the indent has changed
7123                             // (not detectable earlier)
7124                             setColWidth(colIndex, cellWidth, true);
7125                         }
7126                     }
7127                 }
7128                 colIndex++;
7129             }
7130 
7131             if (extraSpace > 0 && checksum != availW) {
7132                 /*
7133                  * There might be in some cases a rounding error of 1px when
7134                  * extra space is divided so if there is one then we give the
7135                  * first undefined column 1 more pixel
7136                  */
7137                 headCells = tHead.iterator();
7138                 colIndex = 0;
7139                 while (headCells.hasNext()) {
7140                     HeaderCell hc = (HeaderCell) headCells.next();
7141                     if (!hc.isResizing && !hc.isDefinedWidth()) {
7142                         setColWidth(colIndex, hc.getWidthWithIndent() + availW
7143                                 - checksum, false);
7144                         break;
7145                     }
7146                     colIndex++;
7147                 }
7148             }
7149 
7150             if (isDynamicHeight() && totalRows == pageLength) {
7151                 // fix body height (may vary if lazy loading is offhorizontal
7152                 // scrollbar appears/disappears)
7153                 int bodyHeight = scrollBody.getRequiredHeight();
7154                 boolean needsSpaceForHorizontalScrollbar = (availW < usedMinimumWidth);
7155                 if (needsSpaceForHorizontalScrollbar) {
7156                     bodyHeight += WidgetUtil.getNativeScrollbarSize();
7157                 }
7158                 int heightBefore = getOffsetHeight();
7159                 scrollBodyPanel.setHeight(bodyHeight + "px");
7160                 if (heightBefore != getOffsetHeight()) {
7161                     Util.notifyParentOfSizeChange(VScrollTablePatched.this, false);
7162                 }
7163             }
7164 
7165             forceRealignColumnHeaders();
7166         }
7167 
7168     };
7169 
7170     private void forceRealignColumnHeaders() {
7171         if (BrowserInfo.get().isIE()) {
7172             /*
7173              * IE does not fire onscroll event if scroll position is reverted to
7174              * 0 due to the content element size growth. Ensure headers are in
7175              * sync with content manually. Safe to use null event as we don't
7176              * actually use the event object in listener.
7177              */
7178             onScroll(null);
7179         }
7180     }
7181 
7182     /**
7183      * helper to set pixel size of head and body part
7184      * 
7185      * @param pixels
7186      */
7187     private void setContentWidth(int pixels) {
7188         tHead.setWidth(pixels + "px");
7189         scrollBodyPanel.setWidth(pixels + "px");
7190         tFoot.setWidth(pixels + "px");
7191     }
7192 
7193     private int borderWidth = -1;
7194 
7195     /**
7196      * @return border left + border right
7197      */
7198     private int getBorderWidth() {
7199         if (borderWidth < 0) {
7200             borderWidth = WidgetUtil.measureHorizontalPaddingAndBorder(
7201                     scrollBodyPanel.getElement(), 2);
7202             if (borderWidth < 0) {
7203                 borderWidth = 0;
7204             }
7205         }
7206         return borderWidth;
7207     }
7208 
7209     /**
7210      * Ensures scrollable area is properly sized. This method is used when fixed
7211      * size is used.
7212      */
7213     private int containerHeight;
7214 
7215     private void setContainerHeight() {
7216         if (!isDynamicHeight()) {
7217 
7218             /*
7219              * Android 2.3 cannot measure the height of the inline-block
7220              * properly, and will return the wrong offset height. So for android
7221              * 2.3 we set the element to a block element while measuring and
7222              * then restore it which yields the correct result. #11331
7223              */
7224             if (BrowserInfo.get().isAndroid23()) {
7225                 getElement().getStyle().setDisplay(Display.BLOCK);
7226             }
7227 
7228             containerHeight = getOffsetHeight();
7229             containerHeight -= showColHeaders ? tHead.getOffsetHeight() : 0;
7230             containerHeight -= tFoot.getOffsetHeight();
7231             containerHeight -= getContentAreaBorderHeight();
7232             if (containerHeight < 0) {
7233                 containerHeight = 0;
7234             }
7235 
7236             scrollBodyPanel.setHeight(containerHeight + "px");
7237 
7238             if (BrowserInfo.get().isAndroid23()) {
7239                 getElement().getStyle().clearDisplay();
7240             }
7241         }
7242     }
7243 
7244     private int contentAreaBorderHeight = -1;
7245     private int scrollLeft;
7246     private int scrollTop;
7247 
7248     /** For internal use only. May be removed or replaced in the future. */
7249     public VScrollTableDropHandler dropHandler;
7250 
7251     private boolean navKeyDown;
7252 
7253     /** For internal use only. May be removed or replaced in the future. */
7254     public boolean multiselectPending;
7255 
7256     /**
7257      * @return border top + border bottom of the scrollable area of table
7258      */
7259     private int getContentAreaBorderHeight() {
7260         if (contentAreaBorderHeight < 0) {
7261 
7262             scrollBodyPanel.getElement().getStyle()
7263                     .setOverflow(Overflow.HIDDEN);
7264             int oh = scrollBodyPanel.getOffsetHeight();
7265             int ch = scrollBodyPanel.getElement()
7266                     .getPropertyInt("clientHeight");
7267             contentAreaBorderHeight = oh - ch;
7268             scrollBodyPanel.getElement().getStyle().setOverflow(Overflow.AUTO);
7269         }
7270         return contentAreaBorderHeight;
7271     }
7272 
7273     @Override
7274     public void setHeight(String height) {
7275         if (height.length() == 0
7276                 && getElement().getStyle().getHeight().length() != 0) {
7277             /*
7278              * Changing from defined to undefined size -> should do a size init
7279              * to take page length into account again
7280              */
7281             sizeNeedsInit = true;
7282         }
7283         super.setHeight(height);
7284     }
7285 
7286     /** For internal use only. May be removed or replaced in the future. */
7287     public void updateHeight() {
7288         setContainerHeight();
7289 
7290         if (initializedAndAttached) {
7291             updatePageLength();
7292         }
7293 
7294         triggerLazyColumnAdjustment(false);
7295 
7296         /*
7297          * setting height may affect wheter the component has scrollbars ->
7298          * needs scrolling or not
7299          */
7300         setProperTabIndex();
7301 
7302     }
7303 
7304     /*
7305      * Overridden due Table might not survive of visibility change (scroll pos
7306      * lost). Example ITabPanel just set contained components invisible and back
7307      * when changing tabs.
7308      */
7309 
7310     @Override
7311     public void setVisible(boolean visible) {
7312         if (isVisible() != visible) {
7313             super.setVisible(visible);
7314             if (initializedAndAttached) {
7315                 if (visible) {
7316                     Scheduler.get().scheduleDeferred(new Command() {
7317 
7318                         @Override
7319                         public void execute() {
7320                             scrollBodyPanel
7321                                     .setScrollPosition(measureRowHeightOffset(firstRowInViewPort));
7322                         }
7323                     });
7324                 }
7325             }
7326         }
7327     }
7328 
7329     /**
7330      * Helper function to build html snippet for column or row headers
7331      * 
7332      * @param uidl
7333      *            possibly with values caption and icon
7334      * @return html snippet containing possibly an icon + caption text
7335      */
7336     protected String buildCaptionHtmlSnippet(UIDL uidl) {
7337         String s = uidl.hasAttribute("caption") ? uidl
7338                 .getStringAttribute("caption") : "";
7339         if (uidl.hasAttribute("icon")) {
7340             Icon icon = client.getIcon(uidl.getStringAttribute("icon"));
7341             icon.setAlternateText("icon");
7342             s = icon.getElement().getString() + s;
7343         }
7344         return s;
7345     }
7346 
7347     // Updates first visible row for the case we cannot wait
7348     // for onScroll
7349     private void updateFirstVisibleRow() {
7350         scrollTop = scrollBodyPanel.getScrollPosition();
7351         firstRowInViewPort = calcFirstRowInViewPort();
7352         int maxFirstRow = totalRows - pageLength;
7353         if (firstRowInViewPort > maxFirstRow && maxFirstRow >= 0) {
7354             firstRowInViewPort = maxFirstRow;
7355         }
7356         lastRequestedFirstvisible = firstRowInViewPort;
7357         client.updateVariable(paintableId, "firstvisible", firstRowInViewPort,
7358                 false);
7359     }
7360 
7361     /**
7362      * This method has logic which rows needs to be requested from server when
7363      * user scrolls
7364      */
7365 
7366     @Override
7367     public void onScroll(ScrollEvent event) {
7368         // Do not handle scroll events while there is scroll initiated from
7369         // server side which is not yet executed (#11454)
7370         if (isLazyScrollerActive()) {
7371             return;
7372         }
7373 
7374         scrollLeft = scrollBodyPanel.getElement().getScrollLeft();
7375         scrollTop = scrollBodyPanel.getScrollPosition();
7376         /*
7377          * #6970 - IE sometimes fires scroll events for a detached table.
7378          * 
7379          * FIXME initializedAndAttached should probably be renamed - its name
7380          * doesn't seem to reflect its semantics. onDetach() doesn't set it to
7381          * false, and changing that might break something else, so we need to
7382          * check isAttached() separately.
7383          */
7384         if (!initializedAndAttached || !isAttached()) {
7385             return;
7386         }
7387         if (!enabled) {
7388             scrollBodyPanel
7389                     .setScrollPosition(measureRowHeightOffset(firstRowInViewPort));
7390             return;
7391         }
7392 
7393         rowRequestHandler.cancel();
7394 
7395         if (BrowserInfo.get().isSafari() && event != null && scrollTop == 0) {
7396             // due to the webkitoverflowworkaround, top may sometimes report 0
7397             // for webkit, although it really is not. Expecting to have the
7398             // correct
7399             // value available soon.
7400             Scheduler.get().scheduleDeferred(new Command() {
7401 
7402                 @Override
7403                 public void execute() {
7404                     onScroll(null);
7405                 }
7406             });
7407             return;
7408         }
7409 
7410         // fix headers horizontal scrolling
7411         tHead.setHorizontalScrollPosition(scrollLeft);
7412 
7413         // fix footers horizontal scrolling
7414         tFoot.setHorizontalScrollPosition(scrollLeft);
7415 
7416         if (totalRows == 0) {
7417             // No rows, no need to fetch new rows
7418             return;
7419         }
7420 
7421         firstRowInViewPort = calcFirstRowInViewPort();
7422         int maxFirstRow = totalRows - pageLength;
7423         if (firstRowInViewPort > maxFirstRow && maxFirstRow >= 0) {
7424             firstRowInViewPort = maxFirstRow;
7425         }
7426 
7427         int postLimit = (int) (firstRowInViewPort + (pageLength - 1) + pageLength
7428                 * cache_react_rate);
7429         if (postLimit > totalRows - 1) {
7430             postLimit = totalRows - 1;
7431         }
7432         int preLimit = (int) (firstRowInViewPort - pageLength
7433                 * cache_react_rate);
7434         if (preLimit < 0) {
7435             preLimit = 0;
7436         }
7437         final int lastRendered = scrollBody.getLastRendered();
7438         final int firstRendered = scrollBody.getFirstRendered();
7439 
7440         if (postLimit <= lastRendered && preLimit >= firstRendered) {
7441             // we're within no-react area, no need to request more rows
7442             // remember which firstvisible we requested, in case the server has
7443             // a differing opinion
7444             lastRequestedFirstvisible = firstRowInViewPort;
7445             client.updateVariable(paintableId, "firstvisible",
7446                     firstRowInViewPort, false);
7447             return;
7448         }
7449 
7450         if (allRenderedRowsAreNew()) {
7451             // need a totally new set of rows
7452             rowRequestHandler
7453                     .setReqFirstRow((firstRowInViewPort - (int) (pageLength * cache_rate)));
7454             int last = firstRowInViewPort + (int) (cache_rate * pageLength)
7455                     + pageLength - 1;
7456             if (last >= totalRows) {
7457                 last = totalRows - 1;
7458             }
7459             rowRequestHandler.setReqRows(last
7460                     - rowRequestHandler.getReqFirstRow() + 1);
7461             updatedReqRows = false;
7462             rowRequestHandler.deferRowFetch();
7463             return;
7464         }
7465         if (preLimit < firstRendered) {
7466             // need some rows to the beginning of the rendered area
7467 
7468             rowRequestHandler
7469                     .setReqFirstRow((int) (firstRowInViewPort - pageLength
7470                             * cache_rate));
7471             rowRequestHandler.setReqRows(firstRendered
7472                     - rowRequestHandler.getReqFirstRow());
7473             rowRequestHandler.deferRowFetch();
7474 
7475             return;
7476         }
7477         if (postLimit > lastRendered) {
7478             // need some rows to the end of the rendered area
7479             int reqRows = (int) ((firstRowInViewPort + pageLength + pageLength
7480                     * cache_rate) - lastRendered);
7481             rowRequestHandler.triggerRowFetch(lastRendered + 1, reqRows);
7482         }
7483     }
7484 
7485     private boolean allRenderedRowsAreNew() {
7486         int firstRowInViewPort = calcFirstRowInViewPort();
7487         int firstRendered = scrollBody.getFirstRendered();
7488         int lastRendered = scrollBody.getLastRendered();
7489         return (firstRowInViewPort - pageLength * cache_rate > lastRendered || firstRowInViewPort
7490                 + pageLength + pageLength * cache_rate < firstRendered);
7491     }
7492 
7493     protected int calcFirstRowInViewPort() {
7494         return (int) Math.ceil(scrollTop / scrollBody.getRowHeight());
7495     }
7496 
7497     @Override
7498     public VScrollTableDropHandler getDropHandler() {
7499         return dropHandler;
7500     }
7501 
7502     private static class TableDDDetails {
7503         int overkey = -1;
7504         VerticalDropLocation dropLocation;
7505         String colkey;
7506 
7507         @Override
7508         public boolean equals(Object obj) {
7509             if (obj instanceof TableDDDetails) {
7510                 TableDDDetails other = (TableDDDetails) obj;
7511                 return dropLocation == other.dropLocation
7512                         && overkey == other.overkey
7513                         && ((colkey != null && colkey.equals(other.colkey)) || (colkey == null && other.colkey == null));
7514             }
7515             return false;
7516         }
7517 
7518         //
7519         // public int hashCode() {
7520         // return overkey;
7521         // }
7522     }
7523 
7524     public class VScrollTableDropHandler extends VAbstractDropHandler {
7525 
7526         private static final String ROWSTYLEBASE = "v-table-row-drag-";
7527         private TableDDDetails dropDetails;
7528         private TableDDDetails lastEmphasized;
7529 
7530         @Override
7531         public void dragEnter(VDragEvent drag) {
7532             updateDropDetails(drag);
7533             super.dragEnter(drag);
7534         }
7535 
7536         private void updateDropDetails(VDragEvent drag) {
7537             dropDetails = new TableDDDetails();
7538             Element elementOver = drag.getElementOver();
7539 
7540             Class<? extends Widget> clazz = getRowClass();
7541             VScrollTableRow row = null;
7542             if (clazz != null) {
7543                 row = WidgetUtil.findWidget(elementOver, clazz);
7544             }
7545             if (row != null) {
7546                 dropDetails.overkey = row.rowKey;
7547                 Element tr = row.getElement();
7548                 Element element = elementOver;
7549                 while (element != null && element.getParentElement() != tr) {
7550                     element = element.getParentElement();
7551                 }
7552                 int childIndex = DOM.getChildIndex(tr, element);
7553                 dropDetails.colkey = tHead.getHeaderCell(childIndex)
7554                         .getColKey();
7555                 dropDetails.dropLocation = DDUtil.getVerticalDropLocation(
7556                         row.getElement(), drag.getCurrentGwtEvent(), 0.2);
7557             }
7558 
7559             drag.getDropDetails().put("itemIdOver", dropDetails.overkey + "");
7560             drag.getDropDetails().put(
7561                     "detail",
7562                     dropDetails.dropLocation != null ? dropDetails.dropLocation
7563                             .toString() : null);
7564 
7565         }
7566 
7567         private Class<? extends Widget> getRowClass() {
7568             // get the row type this way to make dd work in derived
7569             // implementations
7570             Iterator<Widget> iterator = scrollBody.iterator();
7571             if (iterator.hasNext()) {
7572                 return iterator.next().getClass();
7573             } else {
7574                 return null;
7575             }
7576         }
7577 
7578         @Override
7579         public void dragOver(VDragEvent drag) {
7580             TableDDDetails oldDetails = dropDetails;
7581             updateDropDetails(drag);
7582             if (!oldDetails.equals(dropDetails)) {
7583                 deEmphasis();
7584                 final TableDDDetails newDetails = dropDetails;
7585                 VAcceptCallback cb = new VAcceptCallback() {
7586 
7587                     @Override
7588                     public void accepted(VDragEvent event) {
7589                         if (newDetails.equals(dropDetails)) {
7590                             dragAccepted(event);
7591                         }
7592                         /*
7593                          * Else new target slot already defined, ignore
7594                          */
7595                     }
7596                 };
7597                 validate(cb, drag);
7598             }
7599         }
7600 
7601         @Override
7602         public void dragLeave(VDragEvent drag) {
7603             deEmphasis();
7604             super.dragLeave(drag);
7605         }
7606 
7607         @Override
7608         public boolean drop(VDragEvent drag) {
7609             deEmphasis();
7610             return super.drop(drag);
7611         }
7612 
7613         private void deEmphasis() {
7614             UIObject.setStyleName(getElement(),
7615                     getStylePrimaryName() + "-drag", false);
7616             if (lastEmphasized == null) {
7617                 return;
7618             }
7619             for (Widget w : scrollBody.renderedRows) {
7620                 VScrollTableRow row = (VScrollTableRow) w;
7621                 if (lastEmphasized != null
7622                         && row.rowKey == lastEmphasized.overkey) {
7623                     String stylename = ROWSTYLEBASE
7624                             + lastEmphasized.dropLocation.toString()
7625                                     .toLowerCase();
7626                     VScrollTableRow.setStyleName(row.getElement(), stylename,
7627                             false);
7628                     lastEmphasized = null;
7629                     return;
7630                 }
7631             }
7632         }
7633 
7634         /**
7635          * TODO needs different drop modes ?? (on cells, on rows), now only
7636          * supports rows
7637          */
7638         private void emphasis(TableDDDetails details) {
7639             deEmphasis();
7640             UIObject.setStyleName(getElement(),
7641                     getStylePrimaryName() + "-drag", true);
7642             // iterate old and new emphasized row
7643             for (Widget w : scrollBody.renderedRows) {
7644                 VScrollTableRow row = (VScrollTableRow) w;
7645                 if (details != null && details.overkey == row.rowKey) {
7646                     String stylename = ROWSTYLEBASE
7647                             + details.dropLocation.toString().toLowerCase();
7648                     VScrollTableRow.setStyleName(row.getElement(), stylename,
7649                             true);
7650                     lastEmphasized = details;
7651                     return;
7652                 }
7653             }
7654         }
7655 
7656         @Override
7657         protected void dragAccepted(VDragEvent drag) {
7658             emphasis(dropDetails);
7659         }
7660 
7661         @Override
7662         public ComponentConnector getConnector() {
7663             return ConnectorMap.get(client).getConnector(VScrollTablePatched.this);
7664         }
7665 
7666         @Override
7667         public ApplicationConnection getApplicationConnection() {
7668             return client;
7669         }
7670 
7671     }
7672 
7673     protected VScrollTableRow getFocusedRow() {
7674         return focusedRow;
7675     }
7676 
7677     /**
7678      * Moves the selection head to a specific row
7679      * 
7680      * @param row
7681      *            The row to where the selection head should move
7682      * @return Returns true if focus was moved successfully, else false
7683      */
7684     public boolean setRowFocus(VScrollTableRow row) {
7685 
7686         if (!isSelectable()) {
7687             return false;
7688         }
7689 
7690         // Remove previous selection
7691         if (focusedRow != null && focusedRow != row) {
7692             focusedRow.removeStyleName(getStylePrimaryName() + "-focus");
7693         }
7694 
7695         if (row != null) {
7696             // Apply focus style to new selection
7697             row.addStyleName(getStylePrimaryName() + "-focus");
7698 
7699             /*
7700              * Trying to set focus on already focused row
7701              */
7702             if (row == focusedRow) {
7703                 return false;
7704             }
7705 
7706             // Set new focused row
7707             focusedRow = row;
7708 
7709             if (hasFocus) {
7710                 ensureRowIsVisible(row);
7711             }
7712 
7713             return true;
7714         }
7715 
7716         return false;
7717     }
7718 
7719     /**
7720      * Ensures that the row is visible
7721      * 
7722      * @param row
7723      *            The row to ensure is visible
7724      */
7725     private void ensureRowIsVisible(VScrollTableRow row) {
7726         if (BrowserInfo.get().isTouchDevice()) {
7727             // Skip due to android devices that have broken scrolltop will may
7728             // get odd scrolling here.
7729             return;
7730         }
7731         /*
7732          * FIXME The next line doesn't always do what expected, because if the
7733          * row is not in the DOM it won't scroll to it.
7734          */
7735         WidgetUtil.scrollIntoViewVertically(row.getElement());
7736     }
7737 
7738     /**
7739      * Handles the keyboard events handled by the table
7740      * 
7741      * @param event
7742      *            The keyboard event received
7743      * @return true iff the navigation event was handled
7744      */
7745     protected boolean handleNavigation(int keycode, boolean ctrl, boolean shift) {
7746         if (keycode == KeyCodes.KEY_TAB || keycode == KeyCodes.KEY_SHIFT) {
7747             // Do not handle tab key
7748             return false;
7749         }
7750 
7751         // Down navigation
7752         if (!isSelectable() && keycode == getNavigationDownKey()) {
7753             scrollBodyPanel.setScrollPosition(scrollBodyPanel
7754                     .getScrollPosition() + scrollingVelocity);
7755             return true;
7756         } else if (keycode == getNavigationDownKey()) {
7757             if (isMultiSelectModeAny() && moveFocusDown()) {
7758                 selectFocusedRow(ctrl, shift);
7759 
7760             } else if (isSingleSelectMode() && !shift && moveFocusDown()) {
7761                 selectFocusedRow(ctrl, shift);
7762             }
7763             return true;
7764         }
7765 
7766         // Up navigation
7767         if (!isSelectable() && keycode == getNavigationUpKey()) {
7768             scrollBodyPanel.setScrollPosition(scrollBodyPanel
7769                     .getScrollPosition() - scrollingVelocity);
7770             return true;
7771         } else if (keycode == getNavigationUpKey()) {
7772             if (isMultiSelectModeAny() && moveFocusUp()) {
7773                 selectFocusedRow(ctrl, shift);
7774             } else if (isSingleSelectMode() && !shift && moveFocusUp()) {
7775                 selectFocusedRow(ctrl, shift);
7776             }
7777             return true;
7778         }
7779 
7780         if (keycode == getNavigationLeftKey()) {
7781             // Left navigation
7782             scrollBodyPanel.setHorizontalScrollPosition(scrollBodyPanel
7783                     .getHorizontalScrollPosition() - scrollingVelocity);
7784             return true;
7785 
7786         } else if (keycode == getNavigationRightKey()) {
7787             // Right navigation
7788             scrollBodyPanel.setHorizontalScrollPosition(scrollBodyPanel
7789                     .getHorizontalScrollPosition() + scrollingVelocity);
7790             return true;
7791         }
7792 
7793         // Select navigation
7794         if (isSelectable() && keycode == getNavigationSelectKey()) {
7795             if (isSingleSelectMode()) {
7796                 boolean wasSelected = focusedRow.isSelected();
7797                 deselectAll();
7798                 if (!wasSelected || !nullSelectionAllowed) {
7799                     focusedRow.toggleSelection();
7800                 }
7801             } else {
7802                 focusedRow.toggleSelection();
7803                 removeRowFromUnsentSelectionRanges(focusedRow);
7804             }
7805 
7806             sendSelectedRows();
7807             return true;
7808         }
7809 
7810         // Page Down navigation
7811         if (keycode == getNavigationPageDownKey()) {
7812             if (isSelectable()) {
7813                 /*
7814                  * If selectable we plagiate MSW behaviour: first scroll to the
7815                  * end of current view. If at the end, scroll down one page
7816                  * length and keep the selected row in the bottom part of
7817                  * visible area.
7818                  */
7819                 if (!isFocusAtTheEndOfTable()) {
7820                     VScrollTableRow lastVisibleRowInViewPort = scrollBody
7821                             .getRowByRowIndex(firstRowInViewPort
7822                                     + getFullyVisibleRowCount() - 1);
7823                     if (lastVisibleRowInViewPort != null
7824                             && lastVisibleRowInViewPort != focusedRow) {
7825                         // focused row is not at the end of the table, move
7826                         // focus and select the last visible row
7827                         setRowFocus(lastVisibleRowInViewPort);
7828                         selectFocusedRow(ctrl, shift);
7829                         updateFirstVisibleAndSendSelectedRows();
7830                     } else {
7831                         int indexOfToBeFocused = focusedRow.getIndex()
7832                                 + getFullyVisibleRowCount();
7833                         if (indexOfToBeFocused >= totalRows) {
7834                             indexOfToBeFocused = totalRows - 1;
7835                         }
7836                         VScrollTableRow toBeFocusedRow = scrollBody
7837                                 .getRowByRowIndex(indexOfToBeFocused);
7838 
7839                         if (toBeFocusedRow != null) {
7840                             /*
7841                              * if the next focused row is rendered
7842                              */
7843                             setRowFocus(toBeFocusedRow);
7844                             selectFocusedRow(ctrl, shift);
7845                             // TODO needs scrollintoview ?
7846                             updateFirstVisibleAndSendSelectedRows();
7847                         } else {
7848                             // scroll down by pixels and return, to wait for
7849                             // new rows, then select the last item in the
7850                             // viewport
7851                             selectLastItemInNextRender = true;
7852                             multiselectPending = shift;
7853                             scrollByPagelength(1);
7854                         }
7855                     }
7856                 }
7857             } else {
7858                 /* No selections, go page down by scrolling */
7859                 scrollByPagelength(1);
7860             }
7861             return true;
7862         }
7863 
7864         // Page Up navigation
7865         if (keycode == getNavigationPageUpKey()) {
7866             if (isSelectable()) {
7867                 /*
7868                  * If selectable we plagiate MSW behaviour: first scroll to the
7869                  * end of current view. If at the end, scroll down one page
7870                  * length and keep the selected row in the bottom part of
7871                  * visible area.
7872                  */
7873                 if (!isFocusAtTheBeginningOfTable()) {
7874                     VScrollTableRow firstVisibleRowInViewPort = scrollBody
7875                             .getRowByRowIndex(firstRowInViewPort);
7876                     if (firstVisibleRowInViewPort != null
7877                             && firstVisibleRowInViewPort != focusedRow) {
7878                         // focus is not at the beginning of the table, move
7879                         // focus and select the first visible row
7880                         setRowFocus(firstVisibleRowInViewPort);
7881                         selectFocusedRow(ctrl, shift);
7882                         updateFirstVisibleAndSendSelectedRows();
7883                     } else {
7884                         int indexOfToBeFocused = focusedRow.getIndex()
7885                                 - getFullyVisibleRowCount();
7886                         if (indexOfToBeFocused < 0) {
7887                             indexOfToBeFocused = 0;
7888                         }
7889                         VScrollTableRow toBeFocusedRow = scrollBody
7890                                 .getRowByRowIndex(indexOfToBeFocused);
7891 
7892                         if (toBeFocusedRow != null) { // if the next focused row
7893                                                       // is rendered
7894                             setRowFocus(toBeFocusedRow);
7895                             selectFocusedRow(ctrl, shift);
7896                             // TODO needs scrollintoview ?
7897                             updateFirstVisibleAndSendSelectedRows();
7898                         } else {
7899                             // unless waiting for the next rowset already
7900                             // scroll down by pixels and return, to wait for
7901                             // new rows, then select the last item in the
7902                             // viewport
7903                             selectFirstItemInNextRender = true;
7904                             multiselectPending = shift;
7905                             scrollByPagelength(-1);
7906                         }
7907                     }
7908                 }
7909             } else {
7910                 /* No selections, go page up by scrolling */
7911                 scrollByPagelength(-1);
7912             }
7913 
7914             return true;
7915         }
7916 
7917         // Goto start navigation
7918         if (keycode == getNavigationStartKey()) {
7919             scrollBodyPanel.setScrollPosition(0);
7920             if (isSelectable()) {
7921                 if (focusedRow != null && focusedRow.getIndex() == 0) {
7922                     return false;
7923                 } else {
7924                     VScrollTableRow rowByRowIndex = (VScrollTableRow) scrollBody
7925                             .iterator().next();
7926                     if (rowByRowIndex.getIndex() == 0) {
7927                         setRowFocus(rowByRowIndex);
7928                         selectFocusedRow(ctrl, shift);
7929                         updateFirstVisibleAndSendSelectedRows();
7930                     } else {
7931                         // first row of table will come in next row fetch
7932                         if (ctrl) {
7933                             focusFirstItemInNextRender = true;
7934                         } else {
7935                             selectFirstItemInNextRender = true;
7936                             multiselectPending = shift;
7937                         }
7938                     }
7939                 }
7940             }
7941             return true;
7942         }
7943 
7944         // Goto end navigation
7945         if (keycode == getNavigationEndKey()) {
7946             scrollBodyPanel.setScrollPosition(scrollBody.getOffsetHeight());
7947             if (isSelectable()) {
7948                 final int lastRendered = scrollBody.getLastRendered();
7949                 if (lastRendered + 1 == totalRows) {
7950                     VScrollTableRow rowByRowIndex = scrollBody
7951                             .getRowByRowIndex(lastRendered);
7952                     if (focusedRow != rowByRowIndex) {
7953                         setRowFocus(rowByRowIndex);
7954                         selectFocusedRow(ctrl, shift);
7955                         updateFirstVisibleAndSendSelectedRows();
7956                     }
7957                 } else {
7958                     if (ctrl) {
7959                         focusLastItemInNextRender = true;
7960                     } else {
7961                         selectLastItemInNextRender = true;
7962                         multiselectPending = shift;
7963                     }
7964                 }
7965             }
7966             return true;
7967         }
7968 
7969         return false;
7970     }
7971 
7972     private boolean isFocusAtTheBeginningOfTable() {
7973         return focusedRow.getIndex() == 0;
7974     }
7975 
7976     private boolean isFocusAtTheEndOfTable() {
7977         return focusedRow.getIndex() + 1 >= totalRows;
7978     }
7979 
7980     private int getFullyVisibleRowCount() {
7981         return (int) (scrollBodyPanel.getOffsetHeight() / scrollBody
7982                 .getRowHeight());
7983     }
7984 
7985     private void scrollByPagelength(int i) {
7986         int pixels = i * scrollBodyPanel.getOffsetHeight();
7987         int newPixels = scrollBodyPanel.getScrollPosition() + pixels;
7988         if (newPixels < 0) {
7989             newPixels = 0;
7990         } // else if too high, NOP (all know browsers accept illegally big
7991           // values here)
7992         scrollBodyPanel.setScrollPosition(newPixels);
7993     }
7994 
7995     /*
7996      * (non-Javadoc)
7997      * 
7998      * @see
7999      * com.google.gwt.event.dom.client.FocusHandler#onFocus(com.google.gwt.event
8000      * .dom.client.FocusEvent)
8001      */
8002 
8003     @Override
8004     public void onFocus(FocusEvent event) {
8005         if (isFocusable()) {
8006             hasFocus = true;
8007 
8008             // Focus a row if no row is in focus
8009             if (focusedRow == null) {
8010                 focusRowFromBody();
8011             } else {
8012                 setRowFocus(focusedRow);
8013             }
8014         }
8015     }
8016 
8017     /*
8018      * (non-Javadoc)
8019      * 
8020      * @see
8021      * com.google.gwt.event.dom.client.BlurHandler#onBlur(com.google.gwt.event
8022      * .dom.client.BlurEvent)
8023      */
8024 
8025     @Override
8026     public void onBlur(BlurEvent event) {
8027         hasFocus = false;
8028         navKeyDown = false;
8029 
8030         if (BrowserInfo.get().isIE()) {
8031             /*
8032              * IE sometimes moves focus to a clicked table cell... (#7965)
8033              * ...and sometimes it sends blur events even though the focus
8034              * handler is still active. (#10464)
8035              */
8036             Element focusedElement = WidgetUtil.getFocusedElement();
8037             if (Util.getConnectorForElement(client, getParent(), focusedElement) == this
8038                     && focusedElement != null
8039                     && focusedElement != scrollBodyPanel.getFocusElement()) {
8040                 /*
8041                  * Steal focus back to the focus handler if it was moved to some
8042                  * other part of the table. Avoid stealing focus in other cases.
8043                  */
8044                 focus();
8045                 return;
8046             }
8047         }
8048 
8049         if (isFocusable()) {
8050             // Unfocus any row
8051             setRowFocus(null);
8052         }
8053     }
8054 
8055     /**
8056      * Removes a key from a range if the key is found in a selected range
8057      * 
8058      * @param key
8059      *            The key to remove
8060      */
8061     public void removeRowFromUnsentSelectionRanges(VScrollTableRow row) {
8062         Collection<SelectionRange> newRanges = null;
8063         for (Iterator<SelectionRange> iterator = selectedRowRanges.iterator(); iterator
8064                 .hasNext();) {
8065             SelectionRange range = iterator.next();
8066             if (range.inRange(row)) {
8067                 // Split the range if given row is in range
8068                 Collection<SelectionRange> splitranges = range.split(row);
8069                 if (newRanges == null) {
8070                     newRanges = new ArrayList<SelectionRange>();
8071                 }
8072                 newRanges.addAll(splitranges);
8073                 iterator.remove();
8074             }
8075         }
8076         if (newRanges != null) {
8077             selectedRowRanges.addAll(newRanges);
8078         }
8079     }
8080 
8081     /**
8082      * Can the Table be focused?
8083      * 
8084      * @return True if the table can be focused, else false
8085      */
8086     public boolean isFocusable() {
8087         if (scrollBody != null && enabled) {
8088             return !(!hasHorizontalScrollbar() && !hasVerticalScrollbar() && !isSelectable());
8089         }
8090         return false;
8091     }
8092 
8093     private boolean hasHorizontalScrollbar() {
8094         return scrollBody.getOffsetWidth() > scrollBodyPanel.getOffsetWidth();
8095     }
8096 
8097     private boolean hasVerticalScrollbar() {
8098         return scrollBody.getOffsetHeight() > scrollBodyPanel.getOffsetHeight();
8099     }
8100 
8101     /*
8102      * (non-Javadoc)
8103      * 
8104      * @see com.vaadin.client.Focusable#focus()
8105      */
8106 
8107     @Override
8108     public void focus() {
8109         if (isFocusable()) {
8110             scrollBodyPanel.focus();
8111         }
8112     }
8113 
8114     /**
8115      * Sets the proper tabIndex for scrollBodyPanel (the focusable elemen in the
8116      * component).
8117      * <p>
8118      * If the component has no explicit tabIndex a zero is given (default
8119      * tabbing order based on dom hierarchy) or -1 if the component does not
8120      * need to gain focus. The component needs no focus if it has no scrollabars
8121      * (not scrollable) and not selectable. Note that in the future shortcut
8122      * actions may need focus.
8123      * <p>
8124      * For internal use only. May be removed or replaced in the future.
8125      */
8126     public void setProperTabIndex() {
8127         int storedScrollTop = 0;
8128         int storedScrollLeft = 0;
8129 
8130         if (BrowserInfo.get().getOperaVersion() >= 11) {
8131             // Workaround for Opera scroll bug when changing tabIndex (#6222)
8132             storedScrollTop = scrollBodyPanel.getScrollPosition();
8133             storedScrollLeft = scrollBodyPanel.getHorizontalScrollPosition();
8134         }
8135 
8136         if (tabIndex == 0 && !isFocusable()) {
8137             scrollBodyPanel.setTabIndex(-1);
8138         } else {
8139             scrollBodyPanel.setTabIndex(tabIndex);
8140         }
8141 
8142         if (BrowserInfo.get().getOperaVersion() >= 11) {
8143             // Workaround for Opera scroll bug when changing tabIndex (#6222)
8144             scrollBodyPanel.setScrollPosition(storedScrollTop);
8145             scrollBodyPanel.setHorizontalScrollPosition(storedScrollLeft);
8146         }
8147     }
8148 
8149     public void startScrollingVelocityTimer() {
8150         if (scrollingVelocityTimer == null) {
8151             scrollingVelocityTimer = new Timer() {
8152 
8153                 @Override
8154                 public void run() {
8155                     scrollingVelocity++;
8156                 }
8157             };
8158             scrollingVelocityTimer.scheduleRepeating(100);
8159         }
8160     }
8161 
8162     public void cancelScrollingVelocityTimer() {
8163         if (scrollingVelocityTimer != null) {
8164             // Remove velocityTimer if it exists and the Table is disabled
8165             scrollingVelocityTimer.cancel();
8166             scrollingVelocityTimer = null;
8167             scrollingVelocity = 10;
8168         }
8169     }
8170 
8171     /**
8172      * 
8173      * @param keyCode
8174      * @return true if the given keyCode is used by the table for navigation
8175      */
8176     private boolean isNavigationKey(int keyCode) {
8177         return keyCode == getNavigationUpKey()
8178                 || keyCode == getNavigationLeftKey()
8179                 || keyCode == getNavigationRightKey()
8180                 || keyCode == getNavigationDownKey()
8181                 || keyCode == getNavigationPageUpKey()
8182                 || keyCode == getNavigationPageDownKey()
8183                 || keyCode == getNavigationEndKey()
8184                 || keyCode == getNavigationStartKey();
8185     }
8186 
8187     public void lazyRevertFocusToRow(final VScrollTableRow currentlyFocusedRow) {
8188         Scheduler.get().scheduleFinally(new ScheduledCommand() {
8189             @Override
8190             public void execute() {
8191                 if (currentlyFocusedRow != null) {
8192                     setRowFocus(currentlyFocusedRow);
8193                 } else {
8194                     VConsole.log("no row?");
8195                     focusRowFromBody();
8196                 }
8197                 scrollBody.ensureFocus();
8198             }
8199         });
8200     }
8201 
8202     @Override
8203     public Action[] getActions() {
8204         if (bodyActionKeys == null) {
8205             return new Action[] {};
8206         }
8207         final Action[] actions = new Action[bodyActionKeys.length];
8208         for (int i = 0; i < actions.length; i++) {
8209             final String actionKey = bodyActionKeys[i];
8210             Action bodyAction = new TreeAction(this, null, actionKey);
8211             bodyAction.setCaption(getActionCaption(actionKey));
8212             bodyAction.setIconUrl(getActionIcon(actionKey));
8213             actions[i] = bodyAction;
8214         }
8215         return actions;
8216     }
8217 
8218     @Override
8219     public ApplicationConnection getClient() {
8220         return client;
8221     }
8222 
8223     @Override
8224     public String getPaintableId() {
8225         return paintableId;
8226     }
8227 
8228     /**
8229      * Add this to the element mouse down event by using element.setPropertyJSO
8230      * ("onselectstart",applyDisableTextSelectionIEHack()); Remove it then again
8231      * when the mouse is depressed in the mouse up event.
8232      * 
8233      * @return Returns the JSO preventing text selection
8234      */
8235     private static native JavaScriptObject getPreventTextSelectionIEHack()
8236     /*-{
8237             return function(){ return false; };
8238     }-*/;
8239 
8240     public void triggerLazyColumnAdjustment(boolean now) {
8241         lazyAdjustColumnWidths.cancel();
8242         if (now) {
8243             lazyAdjustColumnWidths.run();
8244         } else {
8245             lazyAdjustColumnWidths.schedule(LAZY_COLUMN_ADJUST_TIMEOUT);
8246         }
8247     }
8248 
8249     private boolean isDynamicWidth() {
8250         ComponentConnector paintable = ConnectorMap.get(client).getConnector(
8251                 this);
8252         return paintable.isUndefinedWidth();
8253     }
8254 
8255     private boolean isDynamicHeight() {
8256         ComponentConnector paintable = ConnectorMap.get(client).getConnector(
8257                 this);
8258         if (paintable == null) {
8259             // This should be refactored. As isDynamicHeight can be called from
8260             // a timer it is possible that the connector has been unregistered
8261             // when this method is called, causing getConnector to return null.
8262             return false;
8263         }
8264         return paintable.isUndefinedHeight();
8265     }
8266 
8267     private void debug(String msg) {
8268         if (enableDebug) {
8269             VConsole.error(msg);
8270         }
8271     }
8272 
8273     public Widget getWidgetForPaintable() {
8274         return this;
8275     }
8276 
8277     private static final String SUBPART_HEADER = "header";
8278     private static final String SUBPART_FOOTER = "footer";
8279     private static final String SUBPART_ROW = "row";
8280     private static final String SUBPART_COL = "col";
8281     /**
8282      * Matches header[ix] - used for extracting the index of the targeted header
8283      * cell
8284      */
8285     private static final RegExp SUBPART_HEADER_REGEXP = RegExp
8286             .compile(SUBPART_HEADER + "\\[(\\d+)\\]");
8287     /**
8288      * Matches footer[ix] - used for extracting the index of the targeted footer
8289      * cell
8290      */
8291     private static final RegExp SUBPART_FOOTER_REGEXP = RegExp
8292             .compile(SUBPART_FOOTER + "\\[(\\d+)\\]");
8293     /** Matches row[ix] - used for extracting the index of the targeted row */
8294     private static final RegExp SUBPART_ROW_REGEXP = RegExp.compile(SUBPART_ROW
8295             + "\\[(\\d+)]");
8296     /** Matches col[ix] - used for extracting the index of the targeted column */
8297     private static final RegExp SUBPART_ROW_COL_REGEXP = RegExp
8298             .compile(SUBPART_ROW + "\\[(\\d+)\\]/" + SUBPART_COL
8299                     + "\\[(\\d+)\\]");
8300 
8301     @Override
8302     public com.google.gwt.user.client.Element getSubPartElement(String subPart) {
8303         if (SUBPART_ROW_COL_REGEXP.test(subPart)) {
8304             MatchResult result = SUBPART_ROW_COL_REGEXP.exec(subPart);
8305             int rowIx = Integer.valueOf(result.getGroup(1));
8306             int colIx = Integer.valueOf(result.getGroup(2));
8307             VScrollTableRow row = scrollBody.getRowByRowIndex(rowIx);
8308             if (row != null) {
8309                 Element rowElement = row.getElement();
8310                 if (colIx < rowElement.getChildCount()) {
8311                     return rowElement.getChild(colIx).getFirstChild().cast();
8312                 }
8313             }
8314 
8315         } else if (SUBPART_ROW_REGEXP.test(subPart)) {
8316             MatchResult result = SUBPART_ROW_REGEXP.exec(subPart);
8317             int rowIx = Integer.valueOf(result.getGroup(1));
8318             VScrollTableRow row = scrollBody.getRowByRowIndex(rowIx);
8319             if (row != null) {
8320                 return row.getElement();
8321             }
8322 
8323         } else if (SUBPART_HEADER_REGEXP.test(subPart)) {
8324             MatchResult result = SUBPART_HEADER_REGEXP.exec(subPart);
8325             int headerIx = Integer.valueOf(result.getGroup(1));
8326             HeaderCell headerCell = tHead.getHeaderCell(headerIx);
8327             if (headerCell != null) {
8328                 return headerCell.getElement();
8329             }
8330 
8331         } else if (SUBPART_FOOTER_REGEXP.test(subPart)) {
8332             MatchResult result = SUBPART_FOOTER_REGEXP.exec(subPart);
8333             int footerIx = Integer.valueOf(result.getGroup(1));
8334             FooterCell footerCell = tFoot.getFooterCell(footerIx);
8335             if (footerCell != null) {
8336                 return footerCell.getElement();
8337             }
8338         }
8339         // Nothing found.
8340         return null;
8341     }
8342 
8343     @Override
8344     public String getSubPartName(com.google.gwt.user.client.Element subElement) {
8345         Widget widget = WidgetUtil.findWidget(subElement, null);
8346         if (widget instanceof HeaderCell) {
8347             return SUBPART_HEADER + "[" + tHead.visibleCells.indexOf(widget)
8348                     + "]";
8349         } else if (widget instanceof FooterCell) {
8350             return SUBPART_FOOTER + "[" + tFoot.visibleCells.indexOf(widget)
8351                     + "]";
8352         } else if (widget instanceof VScrollTableRow) {
8353             // a cell in a row
8354             VScrollTableRow row = (VScrollTableRow) widget;
8355             int rowIx = scrollBody.indexOf(row);
8356             if (rowIx >= 0) {
8357                 int colIx = -1;
8358                 for (int ix = 0; ix < row.getElement().getChildCount(); ix++) {
8359                     if (row.getElement().getChild(ix).isOrHasChild(subElement)) {
8360                         colIx = ix;
8361                         break;
8362                     }
8363                 }
8364                 if (colIx >= 0) {
8365                     return SUBPART_ROW + "[" + rowIx + "]/" + SUBPART_COL + "["
8366                             + colIx + "]";
8367                 }
8368                 return SUBPART_ROW + "[" + rowIx + "]";
8369             }
8370         }
8371         // Nothing found.
8372         return null;
8373     }
8374 
8375     /**
8376      * @since 7.2.6
8377      */
8378     public void onUnregister() {
8379         if (addCloseHandler != null) {
8380             addCloseHandler.removeHandler();
8381         }
8382     }
8383 
8384     /*
8385      * Return true if component need to perform some work and false otherwise.
8386      */
8387     @Override
8388     public boolean isWorkPending() {
8389         return lazyAdjustColumnWidths.isRunning();
8390     }
8391 
8392     private static Logger getLogger() {
8393         return Logger.getLogger(VScrollTable.class.getName());
8394     }
8395 }