summaryrefslogtreecommitdiffstats
path: root/src/com/vaadin/ui
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/vaadin/ui
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/vaadin/ui')
-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
3 files changed, 703 insertions, 0 deletions
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);
+ }
+
+}