// Copyright 2012 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package org.eclipse.che.ide.ui.tree;
import elemental.dom.Element;
import elemental.dom.NodeList;
import elemental.events.Event;
import elemental.events.EventListener;
import elemental.events.KeyboardEvent;
import elemental.events.MouseEvent;
import elemental.js.dom.JsElement;
import com.google.gwt.core.client.Duration;
import com.google.gwt.core.client.JavaScriptObject;
import com.google.gwt.core.client.Scheduler;
import com.google.gwt.core.client.Scheduler.ScheduledCommand;
import com.google.gwt.dom.client.Node;
import com.google.gwt.dom.client.Style;
import com.google.gwt.resources.client.ClientBundle;
import com.google.gwt.resources.client.CssResource;
import com.google.gwt.user.client.Timer;
import com.google.gwt.user.client.ui.HTML;
import com.google.gwt.user.client.ui.IsWidget;
import com.google.gwt.user.client.ui.Widget;
import org.eclipse.che.ide.mvp.CompositeView;
import org.eclipse.che.ide.mvp.UiComponent;
import org.eclipse.che.ide.util.AnimationController;
import org.eclipse.che.ide.util.CssUtils;
import org.eclipse.che.ide.util.browser.BrowserUtils;
import org.eclipse.che.ide.util.dom.DomUtils;
import org.eclipse.che.ide.util.dom.Elements;
import org.eclipse.che.ide.util.dom.MouseGestureListener;
import org.eclipse.che.ide.util.input.SignalEvent;
import org.eclipse.che.ide.util.input.SignalEventImpl;
import org.vectomatic.dom.svg.ui.SVGResource;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
/**
* A tree widget that is capable of rendering any tree data structure whose node
* data type is specified in the class parameterization.
* <p/>
* Users of this widget must specify an appropriate
* {@link NodeDataAdapter} and
* {@link NodeRenderer}.
* <p/>
* The DOM structure for a tree is a recursive structure of the following form
* (note that class names will be obfuscated at runtime, and are just specified
* in human readable form for documentation purposes):
* <p/>
* <pre>
*
* <ul class="treeRoot childrenContainer">
* <li class="treeNode">
* <div class="treeNodeBody">
* <div class="expandControl"></div>
* <span class="treeNodeContents"></span>
* </div>
* <ul class="childrenContainer">
* ...
* ...
* </ul>
* </li>
* </ul>
*
* </pre>
*/
public class Tree<D> extends UiComponent<Tree.View<D>> implements IsWidget {
/** Static factory method for obtaining an instance of the Tree. */
@SuppressWarnings({"rawtypes", "unchecked"})
public static <NodeData> Tree<NodeData> create(Resources resources, NodeDataAdapter<NodeData> dataAdapter,
NodeRenderer<NodeData> nodeRenderer, final boolean multilevelSelection) {
final View view = new View(resources);
final Model<NodeData> model = new Model<NodeData>(dataAdapter, nodeRenderer, resources, multilevelSelection);
return new Tree<NodeData>(view, model);
}
public static <NodeData> Tree<NodeData> create(Resources resources, NodeDataAdapter<NodeData> dataAdapter,
NodeRenderer<NodeData> nodeRenderer) {
return create(resources, dataAdapter, nodeRenderer, false);
}
private static boolean thisIsTablet = false;
/** Css selectors applied to DOM elements in the tree. */
public interface Css extends CssResource {
String active();
String childrenContainer();
String expandControl();
String isDropTarget();
String leafIcon();
String selected();
String selectedInactive();
String treeNode();
String treeNodeBody();
String treeNodeLabel();
String treeRoot();
}
/** Listener interface for being notified about tree events. */
public interface Listener<D> {
void onNodeAction(TreeNodeElement<D> node);
void onNodeClosed(TreeNodeElement<D> node);
void onNodeContextMenu(int mouseX, int mouseY, TreeNodeElement<D> node);
void onNodeDragStart(TreeNodeElement<D> node, MouseEvent event);
void onNodeDragDrop(TreeNodeElement<D> node, MouseEvent event);
void onNodeExpanded(TreeNodeElement<D> node);
void onNodeSelected(TreeNodeElement<D> node, SignalEvent event);
void onRootContextMenu(int mouseX, int mouseY);
void onRootDragDrop(MouseEvent event);
void onKeyboard(KeyboardEvent event);
}
/** A visitor interface to visit nodes of the tree. */
public interface Visitor<D> {
/**
* @return whether to visit a given node. This is useful to prune a subtree
* from being visited
*/
boolean shouldVisit(D node);
/**
* Called for nodes that pass the {@link #shouldVisit} check.
*
* @param node
* the node being iterated
* @param willVisitChildren
* true if the given node has a child that will be
* (or has been) visited
*/
void visit(D node, boolean willVisitChildren);
}
/** Instance state for the Tree. */
public static class Model<D> {
private final NodeDataAdapter<D> dataAdapter;
private Listener<D> externalEventDelegate;
private final NodeRenderer<D> nodeRenderer;
private D root;
private final SelectionModel<D> selectionModel;
private final Resources resources;
private final AnimationController animator;
public Model(NodeDataAdapter<D> dataAdapter, NodeRenderer<D> nodeRenderer, Tree.Resources resources) {
this(dataAdapter, nodeRenderer, resources, false);
}
public Model(final NodeDataAdapter<D> dataAdapter, final NodeRenderer<D> nodeRenderer,
final Tree.Resources resources, final boolean multilevelSelection) {
this.dataAdapter = dataAdapter;
this.nodeRenderer = nodeRenderer;
this.resources = resources;
this.selectionModel = new SelectionModel<D>(dataAdapter, resources.treeCss(), multilevelSelection);
this.animator = new AnimationController.Builder().setCollapse(true).setFade(true).build();
}
public NodeDataAdapter<D> getDataAdapter() {
return dataAdapter;
}
public NodeRenderer<D> getNodeRenderer() {
return nodeRenderer;
}
public D getRoot() {
return root;
}
public void setRoot(D root) {
this.root = root;
}
}
/**
* Images and Css resources used by the Tree.
* <p/>
* In order to theme the Tree, you extend this interface and override
* {@link Tree.Resources#treeCss()}.
*/
public interface Resources extends ClientBundle {
@Source("expandedIcon.svg")
SVGResource expandedIcon();
@Source("collapsedIcon.svg")
SVGResource collapsedIcon();
// Default Stylesheet.
@Source({"org/eclipse/che/ide/ui/constants.css", "Tree.css", "org/eclipse/che/ide/api/ui/style.css"})
Css treeCss();
}
/** The view for a Tree is simply a thin wrapper around a ULElement. */
public static class View<D> extends CompositeView<ViewEvents<D>> {
/** Base event listener for DOM events fired by elements in the view. */
private abstract class TreeNodeEventListener implements EventListener {
private final boolean primaryMouseButtonOnly;
/** The {@link Duration#currentTimeMillis()} of the most recent click, or 0 */
private double previousClickMs;
private Element previousClickTreeNodeBody;
TreeNodeEventListener(boolean primaryMouseButtonOnly) {
this.primaryMouseButtonOnly = primaryMouseButtonOnly;
}
@Override
public void handleEvent(Event evt) {
// Don't even bother to do anything unless we have someone ready to
// handle events.
if (getDelegate() == null
|| (primaryMouseButtonOnly && ((MouseEvent)evt).getButton() != MouseEvent.Button.PRIMARY)) {
return;
}
Element eventTarget = (Element)evt.getTarget();
if (CssUtils.containsClassName(eventTarget, css.expandControl())) {
onExpansionControlEvent(evt, eventTarget);
} else {
Element treeNodeBody = CssUtils.getAncestorOrSelfWithClassName(eventTarget, css.treeNodeBody());
if (treeNodeBody != null) {
//this code emulate double click for tablets
if (Event.MOUSEDOWN.equals(evt.getType())) {
double currentClickMs = Duration.currentTimeMillis();
if (currentClickMs - previousClickMs < MouseGestureListener.MAX_CLICK_TIMEOUT_MS
&& treeNodeBody.equals(previousClickTreeNodeBody)) {
if (BrowserUtils.isAndroid() || BrowserUtils.isIPad() ||
BrowserUtils.isIphone()) {
evt.stopPropagation();
evt.preventDefault();
doActionsForDoubleClick(treeNodeBody, evt);
}
return;
} else {
this.previousClickMs = currentClickMs;
this.previousClickTreeNodeBody = treeNodeBody;
}
}
onTreeNodeBodyChildEvent(evt, treeNodeBody);
} else {
onOtherEvent(evt);
}
}
}
private void doActionsForDoubleClick(Element treeNodeBody, Object evt) {
SignalEvent signalEvent = SignalEventImpl.create((com.google.gwt.user.client.Event)evt, true);
// Select the node.
dispatchNodeSelectedEvent(treeNodeBody, signalEvent, css);
// Don't dispatch a node action if there is a modifier key depressed.
if (!(signalEvent.getCommandKey() || signalEvent.getShiftKey())) {
dispatchNodeActionEvent(treeNodeBody, css);
TreeNodeElement<D> node = getTreeNodeFromTreeNodeBody(treeNodeBody, css);
if (node.hasChildrenContainer()) {
dispatchExpansionEvent(node, css);
}
}
}
/**
* Catch-all that is called if the target element was not matched to an
* element rooted at the TreeNodeBody.
*/
protected void onOtherEvent(Event evt) {
}
/**
* If an event was dispatched by the TreeNodeBody, or one of its children.
* <p/>
* IMPORTANT: However, if the event target is the expansion control, do
* not call this method.
*/
protected void onTreeNodeBodyChildEvent(Event evt, Element treeNodeBody) {
}
/** If an event was dispatched by the ExpansionControl. */
protected void onExpansionControlEvent(Event evt, Element expansionControl) {
}
}
private final Tree.Css css;
@SuppressWarnings("unused")
private final Tree.Resources resources;
public View(Tree.Resources resources) {
super(Elements.createElement("ul"));
this.resources = resources;
this.css = resources.treeCss();
getElement().setTabIndex(0);
getElement().setClassName(resources.treeCss().treeRoot());
attachEventListeners();
}
void attachEventListeners() {
// There used to be a MOUSEDOWN handler with stopPropagation() and
// preventDefault() actions, but this badly affected the inline editing
// experience inside the Tree (e.g. debugger's RemoteObjectTree).
// Ok. Currently RemoteObjectTree doesn't exist anymore. So, the event can be changed on MOUSEDOWN.
getElement().addEventListener(Event.MOUSEDOWN, new TreeNodeEventListener(true) {
@Override
protected void onTreeNodeBodyChildEvent(Event evt, Element treeNodeBody) {
SignalEvent signalEvent = SignalEventImpl.create((com.google.gwt.user.client.Event)evt, true);
// Select the node.
dispatchNodeSelectedEvent(treeNodeBody, signalEvent, css);
}
@Override
protected void onExpansionControlEvent(Event evt, Element expansionControl) {
if (!CssUtils.containsClassName(expansionControl, css.leafIcon())) {
/*
* they've clicked on the expand control of a tree node that is a
* directory (so expand it)
*/
TreeNodeElement<D> treeNode =
((JsElement)expansionControl.getParentElement().getParentElement()).<TreeNodeElement<D>>cast();
dispatchExpansionEvent(treeNode, css);
}
}
}, false);
getElement().addEventListener(Event.DBLCLICK, new TreeNodeEventListener(true) {
@Override
protected void onTreeNodeBodyChildEvent(Event evt, Element treeNodeBody) {
SignalEvent signalEvent = SignalEventImpl.create((com.google.gwt.user.client.Event)evt, true);
// Select the node.
dispatchNodeSelectedEvent(treeNodeBody, signalEvent, css);
// Don't dispatch a node action if there is a modifier key depressed.
if (!(signalEvent.getCommandKey() || signalEvent.getShiftKey())) {
dispatchNodeActionEvent(treeNodeBody, css);
TreeNodeElement<D> node = getTreeNodeFromTreeNodeBody(treeNodeBody, css);
if (node.hasChildrenContainer()) {
dispatchExpansionEvent(node, css);
}
}
}
@Override
protected void onExpansionControlEvent(Event evt, Element expansionControl) {
if (!CssUtils.containsClassName(expansionControl, css.leafIcon())) {
/*
* they've clicked on the expand control of a tree node that is a
* directory (so expand it)
*/
TreeNodeElement<D> treeNode =
((JsElement)expansionControl.getParentElement().getParentElement()).<TreeNodeElement<D>>cast();
dispatchExpansionEvent(treeNode, css);
}
}
}, false);
getElement().addEventListener(Event.KEYDOWN, new TreeNodeEventListener(false) {
@Override
public void handleEvent(Event event) {
if (getDelegate() != null) {
getDelegate().onKeyBoard((KeyboardEvent)event);
}
}
}, false);
getElement().addEventListener(Event.CONTEXTMENU, new TreeNodeEventListener(false) {
@Override
public void handleEvent(Event evt) {
super.handleEvent(evt);
evt.stopPropagation();
evt.preventDefault();
}
@Override
protected void onOtherEvent(Event evt) {
MouseEvent mouseEvt = (MouseEvent)evt;
// This is a click on the root.
dispatchOnRootContextMenuEvent(mouseEvt.getClientX(), mouseEvt.getClientY());
}
@Override
protected void onTreeNodeBodyChildEvent(Event evt, Element treeNodeBody) {
MouseEvent mouseEvt = (MouseEvent)evt;
// Dispatch if eventTarget is the treeNodeBody, or if it is a child
// of a treeNodeBody.
dispatchContextMenuEvent(mouseEvt.getClientX(), mouseEvt.getClientY(), treeNodeBody, css);
}
}, false);
getElement().addEventListener(Event.FOCUS, new EventListener() {
@Override
public void handleEvent(Event event) {
if (getDelegate() != null) {
getDelegate().onFocus(event);
}
}
}, false);
getElement().addEventListener(Event.BLUR, new EventListener() {
@Override
public void handleEvent(Event event) {
if (getDelegate() != null) {
getDelegate().onBlur(event);
}
}
}, false);
}
private void dispatchContextMenuEvent(int mouseX, int mouseY, Element treeNodeBody, Css css) {
// We assume the click happened on a TreeNodeBody. We walk up one level
// to grab the treeNode element.
@SuppressWarnings("unchecked")
TreeNodeElement<D> treeNode = (TreeNodeElement<D>)treeNodeBody.getParentElement();
assert (CssUtils.containsClassName(treeNode, css.treeNode())) : "Parent of an expandControl wasn't a TreeNode!";
getDelegate().onNodeContextMenu(mouseX, mouseY, treeNode);
}
private void dispatchExpansionEvent(TreeNodeElement<D> treeNode, Css css) {
// Is the node opened or closed?
if (treeNode.isOpen()) {
getDelegate().onNodeClosed(treeNode);
} else {
// We might have set the CSS to say it is closed, but the animation
// takes a little while. As such, we check to make sure the children
// container is set to display:none before trying to dispatch an open.
// Otherwise we can get into an inconsistent state if we click really
// fast.
Element childrenContainer = treeNode.getChildrenContainer();
if (childrenContainer != null /*&& !CssUtils.isVisible(childrenContainer)*/) {
getDelegate().onNodeExpanded(treeNode);
}
}
}
private void dispatchNodeActionEvent(Element treeNodeBody, Css css) {
getDelegate().onNodeAction(getTreeNodeFromTreeNodeBody(treeNodeBody, css));
}
private void dispatchNodeSelectedEvent(Element treeNodeBody, SignalEvent evt, Css css) {
getDelegate().onNodeSelected(getTreeNodeFromTreeNodeBody(treeNodeBody, css), evt);
}
private void dispatchOnRootContextMenuEvent(int mouseX, int mouseY) {
getDelegate().onRootContextMenu(mouseX, mouseY);
}
private TreeNodeElement<D> getTreeNodeFromTreeNodeBody(Element treeNodeBody, Css css) {
TreeNodeElement<D> treeNode = ((JsElement)treeNodeBody.getParentElement()).<TreeNodeElement<D>>cast();
assert (CssUtils.containsClassName(treeNode, css.treeNode())) : "Unexpected element when looking for tree node: "
+ treeNode.toString();
return treeNode;
}
}
/**
* Logical events sourced by the Tree's View. Note that these events get
* dispatched synchronously in our DOM event handlers.
*/
private interface ViewEvents<D> {
void onNodeAction(TreeNodeElement<D> node);
void onNodeClosed(TreeNodeElement<D> node);
void onNodeContextMenu(int mouseX, int mouseY, TreeNodeElement<D> node);
void onDragDropEvent(MouseEvent event);
void onNodeExpanded(TreeNodeElement<D> node);
void onNodeSelected(TreeNodeElement<D> node, SignalEvent event);
void onRootContextMenu(int mouseX, int mouseY);
void onRootDragDrop(MouseEvent event);
void onKeyBoard(KeyboardEvent event);
void onFocus(Event event);
void onBlur(Event event);
}
private class DragDropController {
private TreeNodeElement<D> targetNode;
private boolean hadDragEnterEvent;
private final ScheduledCommand hadDragEnterEventResetter = new ScheduledCommand() {
@Override
public void execute() {
hadDragEnterEvent = false;
}
};
private final Timer hoverToExpandTimer = new Timer() {
@Override
public void run() {
expandNode(targetNode, true, true);
}
};
void handleDragDropEvent(MouseEvent evt) {
final D rootData = getModel().root;
final NodeDataAdapter<D> dataAdapter = getModel().getDataAdapter();
final Css css = getModel().resources.treeCss();
@SuppressWarnings("unchecked")
TreeNodeElement<D> node =
(TreeNodeElement<D>)CssUtils.getAncestorOrSelfWithClassName((Element)evt.getTarget(), css.treeNode());
D newTargetData = node != null ? dataAdapter.getDragDropTarget(node.getData()) : rootData;
if (newTargetData == null) {
return;
}
TreeNodeElement<D> newTargetNode = dataAdapter.getRenderedTreeNode(newTargetData);
String type = evt.getType();
if (Event.DRAGSTART.equals(type)) {
if (getModel().externalEventDelegate != null) {
D sourceData = node != null ? node.getData() : rootData;
// TODO support multiple folder selection.
// We do not support dragging without any folder/file selection.
if (sourceData != rootData) {
TreeNodeElement<D> sourceNode = dataAdapter.getRenderedTreeNode(sourceData);
getModel().externalEventDelegate.onNodeDragStart(sourceNode, evt);
}
}
return;
}
if (Event.DROP.equals(type)) {
if (getModel().externalEventDelegate != null) {
if (newTargetData == rootData) {
getModel().externalEventDelegate.onRootDragDrop(evt);
} else {
getModel().externalEventDelegate.onNodeDragDrop(newTargetNode, evt);
}
}
clearDropTarget();
} else if (Event.DRAGOVER.equals(type)) {
if (newTargetNode != targetNode) {
clearDropTarget();
if (newTargetNode != null) {
// Highlight the node by setting its drop target property
targetNode = newTargetNode;
targetNode.setIsDropTarget(true, css);
if (dataAdapter.hasChildren(newTargetData) && !targetNode.isOpen()) {
hoverToExpandTimer.schedule(HOVER_TO_EXPAND_DELAY_MS);
}
}
}
} else if (Event.DRAGLEAVE.equals(type)) {
if (!hadDragEnterEvent) {
// This wasn't part of a DRAGENTER-DRAGLEAVE pair (see below)
clearDropTarget();
}
} else if (Event.DRAGENTER.equals(type)) {
/*
* DRAGENTER comes before DRAGLEAVE, and a deferred command scheduled
* here will execute after the DRAGLEAVE. We use hadDragEnter to track a
* paired DRAGENTER-DRAGLEAVE so that we can cleanup when we get an
* unpaired DRAGLEAVE.
*/
hadDragEnterEvent = true;
Scheduler.get().scheduleDeferred(hadDragEnterEventResetter);
}
evt.preventDefault();
evt.stopPropagation();
}
private void clearDropTarget() {
hoverToExpandTimer.cancel();
if (targetNode != null) {
targetNode.setIsDropTarget(false, getModel().resources.treeCss());
targetNode = null;
}
}
}
private final DragDropController dragDropController = new DragDropController();
private HTML widget;
/** Handles logical events sourced by the View. */
private final ViewEvents<D> viewEventHandler = new ViewEvents<D>() {
@Override
public void onNodeAction(final TreeNodeElement<D> node) {
selectSingleNode(node, true);
}
@Override
public void onNodeClosed(TreeNodeElement<D> node) {
closeNode(node, true);
}
@Override
public void onNodeContextMenu(int mouseX, int mouseY, TreeNodeElement<D> node) {
// Select the node the first
getModel().selectionModel.contextSelect(node.getData());
// Display context menu
if (getModel().externalEventDelegate != null) {
getModel().externalEventDelegate.onNodeContextMenu(mouseX, mouseY, node);
}
}
@Override
public void onDragDropEvent(MouseEvent event) {
dragDropController.handleDragDropEvent(event);
}
@Override
public void onNodeExpanded(TreeNodeElement<D> node) {
expandNode(node, true, true);
}
@Override
public void onNodeSelected(TreeNodeElement<D> node, SignalEvent event) {
getSelectionModel().setTreeActive(true);
selectNode(node.getData(), event, true);
}
@Override
public void onRootContextMenu(int mouseX, int mouseY) {
if (getModel().externalEventDelegate != null) {
getModel().externalEventDelegate.onRootContextMenu(mouseX, mouseY);
}
}
@Override
public void onRootDragDrop(MouseEvent event) {
if (getModel().externalEventDelegate != null) {
getModel().externalEventDelegate.onRootDragDrop(event);
}
}
@Override
public void onKeyBoard(KeyboardEvent event) {
if (event.getKeyCode() == KeyboardEvent.KeyCode.UP) {
event.stopPropagation();
event.preventDefault();
upArrowPressed();
} else if (event.getKeyCode() == KeyboardEvent.KeyCode.DOWN) {
event.stopPropagation();
event.preventDefault();
downArrowPressed();
} else if (event.getKeyCode() == KeyboardEvent.KeyCode.HOME) {
event.stopPropagation();
event.preventDefault();
homePressed();
} else if (event.getKeyCode() == KeyboardEvent.KeyCode.END) {
event.stopPropagation();
event.preventDefault();
endPressed();
} else if (event.getKeyCode() == KeyboardEvent.KeyCode.PAGE_UP) {
event.stopPropagation();
event.preventDefault();
pageUpPressed();
} else if (event.getKeyCode() == KeyboardEvent.KeyCode.PAGE_DOWN) {
event.stopPropagation();
event.preventDefault();
pageDownPressed();
} else if (event.getKeyCode() == KeyboardEvent.KeyCode.ENTER) {
enterPressed(event);
} else if (event.getKeyCode() == KeyboardEvent.KeyCode.RIGHT) {
event.stopPropagation();
event.preventDefault();
rightArrowPressed();
} else if (event.getKeyCode() == KeyboardEvent.KeyCode.LEFT) {
event.stopPropagation();
event.preventDefault();
leftArrowPressed();
} else if (getModel().externalEventDelegate != null) {
getModel().externalEventDelegate.onKeyboard(event);
}
}
@Override
public void onFocus(Event event) {
getSelectionModel().updateSelection(true);
}
@Override
public void onBlur(Event event) {
getSelectionModel().updateSelection(false);
}
};
private static final int HOVER_TO_EXPAND_DELAY_MS = 500;
private final Tree.Model<D> treeModel;
/** Constructor. */
public Tree(View<D> view, Model<D> model) {
super(view);
this.treeModel = model;
getView().setDelegate(viewEventHandler);
}
public Tree.Model<D> getModel() {
return treeModel;
}
/**
* Selects a node in the tree and auto expands the tree to this node.
*
* @param nodeData
* the node we want to select and expand to.
* @param dispatchNodeAction
* whether or not to notify listeners of the node
* action for the selected node.
*/
public void autoExpandAndSelectNode(D nodeData, boolean dispatchNodeAction) {
// Expand the tree to the selected element.
expandPathRecursive(getModel().root, getModel().dataAdapter.getNodePath(nodeData), false);
// By now the node should have a rendered element.
TreeNodeElement<D> renderedNode = getModel().dataAdapter.getRenderedTreeNode(nodeData);
assert (renderedNode != null) : "Expanded selection has a null rendered node!";
selectSingleNode(renderedNode, dispatchNodeAction);
}
/**
* Selects a node and dispatches event to perform actions on this node.
*
* @param node node to select
* @param dispatchNodeAction dispatch action or not
*/
private void selectSingleNode(TreeNodeElement<D> node, boolean dispatchNodeAction) {
getModel().selectionModel.selectSingleNode(node.getData());
scrollToSelectedElement();
maybeNotifyNodeActionExternal(node, dispatchNodeAction);
}
/**
* Selects single node and notifies about selecting the node.
*
* @param node node to select
*/
private void selectSingleNode(TreeNodeElement<D> node) {
getModel().selectionModel.selectSingleNode(node.getData());
scrollToSelectedElement();
if (getModel().externalEventDelegate != null) {
SignalEvent event = SignalEventImpl.DEFAULT_FACTORY.create();
getModel().externalEventDelegate.onNodeSelected(node, event);
}
}
/**
* Selects single node and dispatches an event about selecting the node.
*
* @param node
* @param event
* @param dispatchNodeSelected
*/
private void selectNode(D node, SignalEvent event, boolean dispatchNodeSelected) {
getModel().selectionModel.selectNode(node, event);
scrollToSelectedElement();
if (dispatchNodeSelected && getModel().externalEventDelegate != null) {
TreeNodeElement<D> renderedNode = getModel().dataAdapter.getRenderedTreeNode(node);
getModel().externalEventDelegate.onNodeSelected(renderedNode, event);
}
}
/**
* Scrolls tree to selected element.
*/
private void scrollToSelectedElement() {
if (!getSelectionModel().getSelectedNodes().isEmpty()) {
D selected = getSelectionModel().getSelectedNodes().get(0);
TreeNodeElement<D> selectedTreeNodeElement = getModel().dataAdapter.getRenderedTreeNode(selected);
scrollToElement(asWidget().getElement(), selectedTreeNodeElement.getFirstChild());
}
}
/**
* Scrolls tree to specified row.
*
* @param tree tree element
* @param element row element
*/
private native void scrollToElement(JavaScriptObject tree, JavaScriptObject element) /*-{
var maxTop = tree.getBoundingClientRect().top + tree.clientHeight;
var elemTop = element.getBoundingClientRect().top;
var elemHeight = element.getBoundingClientRect().height;
if (elemTop + elemHeight > maxTop) {
var diffHeight = elemTop + elemHeight - maxTop;
tree.scrollTop += diffHeight;
return;
}
if (element.getBoundingClientRect().top < tree.getBoundingClientRect().top) {
tree.scrollTop -= tree.getBoundingClientRect().top - element.getBoundingClientRect().top;
}
}-*/;
/**
* Creates a {@link TreeNodeElement}. This does NOT attach said node to the
* tree. You have to do that manually with {@link TreeNodeElement#addChild}.
*/
public TreeNodeElement<D> createNode(D nodeData) {
return TreeNodeElement.create(nodeData, getModel().dataAdapter, getModel().nodeRenderer,
getModel().resources.treeCss(), getModel().resources);
}
/** @see: {@link #expandNode(TreeNodeElement, boolean, boolean)}. */
public void expandNode(TreeNodeElement<D> treeNode) {
expandNode(treeNode, false, false);
}
/**
* Expands a {@link TreeNodeElement} and renders its children if it "needs
* to". "Needs to" is defined as whether or not the children have never been
* rendered before, or if size of the set of rendered children differs from
* the size of children in the underlying model.
*
* @param treeNode
* the {@link TreeNodeElement} we are expanding.
* @param shouldAnimate
* whether to animate the expansion
* @param dispatchNodeExpanded
* whether or not to notify listeners of the node
* expansion
*/
private void expandNode(TreeNodeElement<D> treeNode, boolean shouldAnimate, boolean dispatchNodeExpanded) {
// This is most likely because someone tried to expand root. Ignore it.
if (treeNode == null) {
return;
}
NodeDataAdapter<D> dataAdapter = getModel().dataAdapter;
// Nothing to do here.
if (!dataAdapter.hasChildren(treeNode.getData())) {
return;
}
// Ensure that the node's children container is birthed.
treeNode.ensureChildrenContainer(dataAdapter, getModel().resources.treeCss());
List<D> children = dataAdapter.getChildren(treeNode.getData());
// Maybe render it's children if they aren't already rendered.
if (treeNode.getChildrenContainer().getChildren().getLength() != children.size()) {
// Then the model has not been correctly reflected in the UI.
// Blank the children and render a single level for each.
treeNode.getChildrenContainer().setInnerHTML("");
for (int i = 0, n = children.size(); i < n; i++) {
renderRecursive(treeNode.getChildrenContainer(), children.get(i), 0);
}
}
// Render the node as being opened after the children have been added, so that
// AnimationController can correctly measure the height of the child container.
treeNode.openNode(dataAdapter, getModel().resources.treeCss(), getModel().animator, shouldAnimate);
// Notify listeners of the event.
if (dispatchNodeExpanded && getModel().externalEventDelegate != null) {
getModel().externalEventDelegate.onNodeExpanded(treeNode);
}
}
public void closeNode(TreeNodeElement<D> treeNode) {
closeNode(treeNode, false);
}
private void closeNode(TreeNodeElement<D> treeNode, boolean dispatchNodeClosed) {
if (!treeNode.isOpen()) {
return;
}
treeNode.closeNode(getModel().dataAdapter, getModel().resources.treeCss(), getModel().animator, true);
if (dispatchNodeClosed && getModel().externalEventDelegate != null) {
getModel().externalEventDelegate.onNodeClosed(treeNode);
}
}
/**
* Takes in a list of paths relative to the root, that correspond to nodes in
* the tree that need to be expanded.
* <p/>
* <p>This will try to expand all the given paths recursively, and return the
* array of paths that could not be fully expanded, i.e. when the leaf that
* the path points to was not found in the tree. In these cases all the middle
* nodes that were found in the tree will be expanded though.
* <p/>
* <p>The returned array of not expanded paths may be used to save and restore
* the expansion history.
*
* @param paths
* array of paths to expand
* @param dispatchNodeExpanded
* whether to dispatch the NodeExpanded event
* @return array of paths that were not expanded, or were partially expanded
*/
public List<List<String>> expandPaths(List<List<String>> paths, boolean dispatchNodeExpanded) {
List<List<String>> notExpanded = new ArrayList<>();
for (int i = 0, n = paths.size(); i < n; i++) {
if (!expandPathRecursive(getModel().root, paths.get(i), dispatchNodeExpanded)) {
notExpanded.add(paths.get(i));
}
}
return notExpanded;
}
/**
* Gets the associated {@link TreeNodeElement} for a given nodeData.
* <p/>
* If there is no such node rendered in the tree, then {@code null} is
* returned.
*/
public TreeNodeElement<D> getNode(D nodeData) {
return getModel().getDataAdapter().getRenderedTreeNode(nodeData);
}
public Tree.Resources getResources() {
return getModel().resources;
}
public SelectionModel<D> getSelectionModel() {
return getModel().selectionModel;
}
/**
* Removes a node from the DOM. Does not mutate the the underlying model. That
* should be already done before calling this method.
*/
public void removeNode(TreeNodeElement<D> node) {
if (node == null) {
return;
}
// Remove from the DOM
node.removeFromTree();
// Notify the selection model in case it was selected.
getModel().selectionModel.removeNode(node.getData());
}
/** Renders the entire tree starting with the root node. */
public void renderTree() {
renderTree(-1);
}
/**
* Renders the tree starting with the root node up until the specified depth.
* <p/>
* This will NOT restore any expansions. If you want to re-render the tree
* obeying previous expansions then,
*
* @param depth
* integer indicating how deep we should auto-expand. -1 means
* render the entire tree.
*/
public void renderTree(int depth) {
// Clear the current view.
Element rootElement = getView().getElement();
rootElement.setInnerHTML("");
rootElement.setAttribute("___depth", "0");
// If the root is not set, we have nothing to render.
D root = getModel().root;
if (root == null) {
return;
}
// Root is special in that we don't render a directory for it. Only its
// children.
List<D> children = getModel().dataAdapter.getChildren(root);
for (int i = 0, n = children.size(); i < n; i++) {
renderRecursive(rootElement, children.get(i), depth);
}
}
/**
* Replaces the old node in the tree with data representing the subtree rooted
* where the old node used to be if the old node was rendered.
* <p/>
* <p>{@code oldSubtreeData} and {@code incomingSubtreeData} are allowed to be
* the same node (it will simply get re-rendered).
* <p/>
* <p>This methods also tries to preserve the original expansion state. Any
* path that was expanded before executing this method but could not be
* expanded after replacing the subtree, will be returned in the result array,
* so that it could be expanded later using the {@link #expandPaths} method,
* if needed (for example, if children of the tree are getting populated
* asynchronously).
*
* @param shouldAnimate
* if true, the subtree will animate open if it is still open
* @return array paths that could not be expanded in the new subtree
*/
public List<List<String>> replaceSubtree(D oldSubtreeData, D incomingSubtreeData, boolean shouldAnimate) {
// Gather paths that were expanded in this subtree so that we can restore
// them later after rendering.
List<List<String>> expandedPaths = gatherExpandedPaths(oldSubtreeData);
boolean wasRoot = (oldSubtreeData == getModel().root);
TreeNodeElement<D> oldRenderedNode = null;
TreeNodeElement<D> newRenderedNode = null;
if (wasRoot) {
// We are rendering root! Just render it from the top. We will restore the
// expansion later.
getModel().setRoot(incomingSubtreeData);
renderTree(0);
} else {
oldRenderedNode = getModel().dataAdapter.getRenderedTreeNode(oldSubtreeData);
// If the node does not have a rendered node, then we have nothing to do.
if (oldRenderedNode == null) {
expandedPaths.clear();
return expandedPaths;
}
JsElement parentElem = oldRenderedNode.getParentElement();
// The old node may have been moved from a rendered to a non-rendered
// state (e.g., into a collapsed folder). In that case, it doesn't have a
// parent, and we're done here.
if (parentElem == null) {
expandedPaths.clear();
return expandedPaths;
}
// Make a new tree node.
newRenderedNode = createNode(incomingSubtreeData);
parentElem.insertBefore(newRenderedNode, oldRenderedNode);
newRenderedNode.updateLeafOffset(parentElem);
// Remove the old rendered node from the tree.
DomUtils.removeFromParent(oldRenderedNode);
}
// If the old node was the root, or if it and its parents were expanded, then we should
// attempt to restore expansion.
boolean shouldExpand = wasRoot;
if (!wasRoot && oldRenderedNode != null) {
shouldExpand = true;
TreeNodeElement<D> curNode = oldRenderedNode;
while (curNode != null) {
if (!curNode.isOpen()) {
// One of the parents is closed, so we should not expand all paths.
shouldExpand = false;
break;
}
D parentData = getModel().dataAdapter.getParent(curNode.getData());
curNode = (parentData == null) ? null : getModel().dataAdapter.getRenderedTreeNode(parentData);
}
}
if (shouldExpand) {
// Animate the top node if it was open. If we should not animate, the newRenderedNode will
// still be expanded by the call to expandPaths() below.
if (shouldAnimate && newRenderedNode != null) {
expandNode(newRenderedNode, true, true);
}
// But if it is open, we need to attempt to restore the expansion.
expandedPaths = expandPaths(expandedPaths, true);
} else {
expandedPaths.clear();
}
// TODO: Be more surgical about restoring the selection model. We
// are currently recomputing all selected nodes.
List<List<String>> selectedPaths = getModel().selectionModel.computeSelectedPaths();
restoreSelectionModel(selectedPaths);
return expandedPaths;
}
/**
* Populates the selection model from a list of selected paths if they
* resolve to nodes in the data model.
*/
private void restoreSelectionModel(List<List<String>> selectedPaths) {
getModel().selectionModel.clearSelections();
for (int i = 0, n = selectedPaths.size(); i < n; i++) {
D node = getModel().dataAdapter.getNodeByPath(getModel().root, selectedPaths.get(i));
if (node != null) {
selectNode(node, null, true);
}
}
}
/**
* Receive callbacks for node expansion and node selection.
*
* @param externalEventDelegate
* The {@link ViewEvents} that will handle the
* events.
*/
public void setTreeEventHandler(Listener<D> externalEventDelegate) {
getModel().externalEventDelegate = externalEventDelegate;
}
/**
* Gathers all visible nodes of subtree.
*
* @param node subtree parent
* @return array containing all visible nodes of subtree
*/
public List<TreeNodeElement<D>> getVisibleTreeNodes(TreeNodeElement<D> node) {
List<TreeNodeElement<D>> nodes = new ArrayList<>();
nodes.add(node);
if (node.isOpen() && node.hasChildNodes()) {
NodeList children = node.getChildrenContainer().getChildNodes();
for (int ci = 0; ci < children.getLength(); ci++) {
TreeNodeElement<D> child = (TreeNodeElement<D>)children.item(ci);
nodes.addAll(getVisibleTreeNodes(child));
}
}
return nodes;
}
/**
* Gathers all visible nodes of the tree.
*
* @return array containing all visible nodes of the tree
*/
public List<TreeNodeElement<D>> getVisibleTreeNodes() {
List<TreeNodeElement<D>> nodes = new ArrayList<>();
List<D> rootItems = getModel().dataAdapter.getChildren(getModel().getRoot());
for (int i = 0; i < rootItems.size(); i++) {
TreeNodeElement<D> rootTreeNode = getModel().dataAdapter.getRenderedTreeNode(rootItems.get(i));
nodes.addAll(getVisibleTreeNodes(rootTreeNode));
}
return nodes;
}
/**
* Handles pressing Up arrow button.
*/
public void upArrowPressed() {
if (getModel().getRoot() == null ||
getSelectionModel().getSelectedNodes().isEmpty() ||
getModel().dataAdapter.getChildren(getModel().getRoot()).isEmpty()) {
return;
}
D selected = getSelectionModel().getSelectedNodes().get(0);
TreeNodeElement<D> selectedTreeNodeElement = getModel().dataAdapter.getRenderedTreeNode(selected);
List<TreeNodeElement<D>> visibleTreeNodes = getVisibleTreeNodes();
for (int i = 0; i < visibleTreeNodes.size(); i++) {
TreeNodeElement<D> treeNode = visibleTreeNodes.get(i);
if (treeNode == selectedTreeNodeElement) {
if (i > 0) {
selectSingleNode(visibleTreeNodes.get(i - 1));
}
return;
}
}
}
/**
* Handles pressing Down arrow button.
*/
public void downArrowPressed() {
if (getModel().getRoot() == null ||
getSelectionModel().getSelectedNodes().isEmpty() ||
getModel().dataAdapter.getChildren(getModel().getRoot()).isEmpty()) {
return;
}
D selected = getSelectionModel().getSelectedNodes().get(0);
TreeNodeElement<D> selectedTreeNodeElement = getModel().dataAdapter.getRenderedTreeNode(selected);
List<TreeNodeElement<D>> visibleTreeNodes = getVisibleTreeNodes();
for (int i = 0; i < visibleTreeNodes.size(); i++) {
TreeNodeElement<D> treeNode = visibleTreeNodes.get(i);
if (treeNode == selectedTreeNodeElement) {
if (i < visibleTreeNodes.size() - 1) {
selectSingleNode(visibleTreeNodes.get(i + 1));
}
return;
}
}
}
/**
* Selects the root element when pressing HOME button.
*/
public void homePressed() {
if (getModel().getRoot() == null ||
getSelectionModel().getSelectedNodes().isEmpty() ||
getModel().dataAdapter.getChildren(getModel().getRoot()).isEmpty()) {
return;
}
D project = getModel().dataAdapter.getChildren(getModel().getRoot()).get(0);
TreeNodeElement<D> projectTreeNode = getModel().dataAdapter.getRenderedTreeNode(project);
selectSingleNode(projectTreeNode);
}
/**
* Selects last element when pressing END button.
*/
public void endPressed() {
if (getModel().getRoot() == null ||
getSelectionModel().getSelectedNodes().isEmpty() ||
getModel().dataAdapter.getChildren(getModel().getRoot()).isEmpty()) {
return;
}
List<TreeNodeElement<D>> visibleTreeNodes = getVisibleTreeNodes();
selectSingleNode(visibleTreeNodes.get(visibleTreeNodes.size() - 1));
}
/**
* Handles the pressing Page Up button.
*/
public void pageUpPressed() {
if (getModel().getRoot() == null ||
getSelectionModel().getSelectedNodes().isEmpty() ||
getModel().dataAdapter.getChildren(getModel().getRoot()).isEmpty()) {
return;
}
D selected = getSelectionModel().getSelectedNodes().get(0);
TreeNodeElement<D> selectedTreeNodeElement = getModel().dataAdapter.getRenderedTreeNode(selected);
int rowHeight = selectedTreeNodeElement.getSelectionElement().getOffsetHeight();
int index = -1;
List<TreeNodeElement<D>> visibleTreeNodes = getVisibleTreeNodes();
for (int i = 0; i < visibleTreeNodes.size(); i++) {
TreeNodeElement<D> treeNode = visibleTreeNodes.get(i);
if (treeNode == selectedTreeNodeElement) {
index = i;
break;
}
}
if (index <= 0) {
return;
}
int visibleAreaHeight = asWidget().getElement().getClientHeight();
int visibleRows = visibleAreaHeight / rowHeight;
if (index > visibleRows) {
selectSingleNode(visibleTreeNodes.get(index - visibleRows));
} else {
selectSingleNode(visibleTreeNodes.get(0));
}
}
/**
* Handles the pressing Page Up button.
*/
public void pageDownPressed() {
if (getModel().getRoot() == null ||
getSelectionModel().getSelectedNodes().isEmpty() ||
getModel().dataAdapter.getChildren(getModel().getRoot()).isEmpty()) {
return;
}
D selected = getSelectionModel().getSelectedNodes().get(0);
TreeNodeElement<D> selectedTreeNodeElement = getModel().dataAdapter.getRenderedTreeNode(selected);
int rowHeight = selectedTreeNodeElement.getSelectionElement().getOffsetHeight();
int index = -1;
List<TreeNodeElement<D>> visibleTreeNodes = getVisibleTreeNodes();
for (int i = 0; i < visibleTreeNodes.size(); i++) {
TreeNodeElement<D> treeNode = visibleTreeNodes.get(i);
if (treeNode == selectedTreeNodeElement) {
index = i;
break;
}
}
if (index < 0) {
return;
}
int visibleAreaHeight = asWidget().getElement().getClientHeight();
int visibleRows = visibleAreaHeight / rowHeight;
if (index + visibleRows < visibleTreeNodes.size()) {
selectSingleNode(visibleTreeNodes.get(index + visibleRows));
} else {
selectSingleNode(visibleTreeNodes.get(visibleTreeNodes.size() - 1));
}
}
/**
* Handles pressing the Enter button.
* Expands or collapses a folder or opens a file.
*/
public void enterPressed(KeyboardEvent event) {
if (getModel().getRoot() == null ||
getSelectionModel().getSelectedNodes().isEmpty() ||
getModel().dataAdapter.getChildren(getModel().getRoot()).isEmpty()) {
return;
}
D selected = getSelectionModel().getSelectedNodes().get(0);
TreeNodeElement<D> selectedTreeNodeElement = getModel().dataAdapter.getRenderedTreeNode(selected);
if (selectedTreeNodeElement.hasChildrenContainer()) {
if (selectedTreeNodeElement.isOpen()) {
closeNode(selectedTreeNodeElement, true);
} else {
// Open the folder
expandNode(selectedTreeNodeElement, true, true);
}
} else {
if (getModel().externalEventDelegate != null) {
getModel().externalEventDelegate.onKeyboard(event);
}
}
}
/**
* Handles pressing the Right arrow button.
* Does nothing when user selected a file. Expands a folder if the user has selected one.
* Selects the first child if the folder is already selected and expanded.
*/
public void rightArrowPressed() {
if (getModel().getRoot() == null ||
getSelectionModel().getSelectedNodes().isEmpty() ||
getModel().dataAdapter.getChildren(getModel().getRoot()).isEmpty()) {
return;
}
D selected = getSelectionModel().getSelectedNodes().get(0);
TreeNodeElement<D> selectedTreeNodeElement = getModel().dataAdapter.getRenderedTreeNode(selected);
if (selectedTreeNodeElement.hasChildrenContainer()) {
if (selectedTreeNodeElement.isOpen()) {
// Select the first child
NodeList children = selectedTreeNodeElement.getChildrenContainer().getChildNodes();
if (children.getLength() > 0) {
TreeNodeElement<D> firstChild = (TreeNodeElement<D>)children.item(0);
selectSingleNode(firstChild);
}
} else {
// Open the folder
expandNode(selectedTreeNodeElement, true, true);
}
}
}
/**
* Handles pressing the Left arrow button.
* Closes the folder if it's selected and opened, otherwise selects parent.
*/
public void leftArrowPressed() {
if (getModel().getRoot() == null ||
getSelectionModel().getSelectedNodes().isEmpty() ||
getModel().dataAdapter.getChildren(getModel().getRoot()).isEmpty()) {
return;
}
D selected = getSelectionModel().getSelectedNodes().get(0);
TreeNodeElement<D> selectedTreeNodeElement = getModel().dataAdapter.getRenderedTreeNode(selected);
if (selectedTreeNodeElement.isOpen()) {
closeNode(selectedTreeNodeElement, true);
} else {
D project = getModel().dataAdapter.getChildren(getModel().getRoot()).get(0);
TreeNodeElement<D> projectTreeNode = getModel().dataAdapter.getRenderedTreeNode(project);
if (selectedTreeNodeElement != projectTreeNode) {
TreeNodeElement<D> parentTreeNode = (TreeNodeElement<D>)selectedTreeNodeElement.getParentElement().getParentElement();
if (parentTreeNode.getData() == null) {
return;
}
selectSingleNode(parentTreeNode);
}
}
}
private boolean expandPathRecursive(D expandedParentNode, List<String> pathToExpand, boolean dispatchNodeExpanded) {
if (expandedParentNode == null) {
return false;
}
NodeDataAdapter<D> dataAdapter = getModel().dataAdapter;
D previousParentNode = expandedParentNode;
for (int pathIndex = 0; pathIndex < pathToExpand.size(); ++pathIndex) {
if (!getModel().dataAdapter.hasChildren(previousParentNode)) {
// Consider this path expanded, even if some path components are left.
return true;
}
// The root is already expanded by default. So we really want to recur the
// child that matches the first component.
List<D> children = getModel().dataAdapter.getChildren(previousParentNode);
previousParentNode = null;
for (int i = 0, n = children.size(); i < n; ++i) {
D child = children.get(i);
if (dataAdapter.getNodeId(child).equals(pathToExpand.get(pathIndex))) {
// We have a match. Look up the rendered element. The parent should
// already be expanded, so this must exist.
TreeNodeElement<D> renderedNode = dataAdapter.getRenderedTreeNode(child);
assert (renderedNode != null);
// If this node is not open, then we open it.
if (!renderedNode.isOpen()) {
expandNode(renderedNode, false, dispatchNodeExpanded);
}
// Continue to expand the remainder of the path.
previousParentNode = child;
break;
}
}
if (previousParentNode == null) {
// The path was only partially expanded.
return false;
}
}
return true;
}
/**
* Walks the tree rooted at the specified renderedNode and gathers a list of
* paths that correspond to nodes that have been expanded below the specified
* rendered node. All paths are expressed as root relative.
* <p/>
* These paths correspond to "expansion leaves". Which are effectively nodes
* whose children are all leaves, or are all collapsed. That is, nodes whose
* children all answer false to {@link TreeNodeElement#isOpen()}.
*/
private List<List<String>> gatherExpandedPaths(D rootData) {
final List<List<String>> expandedPaths = new ArrayList<>();
// Can't gather the expansion state for a null parent.
if (rootData == null) {
return expandedPaths;
}
iterateDfs(rootData, getModel().dataAdapter, new Visitor<D>() {
@Override
public boolean shouldVisit(D node) {
// If a child node is open, it means that it has been expanded and its
// children should have rendered nodes.
TreeNodeElement<D> renderedChild = getModel().dataAdapter.getRenderedTreeNode(node);
return (renderedChild != null) && renderedChild.isOpen();
}
@Override
public void visit(D node, boolean willVisitChildren) {
if (!willVisitChildren) {
// This node is an expansion leaf. Accumulate the path.
expandedPaths.add(getModel().dataAdapter.getNodePath(node));
}
}
});
return expandedPaths;
}
/**
* Recursively iterates children of a given root node using DFS.
*
* @param rootData
* root node to start the iteration from
* @param dataAdapter
* data adapter to get the children of a node
* @param callback
* iteration callback
*/
public static <D> void iterateDfs(D rootData, NodeDataAdapter<D> dataAdapter, Visitor<D> callback) {
LinkedList<D> nodes = new LinkedList<>();
nodes.add(rootData);
// Iterative DFS.
while (!nodes.isEmpty()) {
D parentNodeData = nodes.pop();
boolean willVisitChildren = false;
List<D> children = dataAdapter.getChildren(parentNodeData);
for (int i = 0, n = children.size(); i < n; i++) {
D child = children.get(i);
if (callback.shouldVisit(child)) {
// Add a filtered child to the stack of the nodes to visit.
nodes.add(child);
willVisitChildren = true;
}
}
callback.visit(parentNodeData, willVisitChildren);
}
}
private void maybeNotifyNodeActionExternal(TreeNodeElement<D> renderedNode, boolean dispatchNodeAction) {
if (dispatchNodeAction && getModel().externalEventDelegate != null) {
getModel().externalEventDelegate.onNodeAction(renderedNode);
}
}
private void renderRecursive(Element parentContainer, D nodeData, int depth) {
NodeDataAdapter<D> dataAdapter = getModel().dataAdapter;
Tree.Css css = getResources().treeCss();
// Make the node.
TreeNodeElement<D> newNode = createNode(nodeData);
parentContainer.appendChild(newNode);
newNode.updateLeafOffset(parentContainer);
// If we reach depth 0, we stop the recursion.
if (depth == 0 || !newNode.hasChildrenContainer()) {
if (dataAdapter.hasChildren(nodeData)) {
newNode.closeNode(dataAdapter, css, getModel().animator, false);
}
return;
}
// Maybe continue the expansion.
newNode.openNode(dataAdapter, css, getModel().animator, false);
List<D> children = dataAdapter.getChildren(nodeData);
for (int i = 0, n = children.size(); i < n; i++) {
renderRecursive(newNode.getChildrenContainer(), children.get(i), depth - 1);
}
}
/**
* Returns the tree node whose element is or contains the given element, or
* null if the given element cannot be matched to a tree node.
*/
public TreeNodeElement<D> getNodeFromElement(Element element) {
Css css = getModel().resources.treeCss();
Element treeNodeBody = CssUtils.getAncestorOrSelfWithClassName(element, css.treeNodeBody());
return treeNodeBody != null ? getView().getTreeNodeFromTreeNodeBody(treeNodeBody, css) : null;
}
/** {@inheritDoc} */
@Override
public Widget asWidget() {
if (widget == null) {
widget = new HTML();
Element element = getView().getElement();
widget.getElement().appendChild((Node)element);
widget.getElement().getStyle().setOverflow(Style.Overflow.AUTO);
}
return widget;
}
}