summaryrefslogtreecommitdiffstats
path: root/src/com
diff options
context:
space:
mode:
authorJonatan Kronqvist <jonatan.kronqvist@itmill.com>2011-06-16 07:55:38 +0000
committerJonatan Kronqvist <jonatan.kronqvist@itmill.com>2011-06-16 07:55:38 +0000
commitd9ca864ff7acf2eedb4edda46276fdb98ad35a7a (patch)
treeab810b3b0fb817d7a7a185064bf6b5d101710c64 /src/com
parent5271adfa5c18a458dc69fc444fea454ad3542b80 (diff)
downloadvaadin-framework-d9ca864ff7acf2eedb4edda46276fdb98ad35a7a.tar.gz
vaadin-framework-d9ca864ff7acf2eedb4edda46276fdb98ad35a7a.zip
Merged TreeTable into Vaadin core (#5371)
svn changeset:19416/svn branch:6.7
Diffstat (limited to 'src/com')
-rw-r--r--src/com/vaadin/terminal/gwt/client/ui/VScrollTable.java4
-rw-r--r--src/com/vaadin/terminal/gwt/client/ui/VTreeTable.java345
-rw-r--r--src/com/vaadin/ui/TreeTable.java579
-rw-r--r--src/com/vaadin/ui/treetable/Collapsible.java66
-rw-r--r--src/com/vaadin/ui/treetable/HierarchicalContainerOrderedWrapper.java58
5 files changed, 1050 insertions, 2 deletions
diff --git a/src/com/vaadin/terminal/gwt/client/ui/VScrollTable.java b/src/com/vaadin/terminal/gwt/client/ui/VScrollTable.java
index d36c2137d8..fdf61b8292 100644
--- a/src/com/vaadin/terminal/gwt/client/ui/VScrollTable.java
+++ b/src/com/vaadin/terminal/gwt/client/ui/VScrollTable.java
@@ -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
index 0000000000..974e96e8ae
--- /dev/null
+++ b/src/com/vaadin/terminal/gwt/client/ui/VTreeTable.java
@@ -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
index 0000000000..20fc8e44d7
--- /dev/null
+++ b/src/com/vaadin/ui/TreeTable.java
@@ -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
index 0000000000..649687716c
--- /dev/null
+++ b/src/com/vaadin/ui/treetable/Collapsible.java
@@ -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
index 0000000000..0c00027f26
--- /dev/null
+++ b/src/com/vaadin/ui/treetable/HierarchicalContainerOrderedWrapper.java
@@ -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);
+ }
+
+}