Clover coverage report - DrJava Test Coverage (drjava-20120304-r5456)
Coverage timestamp: Sun Mar 4 2012 03:13:23 CST
file stats: LOC: 990   Methods: 69
NCLOC: 575   Classes: 2
 
 Source file Conditionals Statements Methods TOTAL
JTreeSortNavigator.java 53.9% 66.8% 62.3% 62.3%
coverage coverage
 1    /*BEGIN_COPYRIGHT_BLOCK
 2    *
 3    * Copyright (c) 2001-2010, JavaPLT group at Rice University (drjava@rice.edu)
 4    * All rights reserved.
 5    *
 6    * Redistribution and use in source and binary forms, with or without
 7    * modification, are permitted provided that the following conditions are met:
 8    * * Redistributions of source code must retain the above copyright
 9    * notice, this list of conditions and the following disclaimer.
 10    * * Redistributions in binary form must reproduce the above copyright
 11    * notice, this list of conditions and the following disclaimer in the
 12    * documentation and/or other materials provided with the distribution.
 13    * * Neither the names of DrJava, the JavaPLT group, Rice University, nor the
 14    * names of its contributors may be used to endorse or promote products
 15    * derived from this software without specific prior written permission.
 16    *
 17    * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 18    * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 19    * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 20    * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
 21    * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 22    * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 23    * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 24    * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 25    * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 26    * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 27    * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 28    *
 29    * This software is Open Source Initiative approved Open Source Software.
 30    * Open Source Initative Approved is a trademark of the Open Source Initiative.
 31    *
 32    * This file is part of DrJava. Download the current version of this project
 33    * from http://www.drjava.org/ or http://sourceforge.net/projects/drjava/
 34    *
 35    * END_COPYRIGHT_BLOCK*/
 36   
 37    package edu.rice.cs.util.docnavigation;
 38   
 39    import javax.swing.*;
 40    import javax.swing.event.TreeSelectionListener;
 41    import javax.swing.event.TreeSelectionEvent;
 42    import javax.swing.event.TreeExpansionListener;
 43    import javax.swing.event.TreeExpansionEvent;
 44    import javax.swing.tree.*;
 45    import java.io.File;
 46    import java.awt.*;
 47    import java.util.*;
 48    import java.awt.dnd.*;
 49    import edu.rice.cs.util.swing.*;
 50    import edu.rice.cs.plt.collect.OneToOneRelation;
 51    import edu.rice.cs.plt.collect.IndexedOneToOneRelation;
 52   
 53    import edu.rice.cs.drjava.DrJavaRoot;
 54   
 55    public class JTreeSortNavigator<ItemT extends INavigatorItem> extends JTree
 56    implements IDocumentNavigator<ItemT>, TreeSelectionListener, TreeExpansionListener,
 57    DropTargetListener {
 58   
 59    /** The model of the tree. */
 60    private final DefaultTreeModel _model;
 61   
 62    /** The currently selected item. Updated by a listener. It is not volatile because all accessed are protected by
 63    * explicit synchronization.
 64    */
 65    private volatile NodeData<ItemT> _current;
 66   
 67    /** Maps documents to tree nodes. */
 68    private final HashMap<ItemT, LeafNode<ItemT>> _doc2node = new HashMap<ItemT, LeafNode<ItemT>>();
 69   
 70    /** Maps path's to nodes and nodes to paths. */
 71    private final OneToOneRelation<String, InnerNode<?, ItemT>> _path2node =
 72    new IndexedOneToOneRelation<String, InnerNode<?, ItemT>>();
 73   
 74    /** The collection of INavigationListeners listening to this JListNavigator */
 75    private final ArrayList<INavigationListener<? super ItemT>> navListeners =
 76    new ArrayList<INavigationListener<? super ItemT>>();
 77   
 78    /** The renderer for this JTree. */
 79    private final CustomTreeCellRenderer _renderer;
 80   
 81    private volatile DisplayManager<? super ItemT> _displayManager;
 82    private volatile Icon _rootIcon;
 83   
 84    private java.util.List<GroupNode<ItemT>> _roots = new LinkedList<GroupNode<ItemT>>();
 85   
 86    /** Sets the foreground color of this JTree
 87    * @param c the color to set to
 88    */
 89  4 public void setForeground(Color c) {
 90  4 super.setForeground(c);
 91  4 if (_renderer != null) _renderer.setTextNonSelectionColor(c);
 92    }
 93   
 94    /** Sets the background color of this tree
 95    * @param c the color for the background
 96    */
 97  14 public void setBackground(Color c) {
 98  14 super.setBackground(c);
 99  4 if (_renderer != null) _renderer.setBackgroundNonSelectionColor(c);
 100    }
 101   
 102    /** Standard constructor.
 103    * @param projRoot the path identifying the root node for the project
 104    */
 105  10 public JTreeSortNavigator(String projRoot) {
 106   
 107  10 super(new DefaultTreeModel(new RootNode<ItemT>(projRoot.substring(projRoot.lastIndexOf(File.separator) + 1))));
 108   
 109  10 addTreeSelectionListener(this);
 110  10 addTreeExpansionListener(this);
 111   
 112  10 _model = (DefaultTreeModel) getModel();
 113  10 _renderer = new CustomTreeCellRenderer();
 114  10 _renderer.setOpaque(false);
 115  10 setCellRenderer(_renderer);
 116    // getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION);
 117  10 getSelectionModel().setSelectionMode(TreeSelectionModel.DISCONTIGUOUS_TREE_SELECTION);
 118  10 setRowHeight(18);
 119    // System.err.println(isEditable());
 120    }
 121   
 122    /** Alternate constructor specifying the display manager that provides icons for the navigator.
 123    * @param projRoot the path identifying the root node for the project
 124    * @param dm the display manager for the navigagtor
 125    */
 126  0 public JTreeSortNavigator(String projRoot, DisplayManager<? super ItemT> dm) {
 127  0 this(projRoot);
 128  0 _displayManager = dm;
 129    }
 130   
 131    /** Sets the display manager that is used to select icons for the leaves of the tree.
 132    * This does not apply to the inner nodes or the root.
 133    */
 134  4 public void setDisplayManager(DisplayManager<? super ItemT> manager) { _displayManager = manager; }
 135   
 136    /** Sets the icon to be displayed at the root of the tree */
 137  4 public void setRootIcon(Icon ico) { _rootIcon = ico; }
 138   
 139    /** @return an AWT component which interacts with this document navigator */
 140  37 public Container asContainer() { return this; }
 141   
 142    /** Adds an <code>IDocument</code> to this navigator. Should only executed from event thread.
 143    * @param doc the document to be added into this navigator.
 144    */
 145  5 public void addDocument(ItemT doc) {
 146  5 assert (EventQueue.isDispatchThread() || Utilities.TEST_MODE);
 147  5 addDocument(doc, "");
 148    }
 149    /** Adds an <code>INavigatorItem</code> to this navigator in the specified position. The behavior of this navigator
 150    * and the position associated with a path are left to the implementing class. Only runs in event-handling thread.
 151    * @param doc the document to be added into this navigator.
 152    * @param path in navigator to parent directory for doc
 153    * @throws IllegalArgumentException if this navigator does not contain <code>relativeto</code> as tested by the
 154    * <code>contains</code> method.
 155    */
 156  78 public void addDocument(ItemT doc, String path) {
 157  78 assert (EventQueue.isDispatchThread() || Utilities.TEST_MODE);
 158  78 synchronized(_model) { // lock for mutation
 159   
 160    /* Identify root matching doc if any */
 161  78 GroupNode<ItemT> root = null;
 162   
 163  78 for (GroupNode<ItemT> r: _roots) {
 164  126 if (r.getFilter().accept(doc)) {
 165  78 root = r;
 166  78 break;
 167    }
 168    }
 169   
 170  0 if (root == null) return;
 171   
 172    /* Embed path in matching root, creating folder nodes if necessary */
 173  78 StringTokenizer tok = new StringTokenizer(path, File.separator);
 174    //ArrayList<String> elements = new ArrayList<String>();
 175  78 final StringBuilder pathSoFarBuf = new StringBuilder();
 176  78 InnerNode<?, ItemT> lastNode = root;
 177  78 while (tok.hasMoreTokens()) {
 178  71 String element = tok.nextToken();
 179  71 pathSoFarBuf.append(element).append('/');
 180  71 String pathSoFar = pathSoFarBuf.toString();
 181  71 InnerNode<?, ItemT> thisNode;
 182    //System.out.println("pathsofar = " + pathSoFar);
 183    // if the node is not in the hashmap yet
 184  71 if (!_path2node.containsFirst(pathSoFar)) {
 185    // make a new node
 186   
 187    /* this inserts a folder node */
 188  33 thisNode = new FileNode<ItemT>(new File(pathSoFar));
 189  33 insertFolderSortedInto(thisNode, lastNode);
 190  33 this.expandPath(new TreePath(lastNode.getPath()));
 191    // associate the path so far with that node
 192  33 _path2node.add(pathSoFar, thisNode);
 193    }
 194    else {
 195    // System.out.println("path2node contains pathSoFar");
 196  38 thisNode = _path2node.value(pathSoFar);
 197    }
 198   
 199  71 lastNode = thisNode;
 200   
 201    //elements.add(element);
 202    }
 203   
 204    /* lastNode is the node of the folder to add into */
 205   
 206  78 LeafNode<ItemT> child = new LeafNode<ItemT>(doc);
 207  78 _doc2node.put(doc, child);
 208  78 insertNodeSortedInto(child, lastNode);
 209    // _hasNonProjFilesOpen = (lastNode == root);
 210    // _model.insertNodeInto(child, lastNode, lastNode.getChildCount());
 211  78 this.expandPath(new TreePath(lastNode.getPath()));
 212    }
 213    }
 214   
 215  24 private void addTopLevelGroupToRoot(InnerNode<?, ItemT> parent) {
 216  24 assert (EventQueue.isDispatchThread() || Utilities.TEST_MODE);
 217  24 synchronized(_model) { // lock for mutation
 218  24 int indexInRoots = _roots.indexOf(parent);
 219  24 int num = _model.getChildCount(_model.getRoot());
 220  24 int i;
 221  24 for (i = 0; i < num; i++) {
 222  15 TreeNode n = (TreeNode)_model.getChild(_model.getRoot(), i);
 223  3 if(_roots.indexOf(n) > indexInRoots) break;
 224    }
 225  24 _model.insertNodeInto(parent, (MutableTreeNode)_model.getRoot(), i);
 226    }
 227    }
 228   
 229    /** Inserts the child node (INavigatorItem) into the sorted position as a parent node's child. Only executes in the
 230    * event thread. Assumes that _model lock is already held.
 231    * @param child the node to add
 232    * @param parent the node to add under
 233    */
 234  78 private void insertNodeSortedInto(LeafNode<ItemT> child, InnerNode<?, ItemT> parent) {
 235  78 int numChildren = parent.getChildCount();
 236  78 String newName = child.toString();
 237  78 String oldName = parent.getUserObject().toString();
 238  78 DefaultMutableTreeNode parentsKid;
 239   
 240    /** Make sure that if the parent is a top level group, it is added to the tree model group. */
 241  78 if (((DefaultMutableTreeNode)_model.getRoot()).getIndex(parent) == -1 && _roots.contains(parent)) {
 242  7 addTopLevelGroupToRoot(parent);
 243    }
 244  78 int i;
 245  78 for (i = 0; i < numChildren; i++ ) {
 246  45 parentsKid = ((DefaultMutableTreeNode) parent.getChildAt(i));
 247  45 if (parentsKid instanceof InnerNode<?,?>) {
 248    // do nothing, it's a folder
 249  45 } else if(parentsKid instanceof LeafNode<?>) {
 250  45 oldName = ((LeafNode<?>)parentsKid).getData().getName();
 251  0 if ((newName.toUpperCase().compareTo(oldName.toUpperCase()) < 0)) break;
 252  0 } else throw new IllegalStateException("found a node in navigator that is not an InnerNode or LeafNode");
 253    }
 254  78 _model.insertNodeInto(child, parent, i);
 255    }
 256   
 257    /** Inserts a folder (String) into sorted position under the parent. Only executes in event thread. Assumes that
 258    * _model lock is already held
 259    * @param child the folder to add
 260    * @param parent the folder to add under
 261    */
 262  33 private void insertFolderSortedInto(InnerNode<?, ItemT> child, InnerNode<?, ItemT> parent) {
 263  33 int numChildren = parent.getChildCount();
 264  33 String newName = child.toString();
 265  33 String oldName = parent.getUserObject().toString();
 266  33 DefaultMutableTreeNode parentsKid;
 267   
 268  33 if (((DefaultMutableTreeNode)_model.getRoot()).getIndex(parent) == -1 && _roots.contains(parent)) {
 269  17 addTopLevelGroupToRoot(parent);
 270    }
 271   
 272  33 int countFolders = 0;
 273  33 int i;
 274  33 for (i = 0; i < numChildren; i++) {
 275  18 parentsKid = ((DefaultMutableTreeNode)parent.getChildAt(i));
 276  18 if (parentsKid instanceof InnerNode<?,?>) {
 277  18 countFolders++;
 278  18 oldName = parentsKid.toString();
 279  0 if ((newName.toUpperCase().compareTo(oldName.toUpperCase()) < 0)) break;
 280    }
 281  0 else if (parentsKid instanceof LeafNode<?>) break;
 282    // we're out of folders, and starting into the files, so just break out.
 283  0 else throw new IllegalStateException("found a node in navigator that is not an InnerNode or LeafNode");
 284    }
 285  33 _model.insertNodeInto(child, parent, i);
 286    }
 287   
 288    /** Removes a given <code>INavigatorItem<code> from this navigator. Removes all <code>INavigatorItem</code>s
 289    * from this navigator that are "equal" (using <code>.equals(...)</code>) to the passed argument. Any of
 290    * the removed documents may be returned by this method. If the NavigatorItem is found in the navigator, null
 291    * is returned. Only executes from event thread.
 292    * @param doc the docment to be removed
 293    * @return doc a document removed from this navigator as a result of invoking this method.
 294    * @throws IllegalArgumentException if this navigator contains no document equal to doc
 295    */
 296  10 public ItemT removeDocument(ItemT doc) {
 297  10 assert (EventQueue.isDispatchThread() || Utilities.TEST_MODE);
 298  10 synchronized(_model) { // lock for mutation
 299  10 LeafNode<ItemT> toRemove = getNodeForDoc(doc);
 300  0 if (toRemove == null) return null;
 301  10 return removeNode(getNodeForDoc(doc));
 302    }
 303    }
 304   
 305    /** Assumes lock on _model is already held or that it is being run in the event thread. */
 306  20 private LeafNode<ItemT> getNodeForDoc(ItemT doc) {
 307    // synchronized(_model) {
 308  20 return _doc2node.get(doc);
 309    // }
 310    }
 311   
 312    /** Only takes in nodes that have an INavigatorItem as their object; assumes _model lock is already held.
 313    * Only executes in event thread. */
 314  11 private ItemT removeNode(LeafNode<ItemT> node) {
 315  11 DefaultMutableTreeNode parent = (DefaultMutableTreeNode)node.getParent();
 316  11 _model.removeNodeFromParent(node);
 317  11 _doc2node.remove(node.getData());
 318  11 cleanFolderNode(parent);
 319  11 return node.getData();
 320    }
 321   
 322    /** If the given node is an InnerNode with no children, it removes it from the tree. If the given node is a leaf or
 323    * the root, it does nothing to it. Assumes that _model lock is already held. Only executes in the event thread.
 324    */
 325  22 private void cleanFolderNode(DefaultMutableTreeNode node) {
 326  22 if (node instanceof InnerNode<?,?> && node.getChildCount() == 0) {
 327  11 DefaultMutableTreeNode parent = (DefaultMutableTreeNode)node.getParent();
 328  11 _model.removeNodeFromParent(node);
 329  11 @SuppressWarnings("unchecked") InnerNode<?, ItemT> typedNode = (InnerNode<?, ItemT>) node;
 330  11 _path2node.remove(_path2node.antecedent(typedNode), typedNode);
 331  11 cleanFolderNode(parent);
 332    }
 333    }
 334   
 335    /** Resets a given <code>INavigatorItem<code> in the tree. Updates the placement of the item and its display
 336    * to reflect any changes made in the model. Only executes in the event thread.
 337    * Note: full synchronization commented out because this operation is only performed in the event thread. The
 338    * synchronized sections must be atomic but the rest of the code can run concurrently with read operations in
 339    * other threads.
 340    * @param doc the document to be refreshed
 341    * @param path the path to the parent folder for this document
 342    * @throws IllegalArgumentException if this navigator contains no document equal to doc.
 343    */
 344  2 public void refreshDocument(ItemT doc, String path) {
 345  2 assert (EventQueue.isDispatchThread() || Utilities.TEST_MODE);
 346    // synchronized(_model) {
 347  2 LeafNode<ItemT> node = _doc2node.get(doc);
 348  2 InnerNode<?, ?> oldParent;
 349  2 if (node == null) { // document has not yet been entered in tree
 350  0 addDocument(doc, path);
 351  0 return;
 352    }
 353   
 354  2 InnerNode<?, ?> p = (InnerNode<?, ?>) node.getParent();
 355  2 oldParent = p;
 356   
 357    // Check to see if the new parent (could be same) exists already
 358  2 String newPath = path;
 359   
 360  2 if (newPath.length() > 0) {
 361  0 if (newPath.substring(0,1).equals("/")) newPath = newPath.substring(1);
 362  1 if (! newPath.substring(newPath.length() - 1).equals("/")) newPath = newPath + "/";
 363    }
 364   
 365  2 InnerNode<?, ItemT> newParent = _path2node.value(newPath); // node that should be parent
 366   
 367  2 if (newParent == oldParent) { // no mutation has occurred before this point because oldParent != null
 368  1 if (! node.toString().equals(doc.getName())) { // document has changed name?
 369  0 synchronized(_model) {
 370  0 LeafNode<ItemT> newLeaf = new LeafNode<ItemT>(doc);
 371  0 _doc2node.put(doc, newLeaf);
 372  0 insertNodeSortedInto(newLeaf, newParent);
 373  0 _model.removeNodeFromParent(node);
 374    }
 375    }
 376    // don't do anything if its name or parents haven't changed
 377    }
 378    else { // document has moved within tree
 379  1 synchronized(_model) {
 380  1 removeNode(node);
 381  1 addDocument(doc, path);
 382    }
 383    }
 384    // }
 385    }
 386   
 387    /** Sets the specified document to be active (current). Only executes in the event thread. */
 388  11 public void selectDocument(ItemT doc) {
 389  11 assert EventQueue.isDispatchThread();
 390    // synchronized(_model) { // lock out mutation
 391  11 DefaultMutableTreeNode node = _doc2node.get(doc);
 392  0 if (node == null) return; // doc is not in the navigator
 393  4 if (node == _current) return; // current doc is the active doc
 394    // if (_doc2node.containsKey(doc);) { // this test is obviously true since node == _doc2node.get(doc)
 395  7 TreeNode[] nodes = node.getPath();
 396  7 TreePath path = new TreePath(nodes);
 397  7 expandPath(path);
 398  7 setSelectionPath(path); // fires _gainVisitor in AbstractGlobalModel
 399  7 scrollPathToVisible(path);
 400    // }
 401    // }
 402    }
 403   
 404    /** Returns a typed equivalent to {@code next.getUserObject()}. Assumes the DefaultMutableTreeNode
 405    * is a leaf node in _model and thus, if parameterized, would have type ItemT. This is a workaround for
 406    * the lack of a generic implementation of TreeModel and TreeNode. If those classes become generified,
 407    * this code will no longer be necessary.
 408    */
 409  101 private ItemT getNodeUserObject(DefaultMutableTreeNode n) {
 410  101 @SuppressWarnings("unchecked") ItemT result = (ItemT) n.getUserObject();
 411  101 return result;
 412    }
 413   
 414    /** Returns the next document in the collection (using enumeration order). Executes in any thread.
 415    * @param doc the INavigatorItem of interest
 416    * @return the INavigatorItem which comes after doc
 417    */
 418  3 public ItemT getNext(ItemT doc) {
 419  3 synchronized(_model) { // locks out mutation
 420  3 DefaultMutableTreeNode node = _doc2node.get(doc);
 421  0 if (node == null) return doc; // doc may not be contained in navigator
 422    // TODO: check for "package" case
 423  3 DefaultMutableTreeNode next = node.getNextLeaf();
 424  0 if (next == null || next == _model.getRoot()) { return doc; }
 425  3 else { return getNodeUserObject(next); }
 426    }
 427    }
 428   
 429    /** Returns the previous document in the collection (using enumeration order). Executes in any thread.
 430    * @param doc the INavigatorItem of interest
 431    * @return the INavigatorItem which comes before doc
 432    */
 433  3 public ItemT getPrevious(ItemT doc) {
 434  3 synchronized(_model) { // locks out mutation
 435  3 DefaultMutableTreeNode node = _doc2node.get(doc);
 436  0 if (node == null) return doc; // doc may not be contained in navigator
 437    // TODO: check for "package" case
 438  3 DefaultMutableTreeNode prev = node.getPreviousLeaf();
 439  0 if (prev == null || prev == _model.getRoot()) { return doc; }
 440  3 else { return getNodeUserObject(prev); }
 441    }
 442    }
 443   
 444    /** Returns the first document in the collection (using enumeration order). Executes in any thread.
 445    * @return the INavigatorItem which comes before doc
 446    */
 447  1 public ItemT getFirst() {
 448  1 synchronized(_model) { // locks out mutation
 449  1 DefaultMutableTreeNode root = (DefaultMutableTreeNode) _model.getRoot();
 450  1 return getNodeUserObject(root.getFirstLeaf());
 451    }
 452    }
 453   
 454    /** Returns the last document in the collection (using enumeration order). Executes in any thread.
 455    * @return the INavigatorItem which comes before doc
 456    */
 457  1 public ItemT getLast() {
 458  1 synchronized(_model) { // locks out mutation
 459  1 DefaultMutableTreeNode root = (DefaultMutableTreeNode) _model.getRoot();
 460  1 return getNodeUserObject(root.getLastLeaf());
 461    }
 462    }
 463   
 464    /** Tests to see if a given document is contained in this navigator. Executes in any thread.
 465    * @param doc the document to test for containment.
 466    * @return <code>true</code> if this navigator contains a document that is "equal" (as tested by the
 467    * <code>equals</code< method) to the passed document, else <code>false</code>.
 468    */
 469  0 public boolean contains(ItemT doc) {
 470  0 synchronized(_model) { return _doc2node.containsKey(doc); } // locks out mutation
 471    }
 472   
 473    /** Tests to see if a given document is contained in this navigator. Only executes in event thread.*/
 474  0 public boolean _contains(ItemT doc) { return _doc2node.containsKey(doc); }
 475   
 476    /** Returns all the <code>IDocuments</code> contained in this navigator. Does not assert any type of ordering on
 477    * the returned structure. Executes in any thread.
 478    * @return an <code>INavigatorItem<code> enumeration of this navigator's contents.
 479    */
 480  2 public ArrayList<ItemT> getDocuments() {
 481   
 482  2 final ArrayList<ItemT> list = new ArrayList<ItemT>(getDocumentCount()); // Use Vector because it implements an Enumeration
 483   
 484  2 synchronized(_model) { // locks out mutation
 485    // e has a raw type because depthFirstEnumeration() has a raw type signature
 486  2 Enumeration<?> e = ((DefaultMutableTreeNode)_model.getRoot()).depthFirstEnumeration();
 487   
 488  2 while(e.hasMoreElements()) {
 489  6 DefaultMutableTreeNode node = (DefaultMutableTreeNode) e.nextElement();
 490  6 if (node.isLeaf() && node != _model.getRoot()) {
 491  2 list.add(getNodeUserObject(node));
 492    }
 493    }
 494    }
 495  2 return list;
 496    }
 497   
 498    /** Returns all the <code>IDocuments</code> contained in the specified bin.
 499    * @param binName name of bin
 500    * @return an <code>INavigatorItem<code> enumeration of this navigator's contents.
 501    */
 502  2 public ArrayList<ItemT> getDocumentsInBin(String binName) {
 503  2 final ArrayList<ItemT> list = new ArrayList<ItemT>();
 504   
 505  2 synchronized(_model) { // locks out mutation
 506  2 for(GroupNode<ItemT> gn: _roots) {
 507  4 if (gn.getData().equals(binName)) {
 508    // found the bin with the right name
 509    // e has a raw type because depthFirstEnumeration() has a raw type signature
 510  2 Enumeration<?> e = gn.depthFirstEnumeration();
 511   
 512  2 while(e.hasMoreElements()) {
 513  15 DefaultMutableTreeNode node = (DefaultMutableTreeNode) e.nextElement();
 514  15 if (node.isLeaf() && node != _model.getRoot()) {
 515  9 list.add(getNodeUserObject(node));
 516    }
 517    }
 518    }
 519    }
 520    }
 521   
 522  2 return list;
 523    }
 524   
 525    /** Returns the number of <code>IDocuments</code> contained by this <code>IDocumentNavigator</code>
 526    * Not synchronized on the assumption that size field of a HashMap always has a legitimate
 527    * value (either the size of the current state or the size of its state before some concurrent
 528    * operation started. Executes in any thread. Assume size() always returns a valid (perhaps stale) value.
 529    * @return the number of documents within this navigator.
 530    */
 531  3 public int getDocumentCount() { return _doc2node.size(); }
 532   
 533    /** Returns whether this <code>IDocumentNavigator</code> contains any <code>IDocuments</code>.
 534    * @return <code>true</code> if this navigator contains one or more documents, else <code>false</code>.
 535    * Executes in any thread. Assume isEmpty() always returns a valid (perhaps stale) value.
 536    */
 537  0 public boolean isEmpty() { return _doc2node.isEmpty(); }
 538   
 539    /** Removes all <code>IDocuments</code> from this <code>IDocumentNavigator</code>. Only executes in event thread. */
 540  2 public void clear() {
 541  2 assert (EventQueue.isDispatchThread() || Utilities.TEST_MODE);
 542  2 synchronized(_model) {
 543  2 _doc2node.clear();
 544  2 ((DefaultMutableTreeNode)_model.getRoot()).removeAllChildren();
 545    }
 546    }
 547   
 548    /** Adds an <code>INavigationListener</code> to this navigator. After invoking this method, the passed
 549    * listener will be eligible for observing this navigator. If the provided listener is already observing
 550    * this navigator (as tested by the == operator), no action is taken. Only executes in event thread.
 551    * @param listener the listener to be added to this navigator.
 552    */
 553  3 public void addNavigationListener(INavigationListener<? super ItemT> listener) {
 554  3 assert (EventQueue.isDispatchThread() || Utilities.TEST_MODE);
 555  3 synchronized(_model) { navListeners.add(listener); } // locks out access during mutation
 556    }
 557   
 558    /** Removes the given listener from observing this navigator. After invoking this method, all observers
 559    * watching this navigator "equal" (as tested by the == operator) will no longer receive observable dispatches.
 560    * Only executes in event thread.
 561    * @param listener the listener to be removed from this navigator
 562    */
 563  0 public void removeNavigationListener(INavigationListener<? super ItemT> listener) {
 564  0 assert (EventQueue.isDispatchThread() || Utilities.TEST_MODE);
 565  0 synchronized(_model) { navListeners.remove(listener); }
 566    }
 567   
 568    /** Returns a collection of all navigator listeners. Note: this is a dangerous method since it exposes a shared data
 569    * structure that must be synchronized with _model.
 570    */
 571  2 public Collection<INavigationListener<? super ItemT>> getNavigatorListeners() { return navListeners; }
 572   
 573    /** Standard visitor pattern. Only used within this class.
 574    * @param algo the visitor to run
 575    * @param input the input for the visitor
 576    */
 577  0 public <InType, ReturnType> ReturnType execute(IDocumentNavigatorAlgo<ItemT, InType, ReturnType> algo, InType input) {
 578  0 return algo.forTree(this, input);
 579    }
 580   
 581    /** Called whenever the value of the selection changes. Only runs in event thread. Runs _gainVisitor in global model
 582    * @param e the event that characterizes the change.
 583    */
 584  41 public void valueChanged(TreeSelectionEvent e) {
 585  41 Object treeNode = this.getLastSelectedPathComponent();
 586  17 if (treeNode == null || ! (treeNode instanceof NodeData<?>)) return;
 587  24 @SuppressWarnings("unchecked") NodeData<ItemT> newSelection = (NodeData<ItemT>) treeNode;
 588  24 if (_current != newSelection) {
 589  16 for(INavigationListener<? super ItemT> listener : navListeners) {
 590  6 listener.lostSelection(_current, isNextChangeModelInitiated());
 591  6 listener.gainedSelection(newSelection, isNextChangeModelInitiated());
 592    }
 593  16 _current = newSelection;
 594    }
 595   
 596  24 setNextChangeModelInitiated(false);
 597    }
 598   
 599    /** Returns a renderer for this object. */
 600  0 public Component getRenderer() { return _renderer; }
 601   
 602    /** The cell renderer for this tree. Only runs in event thread. */
 603    private class CustomTreeCellRenderer extends DefaultTreeCellRenderer {
 604   
 605    /** Rreturns the component for a cell
 606    * @param tree
 607    */
 608  302 public Component getTreeCellRendererComponent(JTree tree, Object value, boolean sel, boolean isExpanded,
 609    boolean leaf, int row, boolean hasFocus) {
 610   
 611    // changing last argument from hasFocus to false appears to fix a focus bug when selecting a new document
 612  302 super.getTreeCellRendererComponent(tree, value, sel, isExpanded, leaf, row, false);
 613   
 614  302 DefaultMutableTreeNode node = (DefaultMutableTreeNode) value;
 615  2 if (node instanceof RootNode<?> && _rootIcon != null) setIcon(_rootIcon);
 616   
 617  300 else if (node instanceof LeafNode<?>) {
 618  82 ItemT doc = getNodeUserObject(node);
 619  82 if (leaf && _displayManager != null) {
 620  8 setIcon(_displayManager.getIcon(doc));
 621  8 setText(_displayManager.getName(doc));
 622    }
 623    }
 624  302 return this;
 625    }
 626    }
 627   
 628    /** Selects the document at the x,y coordinate of the navigator pane and installs it as the active document. Only
 629    * runs in event thread.
 630    * @param x the x coordinate of the navigator pane
 631    * @param y the y coordinate of the navigator pane
 632    */
 633  0 public boolean selectDocumentAt(int x, int y) {
 634  0 TreePath path = getPathForLocation(x, y);
 635  0 if (path == null) return false;
 636    else {
 637  0 DefaultMutableTreeNode node = (DefaultMutableTreeNode)path.getLastPathComponent();
 638  0 if (node instanceof LeafNode<?>) {
 639  0 this.expandPath(path);
 640  0 this.setSelectionPath(path);
 641  0 this.scrollPathToVisible(path);
 642  0 return true;
 643    }
 644  0 else if (node instanceof InnerNode<?,?>) {
 645  0 this.expandPath(path);
 646  0 this.setSelectionPath(path);
 647  0 this.scrollPathToVisible(path);
 648  0 return true;
 649    }
 650  0 else if (node instanceof RootNode<?>) {
 651  0 this.expandPath(path);
 652  0 this.setSelectionPath(path);
 653  0 this.scrollPathToVisible(path);
 654  0 return true;
 655    }
 656  0 else return false;
 657    }
 658    // }
 659    }
 660   
 661    /** Returns true if the item at the x,y coordinate of the navigator pane is currently selected.
 662    * Only runs in event thread. O
 663    * @param x the x coordinate of the navigator pane
 664    * @param y the y coordinate of the navigator pane
 665    * @return true if the item is currently selected
 666    */
 667  0 public boolean isSelectedAt(int x, int y) {
 668  0 TreePath path = getPathForLocation(x, y);
 669  0 if (path == null) return false;
 670  0 TreePath[] ps = getSelectionPaths();
 671  0 if (ps == null) { return false; }
 672  0 for(TreePath p: ps) {
 673  0 if (path.equals(p)) { return true; }
 674    }
 675  0 return false;
 676    }
 677   
 678    /** @return true if at least one group of INavigatorItems is selected. Only runs in event thread. */
 679  0 public boolean isGroupSelected() { return getGroupSelectedCount() != 0; }
 680   
 681    /** @return the number of groups selected. Only runs in event thread. */
 682  0 public int getGroupSelectedCount() {
 683  0 assert (EventQueue.isDispatchThread() || Utilities.TEST_MODE);
 684  0 int count = 0;
 685  0 TreePath[] ps = getSelectionPaths();
 686  0 if (ps == null) { return 0; }
 687  0 for(TreePath p: ps) {
 688  0 TreeNode n = (TreeNode) p.getLastPathComponent();
 689  0 if (n instanceof InnerNode<?,?>) { ++count; }
 690    }
 691  0 return count;
 692    }
 693   
 694    /** @return the folders currently selected. Only runs in event thread. */
 695  0 public java.util.List<File> getSelectedFolders() {
 696  0 assert (EventQueue.isDispatchThread() || Utilities.TEST_MODE);
 697  0 ArrayList<File> l = new ArrayList<File>();
 698  0 TreePath[] ps = getSelectionPaths();
 699  0 if (ps == null) { return l; }
 700  0 for(TreePath p: ps) {
 701  0 TreeNode n = (TreeNode) p.getLastPathComponent();
 702  0 if (n instanceof FileNode<?>) {
 703  0 l.add(((FileNode<?>)n).getData());
 704    }
 705    }
 706  0 return l;
 707    }
 708   
 709    /** @return true if at least one document is selected. Only runs in event thread. */
 710  0 public boolean isDocumentSelected() { return getDocumentSelectedCount() != 0; }
 711   
 712    /** @return the number of documents selected. Only runs in event thread. */
 713  0 public int getDocumentSelectedCount() {
 714  0 assert (EventQueue.isDispatchThread() || Utilities.TEST_MODE);
 715  0 int count = 0;
 716  0 TreePath[] ps = getSelectionPaths();
 717  0 if (ps == null) { return 0; }
 718  0 for(TreePath p: ps) {
 719  0 TreeNode n = (TreeNode) p.getLastPathComponent();
 720  0 if (n instanceof LeafNode<?>) {
 721  0 ++count;
 722    }
 723    }
 724  0 return count;
 725    }
 726   
 727    /** @return the documents currently selected. Only runs in event thread. */
 728  9 @SuppressWarnings("unchecked") public java.util.List<ItemT> getSelectedDocuments() {
 729  9 assert (EventQueue.isDispatchThread() || Utilities.TEST_MODE);
 730  9 ArrayList<ItemT> l = new ArrayList<ItemT>();
 731  9 TreePath[] ps = getSelectionPaths();
 732  0 if (ps == null) { return l; }
 733  9 for(TreePath p: ps) {
 734  39 TreeNode n = (TreeNode) p.getLastPathComponent();
 735  39 if (n instanceof LeafNode) {
 736  27 l.add((ItemT)((LeafNode)n).getData());
 737    }
 738    }
 739  9 return l;
 740    }
 741   
 742    /** Returns true if at least one top level group is selected. Only runs in event thread. */
 743  0 public boolean isTopLevelGroupSelected() {
 744  0 assert (EventQueue.isDispatchThread() || Utilities.TEST_MODE);
 745  0 TreePath[] ps = getSelectionPaths();
 746  0 if (ps == null) { return false; }
 747  0 for(TreePath p: ps) {
 748  0 TreeNode n = (TreeNode) p.getLastPathComponent();
 749  0 if (n instanceof GroupNode<?>) { return true; }
 750    }
 751  0 return false;
 752    }
 753   
 754    /** Returns true if the root is selected. Only runs in event thread. */
 755  0 public boolean isRootSelected() {
 756  0 assert (EventQueue.isDispatchThread() || Utilities.TEST_MODE);
 757  0 TreePath[] ps = getSelectionPaths();
 758  0 if (ps == null) { return false; }
 759  0 for(TreePath p: ps) {
 760  0 TreeNode n = (TreeNode) p.getLastPathComponent();
 761  0 if (n == _model.getRoot()) { return true; }
 762    }
 763  0 return false;
 764    }
 765   
 766    /** Returns the names of the top level groups that the selected items descend from. Only runs in event thread. */
 767  10 public java.util.Set<String> getNamesOfSelectedTopLevelGroup() throws GroupNotSelectedException {
 768  10 assert (EventQueue.isDispatchThread() || Utilities.TEST_MODE);
 769   
 770  10 HashSet<String> names = new HashSet<String>();
 771  10 LinkedList<GroupNode<ItemT>> roots = new LinkedList<GroupNode<ItemT>>(_roots);
 772   
 773  10 TreePath[] ps = getSelectionPaths();
 774  10 if (ps != null) {
 775  9 for(TreePath p: ps) {
 776  40 if (p.getLastPathComponent() instanceof DefaultMutableTreeNode) {
 777  40 DefaultMutableTreeNode n = (DefaultMutableTreeNode) p.getLastPathComponent();
 778   
 779  40 for(GroupNode<ItemT> gn: roots) {
 780  44 if (gn.isNodeDescendant(n)) {
 781    // n is a descendent of gn; add the name of the group node
 782  10 names.add(gn.getData());
 783    // this group node definitely contains selected items, no need to check it again;
 784    // remove it from the list of roots to consider
 785  10 roots.remove(gn);
 786  10 break;
 787    }
 788    }
 789    }
 790    }
 791    }
 792   
 793  1 if (names.isEmpty()) { throw new GroupNotSelectedException("there is no top level group for the root of the tree"); }
 794   
 795  9 return names;
 796    }
 797   
 798    /** Returns the currently selected leaf node, or null if the selected node is not a leaf. Only reads a single
 799    * volatile field that always has a valid value. Thread safe.
 800    */
 801  1 public ItemT getCurrent() {
 802  1 NodeData<ItemT> current = _current;
 803  0 if (current == null) return null;
 804  1 return current.execute(_leafVisitor);
 805    }
 806   
 807    /** Returns the model lock. */
 808  0 public Object getModelLock() { return _model; }
 809   
 810    // This visitor is executed in getCurrent(). The case methods for files and strings return
 811    // null, which caused DrJava to revert to the previous selection in requestSelectionUpdate().
 812    // That code has now been commented out; this note is only for documentation purposes.
 813    private final NodeDataVisitor<ItemT, ItemT> _leafVisitor = new NodeDataVisitor<ItemT, ItemT>() {
 814  0 public ItemT fileCase(File f, Object... p){ return null; }
 815  0 public ItemT stringCase(String s, Object... p){ return null; }
 816  1 public ItemT itemCase(ItemT ini, Object... p){ return ini; }
 817    };
 818   
 819    /** @return true if the INavigatorItem is in a selected group, if at least
 820    * one group is selected. Only runs in event thread. */
 821  0 public boolean isSelectedInGroup(ItemT i) {
 822  0 assert (EventQueue.isDispatchThread() || Utilities.TEST_MODE);
 823   
 824  0 TreePath[] ps = getSelectionPaths();
 825  0 if (ps == null) { return false; }
 826  0 for(TreePath p: ps) {
 827  0 TreeNode n = (TreeNode) p.getLastPathComponent();
 828  0 TreeNode l = _doc2node.get(i);
 829   
 830  0 if (n == _model.getRoot()) return true;
 831   
 832  0 while (l.getParent() != _model.getRoot()) {
 833  0 if(l.getParent() == n) return true;
 834  0 l = l.getParent();
 835    }
 836    }
 837   
 838  0 return false;
 839    }
 840   
 841    /** Adds a top level group to the navigator. Only runs in event thread. */
 842  23 public void addTopLevelGroup(String name, INavigatorItemFilter<? super ItemT> f){
 843  23 assert (EventQueue.isDispatchThread() || Utilities.TEST_MODE);
 844   
 845  23 if (f == null)
 846  0 throw new IllegalArgumentException("parameter 'f' is not allowed to be null");
 847  23 GroupNode<ItemT> n = new GroupNode<ItemT>(name, f);
 848  23 _roots.add(n);
 849    }
 850   
 851    /******* Methods that handle expansion/collapsing of folders in tree **********/
 852   
 853    /** Called whenever an item in the tree has been collapsed. Only runs in event thread (except when testing). */
 854  2 public void treeCollapsed(TreeExpansionEvent event) {
 855  2 assert (EventQueue.isDispatchThread() || Utilities.TEST_MODE);
 856   
 857  2 Object o = event.getPath().getLastPathComponent();
 858  2 if (o instanceof InnerNode<?,?>) ((InnerNode<?,?>)o).setCollapsed(true);
 859    }
 860   
 861    /** Called whenever an item in the tree has been expanded. Only runs in event thread. */
 862  67 public void treeExpanded(TreeExpansionEvent event) {
 863  67 assert (EventQueue.isDispatchThread() || Utilities.TEST_MODE);
 864   
 865  67 Object o = event.getPath().getLastPathComponent();
 866  57 if (o instanceof InnerNode<?,?>) ((InnerNode<?,?>)o).setCollapsed(false);
 867    }
 868   
 869    /** Collapses all the paths in the tree that match one of the path strings included in the given hash set. Path
 870    * strings must follow a specific format in order for them to work. See the documentation of
 871    * <code>generatePathString</code> for information on the format of the path strings. Only executes in event thread.
 872    * @param paths A hash set of path strings.
 873    */
 874  3 public void collapsePaths(String[] paths) {
 875  3 assert (EventQueue.isDispatchThread() || Utilities.TEST_MODE);
 876   
 877  3 HashSet<String> set = new HashSet<String>();
 878  0 for (String s : paths) { set.add(s); }
 879  3 collapsePaths(set);
 880    }
 881   
 882    /** Set variation of collapsePaths(String ...). Private except for testing code. Only runs in event thread except
 883    * for testing code.
 884    */
 885  4 void collapsePaths(HashSet<String> paths) {
 886   
 887  4 DefaultMutableTreeNode rootNode = (DefaultMutableTreeNode)_model.getRoot();
 888    // We use a raw type here because depthFirstEnumeration() has a raw type signature
 889  4 Enumeration<?> nodes = rootNode.depthFirstEnumeration();
 890    // ArrayList<String> list = new ArrayList<String>();
 891  4 while (nodes.hasMoreElements()) {
 892  31 DefaultMutableTreeNode tn = (DefaultMutableTreeNode)nodes.nextElement();
 893  31 if (tn instanceof InnerNode<?,?>) {
 894  12 TreePath tp = new TreePath(tn.getPath());
 895  12 String s = generatePathString(tp);
 896  12 boolean shouldCollapse = paths.contains(s);
 897  2 if (shouldCollapse) { collapsePath(tp); }
 898    }
 899    }
 900    }
 901   
 902    /** @return an array of path strings corresponding to the paths of the tree nodes that
 903    * are currently collapsed. See the documentation of <code>generatePathString</code>
 904    * for information on the format of the path strings. Only runs in event thread (except when testing).
 905    */
 906  2 public String[] getCollapsedPaths() {
 907  2 assert (EventQueue.isDispatchThread() || Utilities.TEST_MODE);
 908   
 909  2 ArrayList<String> list = new ArrayList<String>();
 910    // synchronized(_model) {
 911  2 DefaultMutableTreeNode rootNode = (DefaultMutableTreeNode)_model.getRoot();
 912    // We use a raw type here because depthFirstEnumeration() has a raw type signature
 913  2 Enumeration<?> nodes = rootNode.depthFirstEnumeration(); /** This warning is expected **/
 914  2 while (nodes.hasMoreElements()) {
 915  23 DefaultMutableTreeNode tn = (DefaultMutableTreeNode)nodes.nextElement();
 916  23 if (tn instanceof InnerNode<?,?> && ((InnerNode<?,?>)tn).isCollapsed()) {
 917  2 TreePath tp = new TreePath(tn.getPath());
 918  2 list.add(generatePathString(tp));
 919    }
 920    }
 921    // }
 922  2 return list.toArray(new String[list.size()]);
 923    }
 924   
 925    /** Generates a path string for the given tree node. <p>The path string does not include the project
 926    * root node, but rather a period in its place. Following the "./" is one of the 3 main groups,
 927    * "[ Source Files ]", "[ Auxiliary ]", "[ External ]". The nodes in the path are represented by their
 928    * names delimited by the forward slash ("/"). The path ends with a final delimeter.
 929    * (e.g. "./[ Source Files ]/util/docnavigation/") Only runs in event thread.
 930    * @return the path string for the given node in the JTree
 931    */
 932  18 public String generatePathString(TreePath tp) {
 933  18 String path = "";
 934    // synchronized(_model) {
 935  18 TreeNode root = (TreeNode) _model.getRoot();
 936   
 937  18 while (tp != null) {
 938  45 TreeNode curr = (TreeNode) tp.getLastPathComponent();
 939  18 if (curr == root) path = "./" + path;
 940  27 else path = curr + "/" + path;
 941  45 tp = tp.getParentPath();
 942    // }
 943    }
 944   
 945  18 return path;
 946    }
 947   
 948    /** If the currently selected item is not an INavigatorItem, select the one given. Only runs in event thread. */
 949  0 public void requestSelectionUpdate(ItemT ini) {
 950    // synchronized(_model) {
 951    // This code checked if the selected node was a leaf, i.e. a Java source file,
 952    // and if it wasn't, reverts the selection. The code was commented out to allow
 953    // selection of file nodes (i.e. folders) and string nodes (i.e. [ Source Files ], etc.)
 954    // if (getCurrent() == null) { // the currently selected node is not a leaf
 955    // setActiveDoc(ini);
 956    // }
 957    // }
 958    }
 959   
 960    /** Marks the next selection change as model-initiated (true) or user-initiated (false; default). */
 961  31 public void setNextChangeModelInitiated(boolean b) {
 962  31 putClientProperty(MODEL_INITIATED_PROPERTY_NAME, b?Boolean.TRUE:null);
 963    }
 964   
 965    /** @return whether the next selection change is model-initiated (true) or user-initiated (false). */
 966  12 public boolean isNextChangeModelInitiated() {
 967  12 return getClientProperty(MODEL_INITIATED_PROPERTY_NAME) != null;
 968    }
 969   
 970    // /** Unnecessary since "modified" mark is added by the cell renderer */
 971    // public void activeDocumentModified() { }
 972   
 973    /** Drag and drop target. */
 974    DropTarget dropTarget = new DropTarget(this, this);
 975   
 976    /** User dragged something into the component. */
 977  0 public void dragEnter(DropTargetDragEvent dropTargetDragEvent) {
 978  0 DrJavaRoot.dragEnter(dropTargetDragEvent);
 979    }
 980   
 981  0 public void dragExit(DropTargetEvent dropTargetEvent) { }
 982  0 public void dragOver(DropTargetDragEvent dropTargetDragEvent) { }
 983  0 public void dropActionChanged(DropTargetDragEvent dropTargetDragEvent){ }
 984   
 985    /** User dropped something on the component. Only runs in event thread. */
 986  0 public /* synchronized */ void drop(DropTargetDropEvent dropTargetDropEvent) {
 987  0 DrJavaRoot.drop(dropTargetDropEvent);
 988    }
 989    }
 990