/*
* @(#)ElementTreePanel.java 1.16 04/07/26 slightly modified (line 121)
*
* Copyright (c) 2004 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:
*
* -Redistribution of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* -Redistribution 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, Inc. or the names of contributors may
* be used to endorse or promote products derived from this software without
* specific prior written permission.
*
* This software is provided "AS IS," without a warranty of any kind. ALL
* EXPRESS OR IMPLIED CONDITIONS, REPRESENTATIONS AND WARRANTIES, INCLUDING
* ANY IMPLIED WARRANTY OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE
* OR NON-INFRINGEMENT, ARE HEREBY EXCLUDED. SUN MIDROSYSTEMS, INC. ("SUN")
* AND ITS LICENSORS SHALL NOT BE LIABLE FOR ANY DAMAGES SUFFERED BY LICENSEE
* AS A RESULT OF USING, MODIFYING OR DISTRIBUTING THIS SOFTWARE OR ITS
* DERIVATIVES. IN NO EVENT WILL SUN OR ITS LICENSORS BE LIABLE FOR ANY LOST
* REVENUE, PROFIT OR DATA, OR FOR DIRECT, INDIRECT, SPECIAL, CONSEQUENTIAL,
* INCIDENTAL OR PUNITIVE DAMAGES, HOWEVER CAUSED AND REGARDLESS OF THE THEORY
* OF LIABILITY, ARISING OUT OF THE USE OF OR INABILITY TO USE THIS SOFTWARE,
* EVEN IF SUN HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
*
* You acknowledge that this software is not designed, licensed or intended
* for use in the design, construction, operation or maintenance of any
* nuclear facility.
*/
/*
* @(#)ElementTreePanel.java 1.16 04/07/26
*/
/* for use in SimplyHTML */
package com.sun.demo;
import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.Font;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.Enumeration;
import java.util.Vector;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTree;
import javax.swing.SwingConstants;
import javax.swing.event.CaretEvent;
import javax.swing.event.CaretListener;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.event.TreeSelectionEvent;
import javax.swing.event.TreeSelectionListener;
import javax.swing.text.AttributeSet;
import javax.swing.text.Document;
import javax.swing.text.Element;
import javax.swing.text.JTextComponent;
import javax.swing.text.StyleConstants;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.DefaultTreeCellRenderer;
import javax.swing.tree.DefaultTreeModel;
import javax.swing.tree.TreeModel;
import javax.swing.tree.TreeNode;
import javax.swing.tree.TreePath;
/**
* 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.16 07/26/04
*/
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) {
@Override
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);
/* commented out for use in SimplyHTML */// 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() {
@Override
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>
*/
@Override
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>
*/
@Override
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
*/
@Override
public boolean isLeaf(Object node) {
if (node == root)
return false;
return super.isLeaf(node);
}
/**
* Returns the index of child in parent.
*/
@Override
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.
*/
@Override
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.
*/
@Override
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;
}
}
}