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