blob: 37f805ae0291078bb34a3553f69ca8b94b7ff1ec [file] [log] [blame]
J. Duke319a3b92007-12-01 00:00:00 +00001/*
2 * Copyright 1998-2007 Sun Microsystems, Inc. All Rights Reserved.
3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4 *
5 * This code is free software; you can redistribute it and/or modify it
6 * under the terms of the GNU General Public License version 2 only, as
7 * published by the Free Software Foundation. Sun designates this
8 * particular file as subject to the "Classpath" exception as provided
9 * by Sun in the LICENSE file that accompanied this code.
10 *
11 * This code is distributed in the hope that it will be useful, but WITHOUT
12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
14 * version 2 for more details (a copy is included in the LICENSE file that
15 * accompanied this code).
16 *
17 * You should have received a copy of the GNU General Public License version
18 * 2 along with this work; if not, write to the Free Software Foundation,
19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20 *
21 * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa Clara,
22 * CA 95054 USA or visit www.sun.com if you need additional information or
23 * have any questions.
24 */
25
26package javax.swing.tree;
27
28import javax.swing.event.TreeModelEvent;
29import java.awt.Dimension;
30import java.awt.Rectangle;
31import java.util.Enumeration;
32import java.util.Hashtable;
33import java.util.NoSuchElementException;
34import java.util.Stack;
35
36/**
37 * NOTE: This will become more open in a future release.
38 * <p>
39 * <strong>Warning:</strong>
40 * Serialized objects of this class will not be compatible with
41 * future Swing releases. The current serialization support is
42 * appropriate for short term storage or RMI between applications running
43 * the same version of Swing. As of 1.4, support for long term storage
44 * of all JavaBeans<sup><font size="-2">TM</font></sup>
45 * has been added to the <code>java.beans</code> package.
46 * Please see {@link java.beans.XMLEncoder}.
47 *
48 * @author Scott Violet
49 */
50
51public class FixedHeightLayoutCache extends AbstractLayoutCache {
52 /** Root node. */
53 private FHTreeStateNode root;
54
55 /** Number of rows currently visible. */
56 private int rowCount;
57
58 /**
59 * Used in getting sizes for nodes to avoid creating a new Rectangle
60 * every time a size is needed.
61 */
62 private Rectangle boundsBuffer;
63
64 /**
65 * Maps from TreePath to a FHTreeStateNode.
66 */
67 private Hashtable treePathMapping;
68
69 /**
70 * Used for getting path/row information.
71 */
72 private SearchInfo info;
73
74 private Stack tempStacks;
75
76
77 public FixedHeightLayoutCache() {
78 super();
79 tempStacks = new Stack();
80 boundsBuffer = new Rectangle();
81 treePathMapping = new Hashtable();
82 info = new SearchInfo();
83 setRowHeight(1);
84 }
85
86 /**
87 * Sets the TreeModel that will provide the data.
88 *
89 * @param newModel the TreeModel that is to provide the data
90 */
91 public void setModel(TreeModel newModel) {
92 super.setModel(newModel);
93 rebuild(false);
94 }
95
96 /**
97 * Determines whether or not the root node from
98 * the TreeModel is visible.
99 *
100 * @param rootVisible true if the root node of the tree is to be displayed
101 * @see #rootVisible
102 */
103 public void setRootVisible(boolean rootVisible) {
104 if(isRootVisible() != rootVisible) {
105 super.setRootVisible(rootVisible);
106 if(root != null) {
107 if(rootVisible) {
108 rowCount++;
109 root.adjustRowBy(1);
110 }
111 else {
112 rowCount--;
113 root.adjustRowBy(-1);
114 }
115 visibleNodesChanged();
116 }
117 }
118 }
119
120 /**
121 * Sets the height of each cell. If rowHeight is less than or equal to
122 * 0 this will throw an IllegalArgumentException.
123 *
124 * @param rowHeight the height of each cell, in pixels
125 */
126 public void setRowHeight(int rowHeight) {
127 if(rowHeight <= 0)
128 throw new IllegalArgumentException("FixedHeightLayoutCache only supports row heights greater than 0");
129 if(getRowHeight() != rowHeight) {
130 super.setRowHeight(rowHeight);
131 visibleNodesChanged();
132 }
133 }
134
135 /**
136 * Returns the number of visible rows.
137 */
138 public int getRowCount() {
139 return rowCount;
140 }
141
142 /**
143 * Does nothing, FixedHeightLayoutCache doesn't cache width, and that
144 * is all that could change.
145 */
146 public void invalidatePathBounds(TreePath path) {
147 }
148
149
150 /**
151 * Informs the TreeState that it needs to recalculate all the sizes
152 * it is referencing.
153 */
154 public void invalidateSizes() {
155 // Nothing to do here, rowHeight still same, which is all
156 // this is interested in, visible region may have changed though.
157 visibleNodesChanged();
158 }
159
160 /**
161 * Returns true if the value identified by row is currently expanded.
162 */
163 public boolean isExpanded(TreePath path) {
164 if(path != null) {
165 FHTreeStateNode lastNode = getNodeForPath(path, true, false);
166
167 return (lastNode != null && lastNode.isExpanded());
168 }
169 return false;
170 }
171
172 /**
173 * Returns a rectangle giving the bounds needed to draw path.
174 *
175 * @param path a TreePath specifying a node
176 * @param placeIn a Rectangle object giving the available space
177 * @return a Rectangle object specifying the space to be used
178 */
179 public Rectangle getBounds(TreePath path, Rectangle placeIn) {
180 if(path == null)
181 return null;
182
183 FHTreeStateNode node = getNodeForPath(path, true, false);
184
185 if(node != null)
186 return getBounds(node, -1, placeIn);
187
188 // node hasn't been created yet.
189 TreePath parentPath = path.getParentPath();
190
191 node = getNodeForPath(parentPath, true, false);
192 if (node != null && node.isExpanded()) {
193 int childIndex = treeModel.getIndexOfChild
194 (parentPath.getLastPathComponent(),
195 path.getLastPathComponent());
196
197 if(childIndex != -1)
198 return getBounds(node, childIndex, placeIn);
199 }
200 return null;
201 }
202
203 /**
204 * Returns the path for passed in row. If row is not visible
205 * null is returned.
206 */
207 public TreePath getPathForRow(int row) {
208 if(row >= 0 && row < getRowCount()) {
209 if(root.getPathForRow(row, getRowCount(), info)) {
210 return info.getPath();
211 }
212 }
213 return null;
214 }
215
216 /**
217 * Returns the row that the last item identified in path is visible
218 * at. Will return -1 if any of the elements in path are not
219 * currently visible.
220 */
221 public int getRowForPath(TreePath path) {
222 if(path == null || root == null)
223 return -1;
224
225 FHTreeStateNode node = getNodeForPath(path, true, false);
226
227 if(node != null)
228 return node.getRow();
229
230 TreePath parentPath = path.getParentPath();
231
232 node = getNodeForPath(parentPath, true, false);
233 if(node != null && node.isExpanded()) {
234 return node.getRowToModelIndex(treeModel.getIndexOfChild
235 (parentPath.getLastPathComponent(),
236 path.getLastPathComponent()));
237 }
238 return -1;
239 }
240
241 /**
242 * Returns the path to the node that is closest to x,y. If
243 * there is nothing currently visible this will return null, otherwise
244 * it'll always return a valid path. If you need to test if the
245 * returned object is exactly at x, y you should get the bounds for
246 * the returned path and test x, y against that.
247 */
248 public TreePath getPathClosestTo(int x, int y) {
249 if(getRowCount() == 0)
250 return null;
251
252 int row = getRowContainingYLocation(y);
253
254 return getPathForRow(row);
255 }
256
257 /**
258 * Returns the number of visible children for row.
259 */
260 public int getVisibleChildCount(TreePath path) {
261 FHTreeStateNode node = getNodeForPath(path, true, false);
262
263 if(node == null)
264 return 0;
265 return node.getTotalChildCount();
266 }
267
268 /**
269 * Returns an Enumerator that increments over the visible paths
270 * starting at the passed in location. The ordering of the enumeration
271 * is based on how the paths are displayed.
272 */
273 public Enumeration<TreePath> getVisiblePathsFrom(TreePath path) {
274 if(path == null)
275 return null;
276
277 FHTreeStateNode node = getNodeForPath(path, true, false);
278
279 if(node != null) {
280 return new VisibleFHTreeStateNodeEnumeration(node);
281 }
282 TreePath parentPath = path.getParentPath();
283
284 node = getNodeForPath(parentPath, true, false);
285 if(node != null && node.isExpanded()) {
286 return new VisibleFHTreeStateNodeEnumeration(node,
287 treeModel.getIndexOfChild(parentPath.getLastPathComponent(),
288 path.getLastPathComponent()));
289 }
290 return null;
291 }
292
293 /**
294 * Marks the path <code>path</code> expanded state to
295 * <code>isExpanded</code>.
296 */
297 public void setExpandedState(TreePath path, boolean isExpanded) {
298 if(isExpanded)
299 ensurePathIsExpanded(path, true);
300 else if(path != null) {
301 TreePath parentPath = path.getParentPath();
302
303 // YECK! Make the parent expanded.
304 if(parentPath != null) {
305 FHTreeStateNode parentNode = getNodeForPath(parentPath,
306 false, true);
307 if(parentNode != null)
308 parentNode.makeVisible();
309 }
310 // And collapse the child.
311 FHTreeStateNode childNode = getNodeForPath(path, true,
312 false);
313
314 if(childNode != null)
315 childNode.collapse(true);
316 }
317 }
318
319 /**
320 * Returns true if the path is expanded, and visible.
321 */
322 public boolean getExpandedState(TreePath path) {
323 FHTreeStateNode node = getNodeForPath(path, true, false);
324
325 return (node != null) ? (node.isVisible() && node.isExpanded()) :
326 false;
327 }
328
329 //
330 // TreeModelListener methods
331 //
332
333 /**
334 * <p>Invoked after a node (or a set of siblings) has changed in some
335 * way. The node(s) have not changed locations in the tree or
336 * altered their children arrays, but other attributes have
337 * changed and may affect presentation. Example: the name of a
338 * file has changed, but it is in the same location in the file
339 * system.</p>
340 *
341 * <p>e.path() returns the path the parent of the changed node(s).</p>
342 *
343 * <p>e.childIndices() returns the index(es) of the changed node(s).</p>
344 */
345 public void treeNodesChanged(TreeModelEvent e) {
346 if(e != null) {
347 int changedIndexs[];
348 FHTreeStateNode changedParent = getNodeForPath
349 (e.getTreePath(), false, false);
350 int maxCounter;
351
352 changedIndexs = e.getChildIndices();
353 /* Only need to update the children if the node has been
354 expanded once. */
355 // PENDING(scott): make sure childIndexs is sorted!
356 if (changedParent != null) {
357 if (changedIndexs != null &&
358 (maxCounter = changedIndexs.length) > 0) {
359 Object parentValue = changedParent.getUserObject();
360
361 for(int counter = 0; counter < maxCounter; counter++) {
362 FHTreeStateNode child = changedParent.
363 getChildAtModelIndex(changedIndexs[counter]);
364
365 if(child != null) {
366 child.setUserObject(treeModel.getChild(parentValue,
367 changedIndexs[counter]));
368 }
369 }
370 if(changedParent.isVisible() && changedParent.isExpanded())
371 visibleNodesChanged();
372 }
373 // Null for root indicates it changed.
374 else if (changedParent == root && changedParent.isVisible() &&
375 changedParent.isExpanded()) {
376 visibleNodesChanged();
377 }
378 }
379 }
380 }
381
382 /**
383 * <p>Invoked after nodes have been inserted into the tree.</p>
384 *
385 * <p>e.path() returns the parent of the new nodes
386 * <p>e.childIndices() returns the indices of the new nodes in
387 * ascending order.
388 */
389 public void treeNodesInserted(TreeModelEvent e) {
390 if(e != null) {
391 int changedIndexs[];
392 FHTreeStateNode changedParent = getNodeForPath
393 (e.getTreePath(), false, false);
394 int maxCounter;
395
396 changedIndexs = e.getChildIndices();
397 /* Only need to update the children if the node has been
398 expanded once. */
399 // PENDING(scott): make sure childIndexs is sorted!
400 if(changedParent != null && changedIndexs != null &&
401 (maxCounter = changedIndexs.length) > 0) {
402 boolean isVisible =
403 (changedParent.isVisible() &&
404 changedParent.isExpanded());
405
406 for(int counter = 0; counter < maxCounter; counter++) {
407 changedParent.childInsertedAtModelIndex
408 (changedIndexs[counter], isVisible);
409 }
410 if(isVisible && treeSelectionModel != null)
411 treeSelectionModel.resetRowSelection();
412 if(changedParent.isVisible())
413 this.visibleNodesChanged();
414 }
415 }
416 }
417
418 /**
419 * <p>Invoked after nodes have been removed from the tree. Note that
420 * if a subtree is removed from the tree, this method may only be
421 * invoked once for the root of the removed subtree, not once for
422 * each individual set of siblings removed.</p>
423 *
424 * <p>e.path() returns the former parent of the deleted nodes.</p>
425 *
426 * <p>e.childIndices() returns the indices the nodes had before they were deleted in ascending order.</p>
427 */
428 public void treeNodesRemoved(TreeModelEvent e) {
429 if(e != null) {
430 int changedIndexs[];
431 int maxCounter;
432 TreePath parentPath = e.getTreePath();
433 FHTreeStateNode changedParentNode = getNodeForPath
434 (parentPath, false, false);
435
436 changedIndexs = e.getChildIndices();
437 // PENDING(scott): make sure that changedIndexs are sorted in
438 // ascending order.
439 if(changedParentNode != null && changedIndexs != null &&
440 (maxCounter = changedIndexs.length) > 0) {
441 Object[] children = e.getChildren();
442 boolean isVisible =
443 (changedParentNode.isVisible() &&
444 changedParentNode.isExpanded());
445
446 for(int counter = maxCounter - 1; counter >= 0; counter--) {
447 changedParentNode.removeChildAtModelIndex
448 (changedIndexs[counter], isVisible);
449 }
450 if(isVisible) {
451 if(treeSelectionModel != null)
452 treeSelectionModel.resetRowSelection();
453 if (treeModel.getChildCount(changedParentNode.
454 getUserObject()) == 0 &&
455 changedParentNode.isLeaf()) {
456 // Node has become a leaf, collapse it.
457 changedParentNode.collapse(false);
458 }
459 visibleNodesChanged();
460 }
461 else if(changedParentNode.isVisible())
462 visibleNodesChanged();
463 }
464 }
465 }
466
467 /**
468 * <p>Invoked after the tree has drastically changed structure from a
469 * given node down. If the path returned by e.getPath() is of length
470 * one and the first element does not identify the current root node
471 * the first element should become the new root of the tree.<p>
472 *
473 * <p>e.path() holds the path to the node.</p>
474 * <p>e.childIndices() returns null.</p>
475 */
476 public void treeStructureChanged(TreeModelEvent e) {
477 if(e != null) {
478 TreePath changedPath = e.getTreePath();
479 FHTreeStateNode changedNode = getNodeForPath
480 (changedPath, false, false);
481
482 // Check if root has changed, either to a null root, or
483 // to an entirely new root.
484 if (changedNode == root ||
485 (changedNode == null &&
486 ((changedPath == null && treeModel != null &&
487 treeModel.getRoot() == null) ||
488 (changedPath != null && changedPath.getPathCount() <= 1)))) {
489 rebuild(true);
490 }
491 else if(changedNode != null) {
492 boolean wasExpanded, wasVisible;
493 FHTreeStateNode parent = (FHTreeStateNode)
494 changedNode.getParent();
495
496 wasExpanded = changedNode.isExpanded();
497 wasVisible = changedNode.isVisible();
498
499 int index = parent.getIndex(changedNode);
500 changedNode.collapse(false);
501 parent.remove(index);
502
503 if(wasVisible && wasExpanded) {
504 int row = changedNode.getRow();
505 parent.resetChildrenRowsFrom(row, index,
506 changedNode.getChildIndex());
507 changedNode = getNodeForPath(changedPath, false, true);
508 changedNode.expand();
509 }
510 if(treeSelectionModel != null && wasVisible && wasExpanded)
511 treeSelectionModel.resetRowSelection();
512 if(wasVisible)
513 this.visibleNodesChanged();
514 }
515 }
516 }
517
518
519 //
520 // Local methods
521 //
522
523 private void visibleNodesChanged() {
524 }
525
526 /**
527 * Returns the bounds for the given node. If <code>childIndex</code>
528 * is -1, the bounds of <code>parent</code> are returned, otherwise
529 * the bounds of the node at <code>childIndex</code> are returned.
530 */
531 private Rectangle getBounds(FHTreeStateNode parent, int childIndex,
532 Rectangle placeIn) {
533 boolean expanded;
534 int level;
535 int row;
536 Object value;
537
538 if(childIndex == -1) {
539 // Getting bounds for parent
540 row = parent.getRow();
541 value = parent.getUserObject();
542 expanded = parent.isExpanded();
543 level = parent.getLevel();
544 }
545 else {
546 row = parent.getRowToModelIndex(childIndex);
547 value = treeModel.getChild(parent.getUserObject(), childIndex);
548 expanded = false;
549 level = parent.getLevel() + 1;
550 }
551
552 Rectangle bounds = getNodeDimensions(value, row, level,
553 expanded, boundsBuffer);
554 // No node dimensions, bail.
555 if(bounds == null)
556 return null;
557
558 if(placeIn == null)
559 placeIn = new Rectangle();
560
561 placeIn.x = bounds.x;
562 placeIn.height = getRowHeight();
563 placeIn.y = row * placeIn.height;
564 placeIn.width = bounds.width;
565 return placeIn;
566 }
567
568 /**
569 * Adjust the large row count of the AbstractTreeUI the receiver was
570 * created with.
571 */
572 private void adjustRowCountBy(int changeAmount) {
573 rowCount += changeAmount;
574 }
575
576 /**
577 * Adds a mapping for node.
578 */
579 private void addMapping(FHTreeStateNode node) {
580 treePathMapping.put(node.getTreePath(), node);
581 }
582
583 /**
584 * Removes the mapping for a previously added node.
585 */
586 private void removeMapping(FHTreeStateNode node) {
587 treePathMapping.remove(node.getTreePath());
588 }
589
590 /**
591 * Returns the node previously added for <code>path</code>. This may
592 * return null, if you to create a node use getNodeForPath.
593 */
594 private FHTreeStateNode getMapping(TreePath path) {
595 return (FHTreeStateNode)treePathMapping.get(path);
596 }
597
598 /**
599 * Sent to completely rebuild the visible tree. All nodes are collapsed.
600 */
601 private void rebuild(boolean clearSelection) {
602 Object rootUO;
603
604 treePathMapping.clear();
605 if(treeModel != null && (rootUO = treeModel.getRoot()) != null) {
606 root = createNodeForValue(rootUO, 0);
607 root.path = new TreePath(rootUO);
608 addMapping(root);
609 if(isRootVisible()) {
610 rowCount = 1;
611 root.row = 0;
612 }
613 else {
614 rowCount = 0;
615 root.row = -1;
616 }
617 root.expand();
618 }
619 else {
620 root = null;
621 rowCount = 0;
622 }
623 if(clearSelection && treeSelectionModel != null) {
624 treeSelectionModel.clearSelection();
625 }
626 this.visibleNodesChanged();
627 }
628
629 /**
630 * Returns the index of the row containing location. If there
631 * are no rows, -1 is returned. If location is beyond the last
632 * row index, the last row index is returned.
633 */
634 private int getRowContainingYLocation(int location) {
635 if(getRowCount() == 0)
636 return -1;
637 return Math.max(0, Math.min(getRowCount() - 1,
638 location / getRowHeight()));
639 }
640
641 /**
642 * Ensures that all the path components in path are expanded, accept
643 * for the last component which will only be expanded if expandLast
644 * is true.
645 * Returns true if succesful in finding the path.
646 */
647 private boolean ensurePathIsExpanded(TreePath aPath,
648 boolean expandLast) {
649 if(aPath != null) {
650 // Make sure the last entry isn't a leaf.
651 if(treeModel.isLeaf(aPath.getLastPathComponent())) {
652 aPath = aPath.getParentPath();
653 expandLast = true;
654 }
655 if(aPath != null) {
656 FHTreeStateNode lastNode = getNodeForPath(aPath, false,
657 true);
658
659 if(lastNode != null) {
660 lastNode.makeVisible();
661 if(expandLast)
662 lastNode.expand();
663 return true;
664 }
665 }
666 }
667 return false;
668 }
669
670 /**
671 * Creates and returns an instance of FHTreeStateNode.
672 */
673 private FHTreeStateNode createNodeForValue(Object value,int childIndex) {
674 return new FHTreeStateNode(value, childIndex, -1);
675 }
676
677 /**
678 * Messages getTreeNodeForPage(path, onlyIfVisible, shouldCreate,
679 * path.length) as long as path is non-null and the length is > 0.
680 * Otherwise returns null.
681 */
682 private FHTreeStateNode getNodeForPath(TreePath path,
683 boolean onlyIfVisible,
684 boolean shouldCreate) {
685 if(path != null) {
686 FHTreeStateNode node;
687
688 node = getMapping(path);
689 if(node != null) {
690 if(onlyIfVisible && !node.isVisible())
691 return null;
692 return node;
693 }
694 if(onlyIfVisible)
695 return null;
696
697 // Check all the parent paths, until a match is found.
698 Stack paths;
699
700 if(tempStacks.size() == 0) {
701 paths = new Stack();
702 }
703 else {
704 paths = (Stack)tempStacks.pop();
705 }
706
707 try {
708 paths.push(path);
709 path = path.getParentPath();
710 node = null;
711 while(path != null) {
712 node = getMapping(path);
713 if(node != null) {
714 // Found a match, create entries for all paths in
715 // paths.
716 while(node != null && paths.size() > 0) {
717 path = (TreePath)paths.pop();
718 node = node.createChildFor(path.
719 getLastPathComponent());
720 }
721 return node;
722 }
723 paths.push(path);
724 path = path.getParentPath();
725 }
726 }
727 finally {
728 paths.removeAllElements();
729 tempStacks.push(paths);
730 }
731 // If we get here it means they share a different root!
732 return null;
733 }
734 return null;
735 }
736
737 /**
738 * FHTreeStateNode is used to track what has been expanded.
739 * FHTreeStateNode differs from VariableHeightTreeState.TreeStateNode
740 * in that it is highly model intensive. That is almost all queries to a
741 * FHTreeStateNode result in the TreeModel being queried. And it
742 * obviously does not support variable sized row heights.
743 */
744 private class FHTreeStateNode extends DefaultMutableTreeNode {
745 /** Is this node expanded? */
746 protected boolean isExpanded;
747
748 /** Index of this node from the model. */
749 protected int childIndex;
750
751 /** Child count of the receiver. */
752 protected int childCount;
753
754 /** Row of the receiver. This is only valid if the row is expanded.
755 */
756 protected int row;
757
758 /** Path of this node. */
759 protected TreePath path;
760
761
762 public FHTreeStateNode(Object userObject, int childIndex, int row) {
763 super(userObject);
764 this.childIndex = childIndex;
765 this.row = row;
766 }
767
768 //
769 // Overriden DefaultMutableTreeNode methods
770 //
771
772 /**
773 * Messaged when this node is added somewhere, resets the path
774 * and adds a mapping from path to this node.
775 */
776 public void setParent(MutableTreeNode parent) {
777 super.setParent(parent);
778 if(parent != null) {
779 path = ((FHTreeStateNode)parent).getTreePath().
780 pathByAddingChild(getUserObject());
781 addMapping(this);
782 }
783 }
784
785 /**
786 * Messaged when this node is removed from its parent, this messages
787 * <code>removedFromMapping</code> to remove all the children.
788 */
789 public void remove(int childIndex) {
790 FHTreeStateNode node = (FHTreeStateNode)getChildAt(childIndex);
791
792 node.removeFromMapping();
793 super.remove(childIndex);
794 }
795
796 /**
797 * Messaged to set the user object. This resets the path.
798 */
799 public void setUserObject(Object o) {
800 super.setUserObject(o);
801 if(path != null) {
802 FHTreeStateNode parent = (FHTreeStateNode)getParent();
803
804 if(parent != null)
805 resetChildrenPaths(parent.getTreePath());
806 else
807 resetChildrenPaths(null);
808 }
809 }
810
811 //
812 //
813
814 /**
815 * Returns the index of the receiver in the model.
816 */
817 public int getChildIndex() {
818 return childIndex;
819 }
820
821 /**
822 * Returns the <code>TreePath</code> of the receiver.
823 */
824 public TreePath getTreePath() {
825 return path;
826 }
827
828 /**
829 * Returns the child for the passed in model index, this will
830 * return <code>null</code> if the child for <code>index</code>
831 * has not yet been created (expanded).
832 */
833 public FHTreeStateNode getChildAtModelIndex(int index) {
834 // PENDING: Make this a binary search!
835 for(int counter = getChildCount() - 1; counter >= 0; counter--)
836 if(((FHTreeStateNode)getChildAt(counter)).childIndex == index)
837 return (FHTreeStateNode)getChildAt(counter);
838 return null;
839 }
840
841 /**
842 * Returns true if this node is visible. This is determined by
843 * asking all the parents if they are expanded.
844 */
845 public boolean isVisible() {
846 FHTreeStateNode parent = (FHTreeStateNode)getParent();
847
848 if(parent == null)
849 return true;
850 return (parent.isExpanded() && parent.isVisible());
851 }
852
853 /**
854 * Returns the row of the receiver.
855 */
856 public int getRow() {
857 return row;
858 }
859
860 /**
861 * Returns the row of the child with a model index of
862 * <code>index</code>.
863 */
864 public int getRowToModelIndex(int index) {
865 FHTreeStateNode child;
866 int lastRow = getRow() + 1;
867 int retValue = lastRow;
868
869 // This too could be a binary search!
870 for(int counter = 0, maxCounter = getChildCount();
871 counter < maxCounter; counter++) {
872 child = (FHTreeStateNode)getChildAt(counter);
873 if(child.childIndex >= index) {
874 if(child.childIndex == index)
875 return child.row;
876 if(counter == 0)
877 return getRow() + 1 + index;
878 return child.row - (child.childIndex - index);
879 }
880 }
881 // YECK!
882 return getRow() + 1 + getTotalChildCount() -
883 (childCount - index);
884 }
885
886 /**
887 * Returns the number of children in the receiver by descending all
888 * expanded nodes and messaging them with getTotalChildCount.
889 */
890 public int getTotalChildCount() {
891 if(isExpanded()) {
892 FHTreeStateNode parent = (FHTreeStateNode)getParent();
893 int pIndex;
894
895 if(parent != null && (pIndex = parent.getIndex(this)) + 1 <
896 parent.getChildCount()) {
897 // This node has a created sibling, to calc total
898 // child count directly from that!
899 FHTreeStateNode nextSibling = (FHTreeStateNode)parent.
900 getChildAt(pIndex + 1);
901
902 return nextSibling.row - row -
903 (nextSibling.childIndex - childIndex);
904 }
905 else {
906 int retCount = childCount;
907
908 for(int counter = getChildCount() - 1; counter >= 0;
909 counter--) {
910 retCount += ((FHTreeStateNode)getChildAt(counter))
911 .getTotalChildCount();
912 }
913 return retCount;
914 }
915 }
916 return 0;
917 }
918
919 /**
920 * Returns true if this node is expanded.
921 */
922 public boolean isExpanded() {
923 return isExpanded;
924 }
925
926 /**
927 * The highest visible nodes have a depth of 0.
928 */
929 public int getVisibleLevel() {
930 if (isRootVisible()) {
931 return getLevel();
932 } else {
933 return getLevel() - 1;
934 }
935 }
936
937 /**
938 * Recreates the receivers path, and all its childrens paths.
939 */
940 protected void resetChildrenPaths(TreePath parentPath) {
941 removeMapping(this);
942 if(parentPath == null)
943 path = new TreePath(getUserObject());
944 else
945 path = parentPath.pathByAddingChild(getUserObject());
946 addMapping(this);
947 for(int counter = getChildCount() - 1; counter >= 0; counter--)
948 ((FHTreeStateNode)getChildAt(counter)).
949 resetChildrenPaths(path);
950 }
951
952 /**
953 * Removes the receiver, and all its children, from the mapping
954 * table.
955 */
956 protected void removeFromMapping() {
957 if(path != null) {
958 removeMapping(this);
959 for(int counter = getChildCount() - 1; counter >= 0; counter--)
960 ((FHTreeStateNode)getChildAt(counter)).removeFromMapping();
961 }
962 }
963
964 /**
965 * Creates a new node to represent <code>userObject</code>.
966 * This does NOT check to ensure there isn't already a child node
967 * to manage <code>userObject</code>.
968 */
969 protected FHTreeStateNode createChildFor(Object userObject) {
970 int newChildIndex = treeModel.getIndexOfChild
971 (getUserObject(), userObject);
972
973 if(newChildIndex < 0)
974 return null;
975
976 FHTreeStateNode aNode;
977 FHTreeStateNode child = createNodeForValue(userObject,
978 newChildIndex);
979 int childRow;
980
981 if(isVisible()) {
982 childRow = getRowToModelIndex(newChildIndex);
983 }
984 else {
985 childRow = -1;
986 }
987 child.row = childRow;
988 for(int counter = 0, maxCounter = getChildCount();
989 counter < maxCounter; counter++) {
990 aNode = (FHTreeStateNode)getChildAt(counter);
991 if(aNode.childIndex > newChildIndex) {
992 insert(child, counter);
993 return child;
994 }
995 }
996 add(child);
997 return child;
998 }
999
1000 /**
1001 * Adjusts the receiver, and all its children rows by
1002 * <code>amount</code>.
1003 */
1004 protected void adjustRowBy(int amount) {
1005 row += amount;
1006 if(isExpanded) {
1007 for(int counter = getChildCount() - 1; counter >= 0;
1008 counter--)
1009 ((FHTreeStateNode)getChildAt(counter)).adjustRowBy(amount);
1010 }
1011 }
1012
1013 /**
1014 * Adjusts this node, its child, and its parent starting at
1015 * an index of <code>index</code> index is the index of the child
1016 * to start adjusting from, which is not necessarily the model
1017 * index.
1018 */
1019 protected void adjustRowBy(int amount, int startIndex) {
1020 // Could check isVisible, but probably isn't worth it.
1021 if(isExpanded) {
1022 // children following startIndex.
1023 for(int counter = getChildCount() - 1; counter >= startIndex;
1024 counter--)
1025 ((FHTreeStateNode)getChildAt(counter)).adjustRowBy(amount);
1026 }
1027 // Parent
1028 FHTreeStateNode parent = (FHTreeStateNode)getParent();
1029
1030 if(parent != null) {
1031 parent.adjustRowBy(amount, parent.getIndex(this) + 1);
1032 }
1033 }
1034
1035 /**
1036 * Messaged when the node has expanded. This updates all of
1037 * the receivers children rows, as well as the total row count.
1038 */
1039 protected void didExpand() {
1040 int nextRow = setRowAndChildren(row);
1041 FHTreeStateNode parent = (FHTreeStateNode)getParent();
1042 int childRowCount = nextRow - row - 1;
1043
1044 if(parent != null) {
1045 parent.adjustRowBy(childRowCount, parent.getIndex(this) + 1);
1046 }
1047 adjustRowCountBy(childRowCount);
1048 }
1049
1050 /**
1051 * Sets the receivers row to <code>nextRow</code> and recursively
1052 * updates all the children of the receivers rows. The index the
1053 * next row is to be placed as is returned.
1054 */
1055 protected int setRowAndChildren(int nextRow) {
1056 row = nextRow;
1057
1058 if(!isExpanded())
1059 return row + 1;
1060
1061 int lastRow = row + 1;
1062 int lastModelIndex = 0;
1063 FHTreeStateNode child;
1064 int maxCounter = getChildCount();
1065
1066 for(int counter = 0; counter < maxCounter; counter++) {
1067 child = (FHTreeStateNode)getChildAt(counter);
1068 lastRow += (child.childIndex - lastModelIndex);
1069 lastModelIndex = child.childIndex + 1;
1070 if(child.isExpanded) {
1071 lastRow = child.setRowAndChildren(lastRow);
1072 }
1073 else {
1074 child.row = lastRow++;
1075 }
1076 }
1077 return lastRow + childCount - lastModelIndex;
1078 }
1079
1080 /**
1081 * Resets the receivers childrens rows. Starting with the child
1082 * at <code>childIndex</code> (and <code>modelIndex</code>) to
1083 * <code>newRow</code>. This uses <code>setRowAndChildren</code>
1084 * to recursively descend children, and uses
1085 * <code>resetRowSelection</code> to ascend parents.
1086 */
1087 // This can be rather expensive, but is needed for the collapse
1088 // case this is resulting from a remove (although I could fix
1089 // that by having instances of FHTreeStateNode hold a ref to
1090 // the number of children). I prefer this though, making determing
1091 // the row of a particular node fast is very nice!
1092 protected void resetChildrenRowsFrom(int newRow, int childIndex,
1093 int modelIndex) {
1094 int lastRow = newRow;
1095 int lastModelIndex = modelIndex;
1096 FHTreeStateNode node;
1097 int maxCounter = getChildCount();
1098
1099 for(int counter = childIndex; counter < maxCounter; counter++) {
1100 node = (FHTreeStateNode)getChildAt(counter);
1101 lastRow += (node.childIndex - lastModelIndex);
1102 lastModelIndex = node.childIndex + 1;
1103 if(node.isExpanded) {
1104 lastRow = node.setRowAndChildren(lastRow);
1105 }
1106 else {
1107 node.row = lastRow++;
1108 }
1109 }
1110 lastRow += childCount - lastModelIndex;
1111 node = (FHTreeStateNode)getParent();
1112 if(node != null) {
1113 node.resetChildrenRowsFrom(lastRow, node.getIndex(this) + 1,
1114 this.childIndex + 1);
1115 }
1116 else { // This is the root, reset total ROWCOUNT!
1117 rowCount = lastRow;
1118 }
1119 }
1120
1121 /**
1122 * Makes the receiver visible, but invoking
1123 * <code>expandParentAndReceiver</code> on the superclass.
1124 */
1125 protected void makeVisible() {
1126 FHTreeStateNode parent = (FHTreeStateNode)getParent();
1127
1128 if(parent != null)
1129 parent.expandParentAndReceiver();
1130 }
1131
1132 /**
1133 * Invokes <code>expandParentAndReceiver</code> on the parent,
1134 * and expands the receiver.
1135 */
1136 protected void expandParentAndReceiver() {
1137 FHTreeStateNode parent = (FHTreeStateNode)getParent();
1138
1139 if(parent != null)
1140 parent.expandParentAndReceiver();
1141 expand();
1142 }
1143
1144 /**
1145 * Expands the receiver.
1146 */
1147 protected void expand() {
1148 if(!isExpanded && !isLeaf()) {
1149 boolean visible = isVisible();
1150
1151 isExpanded = true;
1152 childCount = treeModel.getChildCount(getUserObject());
1153
1154 if(visible) {
1155 didExpand();
1156 }
1157
1158 // Update the selection model.
1159 if(visible && treeSelectionModel != null) {
1160 treeSelectionModel.resetRowSelection();
1161 }
1162 }
1163 }
1164
1165 /**
1166 * Collapses the receiver. If <code>adjustRows</code> is true,
1167 * the rows of nodes after the receiver are adjusted.
1168 */
1169 protected void collapse(boolean adjustRows) {
1170 if(isExpanded) {
1171 if(isVisible() && adjustRows) {
1172 int childCount = getTotalChildCount();
1173
1174 isExpanded = false;
1175 adjustRowCountBy(-childCount);
1176 // We can do this because adjustRowBy won't descend
1177 // the children.
1178 adjustRowBy(-childCount, 0);
1179 }
1180 else
1181 isExpanded = false;
1182
1183 if(adjustRows && isVisible() && treeSelectionModel != null)
1184 treeSelectionModel.resetRowSelection();
1185 }
1186 }
1187
1188 /**
1189 * Returns true if the receiver is a leaf.
1190 */
1191 public boolean isLeaf() {
1192 TreeModel model = getModel();
1193
1194 return (model != null) ? model.isLeaf(this.getUserObject()) :
1195 true;
1196 }
1197
1198 /**
1199 * Adds newChild to this nodes children at the appropriate location.
1200 * The location is determined from the childIndex of newChild.
1201 */
1202 protected void addNode(FHTreeStateNode newChild) {
1203 boolean added = false;
1204 int childIndex = newChild.getChildIndex();
1205
1206 for(int counter = 0, maxCounter = getChildCount();
1207 counter < maxCounter; counter++) {
1208 if(((FHTreeStateNode)getChildAt(counter)).getChildIndex() >
1209 childIndex) {
1210 added = true;
1211 insert(newChild, counter);
1212 counter = maxCounter;
1213 }
1214 }
1215 if(!added)
1216 add(newChild);
1217 }
1218
1219 /**
1220 * Removes the child at <code>modelIndex</code>.
1221 * <code>isChildVisible</code> should be true if the receiver
1222 * is visible and expanded.
1223 */
1224 protected void removeChildAtModelIndex(int modelIndex,
1225 boolean isChildVisible) {
1226 FHTreeStateNode childNode = getChildAtModelIndex(modelIndex);
1227
1228 if(childNode != null) {
1229 int row = childNode.getRow();
1230 int index = getIndex(childNode);
1231
1232 childNode.collapse(false);
1233 remove(index);
1234 adjustChildIndexs(index, -1);
1235 childCount--;
1236 if(isChildVisible) {
1237 // Adjust the rows.
1238 resetChildrenRowsFrom(row, index, modelIndex);
1239 }
1240 }
1241 else {
1242 int maxCounter = getChildCount();
1243 FHTreeStateNode aChild;
1244
1245 for(int counter = 0; counter < maxCounter; counter++) {
1246 aChild = (FHTreeStateNode)getChildAt(counter);
1247 if(aChild.childIndex >= modelIndex) {
1248 if(isChildVisible) {
1249 adjustRowBy(-1, counter);
1250 adjustRowCountBy(-1);
1251 }
1252 // Since matched and children are always sorted by
1253 // index, no need to continue testing with the
1254 // above.
1255 for(; counter < maxCounter; counter++)
1256 ((FHTreeStateNode)getChildAt(counter)).
1257 childIndex--;
1258 childCount--;
1259 return;
1260 }
1261 }
1262 // No children to adjust, but it was a child, so we still need
1263 // to adjust nodes after this one.
1264 if(isChildVisible) {
1265 adjustRowBy(-1, maxCounter);
1266 adjustRowCountBy(-1);
1267 }
1268 childCount--;
1269 }
1270 }
1271
1272 /**
1273 * Adjusts the child indexs of the receivers children by
1274 * <code>amount</code>, starting at <code>index</code>.
1275 */
1276 protected void adjustChildIndexs(int index, int amount) {
1277 for(int counter = index, maxCounter = getChildCount();
1278 counter < maxCounter; counter++) {
1279 ((FHTreeStateNode)getChildAt(counter)).childIndex += amount;
1280 }
1281 }
1282
1283 /**
1284 * Messaged when a child has been inserted at index. For all the
1285 * children that have a childIndex >= index their index is incremented
1286 * by one.
1287 */
1288 protected void childInsertedAtModelIndex(int index,
1289 boolean isExpandedAndVisible) {
1290 FHTreeStateNode aChild;
1291 int maxCounter = getChildCount();
1292
1293 for(int counter = 0; counter < maxCounter; counter++) {
1294 aChild = (FHTreeStateNode)getChildAt(counter);
1295 if(aChild.childIndex >= index) {
1296 if(isExpandedAndVisible) {
1297 adjustRowBy(1, counter);
1298 adjustRowCountBy(1);
1299 }
1300 /* Since matched and children are always sorted by
1301 index, no need to continue testing with the above. */
1302 for(; counter < maxCounter; counter++)
1303 ((FHTreeStateNode)getChildAt(counter)).childIndex++;
1304 childCount++;
1305 return;
1306 }
1307 }
1308 // No children to adjust, but it was a child, so we still need
1309 // to adjust nodes after this one.
1310 if(isExpandedAndVisible) {
1311 adjustRowBy(1, maxCounter);
1312 adjustRowCountBy(1);
1313 }
1314 childCount++;
1315 }
1316
1317 /**
1318 * Returns true if there is a row for <code>row</code>.
1319 * <code>nextRow</code> gives the bounds of the receiver.
1320 * Information about the found row is returned in <code>info</code>.
1321 * This should be invoked on root with <code>nextRow</code> set
1322 * to <code>getRowCount</code>().
1323 */
1324 protected boolean getPathForRow(int row, int nextRow,
1325 SearchInfo info) {
1326 if(this.row == row) {
1327 info.node = this;
1328 info.isNodeParentNode = false;
1329 info.childIndex = childIndex;
1330 return true;
1331 }
1332
1333 FHTreeStateNode child;
1334 FHTreeStateNode lastChild = null;
1335
1336 for(int counter = 0, maxCounter = getChildCount();
1337 counter < maxCounter; counter++) {
1338 child = (FHTreeStateNode)getChildAt(counter);
1339 if(child.row > row) {
1340 if(counter == 0) {
1341 // No node exists for it, and is first.
1342 info.node = this;
1343 info.isNodeParentNode = true;
1344 info.childIndex = row - this.row - 1;
1345 return true;
1346 }
1347 else {
1348 // May have been in last childs bounds.
1349 int lastChildEndRow = 1 + child.row -
1350 (child.childIndex - lastChild.childIndex);
1351
1352 if(row < lastChildEndRow) {
1353 return lastChild.getPathForRow(row,
1354 lastChildEndRow, info);
1355 }
1356 // Between last child and child, but not in last child
1357 info.node = this;
1358 info.isNodeParentNode = true;
1359 info.childIndex = row - lastChildEndRow +
1360 lastChild.childIndex + 1;
1361 return true;
1362 }
1363 }
1364 lastChild = child;
1365 }
1366
1367 // Not in children, but we should have it, offset from
1368 // nextRow.
1369 if(lastChild != null) {
1370 int lastChildEndRow = nextRow -
1371 (childCount - lastChild.childIndex) + 1;
1372
1373 if(row < lastChildEndRow) {
1374 return lastChild.getPathForRow(row, lastChildEndRow, info);
1375 }
1376 // Between last child and child, but not in last child
1377 info.node = this;
1378 info.isNodeParentNode = true;
1379 info.childIndex = row - lastChildEndRow +
1380 lastChild.childIndex + 1;
1381 return true;
1382 }
1383 else {
1384 // No children.
1385 int retChildIndex = row - this.row - 1;
1386
1387 if(retChildIndex >= childCount) {
1388 return false;
1389 }
1390 info.node = this;
1391 info.isNodeParentNode = true;
1392 info.childIndex = retChildIndex;
1393 return true;
1394 }
1395 }
1396
1397 /**
1398 * Asks all the children of the receiver for their totalChildCount
1399 * and returns this value (plus stopIndex).
1400 */
1401 protected int getCountTo(int stopIndex) {
1402 FHTreeStateNode aChild;
1403 int retCount = stopIndex + 1;
1404
1405 for(int counter = 0, maxCounter = getChildCount();
1406 counter < maxCounter; counter++) {
1407 aChild = (FHTreeStateNode)getChildAt(counter);
1408 if(aChild.childIndex >= stopIndex)
1409 counter = maxCounter;
1410 else
1411 retCount += aChild.getTotalChildCount();
1412 }
1413 if(parent != null)
1414 return retCount + ((FHTreeStateNode)getParent())
1415 .getCountTo(childIndex);
1416 if(!isRootVisible())
1417 return (retCount - 1);
1418 return retCount;
1419 }
1420
1421 /**
1422 * Returns the number of children that are expanded to
1423 * <code>stopIndex</code>. This does not include the number
1424 * of children that the child at <code>stopIndex</code> might
1425 * have.
1426 */
1427 protected int getNumExpandedChildrenTo(int stopIndex) {
1428 FHTreeStateNode aChild;
1429 int retCount = stopIndex;
1430
1431 for(int counter = 0, maxCounter = getChildCount();
1432 counter < maxCounter; counter++) {
1433 aChild = (FHTreeStateNode)getChildAt(counter);
1434 if(aChild.childIndex >= stopIndex)
1435 return retCount;
1436 else {
1437 retCount += aChild.getTotalChildCount();
1438 }
1439 }
1440 return retCount;
1441 }
1442
1443 /**
1444 * Messaged when this node either expands or collapses.
1445 */
1446 protected void didAdjustTree() {
1447 }
1448
1449 } // FixedHeightLayoutCache.FHTreeStateNode
1450
1451
1452 /**
1453 * Used as a placeholder when getting the path in FHTreeStateNodes.
1454 */
1455 private class SearchInfo {
1456 protected FHTreeStateNode node;
1457 protected boolean isNodeParentNode;
1458 protected int childIndex;
1459
1460 protected TreePath getPath() {
1461 if(node == null)
1462 return null;
1463
1464 if(isNodeParentNode)
1465 return node.getTreePath().pathByAddingChild(treeModel.getChild
1466 (node.getUserObject(),
1467 childIndex));
1468 return node.path;
1469 }
1470 } // FixedHeightLayoutCache.SearchInfo
1471
1472
1473 /**
1474 * An enumerator to iterate through visible nodes.
1475 */
1476 // This is very similiar to
1477 // VariableHeightTreeState.VisibleTreeStateNodeEnumeration
1478 private class VisibleFHTreeStateNodeEnumeration
1479 implements Enumeration<TreePath>
1480 {
1481 /** Parent thats children are being enumerated. */
1482 protected FHTreeStateNode parent;
1483 /** Index of next child. An index of -1 signifies parent should be
1484 * visibled next. */
1485 protected int nextIndex;
1486 /** Number of children in parent. */
1487 protected int childCount;
1488
1489 protected VisibleFHTreeStateNodeEnumeration(FHTreeStateNode node) {
1490 this(node, -1);
1491 }
1492
1493 protected VisibleFHTreeStateNodeEnumeration(FHTreeStateNode parent,
1494 int startIndex) {
1495 this.parent = parent;
1496 this.nextIndex = startIndex;
1497 this.childCount = treeModel.getChildCount(this.parent.
1498 getUserObject());
1499 }
1500
1501 /**
1502 * @return true if more visible nodes.
1503 */
1504 public boolean hasMoreElements() {
1505 return (parent != null);
1506 }
1507
1508 /**
1509 * @return next visible TreePath.
1510 */
1511 public TreePath nextElement() {
1512 if(!hasMoreElements())
1513 throw new NoSuchElementException("No more visible paths");
1514
1515 TreePath retObject;
1516
1517 if(nextIndex == -1)
1518 retObject = parent.getTreePath();
1519 else {
1520 FHTreeStateNode node = parent.getChildAtModelIndex(nextIndex);
1521
1522 if(node == null)
1523 retObject = parent.getTreePath().pathByAddingChild
1524 (treeModel.getChild(parent.getUserObject(),
1525 nextIndex));
1526 else
1527 retObject = node.getTreePath();
1528 }
1529 updateNextObject();
1530 return retObject;
1531 }
1532
1533 /**
1534 * Determines the next object by invoking <code>updateNextIndex</code>
1535 * and if not succesful <code>findNextValidParent</code>.
1536 */
1537 protected void updateNextObject() {
1538 if(!updateNextIndex()) {
1539 findNextValidParent();
1540 }
1541 }
1542
1543 /**
1544 * Finds the next valid parent, this should be called when nextIndex
1545 * is beyond the number of children of the current parent.
1546 */
1547 protected boolean findNextValidParent() {
1548 if(parent == root) {
1549 // mark as invalid!
1550 parent = null;
1551 return false;
1552 }
1553 while(parent != null) {
1554 FHTreeStateNode newParent = (FHTreeStateNode)parent.
1555 getParent();
1556
1557 if(newParent != null) {
1558 nextIndex = parent.childIndex;
1559 parent = newParent;
1560 childCount = treeModel.getChildCount
1561 (parent.getUserObject());
1562 if(updateNextIndex())
1563 return true;
1564 }
1565 else
1566 parent = null;
1567 }
1568 return false;
1569 }
1570
1571 /**
1572 * Updates <code>nextIndex</code> returning false if it is beyond
1573 * the number of children of parent.
1574 */
1575 protected boolean updateNextIndex() {
1576 // nextIndex == -1 identifies receiver, make sure is expanded
1577 // before descend.
1578 if(nextIndex == -1 && !parent.isExpanded()) {
1579 return false;
1580 }
1581
1582 // Check that it can have kids
1583 if(childCount == 0) {
1584 return false;
1585 }
1586 // Make sure next index not beyond child count.
1587 else if(++nextIndex >= childCount) {
1588 return false;
1589 }
1590
1591 FHTreeStateNode child = parent.getChildAtModelIndex(nextIndex);
1592
1593 if(child != null && child.isExpanded()) {
1594 parent = child;
1595 nextIndex = -1;
1596 childCount = treeModel.getChildCount(child.getUserObject());
1597 }
1598 return true;
1599 }
1600 } // FixedHeightLayoutCache.VisibleFHTreeStateNodeEnumeration
1601}