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