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 }