/*
* @(#)Notepad.java 1.31 05/11/17
*
* Copyright (c) 2006 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.
*/
/*
* @(#)Notepad.java 1.31 05/11/17
*/
import java.awt.*;
import java.awt.event.*;
import java.beans.*;
import java.io.*;
import java.net.URL;
import java.util.*;
import javax.swing.text.*;
import javax.swing.undo.*;
import javax.swing.event.*;
import javax.swing.*;
/**
* Sample application using the simple text editor component that
* supports only one font.
*
* @author Timothy Prinzing
* @version 1.31 11/17/05
*/
class Notepad extends JPanel {
private static ResourceBundle resources;
private final static String EXIT_AFTER_PAINT = new String("-exit");
private static boolean exitAfterFirstPaint;
static {
try {
resources = ResourceBundle.getBundle("resources.Notepad",
Locale.getDefault());
} catch (MissingResourceException mre) {
System.err.println("resources/Notepad.properties not found");
System.exit(1);
}
}
public void paintChildren(Graphics g) {
super.paintChildren(g);
if (exitAfterFirstPaint) {
System.exit(0);
}
}
Notepad() {
super(true);
// Force SwingSet to come up in the Cross Platform L&F
try {
UIManager.setLookAndFeel(UIManager.getCrossPlatformLookAndFeelClassName());
// If you want the System L&F instead, comment out the above line and
// uncomment the following:
// UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (Exception exc) {
System.err.println("Error loading L&F: " + exc);
}
setBorder(BorderFactory.createEtchedBorder());
setLayout(new BorderLayout());
// create the embedded JTextComponent
editor = createEditor();
// Add this as a listener for undoable edits.
editor.getDocument().addUndoableEditListener(undoHandler);
// install the command table
commands = new Hashtable();
Action[] actions = getActions();
for (int i = 0; i < actions.length; i++) {
Action a = actions[i];
//commands.put(a.getText(Action.NAME), a);
commands.put(a.getValue(Action.NAME), a);
}
JScrollPane scroller = new JScrollPane();
JViewport port = scroller.getViewport();
port.add(editor);
try {
String vpFlag = resources.getString("ViewportBackingStore");
Boolean bs = Boolean.valueOf(vpFlag);
port.setBackingStoreEnabled(bs.booleanValue());
} catch (MissingResourceException mre) {
// just use the viewport default
}
menuItems = new Hashtable();
JPanel panel = new JPanel();
panel.setLayout(new BorderLayout());
panel.add("North",createToolbar());
panel.add("Center", scroller);
add("Center", panel);
add("South", createStatusbar());
}
public static void main(String[] args) {
try {
String vers = System.getProperty("java.version");
if (vers.compareTo("1.1.2") < 0) {
System.out.println("!!!WARNING: Swing must be run with a " +
"1.1.2 or higher version VM!!!");
}
if (args.length > 0 && args[0].equals(EXIT_AFTER_PAINT)) {
exitAfterFirstPaint = true;
}
JFrame frame = new JFrame();
frame.setTitle(resources.getString("Title"));
frame.setBackground(Color.lightGray);
frame.getContentPane().setLayout(new BorderLayout());
Notepad notepad = new Notepad();
frame.getContentPane().add("Center", notepad);
frame.setJMenuBar(notepad.createMenubar());
frame.addWindowListener(new AppCloser());
frame.pack();
frame.setSize(500, 600);
frame.show();
} catch (Throwable t) {
System.out.println("uncaught exception: " + t);
t.printStackTrace();
}
}
/**
* Fetch the list of actions supported by this
* editor. It is implemented to return the list
* of actions supported by the embedded JTextComponent
* augmented with the actions defined locally.
*/
public Action[] getActions() {
return TextAction.augmentList(editor.getActions(), defaultActions);
}
/**
* Create an editor to represent the given document.
*/
protected JTextComponent createEditor() {
JTextComponent c = new JTextArea();
c.setDragEnabled(true);
c.setFont(new Font("monospaced", Font.PLAIN, 12));
return c;
}
/**
* Fetch the editor contained in this panel
*/
protected JTextComponent getEditor() {
return editor;
}
/**
* To shutdown when run as an application. This is a
* fairly lame implementation. A more self-respecting
* implementation would at least check to see if a save
* was needed.
*/
protected static final class AppCloser extends WindowAdapter {
public void windowClosing(WindowEvent e) {
System.exit(0);
}
}
/**
* Find the hosting frame, for the file-chooser dialog.
*/
protected Frame getFrame() {
for (Container p = getParent(); p != null; p = p.getParent()) {
if (p instanceof Frame) {
return (Frame) p;
}
}
return null;
}
/**
* This is the hook through which all menu items are
* created. It registers the result with the menuitem
* hashtable so that it can be fetched with getMenuItem().
* @see #getMenuItem
*/
protected JMenuItem createMenuItem(String cmd) {
JMenuItem mi = new JMenuItem(getResourceString(cmd + labelSuffix));
URL url = getResource(cmd + imageSuffix);
if (url != null) {
mi.setHorizontalTextPosition(JButton.RIGHT);
mi.setIcon(new ImageIcon(url));
}
String astr = getResourceString(cmd + actionSuffix);
if (astr == null) {
astr = cmd;
}
mi.setActionCommand(astr);
Action a = getAction(astr);
if (a != null) {
mi.addActionListener(a);
a.addPropertyChangeListener(createActionChangeListener(mi));
mi.setEnabled(a.isEnabled());
} else {
mi.setEnabled(false);
}
menuItems.put(cmd, mi);
return mi;
}
/**
* Fetch the menu item that was created for the given
* command.
* @param cmd Name of the action.
* @returns item created for the given command or null
* if one wasn't created.
*/
protected JMenuItem getMenuItem(String cmd) {
return (JMenuItem) menuItems.get(cmd);
}
protected Action getAction(String cmd) {
return (Action) commands.get(cmd);
}
protected String getResourceString(String nm) {
String str;
try {
str = resources.getString(nm);
} catch (MissingResourceException mre) {
str = null;
}
return str;
}
protected URL getResource(String key) {
String name = getResourceString(key);
if (name != null) {
URL url = this.getClass().getResource(name);
return url;
}
return null;
}
protected Container getToolbar() {
return toolbar;
}
protected JMenuBar getMenubar() {
return menubar;
}
/**
* Create a status bar
*/
protected Component createStatusbar() {
// need to do something reasonable here
status = new StatusBar();
return status;
}
/**
* Resets the undo manager.
*/
protected void resetUndoManager() {
undo.discardAllEdits();
undoAction.update();
redoAction.update();
}
/**
* Create the toolbar. By default this reads the
* resource file for the definition of the toolbar.
*/
private Component createToolbar() {
toolbar = new JToolBar();
String[] toolKeys = tokenize(getResourceString("toolbar"));
for (int i = 0; i < toolKeys.length; i++) {
if (toolKeys[i].equals("-")) {
toolbar.add(Box.createHorizontalStrut(5));
} else {
toolbar.add(createTool(toolKeys[i]));
}
}
toolbar.add(Box.createHorizontalGlue());
return toolbar;
}
/**
* Hook through which every toolbar item is created.
*/
protected Component createTool(String key) {
return createToolbarButton(key);
}
/**
* Create a button to go inside of the toolbar. By default this
* will load an image resource. The image filename is relative to
* the classpath (including the '.' directory if its a part of the
* classpath), and may either be in a JAR file or a separate file.
*
* @param key The key in the resource file to serve as the basis
* of lookups.
*/
protected JButton createToolbarButton(String key) {
URL url = getResource(key + imageSuffix);
JButton b = new JButton(new ImageIcon(url)) {
public float getAlignmentY() { return 0.5f; }
};
b.setRequestFocusEnabled(false);
b.setMargin(new Insets(1,1,1,1));
String astr = getResourceString(key + actionSuffix);
if (astr == null) {
astr = key;
}
Action a = getAction(astr);
if (a != null) {
b.setActionCommand(astr);
b.addActionListener(a);
} else {
b.setEnabled(false);
}
String tip = getResourceString(key + tipSuffix);
if (tip != null) {
b.setToolTipText(tip);
}
return b;
}
/**
* Take the given string and chop it up into a series
* of strings on whitespace boundaries. This is useful
* for trying to get an array of strings out of the
* resource file.
*/
protected String[] tokenize(String input) {
Vector v = new Vector();
StringTokenizer t = new StringTokenizer(input);
String cmd[];
while (t.hasMoreTokens())
v.addElement(t.nextToken());
cmd = new String[v.size()];
for (int i = 0; i < cmd.length; i++)
cmd[i] = (String) v.elementAt(i);
return cmd;
}
/**
* Create the menubar for the app. By default this pulls the
* definition of the menu from the associated resource file.
*/
protected JMenuBar createMenubar() {
JMenuItem mi;
JMenuBar mb = new JMenuBar();
String[] menuKeys = tokenize(getResourceString("menubar"));
for (int i = 0; i < menuKeys.length; i++) {
JMenu m = createMenu(menuKeys[i]);
if (m != null) {
mb.add(m);
}
}
this.menubar = mb;
return mb;
}
/**
* Create a menu for the app. By default this pulls the
* definition of the menu from the associated resource file.
*/
protected JMenu createMenu(String key) {
String[] itemKeys = tokenize(getResourceString(key));
JMenu menu = new JMenu(getResourceString(key + "Label"));
for (int i = 0; i < itemKeys.length; i++) {
if (itemKeys[i].equals("-")) {
menu.addSeparator();
} else {
JMenuItem mi = createMenuItem(itemKeys[i]);
menu.add(mi);
}
}
return menu;
}
// Yarked from JMenu, ideally this would be public.
protected PropertyChangeListener createActionChangeListener(JMenuItem b) {
return new ActionChangedListener(b);
}
// Yarked from JMenu, ideally this would be public.
private class ActionChangedListener implements PropertyChangeListener {
JMenuItem menuItem;
ActionChangedListener(JMenuItem mi) {
super();
this.menuItem = mi;
}
public void propertyChange(PropertyChangeEvent e) {
String propertyName = e.getPropertyName();
if (e.getPropertyName().equals(Action.NAME)) {
String text = (String) e.getNewValue();
menuItem.setText(text);
} else if (propertyName.equals("enabled")) {
Boolean enabledState = (Boolean) e.getNewValue();
menuItem.setEnabled(enabledState.booleanValue());
}
}
}
private JTextComponent editor;
private Hashtable commands;
private Hashtable menuItems;
private JMenuBar menubar;
private JToolBar toolbar;
private JComponent status;
private JFrame elementTreeFrame;
protected ElementTreePanel elementTreePanel;
protected FileDialog fileDialog;
/**
* Listener for the edits on the current document.
*/
protected UndoableEditListener undoHandler = new UndoHandler();
/** UndoManager that we add edits to. */
protected UndoManager undo = new UndoManager();
/**
* Suffix applied to the key used in resource file
* lookups for an image.
*/
public static final String imageSuffix = "Image";
/**
* Suffix applied to the key used in resource file
* lookups for a label.
*/
public static final String labelSuffix = "Label";
/**
* Suffix applied to the key used in resource file
* lookups for an action.
*/
public static final String actionSuffix = "Action";
/**
* Suffix applied to the key used in resource file
* lookups for tooltip text.
*/
public static final String tipSuffix = "Tooltip";
public static final String openAction = "open";
public static final String newAction = "new";
public static final String saveAction = "save";
public static final String exitAction = "exit";
public static final String showElementTreeAction = "showElementTree";
class UndoHandler implements UndoableEditListener {
/**
* Messaged when the Document has created an edit, the edit is
* added to <code>undo</code>, an instance of UndoManager.
*/
public void undoableEditHappened(UndoableEditEvent e) {
undo.addEdit(e.getEdit());
undoAction.update();
redoAction.update();
}
}
/**
* FIXME - I'm not very useful yet
*/
class StatusBar extends JComponent {
public StatusBar() {
super();
setLayout(new BoxLayout(this, BoxLayout.X_AXIS));
}
public void paint(Graphics g) {
super.paint(g);
}
}
// --- action implementations -----------------------------------
private UndoAction undoAction = new UndoAction();
private RedoAction redoAction = new RedoAction();
/**
* Actions defined by the Notepad class
*/
private Action[] defaultActions = {
new NewAction(),
new OpenAction(),
new SaveAction(),
new ExitAction(),
new ShowElementTreeAction(),
undoAction,
redoAction
};
class UndoAction extends AbstractAction {
public UndoAction() {
super("Undo");
setEnabled(false);
}
public void actionPerformed(ActionEvent e) {
try {
undo.undo();
} catch (CannotUndoException ex) {
System.out.println("Unable to undo: " + ex);
ex.printStackTrace();
}
update();
redoAction.update();
}
protected void update() {
if(undo.canUndo()) {
setEnabled(true);
putValue(Action.NAME, undo.getUndoPresentationName());
}
else {
setEnabled(false);
putValue(Action.NAME, "Undo");
}
}
}
class RedoAction extends AbstractAction {
public RedoAction() {
super("Redo");
setEnabled(false);
}
public void actionPerformed(ActionEvent e) {
try {
undo.redo();
} catch (CannotRedoException ex) {
System.out.println("Unable to redo: " + ex);
ex.printStackTrace();
}
update();
undoAction.update();
}
protected void update() {
if(undo.canRedo()) {
setEnabled(true);
putValue(Action.NAME, undo.getRedoPresentationName());
}
else {
setEnabled(false);
putValue(Action.NAME, "Redo");
}
}
}
class OpenAction extends NewAction {
OpenAction() {
super(openAction);
}
public void actionPerformed(ActionEvent e) {
Frame frame = getFrame();
JFileChooser chooser = new JFileChooser();
int ret = chooser.showOpenDialog(frame);
if (ret != JFileChooser.APPROVE_OPTION) {
return;
}
File f = chooser.getSelectedFile();
if (f.isFile() && f.canRead()) {
Document oldDoc = getEditor().getDocument();
if(oldDoc != null)
oldDoc.removeUndoableEditListener(undoHandler);
if (elementTreePanel != null) {
elementTreePanel.setEditor(null);
}
getEditor().setDocument(new PlainDocument());
frame.setTitle(f.getName());
Thread loader = new FileLoader(f, editor.getDocument());
loader.start();
} else {
JOptionPane.showMessageDialog(getFrame(),
"Could not open file: " + f,
"Error opening file",
JOptionPane.ERROR_MESSAGE);
}
}
}
class SaveAction extends AbstractAction {
SaveAction() {
super(saveAction);
}
public void actionPerformed(ActionEvent e) {
Frame frame = getFrame();
JFileChooser chooser = new JFileChooser();
int ret = chooser.showSaveDialog(frame);
if (ret != JFileChooser.APPROVE_OPTION) {
return;
}
File f = chooser.getSelectedFile();
frame.setTitle(f.getName());
Thread saver = new FileSaver(f, editor.getDocument());
saver.start();
}
}
class NewAction extends AbstractAction {
NewAction() {
super(newAction);
}
NewAction(String nm) {
super(nm);
}
public void actionPerformed(ActionEvent e) {
Document oldDoc = getEditor().getDocument();
if(oldDoc != null)
oldDoc.removeUndoableEditListener(undoHandler);
getEditor().setDocument(new PlainDocument());
getEditor().getDocument().addUndoableEditListener(undoHandler);
resetUndoManager();
getFrame().setTitle(resources.getString("Title"));
revalidate();
}
}
/**
* Really lame implementation of an exit command
*/
class ExitAction extends AbstractAction {
ExitAction() {
super(exitAction);
}
public void actionPerformed(ActionEvent e) {
System.exit(0);
}
}
/**
* Action that brings up a JFrame with a JTree showing the structure
* of the document.
*/
class ShowElementTreeAction extends AbstractAction {
ShowElementTreeAction() {
super(showElementTreeAction);
}
ShowElementTreeAction(String nm) {
super(nm);
}
public void actionPerformed(ActionEvent e) {
if(elementTreeFrame == null) {
// Create a frame containing an instance of
// ElementTreePanel.
try {
String title = resources.getString
("ElementTreeFrameTitle");
elementTreeFrame = new JFrame(title);
} catch (MissingResourceException mre) {
elementTreeFrame = new JFrame();
}
elementTreeFrame.addWindowListener(new WindowAdapter() {
public void windowClosing(WindowEvent weeee) {
elementTreeFrame.setVisible(false);
}
});
Container fContentPane = elementTreeFrame.getContentPane();
fContentPane.setLayout(new BorderLayout());
elementTreePanel = new ElementTreePanel(getEditor());
fContentPane.add(elementTreePanel);
elementTreeFrame.pack();
}
elementTreeFrame.show();
}
}
/**
* Thread to load a file into the text storage model
*/
class FileLoader extends Thread {
FileLoader(File f, Document doc) {
setPriority(4);
this.f = f;
this.doc = doc;
}
public void run() {
try {
// initialize the statusbar
status.removeAll();
JProgressBar progress = new JProgressBar();
progress.setMinimum(0);
progress.setMaximum((int) f.length());
status.add(progress);
status.revalidate();
// try to start reading
Reader in = new FileReader(f);
char[] buff = new char[4096];
int nch;
while ((nch = in.read(buff, 0, buff.length)) != -1) {
doc.insertString(doc.getLength(), new String(buff, 0, nch), null);
progress.setValue(progress.getValue() + nch);
}
}
catch (IOException e) {
final String msg = e.getMessage();
SwingUtilities.invokeLater(new Runnable() {
public void run() {
JOptionPane.showMessageDialog(getFrame(),
"Could not open file: " + msg,
"Error opening file",
JOptionPane.ERROR_MESSAGE);
}
});
}
catch (BadLocationException e) {
System.err.println(e.getMessage());
}
doc.addUndoableEditListener(undoHandler);
// we are done... get rid of progressbar
status.removeAll();
status.revalidate();
resetUndoManager();
if (elementTreePanel != null) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
elementTreePanel.setEditor(getEditor());
}
});
}
}
Document doc;
File f;
}
/**
* Thread to save a document to file
*/
class FileSaver extends Thread {
Document doc;
File f;
FileSaver(File f, Document doc) {
setPriority(4);
this.f = f;
this.doc = doc;
}
public void run() {
try {
// initialize the statusbar
status.removeAll();
JProgressBar progress = new JProgressBar();
progress.setMinimum(0);
progress.setMaximum((int) doc.getLength());
status.add(progress);
status.revalidate();
// start writing
Writer out = new FileWriter(f);
Segment text = new Segment();
text.setPartialReturn(true);
int charsLeft = doc.getLength();
int offset = 0;
while (charsLeft > 0) {
doc.getText(offset, Math.min(4096, charsLeft), text);
out.write(text.array, text.offset, text.count);
charsLeft -= text.count;
offset += text.count;
progress.setValue(offset);
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
out.flush();
out.close();
}
catch (IOException e) {
final String msg = e.getMessage();
SwingUtilities.invokeLater(new Runnable() {
public void run() {
JOptionPane.showMessageDialog(getFrame(),
"Could not save file: " + msg,
"Error saving file",
JOptionPane.ERROR_MESSAGE);
}
});
}
catch (BadLocationException e) {
System.err.println(e.getMessage());
}
// we are done... get rid of progressbar
status.removeAll();
status.revalidate();
}
}
}
/*
* @(#)ElementTreePanel.java 1.17 05/11/17
*
* Copyright (c) 2006 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.17 05/11/17
*/
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
* @version 1.17 11/17/05
*/
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;
}
}
}
|