Package org.apache.wicket.extensions.markup.html.tree

Source Code of org.apache.wicket.extensions.markup.html.tree.TreeState$VisibleTreeStateNodeEnumeration

/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements.  See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.apache.wicket.extensions.markup.html.tree;

import java.util.Enumeration;
import java.util.Hashtable;
import java.util.NoSuchElementException;

import javax.swing.event.TreeModelEvent;
import javax.swing.event.TreeModelListener;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.MutableTreeNode;
import javax.swing.tree.RowMapper;
import javax.swing.tree.TreeModel;
import javax.swing.tree.TreePath;
import javax.swing.tree.TreeSelectionModel;

import org.apache.wicket.IClusterable;
import org.apache.wicket.util.collections.ArrayListStack;


/**
* Holder and handler for tree state.
*
* This class is largely based on
* {@link javax.swing.tree.FixedHeightLayoutCache}from JDK 1.5_01. Using that
* class or {@link javax.swing.tree.VariableHeightLayoutCache}gave problems
* when working in clustered environments. Hence, for this class most of the
* useful workings of FixedHeightLayoutCache were copied, while everything that
* is Swing/paint specific was removed.
*
* @author Eelco Hillenius
* @author Scott Violet (Sun, FixedHeightLayoutCache)
*/
public final class TreeState implements IClusterable, TreeModelListener, RowMapper
{
  private static final long serialVersionUID = 1L;

  /** currently selected path. */
  private TreePath selectedPath;

  /** Model providing information. */
  private TreeModel treeModel;

  /** Selection model. */
  private TreeSelectionModel treeSelectionModel;

  /**
   * True if the root node is displayed, false if its children are the highest
   * visible nodes.
   */
  private boolean rootVisible;

  /** Root node. */
  private TreeStateNode root;

  /** Number of rows currently visible. */
  private int rowCount;

  /**
   * Maps from TreePath to a TreeStateNode.
   */
  private Hashtable treePathMapping;

  /**
   * Used for getting path/row information.
   */
  private SearchInfo info;

  private ArrayListStack tempStacks;

  /**
   * Construct.
   */
  public TreeState()
  {
    tempStacks = new ArrayListStack();
    treePathMapping = new Hashtable();
    info = new SearchInfo();
  }

  /**
   * Expands the selected path and set selection to currently selected path.
   *
   * @param selection
   *        the new selection.
   */
  public void setSelectedPath(TreePath selection)
  {
    setExpandedState(selection, true);
    this.selectedPath = selection;

    // if we have a multiple selection model
    if (treeSelectionModel != null)
    {
      if (treeSelectionModel.isPathSelected(selection))
      {
        treeSelectionModel.removeSelectionPath(selection);
      }
      else
      {
        treeSelectionModel.addSelectionPath(selection);
      }
    }
  }

  /**
   * Gets the currently selected path.
   * @return the currently selected path
   */
  public TreePath getSelectedPath()
  {
    if ((selectedPath == null) && isRootVisible())
    {
      selectedPath = new TreePath(getModel().getRoot());
    }

    return selectedPath;
  }

  /**
   * Returns the <code>TreeModel</code> that is providing the data.
   *
   * @return the <code>TreeModel</code> that is providing the data
   */
  public TreeModel getModel()
  {
    return treeModel;
  }

  /**
   * Returns true if the root node of the tree is displayed.
   *
   * @return true if the root node of the tree is displayed
   */
  public boolean isRootVisible()
  {
    return rootVisible;
  }

  /**
   * Sets the <code>TreeSelectionModel</code> used to manage the selection
   * to new LSM.
   *
   * @param selectionModel
   *        the new <code>TreeSelectionModel</code>
   */
  public void setSelectionModel(TreeSelectionModel selectionModel)
  {
    if (this.treeSelectionModel != null)
    {
      this.treeSelectionModel.setRowMapper(null);
    }
    if (selectionModel != null)
    {
      selectionModel.setRowMapper(this);
    }
    this.treeSelectionModel = selectionModel;
  }

  /**
   * Returns the model used to maintain the selection.
   *
   * @return the <code>treeSelectionModel</code>
   */
  public TreeSelectionModel getSelectionModel()
  {
    return treeSelectionModel;
  }

  //
  // RowMapper
  //

  /**
   * Returns the rows that the <code>TreePath</code> instances in
   * <code>path</code> are being displayed at. This method should return an
   * array of the same length as that passed in, and if one of the
   * <code>TreePaths</code> in <code>path</code> is not valid its entry in
   * the array should be set to -1.
   *
   * @param paths
   *        the array of <code>TreePath</code> s being queried
   * @return an array of the same length that is passed in containing the rows
   *       that each corresponding where each <code>TreePath</code> is
   *       displayed; if <code>paths</code> is <code>null</code>,
   *       <code>null</code> is returned
   */
  public int[] getRowsForPaths(TreePath[] paths)
  {
    if (paths == null)
    {
      return null;
    }

    int numPaths = paths.length;
    int[] rows = new int[numPaths];

    for (int counter = 0; counter < numPaths; counter++)
    {
      rows[counter] = getRowForPath(paths[counter]);
    }
    return rows;
  }

  /**
   * Sets the TreeModel that will provide the data.
   *
   * @param newModel
   *        the TreeModel that is to provide the data
   */
  public void setModel(TreeModel newModel)
  {
    this.treeModel = newModel;
    rebuild(false);
  }

  /**
   * Determines whether or not the root node from the TreeModel is visible.
   *
   * @param rootVisible
   *        true if the root node of the tree is to be displayed
   */
  public void setRootVisible(boolean rootVisible)
  {
    if (isRootVisible() != rootVisible)
    {
      this.rootVisible = rootVisible;
      if (root != null)
      {
        if (rootVisible)
        {
          rowCount++;
          root.adjustRowBy(1);
        }
        else
        {
          rowCount--;
          root.adjustRowBy(-1);
        }
        visibleNodesChanged();
      }
    }
  }

  /**
   * Returns the number of visible rows.
   *
   * @return the number of visible rows
   */
  public int getRowCount()
  {
    return rowCount;
  }

  /**
   * Returns true if the value identified by row is currently expanded.
   *
   * @param path
   *        the row
   * @return true if the value identified by row is currently expanded
   */
  public boolean isExpanded(TreePath path)
  {
    if (path != null)
    {
      TreeStateNode lastNode = getNodeForPath(path, true, false);
      return (lastNode != null && lastNode.isExpanded());
    }
    return false;
  }

  /**
   * Returns the path for passed in row. If row is not visible null is
   * returned.
   *
   * @param row
   *        the row
   * @return the path for passed in row
   */
  public TreePath getPathForRow(int row)
  {
    if (row >= 0 && row < getRowCount())
    {
      if (root.getPathForRow(row, getRowCount(), info))
      {
        return info.getPath();
      }
    }
    return null;
  }

  /**
   * Returns the row that the last item identified in path is visible at. Will
   * return -1 if any of the elements in path are not currently visible.
   *
   * @param path
   *        the path
   * @return the row that the last item identified in path is visible at
   */
  public int getRowForPath(TreePath path)
  {
    if (path == null || root == null)
    {
      return -1;
    }

    TreeStateNode node = getNodeForPath(path, true, false);

    if (node != null)
    {
      return node.getRow();
    }

    TreePath parentPath = path.getParentPath();

    node = getNodeForPath(parentPath, true, false);
    if (node != null && node.isExpanded())
    {
      return node.getRowToModelIndex(treeModel.getIndexOfChild(parentPath
          .getLastPathComponent(), path.getLastPathComponent()));
    }
    return -1;
  }

  /**
   * Returns the number of visible children for row.
   *
   * @param path
   *        the path
   * @return the number of visible children for row
   */
  public int getVisibleChildCount(TreePath path)
  {
    TreeStateNode node = getNodeForPath(path, true, false);
    if (node == null)
    {
      return 0;
    }
    return node.getTotalChildCount();
  }

  /**
   * Returns an Enumerator that increments over the visible paths starting at
   * the passed in location. The ordering of the enumeration is based on how
   * the paths are displayed.
   *
   * @param path
   *        the path
   * @return an Enumerator that increments over the visible paths
   */
  public Enumeration getVisiblePathsFrom(TreePath path)
  {
    if (path == null)
    {
      return null;
    }

    TreeStateNode node = getNodeForPath(path, true, false);

    if (node != null)
    {
      return new VisibleTreeStateNodeEnumeration(node);
    }
    TreePath parentPath = path.getParentPath();

    node = getNodeForPath(parentPath, true, false);
    if (node != null && node.isExpanded())
    {
      return new VisibleTreeStateNodeEnumeration(node, treeModel.getIndexOfChild(
          parentPath.getLastPathComponent(), path.getLastPathComponent()));
    }
    return null;
  }

  /**
   * Marks the path <code>path</code> expanded state to
   * <code>isExpanded</code>.
   *
   * @param path
   *        the path
   * @param isExpanded
   *        whether the path is expanded
   */
  public void setExpandedState(TreePath path, boolean isExpanded)
  {
    if (isExpanded)
    {
      ensurePathIsExpanded(path, true);
    }
    else if (path != null)
    {
      TreePath parentPath = path.getParentPath();

      // YECK! Make the parent expanded.
      if (parentPath != null)
      {
        TreeStateNode parentNode = getNodeForPath(parentPath, false, true);
        if (parentNode != null)
        {
          parentNode.makeVisible();
        }
      }
      // And collapse the child.
      TreeStateNode childNode = getNodeForPath(path, true, false);

      if (childNode != null)
      {
        childNode.collapse(true);
      }
    }
  }

  /**
   * Returns true if the path is expanded, and visible.
   *
   * @param path
   *        the path
   * @return true if the path is expanded, and visible
   */
  public boolean getExpandedState(TreePath path)
  {
    TreeStateNode node = getNodeForPath(path, true, false);
    return (node != null) ? (node.isVisible() && node.isExpanded()) : false;
  }

  //
  // TreeModelListener methods
  //

  /**
   * <p>
   * Invoked after a node (or a set of siblings) has changed in some way. The
   * node(s) have not changed locations in the tree or altered their children
   * arrays, but other attributes have changed and may affect presentation.
   * Example: the name of a file has changed, but it is in the same location
   * in the file system.
   * </p>
   * <p>
   * e.path() returns the path the parent of the changed node(s).
   * </p>
   * <p>
   * e.childIndices() returns the index(es) of the changed node(s).
   * </p>
   *
   * @param e
   *        the tree model event
   */
  public void treeNodesChanged(TreeModelEvent e)
  {
    if (e != null)
    {
      int changedIndexs[];
      TreeStateNode changedParent = getNodeForPath(e.getTreePath(), false, false);
      int maxCounter;

      changedIndexs = e.getChildIndices();
      /*
       * Only need to update the children if the node has been expanded
       * once.
       */
      // PENDING(scott): make sure childIndexs is sorted!
      if (changedParent != null)
      {
        if (changedIndexs != null && (maxCounter = changedIndexs.length) > 0)
        {
          Object parentValue = changedParent.getUserObject();

          for (int counter = 0; counter < maxCounter; counter++)
          {
            TreeStateNode child = changedParent
                .getChildAtModelIndex(changedIndexs[counter]);

            if (child != null)
            {
              child.setUserObject(treeModel.getChild(parentValue,
                  changedIndexs[counter]));
            }
          }
          if (changedParent.isVisible() && changedParent.isExpanded())
          {
            visibleNodesChanged();
          }
        }
        // Null for root indicates it changed.
        else if (changedParent == root
            && changedParent.isVisible() && changedParent.isExpanded())
        {
          visibleNodesChanged();
        }
      }
    }
  }

  /**
   * <p>
   * Invoked after nodes have been inserted into the tree.
   * </p>
   * <p>
   * e.path() returns the parent of the new nodes
   * <p>
   * e.childIndices() returns the indices of the new nodes in ascending order.
   *
   * @param e
   *        the tree model event
   */
  public void treeNodesInserted(TreeModelEvent e)
  {
    if (e != null)
    {
      int changedIndexs[];
      TreeStateNode changedParent = getNodeForPath(e.getTreePath(), false, false);
      int maxCounter;

      changedIndexs = e.getChildIndices();
      /*
       * Only need to update the children if the node has been expanded
       * once.
       */
      // PENDING(scott): make sure childIndexs is sorted!
      if (changedParent != null
          && changedIndexs != null && (maxCounter = changedIndexs.length) > 0)
      {
        boolean isVisible = (changedParent.isVisible() && changedParent
            .isExpanded());

        for (int counter = 0; counter < maxCounter; counter++)
        {
          changedParent.childInsertedAtModelIndex(changedIndexs[counter],
              isVisible);
        }
        if (isVisible && treeSelectionModel != null)
        {
          treeSelectionModel.resetRowSelection();
        }
        if (changedParent.isVisible())
        {
          this.visibleNodesChanged();
        }
      }
    }
  }

  /**
   * <p>
   * Invoked after nodes have been removed from the tree. Note that if a
   * subtree is removed from the tree, this method may only be invoked once
   * for the root of the removed subtree, not once for each individual set of
   * siblings removed.
   * </p>
   * <p>
   * e.path() returns the former parent of the deleted nodes.
   * </p>
   * <p>
   * e.childIndices() returns the indices the nodes had before they were
   * deleted in ascending order.
   * </p>
   *
   * @param e
   *        the tree model event
   */
  public void treeNodesRemoved(TreeModelEvent e)
  {
    if (e != null)
    {
      int changedIndexs[];
      int maxCounter;
      TreePath parentPath = e.getTreePath();
      TreeStateNode changedParentNode = getNodeForPath(parentPath, false, false);

      changedIndexs = e.getChildIndices();
      // PENDING(scott): make sure that changedIndexs are sorted in
      // ascending order.
      if (changedParentNode != null
          && changedIndexs != null && (maxCounter = changedIndexs.length) > 0)
      {
        boolean isVisible = (changedParentNode.isVisible() && changedParentNode
            .isExpanded());

        for (int counter = maxCounter - 1; counter >= 0; counter--)
        {
          changedParentNode.removeChildAtModelIndex(changedIndexs[counter],
              isVisible);
        }
        if (isVisible)
        {
          if (treeSelectionModel != null)
          {
            treeSelectionModel.resetRowSelection();
          }
          if (treeModel.getChildCount(changedParentNode.getUserObject()) == 0
              && changedParentNode.isLeaf())
          {
            // Node has become a leaf, collapse it.
            changedParentNode.collapse(false);
          }
          visibleNodesChanged();
        }
        else if (changedParentNode.isVisible())
        {
          visibleNodesChanged();
        }
      }
    }
  }

  /**
   * <p>
   * Invoked after the tree has drastically changed structure from a given
   * node down. If the path returned by e.getPath() is of length one and the
   * first element does not identify the current root node the first element
   * should become the new root of the tree.
   * <p>
   * <p>
   * e.path() holds the path to the node.
   * </p>
   * <p>
   * e.childIndices() returns null.
   * </p>
   *
   * @param e
   *        the tree model event
   */
  public void treeStructureChanged(TreeModelEvent e)
  {
    if (e != null)
    {
      TreePath changedPath = e.getTreePath();
      TreeStateNode changedNode = getNodeForPath(changedPath, false, false);

      // Check if root has changed, either to a null root, or
      // to an entirely new root.
      if (changedNode == root
          || (changedNode == null && ((changedPath == null && treeModel != null && treeModel
              .getRoot() == null) || (changedPath != null && changedPath
              .getPathCount() <= 1))))
      {
        rebuild(true);
      }
      else if (changedNode != null)
      {
        boolean wasExpanded, wasVisible;
        TreeStateNode parent = (TreeStateNode) changedNode.getParent();

        wasExpanded = changedNode.isExpanded();
        wasVisible = changedNode.isVisible();

        int index = parent.getIndex(changedNode);
        changedNode.collapse(false);
        parent.remove(index);

        if (wasVisible && wasExpanded)
        {
          int row = changedNode.getRow();
          parent.resetChildrenRowsFrom(row, index, changedNode.getChildIndex());
          changedNode = getNodeForPath(changedPath, false, true);
          changedNode.expand();
        }
        if (treeSelectionModel != null && wasVisible && wasExpanded)
        {
          treeSelectionModel.resetRowSelection();
        }
        if (wasVisible)
        {
          this.visibleNodesChanged();
        }
      }
    }
  }

  //
  // Local methods
  //

  /**
   * Called when the visibility of nodes changed.
   */
  private void visibleNodesChanged()
  {
  }

  /**
   * Adjust the large row count.
   *
   * @param change
   *        the change for the row count
   */
  private void adjustRowCountBy(int change)
  {
    rowCount += change;
  }

  /**
   * Adds a mapping for node.
   *
   * @param node
   *        the node to map
   */
  private void addMapping(TreeStateNode node)
  {
    treePathMapping.put(node.getTreePath(), node);
  }

  /**
   * Removes the mapping for a previously added node.
   *
   * @param node
   *        the node to remove the mapping for
   */
  private void removeMapping(TreeStateNode node)
  {
    treePathMapping.remove(node.getTreePath());
  }

  /**
   * Returns the node previously added for <code>path</code>. This may
   * return null, if you to create a node use getNodeForPath.
   *
   * @param path
   *        the path
   * @return the state node
   */
  private TreeStateNode getMapping(TreePath path)
  {
    return (TreeStateNode) treePathMapping.get(path);
  }

  /**
   * Sent to completely rebuild the visible tree. All nodes are collapsed.
   *
   * @param clearSelection
   *        whether to clear the selection
   */
  private void rebuild(boolean clearSelection)
  {
    Object rootUO;

    treePathMapping.clear();
    if (treeModel != null && (rootUO = treeModel.getRoot()) != null)
    {
      root = createNodeForValue(rootUO, 0);
      root.path = new TreePath(rootUO);
      addMapping(root);
      if (isRootVisible())
      {
        rowCount = 1;
        root.row = 0;
      }
      else
      {
        rowCount = 0;
        root.row = -1;
      }
      root.expand();
    }
    else
    {
      root = null;
      rowCount = 0;
    }
    if (clearSelection && treeSelectionModel != null)
    {
      treeSelectionModel.clearSelection();
    }
    this.visibleNodesChanged();
  }

  /**
   * Ensures that all the path components in path are expanded, accept for the
   * last component which will only be expanded if expandLast is true. Returns
   * true if succesful in finding the path.
   *
   * @param aPath
   *        the path
   * @param expandLast
   *        whether to expand the last element
   * @return true if succesful in finding the path
   */
  private boolean ensurePathIsExpanded(TreePath aPath, boolean expandLast)
  {
    if (aPath != null)
    {
      // Make sure the last entry isn't a leaf.
      if (treeModel.isLeaf(aPath.getLastPathComponent()))
      {
        aPath = aPath.getParentPath();
        expandLast = true;
      }
      if (aPath != null)
      {
        TreeStateNode lastNode = getNodeForPath(aPath, false, true);

        if (lastNode != null)
        {
          lastNode.makeVisible();
          if (expandLast)
          {
            lastNode.expand();
          }
          return true;
        }
      }
    }
    return false;
  }

  /**
   * Creates and returns an instance of TreeStateNode.
   *
   * @param userObject
   *        the user object
   * @param childIndex
   *        the index relative to the parent
   * @return the tree state node
   */
  private TreeStateNode createNodeForValue(Object userObject, int childIndex)
  {
    return new TreeStateNode(userObject, childIndex, -1);
  }

  /**
   * Messages getTreeNodeForPage(path, onlyIfVisible, shouldCreate,
   * path.length) as long as path is non-null and the length is > 0. Otherwise
   * returns null.
   *
   * @param path
   *        the path
   * @param onlyIfVisible
   * @param shouldCreate
   * @return the tree state node
   */
  private TreeStateNode getNodeForPath(TreePath path, boolean onlyIfVisible,
      boolean shouldCreate)
  {
    if (path != null)
    {
      TreeStateNode node;

      node = getMapping(path);
      if (node != null)
      {
        if (onlyIfVisible && !node.isVisible())
        {
          return null;
        }
        return node;
      }
      if (onlyIfVisible)
      {
        return null;
      }

      // Check all the parent paths, until a match is found.
      ArrayListStack paths;

      if (tempStacks.size() == 0)
      {
        paths = new ArrayListStack();
      }
      else
      {
        paths = (ArrayListStack)tempStacks.pop();
      }

      try
      {
        paths.push(path);
        path = path.getParentPath();
        while (path != null)
        {
          node = getMapping(path);
          if (node != null)
          {
            // Found a match, create entries for all paths in
            // paths.
            while (node != null && paths.size() > 0)
            {
              path = (TreePath) paths.pop();
              node = node.createChildFor(path.getLastPathComponent());
            }
            return node;
          }
          paths.push(path);
          path = path.getParentPath();
        }
      }
      finally
      {
        paths.clear();
        tempStacks.push(paths);
      }
      // If we get here it means they share a different root!
      return null;
    }
    return null;
  }

  /**
   * TreeStateNode is used to track what has been expanded.
   */
  private final class TreeStateNode extends DefaultMutableTreeNode
  {
    private static final long serialVersionUID = 1L;

    /** Whether this node is expanded */
    private boolean isExpanded;

    /** Index of this node from the model. */
    private int childIndex;

    /** Child count of the receiver. */
    private int childCount;

    /**
     * Row of the receiver. This is only valid if the row is expanded.
     */
    private int row;

    /** Path of this node. */
    private TreePath path;

    /**
     * Construct.
     *
     * @param userObject
     * @param childIndex
     * @param row
     */
    public TreeStateNode(Object userObject, int childIndex, int row)
    {
      super(userObject);
      this.childIndex = childIndex;
      this.row = row;
    }

    //
    // Overriden DefaultMutableTreeNode methods
    //

    /**
     * Messaged when this node is added somewhere, resets the path and adds
     * a mapping from path to this node.
     *
     * @param parent
     *        the parent
     */
    public void setParent(MutableTreeNode parent)
    {
      super.setParent(parent);
      if (parent != null)
      {
        path = ((TreeStateNode) parent).getTreePath().pathByAddingChild(
            getUserObject());
        addMapping(this);
      }
    }

    /**
     * Messaged when this node is removed from its parent, this messages
     * <code>removedFromMapping</code> to remove all the children.
     *
     * @param childIndex
     *        the index of this node relative to its parent
     */
    public void remove(int childIndex)
    {
      TreeStateNode node = (TreeStateNode) getChildAt(childIndex);

      node.removeFromMapping();
      super.remove(childIndex);
    }

    /**
     * Messaged to set the user object. This resets the path.
     *
     * @param o
     *        the user object
     */
    public void setUserObject(Object o)
    {
      super.setUserObject(o);
      if (path != null)
      {
        TreeStateNode parent = (TreeStateNode) getParent();

        if (parent != null)
        {
          resetChildrenPaths(parent.getTreePath());
        }
        else
        {
          resetChildrenPaths(null);
        }
      }
    }

    //
    //

    /**
     * Returns the index of the receiver in the model.
     *
     * @return the index of this node relative to its parent
     */
    public int getChildIndex()
    {
      return childIndex;
    }

    /**
     * Returns the <code>TreePath</code> of the receiver.
     *
     * @return the tree path
     */
    public TreePath getTreePath()
    {
      return path;
    }

    /**
     * Returns the child for the passed in model index, this will return
     * <code>null</code> if the child for <code>index</code> has not yet
     * been created (expanded).
     *
     * @param index
     *        the childs index
     * @return the tree state node
     */
    public TreeStateNode getChildAtModelIndex(int index)
    {
      // PENDING: Make this a binary search!
      for (int counter = getChildCount() - 1; counter >= 0; counter--)
      {
        if (((TreeStateNode) getChildAt(counter)).childIndex == index)
        {
          return (TreeStateNode) getChildAt(counter);
        }
      }
      return null;
    }

    /**
     * Returns true if this node is visible. This is determined by asking
     * all the parents if they are expanded.
     *
     * @return true if this node is visible
     */
    public boolean isVisible()
    {
      TreeStateNode parent = (TreeStateNode) getParent();

      if (parent == null)
      {
        return true;
      }
      return (parent.isExpanded() && parent.isVisible());
    }

    /**
     * Returns the row of the receiver.
     *
     * @return the row of the receiver
     */
    public int getRow()
    {
      return row;
    }

    /**
     * Returns the row of the child with a model index of <code>index</code>.
     *
     * @param index
     *        the model index
     * @return the row of the child with a model index of <code>index</code>
     */
    public int getRowToModelIndex(int index)
    {
      TreeStateNode child;

      // This too could be a binary search!
      for (int counter = 0, maxCounter = getChildCount(); counter < maxCounter; counter++)
      {
        child = (TreeStateNode) getChildAt(counter);
        if (child.childIndex >= index)
        {
          if (child.childIndex == index)
          {
            return child.row;
          }
          if (counter == 0)
          {
            return getRow() + 1 + index;
          }
          return child.row - (child.childIndex - index);
        }
      }
      // YECK!
      return getRow() + 1 + getTotalChildCount() - (childCount - index);
    }

    /**
     * Returns the number of children in the receiver by descending all
     * expanded nodes and messaging them with getTotalChildCount.
     *
     * @return the number of children in the receiver by descending all
     *       expanded nodes and messaging them with getTotalChildCount
     */
    public int getTotalChildCount()
    {
      if (isExpanded())
      {
        TreeStateNode parent = (TreeStateNode) getParent();
        int pIndex;

        if (parent != null
            && (pIndex = parent.getIndex(this)) + 1 < parent.getChildCount())
        {
          // This node has a created sibling, to calc total
          // child count directly from that!
          TreeStateNode nextSibling = (TreeStateNode) parent
              .getChildAt(pIndex + 1);

          return nextSibling.row - row - (nextSibling.childIndex - childIndex);
        }
        else
        {
          int retCount = childCount;

          for (int counter = getChildCount() - 1; counter >= 0; counter--)
          {
            retCount += ((TreeStateNode) getChildAt(counter))
                .getTotalChildCount();
          }
          return retCount;
        }
      }
      return 0;
    }

    /**
     * Returns true if this node is expanded.
     *
     * @return true if this node is expanded
     */
    public boolean isExpanded()
    {
      return isExpanded;
    }

    /**
     * The highest visible nodes have a depth of 0.
     *
     * @return the level of visibility
     */
    public int getVisibleLevel()
    {
      if (isRootVisible())
      {
        return getLevel();
      }
      else
      {
        return getLevel() - 1;
      }
    }

    /**
     * Recreates the receivers path, and all its childrens paths.
     *
     * @param parentPath
     *        the parent of the path to recreate
     */
    private void resetChildrenPaths(TreePath parentPath)
    {
      removeMapping(this);
      if (parentPath == null)
      {
        path = new TreePath(getUserObject());
      }
      else
      {
        path = parentPath.pathByAddingChild(getUserObject());
      }
      addMapping(this);
      for (int counter = getChildCount() - 1; counter >= 0; counter--)
      {
        ((TreeStateNode) getChildAt(counter)).resetChildrenPaths(path);
      }
    }

    /**
     * Removes the receiver, and all its children, from the mapping table.
     */
    private void removeFromMapping()
    {
      if (path != null)
      {
        removeMapping(this);
        for (int counter = getChildCount() - 1; counter >= 0; counter--)
        {
          ((TreeStateNode) getChildAt(counter)).removeFromMapping();
        }
      }
    }

    /**
     * Creates a new node to represent <code>userObject</code>. This does
     * NOT check to ensure there isn't already a child node to manage
     * <code>userObject</code>.
     *
     * @param userObject
     *        the user object of the new node
     * @return the new node
     */
    private TreeStateNode createChildFor(Object userObject)
    {
      int newChildIndex = treeModel.getIndexOfChild(getUserObject(), userObject);

      if (newChildIndex < 0)
      {
        return null;
      }

      TreeStateNode aNode;
      TreeStateNode child = createNodeForValue(userObject, newChildIndex);
      int childRow;

      if (isVisible())
      {
        childRow = getRowToModelIndex(newChildIndex);
      }
      else
      {
        childRow = -1;
      }
      child.row = childRow;
      for (int counter = 0, maxCounter = getChildCount(); counter < maxCounter; counter++)
      {
        aNode = (TreeStateNode) getChildAt(counter);
        if (aNode.childIndex > newChildIndex)
        {
          insert(child, counter);
          return child;
        }
      }
      add(child);
      return child;
    }

    /**
     * Adjusts the receiver, and all its children rows by
     * <code>adjust</code>.
     *
     * @param adjust
     *        adjustement
     */
    private void adjustRowBy(int adjust)
    {
      row += adjust;
      if (isExpanded)
      {
        for (int counter = getChildCount() - 1; counter >= 0; counter--)
        {
          ((TreeStateNode) getChildAt(counter)).adjustRowBy(adjust);
        }
      }
    }

    /**
     * Adjusts this node, its child, and its parent starting at an index of
     * <code>startIndex</code> index is the index of the child to start
     * adjusting from, which is not necessarily the model index.
     *
     * @param adjust
     * @param startIndex
     */
    private void adjustRowBy(int adjust, int startIndex)
    {
      // Could check isVisible, but probably isn't worth it.
      if (isExpanded)
      {
        // children following startIndex.
        for (int counter = getChildCount() - 1; counter >= startIndex; counter--)
        {
          ((TreeStateNode) getChildAt(counter)).adjustRowBy(adjust);
        }
      }
      // Parent
      TreeStateNode parent = (TreeStateNode) getParent();

      if (parent != null)
      {
        parent.adjustRowBy(adjust, parent.getIndex(this) + 1);
      }
    }

    /**
     * Messaged when the node has expanded. This updates all of the
     * receivers children rows, as well as the total row count.
     */
    private void didExpand()
    {
      int nextRow = setRowAndChildren(row);
      TreeStateNode parent = (TreeStateNode) getParent();
      int childRowCount = nextRow - row - 1;

      if (parent != null)
      {
        parent.adjustRowBy(childRowCount, parent.getIndex(this) + 1);
      }
      adjustRowCountBy(childRowCount);
    }

    /**
     * Sets the receivers row to <code>nextRow</code> and recursively
     * updates all the children of the receivers rows. The index the next
     * row is to be placed as is returned.
     *
     * @param nextRow
     * @return the index the next row is to be placed
     */
    private int setRowAndChildren(int nextRow)
    {
      row = nextRow;

      if (!isExpanded())
      {
        return row + 1;
      }

      int lastRow = row + 1;
      int lastModelIndex = 0;
      TreeStateNode child;
      int maxCounter = getChildCount();

      for (int counter = 0; counter < maxCounter; counter++)
      {
        child = (TreeStateNode) getChildAt(counter);
        lastRow += (child.childIndex - lastModelIndex);
        lastModelIndex = child.childIndex + 1;
        if (child.isExpanded)
        {
          lastRow = child.setRowAndChildren(lastRow);
        }
        else
        {
          child.row = lastRow++;
        }
      }
      return lastRow + childCount - lastModelIndex;
    }

    /**
     * Resets the receivers childrens rows. Starting with the child at
     * <code>childIndex</code> (and <code>modelIndex</code>) to
     * <code>newRow</code>. This uses <code>setRowAndChildren</code> to
     * recursively descend children, and uses <code>resetRowSelection</code>
     * to ascend parents.
     *
     * @param newRow
     * @param childIndex
     * @param modelIndex
     */
    // This can be rather expensive, but is needed for the collapse
    // case this is resulting from a remove (although I could fix
    // that by having instances of TreeStateNode hold a ref to
    // the number of children). I prefer this though, making determing
    // the row of a particular node fast is very nice!
    private void resetChildrenRowsFrom(int newRow, int childIndex, int modelIndex)
    {
      int lastRow = newRow;
      int lastModelIndex = modelIndex;
      TreeStateNode node;
      int maxCounter = getChildCount();

      for (int counter = childIndex; counter < maxCounter; counter++)
      {
        node = (TreeStateNode) getChildAt(counter);
        lastRow += (node.childIndex - lastModelIndex);
        lastModelIndex = node.childIndex + 1;
        if (node.isExpanded)
        {
          lastRow = node.setRowAndChildren(lastRow);
        }
        else
        {
          node.row = lastRow++;
        }
      }
      lastRow += childCount - lastModelIndex;
      node = (TreeStateNode) getParent();
      if (node != null)
      {
        node.resetChildrenRowsFrom(lastRow, node.getIndex(this) + 1,
            this.childIndex + 1);
      }
      else
      { // This is the root, reset total ROWCOUNT!
        rowCount = lastRow;
      }
    }

    /**
     * Makes the receiver visible, but invoking
     * <code>expandParentAndReceiver</code> on the superclass.
     */
    private void makeVisible()
    {
      TreeStateNode parent = (TreeStateNode) getParent();

      if (parent != null)
      {
        parent.expandParentAndReceiver();
      }
    }

    /**
     * Invokes <code>expandParentAndReceiver</code> on the parent, and
     * expands the receiver.
     */
    private void expandParentAndReceiver()
    {
      TreeStateNode parent = (TreeStateNode) getParent();

      if (parent != null)
      {
        parent.expandParentAndReceiver();
      }
      expand();
    }

    /**
     * Expands the receiver.
     */
    private void expand()
    {
      if (!isExpanded && !isLeaf())
      {
        boolean visible = isVisible();

        isExpanded = true;
        childCount = treeModel.getChildCount(getUserObject());

        if (visible)
        {
          didExpand();
        }

        // Update the selection model.
        if (visible && treeSelectionModel != null)
        {
          treeSelectionModel.resetRowSelection();
        }
      }
    }

    /**
     * Collapses the receiver. If <code>adjustRows</code> is true, the
     * rows of nodes after the receiver are adjusted.
     *
     * @param adjustRows
     *        whether to adjust the rows of the receiver
     */
    private void collapse(boolean adjustRows)
    {
      if (isExpanded)
      {
        if (isVisible() && adjustRows)
        {
          int childCount = getTotalChildCount();

          isExpanded = false;
          adjustRowCountBy(-childCount);
          // We can do this because adjustRowBy won't descend
          // the children.
          adjustRowBy(-childCount, 0);
        }
        else
        {
          isExpanded = false;
        }

        if (adjustRows && isVisible() && treeSelectionModel != null)
        {
          treeSelectionModel.resetRowSelection();
        }
      }
    }

    /**
     * Returns true if the receiver is a leaf.
     *
     * @return true if the receiver is a leaf
     */
    public boolean isLeaf()
    {
      TreeModel model = getModel();

      return (model != null) ? model.isLeaf(this.getUserObject()) : true;
    }

    /**
     * Adds newChild to this nodes children at the appropriate location. The
     * location is determined from the childIndex of newChild.
     *
     * @param newChild
     *        the node to add
     */
//    private void addNode(TreeStateNode newChild)
//    {
//      boolean added = false;
//      int childIndex = newChild.getChildIndex();
//
//      for (int counter = 0, maxCounter = getChildCount(); counter < maxCounter; counter++)
//      {
//        if (((TreeStateNode) getChildAt(counter)).getChildIndex() > childIndex)
//        {
//          added = true;
//          insert(newChild, counter);
//          counter = maxCounter;
//        }
//      }
//      if (!added)
//        add(newChild);
//    }

    /**
     * Removes the child at <code>modelIndex</code>.
     * <code>isChildVisible</code> should be true if the receiver is
     * visible and expanded.
     *
     * @param modelIndex
     * @param isChildVisible
     */
    private void removeChildAtModelIndex(int modelIndex, boolean isChildVisible)
    {
      TreeStateNode childNode = getChildAtModelIndex(modelIndex);

      if (childNode != null)
      {
        int row = childNode.getRow();
        int index = getIndex(childNode);

        childNode.collapse(false);
        remove(index);
        adjustChildIndexs(index, -1);
        childCount--;
        if (isChildVisible)
        {
          // Adjust the rows.
          resetChildrenRowsFrom(row, index, modelIndex);
        }
      }
      else
      {
        int maxCounter = getChildCount();
        TreeStateNode aChild;

        for (int counter = 0; counter < maxCounter; counter++)
        {
          aChild = (TreeStateNode) getChildAt(counter);
          if (aChild.childIndex >= modelIndex)
          {
            if (isChildVisible)
            {
              adjustRowBy(-1, counter);
              adjustRowCountBy(-1);
            }
            // Since matched and children are always sorted by
            // index, no need to continue testing with the
            // above.
            for (; counter < maxCounter; counter++)
            {
              ((TreeStateNode) getChildAt(counter)).childIndex--;
            }
            childCount--;
            return;
          }
        }
        // No children to adjust, but it was a child, so we still need
        // to adjust nodes after this one.
        if (isChildVisible)
        {
          adjustRowBy(-1, maxCounter);
          adjustRowCountBy(-1);
        }
        childCount--;
      }
    }

    /**
     * Adjusts the child indexs of the receivers children by
     * <code>adjust</code>, starting at <code>index</code>.
     *
     * @param index
     * @param adjust
     */
    private void adjustChildIndexs(int index, int adjust)
    {
      for (int counter = index, maxCounter = getChildCount(); counter < maxCounter; counter++)
      {
        ((TreeStateNode) getChildAt(counter)).childIndex += adjust;
      }
    }

    /**
     * Messaged when a child has been inserted at index. For all the
     * children that have a childIndex >= index their index is incremented
     * by one.
     *
     * @param index
     *        the insertion index
     * @param isExpandedAndVisible
     */
    private void childInsertedAtModelIndex(int index, boolean isExpandedAndVisible)
    {
      TreeStateNode aChild;
      int maxCounter = getChildCount();

      for (int counter = 0; counter < maxCounter; counter++)
      {
        aChild = (TreeStateNode) getChildAt(counter);
        if (aChild.childIndex >= index)
        {
          if (isExpandedAndVisible)
          {
            adjustRowBy(1, counter);
            adjustRowCountBy(1);
          }
          /*
           * Since matched and children are always sorted by index, no
           * need to continue testing with the above.
           */
          for (; counter < maxCounter; counter++)
          {
            ((TreeStateNode) getChildAt(counter)).childIndex++;
          }
          childCount++;
          return;
        }
      }
      // No children to adjust, but it was a child, so we still need
      // to adjust nodes after this one.
      if (isExpandedAndVisible)
      {
        adjustRowBy(1, maxCounter);
        adjustRowCountBy(1);
      }
      childCount++;
    }

    /**
     * Returns true if there is a row for <code>row</code>.
     * <code>nextRow</code> gives the bounds of the receiver. Information
     * about the found row is returned in <code>info</code>. This should
     * be invoked on root with <code>nextRow</code> set to
     * <code>getRowCount</code> ().
     *
     * @param row
     * @param nextRow
     * @param info
     *        search info object
     * @return true if there is a row for <code>row</code>
     */
    private boolean getPathForRow(int row, int nextRow, SearchInfo info)
    {
      if (this.row == row)
      {
        info.node = this;
        info.isNodeParentNode = false;
        info.childIndex = childIndex;
        return true;
      }

      TreeStateNode child;
      TreeStateNode lastChild = null;

      for (int counter = 0, maxCounter = getChildCount(); counter < maxCounter; counter++)
      {
        child = (TreeStateNode) getChildAt(counter);
        if (child.row > row)
        {
          if (counter == 0)
          {
            // No node exists for it, and is first.
            info.node = this;
            info.isNodeParentNode = true;
            info.childIndex = row - this.row - 1;
            return true;
          }
          else
          {
            // May have been in last childs bounds.
            int lastChildEndRow = 1
                + child.row - (child.childIndex - lastChild.childIndex);

            if (row < lastChildEndRow)
            {
              return lastChild.getPathForRow(row, lastChildEndRow, info);
            }
            // Between last child and child, but not in last child
            info.node = this;
            info.isNodeParentNode = true;
            info.childIndex = row
                - lastChildEndRow + lastChild.childIndex + 1;
            return true;
          }
        }
        lastChild = child;
      }

      // Not in children, but we should have it, offset from
      // nextRow.
      if (lastChild != null)
      {
        int lastChildEndRow = nextRow - (childCount - lastChild.childIndex) + 1;

        if (row < lastChildEndRow)
        {
          return lastChild.getPathForRow(row, lastChildEndRow, info);
        }
        // Between last child and child, but not in last child
        info.node = this;
        info.isNodeParentNode = true;
        info.childIndex = row - lastChildEndRow + lastChild.childIndex + 1;
        return true;
      }
      else
      {
        // No children.
        int retChildIndex = row - this.row - 1;

        if (retChildIndex >= childCount)
        {
          return false;
        }
        info.node = this;
        info.isNodeParentNode = true;
        info.childIndex = retChildIndex;
        return true;
      }
    }

    /**
     * Asks all the children of the receiver for their totalChildCount and
     * returns this value (plus stopIndex).
     *
     * @param stopIndex
     *        index to stop on
     * @return childcount of all children of the receiver
     */
//    private int getCountTo(int stopIndex)
//    {
//      TreeStateNode aChild;
//      int retCount = stopIndex + 1;
//
//      for (int counter = 0, maxCounter = getChildCount(); counter < maxCounter; counter++)
//      {
//        aChild = (TreeStateNode) getChildAt(counter);
//        if (aChild.childIndex >= stopIndex)
//          counter = maxCounter;
//        else
//          retCount += aChild.getTotalChildCount();
//      }
//      if (parent != null)
//        return retCount + ((TreeStateNode) getParent()).getCountTo(childIndex);
//      if (!isRootVisible())
//        return (retCount - 1);
//      return retCount;
//    }

    /**
     * Returns the number of children that are expanded to
     * <code>stopIndex</code>. This does not include the number of
     * children that the child at <code>stopIndex</code> might have.
     *
     * @param stopIndex
     *        index to stop on
     * @return the number of children that are expanded to
     *       <code>stopIndex</code>
     */
//    private int getNumExpandedChildrenTo(int stopIndex)
//    {
//      TreeStateNode aChild;
//      int retCount = stopIndex;
//
//      for (int counter = 0, maxCounter = getChildCount(); counter < maxCounter; counter++)
//      {
//        aChild = (TreeStateNode) getChildAt(counter);
//        if (aChild.childIndex >= stopIndex)
//          return retCount;
//        else
//        {
//          retCount += aChild.getTotalChildCount();
//        }
//      }
//      return retCount;
//    }

    /**
     * Messaged when this node either expands or collapses.
     */
//    private void didAdjustTree()
//    {
//    }
  }

  /**
   * Used as a placeholder when getting the path in FHTreeStateNodes.
   */
  private final class SearchInfo implements IClusterable
  {
    private static final long serialVersionUID = 1L;

    private TreeStateNode node;

    private boolean isNodeParentNode;

    private int childIndex;

    private TreePath getPath()
    {
      if (node == null)
      {
        return null;
      }

      if (isNodeParentNode)
      {
        return node.getTreePath().pathByAddingChild(
            treeModel.getChild(node.getUserObject(), childIndex));
      }
      return node.path;
    }
  }

  /**
   * An enumerator to iterate through visible nodes.
   */
  private final class VisibleTreeStateNodeEnumeration implements Enumeration
  {
    /** Parent thats children are being enumerated. */
    private TreeStateNode parent;

    /**
     * Index of next child. An index of -1 signifies parent should be
     * visibled next.
     */
    private int nextIndex;

    /** Number of children in parent. */
    private int childCount;

    private VisibleTreeStateNodeEnumeration(TreeStateNode node)
    {
      this(node, -1);
    }

    private VisibleTreeStateNodeEnumeration(TreeStateNode parent, int startIndex)
    {
      this.parent = parent;
      this.nextIndex = startIndex;
      this.childCount = treeModel.getChildCount(this.parent.getUserObject());
    }

    /**
     * @return true if more visible nodes.
     */
    public boolean hasMoreElements()
    {
      return (parent != null);
    }

    /**
     * @return next visible TreePath.
     */
    public Object nextElement()
    {
      if (!hasMoreElements())
      {
        throw new NoSuchElementException("No more visible paths");
      }

      TreePath retObject;

      if (nextIndex == -1)
      {
        retObject = parent.getTreePath();
      }
      else
      {
        TreeStateNode node = parent.getChildAtModelIndex(nextIndex);

        if (node == null)
        {
          retObject = parent.getTreePath().pathByAddingChild(
              treeModel.getChild(parent.getUserObject(), nextIndex));
        }
        else
        {
          retObject = node.getTreePath();
        }
      }
      updateNextObject();
      return retObject;
    }

    /**
     * Determines the next object by invoking <code>updateNextIndex</code>
     * and if not succesful <code>findNextValidParent</code>.
     */
    private void updateNextObject()
    {
      if (!updateNextIndex())
      {
        findNextValidParent();
      }
    }

    /**
     * Finds the next valid parent, this should be called when nextIndex is
     * beyond the number of children of the current parent.
     *
     * @return whether there is a next valid parent
     */
    private boolean findNextValidParent()
    {
      if (parent == root)
      {
        // mark as invalid!
        parent = null;
        return false;
      }
      while (parent != null)
      {
        TreeStateNode newParent = (TreeStateNode) parent.getParent();

        if (newParent != null)
        {
          nextIndex = parent.childIndex;
          parent = newParent;
          childCount = treeModel.getChildCount(parent.getUserObject());
          if (updateNextIndex())
          {
            return true;
          }
        }
        else
        {
          parent = null;
        }
      }
      return false;
    }

    /**
     * Updates <code>nextIndex</code> returning false if it is beyond the
     * number of children of parent.
     *
     * @return false if it is beyond the number of children of parent
     */
    private boolean updateNextIndex()
    {
      // nextIndex == -1 identifies receiver, make sure is expanded
      // before descend.
      if (nextIndex == -1 && !parent.isExpanded())
      {
        return false;
      }

      // Check that it can have kids
      if (childCount == 0)
      {
        return false;
      }
      // Make sure next index not beyond child count.
      else if (++nextIndex >= childCount)
      {
        return false;
      }

      TreeStateNode child = parent.getChildAtModelIndex(nextIndex);

      if (child != null && child.isExpanded())
      {
        parent = child;
        nextIndex = -1;
        childCount = treeModel.getChildCount(child.getUserObject());
      }
      return true;
    }
  }
}
TOP

Related Classes of org.apache.wicket.extensions.markup.html.tree.TreeState$VisibleTreeStateNodeEnumeration

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.