View Javadoc
1   /*
2    * Copyright 2000-2016 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.v7.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.Icon;
43  import com.vaadin.v7.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,
54                  boolean shift) {
55              this.keycode = keycode;
56              this.ctrl = ctrl;
57              this.shift = shift;
58          }
59  
60          @Override
61          public String toString() {
62              String string = "Keyboard event: " + keycode;
63              if (ctrl) {
64                  string += " + ctrl";
65              }
66              if (shift) {
67                  string += " + shift";
68              }
69              return string;
70          }
71      }
72  
73      /** For internal use only. May be removed or replaced in the future. */
74      public boolean collapseRequest;
75  
76      private boolean selectionPending;
77  
78      /** For internal use only. May be removed or replaced in the future. */
79      public int colIndexOfHierarchy;
80  
81      /** For internal use only. May be removed or replaced in the future. */
82      public String collapsedRowKey;
83  
84      /** For internal use only. May be removed or replaced in the future. */
85      public VTreeTableScrollBody scrollBody;
86  
87      /** For internal use only. May be removed or replaced in the future. */
88      public boolean animationsEnabled;
89  
90      /** For internal use only. May be removed or replaced in the future. */
91      public LinkedList<PendingNavigationEvent> pendingNavigationEvents = new LinkedList<VTreeTablePatched.PendingNavigationEvent>();
92  
93      /** For internal use only. May be removed or replaced in the future. */
94      public boolean focusParentResponsePending;
95  
96      @Override
97      protected VScrollTableBody createScrollBody() {
98          scrollBody = new VTreeTableScrollBody();
99          return scrollBody;
100     }
101 
102     /*
103      * Overridden to allow animation of expands and collapses of nodes.
104      */
105     @Override
106     public void addAndRemoveRows(UIDL partialRowAdditions) {
107         if (partialRowAdditions == null) {
108             return;
109         }
110 
111         if (animationsEnabled) {
112             if (partialRowAdditions.hasAttribute("hide")) {
113                 scrollBody.unlinkRowsAnimatedAndUpdateCacheWhenFinished(
114                         partialRowAdditions.getIntAttribute("firstprowix"),
115                         partialRowAdditions.getIntAttribute("numprows"));
116             } else {
117                 scrollBody.insertRowsAnimated(partialRowAdditions,
118                         partialRowAdditions.getIntAttribute("firstprowix"),
119                         partialRowAdditions.getIntAttribute("numprows"));
120                 discardRowsOutsideCacheWindow();
121             }
122         } else {
123             super.addAndRemoveRows(partialRowAdditions);
124         }
125     }
126 
127     @Override
128     protected int getHierarchyColumnIndex() {
129         return colIndexOfHierarchy + (showRowHeaders ? 1 : 0);
130     }
131 
132     public class VTreeTableScrollBody extends VMagnoliaTable.MagnoliaTableBody {
133         private int indentWidth = -1;
134         private int maxIndent = 0;
135 
136         protected VTreeTableScrollBody() {
137             super();
138         }
139 
140         @Override
141         protected VScrollTableRow createRow(UIDL uidl, char[] aligns2) {
142             if (uidl.hasAttribute("gen_html")) {
143                 // This is a generated row.
144                 return new VTreeTableGeneratedRow(uidl, aligns2);
145             }
146             return new VTreeTableRow(uidl, aligns2);
147         }
148 
149         public class VTreeTableRow
150                 extends VMagnoliaTable.MagnoliaTableBody.MagnoliaTableRow {
151 
152             protected boolean isTreeCellAdded = false;
153             protected com.google.gwt.dom.client.Element treeSpacer;
154             protected boolean open;
155             protected int depth;
156             protected boolean canHaveChildren;
157             protected Widget widgetInHierarchyColumn;
158 
159             public VTreeTableRow(UIDL uidl, char[] aligns2) {
160                 super(uidl, aligns2);
161                 // this fix causes #15118 and doesn't work for treetable anyway
162                 applyZeroWidthFix = false;
163             }
164 
165             @Override
166             public void addCell(UIDL rowUidl, String text, char align,
167                     String style, boolean textIsHTML, boolean isSorted,
168                     String description) {
169                 super.addCell(rowUidl, text, align, style, textIsHTML, isSorted,
170                         description);
171 
172                 addTreeSpacer(rowUidl);
173             }
174 
175             protected boolean addTreeSpacer(UIDL rowUidl) {
176                 if (cellShowsTreeHierarchy(getElement().getChildCount() - 1)) {
177                     Element container = (Element) getElement().getLastChild()
178                             .getFirstChild();
179 
180                     if (rowUidl.hasAttribute("icon")) {
181                         Icon icon = client
182                                 .getIcon(rowUidl.getStringAttribute("icon"));
183                         icon.setAlternateText("icon");
184                         container.insertFirst(icon.getElement());
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")
200                             ? rowUidl.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, 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                 WidgetUtil.setWidthExcludingPaddingAndBorder(
424                         (Element) getElement().getChild(cellIx), spanWidth, 13,
425                         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(
632                         rows.get(0).getIndex() - 1);
633                 preparator.prepareTableForAnimation();
634                 for (VScrollTableRow row : rows) {
635                     cloneAndAppendRow(row);
636                     row.addStyleName("v-table-row-animating");
637                     setCellWrapperDivsToDisplayNone(row);
638                     row.setHeight(getInitialHeight());
639                 }
640             }
641 
642             protected String getInitialHeight() {
643                 return "0px";
644             }
645 
646             private void cloneAndAppendRow(VScrollTableRow row) {
647                 Element clonedTR = null;
648                 clonedTR = row.getElement().cloneNode(true).cast();
649                 clonedTR.getStyle().setVisibility(Visibility.VISIBLE);
650                 cloneTable.appendChild(clonedTR);
651             }
652 
653             protected double getBaseOffset() {
654                 return rows.get(0).getAbsoluteTop()
655                         - rows.get(0).getParent().getAbsoluteTop()
656                         - rows.size() * getRowHeight();
657             }
658 
659             private void buildAndInsertAnimatingDiv() {
660                 cloneDiv = DOM.createDiv();
661                 cloneDiv.addClassName("v-treetable-animation-clone-wrapper");
662                 cloneTable = DOM.createTable();
663                 cloneTable.addClassName("v-treetable-animation-clone");
664                 cloneDiv.appendChild(cloneTable);
665                 insertAnimatingDiv();
666             }
667 
668             private void insertAnimatingDiv() {
669                 Element tableBody = getElement();
670                 Element tableBodyParent = tableBody.getParentElement();
671                 tableBodyParent.insertAfter(cloneDiv, tableBody);
672             }
673 
674             @Override
675             protected void onUpdate(double progress) {
676                 animateDiv(progress);
677                 animateRowHeights(progress);
678             }
679 
680             private void animateDiv(double progress) {
681                 double offset = calculateDivOffset(progress, getRowHeight());
682 
683                 cloneDiv.getStyle().setTop(getBaseOffset() + offset, Unit.PX);
684             }
685 
686             private void animateRowHeights(double progress) {
687                 double rh = getRowHeight();
688                 double vlh = calculateHeightOfAllVisibleLines(progress, rh);
689                 int ix = 0;
690 
691                 while (ix < rows.size()) {
692                     double height = vlh < rh ? vlh : rh;
693                     rows.get(ix).setHeight(height + "px");
694                     vlh -= height;
695                     ix++;
696                 }
697             }
698 
699             protected double calculateHeightOfAllVisibleLines(double progress,
700                     double rh) {
701                 return rows.size() * rh * progress;
702             }
703 
704             protected double calculateDivOffset(double progress, double rh) {
705                 return progress * rows.size() * rh;
706             }
707 
708             @Override
709             protected void onComplete() {
710                 preparator.restoreTableAfterAnimation();
711                 for (VScrollTableRow row : rows) {
712                     resetCellWrapperDivsDisplayProperty(row);
713                     row.removeStyleName("v-table-row-animating");
714                 }
715                 Element tableBodyParent = getElement().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(
732                     VScrollTableRow row) {
733                 Element tr = row.getElement();
734                 for (int ix = 0; ix < tr.getChildCount(); ix++) {
735                     getWrapperDiv(tr, ix).getStyle().clearProperty("display");
736                 }
737             }
738 
739         }
740 
741         /**
742          * This is the inverse of the RowExpandAnimation and is implemented by
743          * extending it and overriding the calculation of offsets and heights.
744          */
745         private class RowCollapseAnimation extends RowExpandAnimation {
746 
747             private final List<VScrollTableRow> rows;
748 
749             /**
750              * @param rows
751              *            List of rows to animate. Must not be empty.
752              */
753             public RowCollapseAnimation(List<VScrollTableRow> rows) {
754                 super(rows);
755                 this.rows = rows;
756             }
757 
758             @Override
759             protected String getInitialHeight() {
760                 return getRowHeight() + "px";
761             }
762 
763             @Override
764             protected double getBaseOffset() {
765                 return getRowHeight();
766             }
767 
768             @Override
769             protected double calculateHeightOfAllVisibleLines(double progress,
770                     double rh) {
771                 return rows.size() * rh * (1 - progress);
772             }
773 
774             @Override
775             protected double calculateDivOffset(double progress, double rh) {
776                 return -super.calculateDivOffset(progress, rh);
777             }
778         }
779     }
780 
781     /**
782      * Icons rendered into first actual column in TreeTable, not to row header
783      * cell
784      */
785     @Override
786     protected String buildCaptionHtmlSnippet(UIDL uidl) {
787         if (uidl.getTag().equals("column")) {
788             return super.buildCaptionHtmlSnippet(uidl);
789         } else {
790             String s = uidl.getStringAttribute("caption");
791             return s;
792         }
793     }
794 
795     /** For internal use only. May be removed or replaced in the future. */
796     @Override
797     public boolean handleNavigation(int keycode, boolean ctrl, boolean shift) {
798         if (collapseRequest || focusParentResponsePending) {
799             // Enqueue the event if there might be pending content changes from
800             // the server
801             if (pendingNavigationEvents.size() < 10) {
802                 // Only keep 10 keyboard events in the queue
803                 PendingNavigationEvent pendingNavigationEvent = new PendingNavigationEvent(
804                         keycode, ctrl, shift);
805                 pendingNavigationEvents.add(pendingNavigationEvent);
806             }
807             return true;
808         }
809 
810         VTreeTableRow focusedRow = (VTreeTableRow) getFocusedRow();
811         if (focusedRow != null) {
812             if (focusedRow.canHaveChildren && ((keycode == KeyCodes.KEY_RIGHT
813                     && !focusedRow.open)
814                     || (keycode == KeyCodes.KEY_LEFT && focusedRow.open))) {
815                 if (!ctrl) {
816                     client.updateVariable(paintableId, "selectCollapsed", true,
817                             false);
818                 }
819                 sendSelectedRows(false);
820                 sendToggleCollapsedUpdate(focusedRow.getKey());
821                 return true;
822             } else if (keycode == KeyCodes.KEY_RIGHT && focusedRow.open) {
823                 // already expanded, move selection down if next is on a deeper
824                 // level (is-a-child)
825                 VTreeTableScrollBody body = (VTreeTableScrollBody) focusedRow
826                         .getParent();
827                 Iterator<Widget> iterator = body.iterator();
828                 VTreeTableRow next = null;
829                 while (iterator.hasNext()) {
830                     next = (VTreeTableRow) iterator.next();
831                     if (next == focusedRow) {
832                         next = (VTreeTableRow) iterator.next();
833                         break;
834                     }
835                 }
836                 if (next != null) {
837                     if (next.depth > focusedRow.depth) {
838                         selectionPending = true;
839                         return super.handleNavigation(getNavigationDownKey(),
840                                 ctrl, shift);
841                     }
842                 } else {
843                     // Note, a minor change here for a bit false behavior if
844                     // cache rows is disabled + last visible row + no childs for
845                     // the node
846                     selectionPending = true;
847                     return super.handleNavigation(getNavigationDownKey(), ctrl,
848                             shift);
849                 }
850             } else if (keycode == KeyCodes.KEY_LEFT) {
851                 // already collapsed move selection up to parent node
852                 // do on the server side as the parent is not necessary
853                 // rendered on the client, could check if parent is visible if
854                 // a performance issue arises
855 
856                 client.updateVariable(paintableId, "focusParent",
857                         focusedRow.getKey(), true);
858 
859                 // Set flag that we should enqueue navigation events until we
860                 // get a response to this request
861                 focusParentResponsePending = true;
862 
863                 return true;
864             }
865         }
866         return super.handleNavigation(keycode, ctrl, shift);
867     }
868 
869     public void sendToggleCollapsedUpdate(String rowKey) {
870         collapsedRowKey = rowKey;
871         collapseRequest = true;
872         client.updateVariable(paintableId, "toggleCollapsed", rowKey, true);
873     }
874 
875     @Override
876     public void onBrowserEvent(Event event) {
877         super.onBrowserEvent(event);
878         if (event.getTypeInt() == Event.ONKEYUP && selectionPending) {
879             sendSelectedRows();
880         }
881     }
882 
883     @Override
884     protected void sendSelectedRows(boolean immediately) {
885         super.sendSelectedRows(immediately);
886         selectionPending = false;
887     }
888 
889     @Override
890     protected void reOrderColumn(String columnKey, int newIndex) {
891         super.reOrderColumn(columnKey, newIndex);
892         // current impl not intelligent enough to survive without visiting the
893         // server to redraw content
894         client.sendPendingVariableChanges();
895     }
896 
897     @Override
898     public void setStyleName(String style) {
899         super.setStyleName(style + " v-treetable");
900     }
901 
902     @Override
903     public void updateTotalRows(UIDL uidl) {
904         // Make sure that initializedAndAttached & al are not reset when the
905         // totalrows are updated on expand/collapse requests.
906         int newTotalRows = uidl.getIntAttribute("totalrows");
907         setTotalRows(newTotalRows);
908     }
909 
910     @Override
911     public void updateFirstVisibleAndScrollIfNeeded(UIDL uidl) {
912         // Left out blank to avoid scrolling glitches described in https://jira.magnolia-cms.com/browse/MGNLUI-960.
913     }
914 }