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