Index: trunk/src/spikes/DocumentSizeFilter.java
===================================================================
--- trunk/src/spikes/DocumentSizeFilter.java	(revision 416)
+++ trunk/src/spikes/DocumentSizeFilter.java	(revision 416)
@@ -0,0 +1,51 @@
+
+package spikes;
+
+import javax.swing.*;
+import javax.swing.text.*;
+import java.awt.Toolkit;
+
+public class DocumentSizeFilter extends DocumentFilter {
+    int maxCharacters;
+    boolean DEBUG = false;
+
+    public DocumentSizeFilter(int maxChars) {
+        maxCharacters = maxChars;
+    }
+
+    public void insertString(FilterBypass fb, int offs,
+                             String str, AttributeSet a)
+        throws BadLocationException {
+        if (DEBUG) {
+            System.out.println("in DocumentSizeFilter's insertString method");
+        }
+
+        //This rejects the entire insertion if it would make
+        //the contents too long. Another option would be
+        //to truncate the inserted string so the contents
+        //would be exactly maxCharacters in length.
+        if ((fb.getDocument().getLength() + str.length()) <= maxCharacters)
+            super.insertString(fb, offs, str, a);
+        else
+            Toolkit.getDefaultToolkit().beep();
+    }
+    
+    public void replace(FilterBypass fb, int offs,
+                        int length, 
+                        String str, AttributeSet a)
+        throws BadLocationException {
+        if (DEBUG) {
+            System.out.println("in DocumentSizeFilter's replace method");
+        }
+        //This rejects the entire replacement if it would make
+        //the contents too long. Another option would be
+        //to truncate the replacement string so the contents
+        //would be exactly maxCharacters in length.
+        if ((fb.getDocument().getLength() + str.length()
+             - length) <= maxCharacters)
+            super.replace(fb, offs, length, str, a);
+        else
+            Toolkit.getDefaultToolkit().beep();
+    }
+
+}
Index: trunk/src/spikes/TextComponentDemo.java
===================================================================
--- trunk/src/spikes/TextComponentDemo.java	(revision 416)
+++ trunk/src/spikes/TextComponentDemo.java	(revision 416)
@@ -0,0 +1,422 @@
+
+package spikes;
+
+
+import java.awt.*;
+import java.awt.event.*;
+import java.util.HashMap;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import javax.swing.*;
+import javax.swing.text.*;
+import javax.swing.event.*;
+import javax.swing.undo.*;
+
+public class TextComponentDemo extends JFrame {
+    JTextPane textPane;
+    AbstractDocument doc;
+    static final int MAX_CHARACTERS = 300;
+    JTextArea changeLog;
+    String newline = "\n";
+    HashMap<Object, Action> actions;
+
+    //undo helpers
+    protected UndoAction undoAction;
+    protected RedoAction redoAction;
+    protected UndoManager undo = new UndoManager();
+    private ClearAction saveAction = new ClearAction();
+
+    public TextComponentDemo() {
+        super("TextComponentDemo");
+
+        //Create the text pane and configure it.
+        textPane = new JTextPane();
+        textPane.setCaretPosition(0);
+        textPane.setMargin(new Insets(5,5,5,5));
+        StyledDocument styledDoc = textPane.getStyledDocument();
+        if (styledDoc instanceof AbstractDocument) {
+            doc = (AbstractDocument)styledDoc;
+            doc.setDocumentFilter(new DocumentSizeFilter(MAX_CHARACTERS));
+        } else {
+            System.err.println("Text pane's document isn't an AbstractDocument!");
+            System.exit(-1);
+        }
+        JScrollPane scrollPane = new JScrollPane(textPane);
+        scrollPane.setPreferredSize(new Dimension(200, 200));
+
+        //Create the text area for the status log and configure it.
+        changeLog = new JTextArea(5, 30);
+        changeLog.setEditable(false);
+        JScrollPane scrollPaneForLog = new JScrollPane(changeLog);
+
+        //Create a split pane for the change log and the text area.
+        JSplitPane splitPane = new JSplitPane(
+                                       JSplitPane.VERTICAL_SPLIT,
+                                       scrollPane, scrollPaneForLog);
+        splitPane.setOneTouchExpandable(true);
+
+        //Create the status area.
+        JPanel statusPane = new JPanel(new GridLayout(1, 1));
+        CaretListenerLabel caretListenerLabel =
+                new CaretListenerLabel("Caret Status");
+        statusPane.add(caretListenerLabel);
+
+        //Add the components.
+        getContentPane().add(splitPane, BorderLayout.CENTER);
+        getContentPane().add(statusPane, BorderLayout.PAGE_END);
+
+        //Set up the menu bar.
+        actions=createActionTable(textPane);
+        JMenu editMenu = createEditMenu();
+        JMenu styleMenu = createStyleMenu();
+        JMenuBar mb = new JMenuBar();
+        mb.add(editMenu);
+        mb.add(styleMenu);
+        setJMenuBar(mb);
+
+        //Add some key bindings.
+        addBindings();
+
+        //Put the initial text into the text pane.
+        initDocument();
+        textPane.setCaretPosition(0);
+
+        //Start watching for undoable edits and caret changes.
+        doc.addUndoableEditListener(new MyUndoableEditListener());
+        textPane.addCaretListener(caretListenerLabel);
+        doc.addDocumentListener(new MyDocumentListener());
+    }
+
+    //This listens for and reports caret movements.
+    protected class CaretListenerLabel extends JLabel
+                                       implements CaretListener {
+        public CaretListenerLabel(String label) {
+            super(label);
+        }
+
+        //Might not be invoked from the event dispatch thread.
+        public void caretUpdate(CaretEvent e) {
+            displaySelectionInfo(e.getDot(), e.getMark());
+        }
+
+        //This method can be invoked from any thread.  It 
+        //invokes the setText and modelToView methods, which 
+        //must run on the event dispatch thread. We use
+        //invokeLater to schedule the code for execution
+        //on the event dispatch thread.
+        protected void displaySelectionInfo(final int dot,
+                                            final int mark) {
+            SwingUtilities.invokeLater(new Runnable() {
+                public void run() {
+                    if (dot == mark) {  // no selection
+                        try {
+                            Rectangle caretCoords = textPane.modelToView(dot);
+                            //Convert it to view coordinates.
+                            setText("caret: text position: " + dot
+                                    + ", view location = ["
+                                    + caretCoords.x + ", "
+                                    + caretCoords.y + "]"
+                                    + newline);
+                        } catch (BadLocationException ble) {
+                            setText("caret: text position: " + dot + newline);
+                        }
+                    } else if (dot < mark) {
+                        setText("selection from: " + dot
+                                + " to " + mark + newline);
+                    } else {
+                        setText("selection from: " + mark
+                                + " to " + dot + newline);
+                    }
+                }
+            });
+        }
+    }
+
+    //This one listens for edits that can be undone.
+    protected class MyUndoableEditListener
+                    implements UndoableEditListener {
+        public void undoableEditHappened(UndoableEditEvent e) {
+            //Remember the edit and update the menus.
+            undo.addEdit(e.getEdit());
+            undoAction.updateUndoState();
+            redoAction.updateRedoState();
+        }
+    }
+
+    //And this one listens for any changes to the document.
+    protected class MyDocumentListener
+                    implements DocumentListener {
+        public void insertUpdate(DocumentEvent e) {
+            displayEditInfo(e);
+        }
+        public void removeUpdate(DocumentEvent e) {
+            displayEditInfo(e);
+        }
+        public void changedUpdate(DocumentEvent e) {
+            displayEditInfo(e);
+        }
+        private void displayEditInfo(DocumentEvent e) {
+            Document document = e.getDocument();
+            int changeLength = e.getLength();
+            changeLog.append(e.getType().toString() + ": " +
+                changeLength + " character" +
+                ((changeLength == 1) ? ". " : "s. ") +
+                " Text length = " + document.getLength() +
+                "." + newline);
+        }
+    }
+
+    //Add a couple of emacs key bindings for navigation.
+    protected void addBindings() {
+        InputMap inputMap = textPane.getInputMap();
+        ActionMap actionMap = textPane.getActionMap();
+        //Ctrl-b to go backward one character
+        KeyStroke key = KeyStroke.getKeyStroke(KeyEvent.VK_B, Event.CTRL_MASK);
+        //inputMap.put(key, DefaultEditorKit.backwardAction);
+        inputMap.put(key, "myAction");
+        actionMap.put("myAction", saveAction);
+        
+        //Ctrl-f to go forward one character
+        key = KeyStroke.getKeyStroke(KeyEvent.VK_F, Event.CTRL_MASK);
+        inputMap.put(key, DefaultEditorKit.forwardAction);
+
+        //Ctrl-p to go up one line
+        key = KeyStroke.getKeyStroke(KeyEvent.VK_P, Event.CTRL_MASK);
+        inputMap.put(key, DefaultEditorKit.upAction);
+
+        //Ctrl-n to go down one line
+        key = KeyStroke.getKeyStroke(KeyEvent.VK_N, Event.CTRL_MASK);
+        inputMap.put(key, DefaultEditorKit.downAction);
+    }
+
+    //Create the edit menu.
+    protected JMenu createEditMenu() {
+        JMenu menu = new JMenu("Edit");
+
+        //Undo and redo are actions of our own creation.
+        undoAction = new UndoAction();
+        menu.add(undoAction);
+
+        redoAction = new RedoAction();
+        menu.add(redoAction);
+
+        menu.addSeparator();
+
+        //These actions come from the default editor kit.
+        //Get the ones we want and stick them in the menu.
+        menu.add(getActionByName(DefaultEditorKit.cutAction));
+        menu.add(getActionByName(DefaultEditorKit.copyAction));
+        menu.add(getActionByName(DefaultEditorKit.pasteAction));
+
+        menu.addSeparator();
+
+        menu.add(getActionByName(DefaultEditorKit.selectAllAction));
+        return menu;
+    }
+
+    //Create the style menu.
+    protected JMenu createStyleMenu() {
+        JMenu menu = new JMenu("Style");
+
+        Action action = new StyledEditorKit.BoldAction();
+        action.putValue(Action.NAME, "Bold");
+        menu.add(action);
+
+        action = new StyledEditorKit.ItalicAction();
+        action.putValue(Action.NAME, "Italic");
+        menu.add(action);
+
+        action = new StyledEditorKit.UnderlineAction();
+        action.putValue(Action.NAME, "Underline");
+        menu.add(action);
+
+        menu.addSeparator();
+
+        menu.add(new StyledEditorKit.FontSizeAction("12", 12));
+        menu.add(new StyledEditorKit.FontSizeAction("14", 14));
+        menu.add(new StyledEditorKit.FontSizeAction("18", 18));
+
+        menu.addSeparator();
+
+        menu.add(new StyledEditorKit.FontFamilyAction("Serif",
+                                                      "Serif"));
+        menu.add(new StyledEditorKit.FontFamilyAction("SansSerif",
+                                                      "SansSerif"));
+
+        menu.addSeparator();
+
+        menu.add(new StyledEditorKit.ForegroundAction("Red",
+                                                      Color.red));
+        menu.add(new StyledEditorKit.ForegroundAction("Green",
+                                                      Color.green));
+        menu.add(new StyledEditorKit.ForegroundAction("Blue",
+                                                      Color.blue));
+        menu.add(new StyledEditorKit.ForegroundAction("Black",
+                                                      Color.black));
+
+        return menu;
+    }
+
+    protected void initDocument() {
+        String initString[] =
+                { "Use the mouse to place the caret.",
+                  "Use the edit menu to cut, copy, paste, and select text.",
+                  "Also to undo and redo changes.",
+                  "Use the style menu to change the style of the text.",
+                  "Use the arrow keys on the keyboard or these emacs key bindings to move the caret:",
+                  "Ctrl-f, Ctrl-b, Ctrl-n, Ctrl-p." };
+
+        SimpleAttributeSet[] attrs = initAttributes(initString.length);
+
+        try {
+            for (int i = 0; i < initString.length; i ++) {
+                doc.insertString(doc.getLength(), initString[i] + newline,
+                        attrs[i]);
+            }
+        } catch (BadLocationException ble) {
+            System.err.println("Couldn't insert initial text.");
+        }
+    }
+
+    protected SimpleAttributeSet[] initAttributes(int length) {
+        //Hard-code some attributes.
+        SimpleAttributeSet[] attrs = new SimpleAttributeSet[length];
+
+        attrs[0] = new SimpleAttributeSet();
+        StyleConstants.setFontFamily(attrs[0], "SansSerif");
+        StyleConstants.setFontSize(attrs[0], 16);
+
+        attrs[1] = new SimpleAttributeSet(attrs[0]);
+        StyleConstants.setBold(attrs[1], true);
+
+        attrs[2] = new SimpleAttributeSet(attrs[0]);
+        StyleConstants.setItalic(attrs[2], true);
+
+        attrs[3] = new SimpleAttributeSet(attrs[0]);
+        StyleConstants.setFontSize(attrs[3], 20);
+
+        attrs[4] = new SimpleAttributeSet(attrs[0]);
+        StyleConstants.setFontSize(attrs[4], 12);
+
+        attrs[5] = new SimpleAttributeSet(attrs[0]);
+        StyleConstants.setForeground(attrs[5], Color.red);
+
+        return attrs;
+    }
+
+    //The following two methods allow us to find an
+    //action provided by the editor kit by its name.
+    private HashMap<Object, Action> createActionTable(JTextComponent textComponent) {
+        HashMap<Object, Action> actions = new HashMap<Object, Action>();
+        Action[] actionsArray = textComponent.getActions();
+        for (int i = 0; i < actionsArray.length; i++) {
+            Action a = actionsArray[i];
+            actions.put(a.getValue(Action.NAME), a);
+        }
+	return actions;
+    }
+
+    private Action getActionByName(String name) {
+        return actions.get(name);
+    }
+
+    class ClearAction extends AbstractAction 
+    {
+        public void actionPerformed(ActionEvent evt) 
+        {
+            JTextPane src = (JTextPane) evt.getSource();
+            StyledDocument doc = src.getStyledDocument();
+            int length = doc.getLength();
+            try {
+                doc.remove(0,length);
+            } catch (BadLocationException ex) {
+                Logger.getLogger(TextComponentDemo.class.getName()).log(Level.SEVERE, null, ex);
+            }
+        }
+    }
+
+    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();
+            }
+            updateUndoState();
+            redoAction.updateRedoState();
+        }
+
+        protected void updateUndoState() {
+            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();
+            }
+            updateRedoState();
+            undoAction.updateUndoState();
+        }
+
+        protected void updateRedoState() {
+            if (undo.canRedo()) {
+                setEnabled(true);
+                putValue(Action.NAME, undo.getRedoPresentationName());
+            } else {
+                setEnabled(false);
+                putValue(Action.NAME, "Redo");
+            }
+        }
+    }
+
+    /**
+     * Create the GUI and show it.  For thread safety,
+     * this method should be invoked from the
+     * event dispatch thread.
+     */
+    private static void createAndShowGUI() {
+        //Create and set up the window.
+        final TextComponentDemo frame = new TextComponentDemo();
+        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
+
+        //Display the window.
+        frame.pack();
+        frame.setVisible(true);
+    }
+
+    //The standard main method.
+    public static void main(String[] args) {
+        //Schedule a job for the event dispatch thread:
+        //creating and showing this application's GUI.
+        SwingUtilities.invokeLater(new Runnable() {
+            public void run() {
+                //Turn off metal's use of bold fonts
+	        UIManager.put("swing.boldMetal", Boolean.FALSE);
+		createAndShowGUI();
+            }
+        });
+    }
+}
