import java.awt.*;
import java.awt.event.*;
import java.io.*;
import java.util.Hashtable;
import java.net.*;

import javax.swing.*;
import javax.swing.text.*;
import javax.swing.event.*;
import javax.swing.undo.*;
import javax.swing.border.*;

public class TextComponentDemo extends JFrame {
    JTextPane textPane;
    JTextArea changeLog;
    String newline;
    static final int MAX_CHARACTERS = 200;
    Hashtable actions;

    //undo helpers
    protected UndoAction undoAction;
    protected RedoAction redoAction;
    protected UndoManager undo = new UndoManager();

    public TextComponentDemo () {

	//Some initial setup
	super("TextComponentDemo");
	newline = System.getProperty("line.separator");

	//Create the document for the text area
	LimitedStyledDocument lpd = new LimitedStyledDocument(MAX_CHARACTERS);
	lpd.addDocumentListener(new MyDocumentListener());

	//Create the text area and configure it
        textPane = new JTextPane();
        textPane.setPreferredSize(new Dimension(200, 200));
	textPane.setDocument(lpd);
	textPane.setCaretPosition(0);
        textPane.setMargin(new Insets(5,5,5,5));
	JScrollPane scrollPane = new JScrollPane(textPane);

	//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 to the frame 
        BorderLayout borderLayout = new BorderLayout();
	JPanel contentPane = new JPanel();
        contentPane.setLayout(borderLayout);
        contentPane.add(splitPane, BorderLayout.CENTER);
        contentPane.add(statusPane, BorderLayout.SOUTH);
	setContentPane(contentPane);

        //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 to the keymap
	addKeymapBindings();

	//Load the text area with text
	try {
            URL url = new URL("http://java.sun.com/docs/books/tutorial/ui/" +
                              "swing/example-swing/TextComponentDemo.html");
	    textPane.setPage(url);
	} catch (Exception e ) {
	    System.err.println("Can't open instructions");
	}

	//Start watching for undoable edits and caret changes
	lpd.addUndoableEditListener(new MyUndoableEditListener());
	textPane.addCaretListener(caretListenerLabel);
    }

    //This listens for and reports caret movements.
    protected class CaretListenerLabel extends JLabel implements CaretListener {
	public CaretListenerLabel (String label) {
	    super(label);
	}
	public void caretUpdate(CaretEvent e) {
	    //Get the location in the text
	    int dot = e.getDot();
	    int mark = e.getMark();
	    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.update();
	    redoAction.update();
	}
    }

    //And this one listens for any changes to the document.
    protected class MyDocumentListener implements DocumentListener {
        public void insertUpdate(DocumentEvent e) {
            update(e);
        }
        public void removeUpdate(DocumentEvent e) {
            update(e);
        }
        public void changedUpdate(DocumentEvent e) {
	    AbstractDocument.DefaultDocumentEvent de =
				 (AbstractDocument.DefaultDocumentEvent)e;
	    //Display the type of edit that occurred
	    changeLog.append(de.getPresentationName() + newline);
        }
        private void update(DocumentEvent e) {
	    AbstractDocument.DefaultDocumentEvent de =
				 (AbstractDocument.DefaultDocumentEvent)e;
	    //Display the type of edit that occurred and
	    //the resulting text length
	    changeLog.append(de.getPresentationName() + 
			     ": text length = " + 
	    		     e.getDocument().getLength() + newline);
        }
    }  

    //Add a couple of emacs key bindings to the key map for navigation
    protected void addKeymapBindings() {
	//Get the current, default map
	Keymap keymap = textPane.getKeymap();

	//Ctrl-b to go backward one character
	Action action = getActionByName(DefaultEditorKit.backwardAction);
	KeyStroke key = KeyStroke.getKeyStroke(KeyEvent.VK_B, Event.CTRL_MASK);
	keymap.addActionForKeyStroke(key, action);

	//Ctrl-f to go forward one character
	action = getActionByName(DefaultEditorKit.forwardAction);
	key = KeyStroke.getKeyStroke(KeyEvent.VK_F, Event.CTRL_MASK);
	keymap.addActionForKeyStroke(key, action);

	//Ctrl-p to go up one line
	action = getActionByName(DefaultEditorKit.upAction);
	key = KeyStroke.getKeyStroke(KeyEvent.VK_P, Event.CTRL_MASK);
	keymap.addActionForKeyStroke(key, action);

	//Ctrl-n to go down one line
	action = getActionByName(DefaultEditorKit.downAction);
	key = KeyStroke.getKeyStroke(KeyEvent.VK_N, Event.CTRL_MASK);
	keymap.addActionForKeyStroke(key, action);
    }

    //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.
	//We just get the ones we want and stick them in the menu.
	Action action = getActionByName(DefaultEditorKit.cutAction);
	action.putValue(Action.NAME, "Cut");
	menu.add(action);

	action = getActionByName(DefaultEditorKit.copyAction);
	action.putValue(Action.NAME, "Copy");
	menu.add(action);

	action = getActionByName(DefaultEditorKit.pasteAction);
	action.putValue(Action.NAME, "Paste");
	menu.add(action);

        menu.addSeparator();

	action = getActionByName(DefaultEditorKit.selectAllAction);
	action.putValue(Action.NAME, "Select All");
	menu.add(action);
	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();

	action = new StyledEditorKit.FontSizeAction("12", 12);
	menu.add(action);

	action = new StyledEditorKit.FontSizeAction("14", 14);
	menu.add(action);

	action = new StyledEditorKit.FontSizeAction("18", 18);
	menu.add(action);

        menu.addSeparator();

	action = new StyledEditorKit.FontFamilyAction("Serif", "Serif");
	menu.add(action);

	action = new StyledEditorKit.FontFamilyAction("SansSerif", "SansSerif");
	menu.add(action);

        menu.addSeparator();

	action = new StyledEditorKit.ForegroundAction("Red", Color.red);
	menu.add(action);

	action = new StyledEditorKit.ForegroundAction("Green", Color.green);
	menu.add(action);

	action = new StyledEditorKit.ForegroundAction("Blue", Color.blue);
	menu.add(action);

	action = new StyledEditorKit.ForegroundAction("Black", Color.black);
	menu.add(action);

	return menu;
    }

    //The following two methods allow us to find an
    //action provided by the editor kit by its name.
    private Hashtable createActionTable(JTextComponent textComponent) {
        Hashtable actions = new Hashtable();
        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 (Action)(actions.get(name));
    }

    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");
            }
        }
    }    

    //The standard main method.
    public static void main(String[] args) {

        final TextComponentDemo frame = new TextComponentDemo();

        WindowListener l = new WindowAdapter() {
            public void windowClosing(WindowEvent e) {
                System.exit(0);
            }
            public void windowActivated(WindowEvent e) {
		frame.textPane.requestFocus();
            }
        };
        frame.addWindowListener(l);

        frame.pack();
        frame.setVisible(true);
    }
}
