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 info.magnolia.ui.vaadin.gwt.client.grid.VMagnoliaTable;
20  
21  import java.util.ArrayList;
22  import java.util.Iterator;
23  import java.util.LinkedList;
24  import java.util.List;
25  
26  import com.google.gwt.animation.client.Animation;
27  import com.google.gwt.core.client.Scheduler;
28  import com.google.gwt.core.client.Scheduler.ScheduledCommand;
29  import com.google.gwt.dom.client.Document;
30  import com.google.gwt.dom.client.ImageElement;
31  import com.google.gwt.dom.client.Style.Display;
32  import com.google.gwt.dom.client.Style.Unit;
33  import com.google.gwt.dom.client.Style.Visibility;
34  import com.google.gwt.dom.client.TableCellElement;
35  import com.google.gwt.event.dom.client.KeyCodes;
36  import com.google.gwt.user.client.DOM;
37  import com.google.gwt.user.client.Element;
38  import com.google.gwt.user.client.Event;
39  import com.google.gwt.user.client.ui.Widget;
40  import com.vaadin.client.ComputedStyle;
41  import com.vaadin.client.UIDL;
42  import com.vaadin.client.Util;
43  import com.vaadin.client.ui.VTreeTablePatched.VTreeTableScrollBody.VTreeTableRow;
44  
45  public class VTreeTablePatched extends VMagnoliaTable {
46  
47      /** For internal use only. May be removed or replaced in the future. */
48      public static class PendingNavigationEvent {
49          public final int keycode;
50          public final boolean ctrl;
51          public final boolean shift;
52  
53          public PendingNavigationEvent(int keycode, boolean ctrl, boolean shift) {
54              this.keycode = keycode;
55              this.ctrl = ctrl;
56              this.shift = shift;
57          }
58  
59          @Override
60          public String toString() {
61              String string = "Keyboard event: " + keycode;
62              if (ctrl) {
63                  string += " + ctrl";
64              }
65              if (shift) {
66                  string += " + shift";
67              }
68              return string;
69          }
70      }
71  
72      /** For internal use only. May be removed or replaced in the future. */
73      public boolean collapseRequest;
74  
75      private boolean selectionPending;
76  
77      /** For internal use only. May be removed or replaced in the future. */
78      public int colIndexOfHierarchy;
79  
80      /** For internal use only. May be removed or replaced in the future. */
81      public String collapsedRowKey;
82  
83      /** For internal use only. May be removed or replaced in the future. */
84      public VTreeTableScrollBody scrollBody;
85  
86      /** For internal use only. May be removed or replaced in the future. */
87      public boolean animationsEnabled;
88  
89      /** For internal use only. May be removed or replaced in the future. */
90      public LinkedList<PendingNavigationEvent> pendingNavigationEvents = new LinkedList<VTreeTablePatched.PendingNavigationEvent>();
91  
92      /** For internal use only. May be removed or replaced in the future. */
93      public boolean focusParentResponsePending;
94  
95      @Override
96      protected VScrollTableBody createScrollBody() {
97          scrollBody = new VTreeTableScrollBody();
98          return scrollBody;
99      }
100 
101     /*
102      * Overridden to allow animation of expands and collapses of nodes.
103      */
104     @Override
105     public void addAndRemoveRows(UIDL partialRowAdditions) {
106         if (partialRowAdditions == null) {
107             return;
108         }
109 
110         if (animationsEnabled) {
111             if (partialRowAdditions.hasAttribute("hide")) {
112                 scrollBody.unlinkRowsAnimatedAndUpdateCacheWhenFinished(
113                         partialRowAdditions.getIntAttribute("firstprowix"),
114                         partialRowAdditions.getIntAttribute("numprows"));
115             } else {
116                 scrollBody.insertRowsAnimated(partialRowAdditions,
117                         partialRowAdditions.getIntAttribute("firstprowix"),
118                         partialRowAdditions.getIntAttribute("numprows"));
119                 discardRowsOutsideCacheWindow();
120             }
121         } else {
122             super.addAndRemoveRows(partialRowAdditions);
123         }
124     }
125 
126     @Override
127     protected int getHierarchyColumnIndex() {
128         return colIndexOfHierarchy + (showRowHeaders ? 1 : 0);
129     }
130 
131     public class VTreeTableScrollBody extends VMagnoliaTable.MagnoliaTableBody {
132         private int indentWidth = -1;
133         private int maxIndent = 0;
134 
135         protected VTreeTableScrollBody() {
136             super();
137         }
138 
139         @Override
140         protected VScrollTableRow createRow(UIDL uidl, char[] aligns2) {
141             if (uidl.hasAttribute("gen_html")) {
142                 // This is a generated row.
143                 return new VTreeTableGeneratedRow(uidl, aligns2);
144             }
145             return new VTreeTableRow(uidl, aligns2);
146         }
147 
148         public class VTreeTableRow extends
149                 VMagnoliaTable.MagnoliaTableBody.MagnoliaTableRow {
150 
151             protected boolean isTreeCellAdded = false;
152             protected com.google.gwt.dom.client.Element treeSpacer;
153             protected boolean open;
154             protected int depth;
155             protected boolean canHaveChildren;
156             protected Widget widgetInHierarchyColumn;
157 
158             public VTreeTableRow(UIDL uidl, char[] aligns2) {
159                 super(uidl, aligns2);
160             }
161 
162             @Override
163             public void addCell(UIDL rowUidl, String text, char align,
164                     String style, boolean textIsHTML, boolean isSorted,
165                     String description) {
166                 super.addCell(rowUidl, text, align, style, textIsHTML,
167                         isSorted, description);
168 
169                 addTreeSpacer(rowUidl);
170             }
171 
172             protected boolean addTreeSpacer(UIDL rowUidl) {
173                 if (cellShowsTreeHierarchy(getElement().getChildCount() - 1)) {
174                     Element container = (Element) getElement().getLastChild()
175                             .getFirstChild();
176 
177                     if (rowUidl.hasAttribute("icon")) {
178                         // icons are in first content cell in TreeTable
179                         ImageElement icon = Document.get().createImageElement();
180                         icon.setClassName("v-icon");
181                         icon.setAlt("icon");
182                         icon.setSrc(client.translateVaadinUri(rowUidl
183                                 .getStringAttribute("icon")));
184                         container.insertFirst(icon);
185                     }
186 
187                     String classname = "v-treetable-treespacer";
188                     if (rowUidl.getBooleanAttribute("ca")) {
189                         canHaveChildren = true;
190                         open = rowUidl.getBooleanAttribute("open");
191                         classname += open ? " v-treetable-node-open"
192                                 : " v-treetable-node-closed";
193                     }
194 
195                     treeSpacer = Document.get().createSpanElement();
196 
197                     treeSpacer.setClassName(classname);
198                     container.insertFirst(treeSpacer);
199                     depth = rowUidl.hasAttribute("depth") ? rowUidl
200                             .getIntAttribute("depth") : 0;
201                     setIndent();
202                     isTreeCellAdded = true;
203                     return true;
204                 }
205                 return false;
206             }
207 
208             protected boolean cellShowsTreeHierarchy(int curColIndex) {
209                 if (isTreeCellAdded) {
210                     return false;
211                 }
212                 return curColIndex == getHierarchyColumnIndex();
213             }
214 
215             @Override
216             public void onBrowserEvent(Event event) {
217                 if (event.getEventTarget().cast() == treeSpacer
218                         && treeSpacer.getClassName().contains("node")) {
219                     if (event.getTypeInt() == Event.ONMOUSEUP) {
220                         sendToggleCollapsedUpdate(getKey());
221                     }
222                     return;
223                 }
224                 super.onBrowserEvent(event);
225             }
226 
227             @Override
228             public void addCell(UIDL rowUidl, Widget w, char align,
229                     String style, boolean isSorted, String description) {
230                 super.addCell(rowUidl, w, align, style, isSorted, description);
231                 if (addTreeSpacer(rowUidl)) {
232                     widgetInHierarchyColumn = w;
233                 }
234 
235             }
236 
237             protected void setIndent() {
238                 if (getIndentWidth() > 0) {
239                     treeSpacer.getParentElement().getStyle()
240                             .setPaddingLeft(getIndent(), Unit.PX);
241                     treeSpacer.getStyle().setWidth(getIndent(), Unit.PX);
242                     int colWidth = getColWidth(getHierarchyColumnIndex());
243                     if (colWidth > 0 && getIndent() > colWidth) {
244                         VTreeTablePatched.this.setColWidth(getHierarchyColumnIndex(),
245                                 getIndent(), false);
246                     }
247                 }
248             }
249 
250             @Override
251             protected void onAttach() {
252                 super.onAttach();
253                 if (getIndentWidth() < 0) {
254                     detectIndent(this);
255                     // If we detect indent here then the size of the hierarchy
256                     // column is still wrong as it has been set when the indent
257                     // was not known.
258                     int w = getCellWidthFromDom(getHierarchyColumnIndex());
259                     if (w >= 0) {
260                         setColWidth(getHierarchyColumnIndex(), w);
261                     }
262                 }
263             }
264 
265             private int getCellWidthFromDom(int cellIndex) {
266                 final Element cell = DOM.getChild(getElement(), cellIndex);
267                 String w = cell.getStyle().getProperty("width");
268                 if (w == null || "".equals(w) || !w.endsWith("px")) {
269                     return -1;
270                 } else {
271                     return Integer.parseInt(w.substring(0, w.length() - 2));
272                 }
273             }
274 
275             private int getHierarchyAndIconWidth() {
276                 int consumedSpace = treeSpacer.getOffsetWidth();
277                 if (treeSpacer.getParentElement().getChildCount() > 2) {
278                     // icon next to tree spacer
279                     consumedSpace += ((com.google.gwt.dom.client.Element) treeSpacer
280                             .getNextSibling()).getOffsetWidth();
281                 }
282                 return consumedSpace;
283             }
284 
285             @Override
286             protected void setCellWidth(int cellIx, int width) {
287                 if (cellIx == getHierarchyColumnIndex()) {
288                     // take indentation padding into account if this is the
289                     // hierarchy column
290                     int indent = getIndent();
291                     if (indent != -1) {
292                         width = Math.max(width - indent, 0);
293                     }
294                 }
295                 super.setCellWidth(cellIx, width);
296             }
297 
298             protected int getIndent() {
299                 return (depth + 1) * getIndentWidth();
300             }
301         }
302 
303         protected class VTreeTableGeneratedRow extends VTreeTableRow {
304             private boolean spanColumns;
305             private boolean htmlContentAllowed;
306 
307             public VTreeTableGeneratedRow(UIDL uidl, char[] aligns) {
308                 super(uidl, aligns);
309                 addStyleName("v-table-generated-row");
310             }
311 
312             public boolean isSpanColumns() {
313                 return spanColumns;
314             }
315 
316             @Override
317             protected void initCellWidths() {
318                 if (spanColumns) {
319                     setSpannedColumnWidthAfterDOMFullyInited();
320                 } else {
321                     super.initCellWidths();
322                 }
323             }
324 
325             private void setSpannedColumnWidthAfterDOMFullyInited() {
326                 // Defer setting width on spanned columns to make sure that
327                 // they are added to the DOM before trying to calculate
328                 // widths.
329                 Scheduler.get().scheduleDeferred(new ScheduledCommand() {
330 
331                     @Override
332                     public void execute() {
333                         if (showRowHeaders) {
334                             setCellWidth(0, tHead.getHeaderCell(0)
335                                     .getWidthWithIndent());
336                             calcAndSetSpanWidthOnCell(1);
337                         } else {
338                             calcAndSetSpanWidthOnCell(0);
339                         }
340                     }
341                 });
342             }
343 
344             @Override
345             protected boolean isRenderHtmlInCells() {
346                 return htmlContentAllowed;
347             }
348 
349             @Override
350             protected void addCellsFromUIDL(UIDL uidl, char[] aligns, int col,
351                     int visibleColumnIndex) {
352                 htmlContentAllowed = uidl.getBooleanAttribute("gen_html");
353                 spanColumns = uidl.getBooleanAttribute("gen_span");
354 
355                 final Iterator<?> cells = uidl.getChildIterator();
356                 if (spanColumns) {
357                     int colCount = uidl.getChildCount();
358                     if (cells.hasNext()) {
359                         final Object cell = cells.next();
360                         if (cell instanceof String) {
361                             addSpannedCell(uidl, cell.toString(), aligns[0],
362                                     "", htmlContentAllowed, false, null,
363                                     colCount);
364                         } else {
365                             addSpannedCell(uidl, (Widget) cell, aligns[0], "",
366                                     false, colCount);
367                         }
368                     }
369                 } else {
370                     super.addCellsFromUIDL(uidl, aligns, col,
371                             visibleColumnIndex);
372                 }
373             }
374 
375             private void addSpannedCell(UIDL rowUidl, Widget w, char align,
376                     String style, boolean sorted, int colCount) {
377                 TableCellElement td = DOM.createTD().cast();
378                 td.setColSpan(colCount);
379                 initCellWithWidget(w, align, style, sorted, td);
380                 td.getStyle().setHeight(getRowHeight(), Unit.PX);
381                 if (addTreeSpacer(rowUidl)) {
382                     widgetInHierarchyColumn = w;
383                 }
384             }
385 
386             private void addSpannedCell(UIDL rowUidl, String text, char align,
387                     String style, boolean textIsHTML, boolean sorted,
388                     String description, int colCount) {
389                 // String only content is optimized by not using Label widget
390                 final TableCellElement td = DOM.createTD().cast();
391                 td.setColSpan(colCount);
392                 initCellWithText(text, align, style, textIsHTML, sorted,
393                         description, td);
394                 td.getStyle().setHeight(getRowHeight(), Unit.PX);
395                 addTreeSpacer(rowUidl);
396             }
397 
398             @Override
399             protected void setCellWidth(int cellIx, int width) {
400                 if (isSpanColumns()) {
401                     if (showRowHeaders) {
402                         if (cellIx == 0) {
403                             super.setCellWidth(0, width);
404                         } else {
405                             // We need to recalculate the spanning TDs width for
406                             // every cellIx in order to support column resizing.
407                             calcAndSetSpanWidthOnCell(1);
408                         }
409                     } else {
410                         // Same as above.
411                         calcAndSetSpanWidthOnCell(0);
412                     }
413                 } else {
414                     super.setCellWidth(cellIx, width);
415                 }
416             }
417 
418             private void calcAndSetSpanWidthOnCell(final int cellIx) {
419                 int spanWidth = 0;
420                 for (int ix = (showRowHeaders ? 1 : 0); ix < tHead
421                         .getVisibleCellCount(); ix++) {
422                     spanWidth += tHead.getHeaderCell(ix).getOffsetWidth();
423                 }
424                 Util.setWidthExcludingPaddingAndBorder((Element) getElement()
425                         .getChild(cellIx), spanWidth, 13, false);
426             }
427         }
428 
429         public int getIndentWidth() {
430             return indentWidth;
431         }
432 
433         @Override
434         protected int getMaxIndent() {
435             return maxIndent;
436         }
437 
438         @Override
439         protected void calculateMaxIndent() {
440             int maxIndent = 0;
441             Iterator<Widget> iterator = iterator();
442             while (iterator.hasNext()) {
443                 VTreeTableRow next = (VTreeTableRow) iterator.next();
444                 maxIndent = Math.max(maxIndent, next.getIndent());
445             }
446             // MGNLUI-962 We don't want expanding the tree to have impact on column widths.
447             // this.maxIndent = maxIndent;
448         }
449 
450         private void detectIndent(VTreeTableRow vTreeTableRow) {
451             indentWidth = vTreeTableRow.treeSpacer.getOffsetWidth();
452             if (indentWidth == 0) {
453                 indentWidth = -1;
454                 return;
455             }
456             Iterator<Widget> iterator = iterator();
457             while (iterator.hasNext()) {
458                 VTreeTableRow next = (VTreeTableRow) iterator.next();
459                 next.setIndent();
460             }
461             calculateMaxIndent();
462         }
463 
464         protected void unlinkRowsAnimatedAndUpdateCacheWhenFinished(
465                 final int firstIndex, final int rows) {
466             List<VScrollTableRow> rowsToDelete = new ArrayList<VScrollTableRow>();
467             for (int ix = firstIndex; ix < firstIndex + rows; ix++) {
468                 VScrollTableRow row = getRowByRowIndex(ix);
469                 if (row != null) {
470                     rowsToDelete.add(row);
471                 }
472             }
473             if (!rowsToDelete.isEmpty()) {
474                 // #8810 Only animate if there's something to animate
475                 RowCollapseAnimation anim = new RowCollapseAnimation(
476                         rowsToDelete) {
477                     @Override
478                     protected void onComplete() {
479                         super.onComplete();
480                         // Actually unlink the rows and update the cache after
481                         // the
482                         // animation is done.
483                         unlinkAndReindexRows(firstIndex, rows);
484                         discardRowsOutsideCacheWindow();
485                         ensureCacheFilled();
486                     }
487                 };
488                 anim.run(150);
489             }
490         }
491 
492         protected List<VScrollTableRow> insertRowsAnimated(UIDL rowData,
493                 int firstIndex, int rows) {
494             List<VScrollTableRow> insertedRows = insertAndReindexRows(rowData,
495                     firstIndex, rows);
496             if (!insertedRows.isEmpty()) {
497                 // Only animate if there's something to animate (#8810)
498                 RowExpandAnimation anim = new RowExpandAnimation(insertedRows);
499                 anim.run(150);
500             }
501             scrollBody.calculateMaxIndent();
502             return insertedRows;
503         }
504 
505         /**
506          * Prepares the table for animation by copying the background colors of
507          * all TR elements to their respective TD elements if the TD element is
508          * transparent. This is needed, since if TDs have transparent
509          * backgrounds, the rows sliding behind them are visible.
510          */
511         private class AnimationPreparator {
512             private final int lastItemIx;
513 
514             public AnimationPreparator(int lastItemIx) {
515                 this.lastItemIx = lastItemIx;
516             }
517 
518             public void prepareTableForAnimation() {
519                 int ix = lastItemIx;
520                 VScrollTableRow row = null;
521                 while ((row = getRowByRowIndex(ix)) != null) {
522                     copyTRBackgroundsToTDs(row);
523                     --ix;
524                 }
525             }
526 
527             private void copyTRBackgroundsToTDs(VScrollTableRow row) {
528                 Element tr = row.getElement();
529                 ComputedStyle cs = new ComputedStyle(tr);
530                 String backgroundAttachment = cs
531                         .getProperty("backgroundAttachment");
532                 String backgroundClip = cs.getProperty("backgroundClip");
533                 String backgroundColor = cs.getProperty("backgroundColor");
534                 String backgroundImage = cs.getProperty("backgroundImage");
535                 String backgroundOrigin = cs.getProperty("backgroundOrigin");
536                 for (int ix = 0; ix < tr.getChildCount(); ix++) {
537                     Element td = tr.getChild(ix).cast();
538                     if (!elementHasBackground(td)) {
539                         td.getStyle().setProperty("backgroundAttachment",
540                                 backgroundAttachment);
541                         td.getStyle().setProperty("backgroundClip",
542                                 backgroundClip);
543                         td.getStyle().setProperty("backgroundColor",
544                                 backgroundColor);
545                         td.getStyle().setProperty("backgroundImage",
546                                 backgroundImage);
547                         td.getStyle().setProperty("backgroundOrigin",
548                                 backgroundOrigin);
549                     }
550                 }
551             }
552 
553             private boolean elementHasBackground(Element element) {
554                 ComputedStyle cs = new ComputedStyle(element);
555                 String clr = cs.getProperty("backgroundColor");
556                 String img = cs.getProperty("backgroundImage");
557                 return !("rgba(0, 0, 0, 0)".equals(clr.trim())
558                         || "transparent".equals(clr.trim()) || img == null);
559             }
560 
561             public void restoreTableAfterAnimation() {
562                 int ix = lastItemIx;
563                 VScrollTableRow row = null;
564                 while ((row = getRowByRowIndex(ix)) != null) {
565                     restoreStyleForTDsInRow(row);
566 
567                     --ix;
568                 }
569             }
570 
571             private void restoreStyleForTDsInRow(VScrollTableRow row) {
572                 Element tr = row.getElement();
573                 for (int ix = 0; ix < tr.getChildCount(); ix++) {
574                     Element td = tr.getChild(ix).cast();
575                     td.getStyle().clearProperty("backgroundAttachment");
576                     td.getStyle().clearProperty("backgroundClip");
577                     td.getStyle().clearProperty("backgroundColor");
578                     td.getStyle().clearProperty("backgroundImage");
579                     td.getStyle().clearProperty("backgroundOrigin");
580                 }
581             }
582         }
583 
584         /**
585          * Animates row expansion using the GWT animation framework.
586          * 
587          * The idea is as follows:
588          * 
589          * 1. Insert all rows normally
590          * 
591          * 2. Insert a newly created DIV containing a new TABLE element below
592          * the DIV containing the actual scroll table body.
593          * 
594          * 3. Clone the rows that were inserted in step 1 and attach the clones
595          * to the new TABLE element created in step 2.
596          * 
597          * 4. The new DIV from step 2 is absolutely positioned so that the last
598          * inserted row is just behind the row that was expanded.
599          * 
600          * 5. Hide the contents of the originally inserted rows by setting the
601          * DIV.v-table-cell-wrapper to display:none;.
602          * 
603          * 6. Set the height of the originally inserted rows to 0.
604          * 
605          * 7. The animation loop slides the DIV from step 2 downwards, while at
606          * the same pace growing the height of each of the inserted rows from 0
607          * to full height. The first inserted row grows from 0 to full and after
608          * this the second row grows from 0 to full, etc until all rows are full
609          * height.
610          * 
611          * 8. Remove the DIV from step 2
612          * 
613          * 9. Restore display:block; to the DIV.v-table-cell-wrapper elements.
614          * 
615          * 10. DONE
616          */
617         private class RowExpandAnimation extends Animation {
618 
619             private final List<VScrollTableRow> rows;
620             private Element cloneDiv;
621             private Element cloneTable;
622             private AnimationPreparator preparator;
623 
624             /**
625              * @param rows
626              *            List of rows to animate. Must not be empty.
627              */
628             public RowExpandAnimation(List<VScrollTableRow> rows) {
629                 this.rows = rows;
630                 buildAndInsertAnimatingDiv();
631                 preparator = new AnimationPreparator(rows.get(0).getIndex() - 1);
632                 preparator.prepareTableForAnimation();
633                 for (VScrollTableRow row : rows) {
634                     cloneAndAppendRow(row);
635                     row.addStyleName("v-table-row-animating");
636                     setCellWrapperDivsToDisplayNone(row);
637                     row.setHeight(getInitialHeight());
638                 }
639             }
640 
641             protected String getInitialHeight() {
642                 return "0px";
643             }
644 
645             private void cloneAndAppendRow(VScrollTableRow row) {
646                 Element clonedTR = null;
647                 clonedTR = row.getElement().cloneNode(true).cast();
648                 clonedTR.getStyle().setVisibility(Visibility.VISIBLE);
649                 cloneTable.appendChild(clonedTR);
650             }
651 
652             protected double getBaseOffset() {
653                 return rows.get(0).getAbsoluteTop()
654                         - rows.get(0).getParent().getAbsoluteTop()
655                         - rows.size() * getRowHeight();
656             }
657 
658             private void buildAndInsertAnimatingDiv() {
659                 cloneDiv = DOM.createDiv();
660                 cloneDiv.addClassName("v-treetable-animation-clone-wrapper");
661                 cloneTable = DOM.createTable();
662                 cloneTable.addClassName("v-treetable-animation-clone");
663                 cloneDiv.appendChild(cloneTable);
664                 insertAnimatingDiv();
665             }
666 
667             private void insertAnimatingDiv() {
668                 Element tableBody = getElement().cast();
669                 Element tableBodyParent = tableBody.getParentElement().cast();
670                 tableBodyParent.insertAfter(cloneDiv, tableBody);
671             }
672 
673             @Override
674             protected void onUpdate(double progress) {
675                 animateDiv(progress);
676                 animateRowHeights(progress);
677             }
678 
679             private void animateDiv(double progress) {
680                 double offset = calculateDivOffset(progress, getRowHeight());
681 
682                 cloneDiv.getStyle().setTop(getBaseOffset() + offset, Unit.PX);
683             }
684 
685             private void animateRowHeights(double progress) {
686                 double rh = getRowHeight();
687                 double vlh = calculateHeightOfAllVisibleLines(progress, rh);
688                 int ix = 0;
689 
690                 while (ix < rows.size()) {
691                     double height = vlh < rh ? vlh : rh;
692                     rows.get(ix).setHeight(height + "px");
693                     vlh -= height;
694                     ix++;
695                 }
696             }
697 
698             protected double calculateHeightOfAllVisibleLines(double progress,
699                     double rh) {
700                 return rows.size() * rh * progress;
701             }
702 
703             protected double calculateDivOffset(double progress, double rh) {
704                 return progress * rows.size() * rh;
705             }
706 
707             @Override
708             protected void onComplete() {
709                 preparator.restoreTableAfterAnimation();
710                 for (VScrollTableRow row : rows) {
711                     resetCellWrapperDivsDisplayProperty(row);
712                     row.removeStyleName("v-table-row-animating");
713                 }
714                 Element tableBodyParent = (Element) getElement()
715                         .getParentElement();
716                 tableBodyParent.removeChild(cloneDiv);
717             }
718 
719             private void setCellWrapperDivsToDisplayNone(VScrollTableRow row) {
720                 Element tr = row.getElement();
721                 for (int ix = 0; ix < tr.getChildCount(); ix++) {
722                     getWrapperDiv(tr, ix).getStyle().setDisplay(Display.NONE);
723                 }
724             }
725 
726             private Element getWrapperDiv(Element tr, int tdIx) {
727                 Element td = tr.getChild(tdIx).cast();
728                 return td.getChild(0).cast();
729             }
730 
731             private void resetCellWrapperDivsDisplayProperty(VScrollTableRow row) {
732                 Element tr = row.getElement();
733                 for (int ix = 0; ix < tr.getChildCount(); ix++) {
734                     getWrapperDiv(tr, ix).getStyle().clearProperty("display");
735                 }
736             }
737 
738         }
739 
740         /**
741          * This is the inverse of the RowExpandAnimation and is implemented by
742          * extending it and overriding the calculation of offsets and heights.
743          */
744         private class RowCollapseAnimation extends RowExpandAnimation {
745 
746             private final List<VScrollTableRow> rows;
747 
748             /**
749              * @param rows
750              *            List of rows to animate. Must not be empty.
751              */
752             public RowCollapseAnimation(List<VScrollTableRow> rows) {
753                 super(rows);
754                 this.rows = rows;
755             }
756 
757             @Override
758             protected String getInitialHeight() {
759                 return getRowHeight() + "px";
760             }
761 
762             @Override
763             protected double getBaseOffset() {
764                 return getRowHeight();
765             }
766 
767             @Override
768             protected double calculateHeightOfAllVisibleLines(double progress,
769                     double rh) {
770                 return rows.size() * rh * (1 - progress);
771             }
772 
773             @Override
774             protected double calculateDivOffset(double progress, double rh) {
775                 return -super.calculateDivOffset(progress, rh);
776             }
777         }
778     }
779 
780     /**
781      * Icons rendered into first actual column in TreeTable, not to row header
782      * cell
783      */
784     @Override
785     protected String buildCaptionHtmlSnippet(UIDL uidl) {
786         if (uidl.getTag().equals("column")) {
787             return super.buildCaptionHtmlSnippet(uidl);
788         } else {
789             String s = uidl.getStringAttribute("caption");
790             return s;
791         }
792     }
793 
794     /** For internal use only. May be removed or replaced in the future. */
795     @Override
796     public boolean handleNavigation(int keycode, boolean ctrl, boolean shift) {
797         if (collapseRequest || focusParentResponsePending) {
798             // Enqueue the event if there might be pending content changes from
799             // the server
800             if (pendingNavigationEvents.size() < 10) {
801                 // Only keep 10 keyboard events in the queue
802                 PendingNavigationEvent pendingNavigationEvent = new PendingNavigationEvent(
803                         keycode, ctrl, shift);
804                 pendingNavigationEvents.add(pendingNavigationEvent);
805             }
806             return true;
807         }
808 
809         VTreeTableRow focusedRow = (VTreeTableRow) getFocusedRow();
810         if (focusedRow != null) {
811             if (focusedRow.canHaveChildren
812                     && ((keycode == KeyCodes.KEY_RIGHT && !focusedRow.open) || (keycode == KeyCodes.KEY_LEFT && focusedRow.open))) {
813                 if (!ctrl) {
814                     client.updateVariable(paintableId, "selectCollapsed", true,
815                             false);
816                 }
817                 sendSelectedRows(false);
818                 sendToggleCollapsedUpdate(focusedRow.getKey());
819                 return true;
820             } else if (keycode == KeyCodes.KEY_RIGHT && focusedRow.open) {
821                 // already expanded, move selection down if next is on a deeper
822                 // level (is-a-child)
823                 VTreeTableScrollBody body = (VTreeTableScrollBody) focusedRow
824                         .getParent();
825                 Iterator<Widget> iterator = body.iterator();
826                 VTreeTableRow next = null;
827                 while (iterator.hasNext()) {
828                     next = (VTreeTableRow) iterator.next();
829                     if (next == focusedRow) {
830                         next = (VTreeTableRow) iterator.next();
831                         break;
832                     }
833                 }
834                 if (next != null) {
835                     if (next.depth > focusedRow.depth) {
836                         selectionPending = true;
837                         return super.handleNavigation(getNavigationDownKey(),
838                                 ctrl, shift);
839                     }
840                 } else {
841                     // Note, a minor change here for a bit false behavior if
842                     // cache rows is disabled + last visible row + no childs for
843                     // the node
844                     selectionPending = true;
845                     return super.handleNavigation(getNavigationDownKey(), ctrl,
846                             shift);
847                 }
848             } else if (keycode == KeyCodes.KEY_LEFT) {
849                 // already collapsed move selection up to parent node
850                 // do on the server side as the parent is not necessary
851                 // rendered on the client, could check if parent is visible if
852                 // a performance issue arises
853 
854                 client.updateVariable(paintableId, "focusParent",
855                         focusedRow.getKey(), true);
856 
857                 // Set flag that we should enqueue navigation events until we
858                 // get a response to this request
859                 focusParentResponsePending = true;
860 
861                 return true;
862             }
863         }
864         return super.handleNavigation(keycode, ctrl, shift);
865     }
866 
867     public void sendToggleCollapsedUpdate(String rowKey) {
868         collapsedRowKey = rowKey;
869         collapseRequest = true;
870         client.updateVariable(paintableId, "toggleCollapsed", rowKey, true);
871     }
872 
873     @Override
874     public void onBrowserEvent(Event event) {
875         super.onBrowserEvent(event);
876         if (event.getTypeInt() == Event.ONKEYUP && selectionPending) {
877             sendSelectedRows();
878         }
879     }
880 
881     @Override
882     protected void sendSelectedRows(boolean immediately) {
883         super.sendSelectedRows(immediately);
884         selectionPending = false;
885     }
886 
887     @Override
888     protected void reOrderColumn(String columnKey, int newIndex) {
889         super.reOrderColumn(columnKey, newIndex);
890         // current impl not intelligent enough to survive without visiting the
891         // server to redraw content
892         client.sendPendingVariableChanges();
893     }
894 
895     @Override
896     public void setStyleName(String style) {
897         super.setStyleName(style + " v-treetable");
898     }
899 
900     @Override
901     public void updateTotalRows(UIDL uidl) {
902         // Make sure that initializedAndAttached & al are not reset when the
903         // totalrows are updated on expand/collapse requests.
904         int newTotalRows = uidl.getIntAttribute("totalrows");
905         setTotalRows(newTotalRows);
906     }
907 
908 }