Tuesday, January 12, 2010

A JTree expansion model

At long last, a technical post!

Recently, I had a need to build a JTree from scratch, and wanted certain nodes in that tree expanded, while others remained collapsed. In my particular situation, I was working on the JavaSideKick plugin to jEdit. The JavaSideKick shows the important parts of a java file. Clicking on the tree moves the caret in the text editor area to the code associated with the node in the tree. If you're an Eclipse user, this is the same as the 'outline' feature, although SideKick is older and has more features than the Eclipse version. So, I had these requirements to display a java file in a tree display:

1. The compilation unit would be expanded.
2. The import node would be visible and not expanded, but would be able to be expanded.
3. The class node(s) would be expanded.
4. The method nodes would be visible.
5. Any inner classes would be expanded so their fields and methods would be visible.
6. Any enum nodes would be visible and not expanded, but would be able to be expanded.

Here is a working example. Notice the ExpansionModel class at the bottom, lines 100 - 127. It is very simple, it is just a wrapper around a list of row numbers to be expanded and a couple of methods to make it easy to build up the model. The top part of the code builds an example tree based on the requirements above and applies the ExpansionModel in lines 85 - 87.

   1 
2 import java.util.*;
3 import javax.swing.*;
4 import javax.swing.tree.*;
5
6 public class TreeTest {
7 public static void main ( String[] args ) {
8 new TreeTest();
9 }
10
11 public TreeTest() {
12 try {
13 SwingUtilities.invokeAndWait(
14 new Runnable() {
15 public void run() {
16 ExpansionModel em = new ExpansionModel();
17
18 DefaultMutableTreeNode root = new DefaultMutableTreeNode( "cu" );
19 em.add();
20 DefaultMutableTreeNode imports = new DefaultMutableTreeNode( "Imports" );
21 root.add( imports );
22 em.inc();
23
24 imports.add( new DefaultMutableTreeNode( "import 1" ) );
25 imports.add( new DefaultMutableTreeNode( "import 2" ) );
26 imports.add( new DefaultMutableTreeNode( "import 3" ) );
27 imports.add( new DefaultMutableTreeNode( "import 4" ) );
28 imports.add( new DefaultMutableTreeNode( "import 5" ) );
29 imports.add( new DefaultMutableTreeNode( "import 6" ) );
30 imports.add( new DefaultMutableTreeNode( "import 7" ) );
31
32 DefaultMutableTreeNode classNode = new DefaultMutableTreeNode( "class" );
33 root.add( classNode );
34 em.add();
35 classNode.add( new DefaultMutableTreeNode( "method 1" ) );
36 em.add();
37 classNode.add( new DefaultMutableTreeNode( "method 2" ) );
38 em.add();
39 classNode.add( new DefaultMutableTreeNode( "method 3" ) );
40 em.add();
41 classNode.add( new DefaultMutableTreeNode( "method 4" ) );
42 em.add();
43 classNode.add( new DefaultMutableTreeNode( "method 5" ) );
44 em.add();
45 classNode.add( new DefaultMutableTreeNode( "method 6" ) );
46 em.add();
47 classNode.add( new DefaultMutableTreeNode( "method 7" ) );
48 em.add();
49
50 DefaultMutableTreeNode innerClass = new DefaultMutableTreeNode( "inner class" );
51 classNode.add( innerClass );
52 em.add();
53 innerClass.add( new DefaultMutableTreeNode( "inner method 1" ) );
54 em.add();
55 innerClass.add( new DefaultMutableTreeNode( "inner method 2" ) );
56 em.add();
57 innerClass.add( new DefaultMutableTreeNode( "inner method 3" ) );
58 em.add();
59 innerClass.add( new DefaultMutableTreeNode( "inner method 4" ) );
60 em.add();
61 innerClass.add( new DefaultMutableTreeNode( "inner method 5" ) );
62 em.add();
63 innerClass.add( new DefaultMutableTreeNode( "inner method 6" ) );
64 em.add();
65
66 DefaultMutableTreeNode enumNode = new DefaultMutableTreeNode( "enum" );
67 classNode.add( enumNode );
68 em.inc();
69 enumNode.add( new DefaultMutableTreeNode( "enum value 1" ) );
70 enumNode.add( new DefaultMutableTreeNode( "enum value 2" ) );
71 enumNode.add( new DefaultMutableTreeNode( "enum value 3" ) );
72 enumNode.add( new DefaultMutableTreeNode( "enum value 4" ) );
73 enumNode.add( new DefaultMutableTreeNode( "enum value 5" ) );
74 enumNode.add( new DefaultMutableTreeNode( "enum value 6" ) );
75 enumNode.add( new DefaultMutableTreeNode( "enum value 7" ) );
76
77 JTree tree = new JTree( root );
78 JFrame frame = new JFrame();
79
80 // might want EXIT_ON_CLOSE if running from command line
81 frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
82
83 frame.getContentPane().add( tree );
84 frame.setSize(400, 600);
85 for (int row : em.getModel()) {
86 tree.expandRow(row);
87 }
88
89 frame.setVisible( true );
90 }
91 }
92 );
93
94 }
95 catch ( Exception e ) {
96 e.printStackTrace();
97 }
98 }
99
100 public class ExpansionModel {
101 private List<Integer> model = new ArrayList<Integer>();
102 private int row = 0;
103
104 /**
105 * @return The expansion model, set this in SideKickParsedData.
106 */
107 public List<Integer> getModel() {
108 return model;
109 }
110
111 /**
112 * Call this for each visible row in the tree that should be expanded.
113 * This will add the current row number to the model and automatically
114 * inc.
115 */
116 public void add() {
117 model.add( row );
118 inc();
119 }
120
121 /**
122 * Call this for each visible row in the tree.
123 */
124 public void inc() {
125 ++row;
126 }
127 }
128
129 }

No comments: