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