/******************************************************************************* * Copyright (c) 2012-2017 Codenvy, S.A. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Codenvy, S.A. - initial API and implementation *******************************************************************************/ package org.eclipse.che.ide.ui.smartTree; import com.google.common.base.Predicate; import com.google.gwt.core.client.GWT; import com.google.gwt.core.client.Scheduler; import com.google.gwt.dom.client.DivElement; import com.google.gwt.dom.client.Document; import com.google.gwt.dom.client.Element; import com.google.gwt.dom.client.Style; import com.google.gwt.event.dom.client.DomEvent; import com.google.gwt.event.shared.GwtEvent; import com.google.gwt.event.shared.HandlerRegistration; import com.google.gwt.user.client.DOM; import com.google.gwt.user.client.Event; import com.google.gwt.user.client.ui.FocusWidget; import com.google.gwt.user.client.ui.impl.FocusImpl; import org.eclipse.che.commons.annotation.Nullable; import org.eclipse.che.ide.DelayedTask; import org.eclipse.che.ide.api.data.tree.HasAction; import org.eclipse.che.ide.api.data.tree.MutableNode; import org.eclipse.che.ide.api.data.tree.Node; import org.eclipse.che.ide.ui.smartTree.event.BeforeCollapseNodeEvent; import org.eclipse.che.ide.ui.smartTree.event.BeforeCollapseNodeEvent.HasBeforeCollapseItemHandlers; import org.eclipse.che.ide.ui.smartTree.event.BeforeExpandNodeEvent; import org.eclipse.che.ide.ui.smartTree.event.BeforeExpandNodeEvent.HasBeforeExpandNodeHandlers; import org.eclipse.che.ide.ui.smartTree.event.BlurEvent; import org.eclipse.che.ide.ui.smartTree.event.BlurEvent.HasBlurHandlers; import org.eclipse.che.ide.ui.smartTree.event.CancellableEvent; import org.eclipse.che.ide.ui.smartTree.event.CollapseNodeEvent; import org.eclipse.che.ide.ui.smartTree.event.CollapseNodeEvent.HasCollapseItemHandlers; import org.eclipse.che.ide.ui.smartTree.event.ExpandNodeEvent; import org.eclipse.che.ide.ui.smartTree.event.ExpandNodeEvent.HasExpandItemHandlers; import org.eclipse.che.ide.ui.smartTree.event.FocusEvent; import org.eclipse.che.ide.ui.smartTree.event.NodeAddedEvent; import org.eclipse.che.ide.ui.smartTree.event.NodeAddedEvent.HasNodeAddedEventHandlers; import org.eclipse.che.ide.ui.smartTree.event.StoreAddEvent; import org.eclipse.che.ide.ui.smartTree.event.StoreAddEvent.StoreAddHandler; import org.eclipse.che.ide.ui.smartTree.event.StoreClearEvent; import org.eclipse.che.ide.ui.smartTree.event.StoreClearEvent.StoreClearHandler; import org.eclipse.che.ide.ui.smartTree.event.StoreDataChangeEvent; import org.eclipse.che.ide.ui.smartTree.event.StoreDataChangeEvent.StoreDataChangeHandler; import org.eclipse.che.ide.ui.smartTree.event.StoreRemoveEvent; import org.eclipse.che.ide.ui.smartTree.event.StoreRemoveEvent.StoreRemoveHandler; import org.eclipse.che.ide.ui.smartTree.event.StoreSortEvent; import org.eclipse.che.ide.ui.smartTree.event.StoreSortEvent.StoreSortHandler; import org.eclipse.che.ide.ui.smartTree.event.StoreUpdateEvent; import org.eclipse.che.ide.ui.smartTree.event.StoreUpdateEvent.StoreUpdateHandler; import org.eclipse.che.ide.ui.smartTree.event.internal.NativeTreeEvent; import org.eclipse.che.ide.ui.smartTree.handler.GroupingHandlerRegistration; import org.eclipse.che.ide.ui.smartTree.presentation.DefaultPresentationRenderer; import org.eclipse.che.ide.ui.smartTree.presentation.HasPresentation; import org.eclipse.che.ide.ui.smartTree.presentation.PresentationRenderer; import org.eclipse.che.ide.ui.status.ComponentWithEmptyStatus; import org.eclipse.che.ide.ui.status.EmptyStatus; import org.eclipse.che.ide.ui.status.StatusText; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Strings.isNullOrEmpty; import static java.util.Collections.singletonList; import static java.util.Collections.unmodifiableList; import static org.eclipse.che.ide.util.dom.Elements.disableTextSelection; /** * Widget which support displaying hierarchical data. Internal data stores in special storage {@code NodeStorage}. * <p> * {@code UniqueKeyProvider} provides interface between external node and internal storage by generating specified * unique id. * <p> * {@code NodeStorage} provides mechanism for storing external nodes. It doesn't response for displaying the last * one's. Each node in {@code NodeStorage} wraps into internal object, called {@code NodeDescriptor} which has * various fields which represent node state, e.g. expand/load state, rendered DOM elements, etc. Applied changes * in {@code NodeStorage} immediately affects on the view representation. * <p> * {@code NodeLoader} provides mechanism for loading node's children. * <p> * {@code SelectionModel} provides mechanism for controlling selection on the current tree widget. * <p> * Communication between {@code Tree}, {@code NodeStorage}, {@link NodeLoader} and {@code SelectionModel} is * organized by own internal event bus. * <p> * Following snippet displays how to initialize tree widget: * <pre> * NodeUniqueKeyProvider idProvider = new NodeUniqueKeyProvider() { * public String getKey(@NotNull Node item) { * return String.valueOf(item.hashCode()); * } * } * * NodeStorage nodeStorage = new NodeStorage(idProvider); * NodeLoader nodeLoader = new NodeLoader(Collections.<NodeInterceptor>emptySet()); * * Tree tree = new Tree(nodeStorage, nodeLoader); * * //add nodes into tree * tree.getNodeStorage().add(Collections.<Node>emptyList()); * * FlowPanel panel = new FlowPanel(); * panel.add(tree); * </pre> * <p> * By default, each node will be rendered with simple name. If you want to have extended rendered presentation for * each node, you should implement {@code Node} with {@code HasPresentation} interface. In this case rendered * presentation will include these attributes which you can configure: * <ul> * <li>SVG Icon</li> * <li>User element</li> * <li>Presentable text</li> * <li>Info text</li> * </ul> * <p> * By `User element` means, that you can provide any {@code com.google.gwt.dom.client.Element} element to display * own elements. * <p> * By `Presentable text` it means that node has base name to be displayed. * By `Info text` it means that node may has additional text to be displayed next to `Presentable text`. * <p> * Presentable text can be styled by providing valid css snippet as parameter {@code org.eclipse.che.ide.ui.smartTree.presentation.NodePresentation#setPresentableTextCss(java.lang.String)} * The same rule is suitable for info text. * <p> * Following snippet displays how to configure node presentation: * <pre> * ... * public void updatePresentation(@NotNull NodePresentation presentation) { * presentation.setPresentableCss("color:black;font-weight:bold;text-decoration:underline;"); * presentation.setPresentableText("node name"); * presentation.setPresentableIcon(icon); //SVGResource * presentation.setInfoText("into text"); * presentation.setInfoTextCss("color:grey;font-size:0.5em;"); * presentation.setInfoTextWrapper(Pair.of("(", ")")); * presentation.setUserElement(Document.get.createDivElement()); //any html element * } * ... * </pre> * * @author Vlad Zhukovskiy * @see NodeStorage * @see NodeLoader * @see SelectionModel * @see Node * @see HasPresentation * @see org.eclipse.che.ide.ui.smartTree.presentation.NodePresentation * @see UniqueKeyProvider * @see NodeUniqueKeyProvider */ public class Tree extends FocusWidget implements HasBeforeExpandNodeHandlers, HasExpandItemHandlers, HasBeforeCollapseItemHandlers, HasCollapseItemHandlers, ComponentWithEmptyStatus, HasBlurHandlers, HasNodeAddedEventHandlers { private static final String NULL_NODE_MSG = "Node should not be a null"; private static final String NULL_NODE_STORAGE_MSG = "Node should not be a null"; /** * Flag that instruct tree widget always expand non-leaf nodes. */ private boolean autoExpand = false; /** * Flag that instruct tree widget always load non-leaf nodes, but not expand one's. */ private boolean autoLoad = false; /** * Flag that instruct tree widget to track onMouseOver event and set hover class to node. */ private boolean trackMouseOver = true; /** * Flag that instruct tree widget to use auto selection. */ private boolean autoSelect = true; /** * Flag that instruct tree widget to allow user selection on nodes element. */ private boolean allowTextSelection = false; /** * Flag that instruct tree widget to disable browser native context menu. */ private boolean disableNativeContextMenu = true; /** * Internal node storage. Contains information about each loaded node with additional information, such as expand state of DOM. */ private NodeStorage nodeStorage; /** * Component that performs loading node's children. */ private NodeLoader nodeLoader; /** * Selection model component. */ private SelectionModel selectionModel; /** * External context menu invocation handler. */ private ContextMenuInvocationHandler contextMenuInvocationHandler; /** * Node presentation renderer. */ private PresentationRenderer<Node> presentationRenderer; /** * Tree's root widget element. */ private Element rootContainer; /** * Component which show some configurable text if there is a condition that tree widget doesn't have any element. */ private EmptyStatus<Tree> emptyStatus; /** * View for the tree. View allow manipulating with node DOM elements. */ private TreeView view; /** * Tree style configuration. */ private TreeStyles treeStyles; /** * Internal temporary storage for node ID and node descriptor. */ private Map<String, NodeDescriptor> nodesByDom; /** * @see FocusImpl#getFocusImplForPanel() */ private FocusImpl focusImpl; /** * @see FocusImpl#createFocusable() */ private Element focusEl; /** * Delayed task to update visual state of specific node. */ private DelayedTask updateTask; /** * Experimental feature that allow tree to simulate "Go Into" on non-leaf node if one's allow this by checking Node#supportGoInto(). */ private GoInto goInto; private GroupingHandlerRegistration storeHandlers; private boolean focusConstrainScheduled = false; private boolean focused = false; public Tree(NodeStorage nodeStorage, NodeLoader nodeLoader) { this(nodeStorage, nodeLoader, GWT.<TreeStyles>create(TreeStyles.class)); } public Tree(NodeStorage nodeStorage, NodeLoader nodeLoader, EmptyStatus<Tree> emptyStatus) { this(nodeStorage, nodeLoader, GWT.<TreeStyles>create(TreeStyles.class), emptyStatus); } public Tree(NodeStorage nodeStorage, NodeLoader nodeLoader, TreeStyles treeStyles) { this(nodeStorage, nodeLoader, treeStyles, null); } public Tree(NodeStorage nodeStorage, NodeLoader nodeLoader, TreeStyles treeStyles, EmptyStatus<Tree> emptyStatus) { checkNotNull(nodeStorage); checkNotNull(nodeLoader); checkNotNull(treeStyles); this.treeStyles = treeStyles; this.treeStyles.styles().ensureInjected(); this.nodesByDom = new HashMap<>(); this.focusImpl = FocusImpl.getFocusImplForPanel(); this.storeHandlers = new GroupingHandlerRegistration(); ensureTreeElement(); ensureFocusElement(); setNodeStorage(nodeStorage); setNodeLoader(nodeLoader); setSelectionModel(new SelectionModel()); setGoInto(new DefaultGoInto()); setView(new TreeView()); setAllowTextSelection(false); disableBrowserContextMenu(true); // use as default if (emptyStatus == null) { emptyStatus = new StatusText<>(); } this.emptyStatus = emptyStatus; this.emptyStatus.init(this, new Predicate<Tree>() { @Override public boolean apply(@Nullable Tree tree) { return tree.getNodeStorage().getRootCount() == 0; } }); } /** * {@inheritDoc} */ @Override public HandlerRegistration addBeforeCollapseHandler(BeforeCollapseNodeEvent.BeforeCollapseNodeHandler handler) { return addHandler(handler, BeforeCollapseNodeEvent.getType()); } /** * {@inheritDoc} */ @Override public HandlerRegistration addBeforeExpandHandler(BeforeExpandNodeEvent.BeforeExpandNodeHandler handler) { return addHandler(handler, BeforeExpandNodeEvent.getType()); } /** * {@inheritDoc} */ @Override public HandlerRegistration addCollapseHandler(CollapseNodeEvent.CollapseNodeHandler handler) { return addHandler(handler, CollapseNodeEvent.getType()); } /** * {@inheritDoc} */ @Override public HandlerRegistration addExpandHandler(ExpandNodeEvent.ExpandNodeHandler handler) { return addHandler(handler, ExpandNodeEvent.getType()); } /** * {@inheritDoc} */ @Override public HandlerRegistration addNodeAddedHandler(NodeAddedEvent.NodeAddedEventHandler handler) { return addHandler(handler, NodeAddedEvent.getType()); } /** * {@inheritDoc} */ @Override public HandlerRegistration addBlurHandler(BlurEvent.BlurHandler handler) { return addHandler(handler, BlurEvent.getType()); } public final void setView(TreeView view) { this.view = view; view.bind(this); } /** * Returns true if checked node was expanded. * * @param node node to check * @return true if node was expanded, otherwise false */ public boolean isExpanded(Node node) { checkNotNull(node, NULL_NODE_MSG); NodeDescriptor nodeDescriptor = getNodeDescriptor(node); return nodeDescriptor != null && nodeDescriptor.isExpanded(); } /** * Returns true if checked node is leaf. * * @param node node to check * @return true if node is leaf, otherwise false */ public boolean isLeaf(Node node) { checkNotNull(node, NULL_NODE_MSG); return node.isLeaf(); } /** * Returns internal node descriptor object. * Descriptor contains expand, load/loading state, rendered DOM elements and parent/children relationship. * * @param node node to process * @return instance of {@link NodeDescriptor} or <code>null</code> if one's doesn't exists */ public NodeDescriptor getNodeDescriptor(Node node) { checkNotNull(node, NULL_NODE_MSG); checkNotNull(nodeStorage, NULL_NODE_STORAGE_MSG); return nodeStorage.getNodeMap().get(getUniqueId(node)); } /** * Returns internal node descriptor object. * Descriptor contains expand, load/loading state, rendered DOM elements and parent/children relationship. * <p> * User can pass any internal element, such as joint, icon, presentable or info text and method will find * the nearest suitable parent element which can be casted into node. * <p> * Method should be used to find nodes after handling DOM events, such as Event.ONCLICK or Event.ONDBLCLICK. * * @param target DOM element, e.g. joint, icon, presentable of info text * @return instance of {@link NodeDescriptor} or <code>null</code> if one's doesn't exists */ public NodeDescriptor getNodeDescriptor(Element target) { checkNotNull(target); Element nodeElement = getNearestParentElement(target, treeStyles.styles().rootContainer()); if (!(nodeElement == null || isNullOrEmpty(nodeElement.getId()))) { return nodesByDom.get(nodeElement.getId()); } return null; } /** * Returns unique internal ID for the specific node. * ID is retrieving not from the stored nodes. It forms dynamically from {@link UniqueKeyProvider}. * * @param node node to process * @return unique ID or null if no ID was found * @see UniqueKeyProvider */ public String getUniqueId(Node node) { checkNotNull(node, NULL_NODE_MSG); return nodeStorage.getKeyProvider().getKey(node); } /** * Set expanded state for the specific node. * Expand performs only for the first nested level. * * @param node node to expand/collapse * @param expand true if node should be expanded, otherwise false * @see Tree#setExpanded(Node, boolean, boolean) */ public void setExpanded(Node node, boolean expand) { checkNotNull(node, NULL_NODE_MSG); setExpanded(node, expand, false); } /** * Set expanded state for the specific node. * * @param node node to expand/collapse * @param expand true if node should be expanded, otherwise false * @param deep true if nested nodes should also be expanded, otherwise false */ public void setExpanded(Node node, boolean expand, boolean deep) { checkNotNull(node, NULL_NODE_MSG); if (expand) { // make item visible by expanding parents List<Node> list = new ArrayList<>(); Node p = node; while ((p = nodeStorage.getParent(p)) != null) { NodeDescriptor nodeDescriptor = getNodeDescriptor(p); if (nodeDescriptor == null || !nodeDescriptor.isExpanded()) { list.add(p); } } for (int i = list.size() - 1; i >= 0; i--) { Node item = list.get(i); setExpanded(item, true, false); } } NodeDescriptor nodeDescriptor = getNodeDescriptor(node); if (nodeDescriptor == null) { return; } if (!isAttached()) { nodeDescriptor.setExpand(expand); return; } if (expand) { onExpand(node, nodeDescriptor, deep); } else { onCollapse(node, nodeDescriptor, deep); } } /** * Set leaf state for the specific node. * To be able to change leaf state, node should implement {@link MutableNode}. * <p> * Useful for dynamically changing node state, e.g. to show members on leaf node. * * @param node node to process * @param leaf true if node should become to be a leaf, otherwise false * @return true if node changed own state, otherwise false */ public boolean setLeaf(Node node, boolean leaf) { checkNotNull(node, NULL_NODE_MSG); if (node instanceof MutableNode) { NodeDescriptor nodeDescriptor = getNodeDescriptor(node); if (nodeDescriptor != null) { nodeDescriptor.setLeaf(leaf); refresh(node); return true; } } return false; } /** * Set custom {@link NodeLoader} component. * * @param nodeLoader instance of {@link NodeLoader} * @see NodeLoader */ public final void setNodeLoader(NodeLoader nodeLoader) { if (this.nodeLoader != null) { this.nodeLoader.bindTree(null); } this.nodeLoader = nodeLoader; if (nodeLoader != null) { nodeLoader.bindTree(this); } } /** * Instruct tree to automatically load all nodes but not expand them. * * @param autoLoad true if nodes should be automatically loaded */ public void setAutoLoad(boolean autoLoad) { this.autoLoad = autoLoad; } /** * Instruct tree to automatically expand all non-leaf nodes. * Be carefully with configuring this field. Because if tree has many nodes that loads asynchronously it may affect performance. * * @param autoExpand true if nodes should be automatically expanded */ public void setAutoExpand(boolean autoExpand) { this.autoExpand = autoExpand; } /** * Instruct tree to automatically setup selection when one's is rendering. * * @param autoSelect true if first node should be selected after node rendering */ public void setAutoSelect(boolean autoSelect) { this.autoSelect = autoSelect; } /** * Returns list of current root nodes. * Before return method check if tree is in "Go Into" mode, if it is, then method will return only one node that is in "Go Into" mode. * Otherwise all root nodes will be returned. * * @return unmodifiable list of root nodes */ public List<Node> getRootNodes() { List<Node> nodes = goInto.isActive() ? singletonList(goInto.getLastUsed()) : nodeStorage.getRootItems(); return unmodifiableList(nodes); } /** * Returns tree view for the specified internal operations with one's. * * @return instance of {@link TreeView} */ public TreeView getView() { return view; } /** * Collect all children for specified list of nodes. Collected nodes may be filtered by visible status. * * @param parent list of nodes, which children should be collected * @param onlyVisible true if only visible children should be collected, otherwise false * @return unmodifiable list of children */ public List<Node> getAllChildNodes(List<Node> parent, boolean onlyVisible) { List<Node> list = new ArrayList<>(); for (Node node : parent) { list.add(node); if (!onlyVisible || getNodeDescriptor(node).isExpanded()) { findChildren(node, list, onlyVisible); } } return unmodifiableList(list); } /** * Render node with specified depth. * Rendered node doesn't affect existed rendered nodes in internal storage * * @param node node to render * @param depth node depth * @return rendered DOM element */ public Element renderNode(Node node, int depth) { checkNotNull(node, NULL_NODE_MSG); return getPresentationRenderer().render(node, register(node), getJoint(node), depth); } /** * Returns joint element for the specified node. * * @param node node to process * @return instance of {@link org.eclipse.che.ide.ui.smartTree.Tree.Joint} element */ public Joint getJoint(Node node) { if (node == null) { return Joint.NONE; } if (isLeaf(node)) { return Joint.NONE; } if (getNodeDescriptor(node) != null && getNodeDescriptor(node).isLoaded() && nodeStorage.getChildCount(node) == 0) { return Joint.NONE; } return getNodeDescriptor(node).isExpanded() ? Joint.EXPANDED : Joint.COLLAPSED; } /** * Scroll focus element into specific node. * * @param node node to scroll */ public void scrollIntoView(Node node) { checkNotNull(node, NULL_NODE_MSG); NodeDescriptor descriptor = getNodeDescriptor(node); if (descriptor == null) { return; } Element container = descriptor.getNodeContainerElement(); if (container == null) { return; } container.scrollIntoView(); focusEl.getStyle().setLeft((nodeStorage.getDepth(node) - 1) * 16, Style.Unit.PX); focusEl.getStyle().setTop(container.getOffsetTop(), Style.Unit.PX); } /** * Sets window focus to current tree. */ public void focus() { focusImpl.focus(focusEl); } /** * Returns instance of {@link NodeStorage}. * * @return instance of {@link NodeStorage} * @see NodeStorage */ public NodeStorage getNodeStorage() { return nodeStorage; } /** * Returns instance of {@link NodeLoader}. * * @return instance of {@link NodeLoader} * @see NodeLoader */ public NodeLoader getNodeLoader() { return nodeLoader; } /** * Returns instance of {@link SelectionModel}. * * @return instance of {@link SelectionModel} * @see SelectionModel */ public SelectionModel getSelectionModel() { return selectionModel; } /** * Sets custom {@link NodeStorage}. * * @param nodeStorage custom {@link NodeStorage} */ public final void setNodeStorage(NodeStorage nodeStorage) { checkNotNull(nodeStorage, NULL_NODE_STORAGE_MSG); if (this.nodeStorage != null) { storeHandlers.removeHandler(); if (isOrWasAttached()) { clear(); } } this.nodeStorage = nodeStorage; Handler handler = new Handler(); storeHandlers.add(nodeStorage.addStoreAddHandler(handler)); storeHandlers.add(nodeStorage.addStoreUpdateHandler(handler)); storeHandlers.add(nodeStorage.addStoreRemoveHandler(handler)); storeHandlers.add(nodeStorage.addStoreDataChangeHandler(handler)); storeHandlers.add(nodeStorage.addStoreClearHandler(handler)); storeHandlers.add(nodeStorage.addStoreSortHandler(handler)); if (getSelectionModel() != null) { getSelectionModel().bindStorage(nodeStorage); } if (isOrWasAttached()) { renderChildren(null); } } /** * Set custom {@link SelectionModel}. * * @param selectionModel custom {@link SelectionModel} */ public final void setSelectionModel(SelectionModel selectionModel) { checkNotNull(selectionModel); if (this.selectionModel != null) { this.selectionModel.bindTree(null); } this.selectionModel = selectionModel; selectionModel.bindTree(this); } /** * Clear tree. Calling this method doesn't remove existed nodes from the internal storage. It affects only visual representation of * nodes. To remove nodes from internal storage, method org.eclipse.che.ide.ui.smartTree.TreeNodeStorage#clear() should be called. */ public void clear() { if (isOrWasAttached()) { Element container = getContainer(null); if (container != null) { container.setInnerHTML(""); } Map<String, NodeDescriptor> nodeMap = getNodeStorage().getNodeMap(); for (NodeDescriptor nodeDescriptor : nodeMap.values()) { nodeDescriptor.clearElements(); } nodesByDom.clear(); if (isAttached()) { moveFocus(getContainer(null)); } getEmptyStatus().paint(); //draw empty label } } /** * {@inheritDoc} */ @Override public void onBrowserEvent(Event event) { switch (event.getTypeInt()) { case Event.ONCLICK: onClick(event); break; case Event.ONDBLCLICK: onDoubleClick(event); break; case Event.ONSCROLL: onScroll(event); break; case Event.ONFOCUS: onFocus(event); break; case Event.ONBLUR: onBlur(event); break; case Event.ONCONTEXTMENU: if (disableNativeContextMenu) { event.preventDefault(); } onRightClick(event); break; } view.onEvent(event); // we are not calling super so must fire dom events DomEvent.fireNativeEvent(event, this, this.getElement()); } /** * Returns {@code true} if nodes are highlighted on mouse over. * * @return true if enabled */ public boolean isTrackMouseOver() { return trackMouseOver; } /** * True to highlight nodes when the mouse is over (defaults to {@code true}). * * @param trackMouseOver {@code true} to highlight nodes on mouse over */ public void setTrackMouseOver(boolean trackMouseOver) { this.trackMouseOver = trackMouseOver; } /** * Allow to select inner text. * * @param enable true if text is allowed to be selected, otherwise false */ public void setAllowTextSelection(boolean enable) { allowTextSelection = enable; if (isAttached()) { disableTextSelection(getRootContainer(), !enable); } } /** * Refresh visual representation for the specific node. * * @param node node to be refreshed */ public void refresh(Node node) { checkNotNull(node, NULL_NODE_MSG); if (!isOrWasAttached()) { return; } NodeDescriptor nodeDescriptor = getNodeDescriptor(node); if (view.getRootContainer(nodeDescriptor) == null) { return; } if (!(node instanceof HasPresentation)) { return; } ((HasPresentation)node).getPresentation(true); //update presentation Element el = getPresentationRenderer().render(node, nodeDescriptor.getDomId(), getJoint(node), nodeStorage.getDepth(node) - 1); view.onElementChanged(nodeDescriptor, el); } /** * Disable or enable browser context menu. * * @param disable true if browser context menu should be disabled, otherwise false */ public void disableBrowserContextMenu(boolean disable) { disableNativeContextMenu = disable; if (disable) { sinkEvents(Event.ONCONTEXTMENU); } } /** * Returns true if it is allowed to select text inside tree. * * @return true if allowed, otherwise false */ public boolean isAllowTextSelection() { return allowTextSelection; } /** * Set external context menu invocation handler. * Need to allow use context menu outside of tree widget. * * @param invocationHandler context menu invocation handler */ public void setContextMenuInvocationHandler(ContextMenuInvocationHandler invocationHandler) { checkNotNull(invocationHandler); contextMenuInvocationHandler = invocationHandler; } /** * Reset registered invocation handler if such exists. */ public void resetContextMenuInvocationHandler() { if (contextMenuInvocationHandler != null) { contextMenuInvocationHandler = null; } } /** * Returns registered invocation handler or null if such handler hadn't registered before. * * @return context menu invocation handler */ public ContextMenuInvocationHandler getContextMenuInvocationHandler() { return contextMenuInvocationHandler; } /** * Returns tree style configuration. * * @return tree style configuration. */ public TreeStyles getTreeStyles() { return treeStyles; } /** * {@inheritDoc} */ @Override public EmptyStatus getEmptyStatus() { return emptyStatus; } /** * Expands all non-leaf node in current tree. * Be careful with this method. In case if you have nodes, which children may be loaded asynchronously it may perform powerful * load to your server and completely reduce your performance. It useful for those tree, which has static data model. */ public void expandAll() { for (Node node : nodeStorage.getRootItems()) { setExpanded(node, true, true); } } /** * Collapse all expanded nodes. */ public void collapseAll() { for (Node node : nodeStorage.getRootItems()) { setExpanded(node, false, true); } } /** * Returns presentation node renderer. Need for unusual operations with node presentations. * * @return {@link DefaultPresentationRenderer} in case if no presentation renderer was registered before */ public PresentationRenderer<Node> getPresentationRenderer() { if (presentationRenderer == null) { presentationRenderer = new DefaultPresentationRenderer<>(treeStyles); } return presentationRenderer; } /** * Set custom node presentation renderer. * Useful to override default mechanism of node rendering. With it you can provide for example custom attributes for each rendered * node. * * @param presentationRenderer presentation renderer */ public void setPresentationRenderer(PresentationRenderer<Node> presentationRenderer) { this.presentationRenderer = presentationRenderer; } /** * Set custom implementation of "Go Into" mode. * * @param goInto {@link GoInto} processor * @see GoInto */ public final void setGoInto(GoInto goInto) { this.goInto = goInto; this.goInto.bind(this); } /** * Returns registered processor for "Go Into" feature. * * @return {@link GoInto} processor * @see GoInto */ public GoInto getGoInto() { return goInto; } /** * {@inheritDoc} */ @Override protected void onAttach() { boolean isOrWasAttached = isOrWasAttached(); super.onAttach(); if (nodeStorage == null) { throw new IllegalStateException("Cannot attach a tree without a store"); } if (!isOrWasAttached) { onAfterFirstAttach(); } update(); } protected void update() { if (updateTask == null) { updateTask = new DelayedTask() { @Override public void onExecute() { int count = getVisibleRowCount(); List<Node> rootItems = getRootNodes(); if (count > 0) { List<Node> visible = getAllChildNodes(rootItems, true); int[] vr = getVisibleRows(visible, count); for (int i = vr[0]; i <= vr[1]; i++) { if (goInto.isActive()) { //constraint node indention int goIntoDirDepth = nodeStorage.getDepth(goInto.getLastUsed()); int currentNodeDepth = nodeStorage.getDepth(visible.get(i)); view.onDepthUpdated(getNodeDescriptor(visible.get(i)), currentNodeDepth - goIntoDirDepth); } if (!isRowRendered(i, visible)) { Node parent = nodeStorage.getParent(visible.get(i)); Element html = renderNode(visible.get(i), nodeStorage.getDepth(parent)); Element rootContainer = view.getRootContainer(getNodeDescriptor(visible.get(i))); rootContainer.replaceChild(rootContainer.getFirstChildElement(), html); } else { refresh(visible.get(i)); } } } if (selectionModel.getSelectedNodes().isEmpty() && autoSelect && !rootItems.isEmpty()) { selectionModel.select(rootItems.get(0), false); } } }; } updateTask.delay(view.getScrollDelay()); } protected Element getContainer(Node node) { if (node == null) { return rootContainer; } NodeDescriptor nodeDescriptor = getNodeDescriptor(node); if (nodeDescriptor != null) { return view.getDescendantsContainer(nodeDescriptor); } return null; } protected void moveFocus(Element selectedElem) { if (selectedElem == null) { return; } int containerLeft = getAbsoluteLeft(); int containerTop = getAbsoluteTop(); int left = selectedElem.getAbsoluteLeft() - containerLeft; int top = selectedElem.getAbsoluteTop() - containerTop; int width = selectedElem.getOffsetWidth(); int height = selectedElem.getOffsetHeight(); if (width == 0 || height == 0) { focusEl.getStyle().setTop(0, Style.Unit.PX); focusEl.getStyle().setLeft(0, Style.Unit.PX); return; } focusEl.getStyle().setTop(top, Style.Unit.PX); focusEl.getStyle().setLeft(left, Style.Unit.PX); } protected void toggle(Node node) { NodeDescriptor nodeDescriptor = getNodeDescriptor(node); if (nodeDescriptor != null) { if (nodeDescriptor.isExpanded()) { setExpanded(node, false, true); } else { setExpanded(node, true); } } } /** * Completely redraws the children of the given parent (or all items if parent is null), throwing away details like * currently expanded nodes, etc. * * @param parent the parent of the items to redraw */ private void redraw(Node parent) { if (!isOrWasAttached()) { return; } if (parent == null) { clear(); renderChildren(null); if (autoSelect) { Node child = nodeStorage.getChild(0); if (child != null) { getSelectionModel().setSelection(singletonList(child)); } } } else { NodeDescriptor nodeDescriptor = getNodeDescriptor(parent); nodeDescriptor.setLoaded(true); nodeDescriptor.setLoading(false); if (isLeaf(nodeDescriptor.getNode())) { return; } if (isExpanded(parent)) { setExpanded(parent, false, true); Element container = getContainer(parent); container.setInnerHTML(""); nodeDescriptor.setChildrenRendered(false); setExpanded(parent, true, nodeDescriptor.isExpandDeep()); } else { if (nodeDescriptor.isChildrenRendered()) { Element container = getContainer(parent); container.setInnerHTML(""); nodeDescriptor.setChildrenRendered(false); } setExpanded(parent, true, nodeDescriptor.isExpandDeep()); } } } private void onExpand(Node node, NodeDescriptor nodeDescriptor, boolean deep) { if (isLeaf(node)) { return; } if (nodeDescriptor.isLoading()) { //node may have been already requested for expanding return; } if (!nodeDescriptor.isExpanded() && nodeLoader != null && (!nodeDescriptor.isLoaded())) { nodeStorage.removeChildren(node); nodeDescriptor.setExpand(true); nodeDescriptor.setExpandDeep(deep); nodeDescriptor.setLoading(true); view.onLoadChange(nodeDescriptor, true); nodeLoader.loadChildren(node); return; } if (!fireCancellableEvent(new BeforeExpandNodeEvent(node))) { if (deep) { nodeDescriptor.setExpandDeep(false); } return; } if (!nodeDescriptor.isExpanded()) { nodeDescriptor.setExpanded(true); if (!nodeDescriptor.isChildrenRendered()) { renderChildren(node); nodeDescriptor.setChildrenRendered(true); } //direct expand on the view view.expand(nodeDescriptor); update(); fireEvent(new ExpandNodeEvent(node)); } if (deep) { setExpandChildren(node, true); } } private void setExpandChildren(Node node, boolean expand) { for (Node child : nodeStorage.getChildren(node)) { setExpanded(child, expand, true); } } private void renderChildren(Node parent) { int depth = nodeStorage.getDepth(parent); List<Node> children = parent == null ? nodeStorage.getRootItems() : nodeStorage.getChildren(parent); if (children.size() == 0) { emptyStatus.paint(); return; } Element container = getContainer(parent); if (container == null) { return; } for (Node child : children) { Element element = renderNode(child, depth); container.appendChild(element); } for (Node child : children) { NodeDescriptor nodeDescriptor = getNodeDescriptor(child); if (autoExpand) { setExpanded(child, true); } else if (nodeDescriptor.isExpand() && !isLeaf(nodeDescriptor.getNode())) { nodeDescriptor.setExpand(false); setExpanded(child, true); } else if (nodeLoader != null) { if (autoLoad) { if (nodeLoader.mayHaveChildren(child)) { nodeLoader.loadChildren(child); } } } else if (autoLoad) { renderChildren(child); } } if (parent == null) { ensureFocusElement(); } update(); } private void onCollapse(Node node, NodeDescriptor nodeDescriptor, boolean deep) { if (nodeDescriptor.isExpanded() && fireCancellableEvent(new BeforeCollapseNodeEvent(node))) { nodeDescriptor.setExpanded(false); view.collapse(nodeDescriptor); fireEvent(new CollapseNodeEvent(node)); } nodeDescriptor.setLoaded(false); for (Node toRemove : nodeStorage.getAllChildren(node)) { nodeStorage.remove(toRemove); } Scheduler.get().scheduleDeferred(new Scheduler.ScheduledCommand() { @Override public void execute() { update(); } }); } private String register(Node node) { String id = getUniqueId(node); if (nodeStorage.getNodeMap().containsKey(id)) { NodeDescriptor nodeDescriptor = nodeStorage.getNodeMap().get(id); if (nodeDescriptor.getDomId() == null || nodeDescriptor.getDomId().isEmpty()) { String domId = Document.get().createUniqueId(); nodeDescriptor.setDomId(domId); } nodeDescriptor.reset(); nodeDescriptor.clearElements(); nodesByDom.put(nodeDescriptor.getDomId(), nodeDescriptor); return nodeDescriptor.getDomId(); } else { NodeDescriptor nodeDescriptor = nodeStorage.wrap(node); String domId = Document.get().createUniqueId(); nodeDescriptor.setDomId(domId); nodesByDom.put(nodeDescriptor.getDomId(), nodeDescriptor); return domId; } } private void unregister(Node node) { if (node != null) { NodeDescriptor nodeDescriptor = nodeStorage.getNodeMap().remove(getUniqueId(node)); if (nodeDescriptor != null) { nodesByDom.remove(nodeDescriptor.getDomId()); nodeDescriptor.clearElements(); nodeStorage.getNodeMap().remove(nodeStorage.getKeyProvider().getKey(node)); } } } private boolean fireCancellableEvent(GwtEvent<?> event) { fireEvent(event); if (event instanceof CancellableEvent) { return !((CancellableEvent)event).isCancelled(); } return true; } private void ensureTreeElement() { DivElement element = Document.get().createDivElement(); element.addClassName(treeStyles.styles().tree()); setElement(element); } private void ensureFocusElement() { if (focusEl != null) { focusEl.removeFromParent(); } focusEl = getElement().appendChild(focusImpl.createFocusable()); focusEl.addClassName(treeStyles.styles().noFocusOutline()); if (focusEl.hasChildNodes()) { focusEl.getFirstChildElement().addClassName(treeStyles.styles().noFocusOutline()); Style focusElStyle = focusEl.getFirstChildElement().getStyle(); focusElStyle.setBorderWidth(0, Style.Unit.PX); focusElStyle.setFontSize(1, Style.Unit.PX); focusElStyle.setPropertyPx("lineHeight", 1); } focusEl.getStyle().setLeft(0, Style.Unit.PX); focusEl.getStyle().setTop(0, Style.Unit.PX); focusEl.getStyle().setPosition(Style.Position.ABSOLUTE); //subscribe for Event.FOCUSEVENTS int bits = DOM.getEventsSunk((Element)focusEl.cast()); //do not remove redundant cast, GWT tests will fail DOM.sinkEvents((Element)focusEl.cast(), bits | Event.FOCUSEVENTS); } private boolean isRowRendered(int i, List<Node> visible) { Element e = view.getRootContainer(getNodeDescriptor(visible.get(i))); return e != null && e.getFirstChild().hasChildNodes(); } private int getVisibleRowCount() { int rh = view.getCalculatedRowHeight(); int visibleHeight = getElement().getOffsetHeight(); return (int)((visibleHeight < 1) ? 0 : Math.ceil(visibleHeight / rh)); } private void findChildren(Node parent, List<Node> list, boolean onlyVisible) { for (Node child : nodeStorage.getChildren(parent)) { final NodeDescriptor descriptor = getNodeDescriptor(child); if (descriptor == null) { continue; } list.add(child); if (!onlyVisible || descriptor.isExpanded()) { findChildren(child, list, onlyVisible); } } } private int[] getVisibleRows(List<Node> visible, int count) { int sc = getElement().getScrollTop(); int start = (int)(sc == 0 ? 0 : Math.floor(sc / view.getCalculatedRowHeight()) - 1); int first = Math.max(start, 0); int last = Math.min(start + count + 2, visible.size() - 1); return new int[] {first, last}; } private Element getRootContainer() { return getElement(); } private void onAdd(StoreAddEvent event) { for (Node child : event.getNodes()) { register(child); } if (isOrWasAttached()) { Node parent = nodeStorage.getParent(event.getNodes().get(0)); final Element container = getContainer(parent); final int index = event.getIndex(); if (parent == null) { for (Node child : event.getNodes()) { if (index == 0) { container.insertFirst(renderNode(child, 0)); } else if (index == getNodeStorage().getRootCount() - event.getNodes().size()) { com.google.gwt.dom.client.Node lastChild = container.getLastChild(); container.insertAfter(renderNode(child, 0), lastChild); } else { container.insertBefore(renderNode(child, 0), container.getChild(index)); } scrollIntoView(child); } } else { NodeDescriptor descriptor = getNodeDescriptor(parent); if (descriptor != null && descriptor.isChildrenRendered()) { int parentDepth = nodeStorage.getDepth(parent); int parentChildCount = nodeStorage.getChildCount(parent); for (Node child : event.getNodes()) { if (!descriptor.isExpanded() && nodeStorage.getChildCount(descriptor.getNode()) == 1) { setExpanded(descriptor.getNode(), true); } if (index == 0) { container.insertFirst(renderNode(child, parentDepth)); } else if (index == parentChildCount - event.getNodes().size()) { com.google.gwt.dom.client.Node lastChild = container.getLastChild(); container.insertAfter(renderNode(child, parentDepth), lastChild); } else { container.insertBefore(renderNode(child, parentDepth), container.getChild(index)); } scrollIntoView(child); } } else { redraw(parent); } } update(); if (selectionModel.getSelectedNodes().isEmpty() && autoSelect) { selectionModel.select(event.getNodes().get(0), false); } fireEvent(new NodeAddedEvent(event.getNodes())); } if (!getRootNodes().isEmpty()) { emptyStatus.paint(); } } @SuppressWarnings("unused") //temporary no need to use event parameter private void onClear(StoreClearEvent event) { clear(); } private void onDataChanged(StoreDataChangeEvent event) { redraw(event.getParent()); } private void onRemove(StoreRemoveEvent se) { NodeDescriptor nodeDescriptor = getNodeDescriptor(se.getNode()); if (nodeDescriptor != null) { if (view.getRootContainer(nodeDescriptor) != null) { nodeDescriptor.getRootContainer().removeFromParent(); } unregister(se.getNode()); for (Node child : se.getChildren()) { unregister(child); } Node parent = se.getParent(); if (parent != null) { NodeDescriptor descriptor = getNodeDescriptor(parent); if (descriptor != null && descriptor.isExpanded() && nodeStorage.getChildCount(descriptor.getNode()) == 0) { if (fireCancellableEvent(new BeforeCollapseNodeEvent(parent))) { descriptor.setExpanded(false); view.onJointChange(descriptor, Joint.COLLAPSED); fireEvent(new CollapseNodeEvent(parent)); } } moveFocus(nodeDescriptor.getRootContainer()); } } if (getRootNodes().isEmpty()) { emptyStatus.paint(); } } @SuppressWarnings("unused") //temporary no need to use event parameter private void onSort(StoreSortEvent se) { redraw(null); } private void onUpdate(StoreUpdateEvent event) { for (Node node : event.getNodes()) { NodeDescriptor nodeDescriptor = getNodeDescriptor(node); if (nodeDescriptor != null) { if (nodeDescriptor.getNode() != node) { nodeDescriptor.setNode(node); } } } } private void onRightClick(Event event) { event.preventDefault(); event.stopPropagation(); final int x = event.getClientX(); final int y = event.getClientY(); Scheduler.get().scheduleDeferred(new Scheduler.ScheduledCommand() { @Override public void execute() { getSelectionModel().fireSelectionChange(); if (contextMenuInvocationHandler != null && disableNativeContextMenu) { contextMenuInvocationHandler.onInvokeContextMenu(x, y); } } }); } private void onFocus(Event event) { fireEvent(new FocusEvent()); focused = true; } private void onBlur(Event event) { fireEvent(new BlurEvent()); focused = false; } private void onScroll(Event event) { update(); constrainFocusElement(); } private void constrainFocusElement() { if (!focusConstrainScheduled) { focusConstrainScheduled = true; Scheduler.get().scheduleFinally(new Scheduler.ScheduledCommand() { @Override public void execute() { focusConstrainScheduled = false; int scrollLeft = getElement().getScrollLeft(); int scrollTop = getElement().getScrollTop(); int left = getElement().getOffsetWidth() / 2 + scrollLeft; int top = getElement().getOffsetHeight() / 2 + scrollTop; focusEl.getStyle().setTop(top, Style.Unit.PX); focusEl.getStyle().setLeft(left, Style.Unit.PX); } }); } } private void onDoubleClick(Event event) { NodeDescriptor nodeDescriptor = getNodeDescriptor(event.getEventTarget().<Element>cast()); if (nodeDescriptor == null) { return; } if (nodeDescriptor.isLeaf()) { if (nodeDescriptor.getNode() instanceof HasAction) { ((HasAction)nodeDescriptor.getNode()).actionPerformed(); } } else { toggle(nodeDescriptor.getNode()); } } private void onClick(Event event) { NativeTreeEvent e = event.cast(); NodeDescriptor node = getNodeDescriptor((Element)event.getEventTarget().cast()); if (node != null) { Element jointEl = view.getJointContainer(node); if (jointEl != null && e.within(jointEl)) { toggle(node.getNode()); } } focus(); } private void onAfterFirstAttach() { rootContainer = getRootContainer(); getElement().getStyle().setVisibility(Style.Visibility.VISIBLE); renderChildren(null); sinkEvents(Event.ONSCROLL | Event.ONCLICK | Event.ONDBLCLICK | Event.MOUSEEVENTS | Event.KEYEVENTS); } private native Element getNearestParentElement(Element target, String selector) /*-{ function findAncestor(el, cls) { while ((el = el.parentElement) && !el.classList.contains(cls)); return el; } return findAncestor(target, selector); }-*/; /** * Describes joint element. By joint element it means * expand/collapse control, which may have one of three * state <code>collapsed</code>, <code>expanded</code> * and <code>hidden</code>. */ public enum Joint { COLLAPSED(1), EXPANDED(2), NONE(0); private int value; Joint(int value) { this.value = value; } public int value() { return value; } } /** * Handler which delegates Event.ONCONTEXTMENU to external code. */ public interface ContextMenuInvocationHandler { /** * Handle Event.ONCONTEXTMENU event. */ void onInvokeContextMenu(int x, int y); } private class Handler implements StoreAddHandler, StoreClearHandler, StoreDataChangeHandler, StoreRemoveHandler, StoreUpdateHandler, StoreSortHandler { @Override public void onAdd(StoreAddEvent event) { Tree.this.onAdd(event); } @Override public void onClear(StoreClearEvent event) { Tree.this.onClear(event); } @Override public void onDataChange(StoreDataChangeEvent event) { Tree.this.onDataChanged(event); } @Override public void onRemove(StoreRemoveEvent event) { Tree.this.onRemove(event); } @Override public void onSort(StoreSortEvent event) { Tree.this.onSort(event); } @Override public void onUpdate(StoreUpdateEvent event) { Tree.this.onUpdate(event); } } }