0001: /*BEGIN_COPYRIGHT_BLOCK
0002: *
0003: * Copyright (c) 2001-2007, JavaPLT group at Rice University (javaplt@rice.edu)
0004: * All rights reserved.
0005: *
0006: * Redistribution and use in source and binary forms, with or without
0007: * modification, are permitted provided that the following conditions are met:
0008: * * Redistributions of source code must retain the above copyright
0009: * notice, this list of conditions and the following disclaimer.
0010: * * Redistributions in binary form must reproduce the above copyright
0011: * notice, this list of conditions and the following disclaimer in the
0012: * documentation and/or other materials provided with the distribution.
0013: * * Neither the names of DrJava, the JavaPLT group, Rice University, nor the
0014: * names of its contributors may be used to endorse or promote products
0015: * derived from this software without specific prior written permission.
0016: *
0017: * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
0018: * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
0019: * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
0020: * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
0021: * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
0022: * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
0023: * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
0024: * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
0025: * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
0026: * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
0027: * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
0028: *
0029: * This software is Open Source Initiative approved Open Source Software.
0030: * Open Source Initative Approved is a trademark of the Open Source Initiative.
0031: *
0032: * This file is part of DrJava. Download the current version of this project
0033: * from http://www.drjava.org/ or http://sourceforge.net/projects/drjava/
0034: *
0035: * END_COPYRIGHT_BLOCK*/
0036:
0037: package edu.rice.cs.util.docnavigation;
0038:
0039: import javax.swing.*;
0040: import javax.swing.event.TreeSelectionListener;
0041: import javax.swing.event.TreeSelectionEvent;
0042: import javax.swing.event.TreeExpansionListener;
0043: import javax.swing.event.TreeExpansionEvent;
0044: import javax.swing.tree.*;
0045: import java.io.File;
0046: import java.awt.*;
0047: import java.util.*;
0048: import java.awt.dnd.*;
0049: import java.awt.datatransfer.*;
0050: import java.io.File;
0051: import edu.rice.cs.util.*;
0052: import edu.rice.cs.util.swing.*;
0053: import edu.rice.cs.drjava.DrJavaRoot;
0054:
0055: public class JTreeSortNavigator<ItemT extends INavigatorItem> extends
0056: JTree implements IDocumentNavigator<ItemT>,
0057: TreeSelectionListener, TreeExpansionListener,
0058: DropTargetListener {
0059:
0060: /** The model of the tree. */
0061: private final DefaultTreeModel _model;
0062:
0063: /** The currently selected item. Updated by a listener. It is not volatile because all accessed are protected by
0064: * explicit synchronization.
0065: */
0066: private volatile NodeData<ItemT> _current;
0067:
0068: /** Maps documents to tree nodes. */
0069: private final HashMap<ItemT, LeafNode<ItemT>> _doc2node = new HashMap<ItemT, LeafNode<ItemT>>();
0070:
0071: /** Maps path's to nodes and nodes to paths. */
0072: private final BidirectionalHashMap<String, InnerNode<?, ItemT>> _path2node = new BidirectionalHashMap<String, InnerNode<?, ItemT>>();
0073:
0074: /** The collection of INavigationListeners listening to this JListNavigator */
0075: private final Vector<INavigationListener<? super ItemT>> navListeners = new Vector<INavigationListener<? super ItemT>>();
0076:
0077: /** The renderer for this JTree. */
0078: private final CustomTreeCellRenderer _renderer;
0079:
0080: private volatile DisplayManager<? super ItemT> _displayManager;
0081: private volatile Icon _rootIcon;
0082:
0083: private java.util.List<GroupNode<ItemT>> _roots = new LinkedList<GroupNode<ItemT>>();
0084:
0085: /** Sets the foreground color of this JTree
0086: * @param c the color to set to
0087: */
0088: public void setForeground(Color c) {
0089: super .setForeground(c);
0090: if (_renderer != null)
0091: _renderer.setTextNonSelectionColor(c);
0092: }
0093:
0094: /** Sets the background color of this tree
0095: * @param c the color for the background
0096: */
0097: public void setBackground(Color c) {
0098: super .setBackground(c);
0099: if (_renderer != null)
0100: _renderer.setBackgroundNonSelectionColor(c);
0101: }
0102:
0103: /** Standard constructor.
0104: * @param projRoot the path identifying the root node for the project
0105: */
0106: public JTreeSortNavigator(String projRoot) {
0107:
0108: super (new DefaultTreeModel(new RootNode<ItemT>(projRoot
0109: .substring(projRoot.lastIndexOf(File.separator) + 1))));
0110:
0111: addTreeSelectionListener(this );
0112: addTreeExpansionListener(this );
0113:
0114: _model = (DefaultTreeModel) getModel();
0115: _renderer = new CustomTreeCellRenderer();
0116: _renderer.setOpaque(false);
0117: setCellRenderer(_renderer);
0118: // getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION);
0119: getSelectionModel().setSelectionMode(
0120: TreeSelectionModel.DISCONTIGUOUS_TREE_SELECTION);
0121: setRowHeight(18);
0122: // System.err.println(isEditable());
0123: }
0124:
0125: /** Alternate constructor specifying the display manager that provides icons for the navigator.
0126: * @param projRoot the path identifying the root node for the project
0127: * @param dm the display manager for the navigagtor
0128: */
0129: public JTreeSortNavigator(String projRoot,
0130: DisplayManager<? super ItemT> dm) {
0131: this (projRoot);
0132: _displayManager = dm;
0133: }
0134:
0135: /** Sets the display manager that is used to select icons for the leaves of the tree.
0136: * This does not apply to the inner nodes or the root.
0137: */
0138: public void setDisplayManager(DisplayManager<? super ItemT> manager) {
0139: _displayManager = manager;
0140: }
0141:
0142: /** Sets the icon to be displayed at the root of the tree */
0143: public void setRootIcon(Icon ico) {
0144: _rootIcon = ico;
0145: }
0146:
0147: /** @return an AWT component which interacts with this document navigator */
0148: public Container asContainer() {
0149: return this ;
0150: }
0151:
0152: /** Adds an <code>IDocument</code> to this navigator. Should only executed from event thread.
0153: * @param doc the document to be added into this navigator.
0154: */
0155: public void addDocument(ItemT doc) {
0156: assert (EventQueue.isDispatchThread() || Utilities.TEST_MODE);
0157: addDocument(doc, "");
0158: }
0159:
0160: /** Adds an <code>INavigatorItem</code> into this navigator in the position specified by path.
0161: * The actual behavior of the navigator and the position associated with a path are left up
0162: * to the implementing class. Only runs in event-handling thread.
0163: * @param doc the document to be added into this navigator.
0164: * @param path in navigator to parent directory for doc
0165: * @throws IllegalArgumentException if this navigator does not contain <code>relativeto</code> as tested by the
0166: * <code>contains</code> method.
0167: */
0168: public void addDocument(ItemT doc, String path) {
0169: assert (EventQueue.isDispatchThread() || Utilities.TEST_MODE);
0170: synchronized (_model) { // lock for mutation
0171:
0172: /* Identify root matching doc if any */
0173: GroupNode<ItemT> root = null;
0174:
0175: for (GroupNode<ItemT> r : _roots) {
0176: if (r.getFilter().accept(doc)) {
0177: root = r;
0178: break;
0179: }
0180: }
0181:
0182: if (root == null)
0183: return;
0184:
0185: /* Embed path in matching root, creating folder nodes if necessary */
0186: StringTokenizer tok = new StringTokenizer(path,
0187: File.separator);
0188: //ArrayList<String> elements = new ArrayList<String>();
0189: final StringBuilder pathSoFarBuf = new StringBuilder();
0190: InnerNode<?, ItemT> lastNode = root;
0191: while (tok.hasMoreTokens()) {
0192: String element = tok.nextToken();
0193: pathSoFarBuf.append(element).append('/');
0194: String pathSoFar = pathSoFarBuf.toString();
0195: InnerNode<?, ItemT> this Node;
0196: //System.out.println("pathsofar = " + pathSoFar);
0197: // if the node is not in the hashmap yet
0198: if (!_path2node.containsKey(pathSoFar)) {
0199: // make a new node
0200:
0201: /* this inserts a folder node */
0202: this Node = new FileNode<ItemT>(new File(pathSoFar));
0203: insertFolderSortedInto(this Node, lastNode);
0204: this .expandPath(new TreePath(lastNode.getPath()));
0205: // associate the path so far with that node
0206: _path2node.put(pathSoFar, this Node);
0207: } else {
0208: // System.out.println("path2node contains pathSoFar");
0209: this Node = _path2node.getValue(pathSoFar);
0210: }
0211:
0212: lastNode = this Node;
0213:
0214: //elements.add(element);
0215: }
0216:
0217: /* lastNode is the node of the folder to add into */
0218:
0219: LeafNode<ItemT> child = new LeafNode<ItemT>(doc);
0220: _doc2node.put(doc, child);
0221: insertNodeSortedInto(child, lastNode);
0222: // _hasNonProjFilesOpen = (lastNode == root);
0223: // _model.insertNodeInto(child, lastNode, lastNode.getChildCount());
0224: this .expandPath(new TreePath(lastNode.getPath()));
0225: }
0226: }
0227:
0228: private void addTopLevelGroupToRoot(InnerNode<?, ItemT> parent) {
0229: assert (EventQueue.isDispatchThread() || Utilities.TEST_MODE);
0230: synchronized (_model) { // lock for mutation
0231: int indexInRoots = _roots.indexOf(parent);
0232: int num = _model.getChildCount(_model.getRoot());
0233: int i;
0234: for (i = 0; i < num; i++) {
0235: TreeNode n = (TreeNode) _model.getChild(_model
0236: .getRoot(), i);
0237: if (_roots.indexOf(n) > indexInRoots)
0238: break;
0239: }
0240: _model.insertNodeInto(parent, (MutableTreeNode) _model
0241: .getRoot(), i);
0242: }
0243: }
0244:
0245: /** Inserts the child node (INavigatorItem) into the sorted position as a parent node's child. Only
0246: * executes in the event thread. Assumes that _model lock is already held.
0247: * @param child the node to add
0248: * @param parent the node to add under
0249: */
0250: private void insertNodeSortedInto(LeafNode<ItemT> child,
0251: InnerNode<?, ItemT> parent) {
0252: int numChildren = parent.getChildCount();
0253: String newName = child.toString();
0254: String oldName = parent.getUserObject().toString();
0255: DefaultMutableTreeNode parentsKid;
0256:
0257: /** Make sure that if the parent is a top level group, it is added to the tree model group. */
0258: if (((DefaultMutableTreeNode) _model.getRoot())
0259: .getIndex(parent) == -1
0260: && _roots.contains(parent)) {
0261: addTopLevelGroupToRoot(parent);
0262: }
0263: int i;
0264: for (i = 0; i < numChildren; i++) {
0265: parentsKid = ((DefaultMutableTreeNode) parent.getChildAt(i));
0266: if (parentsKid instanceof InnerNode) {
0267: // do nothing, it's a folder
0268: } else if (parentsKid instanceof LeafNode) {
0269: oldName = ((LeafNode<?>) parentsKid).getData()
0270: .getName();
0271: if ((newName.toUpperCase().compareTo(
0272: oldName.toUpperCase()) < 0))
0273: break;
0274: } else
0275: throw new IllegalStateException(
0276: "found a node in navigator that is not an InnerNode or LeafNode");
0277: }
0278: _model.insertNodeInto(child, parent, i);
0279: }
0280:
0281: /** Inserts a folder (String) into sorted position under the parent. Only executes in event thread. Assumes that
0282: * _model lock is already held
0283: * @param child the folder to add
0284: * @param parent the folder to add under
0285: */
0286: private void insertFolderSortedInto(InnerNode<?, ItemT> child,
0287: InnerNode<?, ItemT> parent) {
0288: int numChildren = parent.getChildCount();
0289: String newName = child.toString();
0290: String oldName = parent.getUserObject().toString();
0291: DefaultMutableTreeNode parentsKid;
0292:
0293: if (((DefaultMutableTreeNode) _model.getRoot())
0294: .getIndex(parent) == -1
0295: && _roots.contains(parent)) {
0296: addTopLevelGroupToRoot(parent);
0297: }
0298:
0299: int countFolders = 0;
0300: int i;
0301: for (i = 0; i < numChildren; i++) {
0302: parentsKid = ((DefaultMutableTreeNode) parent.getChildAt(i));
0303: if (parentsKid instanceof InnerNode) {
0304: countFolders++;
0305: oldName = parentsKid.toString();
0306: if ((newName.toUpperCase().compareTo(
0307: oldName.toUpperCase()) < 0))
0308: break;
0309: } else if (parentsKid instanceof LeafNode)
0310: break;
0311: // we're out of folders, and starting into the files, so just break out.
0312: else
0313: throw new IllegalStateException(
0314: "found a node in navigator that is not an InnerNode or LeafNode");
0315: }
0316: _model.insertNodeInto(child, parent, i);
0317: }
0318:
0319: /** Removes a given <code>INavigatorItem<code> from this navigator. Removes all <code>INavigatorItem</code>s
0320: * from this navigator that are "equal" (using <code>.equals(...)</code>) to the passed argument. Any of
0321: * the removed documents may be returned by this method. If the NavigatorItem is found in the navigator, null
0322: * is returned. Only executes from event thread.
0323: * @param doc the docment to be removed
0324: * @return doc a document removed from this navigator as a result of invoking this method.
0325: * @throws IllegalArgumentException if this navigator contains no document equal to doc
0326: */
0327: public ItemT removeDocument(ItemT doc) {
0328: assert (EventQueue.isDispatchThread() || Utilities.TEST_MODE);
0329: synchronized (_model) { // lock for mutation
0330: LeafNode<ItemT> toRemove = getNodeForDoc(doc);
0331: if (toRemove == null)
0332: return null;
0333: return removeNode(getNodeForDoc(doc));
0334: }
0335: }
0336:
0337: /** Assumes lock on _model is already held or that it is being run in the event thread. */
0338: private LeafNode<ItemT> getNodeForDoc(ItemT doc) {
0339: // synchronized(_model) {
0340: return _doc2node.get(doc);
0341: // }
0342: }
0343:
0344: /** Only takes in nodes that have an INavigatorItem as their object; assumes _model lock is already held.
0345: * Only executes in event thread. */
0346: private ItemT removeNode(LeafNode<ItemT> node) {
0347: DefaultMutableTreeNode parent = (DefaultMutableTreeNode) node
0348: .getParent();
0349: _model.removeNodeFromParent(node);
0350: _doc2node.remove(node.getData());
0351: cleanFolderNode(parent);
0352: return node.getData();
0353: }
0354:
0355: /** If the given node is an InnerNode with no children, it removes it from the tree. If the given node is a leaf or
0356: * the root, it does nothing to it. Assumes that _model lock is already held. Only executes in the event thread.
0357: */
0358: private void cleanFolderNode(DefaultMutableTreeNode node) {
0359: if (node instanceof InnerNode && node.getChildCount() == 0) {
0360: DefaultMutableTreeNode parent = (DefaultMutableTreeNode) node
0361: .getParent();
0362: _model.removeNodeFromParent(node);
0363: @SuppressWarnings("unchecked")
0364: InnerNode<?, ItemT> typedNode = (InnerNode<?, ItemT>) node;
0365: _path2node.removeKey(typedNode);
0366: cleanFolderNode(parent);
0367: }
0368: }
0369:
0370: /** Resets a given <code>INavigatorItem<code> in the tree. Updates the placement of the item and its display
0371: * to reflect any changes made in the model. Only executes in the event thread.
0372: * Note: full synchronization commented out because this operation is only performed in the event thread. The
0373: * synchronized sections must be atomic but the rest of the code can run concurrently with read operations in
0374: * other threads.
0375: * @param doc the document to be refreshed
0376: * @param path the path to the parent folder for this document
0377: * @throws IllegalArgumentException if this navigator contains no document equal to doc.
0378: */
0379: public void refreshDocument(ItemT doc, String path) {
0380: assert (EventQueue.isDispatchThread() || Utilities.TEST_MODE);
0381: // synchronized(_model) {
0382: LeafNode<ItemT> node = _doc2node.get(doc);
0383: InnerNode<?, ?> oldParent;
0384: if (node == null) { // document has not yet been entered in tree
0385: addDocument(doc, path);
0386: return;
0387: }
0388:
0389: InnerNode<?, ?> p = (InnerNode<?, ?>) node.getParent();
0390: oldParent = p;
0391:
0392: // Check to see if the new parent (could be same) exists already
0393: String newPath = path;
0394:
0395: if (newPath.length() > 0) {
0396: if (newPath.substring(0, 1).equals("/"))
0397: newPath = newPath.substring(1);
0398: if (!newPath.substring(newPath.length() - 1).equals("/"))
0399: newPath = newPath + "/";
0400: }
0401:
0402: InnerNode<?, ItemT> newParent = _path2node.getValue(newPath); // node that should be parent
0403:
0404: if (newParent == oldParent) { // no mutation has occurred before this point because oldParent != null
0405: if (!node.toString().equals(doc.getName())) { // document has changed name?
0406: synchronized (_model) {
0407: LeafNode<ItemT> newLeaf = new LeafNode<ItemT>(doc);
0408: _doc2node.put(doc, newLeaf);
0409: insertNodeSortedInto(newLeaf, newParent);
0410: _model.removeNodeFromParent(node);
0411: }
0412: }
0413: // don't do anything if its name or parents haven't changed
0414: } else { // document has moved within tree
0415: synchronized (_model) {
0416: removeNode(node);
0417: addDocument(doc, path);
0418: }
0419: }
0420: // }
0421: }
0422:
0423: /** Sets the specified document to be active (current). Only executes in the event thread. */
0424: public void setActiveDoc(ItemT doc) {
0425: assert (EventQueue.isDispatchThread() || Utilities.TEST_MODE);
0426: // synchronized (_model) { // lock out mutation
0427: DefaultMutableTreeNode node = _doc2node.get(doc);
0428: if (node == null)
0429: return; // doc is not in the navigator
0430: if (node == _current)
0431: return; // current doc is the active doc
0432: // if (_doc2node.containsKey(doc);) { // this test is obviously true since node == _doc2node.get(doc)
0433: TreeNode[] nodes = node.getPath();
0434: TreePath path = new TreePath(nodes);
0435: expandPath(path);
0436: setSelectionPath(path); // fires _gainVisitor in AbstractGlobalModel
0437: scrollPathToVisible(path);
0438: // }
0439: // }
0440: }
0441:
0442: /** Returns a typed equivalent to {@code next.getUserObject()}. Assumes the DefaultMutableTreeNode
0443: * is a leaf node in _model and thus, if parameterized, would have type ItemT. This is a workaround for
0444: * the lack of a generic implementation of TreeModel and TreeNode. If those classes become generified,
0445: * this code will no longer be necessary.
0446: */
0447: private ItemT getNodeUserObject(DefaultMutableTreeNode n) {
0448: @SuppressWarnings("unchecked")
0449: ItemT result = (ItemT) n.getUserObject();
0450: return result;
0451: }
0452:
0453: /** Returns the next document in the collection (using enumeration order). Executes in any thread.
0454: * @param doc the INavigatorItem of interest
0455: * @return the INavigatorItem which comes after doc
0456: */
0457: public ItemT getNext(ItemT doc) {
0458: synchronized (_model) { // locks out mutation
0459: DefaultMutableTreeNode node = _doc2node.get(doc);
0460: if (node == null)
0461: return doc; // doc may not be contained in navigator
0462: // TODO: check for "package" case
0463: DefaultMutableTreeNode next = node.getNextLeaf();
0464: if (next == null || next == _model.getRoot()) {
0465: return doc;
0466: } else {
0467: return getNodeUserObject(next);
0468: }
0469: }
0470: }
0471:
0472: /** Returns the previous document in the collection (using enumeration order). Executes in any thread.
0473: * @param doc the INavigatorItem of interest
0474: * @return the INavigatorItem which comes before doc
0475: */
0476: public ItemT getPrevious(ItemT doc) {
0477: synchronized (_model) { // locks out mutation
0478: DefaultMutableTreeNode node = _doc2node.get(doc);
0479: if (node == null)
0480: return doc; // doc may not be contained in navigator
0481: // TODO: check for "package" case
0482: DefaultMutableTreeNode prev = node.getPreviousLeaf();
0483: if (prev == null || prev == _model.getRoot()) {
0484: return doc;
0485: } else {
0486: return getNodeUserObject(prev);
0487: }
0488: }
0489: }
0490:
0491: /** Returns the first document in the collection (using enumeration order). Executes in any thread.
0492: * @return the INavigatorItem which comes before doc
0493: */
0494: public ItemT getFirst() {
0495: synchronized (_model) { // locks out mutation
0496: DefaultMutableTreeNode root = (DefaultMutableTreeNode) _model
0497: .getRoot();
0498: return getNodeUserObject(root.getFirstLeaf());
0499: }
0500: }
0501:
0502: /** Returns the last document in the collection (using enumeration order). Executes in any thread.
0503: * @return the INavigatorItem which comes before doc
0504: */
0505: public ItemT getLast() {
0506: synchronized (_model) { // locks out mutation
0507: DefaultMutableTreeNode root = (DefaultMutableTreeNode) _model
0508: .getRoot();
0509: return getNodeUserObject(root.getLastLeaf());
0510: }
0511: }
0512:
0513: /** Tests to see if a given document is contained in this navigator. Executes in any thread.
0514: * @param doc the document to test for containment.
0515: * @return <code>true</code> if this navigator contains a document that is "equal" (as tested by the
0516: * <code>equals</code< method) to the passed document, else <code>false</code>.
0517: */
0518: public boolean contains(ItemT doc) {
0519: synchronized (_model) {
0520: return _doc2node.containsKey(doc);
0521: } // locks out mutation
0522: }
0523:
0524: /** Tests to see if a given document is contained in this navigator. Only executes in event thread.*/
0525: public boolean _contains(ItemT doc) {
0526: return _doc2node.containsKey(doc);
0527: }
0528:
0529: /** Returns all the <code>IDocuments</code> contained in this navigator. Does not assert any type of ordering on
0530: * the returned structure. Executes in any thread.
0531: * @return an <code>INavigatorItem<code> enumeration of this navigator's contents.
0532: */
0533: public Enumeration<ItemT> getDocuments() {
0534:
0535: final Vector<ItemT> list = new Vector<ItemT>(getDocumentCount()); // Use Vector because it implements an Enumeration
0536:
0537: synchronized (_model) { // locks out mutation
0538: // e has a raw type because depthFirstEnumeration() has a raw type signature
0539: Enumeration e = ((DefaultMutableTreeNode) _model.getRoot())
0540: .depthFirstEnumeration();
0541:
0542: while (e.hasMoreElements()) {
0543: DefaultMutableTreeNode node = (DefaultMutableTreeNode) e
0544: .nextElement();
0545: if (node.isLeaf() && node != _model.getRoot()) {
0546: list.add(getNodeUserObject(node));
0547: }
0548: }
0549: }
0550: return list.elements();
0551: }
0552:
0553: /** Returns all the <code>IDocuments</code> contained in the specified bin.
0554: * @param binName name of bin
0555: * @return an <code>INavigatorItem<code> enumeration of this navigator's contents.
0556: */
0557: public Enumeration<ItemT> getDocumentsInBin(String binName) {
0558: final Vector<ItemT> list = new Vector<ItemT>();
0559:
0560: synchronized (_model) { // locks out mutation
0561: for (GroupNode<ItemT> gn : _roots) {
0562: if (gn.getData().equals(binName)) {
0563: // found the bin with the right name
0564: // e has a raw type because depthFirstEnumeration() has a raw type signature
0565: Enumeration e = gn.depthFirstEnumeration();
0566:
0567: while (e.hasMoreElements()) {
0568: DefaultMutableTreeNode node = (DefaultMutableTreeNode) e
0569: .nextElement();
0570: if (node.isLeaf() && node != _model.getRoot()) {
0571: list.add(getNodeUserObject(node));
0572: }
0573: }
0574: }
0575: }
0576: }
0577:
0578: return list.elements();
0579: }
0580:
0581: /** Returns the number of <code>IDocuments</code> contained by this <code>IDocumentNavigator</code>
0582: * Not synchronized on the assumption that size field of a HashMap always has a legitimate
0583: * value (either the size of the current state or the size of its state before some concurrent
0584: * operation started. Executes in any thread. Assume size() always returns a valid (perhaps stale) value.
0585: * @return the number of documents within this navigator.
0586: */
0587: public int getDocumentCount() {
0588: return _doc2node.size();
0589: }
0590:
0591: /** Returns whether this <code>IDocumentNavigator</code> contains any <code>IDocuments</code>.
0592: * @return <code>true</code> if this navigator contains one or more documents, else <code>false</code>.
0593: * Executes in any thread. Assume isEmpty() always returns a valid (perhaps stale) value.
0594: */
0595: public boolean isEmpty() {
0596: return _doc2node.isEmpty();
0597: }
0598:
0599: /** Removes all <code>IDocuments</code> from this <code>IDocumentNavigator</code>. Only executes in event thread. */
0600: public void clear() {
0601: assert (EventQueue.isDispatchThread() || Utilities.TEST_MODE);
0602: synchronized (_model) {
0603: _doc2node.clear();
0604: ((DefaultMutableTreeNode) _model.getRoot())
0605: .removeAllChildren();
0606: }
0607: }
0608:
0609: /** Adds an <code>INavigationListener</code> to this navigator. After invoking this method, the passed
0610: * listener will be eligible for observing this navigator. If the provided listener is already observing
0611: * this navigator (as tested by the == operator), no action is taken. Only executes in event thread.
0612: * @param listener the listener to be added to this navigator.
0613: */
0614: public void addNavigationListener(
0615: INavigationListener<? super ItemT> listener) {
0616: assert (EventQueue.isDispatchThread() || Utilities.TEST_MODE);
0617: synchronized (_model) {
0618: navListeners.add(listener);
0619: } // locks out access during mutation
0620: }
0621:
0622: /** Removes the given listener from observing this navigator. After invoking this method, all observers
0623: * watching this navigator "equal" (as tested by the == operator) will no longer receive observable dispatches.
0624: * Only executes in event thread.
0625: * @param listener the listener to be removed from this navigator
0626: */
0627: public void removeNavigationListener(
0628: INavigationListener<? super ItemT> listener) {
0629: assert (EventQueue.isDispatchThread() || Utilities.TEST_MODE);
0630: synchronized (_model) {
0631: navListeners.remove(listener);
0632: }
0633: }
0634:
0635: /** Returns a collection of all navigator listeners. Note: this is a dangerous method since it exposes a shared data
0636: * structure that must be synchronized with _model.
0637: */
0638: public Collection<INavigationListener<? super ItemT>> getNavigatorListeners() {
0639: return navListeners;
0640: }
0641:
0642: /** Standard visitor pattern. Only used within this class.
0643: * @param algo the visitor to run
0644: * @param input the input for the visitor
0645: */
0646: public <InType, ReturnType> ReturnType execute(
0647: IDocumentNavigatorAlgo<ItemT, InType, ReturnType> algo,
0648: InType input) {
0649: return algo.forTree(this , input);
0650: }
0651:
0652: /** Called whenever the value of the selection changes. Only runs in event thread. Runs _gainVisitor in global model
0653: * @param e the event that characterizes the change.
0654: */
0655: public void valueChanged(TreeSelectionEvent e) {
0656: Object treeNode = this .getLastSelectedPathComponent();
0657: if (treeNode == null || !(treeNode instanceof NodeData))
0658: return;
0659: @SuppressWarnings("unchecked")
0660: NodeData<ItemT> newSelection = (NodeData<ItemT>) treeNode;
0661: if (_current != newSelection) {
0662: for (INavigationListener<? super ItemT> listener : navListeners) {
0663: listener.lostSelection(_current,
0664: isNextChangeModelInitiated());
0665: listener.gainedSelection(newSelection,
0666: isNextChangeModelInitiated());
0667: }
0668: _current = newSelection;
0669: }
0670:
0671: setNextChangeModelInitiated(false);
0672: }
0673:
0674: /** Returns a renderer for this object. */
0675: public Component getRenderer() {
0676: return _renderer;
0677: }
0678:
0679: /** The cell renderer for this tree. Only runs in event thread. */
0680: private class CustomTreeCellRenderer extends
0681: DefaultTreeCellRenderer {
0682:
0683: /** Rreturns the component for a cell
0684: * @param tree
0685: */
0686: public Component getTreeCellRendererComponent(JTree tree,
0687: Object value, boolean sel, boolean isExpanded,
0688: boolean leaf, int row, boolean hasFocus) {
0689:
0690: // changing last argument from hasFocus to false appears to fix a focus bug when selecting a new document
0691: super .getTreeCellRendererComponent(tree, value, sel,
0692: isExpanded, leaf, row, false);
0693:
0694: DefaultMutableTreeNode node = (DefaultMutableTreeNode) value;
0695: if (node instanceof RootNode && _rootIcon != null)
0696: setIcon(_rootIcon);
0697:
0698: else if (node instanceof LeafNode) {
0699: ItemT doc = getNodeUserObject(node);
0700: if (leaf && _displayManager != null) {
0701: setIcon(_displayManager.getIcon(doc));
0702: setText(_displayManager.getName(doc));
0703: }
0704: }
0705: return this ;
0706: }
0707: }
0708:
0709: /** Selects the document at the x,y coordinate of the navigator pane and sets it to be the currently active
0710: * document. Only runs in event thread. O
0711: * @param x the x coordinate of the navigator pane
0712: * @param y the y coordinate of the navigator pane
0713: */
0714: public boolean selectDocumentAt(int x, int y) {
0715: // synchronized (_model) {
0716: TreePath path = getPathForLocation(x, y);
0717: if (path == null)
0718: return false;
0719: else {
0720: DefaultMutableTreeNode node = (DefaultMutableTreeNode) path
0721: .getLastPathComponent();
0722: if (node instanceof LeafNode) {
0723: this .expandPath(path);
0724: this .setSelectionPath(path);
0725: this .scrollPathToVisible(path);
0726: return true;
0727: } else if (node instanceof InnerNode) {
0728: this .expandPath(path);
0729: this .setSelectionPath(path);
0730: this .scrollPathToVisible(path);
0731: return true;
0732: } else if (node instanceof RootNode) {
0733: this .expandPath(path);
0734: this .setSelectionPath(path);
0735: this .scrollPathToVisible(path);
0736: return true;
0737: } else
0738: return false;
0739: }
0740: // }
0741: }
0742:
0743: /** Returns true if the item at the x,y coordinate of the navigator pane is currently selected.
0744: * Only runs in event thread. O
0745: * @param x the x coordinate of the navigator pane
0746: * @param y the y coordinate of the navigator pane
0747: * @return true if the item is currently selected
0748: */
0749: public boolean isSelectedAt(int x, int y) {
0750: TreePath path = getPathForLocation(x, y);
0751: if (path == null)
0752: return false;
0753: TreePath[] ps = getSelectionPaths();
0754: if (ps == null) {
0755: return false;
0756: }
0757: for (TreePath p : ps) {
0758: if (path.equals(p)) {
0759: return true;
0760: }
0761: }
0762: return false;
0763: }
0764:
0765: /** @return true if at least one group of INavigatorItems is selected. Only runs in event thread. */
0766: public boolean isGroupSelected() {
0767: return getGroupSelectedCount() != 0;
0768: }
0769:
0770: /** @return the number of groups selected. Only runs in event thread. */
0771: public int getGroupSelectedCount() {
0772: assert (EventQueue.isDispatchThread() || Utilities.TEST_MODE);
0773: int count = 0;
0774: TreePath[] ps = getSelectionPaths();
0775: if (ps == null) {
0776: return 0;
0777: }
0778: for (TreePath p : ps) {
0779: TreeNode n = (TreeNode) p.getLastPathComponent();
0780: if (n instanceof InnerNode) {
0781: ++count;
0782: }
0783: }
0784: return count;
0785: }
0786:
0787: /** @return the folders currently selected. Only runs in event thread. */
0788: public java.util.List<File> getSelectedFolders() {
0789: assert (EventQueue.isDispatchThread() || Utilities.TEST_MODE);
0790: ArrayList<File> l = new ArrayList<File>();
0791: TreePath[] ps = getSelectionPaths();
0792: if (ps == null) {
0793: return l;
0794: }
0795: for (TreePath p : ps) {
0796: TreeNode n = (TreeNode) p.getLastPathComponent();
0797: if (n instanceof FileNode) {
0798: l.add(((FileNode) n).getData());
0799: }
0800: }
0801: return l;
0802: }
0803:
0804: /** @return true if at least one document is selected. Only runs in event thread. */
0805: public boolean isDocumentSelected() {
0806: return getDocumentSelectedCount() != 0;
0807: }
0808:
0809: /** @return the number of documents selected. Only runs in event thread. */
0810: public int getDocumentSelectedCount() {
0811: assert (EventQueue.isDispatchThread() || Utilities.TEST_MODE);
0812: int count = 0;
0813: TreePath[] ps = getSelectionPaths();
0814: if (ps == null) {
0815: return 0;
0816: }
0817: for (TreePath p : ps) {
0818: TreeNode n = (TreeNode) p.getLastPathComponent();
0819: if (n instanceof LeafNode) {
0820: ++count;
0821: }
0822: }
0823: return count;
0824: }
0825:
0826: /** @return the documents currently selected. Only runs in event thread. */
0827: @SuppressWarnings("unchecked")
0828: public java.util.List<ItemT> getSelectedDocuments() {
0829: assert (EventQueue.isDispatchThread() || Utilities.TEST_MODE);
0830: ArrayList<ItemT> l = new ArrayList<ItemT>();
0831: TreePath[] ps = getSelectionPaths();
0832: if (ps == null) {
0833: return l;
0834: }
0835: for (TreePath p : ps) {
0836: TreeNode n = (TreeNode) p.getLastPathComponent();
0837: if (n instanceof LeafNode) {
0838: l.add((ItemT) ((LeafNode) n).getData());
0839: }
0840: }
0841: return l;
0842: }
0843:
0844: /** Returns true if at least one top level group is selected. Only runs in event thread. */
0845: public boolean isTopLevelGroupSelected() {
0846: assert (EventQueue.isDispatchThread() || Utilities.TEST_MODE);
0847: TreePath[] ps = getSelectionPaths();
0848: if (ps == null) {
0849: return false;
0850: }
0851: for (TreePath p : ps) {
0852: TreeNode n = (TreeNode) p.getLastPathComponent();
0853: if (n instanceof GroupNode) {
0854: return true;
0855: }
0856: }
0857: return false;
0858: }
0859:
0860: /** Returns true if the root is selected. Only runs in event thread. */
0861: public boolean isRootSelected() {
0862: assert (EventQueue.isDispatchThread() || Utilities.TEST_MODE);
0863: TreePath[] ps = getSelectionPaths();
0864: if (ps == null) {
0865: return false;
0866: }
0867: for (TreePath p : ps) {
0868: TreeNode n = (TreeNode) p.getLastPathComponent();
0869: if (n == _model.getRoot()) {
0870: return true;
0871: }
0872: }
0873: return false;
0874: }
0875:
0876: /** Returns the names of the top level groups that the selected items descend from. Only runs in event thread. */
0877: public java.util.Set<String> getNamesOfSelectedTopLevelGroup()
0878: throws GroupNotSelectedException {
0879: assert (EventQueue.isDispatchThread() || Utilities.TEST_MODE);
0880:
0881: HashSet<String> names = new HashSet<String>();
0882: LinkedList<GroupNode<ItemT>> roots = new LinkedList<GroupNode<ItemT>>(
0883: _roots);
0884:
0885: TreePath[] ps = getSelectionPaths();
0886: if (ps != null) {
0887: for (TreePath p : ps) {
0888: if (p.getLastPathComponent() instanceof DefaultMutableTreeNode) {
0889: DefaultMutableTreeNode n = (DefaultMutableTreeNode) p
0890: .getLastPathComponent();
0891:
0892: for (GroupNode<ItemT> gn : roots) {
0893: if (gn.isNodeDescendant(n)) {
0894: // n is a descendent of gn; add the name of the group node
0895: names.add(gn.getData());
0896: // this group node definitely contains selected items, no need to check it again;
0897: // remove it from the list of roots to consider
0898: roots.remove(gn);
0899: break;
0900: }
0901: }
0902: }
0903: }
0904: }
0905:
0906: if (names.isEmpty()) {
0907: throw new GroupNotSelectedException(
0908: "there is no top level group for the root of the tree");
0909: }
0910:
0911: return names;
0912: }
0913:
0914: /** Returns the currently selected leaf node, or null if the selected node is not a leaf. Only reads a single
0915: * volatile field that always has a valid value. Thread safe.
0916: */
0917: public ItemT getCurrent() {
0918: NodeData<ItemT> current = _current;
0919: if (current == null)
0920: return null;
0921: return current.execute(_leafVisitor);
0922: }
0923:
0924: /** Returns the model lock. */
0925: public Object getModelLock() {
0926: return _model;
0927: }
0928:
0929: private final NodeDataVisitor<ItemT, ItemT> _leafVisitor = new NodeDataVisitor<ItemT, ItemT>() {
0930: public ItemT fileCase(File f, Object... p) {
0931: return null;
0932: }
0933:
0934: public ItemT stringCase(String s, Object... p) {
0935: return null;
0936: }
0937:
0938: public ItemT itemCase(ItemT ini, Object... p) {
0939: return ini;
0940: }
0941: };
0942:
0943: /** @return true if the INavigatorItem is in a selected group, if at least
0944: * one group is selected. Only runs in event thread. */
0945: public boolean isSelectedInGroup(ItemT i) {
0946: assert (EventQueue.isDispatchThread() || Utilities.TEST_MODE);
0947:
0948: TreePath[] ps = getSelectionPaths();
0949: if (ps == null) {
0950: return false;
0951: }
0952: for (TreePath p : ps) {
0953: TreeNode n = (TreeNode) p.getLastPathComponent();
0954: TreeNode l = _doc2node.get(i);
0955:
0956: if (n == _model.getRoot())
0957: return true;
0958:
0959: while (l.getParent() != _model.getRoot()) {
0960: if (l.getParent() == n)
0961: return true;
0962: l = l.getParent();
0963: }
0964: }
0965:
0966: return false;
0967: }
0968:
0969: /** Adds a top level group to the navigator. Only runs in event thread. */
0970: public void addTopLevelGroup(String name,
0971: INavigatorItemFilter<? super ItemT> f) {
0972: assert (EventQueue.isDispatchThread() || Utilities.TEST_MODE);
0973:
0974: if (f == null)
0975: throw new IllegalArgumentException(
0976: "parameter 'f' is not allowed to be null");
0977: GroupNode<ItemT> n = new GroupNode<ItemT>(name, f);
0978: _roots.add(n);
0979: }
0980:
0981: /******* Methods that handle expansion/collapsing of folders in tree **********/
0982:
0983: /** Called whenever an item in the tree has been collapsed. Only runs in event thread (except when testing). */
0984: public void treeCollapsed(TreeExpansionEvent event) {
0985: assert (EventQueue.isDispatchThread() || Utilities.TEST_MODE);
0986:
0987: Object o = event.getPath().getLastPathComponent();
0988: if (o instanceof InnerNode)
0989: ((InnerNode<?, ?>) o).setCollapsed(true);
0990: }
0991:
0992: /** Called whenever an item in the tree has been expanded. Only runs in event thread. */
0993: public void treeExpanded(TreeExpansionEvent event) {
0994: assert (EventQueue.isDispatchThread() || Utilities.TEST_MODE);
0995:
0996: Object o = event.getPath().getLastPathComponent();
0997: if (o instanceof InnerNode)
0998: ((InnerNode<?, ?>) o).setCollapsed(false);
0999: }
1000:
1001: /** Collapses all the paths in the tree that match one of the path strings included in the given hash set. Path
1002: * strings must follow a specific format in order for them to work. See the documentation of
1003: * <code>generatePathString</code> for information on the format of the path strings. Only executes in event thread.
1004: * @param paths A hash set of path strings.
1005: */
1006: public void collapsePaths(String[] paths) {
1007: assert (EventQueue.isDispatchThread() || Utilities.TEST_MODE);
1008:
1009: HashSet<String> set = new HashSet<String>();
1010: for (String s : paths) {
1011: set.add(s);
1012: }
1013: collapsePaths(set);
1014: }
1015:
1016: /** Set variation of collapsePaths(String ...). Private except for testing code. Only runs in event thread except
1017: * for testing code.
1018: */
1019: void collapsePaths(HashSet<String> paths) {
1020:
1021: DefaultMutableTreeNode rootNode = (DefaultMutableTreeNode) _model
1022: .getRoot();
1023: // We use a raw type here because depthFirstEnumeration() has a raw type signature
1024: Enumeration nodes = rootNode.depthFirstEnumeration();
1025: ArrayList<String> list = new ArrayList<String>();
1026: while (nodes.hasMoreElements()) {
1027: DefaultMutableTreeNode tn = (DefaultMutableTreeNode) nodes
1028: .nextElement();
1029: if (tn instanceof InnerNode) {
1030: TreePath tp = new TreePath(tn.getPath());
1031: String s = generatePathString(tp);
1032: boolean shouldCollapse = paths.contains(s);
1033: if (shouldCollapse) {
1034: collapsePath(tp);
1035: }
1036: }
1037: }
1038: }
1039:
1040: /** @return an array of path strings corresponding to the paths of the tree nodes that
1041: * are currently collapsed. See the documentation of <code>generatePathString</code>
1042: * for information on the format of the path strings. Only runs in event thread (except when testing).
1043: */
1044: public String[] getCollapsedPaths() {
1045: assert (EventQueue.isDispatchThread() || Utilities.TEST_MODE);
1046:
1047: ArrayList<String> list = new ArrayList<String>();
1048: // synchronized (_model) {
1049: DefaultMutableTreeNode rootNode = (DefaultMutableTreeNode) _model
1050: .getRoot();
1051: // We use a raw type here because depthFirstEnumeration() has a raw type signature
1052: Enumeration nodes = rootNode.depthFirstEnumeration();
1053: /** This warning is expected **/
1054: while (nodes.hasMoreElements()) {
1055: DefaultMutableTreeNode tn = (DefaultMutableTreeNode) nodes
1056: .nextElement();
1057: if (tn instanceof InnerNode
1058: && ((InnerNode<?, ?>) tn).isCollapsed()) {
1059: TreePath tp = new TreePath(tn.getPath());
1060: list.add(generatePathString(tp));
1061: }
1062: }
1063: // }
1064: return list.toArray(new String[list.size()]);
1065: }
1066:
1067: /** Generates a path string for the given tree node. <p>The path string does not include the project
1068: * root node, but rather a period in its place. Following the "./" is one of the 3 main groups,
1069: * "[ Source Files ]", "[ Auxiliary ]", "[ External ]". The nodes in the path are represented by their
1070: * names delimited by the forward slash ("/"). The path ends with a final delimeter.
1071: * (e.g. "./[ Source Files ]/util/docnavigation/") Only runs in event thread.
1072: * @return the path string for the given node in the JTree
1073: */
1074: public String generatePathString(TreePath tp) {
1075: String path = "";
1076: // synchronized (_model) {
1077: TreeNode root = (TreeNode) _model.getRoot();
1078:
1079: while (tp != null) {
1080: TreeNode curr = (TreeNode) tp.getLastPathComponent();
1081: if (curr == root)
1082: path = "./" + path;
1083: else
1084: path = curr + "/" + path;
1085: tp = tp.getParentPath();
1086: // }
1087: }
1088:
1089: return path;
1090: }
1091:
1092: /** If the currently selected item is not an INavigatorItem, select the one given. Only runs in event thread. */
1093: public void requestSelectionUpdate(ItemT ini) {
1094: // synchronized (_model) {
1095: if (getCurrent() == null) { // the currently selected node is not a leaf
1096: setActiveDoc(ini);
1097: }
1098: // }
1099: }
1100:
1101: /** Marks the next selection change as model-initiated (true) or user-initiated (false; default). */
1102: public void setNextChangeModelInitiated(boolean b) {
1103: putClientProperty(MODEL_INITIATED_PROPERTY_NAME,
1104: b ? Boolean.TRUE : null);
1105: }
1106:
1107: /** @return whether the next selection change is model-initiated (true) or user-initiated (false). */
1108: public boolean isNextChangeModelInitiated() {
1109: return getClientProperty(MODEL_INITIATED_PROPERTY_NAME) != null;
1110: }
1111:
1112: // /** Unnecessary since "modified" mark is added by the cell renderer */
1113: // public void activeDocumentModified() { }
1114:
1115: /** Drag and drop target. */
1116: DropTarget dropTarget = new DropTarget(this , this );
1117:
1118: /** User dragged something into the component. */
1119: public void dragEnter(DropTargetDragEvent dropTargetDragEvent) {
1120: DrJavaRoot.dragEnter(dropTargetDragEvent);
1121: }
1122:
1123: public void dragExit(DropTargetEvent dropTargetEvent) {
1124: }
1125:
1126: public void dragOver(DropTargetDragEvent dropTargetDragEvent) {
1127: }
1128:
1129: public void dropActionChanged(
1130: DropTargetDragEvent dropTargetDragEvent) {
1131: }
1132:
1133: /** User dropped something on the component. */
1134: public synchronized void drop(
1135: DropTargetDropEvent dropTargetDropEvent) {
1136: DrJavaRoot.drop(dropTargetDropEvent);
1137: }
1138: }
|