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