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