View Javadoc
1   /*
2    * Copyright 2000-2014 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.Element;
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.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.WidgetUtil;
42  import com.vaadin.client.ui.VTreeTablePatched.VTreeTableScrollBody.VTreeTableRow;
43  
44  public class VTreeTablePatched extends VMagnoliaTable {
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<VTreeTablePatched.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 VMagnoliaTable.MagnoliaTableBody {
131         private int indentWidth = -1;
132         private int maxIndent = 0;
133 
134         protected 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                 VMagnoliaTable.MagnoliaTableBody.MagnoliaTableRow {
149 
150             protected boolean isTreeCellAdded = false;
151             protected com.google.gwt.dom.client.Element treeSpacer;
152             protected boolean open;
153             protected int depth;
154             protected boolean canHaveChildren;
155             protected Widget widgetInHierarchyColumn;
156 
157             public VTreeTableRow(UIDL uidl, char[] aligns2) {
158                 super(uidl, aligns2);
159                 // this fix causes #15118 and doesn't work for treetable anyway
160                 applyZeroWidthFix = false;
161             }
162 
163             @Override
164             public void addCell(UIDL rowUidl, String text, char align,
165                     String style, boolean textIsHTML, boolean isSorted,
166                     String description) {
167                 super.addCell(rowUidl, text, align, style, textIsHTML,
168                         isSorted, description);
169 
170                 addTreeSpacer(rowUidl);
171             }
172 
173             protected boolean addTreeSpacer(UIDL rowUidl) {
174                 if (cellShowsTreeHierarchy(getElement().getChildCount() - 1)) {
175                     Element container = (Element) getElement().getLastChild()
176                             .getFirstChild();
177 
178                     if (rowUidl.hasAttribute("icon")) {
179                         Icon icon = client.getIcon(rowUidl
180                                 .getStringAttribute("icon"));
181                         icon.setAlternateText("icon");
182                         container.insertFirst(icon.getElement());
183                     }
184 
185                     String classname = "v-treetable-treespacer";
186                     if (rowUidl.getBooleanAttribute("ca")) {
187                         canHaveChildren = true;
188                         open = rowUidl.getBooleanAttribute("open");
189                         classname += open ? " v-treetable-node-open"
190                                 : " v-treetable-node-closed";
191                     }
192 
193                     treeSpacer = Document.get().createSpanElement();
194 
195                     treeSpacer.setClassName(classname);
196                     container.insertFirst(treeSpacer);
197                     depth = rowUidl.hasAttribute("depth") ? rowUidl
198                             .getIntAttribute("depth") : 0;
199                     setIndent();
200                     isTreeCellAdded = true;
201                     return true;
202                 }
203                 return false;
204             }
205 
206             protected boolean cellShowsTreeHierarchy(int curColIndex) {
207                 if (isTreeCellAdded) {
208                     return false;
209                 }
210                 return curColIndex == getHierarchyColumnIndex();
211             }
212 
213             @Override
214             public void onBrowserEvent(Event event) {
215                 if (event.getEventTarget().cast() == treeSpacer
216                         && treeSpacer.getClassName().contains("node")) {
217                     if (event.getTypeInt() == Event.ONMOUSEUP) {
218                         sendToggleCollapsedUpdate(getKey());
219                     }
220                     return;
221                 }
222                 super.onBrowserEvent(event);
223             }
224 
225             @Override
226             public void addCell(UIDL rowUidl, Widget w, char align,
227                     String style, boolean isSorted, String description) {
228                 super.addCell(rowUidl, w, align, style, isSorted, description);
229                 if (addTreeSpacer(rowUidl)) {
230                     widgetInHierarchyColumn = w;
231                 }
232 
233             }
234 
235             protected void setIndent() {
236                 if (getIndentWidth() > 0) {
237                     treeSpacer.getParentElement().getStyle()
238                             .setPaddingLeft(getIndent(), Unit.PX);
239                     treeSpacer.getStyle().setWidth(getIndent(), Unit.PX);
240                     int colWidth = getColWidth(getHierarchyColumnIndex());
241                     if (colWidth > 0 && getIndent() > colWidth) {
242                         VTreeTablePatched.this.setColWidth(getHierarchyColumnIndex(),
243                                 getIndent(), false);
244                     }
245                 }
246             }
247 
248             @Override
249             protected void onAttach() {
250                 super.onAttach();
251                 if (getIndentWidth() < 0) {
252                     detectIndent(this);
253                     // If we detect indent here then the size of the hierarchy
254                     // column is still wrong as it has been set when the indent
255                     // was not known.
256                     int w = getCellWidthFromDom(getHierarchyColumnIndex());
257                     if (w >= 0) {
258                         setColWidth(getHierarchyColumnIndex(), w);
259                     }
260                 }
261             }
262 
263             private int getCellWidthFromDom(int cellIndex) {
264                 final Element cell = DOM.getChild(getElement(), cellIndex);
265                 String w = cell.getStyle().getProperty("width");
266                 if (w == null || "".equals(w) || !w.endsWith("px")) {
267                     return -1;
268                 } else {
269                     return Integer.parseInt(w.substring(0, w.length() - 2));
270                 }
271             }
272 
273             private int getHierarchyAndIconWidth() {
274                 int consumedSpace = treeSpacer.getOffsetWidth();
275                 if (treeSpacer.getParentElement().getChildCount() > 2) {
276                     // icon next to tree spacer
277                     consumedSpace += ((com.google.gwt.dom.client.Element) treeSpacer
278                             .getNextSibling()).getOffsetWidth();
279                 }
280                 return consumedSpace;
281             }
282 
283             @Override
284             protected void setCellWidth(int cellIx, int width) {
285                 if (cellIx == getHierarchyColumnIndex()) {
286                     // take indentation padding into account if this is the
287                     // hierarchy column
288                     int indent = getIndent();
289                     if (indent != -1) {
290                         width = Math.max(width - indent, 0);
291                     }
292                 }
293                 super.setCellWidth(cellIx, width);
294             }
295 
296             protected int getIndent() {
297                 return (depth + 1) * getIndentWidth();
298             }
299         }
300 
301         protected class VTreeTableGeneratedRow extends VTreeTableRow {
302             private boolean spanColumns;
303             private boolean htmlContentAllowed;
304 
305             public VTreeTableGeneratedRow(UIDL uidl, char[] aligns) {
306                 super(uidl, aligns);
307                 addStyleName("v-table-generated-row");
308             }
309 
310             public boolean isSpanColumns() {
311                 return spanColumns;
312             }
313 
314             @Override
315             protected void initCellWidths() {
316                 if (spanColumns) {
317                     setSpannedColumnWidthAfterDOMFullyInited();
318                 } else {
319                     super.initCellWidths();
320                 }
321             }
322 
323             private void setSpannedColumnWidthAfterDOMFullyInited() {
324                 // Defer setting width on spanned columns to make sure that
325                 // they are added to the DOM before trying to calculate
326                 // widths.
327                 Scheduler.get().scheduleDeferred(new ScheduledCommand() {
328 
329                     @Override
330                     public void execute() {
331                         if (showRowHeaders) {
332                             setCellWidth(0, tHead.getHeaderCell(0)
333                                     .getWidthWithIndent());
334                             calcAndSetSpanWidthOnCell(1);
335                         } else {
336                             calcAndSetSpanWidthOnCell(0);
337                         }
338                     }
339                 });
340             }
341 
342             @Override
343             protected boolean isRenderHtmlInCells() {
344                 return htmlContentAllowed;
345             }
346 
347             @Override
348             protected void addCellsFromUIDL(UIDL uidl, char[] aligns, int col,
349                     int visibleColumnIndex) {
350                 htmlContentAllowed = uidl.getBooleanAttribute("gen_html");
351                 spanColumns = uidl.getBooleanAttribute("gen_span");
352 
353                 final Iterator<?> cells = uidl.getChildIterator();
354                 if (spanColumns) {
355                     int colCount = uidl.getChildCount();
356                     if (cells.hasNext()) {
357                         final Object cell = cells.next();
358                         if (cell instanceof String) {
359                             addSpannedCell(uidl, cell.toString(), aligns[0],
360                                     "", htmlContentAllowed, false, null,
361                                     colCount);
362                         } else {
363                             addSpannedCell(uidl, (Widget) cell, aligns[0], "",
364                                     false, colCount);
365                         }
366                     }
367                 } else {
368                     super.addCellsFromUIDL(uidl, aligns, col,
369                             visibleColumnIndex);
370                 }
371             }
372 
373             private void addSpannedCell(UIDL rowUidl, Widget w, char align,
374                     String style, boolean sorted, int colCount) {
375                 TableCellElement td = DOM.createTD().cast();
376                 td.setColSpan(colCount);
377                 initCellWithWidget(w, align, style, sorted, td);
378                 td.getStyle().setHeight(getRowHeight(), Unit.PX);
379                 if (addTreeSpacer(rowUidl)) {
380                     widgetInHierarchyColumn = w;
381                 }
382             }
383 
384             private void addSpannedCell(UIDL rowUidl, String text, char align,
385                     String style, boolean textIsHTML, boolean sorted,
386                     String description, int colCount) {
387                 // String only content is optimized by not using Label widget
388                 final TableCellElement td = DOM.createTD().cast();
389                 td.setColSpan(colCount);
390                 initCellWithText(text, align, style, textIsHTML, sorted,
391                         description, td);
392                 td.getStyle().setHeight(getRowHeight(), Unit.PX);
393                 addTreeSpacer(rowUidl);
394             }
395 
396             @Override
397             protected void setCellWidth(int cellIx, int width) {
398                 if (isSpanColumns()) {
399                     if (showRowHeaders) {
400                         if (cellIx == 0) {
401                             super.setCellWidth(0, width);
402                         } else {
403                             // We need to recalculate the spanning TDs width for
404                             // every cellIx in order to support column resizing.
405                             calcAndSetSpanWidthOnCell(1);
406                         }
407                     } else {
408                         // Same as above.
409                         calcAndSetSpanWidthOnCell(0);
410                     }
411                 } else {
412                     super.setCellWidth(cellIx, width);
413                 }
414             }
415 
416             private void calcAndSetSpanWidthOnCell(final int cellIx) {
417                 int spanWidth = 0;
418                 for (int ix = (showRowHeaders ? 1 : 0); ix < tHead
419                         .getVisibleCellCount(); ix++) {
420                     spanWidth += tHead.getHeaderCell(ix).getOffsetWidth();
421                 }
422                 WidgetUtil.setWidthExcludingPaddingAndBorder(
423                         (Element) getElement().getChild(cellIx), spanWidth, 13,
424                         false);
425             }
426         }
427 
428         public 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             // MGNLUI-962 We don't want expanding the tree to have impact on column widths.
446             // this.maxIndent = maxIndent;
447         }
448 
449         private void detectIndent(VTreeTableRow vTreeTableRow) {
450             indentWidth = vTreeTableRow.treeSpacer.getOffsetWidth();
451             if (indentWidth == 0) {
452                 indentWidth = -1;
453                 return;
454             }
455             Iterator<Widget> iterator = iterator();
456             while (iterator.hasNext()) {
457                 VTreeTableRow next = (VTreeTableRow) iterator.next();
458                 next.setIndent();
459             }
460             calculateMaxIndent();
461         }
462 
463         protected void unlinkRowsAnimatedAndUpdateCacheWhenFinished(
464                 final int firstIndex, final int rows) {
465             List<VScrollTableRow> rowsToDelete = new ArrayList<VScrollTableRow>();
466             for (int ix = firstIndex; ix < firstIndex + rows; ix++) {
467                 VScrollTableRow row = getRowByRowIndex(ix);
468                 if (row != null) {
469                     rowsToDelete.add(row);
470                 }
471             }
472             if (!rowsToDelete.isEmpty()) {
473                 // #8810 Only animate if there's something to animate
474                 RowCollapseAnimation anim = new RowCollapseAnimation(
475                         rowsToDelete) {
476                     @Override
477                     protected void onComplete() {
478                         super.onComplete();
479                         // Actually unlink the rows and update the cache after
480                         // the
481                         // animation is done.
482                         unlinkAndReindexRows(firstIndex, rows);
483                         discardRowsOutsideCacheWindow();
484                         ensureCacheFilled();
485                     }
486                 };
487                 anim.run(150);
488             }
489         }
490 
491         protected List<VScrollTableRow> insertRowsAnimated(UIDL rowData,
492                 int firstIndex, int rows) {
493             List<VScrollTableRow> insertedRows = insertAndReindexRows(rowData,
494                     firstIndex, rows);
495             if (!insertedRows.isEmpty()) {
496                 // Only animate if there's something to animate (#8810)
497                 RowExpandAnimation anim = new RowExpandAnimation(insertedRows);
498                 anim.run(150);
499             }
500             scrollBody.calculateMaxIndent();
501             return insertedRows;
502         }
503 
504         /**
505          * Prepares the table for animation by copying the background colors of
506          * all TR elements to their respective TD elements if the TD element is
507          * transparent. This is needed, since if TDs have transparent
508          * backgrounds, the rows sliding behind them are visible.
509          */
510         private class AnimationPreparator {
511             private final int lastItemIx;
512 
513             public AnimationPreparator(int lastItemIx) {
514                 this.lastItemIx = lastItemIx;
515             }
516 
517             public void prepareTableForAnimation() {
518                 int ix = lastItemIx;
519                 VScrollTableRow row = null;
520                 while ((row = getRowByRowIndex(ix)) != null) {
521                     copyTRBackgroundsToTDs(row);
522                     --ix;
523                 }
524             }
525 
526             private void copyTRBackgroundsToTDs(VScrollTableRow row) {
527                 Element tr = row.getElement();
528                 ComputedStyle cs = new ComputedStyle(tr);
529                 String backgroundAttachment = cs
530                         .getProperty("backgroundAttachment");
531                 String backgroundClip = cs.getProperty("backgroundClip");
532                 String backgroundColor = cs.getProperty("backgroundColor");
533                 String backgroundImage = cs.getProperty("backgroundImage");
534                 String backgroundOrigin = cs.getProperty("backgroundOrigin");
535                 for (int ix = 0; ix < tr.getChildCount(); ix++) {
536                     Element td = tr.getChild(ix).cast();
537                     if (!elementHasBackground(td)) {
538                         td.getStyle().setProperty("backgroundAttachment",
539                                 backgroundAttachment);
540                         td.getStyle().setProperty("backgroundClip",
541                                 backgroundClip);
542                         td.getStyle().setProperty("backgroundColor",
543                                 backgroundColor);
544                         td.getStyle().setProperty("backgroundImage",
545                                 backgroundImage);
546                         td.getStyle().setProperty("backgroundOrigin",
547                                 backgroundOrigin);
548                     }
549                 }
550             }
551 
552             private boolean elementHasBackground(Element element) {
553                 ComputedStyle cs = new ComputedStyle(element);
554                 String clr = cs.getProperty("backgroundColor");
555                 String img = cs.getProperty("backgroundImage");
556                 return !("rgba(0, 0, 0, 0)".equals(clr.trim())
557                         || "transparent".equals(clr.trim()) || img == null);
558             }
559 
560             public void restoreTableAfterAnimation() {
561                 int ix = lastItemIx;
562                 VScrollTableRow row = null;
563                 while ((row = getRowByRowIndex(ix)) != null) {
564                     restoreStyleForTDsInRow(row);
565 
566                     --ix;
567                 }
568             }
569 
570             private void restoreStyleForTDsInRow(VScrollTableRow row) {
571                 Element tr = row.getElement();
572                 for (int ix = 0; ix < tr.getChildCount(); ix++) {
573                     Element td = tr.getChild(ix).cast();
574                     td.getStyle().clearProperty("backgroundAttachment");
575                     td.getStyle().clearProperty("backgroundClip");
576                     td.getStyle().clearProperty("backgroundColor");
577                     td.getStyle().clearProperty("backgroundImage");
578                     td.getStyle().clearProperty("backgroundOrigin");
579                 }
580             }
581         }
582 
583         /**
584          * Animates row expansion using the GWT animation framework.
585          * 
586          * The idea is as follows:
587          * 
588          * 1. Insert all rows normally
589          * 
590          * 2. Insert a newly created DIV containing a new TABLE element below
591          * the DIV containing the actual scroll table body.
592          * 
593          * 3. Clone the rows that were inserted in step 1 and attach the clones
594          * to the new TABLE element created in step 2.
595          * 
596          * 4. The new DIV from step 2 is absolutely positioned so that the last
597          * inserted row is just behind the row that was expanded.
598          * 
599          * 5. Hide the contents of the originally inserted rows by setting the
600          * DIV.v-table-cell-wrapper to display:none;.
601          * 
602          * 6. Set the height of the originally inserted rows to 0.
603          * 
604          * 7. The animation loop slides the DIV from step 2 downwards, while at
605          * the same pace growing the height of each of the inserted rows from 0
606          * to full height. The first inserted row grows from 0 to full and after
607          * this the second row grows from 0 to full, etc until all rows are full
608          * height.
609          * 
610          * 8. Remove the DIV from step 2
611          * 
612          * 9. Restore display:block; to the DIV.v-table-cell-wrapper elements.
613          * 
614          * 10. DONE
615          */
616         private class RowExpandAnimation extends Animation {
617 
618             private final List<VScrollTableRow> rows;
619             private Element cloneDiv;
620             private Element cloneTable;
621             private AnimationPreparator preparator;
622 
623             /**
624              * @param rows
625              *            List of rows to animate. Must not be empty.
626              */
627             public RowExpandAnimation(List<VScrollTableRow> rows) {
628                 this.rows = rows;
629                 buildAndInsertAnimatingDiv();
630                 preparator = new AnimationPreparator(rows.get(0).getIndex() - 1);
631                 preparator.prepareTableForAnimation();
632                 for (VScrollTableRow row : rows) {
633                     cloneAndAppendRow(row);
634                     row.addStyleName("v-table-row-animating");
635                     setCellWrapperDivsToDisplayNone(row);
636                     row.setHeight(getInitialHeight());
637                 }
638             }
639 
640             protected String getInitialHeight() {
641                 return "0px";
642             }
643 
644             private void cloneAndAppendRow(VScrollTableRow row) {
645                 Element clonedTR = null;
646                 clonedTR = row.getElement().cloneNode(true).cast();
647                 clonedTR.getStyle().setVisibility(Visibility.VISIBLE);
648                 cloneTable.appendChild(clonedTR);
649             }
650 
651             protected double getBaseOffset() {
652                 return rows.get(0).getAbsoluteTop()
653                         - rows.get(0).getParent().getAbsoluteTop()
654                         - rows.size() * getRowHeight();
655             }
656 
657             private void buildAndInsertAnimatingDiv() {
658                 cloneDiv = DOM.createDiv();
659                 cloneDiv.addClassName("v-treetable-animation-clone-wrapper");
660                 cloneTable = DOM.createTable();
661                 cloneTable.addClassName("v-treetable-animation-clone");
662                 cloneDiv.appendChild(cloneTable);
663                 insertAnimatingDiv();
664             }
665 
666             private void insertAnimatingDiv() {
667                 Element tableBody = getElement();
668                 Element tableBodyParent = tableBody.getParentElement();
669                 tableBodyParent.insertAfter(cloneDiv, tableBody);
670             }
671 
672             @Override
673             protected void onUpdate(double progress) {
674                 animateDiv(progress);
675                 animateRowHeights(progress);
676             }
677 
678             private void animateDiv(double progress) {
679                 double offset = calculateDivOffset(progress, getRowHeight());
680 
681                 cloneDiv.getStyle().setTop(getBaseOffset() + offset, Unit.PX);
682             }
683 
684             private void animateRowHeights(double progress) {
685                 double rh = getRowHeight();
686                 double vlh = calculateHeightOfAllVisibleLines(progress, rh);
687                 int ix = 0;
688 
689                 while (ix < rows.size()) {
690                     double height = vlh < rh ? vlh : rh;
691                     rows.get(ix).setHeight(height + "px");
692                     vlh -= height;
693                     ix++;
694                 }
695             }
696 
697             protected double calculateHeightOfAllVisibleLines(double progress,
698                     double rh) {
699                 return rows.size() * rh * progress;
700             }
701 
702             protected double calculateDivOffset(double progress, double rh) {
703                 return progress * rows.size() * rh;
704             }
705 
706             @Override
707             protected void onComplete() {
708                 preparator.restoreTableAfterAnimation();
709                 for (VScrollTableRow row : rows) {
710                     resetCellWrapperDivsDisplayProperty(row);
711                     row.removeStyleName("v-table-row-animating");
712                 }
713                 Element tableBodyParent = getElement().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     public 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     @Override
907     public void updateFirstVisibleAndScrollIfNeeded(UIDL uidl) {
908         // Left out blank to avoid scrolling glitches described in https://jira.magnolia-cms.com/browse/MGNLUI-960.
909     }
910 }