blob: 7e4ea03051568211e3303ab14fab8cbd9e950d62 [file] [log] [blame]
J. Duke319a3b92007-12-01 00:00:00 +00001/*
2 * Copyright 1998-2000 Sun Microsystems, Inc. All Rights Reserved.
3 *
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions
6 * are met:
7 *
8 * - Redistributions of source code must retain the above copyright
9 * notice, this list of conditions and the following disclaimer.
10 *
11 * - Redistributions in binary form must reproduce the above copyright
12 * notice, this list of conditions and the following disclaimer in the
13 * documentation and/or other materials provided with the distribution.
14 *
15 * - Neither the name of Sun Microsystems nor the names of its
16 * contributors may be used to endorse or promote products derived
17 * from this software without specific prior written permission.
18 *
19 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
20 * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
21 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
22 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
23 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
24 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
25 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
26 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
27 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
28 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
29 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30 */
31
32/*
33 */
34
35import javax.swing.*;
36import javax.swing.event.*;
37import javax.swing.text.*;
38import javax.swing.tree.*;
39import javax.swing.undo.*;
40import java.awt.*;
41import java.beans.*;
42import java.util.*;
43
44/**
45 * Displays a tree showing all the elements in a text Document. Selecting
46 * a node will result in reseting the selection of the JTextComponent.
47 * This also becomes a CaretListener to know when the selection has changed
48 * in the text to update the selected item in the tree.
49 *
50 * @author Scott Violet
51 */
52public class ElementTreePanel extends JPanel implements CaretListener, DocumentListener, PropertyChangeListener, TreeSelectionListener {
53 /** Tree showing the documents element structure. */
54 protected JTree tree;
55 /** Text component showing elemenst for. */
56 protected JTextComponent editor;
57 /** Model for the tree. */
58 protected ElementTreeModel treeModel;
59 /** Set to true when updatin the selection. */
60 protected boolean updatingSelection;
61
62 public ElementTreePanel(JTextComponent editor) {
63 this.editor = editor;
64
65 Document document = editor.getDocument();
66
67 // Create the tree.
68 treeModel = new ElementTreeModel(document);
69 tree = new JTree(treeModel) {
70 public String convertValueToText(Object value, boolean selected,
71 boolean expanded, boolean leaf,
72 int row, boolean hasFocus) {
73 // Should only happen for the root
74 if(!(value instanceof Element))
75 return value.toString();
76
77 Element e = (Element)value;
78 AttributeSet as = e.getAttributes().copyAttributes();
79 String asString;
80
81 if(as != null) {
82 StringBuffer retBuffer = new StringBuffer("[");
83 Enumeration names = as.getAttributeNames();
84
85 while(names.hasMoreElements()) {
86 Object nextName = names.nextElement();
87
88 if(nextName != StyleConstants.ResolveAttribute) {
89 retBuffer.append(" ");
90 retBuffer.append(nextName);
91 retBuffer.append("=");
92 retBuffer.append(as.getAttribute(nextName));
93 }
94 }
95 retBuffer.append(" ]");
96 asString = retBuffer.toString();
97 }
98 else
99 asString = "[ ]";
100
101 if(e.isLeaf())
102 return e.getName() + " [" + e.getStartOffset() +
103 ", " + e.getEndOffset() +"] Attributes: " + asString;
104 return e.getName() + " [" + e.getStartOffset() +
105 ", " + e.getEndOffset() + "] Attributes: " +
106 asString;
107 }
108 };
109 tree.addTreeSelectionListener(this);
110 tree.setDragEnabled(true);
111 // Don't show the root, it is fake.
112 tree.setRootVisible(false);
113 // Since the display value of every node after the insertion point
114 // changes every time the text changes and we don't generate a change
115 // event for all those nodes the display value can become off.
116 // This can be seen as '...' instead of the complete string value.
117 // This is a temporary workaround, increase the needed size by 15,
118 // hoping that will be enough.
119 tree.setCellRenderer(new DefaultTreeCellRenderer() {
120 public Dimension getPreferredSize() {
121 Dimension retValue = super.getPreferredSize();
122 if(retValue != null)
123 retValue.width += 15;
124 return retValue;
125 }
126 });
127 // become a listener on the document to update the tree.
128 document.addDocumentListener(this);
129
130 // become a PropertyChangeListener to know when the Document has
131 // changed.
132 editor.addPropertyChangeListener(this);
133
134 // Become a CaretListener
135 editor.addCaretListener(this);
136
137 // configure the panel and frame containing it.
138 setLayout(new BorderLayout());
139 add(new JScrollPane(tree), BorderLayout.CENTER);
140
141 // Add a label above tree to describe what is being shown
142 JLabel label = new JLabel("Elements that make up the current document", SwingConstants.CENTER);
143
144 label.setFont(new Font("Dialog", Font.BOLD, 14));
145 add(label, BorderLayout.NORTH);
146
147 setPreferredSize(new Dimension(400, 400));
148 }
149
150 /**
151 * Resets the JTextComponent to <code>editor</code>. This will update
152 * the tree accordingly.
153 */
154 public void setEditor(JTextComponent editor) {
155 if (this.editor == editor) {
156 return;
157 }
158
159 if (this.editor != null) {
160 Document oldDoc = this.editor.getDocument();
161
162 oldDoc.removeDocumentListener(this);
163 this.editor.removePropertyChangeListener(this);
164 this.editor.removeCaretListener(this);
165 }
166 this.editor = editor;
167 if (editor == null) {
168 treeModel = null;
169 tree.setModel(null);
170 }
171 else {
172 Document newDoc = editor.getDocument();
173
174 newDoc.addDocumentListener(this);
175 editor.addPropertyChangeListener(this);
176 editor.addCaretListener(this);
177 treeModel = new ElementTreeModel(newDoc);
178 tree.setModel(treeModel);
179 }
180 }
181
182 // PropertyChangeListener
183
184 /**
185 * Invoked when a property changes. We are only interested in when the
186 * Document changes to reset the DocumentListener.
187 */
188 public void propertyChange(PropertyChangeEvent e) {
189 if (e.getSource() == getEditor() &&
190 e.getPropertyName().equals("document")) {
191 JTextComponent editor = getEditor();
192 Document oldDoc = (Document)e.getOldValue();
193 Document newDoc = (Document)e.getNewValue();
194
195 // Reset the DocumentListener
196 oldDoc.removeDocumentListener(this);
197 newDoc.addDocumentListener(this);
198
199 // Recreate the TreeModel.
200 treeModel = new ElementTreeModel(newDoc);
201 tree.setModel(treeModel);
202 }
203 }
204
205
206 // DocumentListener
207
208 /**
209 * Gives notification that there was an insert into the document. The
210 * given range bounds the freshly inserted region.
211 *
212 * @param e the document event
213 */
214 public void insertUpdate(DocumentEvent e) {
215 updateTree(e);
216 }
217
218 /**
219 * Gives notification that a portion of the document has been
220 * removed. The range is given in terms of what the view last
221 * saw (that is, before updating sticky positions).
222 *
223 * @param e the document event
224 */
225 public void removeUpdate(DocumentEvent e) {
226 updateTree(e);
227 }
228
229 /**
230 * Gives notification that an attribute or set of attributes changed.
231 *
232 * @param e the document event
233 */
234 public void changedUpdate(DocumentEvent e) {
235 updateTree(e);
236 }
237
238 // CaretListener
239
240 /**
241 * Messaged when the selection in the editor has changed. Will update
242 * the selection in the tree.
243 */
244 public void caretUpdate(CaretEvent e) {
245 if(!updatingSelection) {
246 JTextComponent editor = getEditor();
247 int selBegin = Math.min(e.getDot(), e.getMark());
248 int end = Math.max(e.getDot(), e.getMark());
249 Vector paths = new Vector();
250 TreeModel model = getTreeModel();
251 Object root = model.getRoot();
252 int rootCount = model.getChildCount(root);
253
254 // Build an array of all the paths to all the character elements
255 // in the selection.
256 for(int counter = 0; counter < rootCount; counter++) {
257 int start = selBegin;
258
259 while(start <= end) {
260 TreePath path = getPathForIndex(start, root,
261 (Element)model.getChild(root, counter));
262 Element charElement = (Element)path.
263 getLastPathComponent();
264
265 paths.addElement(path);
266 if(start >= charElement.getEndOffset())
267 start++;
268 else
269 start = charElement.getEndOffset();
270 }
271 }
272
273 // If a path was found, select it (them).
274 int numPaths = paths.size();
275
276 if(numPaths > 0) {
277 TreePath[] pathArray = new TreePath[numPaths];
278
279 paths.copyInto(pathArray);
280 updatingSelection = true;
281 try {
282 getTree().setSelectionPaths(pathArray);
283 getTree().scrollPathToVisible(pathArray[0]);
284 }
285 finally {
286 updatingSelection = false;
287 }
288 }
289 }
290 }
291
292 // TreeSelectionListener
293
294 /**
295 * Called whenever the value of the selection changes.
296 * @param e the event that characterizes the change.
297 */
298 public void valueChanged(TreeSelectionEvent e) {
299 JTree tree = getTree();
300
301 if(!updatingSelection && tree.getSelectionCount() == 1) {
302 TreePath selPath = tree.getSelectionPath();
303 Object lastPathComponent = selPath.getLastPathComponent();
304
305 if(!(lastPathComponent instanceof DefaultMutableTreeNode)) {
306 Element selElement = (Element)lastPathComponent;
307
308 updatingSelection = true;
309 try {
310 getEditor().select(selElement.getStartOffset(),
311 selElement.getEndOffset());
312 }
313 finally {
314 updatingSelection = false;
315 }
316 }
317 }
318 }
319
320 // Local methods
321
322 /**
323 * @return tree showing elements.
324 */
325 protected JTree getTree() {
326 return tree;
327 }
328
329 /**
330 * @return JTextComponent showing elements for.
331 */
332 protected JTextComponent getEditor() {
333 return editor;
334 }
335
336 /**
337 * @return TreeModel implementation used to represent the elements.
338 */
339 public DefaultTreeModel getTreeModel() {
340 return treeModel;
341 }
342
343 /**
344 * Updates the tree based on the event type. This will invoke either
345 * updateTree with the root element, or handleChange.
346 */
347 protected void updateTree(DocumentEvent event) {
348 updatingSelection = true;
349 try {
350 TreeModel model = getTreeModel();
351 Object root = model.getRoot();
352
353 for(int counter = model.getChildCount(root) - 1; counter >= 0;
354 counter--) {
355 updateTree(event, (Element)model.getChild(root, counter));
356 }
357 }
358 finally {
359 updatingSelection = false;
360 }
361 }
362
363 /**
364 * Creates TreeModelEvents based on the DocumentEvent and messages
365 * the treemodel. This recursively invokes this method with children
366 * elements.
367 * @param event indicates what elements in the tree hierarchy have
368 * changed.
369 * @param element Current element to check for changes against.
370 */
371 protected void updateTree(DocumentEvent event, Element element) {
372 DocumentEvent.ElementChange ec = event.getChange(element);
373
374 if (ec != null) {
375 Element[] removed = ec.getChildrenRemoved();
376 Element[] added = ec.getChildrenAdded();
377 int startIndex = ec.getIndex();
378
379 // Check for removed.
380 if(removed != null && removed.length > 0) {
381 int[] indices = new int[removed.length];
382
383 for(int counter = 0; counter < removed.length; counter++) {
384 indices[counter] = startIndex + counter;
385 }
386 getTreeModel().nodesWereRemoved((TreeNode)element, indices,
387 removed);
388 }
389 // check for added
390 if(added != null && added.length > 0) {
391 int[] indices = new int[added.length];
392
393 for(int counter = 0; counter < added.length; counter++) {
394 indices[counter] = startIndex + counter;
395 }
396 getTreeModel().nodesWereInserted((TreeNode)element, indices);
397 }
398 }
399 if(!element.isLeaf()) {
400 int startIndex = element.getElementIndex
401 (event.getOffset());
402 int elementCount = element.getElementCount();
403 int endIndex = Math.min(elementCount - 1,
404 element.getElementIndex
405 (event.getOffset() + event.getLength()));
406
407 if(startIndex > 0 && startIndex < elementCount &&
408 element.getElement(startIndex).getStartOffset() ==
409 event.getOffset()) {
410 // Force checking the previous element.
411 startIndex--;
412 }
413 if(startIndex != -1 && endIndex != -1) {
414 for(int counter = startIndex; counter <= endIndex; counter++) {
415 updateTree(event, element.getElement(counter));
416 }
417 }
418 }
419 else {
420 // Element is a leaf, assume it changed
421 getTreeModel().nodeChanged((TreeNode)element);
422 }
423 }
424
425 /**
426 * Returns a TreePath to the element at <code>position</code>.
427 */
428 protected TreePath getPathForIndex(int position, Object root,
429 Element rootElement) {
430 TreePath path = new TreePath(root);
431 Element child = rootElement.getElement
432 (rootElement.getElementIndex(position));
433
434 path = path.pathByAddingChild(rootElement);
435 path = path.pathByAddingChild(child);
436 while(!child.isLeaf()) {
437 child = child.getElement(child.getElementIndex(position));
438 path = path.pathByAddingChild(child);
439 }
440 return path;
441 }
442
443
444 /**
445 * ElementTreeModel is an implementation of TreeModel to handle displaying
446 * the Elements from a Document. AbstractDocument.AbstractElement is
447 * the default implementation used by the swing text package to implement
448 * Element, and it implements TreeNode. This makes it trivial to create
449 * a DefaultTreeModel rooted at a particular Element from the Document.
450 * Unfortunately each Document can have more than one root Element.
451 * Implying that to display all the root elements as a child of another
452 * root a fake node has be created. This class creates a fake node as
453 * the root with the children being the root elements of the Document
454 * (getRootElements).
455 * <p>This subclasses DefaultTreeModel. The majority of the TreeModel
456 * methods have been subclassed, primarily to special case the root.
457 */
458 public static class ElementTreeModel extends DefaultTreeModel {
459 protected Element[] rootElements;
460
461 public ElementTreeModel(Document document) {
462 super(new DefaultMutableTreeNode("root"), false);
463 rootElements = document.getRootElements();
464 }
465
466 /**
467 * Returns the child of <I>parent</I> at index <I>index</I> in
468 * the parent's child array. <I>parent</I> must be a node
469 * previously obtained from this data source. This should
470 * not return null if <i>index</i> is a valid index for
471 * <i>parent</i> (that is <i>index</i> >= 0 && <i>index</i>
472 * < getChildCount(<i>parent</i>)).
473 *
474 * @param parent a node in the tree, obtained from this data source
475 * @return the child of <I>parent</I> at index <I>index</I>
476 */
477 public Object getChild(Object parent, int index) {
478 if(parent == root)
479 return rootElements[index];
480 return super.getChild(parent, index);
481 }
482
483
484 /**
485 * Returns the number of children of <I>parent</I>. Returns 0
486 * if the node is a leaf or if it has no children.
487 * <I>parent</I> must be a node previously obtained from this
488 * data source.
489 *
490 * @param parent a node in the tree, obtained from this data source
491 * @return the number of children of the node <I>parent</I>
492 */
493 public int getChildCount(Object parent) {
494 if(parent == root)
495 return rootElements.length;
496 return super.getChildCount(parent);
497 }
498
499
500 /**
501 * Returns true if <I>node</I> is a leaf. It is possible for
502 * this method to return false even if <I>node</I> has no
503 * children. A directory in a filesystem, for example, may
504 * contain no files; the node representing the directory is
505 * not a leaf, but it also has no children.
506 *
507 * @param node a node in the tree, obtained from this data source
508 * @return true if <I>node</I> is a leaf
509 */
510 public boolean isLeaf(Object node) {
511 if(node == root)
512 return false;
513 return super.isLeaf(node);
514 }
515
516 /**
517 * Returns the index of child in parent.
518 */
519 public int getIndexOfChild(Object parent, Object child) {
520 if(parent == root) {
521 for(int counter = rootElements.length - 1; counter >= 0;
522 counter--) {
523 if(rootElements[counter] == child)
524 return counter;
525 }
526 return -1;
527 }
528 return super.getIndexOfChild(parent, child);
529 }
530
531 /**
532 * Invoke this method after you've changed how node is to be
533 * represented in the tree.
534 */
535 public void nodeChanged(TreeNode node) {
536 if(listenerList != null && node != null) {
537 TreeNode parent = node.getParent();
538
539 if(parent == null && node != root) {
540 parent = root;
541 }
542 if(parent != null) {
543 int anIndex = getIndexOfChild(parent, node);
544
545 if(anIndex != -1) {
546 int[] cIndexs = new int[1];
547
548 cIndexs[0] = anIndex;
549 nodesChanged(parent, cIndexs);
550 }
551 }
552 }
553 }
554
555 /**
556 * Returns the path to a particluar node. This is recursive.
557 */
558 protected TreeNode[] getPathToRoot(TreeNode aNode, int depth) {
559 TreeNode[] retNodes;
560
561 /* Check for null, in case someone passed in a null node, or
562 they passed in an element that isn't rooted at root. */
563 if(aNode == null) {
564 if(depth == 0)
565 return null;
566 else
567 retNodes = new TreeNode[depth];
568 }
569 else {
570 depth++;
571 if(aNode == root)
572 retNodes = new TreeNode[depth];
573 else {
574 TreeNode parent = aNode.getParent();
575
576 if(parent == null)
577 parent = root;
578 retNodes = getPathToRoot(parent, depth);
579 }
580 retNodes[retNodes.length - depth] = aNode;
581 }
582 return retNodes;
583 }
584 }
585}