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