]> source.dussan.org Git - vaadin-framework.git/commitdiff
Merged TreeTable into Vaadin core (#5371)
authorJonatan Kronqvist <jonatan.kronqvist@itmill.com>
Thu, 16 Jun 2011 07:55:38 +0000 (07:55 +0000)
committerJonatan Kronqvist <jonatan.kronqvist@itmill.com>
Thu, 16 Jun 2011 07:55:38 +0000 (07:55 +0000)
svn changeset:19416/svn branch:6.7

WebContent/VAADIN/themes/base/treetable/img/arrow-down.png [new file with mode: 0644]
WebContent/VAADIN/themes/base/treetable/img/arrow-right.png [new file with mode: 0644]
WebContent/VAADIN/themes/base/treetable/treetable.css [new file with mode: 0644]
src/com/vaadin/terminal/gwt/client/ui/VScrollTable.java
src/com/vaadin/terminal/gwt/client/ui/VTreeTable.java [new file with mode: 0644]
src/com/vaadin/ui/TreeTable.java [new file with mode: 0644]
src/com/vaadin/ui/treetable/Collapsible.java [new file with mode: 0644]
src/com/vaadin/ui/treetable/HierarchicalContainerOrderedWrapper.java [new file with mode: 0644]
tests/src/com/vaadin/tests/components/table/Tables.java
tests/src/com/vaadin/tests/components/treetable/TreeTables.java [new file with mode: 0644]

diff --git a/WebContent/VAADIN/themes/base/treetable/img/arrow-down.png b/WebContent/VAADIN/themes/base/treetable/img/arrow-down.png
new file mode 100644 (file)
index 0000000..cba812b
Binary files /dev/null and b/WebContent/VAADIN/themes/base/treetable/img/arrow-down.png differ
diff --git a/WebContent/VAADIN/themes/base/treetable/img/arrow-right.png b/WebContent/VAADIN/themes/base/treetable/img/arrow-right.png
new file mode 100644 (file)
index 0000000..7fe6e2b
Binary files /dev/null and b/WebContent/VAADIN/themes/base/treetable/img/arrow-right.png differ
diff --git a/WebContent/VAADIN/themes/base/treetable/treetable.css b/WebContent/VAADIN/themes/base/treetable/treetable.css
new file mode 100644 (file)
index 0000000..ee5aaed
--- /dev/null
@@ -0,0 +1,20 @@
+.v-treetable-treespacer {
+    display: inline-block;
+    background: transparent;
+    height: 9px;
+    /* defines the amount of indent per level */
+    width: 18px;
+}
+
+.v-treetable-node-closed {
+    background: url(../treetable/img/arrow-right.png) right center no-repeat;
+}
+
+.v-treetable-node-open {
+    background: url(../treetable/img/arrow-down.png) right center no-repeat;
+}
+
+.v-treetable .v-checkbox {
+       display: inline-block;
+       padding-bottom: 4px;
+}
\ No newline at end of file
index d36c2137d81bb402ead48ba2ea3a39c2b2b51148..fdf61b8292d81792a1eb5f83008e5e1cdbe9b739 100644 (file)
@@ -1362,7 +1362,7 @@ public class VScrollTable extends FlowPanel implements Table, ScrollHandler,
      *            The key to search with
      * @return
      */
-    private VScrollTableRow getRenderedRowByKey(String key) {
+    protected VScrollTableRow getRenderedRowByKey(String key) {
         if (scrollBody != null) {
             final Iterator<Widget> it = scrollBody.iterator();
             VScrollTableRow r = null;
@@ -5452,7 +5452,7 @@ public class VScrollTable extends FlowPanel implements Table, ScrollHandler,
      *            The row to where the selection head should move
      * @return Returns true if focus was moved successfully, else false
      */
-    private boolean setRowFocus(VScrollTableRow row) {
+    protected boolean setRowFocus(VScrollTableRow row) {
 
         if (selectMode == SELECT_MODE_NONE) {
             return false;
diff --git a/src/com/vaadin/terminal/gwt/client/ui/VTreeTable.java b/src/com/vaadin/terminal/gwt/client/ui/VTreeTable.java
new file mode 100644 (file)
index 0000000..974e96e
--- /dev/null
@@ -0,0 +1,345 @@
+/*
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client.ui;
+
+import java.util.Iterator;
+
+import com.google.gwt.dom.client.Document;
+import com.google.gwt.dom.client.ImageElement;
+import com.google.gwt.dom.client.SpanElement;
+import com.google.gwt.dom.client.Style.Unit;
+import com.google.gwt.event.dom.client.KeyCodes;
+import com.google.gwt.user.client.Element;
+import com.google.gwt.user.client.Event;
+import com.google.gwt.user.client.ui.Widget;
+import com.vaadin.terminal.gwt.client.ApplicationConnection;
+import com.vaadin.terminal.gwt.client.RenderSpace;
+import com.vaadin.terminal.gwt.client.UIDL;
+import com.vaadin.terminal.gwt.client.VConsole;
+import com.vaadin.terminal.gwt.client.ui.VScrollTable.VScrollTableBody.VScrollTableRow;
+import com.vaadin.terminal.gwt.client.ui.VTreeTable.VTreeTableScrollBody.VTreeTableRow;
+
+public class VTreeTable extends VScrollTable {
+
+    public static final String ATTRIBUTE_HIERARCHY_COLUMN_INDEX = "hci";
+    private boolean collapseRequest;
+    private boolean selectionPending;
+    private int colIndexOfHierarchy;
+    private String collapsedRowKey;
+    private VTreeTableScrollBody scrollBody;
+
+    @Override
+    public void updateFromUIDL(UIDL uidl, ApplicationConnection client) {
+        FocusableScrollPanel widget = null;
+        int scrollPosition = 0;
+        if (collapseRequest) {
+            widget = (FocusableScrollPanel) getWidget(1);
+            scrollPosition = widget.getScrollPosition();
+        }
+        colIndexOfHierarchy = uidl
+                .hasAttribute(ATTRIBUTE_HIERARCHY_COLUMN_INDEX) ? uidl
+                .getIntAttribute(ATTRIBUTE_HIERARCHY_COLUMN_INDEX) : 0;
+        super.updateFromUIDL(uidl, client);
+        if (collapseRequest) {
+            if (collapsedRowKey != null && scrollBody != null) {
+                VScrollTableRow row = getRenderedRowByKey(collapsedRowKey);
+                if (row != null) {
+                    setRowFocus(row);
+                    focus();
+                }
+            }
+
+            int scrollPosition2 = widget.getScrollPosition();
+            if (scrollPosition != scrollPosition2) {
+                VConsole.log("TT scrollpos from " + scrollPosition + " to "
+                        + scrollPosition2);
+                widget.setScrollPosition(scrollPosition);
+            }
+            collapseRequest = false;
+        }
+        if (uidl.hasAttribute("focusedRow")) {
+            // TODO figure out if the row needs to focused at all
+
+            // scrolled to parent by the server, focusedRow is probably the sam
+            // as the first row in view port
+        }
+    }
+
+    @Override
+    protected VScrollTableBody createScrollBody() {
+        scrollBody = new VTreeTableScrollBody();
+        return scrollBody;
+    }
+
+    class VTreeTableScrollBody extends VScrollTable.VScrollTableBody {
+        private int identWidth = -1;
+
+        VTreeTableScrollBody() {
+            super();
+        }
+
+        @Override
+        protected VScrollTableRow createRow(UIDL uidl, char[] aligns2) {
+            return new VTreeTableRow(uidl, aligns2);
+        }
+
+        class VTreeTableRow extends
+                VScrollTable.VScrollTableBody.VScrollTableRow {
+
+            private boolean isTreeCellAdded = false;
+            private SpanElement treeSpacer;
+            private boolean open;
+            private int depth;
+            private boolean canHaveChildren;
+            private Widget widgetInHierarchyColumn;
+
+            public VTreeTableRow(UIDL uidl, char[] aligns2) {
+                super(uidl, aligns2);
+            }
+
+            @Override
+            public void addCell(UIDL rowUidl, String text, char align,
+                    String style, boolean textIsHTML, boolean isSorted) {
+                super.addCell(rowUidl, text, align, style, textIsHTML, isSorted);
+
+                addTreeSpacer(rowUidl);
+            }
+
+            private boolean addTreeSpacer(UIDL rowUidl) {
+                if (cellShowsTreeHierarchy(getElement().getChildCount() - 1)) {
+                    Element container = (Element) getElement().getLastChild()
+                            .getFirstChild();
+
+                    if (rowUidl.hasAttribute("icon")) {
+                        // icons are in first content cell in TreeTable
+                        ImageElement icon = Document.get().createImageElement();
+                        icon.setClassName("v-icon");
+                        icon.setAlt("icon");
+                        icon.setSrc(client.translateVaadinUri(rowUidl
+                                .getStringAttribute("icon")));
+                        container.insertFirst(icon);
+                    }
+
+                    String classname = "v-treetable-treespacer";
+                    if (rowUidl.getBooleanAttribute("ca")) {
+                        canHaveChildren = true;
+                        open = rowUidl.getBooleanAttribute("open");
+                        classname += open ? " v-treetable-node-open"
+                                : " v-treetable-node-closed";
+                    }
+
+                    treeSpacer = Document.get().createSpanElement();
+
+                    treeSpacer.setClassName(classname);
+                    container.insertFirst(treeSpacer);
+                    depth = rowUidl.hasAttribute("depth") ? rowUidl
+                            .getIntAttribute("depth") : 0;
+                    setIdent();
+                    isTreeCellAdded = true;
+                    return true;
+                }
+                return false;
+            }
+
+            private boolean cellShowsTreeHierarchy(int curColIndex) {
+                if (isTreeCellAdded) {
+                    return false;
+                }
+                return curColIndex == colIndexOfHierarchy
+                        + (showRowHeaders ? 1 : 0);
+            }
+
+            @Override
+            public void onBrowserEvent(Event event) {
+                if (event.getEventTarget().cast() == treeSpacer
+                        && treeSpacer.getClassName().contains("node")) {
+                    if (event.getTypeInt() == Event.ONMOUSEUP) {
+                        sendToggleCollapsedUpdate(getKey());
+                    }
+                    return;
+                }
+                super.onBrowserEvent(event);
+            }
+
+            @Override
+            public void addCell(UIDL rowUidl, Widget w, char align,
+                    String style, boolean isSorted) {
+                super.addCell(rowUidl, w, align, style, isSorted);
+                if (addTreeSpacer(rowUidl)) {
+                    widgetInHierarchyColumn = w;
+                }
+
+            }
+
+            private void setIdent() {
+                if (getIdentWidth() > 0 && depth != 0) {
+                    treeSpacer.getStyle().setWidth(
+                            (depth + 1) * getIdentWidth(), Unit.PX);
+                }
+            }
+
+            @Override
+            protected void onAttach() {
+                super.onAttach();
+                if (getIdentWidth() < 0) {
+                    detectIdent(this);
+                }
+            }
+
+            @Override
+            public RenderSpace getAllocatedSpace(Widget child) {
+                if (widgetInHierarchyColumn == child) {
+                    final int hierarchyAndIconWidth = getHierarchyAndIconWidth();
+                    final RenderSpace allocatedSpace = super
+                            .getAllocatedSpace(child);
+                    return new RenderSpace() {
+                        @Override
+                        public int getWidth() {
+                            return allocatedSpace.getWidth()
+                                    - hierarchyAndIconWidth;
+                        }
+
+                        @Override
+                        public int getHeight() {
+                            return allocatedSpace.getHeight();
+                        }
+
+                    };
+                }
+                return super.getAllocatedSpace(child);
+            }
+
+            private int getHierarchyAndIconWidth() {
+                int consumedSpace = treeSpacer.getOffsetWidth();
+                if (treeSpacer.getParentElement().getChildCount() > 2) {
+                    // icon next to tree spacer
+                    consumedSpace += ((com.google.gwt.dom.client.Element) treeSpacer
+                            .getNextSibling()).getOffsetWidth();
+                }
+                return consumedSpace;
+            }
+
+        }
+
+        private int getIdentWidth() {
+            return identWidth;
+        }
+
+        private void detectIdent(VTreeTableRow vTreeTableRow) {
+            identWidth = vTreeTableRow.treeSpacer.getOffsetWidth();
+            if (identWidth == 0) {
+                identWidth = -1;
+                return;
+            }
+            Iterator<Widget> iterator = iterator();
+            while (iterator.hasNext()) {
+                VTreeTableRow next = (VTreeTableRow) iterator.next();
+                next.setIdent();
+            }
+        }
+    }
+
+    /**
+     * Icons rendered into first actual column in TreeTable, not to row header
+     * cell
+     */
+    @Override
+    protected String buildCaptionHtmlSnippet(UIDL uidl) {
+        if (uidl.getTag().equals("column")) {
+            return super.buildCaptionHtmlSnippet(uidl);
+        } else {
+            String s = uidl.getStringAttribute("caption");
+            return s;
+        }
+    }
+
+    @Override
+    protected boolean handleNavigation(int keycode, boolean ctrl, boolean shift) {
+        VTreeTableRow focusedRow = (VTreeTableRow) getFocusedRow();
+        if (focusedRow != null) {
+            if (focusedRow.canHaveChildren
+                    && ((keycode == KeyCodes.KEY_RIGHT && !focusedRow.open) || (keycode == KeyCodes.KEY_LEFT && focusedRow.open))) {
+                if (!ctrl) {
+                    client.updateVariable(paintableId, "selectCollapsed", true,
+                            false);
+                }
+                sendToggleCollapsedUpdate(focusedRow.getKey());
+                return true;
+            } else if (keycode == KeyCodes.KEY_RIGHT && focusedRow.open) {
+                // already expanded, move selection down if next is on a deeper
+                // level (is-a-child)
+                VTreeTableScrollBody body = (VTreeTableScrollBody) focusedRow
+                        .getParent();
+                Iterator<Widget> iterator = body.iterator();
+                VTreeTableRow next = null;
+                while (iterator.hasNext()) {
+                    next = (VTreeTableRow) iterator.next();
+                    if (next == focusedRow) {
+                        next = (VTreeTableRow) iterator.next();
+                        break;
+                    }
+                }
+                if (next != null) {
+                    if (next.depth > focusedRow.depth) {
+                        selectionPending = true;
+                        return super.handleNavigation(getNavigationDownKey(),
+                                ctrl, shift);
+                    }
+                } else {
+                    // Note, a minor change here for a bit false behavior if
+                    // cache rows is disabled + last visible row + no childs for
+                    // the node
+                    selectionPending = true;
+                    return super.handleNavigation(getNavigationDownKey(), ctrl,
+                            shift);
+                }
+            } else if (keycode == KeyCodes.KEY_LEFT) {
+                // already collapsed move selection up to parent node
+                // do on the server side as the parent is not necessary
+                // rendered on the client, could check if parent is visible if
+                // a performance issue arises
+
+                client.updateVariable(paintableId, "focusParent",
+                        focusedRow.getKey(), true);
+                return true;
+            }
+        }
+        return super.handleNavigation(keycode, ctrl, shift);
+    }
+
+    private void sendToggleCollapsedUpdate(String rowKey) {
+        collapsedRowKey = rowKey;
+        collapseRequest = true;
+        client.updateVariable(paintableId, "toggleCollapsed", rowKey, true);
+    }
+
+    @Override
+    public void onBrowserEvent(Event event) {
+        super.onBrowserEvent(event);
+        if (event.getTypeInt() == Event.ONKEYUP && selectionPending) {
+            sendSelectedRows();
+        }
+    }
+
+    @Override
+    protected void sendSelectedRows() {
+        super.sendSelectedRows();
+        selectionPending = false;
+    }
+
+    @Override
+    protected void reOrderColumn(String columnKey, int newIndex) {
+        super.reOrderColumn(columnKey, newIndex);
+        // current impl not intelligent enough to survive without visiting the
+        // server to redraw content
+        client.sendPendingVariableChanges();
+    }
+
+    @Override
+    public void setStyleName(String style) {
+        super.setStyleName(style + " v-treetable");
+    }
+
+}
diff --git a/src/com/vaadin/ui/TreeTable.java b/src/com/vaadin/ui/TreeTable.java
new file mode 100644 (file)
index 0000000..20fc8e4
--- /dev/null
@@ -0,0 +1,579 @@
+/*
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.ui;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+
+import com.google.gwt.user.client.ui.Tree;
+import com.vaadin.data.Container;
+import com.vaadin.data.Container.Hierarchical;
+import com.vaadin.data.Container.ItemSetChangeEvent;
+import com.vaadin.data.util.ContainerHierarchicalWrapper;
+import com.vaadin.data.util.HierarchicalContainer;
+import com.vaadin.terminal.PaintException;
+import com.vaadin.terminal.PaintTarget;
+import com.vaadin.terminal.Resource;
+import com.vaadin.terminal.gwt.client.ui.VTreeTable;
+import com.vaadin.ui.treetable.Collapsible;
+import com.vaadin.ui.treetable.HierarchicalContainerOrderedWrapper;
+
+/**
+ * TreeTable extends the {@link Table} component so that it can also visualize a
+ * hierarchy of its Items in a similar manner that {@link Tree} does. The tree
+ * hierarchy is always displayed in the first actual column of the TreeTable.
+ * <p>
+ * The TreeTable supports the usual {@link Table} features like lazy loading, so
+ * it should be no problem to display lots of items at once. Only required rows
+ * and some cache rows are sent to the client.
+ * <p>
+ * TreeTable supports standard {@link Hierarchical} container interfaces, but
+ * also a more fine tuned version - {@link Collapsible}. A container
+ * implementing the {@link Collapsible} interface stores the collapsed/expanded
+ * state internally and can this way scale better on the server side than with
+ * standard Hierarchical implementations. Developer must however note that
+ * {@link Collapsible} containers can not be shared among several users as they
+ * share UI state in the container.
+ */
+@SuppressWarnings({ "serial" })
+@ClientWidget(VTreeTable.class)
+public class TreeTable extends Table implements Hierarchical {
+
+    private interface ContainerStrategy extends Serializable {
+        public int size();
+
+        public boolean isNodeOpen(Object itemId);
+
+        public int getDepth(Object itemId);
+
+        public void toggleChildVisibility(Object itemId);
+
+        public Object getIdByIndex(int index);
+
+        public int indexOfId(Object id);
+
+        public Object nextItemId(Object itemId);
+
+        public Object lastItemId();
+
+        public Object prevItemId(Object itemId);
+
+        public boolean isLastId(Object itemId);
+
+        public Collection<?> getItemIds();
+
+        public void containerItemSetChange(ItemSetChangeEvent event);
+    }
+
+    private abstract class AbstractStrategy implements ContainerStrategy {
+
+        /**
+         * Consider adding getDepth to {@link Collapsible}, might help
+         * scalability with some container implementations.
+         */
+        public int getDepth(Object itemId) {
+            int depth = 0;
+            Hierarchical hierarchicalContainer = getContainerDataSource();
+            while (!hierarchicalContainer.isRoot(itemId)) {
+                depth++;
+                itemId = hierarchicalContainer.getParent(itemId);
+            }
+            return depth;
+        }
+
+        public void containerItemSetChange(ItemSetChangeEvent event) {
+        }
+
+    }
+
+    /**
+     * This strategy is used if current container implements {@link Collapsible}
+     * .
+     * 
+     * open-collapsed logic diverted to container, otherwise use default
+     * implementations.
+     */
+    private class CollapsibleStrategy extends AbstractStrategy {
+
+        private Collapsible c() {
+            return (Collapsible) getContainerDataSource();
+        }
+
+        public void toggleChildVisibility(Object itemId) {
+            c().setCollapsed(itemId, !c().isCollapsed(itemId));
+        }
+
+        public boolean isNodeOpen(Object itemId) {
+            return !c().isCollapsed(itemId);
+        }
+
+        public int size() {
+            return TreeTable.super.size();
+        }
+
+        public Object getIdByIndex(int index) {
+            return TreeTable.super.getIdByIndex(index);
+        }
+
+        public int indexOfId(Object id) {
+            return TreeTable.super.indexOfId(id);
+        }
+
+        public boolean isLastId(Object itemId) {
+            // using the default impl
+            return TreeTable.super.isLastId(itemId);
+        }
+
+        public Object lastItemId() {
+            // using the default impl
+            return TreeTable.super.lastItemId();
+        }
+
+        public Object nextItemId(Object itemId) {
+            return TreeTable.super.nextItemId(itemId);
+        }
+
+        public Object prevItemId(Object itemId) {
+            return TreeTable.super.prevItemId(itemId);
+        }
+
+        public Collection<?> getItemIds() {
+            return TreeTable.super.getItemIds();
+        }
+
+    }
+
+    /**
+     * Strategy for Hierarchical but not Collapsible container like
+     * {@link HierarchicalContainer}.
+     * 
+     * Store collapsed/open states internally, fool Table to use preorder when
+     * accessing items from container via Ordered/Indexed methods.
+     */
+    private class HierarchicalStrategy extends AbstractStrategy {
+
+        private final HashSet<Object> openItems = new HashSet<Object>();
+
+        public boolean isNodeOpen(Object itemId) {
+            return openItems.contains(itemId);
+        }
+
+        public int size() {
+            return getPreOrder().size();
+        }
+
+        public Collection<Object> getItemIds() {
+            return Collections.unmodifiableCollection(getPreOrder());
+        }
+
+        public boolean isLastId(Object itemId) {
+            return itemId.equals(lastItemId());
+        }
+
+        public Object lastItemId() {
+            if (getPreOrder().size() > 0) {
+                return getPreOrder().get(getPreOrder().size() - 1);
+            } else {
+                return null;
+            }
+        }
+
+        public Object nextItemId(Object itemId) {
+            int indexOf = getPreOrder().indexOf(itemId);
+            if (indexOf == -1) {
+                return null;
+            }
+            indexOf++;
+            if (indexOf == getPreOrder().size()) {
+                return null;
+            } else {
+                return getPreOrder().get(indexOf);
+            }
+        }
+
+        public Object prevItemId(Object itemId) {
+            int indexOf = getPreOrder().indexOf(itemId);
+            indexOf--;
+            if (indexOf < 0) {
+                return null;
+            } else {
+                return getPreOrder().get(indexOf);
+            }
+        }
+
+        public void toggleChildVisibility(Object itemId) {
+            boolean removed = openItems.remove(itemId);
+            if (!removed) {
+                openItems.add(itemId);
+            }
+            clearPreorderCache();
+        }
+
+        private void clearPreorderCache() {
+            preOrder = null; // clear preorder cache
+        }
+
+        List<Object> preOrder;
+
+        /**
+         * Preorder of ids currently visible
+         * 
+         * @return
+         */
+        private List<Object> getPreOrder() {
+            if (preOrder == null) {
+                preOrder = new ArrayList<Object>();
+                Collection<?> rootItemIds = getContainerDataSource()
+                        .rootItemIds();
+                for (Object id : rootItemIds) {
+                    preOrder.add(id);
+                    addVisibleChildTree(id);
+                }
+            }
+            return preOrder;
+        }
+
+        private void addVisibleChildTree(Object id) {
+            if (isNodeOpen(id)) {
+                Collection<?> children = getContainerDataSource().getChildren(
+                        id);
+                if (children != null) {
+                    for (Object childId : children) {
+                        preOrder.add(childId);
+                        addVisibleChildTree(childId);
+                    }
+                }
+            }
+
+        }
+
+        public int indexOfId(Object id) {
+            return getPreOrder().indexOf(id);
+        }
+
+        public Object getIdByIndex(int index) {
+            return getPreOrder().get(index);
+        }
+
+        @Override
+        public void containerItemSetChange(ItemSetChangeEvent event) {
+            // preorder becomes invalid on sort, item additions etc.
+            clearPreorderCache();
+            super.containerItemSetChange(event);
+        }
+
+    }
+
+    /**
+     * Creates an empty TreeTable with a default container.
+     */
+    public TreeTable() {
+        super(null, new HierarchicalContainer());
+    }
+
+    /**
+     * Creates an empty TreeTable with a default container.
+     * 
+     * @param caption
+     *            the caption for the TreeTable
+     */
+    public TreeTable(String caption) {
+        this();
+        setCaption(caption);
+    }
+
+    /**
+     * Creates a TreeTable instance with given captions and data source.
+     * 
+     * @param caption
+     *            the caption for the component
+     * @param dataSource
+     *            the dataSource that is used to list items in the component
+     */
+    public TreeTable(String caption, Container dataSource) {
+        super(caption, dataSource);
+    }
+
+    private ContainerStrategy cStrategy;
+    private Object focusedRowId = null;
+    private Object hierarchyColumnId;
+
+    private ContainerStrategy getContainerStrategy() {
+        if (cStrategy == null) {
+            if (getContainerDataSource() instanceof Collapsible) {
+                cStrategy = new CollapsibleStrategy();
+            } else {
+                cStrategy = new HierarchicalStrategy();
+            }
+        }
+        return cStrategy;
+    }
+
+    @Override
+    protected void paintRowAttributes(PaintTarget target, Object itemId)
+            throws PaintException {
+        super.paintRowAttributes(target, itemId);
+        target.addAttribute("depth", getContainerStrategy().getDepth(itemId));
+        if (getContainerDataSource().areChildrenAllowed(itemId)) {
+            target.addAttribute("ca", true);
+            target.addAttribute("open",
+                    getContainerStrategy().isNodeOpen(itemId));
+        }
+    }
+
+    @Override
+    protected void paintRowIcon(PaintTarget target, Object[][] cells,
+            int indexInRowbuffer) throws PaintException {
+        // always paint if present (in parent only if row headers visible)
+        if (getRowHeaderMode() == ROW_HEADER_MODE_HIDDEN) {
+            Resource itemIcon = getItemIcon(cells[CELL_ITEMID][indexInRowbuffer]);
+            if (itemIcon != null) {
+                target.addAttribute("icon", itemIcon);
+            }
+        } else if (cells[CELL_ICON][indexInRowbuffer] != null) {
+            target.addAttribute("icon",
+                    (Resource) cells[CELL_ICON][indexInRowbuffer]);
+        }
+    }
+
+    @Override
+    public void changeVariables(Object source, Map<String, Object> variables) {
+        super.changeVariables(source, variables);
+
+        if (variables.containsKey("toggleCollapsed")) {
+            String object = (String) variables.get("toggleCollapsed");
+            Object itemId = itemIdMapper.get(object);
+            toggleChildVisibility(itemId);
+            if (variables.containsKey("selectCollapsed")) {
+                // ensure collapsed is selected unless opened with selection
+                // head
+                if (isSelectable()) {
+                    select(itemId);
+                }
+            }
+        } else if (variables.containsKey("focusParent")) {
+            String key = (String) variables.get("focusParent");
+            Object refId = itemIdMapper.get(key);
+            Object itemId = getParent(refId);
+            focusParent(itemId);
+        }
+    }
+
+    private void focusParent(Object itemId) {
+        boolean inView = false;
+        Object inPageId = getCurrentPageFirstItemId();
+        for (int i = 0; inPageId != null && i < getPageLength(); i++) {
+            if (inPageId.equals(itemId)) {
+                inView = true;
+                break;
+            }
+            inPageId = nextItemId(inPageId);
+            i++;
+        }
+        if (!inView) {
+            setCurrentPageFirstItemId(itemId);
+        }
+        if (isSelectable()) {
+            if (isMultiSelect()) {
+                setValue(Collections.singleton(itemId));
+            } else {
+                setValue(itemId);
+            }
+        } else {
+            // just instruct the VTreeTable to set focus the row (not to select)
+            setFocusedRow(itemId);
+        }
+    }
+
+    private void setFocusedRow(Object itemId) {
+        focusedRowId = itemId;
+        requestRepaint();
+    }
+
+    @Override
+    public void paintContent(PaintTarget target) throws PaintException {
+        if (focusedRowId != null) {
+            target.addAttribute("focusedRow", itemIdMapper.key(focusedRowId));
+            focusedRowId = null;
+        }
+        if (hierarchyColumnId != null) {
+            Object[] visibleColumns2 = getVisibleColumns();
+            for (int i = 0; i < visibleColumns2.length; i++) {
+                Object object = visibleColumns2[i];
+                if (hierarchyColumnId.equals(object)) {
+                    target.addAttribute(
+                            VTreeTable.ATTRIBUTE_HIERARCHY_COLUMN_INDEX, i);
+                    break;
+                }
+            }
+        }
+        super.paintContent(target);
+    }
+
+    private void toggleChildVisibility(Object itemId) {
+        getContainerStrategy().toggleChildVisibility(itemId);
+        // ensure that page still has first item in page, ignore buffer refresh
+        // (forced in this method)
+        setCurrentPageFirstItemIndex(getCurrentPageFirstItemIndex());
+
+        requestRepaint();
+    }
+
+    @Override
+    public int size() {
+        return getContainerStrategy().size();
+    }
+
+    @Override
+    public Hierarchical getContainerDataSource() {
+        return (Hierarchical) super.getContainerDataSource();
+    }
+
+    @Override
+    public void setContainerDataSource(Container newDataSource) {
+        cStrategy = null;
+        if (!(newDataSource instanceof Hierarchical)) {
+            newDataSource = new ContainerHierarchicalWrapper(newDataSource);
+        }
+
+        if (!(newDataSource instanceof Ordered)) {
+            newDataSource = new HierarchicalContainerOrderedWrapper(
+                    (Hierarchical) newDataSource);
+        }
+
+        super.setContainerDataSource(newDataSource);
+    }
+
+    @Override
+    public void containerItemSetChange(
+            com.vaadin.data.Container.ItemSetChangeEvent event) {
+        getContainerStrategy().containerItemSetChange(event);
+        super.containerItemSetChange(event);
+    }
+
+    @Override
+    protected Object getIdByIndex(int index) {
+        return getContainerStrategy().getIdByIndex(index);
+    }
+
+    @Override
+    protected int indexOfId(Object itemId) {
+        return getContainerStrategy().indexOfId(itemId);
+    }
+
+    @Override
+    public Object nextItemId(Object itemId) {
+        return getContainerStrategy().nextItemId(itemId);
+    }
+
+    @Override
+    public Object lastItemId() {
+        return getContainerStrategy().lastItemId();
+    }
+
+    @Override
+    public Object prevItemId(Object itemId) {
+        return getContainerStrategy().prevItemId(itemId);
+    }
+
+    @Override
+    public boolean isLastId(Object itemId) {
+        return getContainerStrategy().isLastId(itemId);
+    }
+
+    @Override
+    public Collection<?> getItemIds() {
+        return getContainerStrategy().getItemIds();
+    }
+
+    public boolean areChildrenAllowed(Object itemId) {
+        return getContainerDataSource().areChildrenAllowed(itemId);
+    }
+
+    public Collection<?> getChildren(Object itemId) {
+        return getContainerDataSource().getChildren(itemId);
+    }
+
+    public Object getParent(Object itemId) {
+        return getContainerDataSource().getParent(itemId);
+    }
+
+    public boolean hasChildren(Object itemId) {
+        return getContainerDataSource().hasChildren(itemId);
+    }
+
+    public boolean isRoot(Object itemId) {
+        return getContainerDataSource().isRoot(itemId);
+    }
+
+    public Collection<?> rootItemIds() {
+        return getContainerDataSource().rootItemIds();
+    }
+
+    public boolean setChildrenAllowed(Object itemId, boolean areChildrenAllowed)
+            throws UnsupportedOperationException {
+        return getContainerDataSource().setChildrenAllowed(itemId,
+                areChildrenAllowed);
+    }
+
+    public boolean setParent(Object itemId, Object newParentId)
+            throws UnsupportedOperationException {
+        return getContainerDataSource().setParent(itemId, newParentId);
+    }
+
+    /**
+     * Sets the Item specified by given identifier collapsed or expanded. If the
+     * Item is collapsed, its children is not displayed in for the user.
+     * 
+     * @param itemId
+     *            the identifier of the Item
+     * @param collapsed
+     *            true if the Item should be collapsed, false if expanded
+     */
+    public void setCollapsed(Object itemId, boolean collapsed) {
+        if (isCollapsed(itemId) != collapsed) {
+            toggleChildVisibility(itemId);
+        }
+    }
+
+    /**
+     * Checks if Item with given identifier is collapsed in the UI.
+     * 
+     * <p>
+     * 
+     * @param itemId
+     *            the identifier of the checked Item
+     * @return true if the Item with given id is collapsed
+     * @see Collapsible#isCollapsed(Object)
+     */
+    public boolean isCollapsed(Object itemId) {
+        return !getContainerStrategy().isNodeOpen(itemId);
+    }
+
+    /**
+     * Explicitly sets the column in which the TreeTable visualizes the
+     * hierarchy. If hierarchyColumnId is not set, the hierarchy is visualized
+     * in the first visible column.
+     * 
+     * @param hierarchyColumnId
+     */
+    public void setHierarchyColumn(Object hierarchyColumnId) {
+        this.hierarchyColumnId = hierarchyColumnId;
+    }
+
+    /**
+     * @return the identifier of column into which the hierarchy will be
+     *         visualized or null if the column is not explicitly defined.
+     */
+    public Object getHierarchyColumnId() {
+        return hierarchyColumnId;
+    }
+
+}
diff --git a/src/com/vaadin/ui/treetable/Collapsible.java b/src/com/vaadin/ui/treetable/Collapsible.java
new file mode 100644 (file)
index 0000000..6496877
--- /dev/null
@@ -0,0 +1,66 @@
+package com.vaadin.ui.treetable;
+
+import com.vaadin.data.Container;
+import com.vaadin.data.Container.Hierarchical;
+import com.vaadin.data.Container.Ordered;
+import com.vaadin.data.Item;
+
+/**
+ * Container needed by large lazy loading hierarchies displayed in TreeTable.
+ * <p>
+ * Container of this type gets notified when a subtree is opened/closed in a
+ * component displaying its content. This allows container to lazy load subtrees
+ * and release memory when a sub-tree is no longer displayed.
+ * <p>
+ * Methods from {@link Container.Ordered} (and from {@linkContainer.Indexed} if
+ * implemented) are expected to work as in "preorder" of the currently visible
+ * hierarchy. This means for example that the return value of size method
+ * changes when subtree is collapsed/expanded. In other words items in collapsed
+ * sub trees should be "ignored" by container when the container is accessed
+ * with methods introduced in {@link Container.Ordered} or
+ * {@linkContainer.Indexed}. From the accessors point of view, items in
+ * collapsed subtrees don't exist.
+ * <p>
+ * 
+ */
+public interface Collapsible extends Hierarchical, Ordered {
+
+    /**
+     * <p>
+     * Collapsing the {@link Item} indicated by <code>itemId</code> hides all
+     * children, and their respective children, from the {@link Container}.
+     * </p>
+     * 
+     * <p>
+     * If called on a leaf {@link Item}, this method does nothing.
+     * </p>
+     * 
+     * @param itemId
+     *            the identifier of the collapsed {@link Item}
+     * @param collapsed
+     *            <code>true</code> if you want to collapse the children below
+     *            this {@link Item}. <code>false</code> if you want to
+     *            uncollapse the children.
+     */
+    public void setCollapsed(Object itemId, boolean collapsed);
+
+    /**
+     * <p>
+     * Checks whether the {@link Item}, identified by <code>itemId</code> is
+     * collapsed or not.
+     * </p>
+     * 
+     * <p>
+     * If an {@link Item} is "collapsed" its children are not included in
+     * methods used to list Items in this container.
+     * </p>
+     * 
+     * @param itemId
+     *            The {@link Item}'s identifier that is to be checked.
+     * @return <code>true</code> iff the {@link Item} identified by
+     *         <code>itemId</code> is currently collapsed, otherwise
+     *         <code>false</code>.
+     */
+    public boolean isCollapsed(Object itemId);
+
+}
diff --git a/src/com/vaadin/ui/treetable/HierarchicalContainerOrderedWrapper.java b/src/com/vaadin/ui/treetable/HierarchicalContainerOrderedWrapper.java
new file mode 100644 (file)
index 0000000..0c00027
--- /dev/null
@@ -0,0 +1,58 @@
+package com.vaadin.ui.treetable;
+
+import java.util.Collection;
+
+import com.vaadin.data.Container;
+import com.vaadin.data.Container.Hierarchical;
+import com.vaadin.data.util.ContainerOrderedWrapper;
+
+@SuppressWarnings({ "serial", "unchecked" })
+/**
+ * Helper for TreeTable. Does the same thing as ContainerOrderedWrapper 
+ * to fit into table but retains Hierarchical feature.
+ */
+public class HierarchicalContainerOrderedWrapper extends
+        ContainerOrderedWrapper implements Hierarchical {
+
+    private Hierarchical hierarchical;
+
+    public HierarchicalContainerOrderedWrapper(Hierarchical toBeWrapped) {
+        super(toBeWrapped);
+        hierarchical = toBeWrapped;
+    }
+
+    public boolean areChildrenAllowed(Object itemId) {
+        return hierarchical.areChildrenAllowed(itemId);
+    }
+
+    public Collection<?> getChildren(Object itemId) {
+        return hierarchical.getChildren(itemId);
+    }
+
+    public Object getParent(Object itemId) {
+        return hierarchical.getParent(itemId);
+    }
+
+    public boolean hasChildren(Object itemId) {
+        return hierarchical.hasChildren(itemId);
+    }
+
+    public boolean isRoot(Object itemId) {
+        return hierarchical.isRoot(itemId);
+    }
+
+    public Collection<?> rootItemIds() {
+        return hierarchical.rootItemIds();
+    }
+
+    public boolean setChildrenAllowed(Object itemId, boolean areChildrenAllowed)
+            throws UnsupportedOperationException {
+        return hierarchical.setChildrenAllowed(itemId, areChildrenAllowed);
+    }
+
+    public boolean setParent(Object itemId, Object newParentId)
+            throws UnsupportedOperationException {
+        return hierarchical.setParent(itemId, newParentId);
+    }
+
+}
index 4a0b91453402123875565e481eba7df401f07b88..a128879e2175cc9364060cd7577eeb48f756829e 100644 (file)
@@ -18,8 +18,8 @@ import com.vaadin.ui.Table.FooterClickListener;
 import com.vaadin.ui.Table.HeaderClickEvent;\r
 import com.vaadin.ui.Table.HeaderClickListener;\r
 \r
-public class Tables extends AbstractSelectTestCase<Table> implements\r
-        ItemClickListener, HeaderClickListener, FooterClickListener,\r
+public class Tables<T extends Table> extends AbstractSelectTestCase<T>\r
+        implements ItemClickListener, HeaderClickListener, FooterClickListener,\r
         ColumnResizeListener {\r
 \r
     protected static final String CATEGORY_ROWS = "Rows";\r
@@ -28,12 +28,12 @@ public class Tables extends AbstractSelectTestCase<Table> implements
     private static final String CATEGORY_VISIBLE_COLUMNS = "Visible columns";\r
 \r
     @Override\r
-    protected Class<Table> getTestClass() {\r
-        return Table.class;\r
+    protected Class<T> getTestClass() {\r
+        return (Class) Table.class;\r
     }\r
 \r
     /* COMMANDS */\r
-    private Command<Table, Boolean> visibleColumnCommand = new Command<Table, Boolean>() {\r
+    private Command<T, Boolean> visibleColumnCommand = new Command<T, Boolean>() {\r
         public void execute(Table c, Boolean visible, Object propertyId) {\r
             List<Object> visibleColumns = new ArrayList<Object>(Arrays.asList(c\r
                     .getVisibleColumns()));\r
@@ -50,7 +50,7 @@ public class Tables extends AbstractSelectTestCase<Table> implements
         }\r
     };\r
 \r
-    protected Command<Table, Boolean> columnResizeListenerCommand = new Command<Table, Boolean>() {\r
+    protected Command<T, Boolean> columnResizeListenerCommand = new Command<T, Boolean>() {\r
 \r
         public void execute(Table c, Boolean value, Object data) {\r
             if (value) {\r
@@ -61,9 +61,9 @@ public class Tables extends AbstractSelectTestCase<Table> implements
         }\r
     };\r
 \r
-    protected Command<Table, Boolean> headerClickListenerCommand = new Command<Table, Boolean>() {\r
+    protected Command<T, Boolean> headerClickListenerCommand = new Command<T, Boolean>() {\r
 \r
-        public void execute(Table c, Boolean value, Object data) {\r
+        public void execute(T c, Boolean value, Object data) {\r
             if (value) {\r
                 c.addListener((HeaderClickListener) Tables.this);\r
             } else {\r
@@ -72,7 +72,7 @@ public class Tables extends AbstractSelectTestCase<Table> implements
         }\r
     };\r
 \r
-    protected Command<Table, Boolean> footerClickListenerCommand = new Command<Table, Boolean>() {\r
+    protected Command<T, Boolean> footerClickListenerCommand = new Command<T, Boolean>() {\r
 \r
         public void execute(Table c, Boolean value, Object data) {\r
             if (value) {\r
@@ -83,7 +83,7 @@ public class Tables extends AbstractSelectTestCase<Table> implements
         }\r
     };\r
 \r
-    protected Command<Table, Integer> rowHeaderModeCommand = new Command<Table, Integer>() {\r
+    protected Command<T, Integer> rowHeaderModeCommand = new Command<T, Integer>() {\r
 \r
         public void execute(Table c, Integer value, Object data) {\r
             if (value == Table.ROW_HEADER_MODE_PROPERTY) {\r
@@ -93,7 +93,7 @@ public class Tables extends AbstractSelectTestCase<Table> implements
         }\r
     };\r
 \r
-    protected Command<Table, String> footerTextCommand = new Command<Table, String>() {\r
+    protected Command<T, String> footerTextCommand = new Command<T, String>() {\r
 \r
         public void execute(Table c, String value, Object data) {\r
             for (Object propertyId : c.getContainerPropertyIds()) {\r
@@ -111,18 +111,18 @@ public class Tables extends AbstractSelectTestCase<Table> implements
 \r
     }\r
 \r
-    protected Command<Table, Alignments> columnAlignmentCommand = new Command<Table, Alignments>() {\r
+    protected Command<T, Alignments> columnAlignmentCommand = new Command<T, Alignments>() {\r
 \r
-        public void execute(Table c, Alignments value, Object data) {\r
+        public void execute(T c, Alignments value, Object data) {\r
             // TODO\r
             // for (Object propertyId : c.getContainerPropertyIds()) {\r
             // }\r
         }\r
     };\r
 \r
-    private Command<Table, ContextMenu> contextMenuCommand = new Command<Table, ContextMenu>() {\r
+    private Command<T, ContextMenu> contextMenuCommand = new Command<T, ContextMenu>() {\r
 \r
-        public void execute(Table c, final ContextMenu value, Object data) {\r
+        public void execute(T c, final ContextMenu value, Object data) {\r
             c.removeAllActionHandlers();\r
             if (value != null) {\r
                 c.addActionHandler(new Handler() {\r
@@ -207,7 +207,7 @@ public class Tables extends AbstractSelectTestCase<Table> implements
 \r
     private void createColumnReorderingAllowedCheckbox(String category) {\r
         createBooleanAction("Column reordering allowed", category, true,\r
-                new Command<Table, Boolean>() {\r
+                new Command<T, Boolean>() {\r
                     public void execute(Table c, Boolean value, Object data) {\r
                         c.setColumnReorderingAllowed(value);\r
                     }\r
@@ -216,8 +216,8 @@ public class Tables extends AbstractSelectTestCase<Table> implements
 \r
     private void createColumnCollapsingAllowedCheckbox(String category) {\r
         createBooleanAction("Column collapsing allowed", category, true,\r
-                new Command<Table, Boolean>() {\r
-                    public void execute(Table c, Boolean value, Object data) {\r
+                new Command<T, Boolean>() {\r
+                    public void execute(T c, Boolean value, Object data) {\r
                         c.setColumnCollapsingAllowed(value);\r
                     }\r
                 });\r
@@ -266,8 +266,8 @@ public class Tables extends AbstractSelectTestCase<Table> implements
         options.put("Header {id} - every second", "Header {id}");\r
 \r
         createSelectAction("Texts in header", category, options, "None",\r
-                new Command<Table, String>() {\r
-                    public void execute(Table c, String value, Object data) {\r
+                new Command<T, String>() {\r
+                    public void execute(T c, String value, Object data) {\r
                         int nr = 0;\r
                         for (Object propertyId : c.getContainerPropertyIds()) {\r
                             nr++;\r
@@ -326,9 +326,9 @@ public class Tables extends AbstractSelectTestCase<Table> implements
 \r
     protected void createFooterVisibilityCheckbox(String category) {\r
         createBooleanAction("Footer visible", category, true,\r
-                new Command<Table, Boolean>() {\r
+                new Command<T, Boolean>() {\r
 \r
-                    public void execute(Table c, Boolean value, Object data) {\r
+                    public void execute(T c, Boolean value, Object data) {\r
                         c.setFooterVisible(value);\r
                     }\r
                 });\r
@@ -343,9 +343,9 @@ public class Tables extends AbstractSelectTestCase<Table> implements
         options.put("Hidden", Table.COLUMN_HEADER_MODE_HIDDEN);\r
 \r
         createSelectAction("Header mode", category, options,\r
-                "Explicit defaults id", new Command<Table, Integer>() {\r
+                "Explicit defaults id", new Command<T, Integer>() {\r
 \r
-                    public void execute(Table c, Integer value, Object data) {\r
+                    public void execute(T c, Integer value, Object data) {\r
                         c.setColumnHeaderMode(value);\r
 \r
                     }\r
@@ -361,7 +361,7 @@ public class Tables extends AbstractSelectTestCase<Table> implements
         options.put("50", 50);\r
 \r
         createSelectAction("PageLength", category, options, "10",\r
-                new Command<Table, Integer>() {\r
+                new Command<T, Integer>() {\r
 \r
                     public void execute(Table t, Integer value, Object data) {\r
                         t.setPageLength(value);\r
@@ -381,7 +381,7 @@ public class Tables extends AbstractSelectTestCase<Table> implements
         options.put("Multi - ctrl/shift", SelectMode.MULTI);\r
 \r
         createSelectAction("Selection Mode", category, options,\r
-                "Multi - ctrl/shift", new Command<Table, SelectMode>() {\r
+                "Multi - ctrl/shift", new Command<T, SelectMode>() {\r
 \r
                     public void execute(Table t, SelectMode value, Object data) {\r
                         switch (value) {\r
diff --git a/tests/src/com/vaadin/tests/components/treetable/TreeTables.java b/tests/src/com/vaadin/tests/components/treetable/TreeTables.java
new file mode 100644 (file)
index 0000000..523869e
--- /dev/null
@@ -0,0 +1,291 @@
+package com.vaadin.tests.components.treetable;\r
+\r
+import java.util.ArrayList;\r
+import java.util.Iterator;\r
+import java.util.LinkedHashMap;\r
+import java.util.List;\r
+\r
+import com.vaadin.data.Container;\r
+import com.vaadin.data.Container.Hierarchical;\r
+import com.vaadin.data.util.HierarchicalContainer;\r
+import com.vaadin.tests.components.table.Tables;\r
+import com.vaadin.ui.Table.CellStyleGenerator;\r
+import com.vaadin.ui.Tree.CollapseEvent;\r
+import com.vaadin.ui.Tree.CollapseListener;\r
+import com.vaadin.ui.Tree.ExpandEvent;\r
+import com.vaadin.ui.TreeTable;\r
+\r
+public class TreeTables extends Tables<TreeTable> implements CollapseListener {\r
+\r
+    @Override\r
+    protected Class<TreeTable> getTestClass() {\r
+        return TreeTable.class;\r
+    }\r
+\r
+    private int rootItemIds = 3;\r
+    private CellStyleGenerator rootGreenSecondLevelRed = new com.vaadin.ui.Table.CellStyleGenerator() {\r
+\r
+        public String getStyle(Object itemId, Object propertyId) {\r
+            if (propertyId != null) {\r
+                return null;\r
+            }\r
+\r
+            Hierarchical c = getComponent().getContainerDataSource();\r
+            if (c.isRoot(itemId)) {\r
+                return "green";\r
+            }\r
+\r
+            Object parent = c.getParent(itemId);\r
+            if (!c.isRoot(parent)) {\r
+                return "red";\r
+            }\r
+\r
+            return null;\r
+        }\r
+\r
+        @Override\r
+        public String toString() {\r
+            return "Root green, second level red";\r
+        }\r
+\r
+    };\r
+\r
+    private CellStyleGenerator evenItemsBold = new CellStyleGenerator() {\r
+\r
+        public String getStyle(Object itemId, Object propertyId) {\r
+            if (propertyId != null) {\r
+                return null;\r
+            }\r
+\r
+            Hierarchical c = getComponent().getContainerDataSource();\r
+            int idx = 0;\r
+\r
+            for (Iterator<?> i = c.getItemIds().iterator(); i.hasNext();) {\r
+                Object id = i.next();\r
+                if (id == itemId) {\r
+                    if (idx % 2 == 1) {\r
+                        return "bold";\r
+                    } else {\r
+                        return null;\r
+                    }\r
+                }\r
+\r
+                idx++;\r
+            }\r
+\r
+            return null;\r
+        }\r
+\r
+        @Override\r
+        public String toString() {\r
+            return "Even items bold";\r
+        };\r
+\r
+    };\r
+\r
+    @Override\r
+    protected void createActions() {\r
+        super.createActions();\r
+\r
+        // Causes container changes so doing this first..\r
+        createRootItemSelectAction(CATEGORY_DATA_SOURCE);\r
+\r
+        createExpandCollapseActions(CATEGORY_FEATURES);\r
+        createSelectionModeSelect(CATEGORY_SELECTION);\r
+        createChildrenAllowedAction(CATEGORY_DATA_SOURCE);\r
+\r
+        createListeners(CATEGORY_LISTENERS);\r
+        // createItemStyleGenerator(CATEGORY_FEATURES);\r
+\r
+        // TODO: DropHandler\r
+        // TODO: DragMode\r
+        // TODO: ActionHandler\r
+\r
+    }\r
+\r
+    @Override\r
+    protected Container createContainer(int properties, int items) {\r
+        return createHierarchicalContainer(properties, items, rootItemIds);\r
+    }\r
+\r
+    private void createListeners(String category) {\r
+        // createBooleanAction("Expand listener", category, false,\r
+        // expandListenerCommand);\r
+        // createBooleanAction("Collapse listener", category, false,\r
+        // collapseListenerCommand);\r
+        createBooleanAction("Item click listener", category, false,\r
+                itemClickListenerCommand);\r
+\r
+    }\r
+\r
+    private Container.Hierarchical createHierarchicalContainer(int properties,\r
+            int items, int roots) {\r
+        Container.Hierarchical c = new HierarchicalContainer();\r
+\r
+        populateContainer(c, properties, items);\r
+\r
+        if (items <= roots) {\r
+            return c;\r
+        }\r
+\r
+        // "roots" roots, each with\r
+        // "firstLevel" children, two with no children (one with childAllowed,\r
+        // one without)\r
+        // ("firstLevel"-2)*"secondLevel" children ("secondLevel"/2 with\r
+        // childAllowed, "secondLevel"/2 without)\r
+\r
+        // N*M+N*(M-2)*C = items\r
+        // items=N(M+MC-2C)\r
+\r
+        // Using secondLevel=firstLevel/2 =>\r
+        // items = roots*(firstLevel+firstLevel*firstLevel/2-2*firstLevel/2)\r
+        // =roots*(firstLevel+firstLevel^2/2-firstLevel)\r
+        // = roots*firstLevel^2/2\r
+        // => firstLevel = sqrt(items/roots*2)\r
+\r
+        int firstLevel = (int) Math.ceil(Math.sqrt(items / roots * 2.0));\r
+        int secondLevel = firstLevel / 2;\r
+\r
+        while (roots * (1 + 2 + (firstLevel - 2) * secondLevel) < items) {\r
+            // Increase something so we get enough items\r
+            secondLevel++;\r
+        }\r
+\r
+        List<Object> itemIds = new ArrayList<Object>(c.getItemIds());\r
+\r
+        int nextItemId = roots;\r
+        for (int rootIndex = 0; rootIndex < roots; rootIndex++) {\r
+            // roots use items 0..roots-1\r
+            Object rootItemId = itemIds.get(rootIndex);\r
+\r
+            // force roots to be roots even though they automatically should be\r
+            c.setParent(rootItemId, null);\r
+\r
+            for (int firstLevelIndex = 0; firstLevelIndex < firstLevel; firstLevelIndex++) {\r
+                if (nextItemId >= items) {\r
+                    break;\r
+                }\r
+                Object firstLevelItemId = itemIds.get(nextItemId++);\r
+                c.setParent(firstLevelItemId, rootItemId);\r
+\r
+                if (firstLevelIndex < 2) {\r
+                    continue;\r
+                }\r
+\r
+                // firstLevelChildren 2.. have child nodes\r
+                for (int secondLevelIndex = 0; secondLevelIndex < secondLevel; secondLevelIndex++) {\r
+                    if (nextItemId >= items) {\r
+                        break;\r
+                    }\r
+\r
+                    Object secondLevelItemId = itemIds.get(nextItemId++);\r
+                    c.setParent(secondLevelItemId, firstLevelItemId);\r
+                }\r
+            }\r
+        }\r
+\r
+        return c;\r
+    }\r
+\r
+    private void createRootItemSelectAction(String category) {\r
+        LinkedHashMap<String, Integer> options = new LinkedHashMap<String, Integer>();\r
+        for (int i = 1; i <= 10; i++) {\r
+            options.put(String.valueOf(i), i);\r
+        }\r
+        options.put("20", 20);\r
+        options.put("50", 50);\r
+        options.put("100", 100);\r
+\r
+        createSelectAction("Number of root items", category, options, "3",\r
+                rootItemIdsCommand);\r
+    }\r
+\r
+    private void createExpandCollapseActions(String category) {\r
+        LinkedHashMap<String, Object> options = new LinkedHashMap<String, Object>();\r
+\r
+        for (Object id : getComponent().getItemIds()) {\r
+            options.put(id.toString(), id);\r
+        }\r
+        createMultiClickAction("Expand", category, options, expandItemCommand,\r
+                null);\r
+        // createMultiClickAction("Expand recursively", category, options,\r
+        // expandItemRecursivelyCommand, null);\r
+        createMultiClickAction("Collapse", category, options,\r
+                collapseItemCommand, null);\r
+\r
+    }\r
+\r
+    private void createChildrenAllowedAction(String category) {\r
+        LinkedHashMap<String, Object> options = new LinkedHashMap<String, Object>();\r
+\r
+        for (Object id : getComponent().getItemIds()) {\r
+            options.put(id.toString(), id);\r
+        }\r
+        createMultiToggleAction("Children allowed", category, options,\r
+                setChildrenAllowedCommand, true);\r
+\r
+    }\r
+\r
+    /*\r
+     * COMMANDS\r
+     */\r
+    private Command<TreeTable, Integer> rootItemIdsCommand = new Command<TreeTable, Integer>() {\r
+\r
+        public void execute(TreeTable c, Integer value, Object data) {\r
+            rootItemIds = value;\r
+            updateContainer();\r
+        }\r
+    };\r
+\r
+    private Command<TreeTable, Object> expandItemCommand = new Command<TreeTable, Object>() {\r
+\r
+        public void execute(TreeTable c, Object itemId, Object data) {\r
+            c.setCollapsed(itemId, false);\r
+        }\r
+    };\r
+\r
+    private Command<TreeTable, Object> collapseItemCommand = new Command<TreeTable, Object>() {\r
+\r
+        public void execute(TreeTable c, Object itemId, Object data) {\r
+            c.setCollapsed(itemId, true);\r
+        }\r
+    };\r
+\r
+    private Command<TreeTable, Boolean> setChildrenAllowedCommand = new Command<TreeTable, Boolean>() {\r
+\r
+        public void execute(TreeTable c, Boolean areChildrenAllowed,\r
+                Object itemId) {\r
+            c.setChildrenAllowed(itemId, areChildrenAllowed);\r
+        }\r
+    };\r
+\r
+    // private Command<TreeTable, Boolean> expandListenerCommand = new\r
+    // Command<TreeTable, Boolean>() {\r
+    // public void execute(TreeTable c, Boolean value, Object data) {\r
+    // if (value) {\r
+    // c.addListener((ExpandListener) TreeTables.this);\r
+    // } else {\r
+    // c.removeListener((ExpandListener) TreeTables.this);\r
+    // }\r
+    // }\r
+    // };\r
+    //\r
+    // private Command<TreeTable, Boolean> collapseListenerCommand = new\r
+    // Command<TreeTable, Boolean>() {\r
+    // public void execute(TreeTable c, Boolean value, Object data) {\r
+    // if (value) {\r
+    // c.addListener((CollapseListener) TreeTables.this);\r
+    // } else {\r
+    // c.removeListener((CollapseListener) TreeTables.this);\r
+    // }\r
+    // }\r
+    // };\r
+\r
+    public void nodeCollapse(CollapseEvent event) {\r
+        log(event.getClass().getSimpleName() + ": " + event.getItemId());\r
+    }\r
+\r
+    public void nodeExpand(ExpandEvent event) {\r
+        log(event.getClass().getSimpleName() + ": " + event.getItemId());\r
+    }\r
+}\r