/* * @(#)ElementTreePanel.java 1.4 98/06/29 * * Copyright 1998 by Sun Microsystems, Inc., * 901 San Antonio Road, Palo Alto, California, 94303, U.S.A. * All rights reserved. * * This software is the confidential and proprietary information * of Sun Microsystems, Inc. ("Confidential Information"). You * shall not disclose such Confidential Information and shall use * it only in accordance with the terms of the license agreement * you entered into with Sun. */ import com.sun.java.swing.*; import com.sun.java.swing.event.*; import com.sun.java.swing.text.*; import com.sun.java.swing.tree.*; import com.sun.java.swing.undo.*; import java.awt.*; 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 * @version 1.4 06/29/98 */ public class ElementTreePanel extends JPanel implements CaretListener, DocumentListener, TreeSelectionListener { /** Tree showing the documents element structure. */ protected JTree tree; /** Text component showing elemenst for. */ protected JTextComponent editor; /** Model for the tree. */ protected DefaultTreeModel 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 DefaultTreeModel((TreeNode)document. getDefaultRootElement()); tree = new JTree(treeModel) { public String convertValueToText(Object value, boolean selected, boolean expanded, boolean leaf, int row, boolean hasFocus) { 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 "LEAF: " + e.getName() + " Attributes: " + asString; return "Branch: " + e.getName() + " Attributes: " + asString; } }; tree.addTreeSelectionListener(this); // become a listener on the document to update the tree. document.addDocumentListener(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)); } // 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 start = Math.min(e.getDot(), e.getMark()); int end = Math.max(e.getDot(), e.getMark()); Vector paths = new Vector(); // Build an array of all the paths to all the character elements // in the selection. while(start <= end) { TreePath path = getPathForIndex(start); Element charElement = (Element)path.getLastPathComponent(); paths.addElement(path); if(start >= charElement.getEndOffset()) start++; else start = charElement.getEndOffset(); } // If more than one path was found, selected it. 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(); Element selElement = (Element)selPath.getLastPathComponent(); 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 root element of editor. */ protected Element getRootElement() { return getEditor().getDocument().getDefaultRootElement(); } /** * @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 { updateTree(event, getRootElement()); } 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 position. */ protected TreePath getPathForIndex(int position) { Element root = getEditor().getDocument().getDefaultRootElement(); TreePath path = new TreePath(root); Element child = root.getElement(root.getElementIndex(position)); path = path.pathByAddingChild(child); while(!child.isLeaf()) { child = child.getElement(child.getElementIndex(position)); path = path.pathByAddingChild(child); } return path; } }