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