All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.vaadin.ui.Tree Maven / Gradle / Ivy

There is a newer version: 8.27.3
Show newest version
/*
 * Vaadin Framework 7
 *
 * Copyright (C) 2000-2024 Vaadin Ltd
 *
 * This program is available under Vaadin Commercial License and Service Terms.
 *
 * See  for the full
 * license.
 */

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 org.jsoup.nodes.Element;

import com.vaadin.data.Container;
import com.vaadin.data.Item;
import com.vaadin.data.util.ContainerHierarchicalWrapper;
import com.vaadin.data.util.HierarchicalContainer;
import com.vaadin.event.Action;
import com.vaadin.event.Action.Handler;
import com.vaadin.event.ContextClickEvent;
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.server.KeyMapper;
import com.vaadin.server.PaintException;
import com.vaadin.server.PaintTarget;
import com.vaadin.server.Resource;
import com.vaadin.shared.MouseEventDetails;
import com.vaadin.shared.ui.MultiSelectMode;
import com.vaadin.shared.ui.dd.VerticalDropLocation;
import com.vaadin.shared.ui.tree.TreeConstants;
import com.vaadin.shared.ui.tree.TreeServerRpc;
import com.vaadin.shared.ui.tree.TreeState;
import com.vaadin.ui.declarative.DesignAttributeHandler;
import com.vaadin.ui.declarative.DesignContext;
import com.vaadin.ui.declarative.DesignException;
import com.vaadin.util.ReflectTools;

/**
 * Tree component. A Tree can be used to select an item (or multiple items) from
 * a hierarchical set of items.
 *
 * @author Vaadin Ltd.
 * @since 3.0
 */
@SuppressWarnings({ "serial", "deprecation" })
public class Tree extends AbstractSelect implements Container.Hierarchical,
        Action.Container, ItemClickNotifier, DragSource, DropTarget {

    /**
     * ContextClickEvent for the Tree Component.
     *
     * @since 7.6
     */
    public static class TreeContextClickEvent extends ContextClickEvent {

        private final Object itemId;

        public TreeContextClickEvent(Tree source, Object itemId,
                MouseEventDetails mouseEventDetails) {
            super(source, mouseEventDetails);
            this.itemId = itemId;
        }

        @Override
        public Tree getComponent() {
            return (Tree) super.getComponent();
        }

        /**
         * Returns the item id of context clicked row.
         *
         * @return item id of clicked row; null if no row is
         *         present at the location
         */
        public Object getItemId() {
            return itemId;
        }
    }

    /* Private members */

    private static final String NULL_ALT_EXCEPTION_MESSAGE = "Parameter 'altText' needs to be non null";

    /**
     * Item icons alt texts.
     */
    private final HashMap itemIconAlts = new HashMap();

    /**
     * Set of expanded nodes.
     */
    private HashSet expanded = new HashSet();

    /**
     * List of action handlers.
     */
    private LinkedList actionHandlers = null;

    /**
     * Action mapper.
     */
    private KeyMapper 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() {
        this(null);

        registerRpc(new TreeServerRpc() {
            @Override
            public void contextClick(String rowKey, MouseEventDetails details) {
                fireEvent(new TreeContextClickEvent(Tree.this,
                        itemIdMapper.get(rowKey), details));
            }
        });
    }

    /**
     * Creates a new empty tree with caption.
     *
     * @param caption
     */
    public Tree(String caption) {
        this(caption, new HierarchicalContainer());
    }

    /**
     * Creates a new tree with caption and connect it to a Container.
     *
     * @param caption
     * @param dataSource
     */
    public Tree(String caption, Container dataSource) {
        super(caption, dataSource);
    }

    @Override
    public void setItemIcon(Object itemId, Resource icon) {
        setItemIcon(itemId, icon, "");
    }

    /**
     * Sets the icon for an item.
     *
     * @param itemId
     *            the id of the item to be assigned an icon.
     * @param icon
     *            the icon to use or null.
     *
     * @param altText
     *            the alternative text for the icon
     */
    public void setItemIcon(Object itemId, Resource icon, String altText) {
        if (itemId != null) {
            super.setItemIcon(itemId, icon);

            if (icon == null) {
                itemIconAlts.remove(itemId);
            } else if (altText == null) {
                throw new IllegalArgumentException(NULL_ALT_EXCEPTION_MESSAGE);
            } else {
                itemIconAlts.put(itemId, altText);
            }
            markAsDirty();
        }
    }

    /**
     * Set the alternate text for an item.
     *
     * Used when the item has an icon.
     *
     * @param itemId
     *            the id of the item to be assigned an icon.
     * @param altText
     *            the alternative text for the icon
     */
    public void setItemIconAlternateText(Object itemId, String altText) {
        if (itemId != null) {
            if (altText == null) {
                throw new IllegalArgumentException(NULL_ALT_EXCEPTION_MESSAGE);
            } else {
                itemIconAlts.put(itemId, altText);
            }
        }
    }

    /**
     * Return the alternate text of an icon in a tree item.
     *
     * @param itemId
     *            Object with the ID of the item
     * @return String with the alternate text of the icon, or null when no icon
     *         was set
     */
    public String getItemIconAlternateText(Object itemId) {
        String storedAlt = itemIconAlts.get(itemId);
        return storedAlt == null ? "" : storedAlt;
    }

    /* 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);
        markAsDirty();
        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 if 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) {
            markAsDirty();
        } else if (sendChildTree) {
            requestPartialRepaint();
        }
        fireExpandEvent(itemId);

        return true;
    }

    @Override
    public void markAsDirty() {
        super.markAsDirty();
        partialUpdate = false;
    }

    private void requestPartialRepaint() {
        super.markAsDirty();
        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 todo = new Stack();
        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));
            }
        }
        markAsDirty();
        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);
        markAsDirty();
        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 todo = new Stack();
        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)}.
     *
     * 

* The tree is selectable by default. *

* * @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)}. * *

* The tree is selectable by default. *

* * @param selectable * The new selectable state. */ public void setSelectable(boolean selectable) { if (this.selectable != selectable) { this.selectable = selectable; markAsDirty(); } } /** * 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; markAsDirty(); } } /** * 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 */ @Override public void changeVariables(Object source, Map 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(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); if (expandedItemId == id) { expandedItemId = null; } 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(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 variables) { final String[] ka = (String[]) variables.get("selected"); // Converts the key-array to id-set final LinkedList s = new LinkedList(); 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 markAsDirty(); } else if (id != null && containsId(id)) { s.add(id); } } if (!isNullSelectionAllowed() && s.size() < 1) { // empty selection not allowed, keep old value markAsDirty(); return; } setValue(s, true); } /** * Paints any needed component-specific things to the given UIDL stream. * * @see 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.toString()); } } 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()); } if (isHtmlContentAllowed()) { target.addAttribute(TreeConstants.ATTRIBUTE_HTML_ALLOWED, true); } } // Initialize variables final Set actionSet = new LinkedHashSet(); // rendered selectedKeys LinkedList selectedKeys = new LinkedList(); final LinkedList expandedKeys = new LinkedList(); // Iterates through hierarchical tree using a stack of iterators final Stack> iteratorStack = new Stack>(); 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 keys = new ArrayList(); 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(this, itemId); if (stylename != null) { target.addAttribute(TreeConstants.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(TreeConstants.ATTRIBUTE_NODE_CAPTION, getItemCaption(itemId)); final Resource icon = getItemIcon(itemId); if (icon != null) { target.addAttribute(TreeConstants.ATTRIBUTE_NODE_ICON, getItemIcon(itemId)); target.addAttribute(TreeConstants.ATTRIBUTE_NODE_ICON_ALT, getItemIconAlternateText(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 keys = new ArrayList(); final Iterator 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 i = actionSet.iterator(); while (i.hasNext()) { final Action a = i.next(); target.startTag("action"); if (a.getCaption() != null) { target.addAttribute(TreeConstants.ATTRIBUTE_ACTION_CAPTION, a.getCaption()); } if (a.getIcon() != null) { target.addAttribute(TreeConstants.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 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 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 Container.Hierarchical#getParent(Object) */ @Override public Object getParent(Object itemId) { return ((Container.Hierarchical) items).getParent(itemId); } /** * Tests if the Item specified with itemId has child Items. * * @see Container.Hierarchical#hasChildren(Object) */ @Override public boolean hasChildren(Object itemId) { return ((Container.Hierarchical) items).hasChildren(itemId); } /** * Tests if the Item specified with itemId is a root Item. * * @see 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 Container.Hierarchical#rootItemIds() */ @Override public Collection rootItemIds() { return ((Container.Hierarchical) items).rootItemIds(); } /** * Sets the given Item's capability to have children. * * @see Container.Hierarchical#setChildrenAllowed(Object, boolean) */ @Override public boolean setChildrenAllowed(Object itemId, boolean areChildrenAllowed) { final boolean success = ((Container.Hierarchical) items) .setChildrenAllowed(itemId, areChildrenAllowed); if (success) { markAsDirty(); } return success; } @Override public boolean setParent(Object itemId, Object newParentId) { final boolean success = ((Container.Hierarchical) items) .setParent(itemId, newParentId); if (success) { markAsDirty(); } return success; } /* Overriding select behavior */ /** * Sets the Container that serves as the data source of the viewer. * * @see Container.Viewer#setContainerDataSource(Container) */ @Override public void setContainerDataSource(Container newDataSource) { if (newDataSource == null) { newDataSource = new HierarchicalContainer(); } // 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)); } /* * Ensure previous expanded items are cleaned up if they don't exist in * the new container */ if (expanded != null) { /* * We need to check that the expanded-field is not null since * setContainerDataSource() is called from the parent constructor * (AbstractSelect()) and at that time the expanded field is not yet * initialized. */ cleanupExpandedItems(); } } @Override public void containerItemSetChange(Container.ItemSetChangeEvent event) { super.containerItemSetChange(event); if (getContainerDataSource() instanceof Filterable) { boolean hasFilters = !((Filterable) getContainerDataSource()) .getContainerFilters().isEmpty(); if (!hasFilters) { /* * If Container is not filtered then the itemsetchange is caused * by either adding or removing items to the container. To * prevent a memory leak we should cleanup the expanded list * from items which was removed. * * However, there will still be a leak if the container is * filtered to show only a subset of the items in the tree and * later unfiltered items are removed from the container. In * that case references to the unfiltered item ids will remain * in the expanded list until the Tree instance is removed and * the list is destroyed, or the container data source is * replaced/updated. To force the removal of the removed items * the application developer needs to a) remove the container * filters temporarly or b) re-apply the container datasource * using setContainerDataSource(getContainerDataSource()) */ cleanupExpandedItems(); } } } /* 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. * @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. * @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 addExpandListener(ExpandListener listener) { addListener(ExpandEvent.class, listener, ExpandListener.EXPAND_METHOD); } /** * @deprecated As of 7.0, replaced by * {@link #addExpandListener(ExpandListener)} **/ @Deprecated public void addListener(ExpandListener listener) { addExpandListener(listener); } /** * Removes the expand listener. * * @param listener * the Listener to be removed. */ public void removeExpandListener(ExpandListener listener) { removeListener(ExpandEvent.class, listener, ExpandListener.EXPAND_METHOD); } /** * @deprecated As of 7.0, replaced by * {@link #removeExpandListener(ExpandListener)} **/ @Deprecated public void removeListener(ExpandListener listener) { removeExpandListener(listener); } /** * 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. * @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. * @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 addCollapseListener(CollapseListener listener) { addListener(CollapseEvent.class, listener, CollapseListener.COLLAPSE_METHOD); } /** * @deprecated As of 7.0, replaced by * {@link #addCollapseListener(CollapseListener)} **/ @Deprecated public void addListener(CollapseListener listener) { addCollapseListener(listener); } /** * Removes the collapse listener. * * @param listener * the Listener to be removed. */ public void removeCollapseListener(CollapseListener listener) { removeListener(CollapseEvent.class, listener, CollapseListener.COLLAPSE_METHOD); } /** * @deprecated As of 7.0, replaced by * {@link #removeCollapseListener(CollapseListener)} **/ @Deprecated public void removeListener(CollapseListener listener) { removeCollapseListener(listener); } /** * 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(); actionMapper = new KeyMapper(); } if (!actionHandlers.contains(actionHandler)) { actionHandlers.add(actionHandler); markAsDirty(); } } } /** * 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; } markAsDirty(); } } /** * Removes all action handlers */ public void removeAllActionHandlers() { actionHandlers = null; actionMapper = null; markAsDirty(); } /** * Gets the visible item ids. * * @see Select#getVisibleItemIds() */ @Override public Collection getVisibleItemIds() { final LinkedList visible = new LinkedList(); // Iterates trough hierarchical tree using a stack of iterators final Stack> iteratorStack = new Stack>(); 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 setNullSelectionItemId. * * @see 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 Select#setNewItemsAllowed(boolean) */ @Override public void setNewItemsAllowed(boolean allowNewOptions) throws UnsupportedOperationException { if (allowNewOptions) { throw new UnsupportedOperationException(); } } private ItemStyleGenerator itemStyleGenerator; private DropHandler dropHandler; private boolean htmlContentAllowed; @Override public void addItemClickListener(ItemClickListener listener) { addListener(TreeConstants.ITEM_CLICK_EVENT_ID, ItemClickEvent.class, listener, ItemClickEvent.ITEM_CLICK_METHOD); } /** * @deprecated As of 7.0, replaced by * {@link #addItemClickListener(ItemClickListener)} **/ @Override @Deprecated public void addListener(ItemClickListener listener) { addItemClickListener(listener); } @Override public void removeItemClickListener(ItemClickListener listener) { removeListener(TreeConstants.ITEM_CLICK_EVENT_ID, ItemClickEvent.class, listener); } /** * @deprecated As of 7.0, replaced by * {@link #removeItemClickListener(ItemClickListener)} **/ @Override @Deprecated public void removeListener(ItemClickListener listener) { removeItemClickListener(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; markAsDirty(); } } /** * @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 item content is * v-tree-node-[style name]. */ public interface ItemStyleGenerator extends Serializable { /** * Called by Tree when an item is painted. * * @param source * the source Tree * @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(Tree source, 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 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()}. *

* In other words this method returns the identifier of the "folder" * into the drag operation is targeted. *

* If the method returns null, the current target is on a root node or * on other undefined area over the tree component. *

* 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 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; markAsDirty(); } /** * @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 DataBoundTransferable * * @since 6.3 */ protected class TreeTransferable extends DataBoundTransferable { public TreeTransferable(Component sourceComponent, Map 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 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. * *

* 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 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.server.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 getAllowedItemIds( DragAndDropEvent dragEvent, Tree tree); } /** * A criterion that accepts {@link Transferable} only directly on a tree * node that can have children. *

* 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. *

* 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; markAsDirty(); } } /** * Get the item description generator which generates tooltips for tree * items */ public ItemDescriptionGenerator getItemDescriptionGenerator() { return itemDescriptionGenerator; } private void cleanupExpandedItems() { Set removedItemIds = new HashSet(); for (Object expandedItemId : expanded) { if (getItem(expandedItemId) == null) { removedItemIds.add(expandedItemId); if (this.expandedItemId == expandedItemId) { this.expandedItemId = null; } } } expanded.removeAll(removedItemIds); } /** * Reads an Item from a design and inserts it into the data source. * Recursively handles any children of the item as well. * * @since 7.5.0 * @param node * an element representing the item (tree node). * @param selected * A set accumulating selected items. If the item that is read is * marked as selected, its item id should be added to this set. * @param context * the DesignContext instance used in parsing * @return the item id of the new item * * @throws DesignException * if the tag name of the {@code node} element is not * {@code node}. */ @Override protected String readItem(Element node, Set selected, DesignContext context) { if (!"node".equals(node.tagName())) { throw new DesignException("Unrecognized child element in " + getClass().getSimpleName() + ": " + node.tagName()); } String itemId = node.attr("text"); addItem(itemId); if (node.hasAttr("icon")) { Resource icon = DesignAttributeHandler.readAttribute("icon", node.attributes(), Resource.class); setItemIcon(itemId, icon); } if (node.hasAttr("selected")) { selected.add(itemId); } for (Element child : node.children()) { String childItemId = readItem(child, selected, context); setParent(childItemId, itemId); } return itemId; } /** * Recursively writes the root items and their children to a design. * * @since 7.5.0 * @param design * the element into which to insert the items * @param context * the DesignContext instance used in writing */ @Override protected void writeItems(Element design, DesignContext context) { for (Object itemId : rootItemIds()) { writeItem(design, itemId, context); } } /** * Recursively writes a data source Item and its children to a design. * * @since 7.5.0 * @param design * the element into which to insert the item * @param itemId * the id of the item to write * @param context * the DesignContext instance used in writing * @return */ @Override protected Element writeItem(Element design, Object itemId, DesignContext context) { Element element = design.appendElement("node"); element.attr("text", itemId.toString()); Resource icon = getItemIcon(itemId); if (icon != null) { DesignAttributeHandler.writeAttribute("icon", element.attributes(), icon, null, Resource.class); } if (isSelected(itemId)) { element.attr("selected", ""); } Collection children = getChildren(itemId); if (children != null) { // Yeah... see #5864 for (Object childItemId : children) { writeItem(element, childItemId, context); } } return element; } /** * Sets whether html is allowed in the item captions. If set to * true, the captions are passed to the browser as html and the * developer is responsible for ensuring no harmful html is used. If set to * false, the content is passed to the browser as plain text. * The default setting is false * * @since 7.6 * @param htmlContentAllowed * true if the captions are used as html, * false if used as plain text */ public void setHtmlContentAllowed(boolean htmlContentAllowed) { this.htmlContentAllowed = htmlContentAllowed; markAsDirty(); } /** * Checks whether captions are interpreted as html or plain text. * * @since 7.6 * @return true if the captions are displayed as html, * false if displayed as plain text * @see #setHtmlContentAllowed(boolean) */ public boolean isHtmlContentAllowed() { return htmlContentAllowed; } @Override protected TreeState getState() { return (TreeState) super.getState(); } }