Path: blob/master/src/demo/share/jfc/Notepad/ElementTreePanel.java
41149 views
/*1* Copyright (c) 1998, 2011, Oracle and/or its affiliates. All rights reserved.2*3* Redistribution and use in source and binary forms, with or without4* modification, are permitted provided that the following conditions5* are met:6*7* - Redistributions of source code must retain the above copyright8* notice, this list of conditions and the following disclaimer.9*10* - Redistributions in binary form must reproduce the above copyright11* notice, this list of conditions and the following disclaimer in the12* documentation and/or other materials provided with the distribution.13*14* - Neither the name of Oracle nor the names of its15* contributors may be used to endorse or promote products derived16* from this software without specific prior written permission.17*18* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS19* IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,20* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR21* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR22* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,23* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,24* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR25* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF26* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING27* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS28* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.29*/3031/*32* This source code is provided to illustrate the usage of a given feature33* or technique and has been deliberately simplified. Additional steps34* required for a production-quality application, such as security checks,35* input validation and proper error handling, might not be present in36* this sample code.37*/38394041import java.awt.BorderLayout;42import java.awt.Dimension;43import java.awt.Font;44import java.beans.PropertyChangeEvent;45import java.beans.PropertyChangeListener;46import java.util.*;47import javax.swing.JLabel;48import javax.swing.JPanel;49import javax.swing.JScrollPane;50import javax.swing.JTree;51import javax.swing.SwingConstants;52import javax.swing.event.CaretEvent;53import javax.swing.event.CaretListener;54import javax.swing.event.DocumentEvent;55import javax.swing.event.DocumentListener;56import javax.swing.event.TreeSelectionEvent;57import javax.swing.event.TreeSelectionListener;58import javax.swing.text.AttributeSet;59import javax.swing.text.Document;60import javax.swing.text.Element;61import javax.swing.text.JTextComponent;62import javax.swing.text.StyleConstants;63import javax.swing.tree.DefaultMutableTreeNode;64import javax.swing.tree.DefaultTreeCellRenderer;65import javax.swing.tree.DefaultTreeModel;66import javax.swing.tree.TreeModel;67import javax.swing.tree.TreeNode;68import javax.swing.tree.TreePath;697071/**72* Displays a tree showing all the elements in a text Document. Selecting73* a node will result in reseting the selection of the JTextComponent.74* This also becomes a CaretListener to know when the selection has changed75* in the text to update the selected item in the tree.76*77* @author Scott Violet78*/79@SuppressWarnings("serial")80public class ElementTreePanel extends JPanel implements CaretListener,81DocumentListener, PropertyChangeListener, TreeSelectionListener {8283/** Tree showing the documents element structure. */84protected JTree tree;85/** Text component showing elemenst for. */86protected JTextComponent editor;87/** Model for the tree. */88protected ElementTreeModel treeModel;89/** Set to true when updatin the selection. */90protected boolean updatingSelection;9192@SuppressWarnings("LeakingThisInConstructor")93public ElementTreePanel(JTextComponent editor) {94this.editor = editor;9596Document document = editor.getDocument();9798// Create the tree.99treeModel = new ElementTreeModel(document);100tree = new JTree(treeModel) {101102@Override103public String convertValueToText(Object value, boolean selected,104boolean expanded, boolean leaf,105int row, boolean hasFocus) {106// Should only happen for the root107if (!(value instanceof Element)) {108return value.toString();109}110111Element e = (Element) value;112AttributeSet as = e.getAttributes().copyAttributes();113String asString;114115if (as != null) {116StringBuilder retBuffer = new StringBuilder("[");117Enumeration<?> names = as.getAttributeNames();118119while (names.hasMoreElements()) {120Object nextName = names.nextElement();121122if (nextName != StyleConstants.ResolveAttribute) {123retBuffer.append(" ");124retBuffer.append(nextName);125retBuffer.append("=");126retBuffer.append(as.getAttribute(nextName));127}128}129retBuffer.append(" ]");130asString = retBuffer.toString();131} else {132asString = "[ ]";133}134135if (e.isLeaf()) {136return e.getName() + " [" + e.getStartOffset() + ", " + e.137getEndOffset() + "] Attributes: " + asString;138}139return e.getName() + " [" + e.getStartOffset() + ", " + e.140getEndOffset() + "] Attributes: " + asString;141}142};143tree.addTreeSelectionListener(this);144tree.setDragEnabled(true);145// Don't show the root, it is fake.146tree.setRootVisible(false);147// Since the display value of every node after the insertion point148// changes every time the text changes and we don't generate a change149// event for all those nodes the display value can become off.150// This can be seen as '...' instead of the complete string value.151// This is a temporary workaround, increase the needed size by 15,152// hoping that will be enough.153tree.setCellRenderer(new DefaultTreeCellRenderer() {154155@Override156public Dimension getPreferredSize() {157Dimension retValue = super.getPreferredSize();158if (retValue != null) {159retValue.width += 15;160}161return retValue;162}163});164// become a listener on the document to update the tree.165document.addDocumentListener(this);166167// become a PropertyChangeListener to know when the Document has168// changed.169editor.addPropertyChangeListener(this);170171// Become a CaretListener172editor.addCaretListener(this);173174// configure the panel and frame containing it.175setLayout(new BorderLayout());176add(new JScrollPane(tree), BorderLayout.CENTER);177178// Add a label above tree to describe what is being shown179JLabel label = new JLabel("Elements that make up the current document",180SwingConstants.CENTER);181182label.setFont(new Font("Dialog", Font.BOLD, 14));183add(label, BorderLayout.NORTH);184185setPreferredSize(new Dimension(400, 400));186}187188/**189* Resets the JTextComponent to <code>editor</code>. This will update190* the tree accordingly.191*/192public void setEditor(JTextComponent editor) {193if (this.editor == editor) {194return;195}196197if (this.editor != null) {198Document oldDoc = this.editor.getDocument();199200oldDoc.removeDocumentListener(this);201this.editor.removePropertyChangeListener(this);202this.editor.removeCaretListener(this);203}204this.editor = editor;205if (editor == null) {206treeModel = null;207tree.setModel(null);208} else {209Document newDoc = editor.getDocument();210211newDoc.addDocumentListener(this);212editor.addPropertyChangeListener(this);213editor.addCaretListener(this);214treeModel = new ElementTreeModel(newDoc);215tree.setModel(treeModel);216}217}218219// PropertyChangeListener220/**221* Invoked when a property changes. We are only interested in when the222* Document changes to reset the DocumentListener.223*/224public void propertyChange(PropertyChangeEvent e) {225if (e.getSource() == getEditor() && e.getPropertyName().equals(226"document")) {227Document oldDoc = (Document) e.getOldValue();228Document newDoc = (Document) e.getNewValue();229230// Reset the DocumentListener231oldDoc.removeDocumentListener(this);232newDoc.addDocumentListener(this);233234// Recreate the TreeModel.235treeModel = new ElementTreeModel(newDoc);236tree.setModel(treeModel);237}238}239240// DocumentListener241/**242* Gives notification that there was an insert into the document. The243* given range bounds the freshly inserted region.244*245* @param e the document event246*/247public void insertUpdate(DocumentEvent e) {248updateTree(e);249}250251/**252* Gives notification that a portion of the document has been253* removed. The range is given in terms of what the view last254* saw (that is, before updating sticky positions).255*256* @param e the document event257*/258public void removeUpdate(DocumentEvent e) {259updateTree(e);260}261262/**263* Gives notification that an attribute or set of attributes changed.264*265* @param e the document event266*/267public void changedUpdate(DocumentEvent e) {268updateTree(e);269}270271// CaretListener272/**273* Messaged when the selection in the editor has changed. Will update274* the selection in the tree.275*/276public void caretUpdate(CaretEvent e) {277if (!updatingSelection) {278int selBegin = Math.min(e.getDot(), e.getMark());279int end = Math.max(e.getDot(), e.getMark());280List<TreePath> paths = new ArrayList<TreePath>();281TreeModel model = getTreeModel();282Object root = model.getRoot();283int rootCount = model.getChildCount(root);284285// Build an array of all the paths to all the character elements286// in the selection.287for (int counter = 0; counter < rootCount; counter++) {288int start = selBegin;289290while (start <= end) {291TreePath path = getPathForIndex(start, root,292(Element) model.getChild(root, counter));293Element charElement = (Element) path.getLastPathComponent();294295paths.add(path);296if (start >= charElement.getEndOffset()) {297start++;298} else {299start = charElement.getEndOffset();300}301}302}303304// If a path was found, select it (them).305int numPaths = paths.size();306307if (numPaths > 0) {308TreePath[] pathArray = new TreePath[numPaths];309310paths.toArray(pathArray);311updatingSelection = true;312try {313getTree().setSelectionPaths(pathArray);314getTree().scrollPathToVisible(pathArray[0]);315} finally {316updatingSelection = false;317}318}319}320}321322// TreeSelectionListener323/**324* Called whenever the value of the selection changes.325* @param e the event that characterizes the change.326*/327public void valueChanged(TreeSelectionEvent e) {328329if (!updatingSelection && tree.getSelectionCount() == 1) {330TreePath selPath = tree.getSelectionPath();331Object lastPathComponent = selPath.getLastPathComponent();332333if (!(lastPathComponent instanceof DefaultMutableTreeNode)) {334Element selElement = (Element) lastPathComponent;335336updatingSelection = true;337try {338getEditor().select(selElement.getStartOffset(),339selElement.getEndOffset());340} finally {341updatingSelection = false;342}343}344}345}346347// Local methods348/**349* @return tree showing elements.350*/351protected JTree getTree() {352return tree;353}354355/**356* @return JTextComponent showing elements for.357*/358protected JTextComponent getEditor() {359return editor;360}361362/**363* @return TreeModel implementation used to represent the elements.364*/365public DefaultTreeModel getTreeModel() {366return treeModel;367}368369/**370* Updates the tree based on the event type. This will invoke either371* updateTree with the root element, or handleChange.372*/373protected void updateTree(DocumentEvent event) {374updatingSelection = true;375try {376TreeModel model = getTreeModel();377Object root = model.getRoot();378379for (int counter = model.getChildCount(root) - 1; counter >= 0;380counter--) {381updateTree(event, (Element) model.getChild(root, counter));382}383} finally {384updatingSelection = false;385}386}387388/**389* Creates TreeModelEvents based on the DocumentEvent and messages390* the treemodel. This recursively invokes this method with children391* elements.392* @param event indicates what elements in the tree hierarchy have393* changed.394* @param element Current element to check for changes against.395*/396protected void updateTree(DocumentEvent event, Element element) {397DocumentEvent.ElementChange ec = event.getChange(element);398399if (ec != null) {400Element[] removed = ec.getChildrenRemoved();401Element[] added = ec.getChildrenAdded();402int startIndex = ec.getIndex();403404// Check for removed.405if (removed != null && removed.length > 0) {406int[] indices = new int[removed.length];407408for (int counter = 0; counter < removed.length; counter++) {409indices[counter] = startIndex + counter;410}411getTreeModel().nodesWereRemoved((TreeNode) element, indices,412removed);413}414// check for added415if (added != null && added.length > 0) {416int[] indices = new int[added.length];417418for (int counter = 0; counter < added.length; counter++) {419indices[counter] = startIndex + counter;420}421getTreeModel().nodesWereInserted((TreeNode) element, indices);422}423}424if (!element.isLeaf()) {425int startIndex = element.getElementIndex(event.getOffset());426int elementCount = element.getElementCount();427int endIndex = Math.min(elementCount - 1,428element.getElementIndex(event.getOffset()429+ event.getLength()));430431if (startIndex > 0 && startIndex < elementCount && element.432getElement(startIndex).getStartOffset() == event.getOffset()) {433// Force checking the previous element.434startIndex--;435}436if (startIndex != -1 && endIndex != -1) {437for (int counter = startIndex; counter <= endIndex; counter++) {438updateTree(event, element.getElement(counter));439}440}441} else {442// Element is a leaf, assume it changed443getTreeModel().nodeChanged((TreeNode) element);444}445}446447/**448* Returns a TreePath to the element at <code>position</code>.449*/450protected TreePath getPathForIndex(int position, Object root,451Element rootElement) {452TreePath path = new TreePath(root);453Element child = rootElement.getElement(rootElement.getElementIndex(454position));455456path = path.pathByAddingChild(rootElement);457path = path.pathByAddingChild(child);458while (!child.isLeaf()) {459child = child.getElement(child.getElementIndex(position));460path = path.pathByAddingChild(child);461}462return path;463}464465466/**467* ElementTreeModel is an implementation of TreeModel to handle displaying468* the Elements from a Document. AbstractDocument.AbstractElement is469* the default implementation used by the swing text package to implement470* Element, and it implements TreeNode. This makes it trivial to create471* a DefaultTreeModel rooted at a particular Element from the Document.472* Unfortunately each Document can have more than one root Element.473* Implying that to display all the root elements as a child of another474* root a fake node has be created. This class creates a fake node as475* the root with the children being the root elements of the Document476* (getRootElements).477* <p>This subclasses DefaultTreeModel. The majority of the TreeModel478* methods have been subclassed, primarily to special case the root.479*/480public static class ElementTreeModel extends DefaultTreeModel {481482protected Element[] rootElements;483484public ElementTreeModel(Document document) {485super(new DefaultMutableTreeNode("root"), false);486rootElements = document.getRootElements();487}488489/**490* Returns the child of <I>parent</I> at index <I>index</I> in491* the parent's child array. <I>parent</I> must be a node492* previously obtained from this data source. This should493* not return null if <i>index</i> is a valid index for494* <i>parent</i> (that is <i>index</i> >= 0 && <i>index</i>495* < getChildCount(<i>parent</i>)).496*497* @param parent a node in the tree, obtained from this data source498* @return the child of <I>parent</I> at index <I>index</I>499*/500@Override501public Object getChild(Object parent, int index) {502if (parent == root) {503return rootElements[index];504}505return super.getChild(parent, index);506}507508/**509* Returns the number of children of <I>parent</I>. Returns 0510* if the node is a leaf or if it has no children.511* <I>parent</I> must be a node previously obtained from this512* data source.513*514* @param parent a node in the tree, obtained from this data source515* @return the number of children of the node <I>parent</I>516*/517@Override518public int getChildCount(Object parent) {519if (parent == root) {520return rootElements.length;521}522return super.getChildCount(parent);523}524525/**526* Returns true if <I>node</I> is a leaf. It is possible for527* this method to return false even if <I>node</I> has no528* children. A directory in a filesystem, for example, may529* contain no files; the node representing the directory is530* not a leaf, but it also has no children.531*532* @param node a node in the tree, obtained from this data source533* @return true if <I>node</I> is a leaf534*/535@Override536public boolean isLeaf(Object node) {537if (node == root) {538return false;539}540return super.isLeaf(node);541}542543/**544* Returns the index of child in parent.545*/546@Override547public int getIndexOfChild(Object parent, Object child) {548if (parent == root) {549for (int counter = rootElements.length - 1; counter >= 0;550counter--) {551if (rootElements[counter] == child) {552return counter;553}554}555return -1;556}557return super.getIndexOfChild(parent, child);558}559560/**561* Invoke this method after you've changed how node is to be562* represented in the tree.563*/564@Override565public void nodeChanged(TreeNode node) {566if (listenerList != null && node != null) {567TreeNode parent = node.getParent();568569if (parent == null && node != root) {570parent = root;571}572if (parent != null) {573int anIndex = getIndexOfChild(parent, node);574575if (anIndex != -1) {576int[] cIndexs = new int[1];577578cIndexs[0] = anIndex;579nodesChanged(parent, cIndexs);580}581}582}583}584585/**586* Returns the path to a particluar node. This is recursive.587*/588@Override589protected TreeNode[] getPathToRoot(TreeNode aNode, int depth) {590TreeNode[] retNodes;591592/* Check for null, in case someone passed in a null node, or593they passed in an element that isn't rooted at root. */594if (aNode == null) {595if (depth == 0) {596return null;597} else {598retNodes = new TreeNode[depth];599}600} else {601depth++;602if (aNode == root) {603retNodes = new TreeNode[depth];604} else {605TreeNode parent = aNode.getParent();606607if (parent == null) {608parent = root;609}610retNodes = getPathToRoot(parent, depth);611}612retNodes[retNodes.length - depth] = aNode;613}614return retNodes;615}616}617}618619620