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