Package com.google.collide.client.ui.tree

Source Code of com.google.collide.client.ui.tree.Tree$DragDropController

// 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 com.google.collide.client.ui.tree;

import com.google.collide.client.util.AnimationController;
import com.google.collide.client.util.CssUtils;
import com.google.collide.client.util.Elements;
import com.google.collide.client.util.dom.MouseGestureListener;
import com.google.collide.json.shared.JsonArray;
import com.google.collide.mvp.CompositeView;
import com.google.collide.mvp.UiComponent;
import com.google.collide.shared.util.JsonCollections;
import com.google.gwt.core.client.Duration;
import com.google.gwt.core.client.Scheduler;
import com.google.gwt.core.client.Scheduler.ScheduledCommand;
import com.google.gwt.resources.client.ClientBundle;
import com.google.gwt.resources.client.CssResource;
import com.google.gwt.resources.client.ImageResource;
import com.google.gwt.user.client.Timer;

import org.waveprotocol.wave.client.common.util.SignalEvent;
import org.waveprotocol.wave.client.common.util.SignalEventImpl;

import elemental.events.Event;
import elemental.events.EventListener;
import elemental.events.MouseEvent;
import elemental.html.DragEvent;
import elemental.html.Element;
import elemental.js.html.JsElement;

/**
* A tree widget that is capable of rendering any tree data structure whose node
* data type is specified in the class parameterization.
*
* Users of this widget must specify an appropriate
* {@link com.google.collide.client.ui.tree.NodeDataAdapter} and
* {@link com.google.collide.client.ui.tree.NodeRenderer}.
*
*  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):
*
* <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>> {

  /**
   * Static factory method for obtaining an instance of the Tree.
   */
  public static <NodeData> Tree<NodeData> create(View<NodeData> view,
      NodeDataAdapter<NodeData> dataAdapter, NodeRenderer<NodeData> nodeRenderer,
      Tree.Resources resources) {
    Model<NodeData> model = new Model<NodeData>(dataAdapter, nodeRenderer, resources);
    return new Tree<NodeData>(view, model);
  }

  /**
   * Css selectors applied to DOM elements in the tree.
   */
  public interface Css extends CssResource {
    String active();

    String childrenContainer();

    String closedIcon();

    String expandControl();

    String isDropTarget();

    String leafIcon();

    String openedIcon();

    String selected();

    String treeNode();

    String treeNodeBody();

    String treeNodeLabel();

    String treeRoot();

    String treeNodeLabelLoading();
  }

  /**
   * 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, DragEvent event);

    void onNodeDragDrop(TreeNodeElement<D> node, DragEvent event);

    void onNodeExpanded(TreeNodeElement<D> node);

    void onRootContextMenu(int mouseX, int mouseY);

    void onRootDragDrop(DragEvent 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 = dataAdapter;
      this.nodeRenderer = nodeRenderer;
      this.resources = resources;
      this.selectionModel = new SelectionModel<D>(dataAdapter, resources.treeCss());
      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.
   *
   * In order to theme the Tree, you extend this interface and override
   * {@link Tree.Resources#treeCss()}.
   */
  public interface Resources extends ClientBundle {
    @Source("expansionIcon.png")
    ImageResource expansionIcon();

    // Default Stylesheet.
    @Source({"com/google/collide/client/common/constants.css",
        "Tree.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) {
           
            if (Event.CLICK.equals(evt.getType())) {
              double currentClickMs = Duration.currentTimeMillis();
              if (currentClickMs - previousClickMs < MouseGestureListener.MAX_CLICK_TIMEOUT_MS
                  && treeNodeBody.equals(previousClickTreeNodeBody)) {
                // Swallow double, triple, etc. clicks on an item's label
                return;
              } else {
                this.previousClickMs = currentClickMs;
                this.previousClickTreeNodeBody = treeNodeBody;
              }
            }
       
            onTreeNodeBodyChildEvent(evt, treeNodeBody);
          } else {
            onOtherEvent(evt);
          }
        }
      }

      /**
       * 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.
       *
       *  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;
    private final Tree.Resources resources;

    public View(Tree.Resources resources) {
      super(Elements.createElement("ul"));
      this.resources = resources;
      this.css = resources.treeCss();
      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).

      getElement().addEventListener(Event.CLICK, 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.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);

      EventListener dragDropEventListener = new EventListener() {
        @Override
        public void handleEvent(Event event) {
          if (getDelegate() != null) {
            getDelegate().onDragDropEvent((DragEvent) event);
          }
        }
      };
      getElement().addEventListener(Event.DROP, dragDropEventListener, false);
      getElement().addEventListener(Event.DRAGOVER, dragDropEventListener, false);
      getElement().addEventListener(Event.DRAGENTER, dragDropEventListener, false);
      getElement().addEventListener(Event.DRAGLEAVE, dragDropEventListener, false);
      getElement().addEventListener(Event.DRAGSTART, dragDropEventListener, 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);
    }

    @SuppressWarnings("unchecked")
    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> {
    public void onNodeAction(TreeNodeElement<D> node);

    public void onNodeClosed(TreeNodeElement<D> node);

    public void onNodeContextMenu(int mouseX, int mouseY, TreeNodeElement<D> node);

    public void onDragDropEvent(DragEvent event);

    public void onNodeExpanded(TreeNodeElement<D> node);

    public void onNodeSelected(TreeNodeElement<D> node, SignalEvent event);

    public void onRootContextMenu(int mouseX, int mouseY);

    public void onRootDragDrop(DragEvent 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(DragEvent 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;
      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();

  /**
   * 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) {

      // We want to select the node if it isn't already selected.
      getModel().selectionModel.contextSelect(node.getData());

      if (getModel().externalEventDelegate != null) {
        getModel().externalEventDelegate.onNodeContextMenu(mouseX, mouseY, node);
      }
    }

    @Override
    public void onDragDropEvent(DragEvent event) {
      dragDropController.handleDragDropEvent(event);
    }

    @Override
    public void onNodeExpanded(TreeNodeElement<D> node) {
      expandNode(node, true, true);
    }

    @Override
    public void onNodeSelected(TreeNodeElement<D> node, SignalEvent event) {
      getModel().selectionModel.selectNode(node.getData(), event);
    }

    @Override
    public void onRootContextMenu(int mouseX, int mouseY) {
      if (getModel().externalEventDelegate != null) {
        getModel().externalEventDelegate.onRootContextMenu(mouseX, mouseY);
      }
    }

    @Override
    public void onRootDragDrop(DragEvent event) {
      if (getModel().externalEventDelegate != null) {
        getModel().externalEventDelegate.onRootDragDrop(event);
      }
    }
  };

  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);
  }

  private void selectSingleNode(TreeNodeElement<D> renderedNode, boolean dispatchNodeAction) {
    getModel().selectionModel.selectSingleNode(renderedNode.getData());
    maybeNotifyNodeActionExternal(renderedNode, dispatchNodeAction);   
  }

  /**
   * 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());
  }

  /**
   * @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());

    JsonArray<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>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>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 JsonArray<JsonArray<String>> expandPaths(JsonArray<JsonArray<String>> paths,
      boolean dispatchNodeExpanded) {
    JsonArray<JsonArray<String>> notExpanded = JsonCollections.createArray();
    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.
   *
   * 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.
   *
   *  This will NOT restore any expansions. If you want to re-render the tree
   * obeying previous expansions then,
   *
   * @see: {@link #replaceSubtree(Object, Object)}.
   *
   * @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("");

    // 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.
    JsonArray<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 iff the old node was rendered.
   *
   * <p>{@code oldSubtreeData} and {@code incomingSubtreeData} are allowed to be
   * the same node (it will simply get re-rendered).
   *
   * <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 JsonArray<JsonArray<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.
    JsonArray<JsonArray<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);

      // Remove the old rendered node from the tree.
      oldRenderedNode.removeFromParent();
    }

    // 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.
    JsonArray<JsonArray<String>> selectedPaths = getModel().selectionModel.computeSelectedPaths();
    restoreSelectionModel(selectedPaths);
   
    return expandedPaths;
  }

  /**
   * Populates the selection model from a list of selected paths iff they
   * resolve to nodes in the data model.
   */
  private void restoreSelectionModel(JsonArray<JsonArray<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) {
        getModel().selectionModel.selectNode(node, null);
      }
    }
  }

  /**
   * 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;
  }

  private boolean expandPathRecursive(D expandedParentNode, JsonArray<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.
      JsonArray<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.
   *
   *  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 JsonArray<JsonArray<String>> gatherExpandedPaths(D rootData) {
    final JsonArray<JsonArray<String>> expandedPaths = JsonCollections.createArray();

    // 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) {

    JsonArray<D> nodes = JsonCollections.createArray();
    nodes.add(rootData);

    // Iterative DFS.
    while (!nodes.isEmpty()) {
      D parentNodeData = nodes.pop();
      boolean willVisitChildren = false;
      JsonArray<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);

    // 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);
    JsonArray<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;
  }
}
TOP

Related Classes of com.google.collide.client.ui.tree.Tree$DragDropController

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.