ElementTreePanel.java revision 4378
0N/A/*
2120N/A * Copyright (c) 1998, 2011, Oracle and/or its affiliates. All rights reserved.
0N/A *
0N/A * Redistribution and use in source and binary forms, with or without
0N/A * modification, are permitted provided that the following conditions
0N/A * are met:
0N/A *
0N/A * - Redistributions of source code must retain the above copyright
0N/A * notice, this list of conditions and the following disclaimer.
0N/A *
0N/A * - Redistributions in binary form must reproduce the above copyright
0N/A * notice, this list of conditions and the following disclaimer in the
0N/A * documentation and/or other materials provided with the distribution.
0N/A *
0N/A * - Neither the name of Oracle nor the names of its
0N/A * contributors may be used to endorse or promote products derived
0N/A * from this software without specific prior written permission.
0N/A *
1472N/A * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
1472N/A * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
1472N/A * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
0N/A * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
0N/A * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
0N/A * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
1879N/A * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
1879N/A * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
1879N/A * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
1879N/A * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
1879N/A * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
1879N/A */
1879N/A
1879N/A/*
1879N/A * This source code is provided to illustrate the usage of a given feature
0N/A * or technique and has been deliberately simplified. Additional steps
0N/A * required for a production-quality application, such as security checks,
0N/A * input validation and proper error handling, might not be present in
0N/A * this sample code.
0N/A */
0N/A
0N/A
0N/A
1172N/Aimport java.awt.BorderLayout;
0N/Aimport java.awt.Dimension;
0N/Aimport java.awt.Font;
0N/Aimport java.beans.PropertyChangeEvent;
0N/Aimport java.beans.PropertyChangeListener;
0N/Aimport java.util.*;
0N/Aimport javax.swing.JLabel;
0N/Aimport javax.swing.JPanel;
0N/Aimport javax.swing.JScrollPane;
0N/Aimport javax.swing.JTree;
0N/Aimport javax.swing.SwingConstants;
0N/Aimport javax.swing.event.CaretEvent;
0N/Aimport javax.swing.event.CaretListener;
0N/Aimport javax.swing.event.DocumentEvent;
0N/Aimport javax.swing.event.DocumentListener;
0N/Aimport javax.swing.event.TreeSelectionEvent;
0N/Aimport javax.swing.event.TreeSelectionListener;
0N/Aimport javax.swing.text.AttributeSet;
0N/Aimport javax.swing.text.Document;
0N/Aimport javax.swing.text.Element;
0N/Aimport javax.swing.text.JTextComponent;
0N/Aimport javax.swing.text.StyleConstants;
0N/Aimport javax.swing.tree.DefaultMutableTreeNode;
0N/Aimport javax.swing.tree.DefaultTreeCellRenderer;
0N/Aimport javax.swing.tree.DefaultTreeModel;
0N/Aimport javax.swing.tree.TreeModel;
0N/Aimport javax.swing.tree.TreeNode;
0N/Aimport javax.swing.tree.TreePath;
0N/A
0N/A
0N/A/**
0N/A * Displays a tree showing all the elements in a text Document. Selecting
0N/A * a node will result in reseting the selection of the JTextComponent.
0N/A * This also becomes a CaretListener to know when the selection has changed
0N/A * in the text to update the selected item in the tree.
0N/A *
0N/A * @author Scott Violet
0N/A */
0N/A@SuppressWarnings("serial")
0N/Apublic class ElementTreePanel extends JPanel implements CaretListener,
0N/A DocumentListener, PropertyChangeListener, TreeSelectionListener {
0N/A
0N/A /** Tree showing the documents element structure. */
0N/A protected JTree tree;
0N/A /** Text component showing elemenst for. */
0N/A protected JTextComponent editor;
0N/A /** Model for the tree. */
0N/A protected ElementTreeModel treeModel;
0N/A /** Set to true when updatin the selection. */
0N/A protected boolean updatingSelection;
0N/A
0N/A @SuppressWarnings("LeakingThisInConstructor")
0N/A public ElementTreePanel(JTextComponent editor) {
0N/A this.editor = editor;
2230N/A
0N/A Document document = editor.getDocument();
0N/A
0N/A // Create the tree.
0N/A treeModel = new ElementTreeModel(document);
0N/A tree = new JTree(treeModel) {
0N/A
0N/A @Override
0N/A public String convertValueToText(Object value, boolean selected,
2230N/A boolean expanded, boolean leaf,
2230N/A int row, boolean hasFocus) {
0N/A // Should only happen for the root
0N/A if (!(value instanceof Element)) {
0N/A return value.toString();
0N/A }
0N/A
0N/A Element e = (Element) value;
0N/A AttributeSet as = e.getAttributes().copyAttributes();
0N/A String asString;
2230N/A
0N/A if (as != null) {
0N/A StringBuilder retBuffer = new StringBuilder("[");
0N/A Enumeration names = as.getAttributeNames();
0N/A
0N/A while (names.hasMoreElements()) {
0N/A Object nextName = names.nextElement();
0N/A
0N/A if (nextName != StyleConstants.ResolveAttribute) {
0N/A retBuffer.append(" ");
0N/A retBuffer.append(nextName);
0N/A retBuffer.append("=");
0N/A retBuffer.append(as.getAttribute(nextName));
0N/A }
0N/A }
0N/A retBuffer.append(" ]");
0N/A asString = retBuffer.toString();
0N/A } else {
0N/A asString = "[ ]";
0N/A }
0N/A
0N/A if (e.isLeaf()) {
0N/A return e.getName() + " [" + e.getStartOffset() + ", " + e.
0N/A getEndOffset() + "] Attributes: " + asString;
0N/A }
0N/A return e.getName() + " [" + e.getStartOffset() + ", " + e.
0N/A getEndOffset() + "] Attributes: " + asString;
0N/A }
0N/A };
0N/A tree.addTreeSelectionListener(this);
0N/A tree.setDragEnabled(true);
0N/A // Don't show the root, it is fake.
0N/A tree.setRootVisible(false);
0N/A // Since the display value of every node after the insertion point
0N/A // changes every time the text changes and we don't generate a change
0N/A // event for all those nodes the display value can become off.
0N/A // This can be seen as '...' instead of the complete string value.
0N/A // This is a temporary workaround, increase the needed size by 15,
0N/A // hoping that will be enough.
0N/A tree.setCellRenderer(new DefaultTreeCellRenderer() {
0N/A
0N/A @Override
0N/A public Dimension getPreferredSize() {
0N/A Dimension retValue = super.getPreferredSize();
0N/A if (retValue != null) {
0N/A retValue.width += 15;
0N/A }
0N/A return retValue;
0N/A }
0N/A });
0N/A // become a listener on the document to update the tree.
0N/A document.addDocumentListener(this);
0N/A
0N/A // become a PropertyChangeListener to know when the Document has
0N/A // changed.
0N/A editor.addPropertyChangeListener(this);
0N/A
0N/A // Become a CaretListener
0N/A editor.addCaretListener(this);
0N/A
0N/A // configure the panel and frame containing it.
0N/A setLayout(new BorderLayout());
0N/A add(new JScrollPane(tree), BorderLayout.CENTER);
0N/A
0N/A // Add a label above tree to describe what is being shown
0N/A JLabel label = new JLabel("Elements that make up the current document",
0N/A SwingConstants.CENTER);
0N/A
0N/A label.setFont(new Font("Dialog", Font.BOLD, 14));
0N/A add(label, BorderLayout.NORTH);
0N/A
0N/A setPreferredSize(new Dimension(400, 400));
0N/A }
0N/A
0N/A /**
0N/A * Resets the JTextComponent to <code>editor</code>. This will update
0N/A * the tree accordingly.
0N/A */
0N/A public void setEditor(JTextComponent editor) {
0N/A if (this.editor == editor) {
0N/A return;
0N/A }
0N/A
0N/A if (this.editor != null) {
0N/A Document oldDoc = this.editor.getDocument();
0N/A
0N/A oldDoc.removeDocumentListener(this);
0N/A this.editor.removePropertyChangeListener(this);
0N/A this.editor.removeCaretListener(this);
0N/A }
367N/A this.editor = editor;
367N/A if (editor == null) {
0N/A treeModel = null;
0N/A tree.setModel(null);
0N/A } else {
0N/A Document newDoc = editor.getDocument();
0N/A
0N/A newDoc.addDocumentListener(this);
0N/A editor.addPropertyChangeListener(this);
0N/A editor.addCaretListener(this);
0N/A treeModel = new ElementTreeModel(newDoc);
0N/A tree.setModel(treeModel);
0N/A }
0N/A }
0N/A
0N/A // PropertyChangeListener
0N/A /**
0N/A * Invoked when a property changes. We are only interested in when the
0N/A * Document changes to reset the DocumentListener.
0N/A */
0N/A public void propertyChange(PropertyChangeEvent e) {
0N/A if (e.getSource() == getEditor() && e.getPropertyName().equals(
0N/A "document")) {
0N/A Document oldDoc = (Document) e.getOldValue();
0N/A Document newDoc = (Document) e.getNewValue();
0N/A
0N/A // Reset the DocumentListener
0N/A oldDoc.removeDocumentListener(this);
0N/A newDoc.addDocumentListener(this);
0N/A
0N/A // Recreate the TreeModel.
0N/A treeModel = new ElementTreeModel(newDoc);
0N/A tree.setModel(treeModel);
0N/A }
0N/A }
0N/A
0N/A // DocumentListener
0N/A /**
0N/A * Gives notification that there was an insert into the document. The
0N/A * given range bounds the freshly inserted region.
0N/A *
0N/A * @param e the document event
0N/A */
0N/A public void insertUpdate(DocumentEvent e) {
0N/A updateTree(e);
0N/A }
0N/A
0N/A /**
0N/A * Gives notification that a portion of the document has been
0N/A * removed. The range is given in terms of what the view last
0N/A * saw (that is, before updating sticky positions).
0N/A *
0N/A * @param e the document event
0N/A */
0N/A public void removeUpdate(DocumentEvent e) {
0N/A updateTree(e);
0N/A }
0N/A
0N/A /**
0N/A * Gives notification that an attribute or set of attributes changed.
0N/A *
0N/A * @param e the document event
0N/A */
0N/A public void changedUpdate(DocumentEvent e) {
0N/A updateTree(e);
0N/A }
0N/A
0N/A // CaretListener
0N/A /**
0N/A * Messaged when the selection in the editor has changed. Will update
0N/A * the selection in the tree.
0N/A */
0N/A public void caretUpdate(CaretEvent e) {
0N/A if (!updatingSelection) {
0N/A int selBegin = Math.min(e.getDot(), e.getMark());
0N/A int end = Math.max(e.getDot(), e.getMark());
0N/A List<TreePath> paths = new ArrayList<TreePath>();
0N/A TreeModel model = getTreeModel();
0N/A Object root = model.getRoot();
0N/A int rootCount = model.getChildCount(root);
0N/A
0N/A // Build an array of all the paths to all the character elements
0N/A // in the selection.
0N/A for (int counter = 0; counter < rootCount; counter++) {
0N/A int start = selBegin;
0N/A
0N/A while (start <= end) {
0N/A TreePath path = getPathForIndex(start, root,
0N/A (Element) model.getChild(root, counter));
0N/A Element charElement = (Element) path.getLastPathComponent();
0N/A
0N/A paths.add(path);
0N/A if (start >= charElement.getEndOffset()) {
0N/A start++;
0N/A } else {
0N/A start = charElement.getEndOffset();
0N/A }
0N/A }
0N/A }
39N/A
39N/A // If a path was found, select it (them).
0N/A int numPaths = paths.size();
0N/A
0N/A if (numPaths > 0) {
0N/A TreePath[] pathArray = new TreePath[numPaths];
0N/A
0N/A paths.toArray(pathArray);
39N/A updatingSelection = true;
0N/A try {
0N/A getTree().setSelectionPaths(pathArray);
0N/A getTree().scrollPathToVisible(pathArray[0]);
0N/A } finally {
0N/A updatingSelection = false;
0N/A }
0N/A }
0N/A }
0N/A }
0N/A
0N/A // TreeSelectionListener
0N/A /**
0N/A * Called whenever the value of the selection changes.
0N/A * @param e the event that characterizes the change.
0N/A */
0N/A public void valueChanged(TreeSelectionEvent e) {
0N/A
0N/A if (!updatingSelection && tree.getSelectionCount() == 1) {
0N/A TreePath selPath = tree.getSelectionPath();
0N/A Object lastPathComponent = selPath.getLastPathComponent();
0N/A
0N/A if (!(lastPathComponent instanceof DefaultMutableTreeNode)) {
0N/A Element selElement = (Element) lastPathComponent;
0N/A
0N/A updatingSelection = true;
1172N/A try {
1172N/A getEditor().select(selElement.getStartOffset(),
1172N/A selElement.getEndOffset());
1172N/A } finally {
401N/A updatingSelection = false;
401N/A }
401N/A }
401N/A }
0N/A }
401N/A
401N/A // Local methods
401N/A /**
0N/A * @return tree showing elements.
0N/A */
0N/A protected JTree getTree() {
0N/A return tree;
0N/A }
0N/A
0N/A /**
0N/A * @return JTextComponent showing elements for.
0N/A */
0N/A protected JTextComponent getEditor() {
0N/A return editor;
0N/A }
0N/A
0N/A /**
0N/A * @return TreeModel implementation used to represent the elements.
0N/A */
0N/A public DefaultTreeModel getTreeModel() {
0N/A return treeModel;
0N/A }
0N/A
0N/A /**
0N/A * Updates the tree based on the event type. This will invoke either
0N/A * updateTree with the root element, or handleChange.
0N/A */
0N/A protected void updateTree(DocumentEvent event) {
0N/A updatingSelection = true;
0N/A try {
0N/A TreeModel model = getTreeModel();
0N/A Object root = model.getRoot();
0N/A
0N/A for (int counter = model.getChildCount(root) - 1; counter >= 0;
0N/A counter--) {
0N/A updateTree(event, (Element) model.getChild(root, counter));
0N/A }
0N/A } finally {
0N/A updatingSelection = false;
0N/A }
0N/A }
0N/A
0N/A /**
0N/A * Creates TreeModelEvents based on the DocumentEvent and messages
0N/A * the treemodel. This recursively invokes this method with children
0N/A * elements.
0N/A * @param event indicates what elements in the tree hierarchy have
0N/A * changed.
0N/A * @param element Current element to check for changes against.
0N/A */
0N/A protected void updateTree(DocumentEvent event, Element element) {
0N/A DocumentEvent.ElementChange ec = event.getChange(element);
0N/A
0N/A if (ec != null) {
0N/A Element[] removed = ec.getChildrenRemoved();
0N/A Element[] added = ec.getChildrenAdded();
0N/A int startIndex = ec.getIndex();
0N/A
0N/A // Check for removed.
0N/A if (removed != null && removed.length > 0) {
605N/A int[] indices = new int[removed.length];
0N/A
0N/A for (int counter = 0; counter < removed.length; counter++) {
0N/A indices[counter] = startIndex + counter;
0N/A }
1172N/A getTreeModel().nodesWereRemoved((TreeNode) element, indices,
1172N/A removed);
1172N/A }
0N/A // check for added
0N/A if (added != null && added.length > 0) {
0N/A int[] indices = new int[added.length];
0N/A
0N/A for (int counter = 0; counter < added.length; counter++) {
0N/A indices[counter] = startIndex + counter;
0N/A }
0N/A getTreeModel().nodesWereInserted((TreeNode) element, indices);
605N/A }
0N/A }
0N/A if (!element.isLeaf()) {
0N/A int startIndex = element.getElementIndex(event.getOffset());
0N/A int elementCount = element.getElementCount();
0N/A int endIndex = Math.min(elementCount - 1,
0N/A element.getElementIndex(event.getOffset()
0N/A + event.getLength()));
0N/A
0N/A if (startIndex > 0 && startIndex < elementCount && element.
0N/A getElement(startIndex).getStartOffset() == event.getOffset()) {
0N/A // Force checking the previous element.
0N/A startIndex--;
0N/A }
0N/A if (startIndex != -1 && endIndex != -1) {
0N/A for (int counter = startIndex; counter <= endIndex; counter++) {
0N/A updateTree(event, element.getElement(counter));
0N/A }
0N/A }
0N/A } else {
0N/A // Element is a leaf, assume it changed
0N/A getTreeModel().nodeChanged((TreeNode) element);
0N/A }
0N/A }
0N/A
0N/A /**
0N/A * Returns a TreePath to the element at <code>position</code>.
0N/A */
0N/A protected TreePath getPathForIndex(int position, Object root,
0N/A Element rootElement) {
0N/A TreePath path = new TreePath(root);
0N/A Element child = rootElement.getElement(rootElement.getElementIndex(
0N/A position));
0N/A
0N/A path = path.pathByAddingChild(rootElement);
0N/A path = path.pathByAddingChild(child);
0N/A while (!child.isLeaf()) {
0N/A child = child.getElement(child.getElementIndex(position));
0N/A path = path.pathByAddingChild(child);
921N/A }
921N/A return path;
921N/A }
0N/A
0N/A
0N/A /**
0N/A * ElementTreeModel is an implementation of TreeModel to handle displaying
0N/A * the Elements from a Document. AbstractDocument.AbstractElement is
0N/A * the default implementation used by the swing text package to implement
0N/A * Element, and it implements TreeNode. This makes it trivial to create
0N/A * a DefaultTreeModel rooted at a particular Element from the Document.
0N/A * Unfortunately each Document can have more than one root Element.
0N/A * Implying that to display all the root elements as a child of another
0N/A * root a fake node has be created. This class creates a fake node as
0N/A * the root with the children being the root elements of the Document
0N/A * (getRootElements).
0N/A * <p>This subclasses DefaultTreeModel. The majority of the TreeModel
0N/A * methods have been subclassed, primarily to special case the root.
0N/A */
0N/A public static class ElementTreeModel extends DefaultTreeModel {
0N/A
0N/A protected Element[] rootElements;
0N/A
0N/A public ElementTreeModel(Document document) {
0N/A super(new DefaultMutableTreeNode("root"), false);
0N/A rootElements = document.getRootElements();
0N/A }
0N/A
0N/A /**
0N/A * Returns the child of <I>parent</I> at index <I>index</I> in
0N/A * the parent's child array. <I>parent</I> must be a node
0N/A * previously obtained from this data source. This should
0N/A * not return null if <i>index</i> is a valid index for
0N/A * <i>parent</i> (that is <i>index</i> >= 0 && <i>index</i>
0N/A * < getChildCount(<i>parent</i>)).
0N/A *
0N/A * @param parent a node in the tree, obtained from this data source
0N/A * @return the child of <I>parent</I> at index <I>index</I>
0N/A */
0N/A @Override
0N/A public Object getChild(Object parent, int index) {
0N/A if (parent == root) {
0N/A return rootElements[index];
0N/A }
0N/A return super.getChild(parent, index);
0N/A }
0N/A
0N/A /**
0N/A * Returns the number of children of <I>parent</I>. Returns 0
0N/A * if the node is a leaf or if it has no children.
0N/A * <I>parent</I> must be a node previously obtained from this
0N/A * data source.
0N/A *
0N/A * @param parent a node in the tree, obtained from this data source
0N/A * @return the number of children of the node <I>parent</I>
0N/A */
0N/A @Override
0N/A public int getChildCount(Object parent) {
921N/A if (parent == root) {
921N/A return rootElements.length;
921N/A }
921N/A return super.getChildCount(parent);
921N/A }
921N/A
0N/A /**
0N/A * Returns true if <I>node</I> is a leaf. It is possible for
0N/A * this method to return false even if <I>node</I> has no
0N/A * children. A directory in a filesystem, for example, may
0N/A * contain no files; the node representing the directory is
0N/A * not a leaf, but it also has no children.
0N/A *
0N/A * @param node a node in the tree, obtained from this data source
0N/A * @return true if <I>node</I> is a leaf
0N/A */
0N/A @Override
0N/A public boolean isLeaf(Object node) {
0N/A if (node == root) {
0N/A return false;
921N/A }
0N/A return super.isLeaf(node);
0N/A }
0N/A
0N/A /**
0N/A * Returns the index of child in parent.
0N/A */
0N/A @Override
0N/A public int getIndexOfChild(Object parent, Object child) {
0N/A if (parent == root) {
0N/A for (int counter = rootElements.length - 1; counter >= 0;
0N/A counter--) {
0N/A if (rootElements[counter] == child) {
0N/A return counter;
0N/A }
0N/A }
0N/A return -1;
0N/A }
0N/A return super.getIndexOfChild(parent, child);
0N/A }
0N/A
0N/A /**
0N/A * Invoke this method after you've changed how node is to be
0N/A * represented in the tree.
0N/A */
0N/A @Override
0N/A public void nodeChanged(TreeNode node) {
0N/A if (listenerList != null && node != null) {
0N/A TreeNode parent = node.getParent();
0N/A
0N/A if (parent == null && node != root) {
0N/A parent = root;
0N/A }
0N/A if (parent != null) {
0N/A int anIndex = getIndexOfChild(parent, node);
0N/A
0N/A if (anIndex != -1) {
0N/A int[] cIndexs = new int[1];
0N/A
0N/A cIndexs[0] = anIndex;
0N/A nodesChanged(parent, cIndexs);
0N/A }
0N/A }
0N/A }
0N/A }
0N/A
0N/A /**
0N/A * Returns the path to a particluar node. This is recursive.
0N/A */
1172N/A @Override
1172N/A protected TreeNode[] getPathToRoot(TreeNode aNode, int depth) {
1172N/A TreeNode[] retNodes;
1172N/A
1172N/A /* Check for null, in case someone passed in a null node, or
1172N/A they passed in an element that isn't rooted at root. */
1172N/A if (aNode == null) {
1172N/A if (depth == 0) {
1172N/A return null;
1172N/A } else {
1172N/A retNodes = new TreeNode[depth];
0N/A }
0N/A } else {
0N/A depth++;
0N/A if (aNode == root) {
0N/A retNodes = new TreeNode[depth];
0N/A } else {
0N/A TreeNode parent = aNode.getParent();
0N/A
0N/A if (parent == null) {
0N/A parent = root;
0N/A }
0N/A retNodes = getPathToRoot(parent, depth);
0N/A }
0N/A retNodes[retNodes.length - depth] = aNode;
0N/A }
0N/A return retNodes;
0N/A }
0N/A }
0N/A}
0N/A