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