diff options
Diffstat (limited to 'server/src/com/vaadin/ui/TreeTable.java')
-rw-r--r-- | server/src/com/vaadin/ui/TreeTable.java | 836 |
1 files changed, 836 insertions, 0 deletions
diff --git a/server/src/com/vaadin/ui/TreeTable.java b/server/src/com/vaadin/ui/TreeTable.java new file mode 100644 index 0000000000..7548a4840b --- /dev/null +++ b/server/src/com/vaadin/ui/TreeTable.java @@ -0,0 +1,836 @@ +/* + * Copyright 2011 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +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 java.util.logging.Logger; + +import com.vaadin.data.Collapsible; +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.data.util.HierarchicalContainerOrderedWrapper; +import com.vaadin.shared.ui.treetable.TreeTableConstants; +import com.vaadin.terminal.PaintException; +import com.vaadin.terminal.PaintTarget; +import com.vaadin.terminal.Resource; +import com.vaadin.ui.Tree.CollapseEvent; +import com.vaadin.ui.Tree.CollapseListener; +import com.vaadin.ui.Tree.ExpandEvent; +import com.vaadin.ui.Tree.ExpandListener; + +/** + * 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" }) +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. + */ + + @Override + public int getDepth(Object itemId) { + int depth = 0; + Hierarchical hierarchicalContainer = getContainerDataSource(); + while (!hierarchicalContainer.isRoot(itemId)) { + depth++; + itemId = hierarchicalContainer.getParent(itemId); + } + return depth; + } + + @Override + 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(); + } + + @Override + public void toggleChildVisibility(Object itemId) { + c().setCollapsed(itemId, !c().isCollapsed(itemId)); + } + + @Override + public boolean isNodeOpen(Object itemId) { + return !c().isCollapsed(itemId); + } + + @Override + public int size() { + return TreeTable.super.size(); + } + + @Override + public Object getIdByIndex(int index) { + return TreeTable.super.getIdByIndex(index); + } + + @Override + public int indexOfId(Object id) { + return TreeTable.super.indexOfId(id); + } + + @Override + public boolean isLastId(Object itemId) { + // using the default impl + return TreeTable.super.isLastId(itemId); + } + + @Override + public Object lastItemId() { + // using the default impl + return TreeTable.super.lastItemId(); + } + + @Override + public Object nextItemId(Object itemId) { + return TreeTable.super.nextItemId(itemId); + } + + @Override + public Object prevItemId(Object itemId) { + return TreeTable.super.prevItemId(itemId); + } + + @Override + 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>(); + + @Override + public boolean isNodeOpen(Object itemId) { + return openItems.contains(itemId); + } + + @Override + public int size() { + return getPreOrder().size(); + } + + @Override + public Collection<Object> getItemIds() { + return Collections.unmodifiableCollection(getPreOrder()); + } + + @Override + public boolean isLastId(Object itemId) { + if (itemId == null) { + return false; + } + + return itemId.equals(lastItemId()); + } + + @Override + public Object lastItemId() { + if (getPreOrder().size() > 0) { + return getPreOrder().get(getPreOrder().size() - 1); + } else { + return null; + } + } + + @Override + 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); + } + } + + @Override + public Object prevItemId(Object itemId) { + int indexOf = getPreOrder().indexOf(itemId); + indexOf--; + if (indexOf < 0) { + return null; + } else { + return getPreOrder().get(indexOf); + } + } + + @Override + public void toggleChildVisibility(Object itemId) { + boolean removed = openItems.remove(itemId); + if (!removed) { + openItems.add(itemId); + getLogger().finest("Item " + itemId + " is now expanded"); + } else { + getLogger().finest("Item " + itemId + " is now collapsed"); + } + 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); + } + } + } + + } + + @Override + public int indexOfId(Object id) { + return getPreOrder().indexOf(id); + } + + @Override + 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; + + /** + * The item id that was expanded or collapsed during this request. Reset at + * the end of paint and only used for determining if a partial or full paint + * should be done. + * + * Can safely be reset to null whenever a change occurs that would prevent a + * partial update from rendering the correct result, e.g. rows added or + * removed during an expand operation. + */ + private Object toggledItemId; + private boolean animationsEnabled; + private boolean clearFocusedRowPending; + + /** + * If the container does not send item set change events, always do a full + * repaint instead of a partial update when expanding/collapsing nodes. + */ + private boolean containerSupportsPartialUpdates; + + 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); + toggledItemId = itemId; + toggleChildVisibility(itemId, false); + 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); + } + // Select the row if it is selectable. + if (isSelectable()) { + if (isMultiSelect()) { + setValue(Collections.singleton(itemId)); + } else { + setValue(itemId); + } + } + setFocusedRow(itemId); + } + + private void setFocusedRow(Object itemId) { + focusedRowId = itemId; + if (focusedRowId == null) { + // Must still inform the client that the focusParent request has + // been processed + clearFocusedRowPending = true; + } + requestRepaint(); + } + + @Override + public void paintContent(PaintTarget target) throws PaintException { + if (focusedRowId != null) { + target.addAttribute("focusedRow", itemIdMapper.key(focusedRowId)); + focusedRowId = null; + } else if (clearFocusedRowPending) { + // Must still inform the client that the focusParent request has + // been processed + target.addAttribute("clearFocusPending", true); + clearFocusedRowPending = false; + } + target.addAttribute("animate", animationsEnabled); + if (hierarchyColumnId != null) { + Object[] visibleColumns2 = getVisibleColumns(); + for (int i = 0; i < visibleColumns2.length; i++) { + Object object = visibleColumns2[i]; + if (hierarchyColumnId.equals(object)) { + target.addAttribute( + TreeTableConstants.ATTRIBUTE_HIERARCHY_COLUMN_INDEX, + i); + break; + } + } + } + super.paintContent(target); + toggledItemId = null; + } + + /* + * Override methods for partial row updates and additions when expanding / + * collapsing nodes. + */ + + @Override + protected boolean isPartialRowUpdate() { + return toggledItemId != null && containerSupportsPartialUpdates + && !isRowCacheInvalidated(); + } + + @Override + protected int getFirstAddedItemIndex() { + return indexOfId(toggledItemId) + 1; + } + + @Override + protected int getAddedRowCount() { + return countSubNodesRecursively(getContainerDataSource(), toggledItemId); + } + + private int countSubNodesRecursively(Hierarchical hc, Object itemId) { + int count = 0; + // we need the number of children for toggledItemId no matter if its + // collapsed or expanded. Other items' children are only counted if the + // item is expanded. + if (getContainerStrategy().isNodeOpen(itemId) + || itemId == toggledItemId) { + Collection<?> children = hc.getChildren(itemId); + if (children != null) { + count += children != null ? children.size() : 0; + for (Object id : children) { + count += countSubNodesRecursively(hc, id); + } + } + } + return count; + } + + @Override + protected int getFirstUpdatedItemIndex() { + return indexOfId(toggledItemId); + } + + @Override + protected int getUpdatedRowCount() { + return 1; + } + + @Override + protected boolean shouldHideAddedRows() { + return !getContainerStrategy().isNodeOpen(toggledItemId); + } + + private void toggleChildVisibility(Object itemId, boolean forceFullRefresh) { + getContainerStrategy().toggleChildVisibility(itemId); + // ensure that page still has first item in page, DON'T clear the + // caches. + setCurrentPageFirstItemIndex(getCurrentPageFirstItemIndex(), false); + + if (isCollapsed(itemId)) { + fireCollapseEvent(itemId); + } else { + fireExpandEvent(itemId); + } + + if (containerSupportsPartialUpdates && !forceFullRefresh) { + requestRepaint(); + } else { + // For containers that do not send item set change events, always do + // full repaint instead of partial row update. + refreshRowCache(); + } + } + + @Override + public int size() { + return getContainerStrategy().size(); + } + + @Override + public Hierarchical getContainerDataSource() { + return (Hierarchical) super.getContainerDataSource(); + } + + @Override + public void setContainerDataSource(Container newDataSource) { + cStrategy = null; + + // FIXME: This disables partial updates until TreeTable is fixed so it + // does not change component hierarchy during paint + containerSupportsPartialUpdates = (newDataSource instanceof ItemSetChangeNotifier) && false; + + 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) { + // Can't do partial repaints if items are added or removed during the + // expand/collapse request + toggledItemId = null; + 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(); + } + + @Override + public boolean areChildrenAllowed(Object itemId) { + return getContainerDataSource().areChildrenAllowed(itemId); + } + + @Override + public Collection<?> getChildren(Object itemId) { + return getContainerDataSource().getChildren(itemId); + } + + @Override + public Object getParent(Object itemId) { + return getContainerDataSource().getParent(itemId); + } + + @Override + public boolean hasChildren(Object itemId) { + return getContainerDataSource().hasChildren(itemId); + } + + @Override + public boolean isRoot(Object itemId) { + return getContainerDataSource().isRoot(itemId); + } + + @Override + public Collection<?> rootItemIds() { + return getContainerDataSource().rootItemIds(); + } + + @Override + public boolean setChildrenAllowed(Object itemId, boolean areChildrenAllowed) + throws UnsupportedOperationException { + return getContainerDataSource().setChildrenAllowed(itemId, + areChildrenAllowed); + } + + @Override + public boolean setParent(Object itemId, Object newParentId) + throws UnsupportedOperationException { + return getContainerDataSource().setParent(itemId, newParentId); + } + + /** + * Sets the Item specified by given identifier as collapsed or expanded. If + * the Item is collapsed, its children are not displayed to 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) { + if (null == toggledItemId && !isRowCacheInvalidated() + && getVisibleItemIds().contains(itemId)) { + // optimization: partial refresh if only one item is + // collapsed/expanded + toggledItemId = itemId; + toggleChildVisibility(itemId, false); + } else { + // make sure a full refresh takes place - otherwise neither + // partial nor full repaint of table content is performed + toggledItemId = null; + toggleChildVisibility(itemId, true); + } + } + } + + /** + * 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; + } + + /** + * Adds an expand listener. + * + * @param listener + * the Listener to be added. + */ + public void addListener(ExpandListener listener) { + addListener(ExpandEvent.class, listener, ExpandListener.EXPAND_METHOD); + } + + /** + * Removes an expand listener. + * + * @param listener + * the Listener to be removed. + */ + public void removeListener(ExpandListener listener) { + removeListener(ExpandEvent.class, listener, + ExpandListener.EXPAND_METHOD); + } + + /** + * Emits an expand event. + * + * @param itemId + * the item id. + */ + protected void fireExpandEvent(Object itemId) { + fireEvent(new ExpandEvent(this, itemId)); + } + + /** + * Adds a collapse listener. + * + * @param listener + * the Listener to be added. + */ + public void addListener(CollapseListener listener) { + addListener(CollapseEvent.class, listener, + CollapseListener.COLLAPSE_METHOD); + } + + /** + * Removes a collapse listener. + * + * @param listener + * the Listener to be removed. + */ + public void removeListener(CollapseListener listener) { + removeListener(CollapseEvent.class, listener, + CollapseListener.COLLAPSE_METHOD); + } + + /** + * Emits a collapse event. + * + * @param itemId + * the item id. + */ + protected void fireCollapseEvent(Object itemId) { + fireEvent(new CollapseEvent(this, itemId)); + } + + /** + * @return true if animations are enabled + */ + public boolean isAnimationsEnabled() { + return animationsEnabled; + } + + /** + * Animations can be enabled by passing true to this method. Currently + * expanding rows slide in from the top and collapsing rows slide out the + * same way. NOTE! not supported in Internet Explorer 6 or 7. + * + * @param animationsEnabled + * true or false whether to enable animations or not. + */ + public void setAnimationsEnabled(boolean animationsEnabled) { + this.animationsEnabled = animationsEnabled; + requestRepaint(); + } + + private static final Logger getLogger() { + return Logger.getLogger(TreeTable.class.getName()); + } + +} |