diff options
Diffstat (limited to 'server/src/com/vaadin/ui/Tree.java')
-rw-r--r-- | server/src/com/vaadin/ui/Tree.java | 1615 |
1 files changed, 1615 insertions, 0 deletions
diff --git a/server/src/com/vaadin/ui/Tree.java b/server/src/com/vaadin/ui/Tree.java new file mode 100644 index 0000000000..c15975d879 --- /dev/null +++ b/server/src/com/vaadin/ui/Tree.java @@ -0,0 +1,1615 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.ui; + +import java.io.Serializable; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.LinkedList; +import java.util.Map; +import java.util.Set; +import java.util.Stack; +import java.util.StringTokenizer; + +import com.vaadin.data.Container; +import com.vaadin.data.Item; +import com.vaadin.data.util.ContainerHierarchicalWrapper; +import com.vaadin.data.util.IndexedContainer; +import com.vaadin.event.Action; +import com.vaadin.event.Action.Handler; +import com.vaadin.event.DataBoundTransferable; +import com.vaadin.event.ItemClickEvent; +import com.vaadin.event.ItemClickEvent.ItemClickListener; +import com.vaadin.event.ItemClickEvent.ItemClickNotifier; +import com.vaadin.event.Transferable; +import com.vaadin.event.dd.DragAndDropEvent; +import com.vaadin.event.dd.DragSource; +import com.vaadin.event.dd.DropHandler; +import com.vaadin.event.dd.DropTarget; +import com.vaadin.event.dd.TargetDetails; +import com.vaadin.event.dd.acceptcriteria.ClientSideCriterion; +import com.vaadin.event.dd.acceptcriteria.ServerSideCriterion; +import com.vaadin.event.dd.acceptcriteria.TargetDetailIs; +import com.vaadin.shared.MouseEventDetails; +import com.vaadin.shared.ui.dd.VerticalDropLocation; +import com.vaadin.terminal.KeyMapper; +import com.vaadin.terminal.PaintException; +import com.vaadin.terminal.PaintTarget; +import com.vaadin.terminal.Resource; +import com.vaadin.terminal.gwt.client.ui.tree.TreeConnector; +import com.vaadin.terminal.gwt.client.ui.tree.VTree; +import com.vaadin.tools.ReflectTools; + +/** + * Tree component. A Tree can be used to select an item (or multiple items) from + * a hierarchical set of items. + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ +@SuppressWarnings({ "serial", "deprecation" }) +public class Tree extends AbstractSelect implements Container.Hierarchical, + Action.Container, ItemClickNotifier, DragSource, DropTarget { + + /* Private members */ + + /** + * Set of expanded nodes. + */ + private final HashSet<Object> expanded = new HashSet<Object>(); + + /** + * List of action handlers. + */ + private LinkedList<Action.Handler> actionHandlers = null; + + /** + * Action mapper. + */ + private KeyMapper<Action> actionMapper = null; + + /** + * Is the tree selectable on the client side. + */ + private boolean selectable = true; + + /** + * Flag to indicate sub-tree loading + */ + private boolean partialUpdate = false; + + /** + * Holds a itemId which was recently expanded + */ + private Object expandedItemId; + + /** + * a flag which indicates initial paint. After this flag set true partial + * updates are allowed. + */ + private boolean initialPaint = true; + + /** + * Item tooltip generator + */ + private ItemDescriptionGenerator itemDescriptionGenerator; + + /** + * Supported drag modes for Tree. + */ + public enum TreeDragMode { + /** + * When drag mode is NONE, dragging from Tree is not supported. Browsers + * may still support selecting text/icons from Tree which can initiate + * HTML 5 style drag and drop operation. + */ + NONE, + /** + * When drag mode is NODE, users can initiate drag from Tree nodes that + * represent {@link Item}s in from the backed {@link Container}. + */ + NODE + // , SUBTREE + } + + private TreeDragMode dragMode = TreeDragMode.NONE; + + private MultiSelectMode multiSelectMode = MultiSelectMode.DEFAULT; + + /* Tree constructors */ + + /** + * Creates a new empty tree. + */ + public Tree() { + } + + /** + * Creates a new empty tree with caption. + * + * @param caption + */ + public Tree(String caption) { + setCaption(caption); + } + + /** + * Creates a new tree with caption and connect it to a Container. + * + * @param caption + * @param dataSource + */ + public Tree(String caption, Container dataSource) { + setCaption(caption); + setContainerDataSource(dataSource); + } + + /* Expanding and collapsing */ + + /** + * Check is an item is expanded + * + * @param itemId + * the item id. + * @return true iff the item is expanded. + */ + public boolean isExpanded(Object itemId) { + return expanded.contains(itemId); + } + + /** + * Expands an item. + * + * @param itemId + * the item id. + * @return True iff the expand operation succeeded + */ + public boolean expandItem(Object itemId) { + boolean success = expandItem(itemId, true); + requestRepaint(); + return success; + } + + /** + * Expands an item. + * + * @param itemId + * the item id. + * @param sendChildTree + * flag to indicate if client needs subtree or not (may be + * cached) + * @return True iff the expand operation succeeded + */ + private boolean expandItem(Object itemId, boolean sendChildTree) { + + // Succeeds if the node is already expanded + if (isExpanded(itemId)) { + return true; + } + + // Nodes that can not have children are not expandable + if (!areChildrenAllowed(itemId)) { + return false; + } + + // Expands + expanded.add(itemId); + + expandedItemId = itemId; + if (initialPaint) { + requestRepaint(); + } else if (sendChildTree) { + requestPartialRepaint(); + } + fireExpandEvent(itemId); + + return true; + } + + @Override + public void requestRepaint() { + super.requestRepaint(); + partialUpdate = false; + } + + private void requestPartialRepaint() { + super.requestRepaint(); + partialUpdate = true; + } + + /** + * Expands the items recursively + * + * Expands all the children recursively starting from an item. Operation + * succeeds only if all expandable items are expanded. + * + * @param startItemId + * @return True iff the expand operation succeeded + */ + public boolean expandItemsRecursively(Object startItemId) { + + boolean result = true; + + // Initial stack + final Stack<Object> todo = new Stack<Object>(); + todo.add(startItemId); + + // Expands recursively + while (!todo.isEmpty()) { + final Object id = todo.pop(); + if (areChildrenAllowed(id) && !expandItem(id, false)) { + result = false; + } + if (hasChildren(id)) { + todo.addAll(getChildren(id)); + } + } + requestRepaint(); + return result; + } + + /** + * Collapses an item. + * + * @param itemId + * the item id. + * @return True iff the collapse operation succeeded + */ + public boolean collapseItem(Object itemId) { + + // Succeeds if the node is already collapsed + if (!isExpanded(itemId)) { + return true; + } + + // Collapse + expanded.remove(itemId); + requestRepaint(); + fireCollapseEvent(itemId); + + return true; + } + + /** + * Collapses the items recursively. + * + * Collapse all the children recursively starting from an item. Operation + * succeeds only if all expandable items are collapsed. + * + * @param startItemId + * @return True iff the collapse operation succeeded + */ + public boolean collapseItemsRecursively(Object startItemId) { + + boolean result = true; + + // Initial stack + final Stack<Object> todo = new Stack<Object>(); + todo.add(startItemId); + + // Collapse recursively + while (!todo.isEmpty()) { + final Object id = todo.pop(); + if (areChildrenAllowed(id) && !collapseItem(id)) { + result = false; + } + if (hasChildren(id)) { + todo.addAll(getChildren(id)); + } + } + + return result; + } + + /** + * Returns the current selectable state. Selectable determines if the a node + * can be selected on the client side. Selectable does not affect + * {@link #setValue(Object)} or {@link #select(Object)}. + * + * <p> + * The tree is selectable by default. + * </p> + * + * @return the current selectable state. + */ + public boolean isSelectable() { + return selectable; + } + + /** + * Sets the selectable state. Selectable determines if the a node can be + * selected on the client side. Selectable does not affect + * {@link #setValue(Object)} or {@link #select(Object)}. + * + * <p> + * The tree is selectable by default. + * </p> + * + * @param selectable + * The new selectable state. + */ + public void setSelectable(boolean selectable) { + if (this.selectable != selectable) { + this.selectable = selectable; + requestRepaint(); + } + } + + /** + * Sets the behavior of the multiselect mode + * + * @param mode + * The mode to set + */ + public void setMultiselectMode(MultiSelectMode mode) { + if (multiSelectMode != mode && mode != null) { + multiSelectMode = mode; + requestRepaint(); + } + } + + /** + * Returns the mode the multiselect is in. The mode controls how + * multiselection can be done. + * + * @return The mode + */ + public MultiSelectMode getMultiselectMode() { + return multiSelectMode; + } + + /* Component API */ + + /* + * (non-Javadoc) + * + * @see com.vaadin.ui.AbstractSelect#changeVariables(java.lang.Object, + * java.util.Map) + */ + @Override + public void changeVariables(Object source, Map<String, Object> variables) { + + if (variables.containsKey("clickedKey")) { + String key = (String) variables.get("clickedKey"); + + Object id = itemIdMapper.get(key); + MouseEventDetails details = MouseEventDetails + .deSerialize((String) variables.get("clickEvent")); + Item item = getItem(id); + if (item != null) { + fireEvent(new ItemClickEvent(this, item, id, null, details)); + } + } + + if (!isSelectable() && variables.containsKey("selected")) { + // Not-selectable is a special case, AbstractSelect does not support + // TODO could be optimized. + variables = new HashMap<String, Object>(variables); + variables.remove("selected"); + } + + // Collapses the nodes + if (variables.containsKey("collapse")) { + final String[] keys = (String[]) variables.get("collapse"); + for (int i = 0; i < keys.length; i++) { + final Object id = itemIdMapper.get(keys[i]); + if (id != null && isExpanded(id)) { + expanded.remove(id); + fireCollapseEvent(id); + } + } + } + + // Expands the nodes + if (variables.containsKey("expand")) { + boolean sendChildTree = false; + if (variables.containsKey("requestChildTree")) { + sendChildTree = true; + } + final String[] keys = (String[]) variables.get("expand"); + for (int i = 0; i < keys.length; i++) { + final Object id = itemIdMapper.get(keys[i]); + if (id != null) { + expandItem(id, sendChildTree); + } + } + } + + // AbstractSelect cannot handle multiselection so we handle + // it ourself + if (variables.containsKey("selected") && isMultiSelect() + && multiSelectMode == MultiSelectMode.DEFAULT) { + handleSelectedItems(variables); + variables = new HashMap<String, Object>(variables); + variables.remove("selected"); + } + + // Selections are handled by the select component + super.changeVariables(source, variables); + + // Actions + if (variables.containsKey("action")) { + final StringTokenizer st = new StringTokenizer( + (String) variables.get("action"), ","); + if (st.countTokens() == 2) { + final Object itemId = itemIdMapper.get(st.nextToken()); + final Action action = actionMapper.get(st.nextToken()); + if (action != null && (itemId == null || containsId(itemId)) + && actionHandlers != null) { + for (Handler ah : actionHandlers) { + ah.handleAction(action, this, itemId); + } + } + } + } + } + + /** + * Handles the selection + * + * @param variables + * The variables sent to the server from the client + */ + private void handleSelectedItems(Map<String, Object> variables) { + final String[] ka = (String[]) variables.get("selected"); + + // Converts the key-array to id-set + final LinkedList<Object> s = new LinkedList<Object>(); + for (int i = 0; i < ka.length; i++) { + final Object id = itemIdMapper.get(ka[i]); + if (!isNullSelectionAllowed() + && (id == null || id == getNullSelectionItemId())) { + // skip empty selection if nullselection is not allowed + requestRepaint(); + } else if (id != null && containsId(id)) { + s.add(id); + } + } + + if (!isNullSelectionAllowed() && s.size() < 1) { + // empty selection not allowed, keep old value + requestRepaint(); + return; + } + + setValue(s, true); + } + + /** + * Paints any needed component-specific things to the given UIDL stream. + * + * @see com.vaadin.ui.AbstractComponent#paintContent(PaintTarget) + */ + @Override + public void paintContent(PaintTarget target) throws PaintException { + initialPaint = false; + + if (partialUpdate) { + target.addAttribute("partialUpdate", true); + target.addAttribute("rootKey", itemIdMapper.key(expandedItemId)); + } else { + getCaptionChangeListener().clear(); + + // The tab ordering number + if (getTabIndex() > 0) { + target.addAttribute("tabindex", getTabIndex()); + } + + // Paint tree attributes + if (isSelectable()) { + target.addAttribute("selectmode", (isMultiSelect() ? "multi" + : "single")); + if (isMultiSelect()) { + target.addAttribute("multiselectmode", + multiSelectMode.ordinal()); + } + } else { + target.addAttribute("selectmode", "none"); + } + if (isNewItemsAllowed()) { + target.addAttribute("allownewitem", true); + } + + if (isNullSelectionAllowed()) { + target.addAttribute("nullselect", true); + } + + if (dragMode != TreeDragMode.NONE) { + target.addAttribute("dragMode", dragMode.ordinal()); + } + + } + + // Initialize variables + final Set<Action> actionSet = new LinkedHashSet<Action>(); + + // rendered selectedKeys + LinkedList<String> selectedKeys = new LinkedList<String>(); + + final LinkedList<String> expandedKeys = new LinkedList<String>(); + + // Iterates through hierarchical tree using a stack of iterators + final Stack<Iterator<?>> iteratorStack = new Stack<Iterator<?>>(); + Collection<?> ids; + if (partialUpdate) { + ids = getChildren(expandedItemId); + } else { + ids = rootItemIds(); + } + + if (ids != null) { + iteratorStack.push(ids.iterator()); + } + + /* + * Body actions - Actions which has the target null and can be invoked + * by right clicking on the Tree body + */ + if (actionHandlers != null) { + final ArrayList<String> keys = new ArrayList<String>(); + for (Handler ah : actionHandlers) { + + // Getting action for the null item, which in this case + // means the body item + final Action[] aa = ah.getActions(null, this); + if (aa != null) { + for (int ai = 0; ai < aa.length; ai++) { + final String akey = actionMapper.key(aa[ai]); + actionSet.add(aa[ai]); + keys.add(akey); + } + } + } + target.addAttribute("alb", keys.toArray()); + } + + while (!iteratorStack.isEmpty()) { + + // Gets the iterator for current tree level + final Iterator<?> i = iteratorStack.peek(); + + // If the level is finished, back to previous tree level + if (!i.hasNext()) { + + // Removes used iterator from the stack + iteratorStack.pop(); + + // Closes node + if (!iteratorStack.isEmpty()) { + target.endTag("node"); + } + } + + // Adds the item on current level + else { + final Object itemId = i.next(); + + // Starts the item / node + final boolean isNode = areChildrenAllowed(itemId); + if (isNode) { + target.startTag("node"); + } else { + target.startTag("leaf"); + } + + if (itemStyleGenerator != null) { + String stylename = itemStyleGenerator.getStyle(itemId); + if (stylename != null) { + target.addAttribute(TreeConnector.ATTRIBUTE_NODE_STYLE, + stylename); + } + } + + if (itemDescriptionGenerator != null) { + String description = itemDescriptionGenerator + .generateDescription(this, itemId, null); + if (description != null && !description.equals("")) { + target.addAttribute("descr", description); + } + } + + // Adds the attributes + target.addAttribute(TreeConnector.ATTRIBUTE_NODE_CAPTION, + getItemCaption(itemId)); + final Resource icon = getItemIcon(itemId); + if (icon != null) { + target.addAttribute(TreeConnector.ATTRIBUTE_NODE_ICON, + getItemIcon(itemId)); + } + final String key = itemIdMapper.key(itemId); + target.addAttribute("key", key); + if (isSelected(itemId)) { + target.addAttribute("selected", true); + selectedKeys.add(key); + } + if (areChildrenAllowed(itemId) && isExpanded(itemId)) { + target.addAttribute("expanded", true); + expandedKeys.add(key); + } + + // Add caption change listener + getCaptionChangeListener().addNotifierForItem(itemId); + + // Actions + if (actionHandlers != null) { + final ArrayList<String> keys = new ArrayList<String>(); + final Iterator<Action.Handler> ahi = actionHandlers + .iterator(); + while (ahi.hasNext()) { + final Action[] aa = ahi.next().getActions(itemId, this); + if (aa != null) { + for (int ai = 0; ai < aa.length; ai++) { + final String akey = actionMapper.key(aa[ai]); + actionSet.add(aa[ai]); + keys.add(akey); + } + } + } + target.addAttribute("al", keys.toArray()); + } + + // Adds the children if expanded, or close the tag + if (isExpanded(itemId) && hasChildren(itemId) + && areChildrenAllowed(itemId)) { + iteratorStack.push(getChildren(itemId).iterator()); + } else { + if (isNode) { + target.endTag("node"); + } else { + target.endTag("leaf"); + } + } + } + } + + // Actions + if (!actionSet.isEmpty()) { + target.addVariable(this, "action", ""); + target.startTag("actions"); + final Iterator<Action> i = actionSet.iterator(); + while (i.hasNext()) { + final Action a = i.next(); + target.startTag("action"); + if (a.getCaption() != null) { + target.addAttribute(TreeConnector.ATTRIBUTE_ACTION_CAPTION, + a.getCaption()); + } + if (a.getIcon() != null) { + target.addAttribute(TreeConnector.ATTRIBUTE_ACTION_ICON, + a.getIcon()); + } + target.addAttribute("key", actionMapper.key(a)); + target.endTag("action"); + } + target.endTag("actions"); + } + + if (partialUpdate) { + partialUpdate = false; + } else { + // Selected + target.addVariable(this, "selected", + selectedKeys.toArray(new String[selectedKeys.size()])); + + // Expand and collapse + target.addVariable(this, "expand", new String[] {}); + target.addVariable(this, "collapse", new String[] {}); + + // New items + target.addVariable(this, "newitem", new String[] {}); + + if (dropHandler != null) { + dropHandler.getAcceptCriterion().paint(target); + } + + } + } + + /* Container.Hierarchical API */ + + /** + * Tests if the Item with given ID can have any children. + * + * @see com.vaadin.data.Container.Hierarchical#areChildrenAllowed(Object) + */ + @Override + public boolean areChildrenAllowed(Object itemId) { + return ((Container.Hierarchical) items).areChildrenAllowed(itemId); + } + + /** + * Gets the IDs of all Items that are children of the specified Item. + * + * @see com.vaadin.data.Container.Hierarchical#getChildren(Object) + */ + @Override + public Collection<?> getChildren(Object itemId) { + return ((Container.Hierarchical) items).getChildren(itemId); + } + + /** + * Gets the ID of the parent Item of the specified Item. + * + * @see com.vaadin.data.Container.Hierarchical#getParent(Object) + */ + @Override + public Object getParent(Object itemId) { + return ((Container.Hierarchical) items).getParent(itemId); + } + + /** + * Tests if the Item specified with <code>itemId</code> has child Items. + * + * @see com.vaadin.data.Container.Hierarchical#hasChildren(Object) + */ + @Override + public boolean hasChildren(Object itemId) { + return ((Container.Hierarchical) items).hasChildren(itemId); + } + + /** + * Tests if the Item specified with <code>itemId</code> is a root Item. + * + * @see com.vaadin.data.Container.Hierarchical#isRoot(Object) + */ + @Override + public boolean isRoot(Object itemId) { + return ((Container.Hierarchical) items).isRoot(itemId); + } + + /** + * Gets the IDs of all Items in the container that don't have a parent. + * + * @see com.vaadin.data.Container.Hierarchical#rootItemIds() + */ + @Override + public Collection<?> rootItemIds() { + return ((Container.Hierarchical) items).rootItemIds(); + } + + /** + * Sets the given Item's capability to have children. + * + * @see com.vaadin.data.Container.Hierarchical#setChildrenAllowed(Object, + * boolean) + */ + @Override + public boolean setChildrenAllowed(Object itemId, boolean areChildrenAllowed) { + final boolean success = ((Container.Hierarchical) items) + .setChildrenAllowed(itemId, areChildrenAllowed); + if (success) { + requestRepaint(); + } + return success; + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.data.Container.Hierarchical#setParent(java.lang.Object , + * java.lang.Object) + */ + @Override + public boolean setParent(Object itemId, Object newParentId) { + final boolean success = ((Container.Hierarchical) items).setParent( + itemId, newParentId); + if (success) { + requestRepaint(); + } + return success; + } + + /* Overriding select behavior */ + + /** + * Sets the Container that serves as the data source of the viewer. + * + * @see com.vaadin.data.Container.Viewer#setContainerDataSource(Container) + */ + @Override + public void setContainerDataSource(Container newDataSource) { + if (newDataSource == null) { + // Note: using wrapped IndexedContainer to match constructor (super + // creates an IndexedContainer, which is then wrapped). + newDataSource = new ContainerHierarchicalWrapper( + new IndexedContainer()); + } + + // Assure that the data source is ordered by making unordered + // containers ordered by wrapping them + if (Container.Hierarchical.class.isAssignableFrom(newDataSource + .getClass())) { + super.setContainerDataSource(newDataSource); + } else { + super.setContainerDataSource(new ContainerHierarchicalWrapper( + newDataSource)); + } + } + + /* Expand event and listener */ + + /** + * Event to fired when a node is expanded. ExapandEvent is fired when a node + * is to be expanded. it can me used to dynamically fill the sub-nodes of + * the node. + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ + public static class ExpandEvent extends Component.Event { + + private final Object expandedItemId; + + /** + * New instance of options change event + * + * @param source + * the Source of the event. + * @param expandedItemId + */ + public ExpandEvent(Component source, Object expandedItemId) { + super(source); + this.expandedItemId = expandedItemId; + } + + /** + * Node where the event occurred. + * + * @return the Source of the event. + */ + public Object getItemId() { + return expandedItemId; + } + } + + /** + * Expand event listener. + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ + public interface ExpandListener extends Serializable { + + public static final Method EXPAND_METHOD = ReflectTools.findMethod( + ExpandListener.class, "nodeExpand", ExpandEvent.class); + + /** + * A node has been expanded. + * + * @param event + * the Expand event. + */ + public void nodeExpand(ExpandEvent event); + } + + /** + * Adds the expand listener. + * + * @param listener + * the Listener to be added. + */ + public void addListener(ExpandListener listener) { + addListener(ExpandEvent.class, listener, ExpandListener.EXPAND_METHOD); + } + + /** + * Removes the expand listener. + * + * @param listener + * the Listener to be removed. + */ + public void removeListener(ExpandListener listener) { + removeListener(ExpandEvent.class, listener, + ExpandListener.EXPAND_METHOD); + } + + /** + * Emits the expand event. + * + * @param itemId + * the item id. + */ + protected void fireExpandEvent(Object itemId) { + fireEvent(new ExpandEvent(this, itemId)); + } + + /* Collapse event */ + + /** + * Collapse event + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ + public static class CollapseEvent extends Component.Event { + + private final Object collapsedItemId; + + /** + * New instance of options change event. + * + * @param source + * the Source of the event. + * @param collapsedItemId + */ + public CollapseEvent(Component source, Object collapsedItemId) { + super(source); + this.collapsedItemId = collapsedItemId; + } + + /** + * Gets tge Collapsed Item id. + * + * @return the collapsed item id. + */ + public Object getItemId() { + return collapsedItemId; + } + } + + /** + * Collapse event listener. + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ + public interface CollapseListener extends Serializable { + + public static final Method COLLAPSE_METHOD = ReflectTools.findMethod( + CollapseListener.class, "nodeCollapse", CollapseEvent.class); + + /** + * A node has been collapsed. + * + * @param event + * the Collapse event. + */ + public void nodeCollapse(CollapseEvent event); + } + + /** + * Adds the collapse listener. + * + * @param listener + * the Listener to be added. + */ + public void addListener(CollapseListener listener) { + addListener(CollapseEvent.class, listener, + CollapseListener.COLLAPSE_METHOD); + } + + /** + * Removes the collapse listener. + * + * @param listener + * the Listener to be removed. + */ + public void removeListener(CollapseListener listener) { + removeListener(CollapseEvent.class, listener, + CollapseListener.COLLAPSE_METHOD); + } + + /** + * Emits collapse event. + * + * @param itemId + * the item id. + */ + protected void fireCollapseEvent(Object itemId) { + fireEvent(new CollapseEvent(this, itemId)); + } + + /* Action container */ + + /** + * Adds an action handler. + * + * @see com.vaadin.event.Action.Container#addActionHandler(Action.Handler) + */ + @Override + public void addActionHandler(Action.Handler actionHandler) { + + if (actionHandler != null) { + + if (actionHandlers == null) { + actionHandlers = new LinkedList<Action.Handler>(); + actionMapper = new KeyMapper<Action>(); + } + + if (!actionHandlers.contains(actionHandler)) { + actionHandlers.add(actionHandler); + requestRepaint(); + } + } + } + + /** + * Removes an action handler. + * + * @see com.vaadin.event.Action.Container#removeActionHandler(Action.Handler) + */ + @Override + public void removeActionHandler(Action.Handler actionHandler) { + + if (actionHandlers != null && actionHandlers.contains(actionHandler)) { + + actionHandlers.remove(actionHandler); + + if (actionHandlers.isEmpty()) { + actionHandlers = null; + actionMapper = null; + } + + requestRepaint(); + } + } + + /** + * Removes all action handlers + */ + public void removeAllActionHandlers() { + actionHandlers = null; + actionMapper = null; + requestRepaint(); + } + + /** + * Gets the visible item ids. + * + * @see com.vaadin.ui.Select#getVisibleItemIds() + */ + @Override + public Collection<?> getVisibleItemIds() { + + final LinkedList<Object> visible = new LinkedList<Object>(); + + // Iterates trough hierarchical tree using a stack of iterators + final Stack<Iterator<?>> iteratorStack = new Stack<Iterator<?>>(); + final Collection<?> ids = rootItemIds(); + if (ids != null) { + iteratorStack.push(ids.iterator()); + } + while (!iteratorStack.isEmpty()) { + + // Gets the iterator for current tree level + final Iterator<?> i = iteratorStack.peek(); + + // If the level is finished, back to previous tree level + if (!i.hasNext()) { + + // Removes used iterator from the stack + iteratorStack.pop(); + } + + // Adds the item on current level + else { + final Object itemId = i.next(); + + visible.add(itemId); + + // Adds children if expanded, or close the tag + if (isExpanded(itemId) && hasChildren(itemId)) { + iteratorStack.push(getChildren(itemId).iterator()); + } + } + } + + return visible; + } + + /** + * Tree does not support <code>setNullSelectionItemId</code>. + * + * @see com.vaadin.ui.AbstractSelect#setNullSelectionItemId(java.lang.Object) + */ + @Override + public void setNullSelectionItemId(Object nullSelectionItemId) + throws UnsupportedOperationException { + if (nullSelectionItemId != null) { + throw new UnsupportedOperationException(); + } + + } + + /** + * Adding new items is not supported. + * + * @throws UnsupportedOperationException + * if set to true. + * @see com.vaadin.ui.Select#setNewItemsAllowed(boolean) + */ + @Override + public void setNewItemsAllowed(boolean allowNewOptions) + throws UnsupportedOperationException { + if (allowNewOptions) { + throw new UnsupportedOperationException(); + } + } + + /** + * Tree does not support lazy options loading mode. Setting this true will + * throw UnsupportedOperationException. + * + * @see com.vaadin.ui.Select#setLazyLoading(boolean) + */ + public void setLazyLoading(boolean useLazyLoading) { + if (useLazyLoading) { + throw new UnsupportedOperationException( + "Lazy options loading is not supported by Tree."); + } + } + + private ItemStyleGenerator itemStyleGenerator; + + private DropHandler dropHandler; + + @Override + public void addListener(ItemClickListener listener) { + addListener(VTree.ITEM_CLICK_EVENT_ID, ItemClickEvent.class, listener, + ItemClickEvent.ITEM_CLICK_METHOD); + } + + @Override + public void removeListener(ItemClickListener listener) { + removeListener(VTree.ITEM_CLICK_EVENT_ID, ItemClickEvent.class, + listener); + } + + /** + * Sets the {@link ItemStyleGenerator} to be used with this tree. + * + * @param itemStyleGenerator + * item style generator or null to remove generator + */ + public void setItemStyleGenerator(ItemStyleGenerator itemStyleGenerator) { + if (this.itemStyleGenerator != itemStyleGenerator) { + this.itemStyleGenerator = itemStyleGenerator; + requestRepaint(); + } + } + + /** + * @return the current {@link ItemStyleGenerator} for this tree. Null if + * {@link ItemStyleGenerator} is not set. + */ + public ItemStyleGenerator getItemStyleGenerator() { + return itemStyleGenerator; + } + + /** + * ItemStyleGenerator can be used to add custom styles to tree items. The + * CSS class name that will be added to the cell content is + * <tt>v-tree-node-[style name]</tt>. + */ + public interface ItemStyleGenerator extends Serializable { + + /** + * Called by Tree when an item is painted. + * + * @param itemId + * The itemId of the item to be painted + * @return The style name to add to this item. (the CSS class name will + * be v-tree-node-[style name] + */ + public abstract String getStyle(Object itemId); + } + + // Overriden so javadoc comes from Container.Hierarchical + @Override + public boolean removeItem(Object itemId) + throws UnsupportedOperationException { + return super.removeItem(itemId); + } + + @Override + public DropHandler getDropHandler() { + return dropHandler; + } + + public void setDropHandler(DropHandler dropHandler) { + this.dropHandler = dropHandler; + } + + /** + * A {@link TargetDetails} implementation with Tree specific api. + * + * @since 6.3 + */ + public class TreeTargetDetails extends AbstractSelectTargetDetails { + + TreeTargetDetails(Map<String, Object> rawVariables) { + super(rawVariables); + } + + @Override + public Tree getTarget() { + return (Tree) super.getTarget(); + } + + /** + * If the event is on a node that can not have children (see + * {@link Tree#areChildrenAllowed(Object)}), this method returns the + * parent item id of the target item (see {@link #getItemIdOver()} ). + * The identifier of the parent node is also returned if the cursor is + * on the top part of node. Else this method returns the same as + * {@link #getItemIdOver()}. + * <p> + * In other words this method returns the identifier of the "folder" + * into the drag operation is targeted. + * <p> + * If the method returns null, the current target is on a root node or + * on other undefined area over the tree component. + * <p> + * The default Tree implementation marks the targetted tree node with + * CSS classnames v-tree-node-dragfolder and + * v-tree-node-caption-dragfolder (for the caption element). + */ + public Object getItemIdInto() { + + Object itemIdOver = getItemIdOver(); + if (areChildrenAllowed(itemIdOver) + && getDropLocation() == VerticalDropLocation.MIDDLE) { + return itemIdOver; + } + return getParent(itemIdOver); + } + + /** + * If drop is targeted into "folder node" (see {@link #getItemIdInto()} + * ), this method returns the item id of the node after the drag was + * targeted. This method is useful when implementing drop into specific + * location (between specific nodes) in tree. + * + * @return the id of the item after the user targets the drop or null if + * "target" is a first item in node list (or the first in root + * node list) + */ + public Object getItemIdAfter() { + Object itemIdOver = getItemIdOver(); + Object itemIdInto2 = getItemIdInto(); + if (itemIdOver.equals(itemIdInto2)) { + return null; + } + VerticalDropLocation dropLocation = getDropLocation(); + if (VerticalDropLocation.TOP == dropLocation) { + // if on top of the caption area, add before + Collection<?> children; + Object itemIdInto = getItemIdInto(); + if (itemIdInto != null) { + // seek the previous from child list + children = getChildren(itemIdInto); + } else { + children = rootItemIds(); + } + Object ref = null; + for (Object object : children) { + if (object.equals(itemIdOver)) { + return ref; + } + ref = object; + } + } + return itemIdOver; + } + + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.event.dd.DropTarget#translateDropTargetDetails(java.util.Map) + */ + @Override + public TreeTargetDetails translateDropTargetDetails( + Map<String, Object> clientVariables) { + return new TreeTargetDetails(clientVariables); + } + + /** + * Helper API for {@link TreeDropCriterion} + * + * @param itemId + * @return + */ + private String key(Object itemId) { + return itemIdMapper.key(itemId); + } + + /** + * Sets the drag mode that controls how Tree behaves as a {@link DragSource} + * . + * + * @param dragMode + */ + public void setDragMode(TreeDragMode dragMode) { + this.dragMode = dragMode; + requestRepaint(); + } + + /** + * @return the drag mode that controls how Tree behaves as a + * {@link DragSource}. + * + * @see TreeDragMode + */ + public TreeDragMode getDragMode() { + return dragMode; + } + + /** + * Concrete implementation of {@link DataBoundTransferable} for data + * transferred from a tree. + * + * @see {@link DataBoundTransferable}. + * + * @since 6.3 + */ + protected class TreeTransferable extends DataBoundTransferable { + + public TreeTransferable(Component sourceComponent, + Map<String, Object> rawVariables) { + super(sourceComponent, rawVariables); + } + + @Override + public Object getItemId() { + return getData("itemId"); + } + + @Override + public Object getPropertyId() { + return getItemCaptionPropertyId(); + } + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.event.dd.DragSource#getTransferable(java.util.Map) + */ + @Override + public Transferable getTransferable(Map<String, Object> payload) { + TreeTransferable transferable = new TreeTransferable(this, payload); + // updating drag source variables + Object object = payload.get("itemId"); + if (object != null) { + transferable.setData("itemId", itemIdMapper.get((String) object)); + } + + return transferable; + } + + /** + * Lazy loading accept criterion for Tree. Accepted target nodes are loaded + * from server once per drag and drop operation. Developer must override one + * method that decides accepted tree nodes for the whole Tree. + * + * <p> + * Initially pretty much no data is sent to client. On first required + * criterion check (per drag request) the client side data structure is + * initialized from server and no subsequent requests requests are needed + * during that drag and drop operation. + */ + public static abstract class TreeDropCriterion extends ServerSideCriterion { + + private Tree tree; + + private Set<Object> allowedItemIds; + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.event.dd.acceptCriteria.ServerSideCriterion#getIdentifier + * () + */ + @Override + protected String getIdentifier() { + return TreeDropCriterion.class.getCanonicalName(); + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.event.dd.acceptCriteria.AcceptCriterion#accepts(com.vaadin + * .event.dd.DragAndDropEvent) + */ + @Override + public boolean accept(DragAndDropEvent dragEvent) { + AbstractSelectTargetDetails dropTargetData = (AbstractSelectTargetDetails) dragEvent + .getTargetDetails(); + tree = (Tree) dragEvent.getTargetDetails().getTarget(); + allowedItemIds = getAllowedItemIds(dragEvent, tree); + + return allowedItemIds.contains(dropTargetData.getItemIdOver()); + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.event.dd.acceptCriteria.AcceptCriterion#paintResponse( + * com.vaadin.terminal.PaintTarget) + */ + @Override + public void paintResponse(PaintTarget target) throws PaintException { + /* + * send allowed nodes to client so subsequent requests can be + * avoided + */ + Object[] array = allowedItemIds.toArray(); + for (int i = 0; i < array.length; i++) { + String key = tree.key(array[i]); + array[i] = key; + } + target.addAttribute("allowedIds", array); + } + + protected abstract Set<Object> getAllowedItemIds( + DragAndDropEvent dragEvent, Tree tree); + + } + + /** + * A criterion that accepts {@link Transferable} only directly on a tree + * node that can have children. + * <p> + * Class is singleton, use {@link TargetItemAllowsChildren#get()} to get the + * instance. + * + * @see Tree#setChildrenAllowed(Object, boolean) + * + * @since 6.3 + */ + public static class TargetItemAllowsChildren extends TargetDetailIs { + + private static TargetItemAllowsChildren instance = new TargetItemAllowsChildren(); + + public static TargetItemAllowsChildren get() { + return instance; + } + + private TargetItemAllowsChildren() { + super("itemIdOverIsNode", Boolean.TRUE); + } + + /* + * Uses enhanced server side check + */ + @Override + public boolean accept(DragAndDropEvent dragEvent) { + try { + // must be over tree node and in the middle of it (not top or + // bottom + // part) + TreeTargetDetails eventDetails = (TreeTargetDetails) dragEvent + .getTargetDetails(); + + Object itemIdOver = eventDetails.getItemIdOver(); + if (!eventDetails.getTarget().areChildrenAllowed(itemIdOver)) { + return false; + } + // return true if directly over + return eventDetails.getDropLocation() == VerticalDropLocation.MIDDLE; + } catch (Exception e) { + return false; + } + } + + } + + /** + * An accept criterion that checks the parent node (or parent hierarchy) for + * the item identifier given in constructor. If the parent is found, content + * is accepted. Criterion can be used to accepts drags on a specific sub + * tree only. + * <p> + * The root items is also consider to be valid target. + */ + public class TargetInSubtree extends ClientSideCriterion { + + private Object rootId; + private int depthToCheck = -1; + + /** + * Constructs a criteria that accepts the drag if the targeted Item is a + * descendant of Item identified by given id + * + * @param parentItemId + * the item identifier of the parent node + */ + public TargetInSubtree(Object parentItemId) { + rootId = parentItemId; + } + + /** + * Constructs a criteria that accepts drops within given level below the + * subtree root identified by given id. + * + * @param rootId + * the item identifier to be sought for + * @param depthToCheck + * the depth that tree is traversed upwards to seek for the + * parent, -1 means that the whole structure should be + * checked + */ + public TargetInSubtree(Object rootId, int depthToCheck) { + this.rootId = rootId; + this.depthToCheck = depthToCheck; + } + + @Override + public boolean accept(DragAndDropEvent dragEvent) { + try { + TreeTargetDetails eventDetails = (TreeTargetDetails) dragEvent + .getTargetDetails(); + + if (eventDetails.getItemIdOver() != null) { + Object itemId = eventDetails.getItemIdOver(); + int i = 0; + while (itemId != null + && (depthToCheck == -1 || i <= depthToCheck)) { + if (itemId.equals(rootId)) { + return true; + } + itemId = getParent(itemId); + i++; + } + } + return false; + } catch (Exception e) { + return false; + } + } + + @Override + public void paintContent(PaintTarget target) throws PaintException { + super.paintContent(target); + target.addAttribute("depth", depthToCheck); + target.addAttribute("key", key(rootId)); + } + + } + + /** + * Set the item description generator which generates tooltips for the tree + * items + * + * @param generator + * The generator to use or null to disable + */ + public void setItemDescriptionGenerator(ItemDescriptionGenerator generator) { + if (generator != itemDescriptionGenerator) { + itemDescriptionGenerator = generator; + requestRepaint(); + } + } + + /** + * Get the item description generator which generates tooltips for tree + * items + */ + public ItemDescriptionGenerator getItemDescriptionGenerator() { + return itemDescriptionGenerator; + } + +} |