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