/*
 * :tabSize=4:indentSize=4:noTabs=false:
 * :folding=explicit:collapseFolds=1:
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 */
package projectviewer;

//{{{ Imports
import java.io.File;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;

import java.awt.BorderLayout;
import java.awt.Component;

import java.awt.event.KeyEvent;
import java.awt.event.HierarchyEvent;
import java.awt.event.HierarchyListener;

import java.awt.dnd.DragSource;
import java.awt.dnd.DnDConstants;
import java.awt.dnd.DragGestureEvent;
import java.awt.dnd.DragGestureListener;

import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.Transferable;

import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;

import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.JLabel;
import javax.swing.JTree;
import javax.swing.JPanel;
import javax.swing.JToolBar;
import javax.swing.JCheckBox;
import javax.swing.JOptionPane;
import javax.swing.JScrollPane;
import javax.swing.JTabbedPane;
import javax.swing.SwingUtilities;
import javax.swing.ToolTipManager;

import javax.swing.tree.TreeModel;
import javax.swing.tree.TreePath;
import javax.swing.tree.DefaultTreeModel;
import javax.swing.tree.DefaultMutableTreeNode;

import org.gjt.sp.jedit.View;
import org.gjt.sp.jedit.jEdit;
import org.gjt.sp.jedit.Buffer;
import org.gjt.sp.jedit.EditBus;
import org.gjt.sp.jedit.EBMessage;
import org.gjt.sp.jedit.PluginJAR;
import org.gjt.sp.jedit.EditPlugin;
import org.gjt.sp.jedit.EBComponent;

import org.gjt.sp.jedit.msg.BufferUpdate;
import org.gjt.sp.jedit.msg.DynamicMenuChanged;
import org.gjt.sp.jedit.msg.EditPaneUpdate;
import org.gjt.sp.jedit.msg.ViewUpdate;

import common.threads.WorkerThreadPool;

import errorlist.ErrorSource;
import errorlist.ErrorSourceUpdate;

import projectviewer.gui.ProjectComboBox;

import projectviewer.vpt.VPTFile;
import projectviewer.vpt.VPTGroup;
import projectviewer.vpt.VPTNode;
import projectviewer.vpt.VPTRoot;
import projectviewer.vpt.VPTProject;
import projectviewer.vpt.VPTContextMenu;
import projectviewer.vpt.VPTCellRenderer;
import projectviewer.vpt.VPTFileListModel;
import projectviewer.vpt.VPTFilteredModel;
import projectviewer.vpt.VPTSelectionListener;
import projectviewer.vpt.VPTWorkingFileListModel;
import projectviewer.vpt.VPTCompactModel;

import projectviewer.event.ProjectViewerEvent;
import projectviewer.event.ProjectViewerListener;

import projectviewer.action.Action;
import projectviewer.action.ExpandAllAction;
import projectviewer.action.CollapseAllAction;
import projectviewer.action.EditProjectAction;
import projectviewer.action.OldStyleAddFileAction;
import projectviewer.config.ProjectViewerConfig;
import projectviewer.importer.NewFileImporter;
//}}}

/**
 *  Main GUI for the project viewer plugin.
 *
 *	@author		Marcelo Vanzin (with much code from original version)
 *	@version    $Id: ProjectViewer.java 6386 2006-02-16 05:18:36Z vanza $
 */
public final class ProjectViewer extends JPanel
									implements HierarchyListener,
												EBComponent {

	//{{{ Static members

	private static final ProjectViewerConfig config = ProjectViewerConfig.getInstance();
	private static final HashMap viewers		= new HashMap();
	private static final HashMap listeners		= new HashMap();
	private static final ArrayList actions		= new ArrayList();

	//{{{ Static Initialization
	/**
	 *	Initializes the default actions, and gets the PV plugins from the list
	 *	of active jEdit plugins.
	 */
	static {
		// Default toolbar actions
		actions.add(new EditProjectAction());
		actions.add(new ExpandAllAction());
		actions.add(new CollapseAllAction());
		actions.add(new OldStyleAddFileAction());
	} //}}}

	//{{{ Action Handling

	//{{{ +_registerAction(Action)_ : void
	/** Adds an action to be shown on the toolbar. */
	public static void registerAction(Action action) {
		actions.add(action);
		actionsChanged();
	} //}}}

	//{{{ +_unregisterAction(Action)_ : void
	/** Removes an action from the toolbar. */
	public static void unregisterAction(Action action) {
		actions.remove(action);
		actionsChanged();
	} //}}}

	//{{{ +_removeToolbarActions(PluginJAR)_ : void
	/**
	 *	Removes the project listeners of the given plugin from the list, and
	 *	from any active project in ProjectViewer.
	 */
	public static void removeToolbarActions(PluginJAR jar) {
		Collection removed = PVActions.prune(actions, jar);
		if (removed != null) {
			actionsChanged();
		}
	} //}}}

	//{{{ +_addToolbarActions(PluginJAR)_ : void
	/**
	 *	Adds to the list of listeners for the given view the listeners that
	 *	have been declared by the given plugin using properties. For global
	 *	listeners, "view" should be null.
	 */
	public static void addToolbarActions(PluginJAR jar) {
		if (jar.getPlugin() == null) return;
		String list = jEdit.getProperty("plugin.projectviewer." +
							jar.getPlugin().getClassName() + ".toolbar-actions");
		Collection aList = PVActions.listToObjectCollection(list, jar, Action.class);
		if (aList != null && aList.size() > 0) {
			actions.addAll(aList);
			actionsChanged();
		}
	} //}}}

	//{{{ -_actionsChanged()_ : void
	/** Reloads the action list for the toolbar. */
	private static void actionsChanged() {
		for (Iterator it = viewers.values().iterator(); it.hasNext(); ) {
			ViewerEntry ve = (ViewerEntry) it.next();
			ProjectViewer v = ve.dockable;
			if (v != null && v.toolBar != null)
				v.populateToolBar();
		}
	} //}}}

	//}}}

	//{{{ +_getViewer(View)_ : ProjectViewer
	/**
	 *	Returns the viewer associated with the given view, or null if none
	 *	exists.
	 */
	public static ProjectViewer getViewer(View view) {
		ViewerEntry ve = (ViewerEntry) viewers.get(view);
		if (ve != null)
			return ve.dockable;
		return null;
	} //}}}

	//{{{ Event Handling

	//{{{ +_addProjectViewerListener(ProjectViewerListener, View)_ : void
	/**
	 *	Add a listener for the instance of project viewer of the given
	 *	view. If the given view is null, the listener will be called from
	 *	all instances.
	 *
	 *	<p>Additionally, for listeners that are registered for all views, a
	 *	ProjectViewerEvent is fired when a different view is selected.</p>
	 *
	 *	@param	lstnr	The listener to add.
	 *	@param	view	The view that the lstnr is attached to, or <code>null</code>
	 *					if the listener wants to be called from all views.
	 */
	public static void addProjectViewerListener(ProjectViewerListener lstnr, View view) {
		ArrayList lst = (ArrayList) listeners.get(view);
		if (lst == null) {
			lst = new ArrayList();
			listeners.put(view, lst);
		}
		lst.add(lstnr);
	} //}}}

	//{{{ +_removeProjectViewerListener(ProjectViewerListener, View)_ : void
	/**
	 *	Remove the listener from the list of listeners for the given view. As
	 *	with the {@link #addProjectViewerListener(ProjectViewerListener, View) add}
	 *	method, <code>view</code> can be <code>null</code>.
	 */
	public static void removeProjectViewerListener(ProjectViewerListener lstnr, View view) {
		ArrayList lst = (ArrayList) listeners.get(view);
		if (lst != null) {
			lst.remove(lstnr);
		}
	} //}}}

	//{{{ +_fireProjectLoaded(Object, VPTProject, View)_ : void
	/**
	 *	Fires an event for the loading of a project. Notify all the listeners
	 *	registered for the given view and listeners registered for all
	 *	views.
	 *
	 *	<p>If the view provided is null, only the listeners registered for the
	 *	null View will receive the event.</p>
	 *
	 *	@param	src		The viewer that generated the change, or null.
	 *	@param	p		The activated project.
	 *	@param	v		The view where the change occured, or null.
	 */
	public static void fireProjectLoaded(Object src, VPTProject p, View v) {
		ProjectViewerEvent evt;
		if (src instanceof ProjectViewer) {
			evt = new ProjectViewerEvent((ProjectViewer) src, p);
		} else {
			ProjectViewer viewer = getViewer(v);
			if (viewer != null) {
				viewer.setRootNode(p);
				return;
			}
			evt = new ProjectViewerEvent(src, p);
		}

		Set listeners = getAllListeners(v);
		for (Iterator i = listeners.iterator(); i.hasNext(); ) {
			((ProjectViewerListener)i.next()).projectLoaded(evt);
		}
	} //}}}

	//{{{ +_fireGroupActivated(VPTGroup, View)_ : void
	/**
	 *	Fires an event for the loading of a group. Notify all the listeners
	 *	registered for the given view and listeners registered for all
	 *	views.
	 *
	 *	<p>If the view provided is null, only the listeners registered for the
	 *	null View will receive the event.</p>
	 *
	 *	@param	grp		The activated group.
	 *	@param	v		The view where the change occured, or null.
	 */
	public static void fireGroupActivated(VPTGroup grp, View v) {
		ProjectViewer viewer = getViewer(v);
		ProjectViewerEvent evt = new ProjectViewerEvent(grp, viewer);
		Set listeners = getAllListeners(v);
		for (Iterator i = listeners.iterator(); i.hasNext(); ) {
			((ProjectViewerListener)i.next()).groupActivated(evt);
		}
	} //}}}

	//{{{ +_fireNodeSelected(ProjectViewer, VPTNode)_ : void
	public static void fireNodeSelected(ProjectViewer src, VPTNode node) {
		View v = jEdit.getActiveView();
		ProjectViewerEvent evt = new ProjectViewerEvent(node, src);
		Set listeners = getAllListeners(v);
		for (Iterator i = listeners.iterator(); i.hasNext(); ) {
			((ProjectViewerListener)i.next()).nodeSelected(evt);
		}
	} //}}}

	//{{{ +_fireProjectAdded(Object, VPTProject)_ : void
	/**
	 *	Fires a "project added" event. All listeners, regardless of the view, are
	 *	notified of this event.
	 */
	public static void fireProjectAdded(Object src, VPTProject p) {
		Set notify = getAllListeners(null);
		ProjectViewerEvent evt = new ProjectViewerEvent(src, p);
		for (Iterator i = notify.iterator(); i.hasNext(); ) {
			((ProjectViewerListener)i.next()).projectAdded(evt);
		}
	} //}}}

	//{{{ +_fireProjectRemoved(Object, VPTProject)_ : void
	/**
	 *	Fires a "project removed" event. All listeners, regardless of the view, are
	 *	notified of this event.
	 */
	public static void fireProjectRemoved(Object src, VPTProject p) {
		Set notify = getAllListeners(null);
		ProjectViewerEvent evt = new ProjectViewerEvent(src, p);
		for (Iterator i = notify.iterator(); i.hasNext(); ) {
			((ProjectViewerListener)i.next()).projectRemoved(evt);
		}
	} //}}}

	//{{{ +_removeProjectViewerListeners(PluginJAR)_ : void
	/**
	 *	Removes the listeners loaded by the given plugin from the listener
	 *	list. Meant to be called when said plugin is unloaded by jEdit.
	 */
	public static void removeProjectViewerListeners(PluginJAR jar) {
		for (Iterator i = listeners.values().iterator(); i.hasNext();) {
			PVActions.prune((Collection) i.next(), jar);
		}
	} //}}}

	//{{{ +_addProjectViewerListeners(PluginJAR, View)_ : void
	/**
	 *	Adds to the list of listeners for the given view the listeners that
	 *	have been declared by the given plugin using properties. For global
	 *	listeners, "view" should be null.
	 */
	public static void addProjectViewerListeners(PluginJAR jar, View view) {
		if (jar.getPlugin() == null) return;
		String list;
		if (view == null) {
			list = jEdit.getProperty("plugin.projectviewer." +
							jar.getPlugin().getClassName() + ".global-pv-listeners");
		} else {
			list = jEdit.getProperty("plugin.projectviewer." +
							jar.getPlugin().getClassName() + ".pv-listeners");
		}

		Collection aList = PVActions.listToObjectCollection(list, jar, ProjectViewerListener.class);
		if (aList != null && aList.size() > 0) {
			ArrayList existing = (ArrayList) listeners.get(view);
			if (existing == null) {
				listeners.put(view, aList);
			} else {
				existing.addAll(aList);
			}
		}
	} //}}}

	//{{{ +_fireNodeMovedEvent(VPTNode, VPTGroup)_ : void
	public static void fireNodeMovedEvent(VPTNode moved, VPTGroup oldParent) {
		Set notify = getAllListeners(null);
		ProjectViewerEvent pve = new ProjectViewerEvent(moved, oldParent);
		for (Iterator i = notify.iterator(); i.hasNext(); ) {
			((ProjectViewerListener)i.next()).nodeMoved(pve);
		}

	} //}}}

	//{{{ +_fireGroupAddedEvent(VPTGroup)_ : void
	public static void fireGroupAddedEvent(VPTGroup group) {
		Set notify = getAllListeners(null);
		ProjectViewerEvent pve = new ProjectViewerEvent(group);
		for (Iterator i = notify.iterator(); i.hasNext(); ) {
			((ProjectViewerListener)i.next()).groupAdded(pve);
		}
	} //}}}

	//{{{ +_fireGroupRemovedEvent(VPTGroup)_ : void
	public static void fireGroupRemovedEvent(VPTGroup group) {
		Set notify = getAllListeners(null);
		ProjectViewerEvent pve = new ProjectViewerEvent(group);
		for (Iterator i = notify.iterator(); i.hasNext(); ) {
			((ProjectViewerListener)i.next()).groupRemoved(pve);
		}
	} //}}}

	//{{{ -_getAllListeners(View)_ : Set
	/**
	 *	Returns a set of all registered ProjectViewerListeners. If a view
	 *	is provided, return only the listeners registered to that view, plus
	 *	the listeners registered globaly.
	 */
	private static Set getAllListeners(View v) {
		HashSet all = new HashSet();
		if (v == null) {
			for (Iterator i = listeners.values().iterator(); i.hasNext(); ) {
				all.addAll((ArrayList)i.next());
			}
		} else {
			Object o = listeners.get(v);
			if (o != null)
				all.addAll((ArrayList)o);
			o = listeners.get(null);
			if (o != null)
				all.addAll((ArrayList)o);
		}
		return all;
	} //}}}

	//}}}

	//{{{ Tree Changes Broadcast Methods

	//{{{ +_nodeStructureChanged(VPTNode)_ : void
	/**
	 *	Notify all project viewer instances of a change in a node's structure.
	 */
	public static void nodeStructureChanged(VPTNode node) {
		VPTProject p = VPTNode.findProjectFor(node);
		for (Iterator it = viewers.values().iterator(); it.hasNext(); ) {
			ViewerEntry ve = (ViewerEntry) it.next();
			ProjectViewer v = ve.dockable;
			if (v == null)
				continue;
			if (v.treeRoot.isNodeDescendant(node)) {
				if (v.folderTree != null) {
					((DefaultTreeModel)v.folderTree.getModel()).nodeStructureChanged(node);
				}
				if (v.fileTree != null) {
					((DefaultTreeModel)v.fileTree.getModel()).nodeStructureChanged(node);
				}

				if (v.workingFileTree != null) {
					((DefaultTreeModel)v.workingFileTree.getModel()).nodeStructureChanged(node);
				}

				if (v.compactTree != null) {
					((DefaultTreeModel)v.compactTree.getModel()).nodeStructureChanged(node);
				}

				if (v.filteredTree != null) {
					((DefaultTreeModel)v.filteredTree.getModel()).nodeStructureChanged(node);
				}
			}
		}
	} //}}}

	//{{{ +_nodeChanged(VPTNode)_ : void
	/** Notify all project viewer instances of a change in a node. */
	public static void nodeChanged(VPTNode node) {
		if (node == null) return;
		for (Iterator it = viewers.values().iterator(); it.hasNext(); ) {
			ViewerEntry ve = (ViewerEntry) it.next();
			ProjectViewer v = ve.dockable;
			if (v == null)
				continue;
			if (v.treeRoot.isNodeDescendant(node)) {
				if (v.folderTree != null) {
					((DefaultTreeModel)v.folderTree.getModel()).nodeChanged(node);
				}
				if (node.canOpen() || node.isProject() || node.isGroup()) {
					if (v.fileTree != null) {
						((DefaultTreeModel)v.fileTree.getModel()).nodeChanged(node);
					}

					if (v.workingFileTree != null) {
						((DefaultTreeModel)v.workingFileTree.getModel()).nodeChanged(node);
					}

					if (v.compactTree != null) {
						((DefaultTreeModel)v.compactTree.getModel()).nodeChanged(node);
					}

					if (v.filteredTree != null) {
						((DefaultTreeModel)v.filteredTree.getModel()).nodeChanged(node);
					}
				}
				if (node == v.treeRoot && v.pList != null) {
					// force a refresh of the "selected node" of the "combo"
					v.pList.setSelectedNode(node);
				}
			}
		}
	} //}}}

	//{{{ +_insertNodeInto(VPTNode, VPTNode)_ : void
	/**
	 *	Inserts a node in the given parent node (in a sorted position according
	 *	to {@link projectviewer.vpt.VPTNode#findIndexForChild(VPTNode) } and
	 *	notifies folder trees in all instances of ProjectViewer.
	 */
	public static void insertNodeInto(VPTNode child, VPTNode parent) {
		int idx = parent.findIndexForChild(child);
		parent.insert(child, idx);

		int[] ind = new int[] { idx };

		for (Iterator it = viewers.values().iterator(); it.hasNext(); ) {
			ViewerEntry ve = (ViewerEntry) it.next();
			ProjectViewer v = ve.dockable;
			if (v == null || !v.getRoot().isNodeDescendant(parent))
				continue;
			if (v.folderTree != null) {
				((DefaultTreeModel)v.folderTree.getModel())
					.nodesWereInserted(parent, ind);
			}
			if (v.compactTree != null) {
				((DefaultTreeModel)v.compactTree.getModel())
					.nodesWereInserted(parent, ind);
			}
			if (v.filteredTree != null) {
				((DefaultTreeModel)v.filteredTree.getModel())
					.nodesWereInserted(parent, ind);
			}
			if (child.isProject() || child.isGroup()) {
				if (v.fileTree != null) {
					((DefaultTreeModel)v.fileTree.getModel())
						.nodesWereInserted(parent, ind);
				}
				if (v.workingFileTree != null) {
					((DefaultTreeModel)v.workingFileTree.getModel())
						.nodesWereInserted(parent, ind);
				}
			}
		}
	} //}}}

	//{{{ +_nodeStructureChangedFlat(VPTNode)_ : void
	/**
	 *	Notify all "flat trees" in any project viewer instances of a change in
	 *	a node's structure.
	 */
	public static void nodeStructureChangedFlat(VPTNode node) {
		if (config.getShowFilesTree() || config.getShowWorkingFilesTree()) {
			for (Iterator it = viewers.values().iterator(); it.hasNext(); ) {
				ViewerEntry ve = (ViewerEntry) it.next();
				ProjectViewer v = ve.dockable;
				if (v == null)
					continue;
				if (v.treeRoot.isNodeDescendant(node)) {
					if (v.fileTree != null) {
						((DefaultTreeModel)v.fileTree.getModel())
							.nodeStructureChanged(node);
					}

					if (v.workingFileTree != null) {
						((DefaultTreeModel)v.workingFileTree.getModel())
							.nodeStructureChanged(node);
					}
				}
			}
		}
	} //}}}

	//{{{ +_removeNodeFromParent(VPTNode)_ : void
	/**
	 *	Removes a node from its parent, and notifies all folder trees in all
	 *	instances of ProjectViewer.
	 */
	public static void removeNodeFromParent(VPTNode child) {
		VPTNode parent = (VPTNode) child.getParent();
		int index = parent.getIndex(child);
		parent.remove(index);

		Object[] removed = new Object[] { child };
		int[] idx = new int[] { index };

		for (Iterator it = viewers.values().iterator(); it.hasNext(); ) {
			ViewerEntry ve = (ViewerEntry) it.next();
			ProjectViewer v = ve.dockable;
			if (v == null || !v.getRoot().isNodeDescendant(parent))
				continue;
			if (v.folderTree != null) {
				((DefaultTreeModel)v.folderTree.getModel())
					.nodesWereRemoved(parent, idx, removed);
			}
			if (v.compactTree != null) {
				((DefaultTreeModel)v.compactTree.getModel())
					.nodesWereRemoved(parent, idx, removed);
			}
			if (v.filteredTree != null) {
				((DefaultTreeModel)v.filteredTree.getModel())
					.nodesWereRemoved(parent, idx, removed);
			}
			if (child.isProject() || child.isGroup()) {
				if (v.fileTree != null) {
					((DefaultTreeModel)v.fileTree.getModel())
						.nodesWereRemoved(parent, idx, removed);
				}
				if (v.workingFileTree != null) {
					((DefaultTreeModel)v.workingFileTree.getModel())
						.nodesWereRemoved(parent, idx, removed);
				}
			}
		}
	} //}}}

	//{{{ +_projectRemoved(Object, VPTProject)_ : void
	/**
	 *	Notify all "flat trees" in any project viewer instances of a change in
	 *	a node's structure. Then, rebuild the project combo boxes.
	 */
	public static void projectRemoved(Object src, VPTProject p) {
		VPTNode parent = (VPTNode) p.getParent();
		int index = parent.getIndex(p);
		parent.remove(index);

		if (config.getShowFoldersTree() || config.getShowFilesTree()
			|| config.getShowWorkingFilesTree() || config.getShowCompactTree()
			|| config.getShowFilteredTree())
		{

			Object[] removed = new Object[] { p };
			int[] idx = new int[] { index };

			for (Iterator it = viewers.values().iterator(); it.hasNext(); ) {
				ViewerEntry ve = (ViewerEntry) it.next();
				ProjectViewer v = ve.dockable;
				if (v == null)
					continue;
				if (p == v.treeRoot) {
					v.setRootNode(VPTRoot.getInstance());
					continue;
				}
				if (v.treeRoot.isNodeDescendant(parent)) {
					if (v.folderTree != null) {
						((DefaultTreeModel)v.folderTree.getModel())
							.nodesWereRemoved(parent, idx, removed);
					}

					if (v.fileTree != null) {
						((DefaultTreeModel)v.fileTree.getModel())
							.nodesWereRemoved(parent, idx, removed);
					}

					if (v.workingFileTree != null) {
						((DefaultTreeModel)v.workingFileTree.getModel())
							.nodesWereRemoved(parent, idx, removed);
					}

					if (v.compactTree != null) {
						((DefaultTreeModel)v.compactTree.getModel())
							.nodesWereRemoved(parent, idx, removed);
					}

					if (v.filteredTree != null) {
						((DefaultTreeModel)v.filteredTree.getModel())
							.nodesWereRemoved(parent, idx, removed);
					}
				}
			}
		}
		fireProjectRemoved(src, p);
	} //}}}

	//}}}

	//{{{ +_setActiveNode(View, VPTNode)_ : void
	/**
	 *	Sets the current active node for the view. If a viewer is
	 *	available for the given view, the root node of the viewer
	 *	is also changed.
	 *
	 *	@throws IllegalArgumentException If node is not a project or group.
	 *	@since PV 2.1.0
	 */
	public static void setActiveNode(View aView, VPTNode n) {
		if (!n.isGroup() && !n.isProject()) {
			throw new IllegalArgumentException("PV can only use Projects and Groups as root.");
		}

		ViewerEntry ve = (ViewerEntry) viewers.get(aView);
		if (ve == null) {
			ve = new ViewerEntry();
			ve.node = n;
			viewers.put(aView, ve);
		} else {
			if (n == ve.node)
				return;
			if (ve.dockable != null) {
				ve.dockable.setRootNode(n);
			} else {
				ve.node = n;
				modifyViewTitle(aView, n);
			}
		}

		if (ve.dockable == null) {
			// Fires events if the dockable is not available
			// (setRootNode() fires events when the dockable is available)
			if (n.isProject()) {
				fireProjectLoaded(ProjectViewer.class, (VPTProject) n, aView);
			} else {
				fireGroupActivated((VPTGroup)n, aView);
			}

			// Loads projects if not yet loaded
			if (n.isProject()
					&& !ProjectManager.getInstance().isLoaded(n.getName())) {
				ProjectManager.getInstance().getProject(n.getName());
			}
		}

	} //}}}

	//{{{ +_getActiveNode(View)_ : VPTNode
	/**
	 *	Return the current active node for the view. Returns null if no
	 *	active node is known for the view.
	 *
	 *	@since	PV 2.1.0
	 */
	public static VPTNode getActiveNode(View aView) {
		ViewerEntry ve = (ViewerEntry) viewers.get(aView);
		if (ve == null) {
			setActiveNode(aView, config.getLastNode());
			ve = (ViewerEntry) viewers.get(aView);
		}
		if (ve.dockable != null) {
			ve.dockable.waitForLoadLock();
		}

		return ve.node;
	} //}}}

	//{{{ -_modifyViewTitle(View, VPTNode)_ : void
	/**
	 *	Mofifies the title of a jEdit view, adding information about
	 *	the given node at the end of the current string.
	 */
	private static void modifyViewTitle(final View view, final VPTNode info) {
		// (info == null) might happen during jEdit startup.
		if (info != null
			&& config.getShowProjectInTitle()
			&& config.isJEdit43())
		{
			view.updateTitle();
			StringBuffer title = new StringBuffer(view.getTitle());
			title.append(" [");
			if (info.isGroup()) {
				title.append("Group: ");
			} else {
				title.append("Project: ");
			}
			title.append(info.getName()).append(']');
			view.setTitle(title.toString());
		}
	} //}}}

	//{{{ +_getActiveProject(View)_ : VPTProject
	/**
	 *	Return the current active project for the view. If no project is
	 *	active, return null.
	 *
	 *	@since	PV 2.1.0
	 */
	public static VPTProject getActiveProject(View aView) {
		VPTNode n = getActiveNode(aView);
		return (n != null && n.isProject()) ? (VPTProject) n : null;
	} //}}}

	//{{{ #_cleanViewEntry(View)_ : void
	/**
	 *	Removes the "viewer entry" related to the given view. Called
	 *	by the ProjectPlugin class when a view closed message is
	 *	received.
	 */
	protected static void cleanViewEntry(View aView) {
		viewers.remove(aView);
	} //}}}

	//}}}

	//{{{ Constants

	private final static String FOLDERS_TAB_TITLE 		= "projectviewer.folderstab";
	private final static String FILES_TAB_TITLE 		= "projectviewer.filestab";
	private final static String WORKING_FILES_TAB_TITLE = "projectviewer.workingfilestab";
	private final static String COMPACT_TAB_TITLE		= "projectviewer.compacttab";
	private final static String FILTERED_TAB_TITLE		= "projectviewer.filteredtab";

	private final static String TREE_STATE_PROP = "projectviewer.folder_tree_state";
	private final static char NOT_EXPANDED		= '0';
	private final static char EXPANDED			= '1';

	//}}}

	//{{{ Attributes
	private View 					view;
	private HashSet					dontAsk;

	private JTree					folderTree;
	private JScrollPane				folderTreeScroller;
	private JTree					fileTree;
	private JScrollPane				fileTreeScroller;
	private JTree					workingFileTree;
	private JScrollPane				workingFileTreeScroller;
	private JTree					compactTree;
	private JScrollPane				compactTreeScroller;
	private JTree					filteredTree;
	private JScrollPane				filteredTreeScroller;
	private JToolBar				toolBar;

	private JPanel					topPane;
	private JTabbedPane				treePane;
	private ProjectComboBox			pList;

	private VPTNode 				treeRoot;
	private VPTContextMenu			vcm;
	private VPTSelectionListener	vsl;
	private ConfigChangeListener	ccl;

	private TreeDragListener		tdl;
	private DragSource				dragSource;

	private boolean					isChangingBuffers;
	private volatile boolean		isLoadingProject;
	private volatile boolean		noTitleUpdate;
	//}}}

	//{{{ +ProjectViewer(View) : <init>
	/**
	 *	Create a new <code>ProjectViewer</code>. Only one instance is allowed
	 *	per view.
	 *
	 *	@param  aView  The jEdit view where the viewer is to be created.
	 *	@throws	UnsupportedOperationException	If a viewer is already instantiated
	 *											for the given view.
`	 */
	public ProjectViewer(View aView) {
		ProjectViewer existant = getViewer(aView);
		if (existant != null) {
			throw new UnsupportedOperationException(
				jEdit.getProperty("projectviewer.error.multiple_views"));
		}

		setLayout(new BorderLayout());
		view = aView;
		vcm = new VPTContextMenu(this);
		vsl = new VPTSelectionListener(this);
		treeRoot = VPTRoot.getInstance();
		isLoadingProject = false;

		addHierarchyListener(this);

		// drag support
		tdl = new TreeDragListener();
		dragSource = new DragSource();

		// GUI
		buildGUI();

		ccl = new ConfigChangeListener();
		config.addPropertyChangeListener(ccl);

		// Loads the listeners from plugins that register listeners using global
		// properties instead of calling the addProjectViewerListener() method.
		EditPlugin[] plugins = jEdit.getPlugins();
		for (int i = 0; i < plugins.length; i++) {
			addProjectViewerListeners(plugins[i].getPluginJAR(), view);
		}

		// Register the dockable window in the viewer list
		ViewerEntry ve = (ViewerEntry) viewers.get(aView);
		if (ve != null) {
			ve.dockable = this;
		} else {
			ve = new ViewerEntry();
			ve.dockable = this;
			ve.node = config.getLastNode();
			viewers.put(aView, ve);
		}
		EditBus.addToBus(this);
		setRootNode(ve.node);
		noTitleUpdate = false;
		setChangingBuffers(false);
	} //}}}

	//{{{ Private methods

	//{{{ -createTree(TreeModel) : JTree
	/** Creates a new tree to be added to the viewer. */
	private JTree createTree(TreeModel model) {
		JTree tree = new PVTree(model);
		tree.setCellRenderer(new VPTCellRenderer());
		//tree.setBorder(BorderFactory.createEtchedBorder());

		// don't change order!
		tree.addMouseListener(vsl);
		tree.addMouseListener(vcm);
		tree.addTreeSelectionListener(vsl);

		// drag support
		dragSource.createDefaultDragGestureRecognizer(tree,
			DnDConstants.ACTION_COPY, tdl);

		return tree;
	} //}}}

	//{{{ -populateToolBar() : void
	/** Loads the toolbar. */
	private void populateToolBar() {
		toolBar.removeAll();
		for (Iterator i = actions.iterator(); i.hasNext(); ) {
			Action a = (Action) i.next();
			a = (Action) a.clone();
			a.setViewer(this);
			toolBar.add(a.getButton());
		}
		toolBar.repaint();
	} //}}}

	//{{{ -buildGUI() : void
	/** Builds the viewer GUI. */
	private void buildGUI() {
		treePane = new JTabbedPane();

		topPane = new JPanel(new BorderLayout());

		pList = new ProjectComboBox(view);

		Box box = new Box(BoxLayout.Y_AXIS);
		box.add(Box.createGlue());
		box.add(pList);
		box.add(Box.createGlue());

		topPane.add(BorderLayout.CENTER, box);

		showTrees();
		showToolBar(config.getShowToolBar());
		add(BorderLayout.NORTH, topPane);

	} //}}}

	//{{{ -closeGroup(VPTGroup, VPTNode) : void
	private void closeGroup(VPTGroup group, VPTNode ignore) {
		for (int i = 0; i < group.getChildCount(); i++) {
			VPTNode child = (VPTNode) group.getChildAt(i);
			if (child != ignore) {
				if (child.isGroup()) {
					closeGroup((VPTGroup)child, ignore);
				} else if (ProjectManager.getInstance().isLoaded(child.getName())){
					closeProject((VPTProject)child);
				}
			}
		}
	} //}}}

	//{{{ -closeProject(VPTProject) : void
	/**
	 *	Closes a project: searches the open buffers for files related to the
	 *	given project and closes them (if desired) and/or saves them to the
	 *	open list (if desired).
	 */
	private void closeProject(VPTProject p) {
		setChangingBuffers(true);
		noTitleUpdate = true;
		p.clearOpenFiles();

		// check to see if project is active in some other viewer, so we
		// don't mess up that guy.
		if (config.getCloseFiles()) {
			for (Iterator it = viewers.values().iterator(); it.hasNext(); ) {
				ViewerEntry ve = (ViewerEntry) it.next();
				if (ve.dockable != this && ve.node.isNodeDescendant(p)) {
					noTitleUpdate = false;
					setChangingBuffers(false);
					return;
				}
			}
		}

		// close files & populate "remember" list
		if (config.getCloseFiles() || config.getRememberOpen()) {
			Buffer[] bufs = jEdit.getBuffers();

			String currFile = null;
			if (p.getChildNode(view.getBuffer().getPath()) != null) {
				currFile = view.getBuffer().getPath();
			}

			for (int i = 0; i < bufs.length; i++) {
				if (p.getChildNode(bufs[i].getPath()) != null) {
					if (config.getRememberOpen() && !bufs[i].getPath().equals(currFile)) {
						p.addOpenFile(bufs[i].getPath());
					}
					if (config.getCloseFiles()) {
						jEdit.closeBuffer(view, bufs[i]);
					}
				}
			}

			if (config.getRememberOpen() && currFile != null) {
				p.addOpenFile(currFile);
			}
		}

		// saves the folder tree state
		String state = getFolderTreeState(p);
		if (state != null) {
			p.setProperty(TREE_STATE_PROP, state);
		} else {
			p.removeProperty(TREE_STATE_PROP);
		}
		noTitleUpdate = false;
		setChangingBuffers(false);
	} //}}}

	//{{{ -openProject(VPTProject) : void
	/** Opens all the files that were previously opened in the project. */
	private void openProject(final VPTProject p) {
		setChangingBuffers(true);
		if (config.getRememberOpen()) {
			for (Iterator i = p.getOpenFiles(); i.hasNext(); ) {
				String next = (String) i.next();
				if (i.hasNext())
					jEdit.openFile(null, next);
				else
					jEdit.openFile(view, next);
			}
		}
		p.clearOpenFiles();

		// loads tree state from the project, if saved
		final String state = p.getProperty(TREE_STATE_PROP);
		if (state != null && folderTree != null) {
			SwingUtilities.invokeLater(
				new Runnable() {
					//{{{ +run() : void
					public void run() {
						setFolderTreeState(p, state);
					} //}}}
				}
			);
		}
		setChangingBuffers(false);
	} //}}}

	//{{{ -showTrees() : void
	/**
	 *	Loads the trees (folders, files, working files) into the view, deciding
	 *  what to show according to the configuration of the plugin
	 */
	private void showTrees() {
		treePane.removeAll();

		// Folders tree
		if (config.getShowFoldersTree()) {
			if(folderTree == null) {
				folderTree = createTree(new DefaultTreeModel(treeRoot, true));
				folderTreeScroller = new JScrollPane(folderTree);
			}
			treePane.addTab(jEdit.getProperty(FOLDERS_TAB_TITLE), folderTreeScroller);
		} else if (folderTree != null) {
			folderTree = null;
			folderTreeScroller = null;
		}

		// Files tree
		if (config.getShowFilesTree()) {
			if(fileTree == null) {
				fileTree = createTree(new VPTFileListModel(treeRoot));
				fileTreeScroller = new JScrollPane(fileTree);
			}
			treePane.addTab(jEdit.getProperty(FILES_TAB_TITLE), fileTreeScroller);
		} else if (fileTree != null) {
			fileTree = null;
			fileTreeScroller = null;
		}

		// Working files tree
		if (config.getShowWorkingFilesTree()) {
			if(workingFileTree == null) {
				VPTWorkingFileListModel model = new VPTWorkingFileListModel(treeRoot);
				workingFileTree = createTree(model);
				workingFileTreeScroller = new JScrollPane(workingFileTree);
			}
			treePane.addTab(jEdit.getProperty(WORKING_FILES_TAB_TITLE), workingFileTreeScroller);
		} else if (workingFileTree != null) {
			workingFileTree = null;
			workingFileTreeScroller = null;
		}

		// compact tree
		if (config.getShowCompactTree()) {
			if(compactTree == null) {
				VPTCompactModel model = new VPTCompactModel(treeRoot);
				compactTree = createTree(model);
				compactTreeScroller = new JScrollPane(compactTree);
			}
			treePane.addTab(jEdit.getProperty(COMPACT_TAB_TITLE,"Compact"), compactTreeScroller);
		} else {
			compactTree = null;
			compactTreeScroller = null;
		}

		// filtered tree
		if (config.getShowFilteredTree()) {
			if(filteredTree == null) {
				VPTFilteredModel model = new VPTFilteredModel(treeRoot);
				filteredTree = createTree(model);
				// show tool tips
				ToolTipManager.sharedInstance().registerComponent(filteredTree);
				filteredTreeScroller = new JScrollPane(filteredTree);
			}
			treePane.addTab(jEdit.getProperty(FILTERED_TAB_TITLE,"Filtered"), filteredTreeScroller);
		} else {
			filteredTree = null;
			filteredTreeScroller = null;
		}

		if (treePane.getTabCount() == 0) {
			remove(treePane);
		} else if (treePane.getTabCount() == 1) {
			remove(treePane);
			add(BorderLayout.CENTER,treePane.getComponentAt(0));
		} else {
			add(BorderLayout.CENTER,treePane);
			treePane.setSelectedIndex(0);
		}
	}//}}}

	//{{{ -showToolBar(boolean) : void
	/** Shows/Hides the toolbar. */
	private void showToolBar(boolean flag) {
		if (toolBar != null) {
			topPane.remove(toolBar);
			toolBar.removeAll();
			toolBar = null;
		}

		if (flag) {
			toolBar = new JToolBar();
			toolBar.setFloatable(false);
			populateToolBar();
			topPane.add(BorderLayout.EAST, toolBar);
		}
	} //}}}

	//{{{ -unloadInactiveProjects() : void
	/** Checks if some of the projects that are loaded can be unloaded. */
	private void unloadInactiveProjects() {
		ArrayList active = null;
		for (Iterator i = viewers.values().iterator(); i.hasNext(); ) {
			ViewerEntry ve = (ViewerEntry) i.next();
			if (ve.node != null && ve.dockable != this) {
				if (active == null)
					active = new ArrayList();
				if (ve.node.isProject()) {
					active.add(ve.node.getName());
				} else if (!ve.node.isRoot()) {
					addProjectsToList(ve.node, active);
				} else {
					return;
				}
			}
		}

		ProjectManager pm = ProjectManager.getInstance();
		for (Iterator i = pm.getProjects(); i.hasNext(); ) {
			VPTProject p = (VPTProject) i.next();
			if (pm.isLoaded(p.getName())
					&& (active == null || !active.contains(p.getName()))) {
				pm.unloadProject(p);
			}
		}
	} //}}}

	//{{{ -addProjectsToList(VPTNode, List) : void
	private void addProjectsToList(VPTNode src, List l) {
		for (int i = 0; i < src.getChildCount(); i++) {
			VPTNode n = (VPTNode) src.getChildAt(i);
			if (n.isProject()) {
				l.add(n.getName());
			} else {
				addProjectsToList(n, l);
			}
		}
	} //}}}

	//{{{ -waitForLoadLock() : void
	/**
	 *	If the isLoadingProject flag is true, wait until notified
	 *	by the thread that is loading the project that loading is
	 *	done. This is more effective than the old way of using a
	 *	few synchronized blocks here and there.
	 */
	private void waitForLoadLock() {
		if (isLoadingProject) {
			synchronized (this) {
				while (isLoadingProject) {
					try {
						this.wait();
					} catch (InterruptedException ie) {
						// ignore
					}
				}
			}
		}
	} //}}}

	//}}}

	//{{{ Public Methods

	//{{{ +setStatus(String) : void
	/** Changes jEdit's status bar message for the current view. */
	public void setStatus(String message) {
		view.getStatus().setMessageAndClear(message);
	} //}}}

	//{{{ +getSelectedNode() : VPTNode
	/** Returns the currently selected node in the tree. */
	public VPTNode getSelectedNode() {
		JTree tree = getCurrentTree();
		if (tree != null && tree.getSelectionPath() != null) {
			return (VPTNode) tree.getSelectionPath().getLastPathComponent();
		} else {
			return null;
		}
	} //}}}

	//{{{ +getSelectedFilePaths() : ArrayList
    /**
     *  Returns an ArrayList of Strings containing the file paths of the selected file and folder nodes.
     *  This is mostly a utility method so other plugins/macros can peform actions on a selection of files.
     *
     */
    public ArrayList getSelectedFilePaths() {

		TreePath last = null;
        ArrayList obfp = new ArrayList();
		String sFiles="";

		JTree tree = getCurrentTree();
		if (tree == null)
			return null;

		if (tree.getSelectionPaths() != null) {
			TreePath[] paths= tree.getSelectionPaths();

		for (int i =0; i < paths.length; i++) {
			   VPTNode nd = (VPTNode)paths[i].getLastPathComponent();

			   if (nd instanceof projectviewer.vpt.VPTFile) {
			   	   sFiles += nd.getNodePath() + "\n";
			   		obfp.add(nd.getNodePath());
			   }
			}
			return obfp;
		} else {
			return null;
		}
    } //}}}

	//{{{ +getCurrentTree() : JTree
	/** Returns the currently active tree. */
	public JTree getCurrentTree() {
		if (treePane.getTabCount() > 0) {
			switch(treePane.getSelectedIndex()) {
				case 0:
					if (folderTree != null) return folderTree;
					if (fileTree != null) return fileTree;
					if (workingFileTree != null) return workingFileTree;
					if (compactTree != null) return compactTree;
					if (filteredTree != null) return filteredTree;
				case 1:
					if (fileTree != null) return fileTree;
					if (workingFileTree != null) return workingFileTree;
					if (compactTree != null) return compactTree;
					if (filteredTree != null) return filteredTree;
				case 2:
					if (workingFileTree != null) return workingFileTree;
					if (compactTree != null) return compactTree;
					if (filteredTree != null) return filteredTree;
				case 3:
					if (compactTree != null) return compactTree;
					if (filteredTree != null) return filteredTree;
				case 4:
					if (filteredTree != null) return filteredTree;
				default:
					return null;
			}
		} else {
			if (folderTree != null) return folderTree;
			if (fileTree != null) return fileTree;
			if (workingFileTree != null) return workingFileTree;
			if (compactTree != null) return compactTree;
			if (filteredTree != null) return filteredTree;
			return null;
		}
	} //}}}

	//{{{ +getView() : View
	/** Returns the View associated with this instance. */
	public View getView() {
		return view;
	} //}}}

	//{{{ +setRootNode(VPTNode) : void
	/**
	 *	Sets the root node of the trees showm by this viewer. The current root
	 *	is cleaned up before setting the new root (e.g., project files are closed,
	 *	etc.)
	 *
	 *	@throws IllegalArgumentException If node is not a project or group.
	 *	@since PV 2.1.0
	 */
	public void setRootNode(VPTNode n) {
		if (n == null)
			n = VPTRoot.getInstance();
		if (n == treeRoot)
			return;

		waitForLoadLock();

		if (!n.isGroup() && !n.isProject()) {
			throw new IllegalArgumentException("PV can only use Projects and Groups as root.");
		}

		// clean up the old root
		if (treeRoot != null && !n.isNodeDescendant(treeRoot)) {
			if (treeRoot.isProject()) {
				closeProject((VPTProject) treeRoot);
			} else {
				closeGroup((VPTGroup)treeRoot, n);
			}
			unloadInactiveProjects();
		}

		// set the new root
		if (n.isProject()) {
			VPTProject p = (VPTProject) n;
			if (!ProjectManager.getInstance().isLoaded(p.getName())) {
				setEnabled(false);
				new ProjectLoader(p.getName()).loadProject();
				return;
			}
			openProject(p);
			fireProjectLoaded(this, p, view);
		} else if (n.isGroup()){
			fireGroupActivated((VPTGroup)n, view);
		}

		treeRoot = n;
		if (folderTree != null)
			((DefaultTreeModel)folderTree.getModel()).setRoot(treeRoot);
		if (fileTree != null)
			((DefaultTreeModel)fileTree.getModel()).setRoot(treeRoot);
		if (workingFileTree != null)
			((DefaultTreeModel)workingFileTree.getModel()).setRoot(treeRoot);
		if (compactTree != null)
			((DefaultTreeModel)compactTree.getModel()).setRoot(treeRoot);
		if (filteredTree != null)
			((DefaultTreeModel)filteredTree.getModel()).setRoot(treeRoot);

		dontAsk = null;
		config.setLastNode(n);
		((ViewerEntry)viewers.get(view)).node = n;
		ProjectManager.getInstance().fireDynamicMenuChange();
		pList.setSelectedNode(treeRoot);
		modifyViewTitle(view, treeRoot);
	} //}}}

	//{{{ +setProject(VPTProject) : void
	/**
	 *	Sets the given project to be the root of the tree. If "p" is null,
	 *	then the root node is set to the "VPTRoot" node.
	 *
	 *	@deprecated		Use {@link #setRootNode(VPTNode) setRootNode(VPTNode)}
	 *					instead.
	 */
	public void setProject(VPTProject p) {
		setRootNode(p);
	} //}}}

	//{{{ +getRoot() : VPTNode
	/**	Returns the root node of the current tree. */
	public VPTNode getRoot() {
		waitForLoadLock();
		return treeRoot;
	} //}}}

	//{{{ +setEnabled(boolean) : void
	/** Enables or disables the viewer GUI. */
	public void setEnabled(boolean flag) {
		treePane.setEnabled(flag);
		pList.setEnabled(flag);
		if (folderTree != null) folderTree.setEnabled(flag);
		if (fileTree != null) fileTree.setEnabled(flag);
		if (workingFileTree != null) workingFileTree.setEnabled(flag);
		if (compactTree != null) compactTree.setEnabled(flag);
		if (filteredTree != null) filteredTree.setEnabled(flag);
		if (toolBar != null) {
			Component[] buttons = toolBar.getComponents();
			for (int i = 0; i < buttons.length; i++)
				buttons[i].setEnabled(flag);
		}
		super.setEnabled(flag);
	} //}}}

	//{{{ +getFolderTreeState(VPTNode) : String
	/**
	 *	Returns a String representing the state of the folder tree.
	 *
	 *	@see	#setFolderTreeState(VPTNode, String)
	 *	@return	The state of the tree, starting at the given node, or
	 *			null if the folderTree is not visible.
	 */
	public String getFolderTreeState(VPTNode node) {
		if (folderTree != null) {
			DefaultTreeModel model = (DefaultTreeModel) folderTree.getModel();
			int start = folderTree.getRowForPath(new TreePath(model.getPathToRoot(node)));
			if (start >= 0) {
				StringBuffer state = new StringBuffer();
				if(folderTree.isExpanded(start)) {
					for(int i = start; i < folderTree.getRowCount(); i++) {
						VPTNode n = (VPTNode) folderTree.getPathForRow(i)
										.getLastPathComponent();
						if (!node.isNodeDescendant(n))
							break;
						if (folderTree.isExpanded(i)) {
							state.append(EXPANDED);
						} else {
							state.append(NOT_EXPANDED);
						}
					}
				}
				return state.toString();
			}
		}
		return null;
	} //}}}

	//{{{ +setFolderTreeState(VPTNode, String) : void
	/**
	 *	Sets the folder tree state from the given String.
	 *
	 *	@see	#getFolderTreeState(VPTNode)
	 */
	public void setFolderTreeState(VPTNode node, String state) {
		if (folderTree != null && state != null) {
			DefaultTreeModel model = (DefaultTreeModel) folderTree.getModel();
			int start = folderTree.getRowForPath(new TreePath(model.getPathToRoot(node)));
			for(int i = 0; i < state.length(); i++) {
				int row = start + i;
				if (row >= folderTree.getRowCount())
					break;

				TreePath path = folderTree.getPathForRow(row);
				if (path == null)
					return;
				VPTNode n = (VPTNode) path.getLastPathComponent();
				if (!node.isNodeDescendant(n))
					break;

				if (state.charAt(i) == EXPANDED) {
					folderTree.expandRow(row);
				}
			}
		}
	} //}}}

	//{{{ +setChangingBuffers(boolean) : void
	/**
	 *	Method intended for use by classes that manage clicks on the
	 *	project trees to open buffers in jEdit; by setting this flag
	 *	to true, the auto-selecting of the new active buffer in jEdit
	 *	is temporarily disabled, preventing the tree from shifting
	 *	around when the user is interacting with it.
	 *
	 *	@since	PV 2.1.1
	 */
	public void setChangingBuffers(boolean flag) {
		isChangingBuffers = flag;
	} //}}}

	//}}}

	//{{{ Message handling

	//{{{ +handleMessage(EBMessage) : void
	/** Handles an EditBus message. */
	public void handleMessage(EBMessage msg) {
		if (msg instanceof ViewUpdate) {
			handleViewUpdateMessage((ViewUpdate) msg);
		} else if (msg instanceof BufferUpdate) {
			handleBufferUpdateMessage((BufferUpdate) msg, treeRoot);
		} else if (msg instanceof DynamicMenuChanged) {
			handleDynamicMenuChanged((DynamicMenuChanged)msg);
		} else if (msg instanceof EditPaneUpdate) {
			handleEditPaneUpdate((EditPaneUpdate)msg);
		} else if (treeRoot != null && treeRoot.isProject()) {
			if (config.isErrorListAvailable()) {
				new Helper().handleErrorListMessage(msg);
			}
		}
	} //}}}

	//{{{ -handleDynamicMenuChanged(DynamicMenuChanged) : void
	/** Handles a handleDynamicMenuChanged EditBus message. */
	private void handleDynamicMenuChanged(DynamicMenuChanged dmg) {
		if (dmg.getMenuName().equals("plugin.projectviewer.ProjectPlugin.menu")) {
			pList.updateMenu();
		}
	}//}}}

	//{{{ -handleViewUpdateMessage(ViewUpdate) : void
	/** Handles a ViewUpdate EditBus message. Checks only whether
	    the EditPane was changed, and focus the file corresponding
		to the buffer on the EditPane on the PV tree. */
	private void handleViewUpdateMessage(ViewUpdate vu) {
		if (vu.getView() == view
			&& vu.getWhat() == ViewUpdate.EDIT_PANE_CHANGED)
		{
			PVActions.focusActiveBuffer(view, treeRoot);
		}
	}//}}}

	//{{{ -handleBufferUpdateMessage(BufferUpdate, VPTNode) : boolean
	/** Handles a BufferUpdate EditBus message.
	 */
	private boolean handleBufferUpdateMessage(BufferUpdate bu, VPTNode where) {
		if (bu.getView() != null && bu.getView() != view) return false;

		boolean ask = false;
		if (bu.getWhat() == BufferUpdate.SAVED) {
			if (where == null || !where.isProject())
				return false;

			VPTProject p = (VPTProject) treeRoot;
			VPTNode f = p.getChildNode(bu.getBuffer().getPath());
			if (f != null)
				return false;

			File file = new File(bu.getBuffer().getPath());
			String fileParentPath = file.getParent() + File.separator;
			String projectRootPath = p.getRootPath() + File.separator;
			ask = (config.getAskImport() != ProjectViewerConfig.ASK_NEVER &&
					(dontAsk == null ||
						config.getAskImport() == ProjectViewerConfig.ASK_ALWAYS ||
						!dontAsk.contains(bu.getBuffer().getPath())) &&
					fileParentPath.startsWith(projectRootPath));

			// Try to import newly created files to the project
			if (ask) {
				int res = JOptionPane.YES_OPTION;
				JCheckBox cBox = null;
				if (config.getAskImport() != ProjectViewerConfig.AUTO_IMPORT) {
					JPanel panel = new JPanel();
					BoxLayout bl = new BoxLayout(panel, BoxLayout.Y_AXIS);
					panel.setLayout(bl);

					JLabel msg = new JLabel(
						jEdit.getProperty("projectviewer.import_new",
							new Object[] { bu.getBuffer().getName(), p.getName() }));
					cBox = new JCheckBox(jEdit.getProperty("projectviewer.import_always_cb"));
					cBox.setSelected(false);
					panel.add(msg);
					panel.add(cBox);

					res = JOptionPane.showConfirmDialog(view,
							panel,
							jEdit.getProperty("projectviewer.import_new.title"),
							JOptionPane.YES_NO_OPTION);
				}

				if (res == JOptionPane.YES_OPTION) {
					new NewFileImporter(p, this, bu.getBuffer().getPath()).doImport();

					if (cBox != null && cBox.isSelected()) {
						config.setAskImport(ProjectViewerConfig.AUTO_IMPORT);
						JPanel panel = new JPanel();
						BoxLayout bl = new BoxLayout(panel, BoxLayout.Y_AXIS);
						panel.setLayout(bl);
						panel.add(new JLabel(jEdit.getProperty("projectviewer.import_always_disable.1")));
						panel.add(new JLabel(jEdit.getProperty("projectviewer.import_always_disable.2")));
						JOptionPane.showMessageDialog(view, panel,
							jEdit.getProperty("projectviewer.import_new.title"),
							JOptionPane.INFORMATION_MESSAGE);
					}
				} else if (config.getAskImport() == ProjectViewerConfig.ASK_ONCE) {
					if (dontAsk == null) {
						dontAsk = new HashSet();
					}
					dontAsk.add(bu.getBuffer().getPath());
				}
			}
		}

		// Notifies trees when a buffer is closed (so it should not be
		// underlined anymore) or opened (should underline it).
		if ((bu.getWhat() == BufferUpdate.CLOSED
			 || bu.getWhat() == BufferUpdate.LOADED
			 || bu.getWhat() == BufferUpdate.DIRTY_CHANGED)
		) {
			if (!noTitleUpdate)
				modifyViewTitle(view, treeRoot);
			if (where != null && where.isProject()) {
				VPTNode f = ((VPTProject)where).getChildNode(bu.getBuffer().getPath());
				if (f != null) {
					if (workingFileTree != null) {
						if (bu.getWhat() == BufferUpdate.CLOSED) {
							((VPTWorkingFileListModel)workingFileTree.getModel())
								.removeOpenFile(f.getNodePath());
						} else if (bu.getWhat() == BufferUpdate.LOADED) {
							((VPTWorkingFileListModel)workingFileTree.getModel())
								.addOpenFile(f.getNodePath());
						}
					}

					ProjectViewer.nodeChanged(f);
					return true;
				}
			} else if (where != null) {
				for (int i = 0; i < where.getChildCount(); i++) {
					if (handleBufferUpdateMessage(bu, (VPTNode)where.getChildAt(i))) {
						return true;
					}
				}
			}
		}

		return false;
 	}//}}}

	//{{{ -handleEditPaneUpdate(EditPaneUpdate) : void
	private void handleEditPaneUpdate(EditPaneUpdate msg) {
		if (msg.getWhat() == EditPaneUpdate.BUFFER_CHANGED
			&& msg.getEditPane().getView() == view
			&& !isChangingBuffers)
		{
			PVActions.focusActiveBuffer(view, treeRoot);
			modifyViewTitle(view, treeRoot);
		}
	} //}}}

	//}}}

	//{{{ +hierarchyChanged(HierarchyEvent) : void
	public void hierarchyChanged(HierarchyEvent he) {
		if (he.getChanged() == this && !isDisplayable() &&
				((he.getChangeFlags() & HierarchyEvent.DISPLAYABILITY_CHANGED)
					== HierarchyEvent.DISPLAYABILITY_CHANGED)) {
			// we're being removed from the GUI, so clean up
			EditBus.removeFromBus(this);
			if (treeRoot != null && treeRoot.isProject()) {
				closeProject((VPTProject)treeRoot);
				config.setLastNode(treeRoot);
			}
			ViewerEntry ve = (ViewerEntry) viewers.get(view);
			if (ve != null) {
				ve.dockable = null;
			}
		}
	} //}}}

	//{{{ -class ConfigChangeListener
	/** Listens for changes in the PV configuration. */
	private class ConfigChangeListener implements PropertyChangeListener, Runnable {

		private boolean willRun = false;

		//{{{ +propertyChange(PropertyChangeEvent) : void
		/** Listens for property change events in the plugin's configuration.
		 *  Shows/Hides the toolbar and the trees, according to the user's wish.
		 *
		 * @param  evt  Description of Parameter
		 */
		public void propertyChange(PropertyChangeEvent evt) {
			// Toolbar show/hide.
			if (evt.getPropertyName().equals(ProjectViewerConfig.SHOW_TOOLBAR_OPT)) {
				showToolBar( ((Boolean)evt.getNewValue()).booleanValue() &&
					(folderTree != null || fileTree != null
					 || workingFileTree != null || compactTree != null
					 || filteredTree != null) );
				return;
			}

			if (evt.getPropertyName().equals(ProjectViewerConfig.ASK_IMPORT_OPT)) {
				int opt = ((Integer)evt.getNewValue()).intValue();
				if (opt == ProjectViewerConfig.ASK_NEVER ||
						opt == ProjectViewerConfig.ASK_ONCE) {
					dontAsk = null;
				}
				return;
			}

			if (evt.getPropertyName().equals(ProjectViewerConfig.SHOW_FILES_OPT) ||
					evt.getPropertyName().equals(ProjectViewerConfig.SHOW_WFILES_OPT) ||
					evt.getPropertyName().equals(ProjectViewerConfig.SHOW_FOLDERS_OPT)||
					evt.getPropertyName().equals(ProjectViewerConfig.SHOW_COMPACT_OPT) ||
					evt.getPropertyName().equals(ProjectViewerConfig.SHOW_FILTERED_OPT))
			{
				if (!willRun) {
					SwingUtilities.invokeLater(this);
					willRun = true;
				}
				return;
			}

		}//}}}

		//{{{ +run() : void
		/** "Run" method, called by the Swing runtime after a config option for one
		 *  or more of the trees has changed.
		 */
		public void run() {
			showTrees();
			showToolBar(config.getShowToolBar());
			willRun = false;
		}//}}}

	} //}}}

	//{{{ -class ProjectLoader
	/** Loads a project in the background. */
	private class ProjectLoader implements Runnable {

		private String pName;
		private final JTree tree;
		private final DefaultTreeModel tModel;

		//{{{ +ProjectLoader(String) : <init>
		public ProjectLoader(String pName) {
			this.pName = pName;
			this.tree = getCurrentTree();
			this.tModel = (tree != null)
					? (DefaultTreeModel) tree.getModel() : null;
		} //}}}

		//{{{ +loadProject() : void
		public void loadProject() {
			// This method is called in the AWT Thread, so do some of the
			// processing here before we start the thread itself.
			treeRoot = null;
			setEnabled(false);
			if (tree != null) {
				tree.setModel(new DefaultTreeModel(
					new DefaultMutableTreeNode(
						jEdit.getProperty("projectviewer.loading_project",
							new Object[] { pName } ))));
			} else {
				setStatus(jEdit.getProperty("projectviewer.loading_project",
							new Object[] { pName } ));
			}

			isLoadingProject = true;

			WorkerThreadPool.getSharedInstance().addRequest(this);
		} //}}}

		//{{{ +run() : void
		public void run() {
			final VPTProject p;
			p = ProjectManager.getInstance().getProject(pName);

			synchronized (ProjectViewer.this) {
				isLoadingProject = false;
				ProjectViewer.this.notifyAll();
			}

			PVActions.swingInvoke(
				new Runnable() {
					public void run() {
						if (tree != null) {
							tModel.setRoot(p);
							tree.setModel(tModel);
						}
						setRootNode(p);
						setEnabled(true);
					}
				}
			);
		} //}}}

	} //}}}

	//{{{ -class PVTree
	/** Listens for key events in the trees. */
	private class PVTree extends JTree {

		//{{{ +PVTree(TreeModel) : <init>
		public PVTree(TreeModel model) {
			super(model);
		} //}}}

		//{{{ +processKeyEvent(KeyEvent) : void
		public void processKeyEvent(KeyEvent e) {
			if (e.getID() == KeyEvent.KEY_PRESSED && e.getKeyCode() == KeyEvent.VK_ENTER) {
				TreePath[] paths = getSelectionPaths();
				for (int i = 0; i < paths.length; i++) {
					VPTNode n = (VPTNode) paths[i].getLastPathComponent();
					if (n.isFile()) {
						n.open();
					}
				}
				e.consume();
			} else {
				super.processKeyEvent(e);
			}
		} //}}}

		//{{{ +expandPath(TreePath) : void
		/**
		 *	If trying to expand unloaded projects, load them before expansion
		 *	occurs.
		 */
		public void expandPath(TreePath path) {
			VPTNode n = (VPTNode) path.getLastPathComponent();
			if (n.isProject()
					&& !ProjectManager.getInstance().isLoaded(n.getName())) {

				synchronized (n) {
					if (!ProjectManager.getInstance().isLoaded(n.getName())) {
						setStatus(jEdit.getProperty("projectviewer.loading_project",
							new Object[] { n.getName() } ));
						ProjectManager.getInstance().getProject(n.getName());
					}
				}
			}
			super.expandPath(path);

			if (n.isProject() || n.isGroup()) {
				if (folderTree != null && folderTree != this)
					((PVTree)folderTree).expand(path);
				if (fileTree != null && fileTree != this)
					((PVTree)fileTree).expand(path);
				if (workingFileTree != null && workingFileTree != this)
					((PVTree)workingFileTree).expand(path);
				if (compactTree != null && compactTree != this)
					((PVTree)compactTree).expand(path);
				if (filteredTree != null && filteredTree != this)
					((PVTree)filteredTree).expand(path);
			}

		} //}}}

		//{{{ +collapsePath(TreePath) : void
		/** Keeps trees syncd w.r.t. projects and groups. */
		public void collapsePath(TreePath path) {
			super.collapsePath(path);
			VPTNode n = (VPTNode) path.getLastPathComponent();
			if (n.isProject() || n.isGroup()) {
				if (folderTree != null && folderTree != this)
					((PVTree)folderTree).collapse(path);
				if (fileTree != null && fileTree != this)
					((PVTree)fileTree).collapse(path);
				if (workingFileTree != null && workingFileTree != this)
					((PVTree)workingFileTree).collapse(path);
				if (compactTree != null && compactTree != this)
					((PVTree)compactTree).collapse(path);
				if (filteredTree != null && filteredTree != this)
					((PVTree)filteredTree).collapse(path);
			}
		} //}}}

		//{{{ -expand(TreePath) : void
		/**
		 *	Used internally to bypass the overridden "expandPath()" method and
		 *	keep the different trees synced w.r.t. projects and groups.
		 */
		private void expand(TreePath path) {
			super.expandPath(path);
		} //}}}

		//{{{ -collapse(TreePath) : void
		/**
		 *	Used internally to bypass the overridden "expandPath()" method and
		 *	keep the different trees synced w.r.t. projects and groups.
		 */
		private void collapse(TreePath path) {
			super.collapsePath(path);
		} //}}}

	} //}}}

	//{{{ -class TreeDragListener
	/**
	 *	Implements a DragGestureListener for the trees, that will detect when
	 *	the user tries to drag a file to somewhere. Other kinds of nodes will
	 *	be ignored.
	 */
	private class TreeDragListener implements DragGestureListener {

		//{{{ +dragGestureRecognized(DragGestureEvent) : void
		public void dragGestureRecognized(DragGestureEvent dge) {
			JTree tree = getCurrentTree();
			TreePath path = tree.getPathForLocation( (int) dge.getDragOrigin().getX(),
								(int) dge.getDragOrigin().getY());

			if (path != null) {
				VPTNode n = (VPTNode) path.getLastPathComponent();
				if (n.isFile()) {
					dge.startDrag(DragSource.DefaultCopyDrop,
									new FileListTransferable((VPTFile)n));
				}
			}
		} //}}}

	} //}}}

	//{{{ -class _FileListTransferable_
	/** A transferable for a file. */
	private static class FileListTransferable extends LinkedList implements Transferable {

		//{{{ +FileListTransferable(VPTFile) : <init>
		public FileListTransferable(VPTFile file) {
			super.add(file.getFile());
		} //}}}

		//{{{ +getTransferData(DataFlavor) : Object
		public Object getTransferData(DataFlavor flavor) {
			if (flavor == DataFlavor.javaFileListFlavor) {
				return this;
			}
			return null;
		} //}}}

		//{{{ +getTransferDataFlavors() : DataFlavor[]
		public DataFlavor[] getTransferDataFlavors() {
			return new DataFlavor[] { DataFlavor.javaFileListFlavor };
		} //}}}

		//{{{ +isDataFlavorSupported(DataFlavor) : boolean
		public boolean isDataFlavorSupported(DataFlavor flavor) {
			return (flavor == DataFlavor.javaFileListFlavor);
		} //}}}

	} //}}}

	//{{{ -class _ViewerEntry_
	/**
	 *	Holds information about what's active on what view, to allow active
	 *	nodes even with no dockable open.
	 *
	 *	@since	PV 2.1.0
	 */
	private static class ViewerEntry {
		public ProjectViewer dockable;
		public VPTNode node;
	} //}}}

	//{{{ -class Helper
	/**
	 *	Class to hold methods that require classes that may not be available,
	 *	so that PV behaves well when called from a BeanShell script.
	 */
	private class Helper {

		//{{{ +handleErrorListMessage(EBMessage) : void
		public void handleErrorListMessage(EBMessage msg) {
			if (msg instanceof ErrorSourceUpdate) {
				handleErrorSourceUpdateMessage((ErrorSourceUpdate) msg);
			}
		} //}}}

		//{{{ -handleErrorSourceUpdateMessage(ErrorSourceUpdate) : void
		/** Handles a ErrorSourceUpdate EditBus message. */
		private void handleErrorSourceUpdateMessage(ErrorSourceUpdate esu) {
			//Log.log(Log.DEBUG, this, "ErrorSourceUpdate received :["+esu.getWhat()+"]["+esu.getErrorSource().getName()+"]");
			if ( esu.getWhat() == ErrorSourceUpdate.ERROR_ADDED ||
				esu.getWhat() == ErrorSourceUpdate.ERROR_REMOVED) {
				VPTProject p = (VPTProject) treeRoot;
				ErrorSource.Error error = esu.getError();
				VPTNode f = p.getChildNode(error.getFilePath());
				if ( f != null ) {
					//Log.log(Log.DEBUG, this, "ErrorSourceUpdate for :["+error.getFilePath()+"]");
					if (folderTree != null) {
						((DefaultTreeModel)folderTree.getModel()).nodeChanged(f);
					}
					if (fileTree != null) {
						((DefaultTreeModel)fileTree.getModel()).nodeChanged(f);
					}
					if (workingFileTree != null) {
						((VPTWorkingFileListModel)workingFileTree.getModel()).nodeChanged(f);
					}
					if (compactTree != null) {
						((DefaultTreeModel)compactTree.getModel()).nodeChanged(f);
					}
					if (filteredTree != null) {
						((DefaultTreeModel)filteredTree.getModel()).nodeChanged(f);
					}
				}
			}
			if (esu.getWhat() == ErrorSourceUpdate.ERROR_SOURCE_ADDED
					|| esu.getWhat() == ErrorSourceUpdate.ERROR_SOURCE_REMOVED
					|| esu.getWhat() == ErrorSourceUpdate.ERRORS_CLEARED) {
				VPTProject p = (VPTProject) treeRoot;
				if (folderTree != null) {
					folderTree.repaint();
				}
				if (fileTree != null) {
					fileTree.repaint();
				}
				if (workingFileTree != null) {
					workingFileTree.repaint();
				}
				if (compactTree != null) {
					compactTree.repaint();
				}
				if (filteredTree != null) {
					filteredTree.repaint();
				}
			}
		}//}}}

	} //}}}

}