/* * NodeTreeManager.java * JCollider * * Copyright (c) 2004-2010 Hanns Holger Rutz. All rights reserved. * * This software 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, june 1991 of the License, or (at your option) any later version. * * This software 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 (gpl.txt) along with this software; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA * * * For further information, please contact Hanns Holger Rutz at * contact@sciss.de , or visit http://www.sciss.de/jcollider * * * JCollider is closely modelled after SuperCollider Language, * often exhibiting a direct translation from Smalltalk to Java. * SCLang is a software originally developed by James McCartney, * which has become an Open Source project. * See http://www.audiosynth.com/ for details. * * * Changelog: * 02-Oct-05 created */ package de.sciss.jcollider.gui; import java.io.PrintStream; import java.util.Enumeration; import java.util.HashMap; import java.util.List; import java.util.Map; import javax.swing.tree.DefaultTreeModel; import javax.swing.tree.TreeModel; import javax.swing.tree.TreeNode; import de.sciss.jcollider.Group; import de.sciss.jcollider.Node; import de.sciss.jcollider.NodeEvent; import de.sciss.jcollider.NodeListener; import de.sciss.jcollider.NodeWatcher; /** * A class that helps to set up a <code>JTree</code> with a view of * a node graph. Since <code>Node</code> already implements the * <code>TreeNode</code> interface, what we do here is to install * a <code>NodeListener</code> on a provided <code>NodeWatcher</code> * and keep a <code>TreeModel</code> up-to-date. In many cases * you will not need to set up a <code>JTree</code> yourself, * but simply create an instance of <code>NodeTreePanel</code>. * <p> * Because of the asynchronous nature of node manipulation, * we have decided to not use the <code>MutableTreeNode</code> interface * which cannot account for method failures, for example. The proposed * implementation therefore is, to call the node's methods like * <code>free</code> or <code>run</code> directly, and the tree will be * updated as soon as the <code>"/n_go"</code>, <code>"/n_off"</code> etc. * messages arrive. * * @author Hanns Holger Rutz * @version 0.31, 08-Oct-07 * * @see de.sciss.jcollider.gui.NodeTreePanel * @see de.sciss.jcollider.NodeWatcher * * @todo seems to miss /n_end when fired very quickly after /n_go ... */ public class NodeTreeManager implements NodeListener { /** * Set this to <code>true</code> for debugging * the incoming node notifications and tree model updates */ public boolean VERBOSE = false; private final DefaultTreeModel model; private final NodeWatcher nw; private final Map mapNodeBackups = new HashMap(); /** * Creates a new <code>NodeTreeManager</code> for a given * <code>NodeWatcher</code> instance and root <code>Node</code>. * * @param nw the nodewatcher to listen to for node updates. * the tree manager copies the current list of registered * nodes from the watcher and continuously updates the tree * when node notfication messages arrive. * @param rootNode the supposed root element in the tree display. * can be something like <code>server.getDefaultGroup()</code>. */ public NodeTreeManager( NodeWatcher nw, Node rootNode ) { this.nw = nw; model = new DefaultTreeModel( rootNode, true ); nw.addListener( this ); final List collNodes = nw.getAllNodes(); Node node; for( int i = 0; i < collNodes.size(); i++ ) { node = (Node) collNodes.get( i ); mapNodeBackups.put( new Integer( node.getNodeID() ), node ); } } public NodeTreeManager( NodeWatcher nw ) { this( nw, null ); } public void setRoot( Node rootNode ) { model.setRoot( rootNode ); } /** * Disposes all resources associated with the tree * manager. This clears the node backup list and * stops listening to the node watcher. Call this * method before calling <code>dispose</code> on * the node watcher. */ public void dispose() { mapNodeBackups.clear(); setRoot( null ); nw.removeListener( this ); } /** * Returns a <code>TreeModel</code> suitable to * creating a <code>JTree</code> gadget. * * @return a tree model that reflects the node graph * monitored by this manager */ public TreeModel getModel() { return model; } /** * Dumps a list of nodes to the given stream. * Starts at the provided root node and traverses * all child elements. Usefull for debugging. * * @param stream a stream to print on, such as <code>System.err</code> * @param rootNode the top element of the tree, or <code>null</code> (no action) */ public static void dumpTree( PrintStream stream, TreeNode rootNode ) { if( rootNode == null ) { stream.println( "Empty tree" ); } else { dumpTree( stream, rootNode, 0 ); } } private static void dumpTree( PrintStream stream, TreeNode node, int nestCount ) { for( int i = 0; i < nestCount; i++ ) stream.print( " " ); if( node.isLeaf() ) { stream.println( "- " + node.toString() ); } else { stream.println( node.toString() ); // stream.println( node.toString() + " : " + node.getChildCount() + " leafs" ); if( ++nestCount > 100 ) { stream.println( "\nNest count exceeds 100, probably closed loop. Terminating" ); } int childCount = 0; for( Enumeration children = node.children(); children.hasMoreElements() && (childCount < 300); childCount++ ) { dumpTree( stream, (TreeNode) children.nextElement(), nestCount ); } if( childCount == 300 ) { stream.println( "\nChild count exceeds 300, probably closed loop. Terminating" ); } } } // ---------- NodeListener interface ---------- /** * This method is part of the <code>NodeListener</code> interface. * Do not call this method. */ public void nodeAction( NodeEvent e ) { final Node node = e.getNode(); if( node == null ) return; // only if we've got a client representation final Integer key = new Integer( e.getNodeID() ); final Group group; final Group groupBak; final Node predBak; final int idx; switch( e.getID() ) { case NodeEvent.GO: group = node.getGroup(); if( group != null ) { idx = group.getIndex( node ); if( VERBOSE ) System.err.println( "nodesWereInserted( "+group+", { "+idx+" }, { "+node+" })" ); model.nodesWereInserted( group, new int[] { idx }); } mapNodeBackups.put( key, node ); break; case NodeEvent.END: groupBak = (Group) mapNodeBackups.get( new Integer( e.getOldParentGroupID() )); predBak = (Node) mapNodeBackups.get( new Integer( e.getOldPredNodeID() )); if( groupBak != null ) { idx = predBak == null ? 0 : groupBak.getIndex( predBak ) + 1; if( VERBOSE ) System.err.println( "nodesWereRemoved( "+groupBak+", { "+idx+" }, { "+node+" })" ); model.nodesWereRemoved( groupBak, new int[] { idx }, new Object[] { node }); } mapNodeBackups.remove( key ); break; case NodeEvent.ON: case NodeEvent.OFF: if( VERBOSE ) System.err.println( "nodeChanged( "+node+")" ); model.nodeChanged( node ); break; case NodeEvent.MOVE: groupBak = (Group) mapNodeBackups.get( new Integer( e.getOldParentGroupID() )); group = node.getGroup(); if( (group != null) && (groupBak != null) ) { final TreeNode[] oldPath = model.getPathToRoot( groupBak ); final TreeNode[] newPath = model.getPathToRoot( group ); TreeNode commonParent = (TreeNode) model.getRoot(); int nodeID; // now determine the nearest parent of both groups ; XXX does is make sense? commonLp: for( int i = 0; i < oldPath.length; i++ ) { nodeID = ((Node) oldPath[ i ]).getNodeID(); for( int j = 0; j < newPath.length; j++ ) { if( nodeID == ((Node) newPath[ j ]).getNodeID() ) { commonParent = oldPath[ i ]; break commonLp; } } } if( commonParent != null ) { if( VERBOSE ) System.err.println( "nodeStructureChanged( "+commonParent+")" ); model.nodeStructureChanged( commonParent ); } else { model.nodeStructureChanged( (TreeNode) model.getRoot() ); } } break; case NodeEvent.INFO: group = node.getGroup(); if( group != null ) { if( VERBOSE ) System.err.println( "nodeStructureChanged( "+group+")" ); model.nodeStructureChanged( group ); } mapNodeBackups.put( key, node ); break; default: break; } } }