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