001/* A tree model for Ptolemy II objects, for use with JTree. 002 003 Copyright (c) 2000-2014 The Regents of the University of California. 004 All rights reserved. 005 Permission is hereby granted, without written agreement and without 006 license or royalty fees, to use, copy, modify, and distribute this 007 software and its documentation for any purpose, provided that the above 008 copyright notice and the following two paragraphs appear in all copies 009 of this software. 010 011 IN NO EVENT SHALL THE UNIVERSITY OF CALIFORNIA BE LIABLE TO ANY PARTY 012 FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES 013 ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF 014 THE UNIVERSITY OF CALIFORNIA HAS BEEN ADVISED OF THE POSSIBILITY OF 015 SUCH DAMAGE. 016 017 THE UNIVERSITY OF CALIFORNIA SPECIFICALLY DISCLAIMS ANY WARRANTIES, 018 INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 019 MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE 020 PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, AND THE UNIVERSITY OF 021 CALIFORNIA HAS NO OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, 022 ENHANCEMENTS, OR MODIFICATIONS. 023 024 PT_COPYRIGHT_VERSION_2 025 COPYRIGHTENDKEY 026 027 */ 028package ptolemy.vergil.tree; 029 030import java.lang.ref.WeakReference; 031import java.util.ArrayList; 032import java.util.Arrays; 033import java.util.Iterator; 034import java.util.LinkedList; 035import java.util.List; 036 037import javax.swing.SwingUtilities; 038import javax.swing.event.TreeModelEvent; 039import javax.swing.event.TreeModelListener; 040import javax.swing.tree.TreeModel; 041import javax.swing.tree.TreePath; 042 043import ptolemy.kernel.CompositeEntity; 044import ptolemy.kernel.util.ChangeListener; 045import ptolemy.kernel.util.ChangeRequest; 046import ptolemy.kernel.util.NamedObj; 047 048/////////////////////////////////////////////////////////////////// 049//// EntityTreeModel 050 051/** 052 053 A tree model for Ptolemy II objects. This class makes it easy to 054 view ptolemy models in a JTree, which renders the hierarchy. 055 This base class handles only composite entities and their contained 056 entities. It does not include entities that are class definitions. 057 Derived classes represent more (or less) of the Ptolemy II model. 058 059 @author Steve Neuendorffer and Edward A. Lee, Contributor: Jianwu Wang 060 @version $Id$ 061 @since Ptolemy II 1.0 062 @Pt.ProposedRating Red (eal) 063 @Pt.AcceptedRating Red (johnr) 064 */ 065public class EntityTreeModel implements TreeModel { 066 /** Create a new tree model with the specified root. 067 * Normally the root is an instance of CompositeEntity, but other 068 * root objects might be used by derived classes. 069 * @param root The root of the tree. 070 */ 071 public EntityTreeModel(NamedObj root) { 072 setRoot(root); 073 } 074 075 /////////////////////////////////////////////////////////////////// 076 //// public methods //// 077 078 /** Add a listener to this model. 079 * @param listener The listener to add. 080 * @see #removeTreeModelListener(TreeModelListener) 081 */ 082 @Override 083 public void addTreeModelListener(TreeModelListener listener) { 084 // In http://bugzilla.ecoinformatics.org/show_bug.cgi?id=4801, 085 // Jianwu Wang found that the _listenerList attribute in 086 // ptolemy.vergil.tree.EntityTreeModel was leaking memory. 087 // This attribute keeps holding some references, so that 088 // instances of EntityTreeModel and its sub-classes can not be 089 // GCed when a window is open and closed. 090 // 091 // When it and its sub-classes are used in Kepler, 092 // addTreeModelListener() and removeTreeModelListener() 093 // functions of EntityTreeModel class are called implicitly. 094 // Jianwu did not find a good way to clean up after a window 095 // is closed. Changing the _listenerList attribute using 096 // WeakReference fixe the leak. 097 _listenerList.add(new WeakReference(listener)); 098 } 099 100 /** Get the child of the given parent at the given index. 101 * If the child does not exist, then return null. 102 * In this base class, a child is a contained entity. 103 * @param parent A node in the tree. 104 * @param index The index of the desired child. 105 * @return A node, or null if there is no such child. 106 */ 107 @Override 108 public Object getChild(Object parent, int index) { 109 if (index > getChildCount(parent)) { 110 return null; 111 } 112 113 CompositeEntity entity = (CompositeEntity) parent; 114 return entity.entityList().get(index); 115 } 116 117 /** Return the number of children of the given parent, which in 118 * this base class is the number of contained entities. 119 * @param parent A parent node. 120 * @return The number of contained entities. 121 */ 122 @Override 123 public int getChildCount(Object parent) { 124 if (!(parent instanceof CompositeEntity)) { 125 return 0; 126 } 127 128 CompositeEntity entity = (CompositeEntity) parent; 129 return entity.numberOfEntities(); 130 } 131 132 /** Return the index of the given child within the given parent. 133 * If the parent is not contained in the child or is not an instance 134 * of CompositeEntity return -1. 135 * @param parent The parent, which is usually a CompositeEntity. 136 * @param child The child. 137 * @return The index of the specified child. 138 */ 139 @Override 140 public int getIndexOfChild(Object parent, Object child) { 141 if (!(parent instanceof CompositeEntity)) { 142 return -1; 143 } 144 145 CompositeEntity entity = (CompositeEntity) parent; 146 return entity.entityList().indexOf(child); 147 } 148 149 /** Get the root of this tree model. 150 * @return A NamedObj, usually an Entity. 151 * @see #setRoot(NamedObj) 152 */ 153 @Override 154 public Object getRoot() { 155 return _root; 156 } 157 158 /** Return true if the object is a leaf node. In this base class, 159 * an object is a leaf node if it is not an instance of CompositeEntity. 160 * @param object The object in question. 161 * @return True if the node has no children. 162 */ 163 @Override 164 public boolean isLeaf(Object object) { 165 if (!(object instanceof CompositeEntity)) { 166 return true; 167 } 168 169 // NOTE: The following is probably not a good idea because it 170 // will force evaluation of the contents of a Library prematurely. 171 // if (((CompositeEntity)object).numEntities() == 0) return true; 172 return false; 173 } 174 175 /** Set the object that this treemodel looks at. 176 * @param root The root NamedObj. BasicGraphFrame.dispose() calls 177 * this method and passes null as the value of root. 178 * @see #getRoot() 179 */ 180 public void setRoot(NamedObj root) { 181 if (_root != null) { 182 _root.removeChangeListener(_rootListener); 183 } 184 185 _root = root; 186 187 if (_root != null) { 188 _root.addChangeListener(_rootListener); 189 } 190 } 191 192 /** Remove the specified listener. 193 * @param listener The listener to remove. 194 * @see #addTreeModelListener(TreeModelListener) 195 */ 196 @Override 197 public void removeTreeModelListener(TreeModelListener listener) { 198 int i = 0; 199 int size = _listenerList.size(); 200 while (i < size) { 201 Object _listener = ((WeakReference) _listenerList.get(i)).get(); 202 if (_listener == null) { 203 _listenerList.remove(i); 204 size--; 205 } else { 206 if (_listener == listener) { 207 _listenerList.remove(i); 208 size--; 209 } 210 i++; 211 } 212 } 213 } 214 215 /** Notify listeners that the object at the given path has changed. 216 * @param path The path of the node that has changed. 217 * @param newValue The new value of the node. 218 */ 219 @Override 220 public void valueForPathChanged(TreePath path, Object newValue) { 221 Iterator listeners = _listenerList.iterator(); 222 TreeModelEvent event = new TreeModelEvent(this, path); 223 224 while (listeners.hasNext()) { 225 Object listener = ((WeakReference) listeners.next()).get(); 226 if (listener != null) { 227 ((TreeModelListener) listener).treeStructureChanged(event); 228 } 229 } 230 } 231 232 /////////////////////////////////////////////////////////////////// 233 //// inner classes //// 234 235 /** A ChangeListener that updates the Tree. */ 236 public class TreeUpdateListener implements ChangeListener { 237 /** Trigger an update of the tree. If the change 238 * request indicates that it is localized, then only 239 * the relevant portion of the tree is updated. 240 * Otherwise, the entire tree is modified. 241 */ 242 @Override 243 public void changeExecuted(final ChangeRequest change) { 244 // If the change is not structural, say for example SetVariable setting its variable, 245 // then ignore the change because it will not modify the tree. 246 if (!change.isStructuralChange()) { 247 return; 248 } 249 //System.out.println("change = " + change + change.getDescription()); 250 // Note that this should be in the swing thread. 251 SwingUtilities.invokeLater(new Runnable() { 252 @Override 253 public void run() { 254 ArrayList path = new ArrayList(); 255 Object root = getRoot(); 256 257 // If the change request is local, then it 258 // should return non-null to this method. 259 NamedObj locality = change.getLocality(); 260 261 if (locality == null) { 262 if (root != null) { 263 path.add(0, root); 264 } else { 265 // BasicGraphFrame.dispose() calls setRoot(null), 266 // so root might be null. 267 268 // Exporting gt models to html in a 269 // headless environment with Xvfb results 270 // in root being null. However, this is 271 // not always reproducible. 272 273 // To replicate on sisyphus (RHEL 6) as 274 // the hudson user: 275 276 // Xvfb :2 -screen 0 1024x768x24 & 277 // export DISPLAY=localhost:2.0 278 // ant test.single -Dtest.name=ptolemy.vergil.basic.export.test.junit.ExportModelJUnitTest -Djunit.formatter=plain 279 // The error is 280 // java.lang.IllegalArgumentException: Last path component must be non-null 281 // at javax.swing.tree.TreePath.<init>(TreePath.java:105) 282 283 // Just return, our work here is done. 284 return; 285 } 286 } else { 287 // The change has a declared locality. 288 // Construct a path to that locality. 289 NamedObj container = locality; 290 291 while (container != root) { 292 if (container == null) { 293 // This should not occur, but if it 294 // does, we revert to just using the 295 // root. 296 path = new ArrayList(); 297 path.add(0, root); 298 break; 299 } 300 301 path.add(0, container); 302 container = container.getContainer(); 303 } 304 } 305 306 try { 307 valueForPathChanged(new TreePath(path.toArray()), 308 locality); 309 } catch (IllegalArgumentException ex) { 310 throw new RuntimeException( 311 "Failed to instantiate a TreePath, path was " 312 + Arrays.toString(path.toArray()) 313 + " locality was " + locality 314 + " root was: " + root 315 + " changeRequest was: " + change 316 + " changeRequest description: " 317 + change.getDescription() 318 + " changeRequest source: " 319 + change.getSource() 320 + " changeRequest class: " 321 + change.getClass().getName(), 322 ex); 323 } 324 } 325 }); 326 } 327 328 /** Trigger an update of the tree. If the change 329 * request indicates that it is localized, then only 330 * the relevant portion of the tree is updated. 331 * Otherwise, the entire tree is modified. 332 */ 333 @Override 334 public void changeFailed(ChangeRequest change, Exception exception) { 335 // We do the same thing whether the change succeeded or failed. 336 changeExecuted(change); 337 } 338 } 339 340 /////////////////////////////////////////////////////////////////// 341 //// protected variables //// 342 343 /** The root of the tree. */ 344 protected NamedObj _root = null; 345 346 /////////////////////////////////////////////////////////////////// 347 //// private variables //// 348 349 /** The list of listeners. */ 350 private List _listenerList = new LinkedList<WeakReference>(); 351 352 /** The model listener. */ 353 private ChangeListener _rootListener = new TreeUpdateListener(); 354}