org.netbeans.swing.outline.TreePathSupport Maven / Gradle / Ivy
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.netbeans.swing.outline;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import javax.swing.SwingUtilities;
import javax.swing.event.TreeExpansionEvent;
import javax.swing.event.TreeExpansionListener;
import javax.swing.event.TreeModelEvent;
import javax.swing.event.TreeWillExpandListener;
import javax.swing.tree.AbstractLayoutCache;
import javax.swing.tree.ExpandVetoException;
import javax.swing.tree.TreeModel;
import javax.swing.tree.TreePath;
/** Manages expanded/collapsed paths for the Outline. Provides services similar
* to those JTree implements inside its own class body. Propagates changes
* in expanded state to the layout cache.
*
* Principally what this class does is manage the state of expanded paths which
* are not visible, or whose parents have been closed/opened. Whereas the
* layout cache retains information only about what is visibly expanded, this
* class manages information about any path that has been expanded at some
* point in the lifetime of an Outline, so that for example, if A contains B
* contains C, and A and B and C are expanded, then the user collapses A,
* and later re-expands A, B and C will retain their expanded state and
* appear as they did the last time A was expanded.
*
* When nodes are removed, the OutlineModel must call removePath() for any
* defunct paths to avoid memory leaks by the TreePathSupport holding
* references to defunct nodes and not allowing them to be garbage collected.
*
* Its addTreeWillExpandListener
code supports
* ExtTreeWillExpandListener
, so such a listener may be notified
* if some other listener vetoes a pending expansion event.
*
* @author Tim Boudreau
*/
public final class TreePathSupport {
private List eListeners = new ArrayList();
private List weListeners = new ArrayList();
private AbstractLayoutCache layout;
/** Creates a new instance of TreePathSupport */
public TreePathSupport(OutlineModel mdl, AbstractLayoutCache layout) {
this.layout = layout;
}
/** Clear all expanded path data. This is called if the tree model fires
* a structural change, and any or all of the nodes it contains may no
* longer be present. */
public void clear() {
}
/** Expand a path. Notifies the layout cache of the change,
* stores the expanded path info (so re-expanding a parent node also re-expands
* this path if a parent node containing it is later collapsed). Fires
* TreeWillExpand and TreeExpansion events.
* @param path The tree path to expand
*/
public void expandPath (TreePath path) {
assert SwingUtilities.isEventDispatchThread();
if (layout.isExpanded(path)) {
//It's already expanded, don't waste cycles firing bogus events
return;
}
TreePath parentPath = path.getParentPath();
if (parentPath != null) {
expandPath(parentPath);
}
TreeExpansionEvent e = new TreeExpansionEvent (this, path);
try {
fireTreeWillExpand(e, true);
layout.setExpandedState(path, true);
fireTreeExpansion(e, true);
} catch (ExpandVetoException eve) {
fireTreeExpansionVetoed (e, eve);
}
}
/** Collapse a path. Notifies the layout cache of the change,
* stores the expanded path info (so re-expanding a parent node also re-expands
* this path if a parent node containing it is later collapsed). Fires
* TreeWillExpand and TreeExpansion events.
* @param path The tree path to collapse
*/
public void collapsePath (TreePath path) {
assert SwingUtilities.isEventDispatchThread();
if (!layout.isExpanded(path)) {
//It's already collapsed, don't waste cycles firing bogus events
return;
}
TreeExpansionEvent e = new TreeExpansionEvent (this, path);
try {
fireTreeWillExpand(e, false);
layout.setExpandedState(path, false);
fireTreeExpansion(e, false);
} catch (ExpandVetoException eve) {
fireTreeExpansionVetoed (e, eve);
}
}
/** Remove a path's data from the list of known paths. Called when
* a tree model deletion event occurs
* @param path The tree path to remove
*/
public void removePath (TreePath path) {
}
private void fireTreeExpansion (TreeExpansionEvent e, boolean expanded) {
int size = eListeners.size();
TreeExpansionListener[] listeners = new TreeExpansionListener[size];
synchronized (this) {
listeners = eListeners.toArray(listeners);
}
for (int i=0; i < listeners.length; i++) {
if (expanded) {
listeners[i].treeExpanded(e);
} else {
listeners[i].treeCollapsed(e);
}
}
}
private void fireTreeWillExpand (TreeExpansionEvent e, boolean expanded) throws ExpandVetoException {
int size = weListeners.size();
TreeWillExpandListener[] listeners = new TreeWillExpandListener[size];
synchronized (this) {
listeners = weListeners.toArray(listeners);
}
for (int i=0; i < listeners.length; i++) {
if (expanded) {
listeners[i].treeWillExpand(e);
} else {
listeners[i].treeWillCollapse(e);
}
}
}
private void fireTreeExpansionVetoed (TreeExpansionEvent e, ExpandVetoException ex) {
int size = weListeners.size();
TreeWillExpandListener[] listeners = new TreeWillExpandListener[size];
synchronized (this) {
listeners = weListeners.toArray(listeners);
}
for (int i=0; i < listeners.length; i++) {
if (listeners[i] instanceof ExtTreeWillExpandListener) {
((ExtTreeWillExpandListener) listeners[i]).treeExpansionVetoed(e,
ex);
}
}
}
/**
* Test if the tree path is expanded.
* @param path The tree path to test
* @return true
if the path is expanded, false
otherwise.
*/
public boolean hasBeenExpanded(TreePath path) {
assert SwingUtilities.isEventDispatchThread();
return (path != null && layout.isExpanded(path));
}
/**
* Returns true if the node identified by the path is currently expanded,
*
* @param path the TreePath
specifying the node to check
* @return false if any of the nodes in the node's path are collapsed,
* true if all nodes in the path are expanded
*/
public boolean isExpanded(TreePath path) {
assert SwingUtilities.isEventDispatchThread();
if(path == null)
return false;
if (!layout.isRootVisible() && path.getParentPath() == null) {
return true; // Invisible root is always expanded
}
// Is this node expanded?
boolean nodeExpanded = layout.isExpanded(path);
if (!nodeExpanded) {
return false;
}
// It is, make sure its parent is also expanded.
TreePath parentPath = path.getParentPath();
if(parentPath != null)
return isExpanded(parentPath);
return true;
}
/**
* Test if the tree path is visible (the parent path is expanded).
* @param path The tree path to test
* @return true
if the path is visible, false
otherwise.
*/
public boolean isVisible(TreePath path) {
if(path != null) {
TreePath parentPath = path.getParentPath();
if(parentPath != null) {
return isExpanded(parentPath);
}
// Root.
return true;
}
return false;
}
/**
* Get all expanded descendants of the given tree path.
* @param parent Tree path to find the expanded descendants for
* @return All expanded descendants.
*/
public TreePath[] getExpandedDescendants(TreePath parent) {
assert SwingUtilities.isEventDispatchThread();
TreePath[] result = new TreePath[0];
if(isExpanded(parent)) {
TreePath path;
List results = null;
Enumeration tpe = layout.getVisiblePathsFrom(parent);
if (tpe != null) {
while (tpe.hasMoreElements()) {
path = tpe.nextElement();
// Add the path if it is expanded, a descendant of parent,
// and it is visible (all parents expanded). This is rather
// expensive!
if (path != parent &&
layout.isExpanded(path) &&
parent.isDescendant(path)) {
if (results == null) {
results = new ArrayList();
}
results.add (path);
}
}
if (results != null) {
result = results.toArray(result);
}
}
}
return result;
}
/** Add a TreeExpansionListener. If the TreeWillExpandListener implements
* ExtTreeExpansionListener, it will be notified if another
* TreeWillExpandListener vetoes the expansion event
* @param l The tree expansion listener
*/
public synchronized void addTreeExpansionListener (TreeExpansionListener l) {
eListeners.add(l);
}
/**
* Remove a TreeExpansionListener.
* @param l The tree expansion listener
*/
public synchronized void removeTreeExpansionListener (TreeExpansionListener l) {
eListeners.remove(l);
}
/**
* Add a TreeWillExpandListener.
* @param l The tree will expand listener
*/
public synchronized void addTreeWillExpandListener (TreeWillExpandListener l) {
weListeners.add(l);
}
/**
* Remove a TreeWillExpandListener.
* @param l The tree will expand listener
*/
public synchronized void removeTreeWillExpandListener (TreeWillExpandListener l) {
weListeners.remove(l);
}
/**
* We need to copy the expansion state to the structurally changed tree.
* The structural change of the tree will have the effect of collapsing
* all expanded paths.
* This method takes care of dumping the layout expansion state and
* re-expand the originally expanded nodes.
* @param e
*/
void treeStructureChanged(TreeModelEvent event) {
TreePath path = event.getTreePath();
TreeModel model = layout.getModel();
if ((path == null) && (model != null)) {
Object root = model.getRoot();
if (root != null) {
path = new TreePath(root);
}
}
TreePath[] expandedDescendants = getExpandedDescendants(path);
layout.treeStructureChanged(event);
for (TreePath tp : expandedDescendants) {
layout.setExpandedState(tp, true);
}
}
}