/*
 * InputHandler.java - Manages key bindings and executes actions
 * Copyright (C) 1999 Slava Pestov
 *
 * You may use and modify this package for any purpose. Redistribution is
 * permitted, in both source and binary form, provided that this notice
 * remains intact in all source distributions of this package.
 */

package processing.app.syntax;

import javax.swing.text.*;
import javax.swing.JPopupMenu;

import processing.app.Preferences;

import java.awt.event.*;
import java.awt.Component;
import java.util.*;

/**
 * An input handler converts the user's key strokes into concrete actions.
 * It also takes care of macro recording and action repetition.<p>
 *
 * This class provides all the necessary support code for an input
 * handler, but doesn't actually do any key binding logic. It is up
 * to the implementations of this class to do so.
 *
 * @author Slava Pestov
 * @version $Id$
 */
public abstract class InputHandler extends KeyAdapter
{
        /**
         * If this client property is set to Boolean.TRUE on the text area,
         * the home/end keys will support 'smart' BRIEF-like behaviour
         * (one press = start/end of line, two presses = start/end of
         * viewscreen, three presses = start/end of document). By default,
         * this property is not set.
         */
        public static final String SMART_HOME_END_PROPERTY = "InputHandler.homeEnd";
        
        /**
         * If this PDE property is set to Boolean.TRUE, the home/end keys will
         * go to the first/last non-whitespace character of the line. If already at
         * the that character, the keypress will move the cursor to the actual
         * start/end of the line.
         * 
         * SMART_HOME_END_PROPERTY takes precedence over this property.
         */
        public static final String CONTEXT_AWARE_HOME_END = "editor.keys.home_and_end_travel_smart";

        public static final ActionListener BACKSPACE = new backspace();
        public static final ActionListener BACKSPACE_WORD = new backspace_word();
        public static final ActionListener DELETE = new delete();
        public static final ActionListener DELETE_WORD = new delete_word();
        public static final ActionListener END = new end(false);
        public static final ActionListener DOCUMENT_END = new document_end(false);
        public static final ActionListener SELECT_END = new end(true);
        public static final ActionListener SELECT_DOC_END = new document_end(true);
        public static final ActionListener INSERT_BREAK = new insert_break();
        public static final ActionListener INSERT_TAB = new insert_tab();
        public static final ActionListener HOME = new home(false);
        public static final ActionListener DOCUMENT_HOME = new document_home(false);
        public static final ActionListener SELECT_HOME = new home(true);
        public static final ActionListener SELECT_DOC_HOME = new document_home(true);
        public static final ActionListener NEXT_CHAR = new next_char(false);
        public static final ActionListener NEXT_LINE = new next_line(false);
        public static final ActionListener NEXT_PAGE = new next_page(false);
        public static final ActionListener NEXT_WORD = new next_word(false);
        public static final ActionListener SELECT_NEXT_CHAR = new next_char(true);
        public static final ActionListener SELECT_NEXT_LINE = new next_line(true);
        public static final ActionListener SELECT_NEXT_PAGE = new next_page(true);
        public static final ActionListener SELECT_NEXT_WORD = new next_word(true);
        public static final ActionListener OVERWRITE = new overwrite();
        public static final ActionListener PREV_CHAR = new prev_char(false);
        public static final ActionListener PREV_LINE = new prev_line(false);
        public static final ActionListener PREV_PAGE = new prev_page(false);
        public static final ActionListener PREV_WORD = new prev_word(false);
        public static final ActionListener SELECT_PREV_CHAR = new prev_char(true);
        public static final ActionListener SELECT_PREV_LINE = new prev_line(true);
        public static final ActionListener SELECT_PREV_PAGE = new prev_page(true);
        public static final ActionListener SELECT_PREV_WORD = new prev_word(true);
        public static final ActionListener REPEAT = new repeat();
        public static final ActionListener TOGGLE_RECT = new toggle_rect();
        public static final ActionListener CLIPBOARD_CUT = new clipboard_cut();  // [fry]
        public static final ActionListener CLIPBOARD_COPY = new clipboard_copy();
        public static final ActionListener CLIPBOARD_PASTE = new clipboard_paste();

        // Default action
        public static final ActionListener INSERT_CHAR = new insert_char();

        private static Hashtable actions;

        static
        {
                actions = new Hashtable();
                actions.put("backspace",BACKSPACE);
                actions.put("backspace-word",BACKSPACE_WORD);
                actions.put("delete",DELETE);
                actions.put("delete-word",DELETE_WORD);
                actions.put("end",END);
                actions.put("select-end",SELECT_END);
                actions.put("document-end",DOCUMENT_END);
                actions.put("select-doc-end",SELECT_DOC_END);
                actions.put("insert-break",INSERT_BREAK);
                actions.put("insert-tab",INSERT_TAB);
                actions.put("home",HOME);
                actions.put("select-home",SELECT_HOME);
                actions.put("document-home",DOCUMENT_HOME);
                actions.put("select-doc-home",SELECT_DOC_HOME);
                actions.put("next-char",NEXT_CHAR);
                actions.put("next-line",NEXT_LINE);
                actions.put("next-page",NEXT_PAGE);
                actions.put("next-word",NEXT_WORD);
                actions.put("select-next-char",SELECT_NEXT_CHAR);
                actions.put("select-next-line",SELECT_NEXT_LINE);
                actions.put("select-next-page",SELECT_NEXT_PAGE);
                actions.put("select-next-word",SELECT_NEXT_WORD);
                actions.put("overwrite",OVERWRITE);
                actions.put("prev-char",PREV_CHAR);
                actions.put("prev-line",PREV_LINE);
                actions.put("prev-page",PREV_PAGE);
                actions.put("prev-word",PREV_WORD);
                actions.put("select-prev-char",SELECT_PREV_CHAR);
                actions.put("select-prev-line",SELECT_PREV_LINE);
                actions.put("select-prev-page",SELECT_PREV_PAGE);
                actions.put("select-prev-word",SELECT_PREV_WORD);
                actions.put("repeat",REPEAT);
                actions.put("toggle-rect",TOGGLE_RECT);
                actions.put("insert-char",INSERT_CHAR);
                actions.put("clipboard-cut",CLIPBOARD_CUT);
                actions.put("clipboard-copy",CLIPBOARD_COPY);
                actions.put("clipboard-paste",CLIPBOARD_PASTE);
        }

        /**
         * Returns a named text area action.
         * @param name The action name
         */
        public static ActionListener getAction(String name)
        {
                return (ActionListener)actions.get(name);
        }

        /**
         * Returns the name of the specified text area action.
         * @param listener The action
         */
        public static String getActionName(ActionListener listener)
        {
                Enumeration en = getActions();
                while(en.hasMoreElements())
                {
                        String name = (String)en.nextElement();
                        ActionListener _listener = getAction(name);
                        if(_listener == listener) {
                                return name;
                        }
                }
                return null;
        }

        /**
         * Returns an enumeration of all available actions.
         */
        public static Enumeration getActions()
        {
                return actions.keys();
        }

        /**
         * Adds the default key bindings to this input handler.
         * This should not be called in the constructor of this
         * input handler, because applications might load the
         * key bindings from a file, etc.
         */
        public abstract void addDefaultKeyBindings();

        /**
         * Adds a key binding to this input handler.
         * @param keyBinding The key binding (the format of this is
         * input-handler specific)
         * @param action The action
         */
        public abstract void addKeyBinding(String keyBinding, ActionListener action);

        /**
         * Removes a key binding from this input handler.
         * @param keyBinding The key binding
         */
        public abstract void removeKeyBinding(String keyBinding);

        /**
         * Removes all key bindings from this input handler.
         */
        public abstract void removeAllKeyBindings();

        /**
         * Grabs the next key typed event and invokes the specified
         * action with the key as a the action command.
         */
        public void grabNextKeyStroke(ActionListener listener)
        {
                grabAction = listener;
        }

        /**
         * Returns if repeating is enabled. When repeating is enabled,
         * actions will be executed multiple times. This is usually
         * invoked with a special key stroke in the input handler.
         */
        public boolean isRepeatEnabled()
        {
                return repeat;
        }

        /**
         * Enables repeating. When repeating is enabled, actions will be
         * executed multiple times. Once repeating is enabled, the input
         * handler should read a number from the keyboard.
         */
        public void setRepeatEnabled(boolean repeat)
        {
                this.repeat = repeat;
        }

        /**
         * Returns the number of times the next action will be repeated.
         */
        public int getRepeatCount()
        {
                return (repeat ? Math.max(1,repeatCount) : 1);
        }

        /**
         * Sets the number of times the next action will be repeated.
         * @param repeatCount The repeat count
         */
        public void setRepeatCount(int repeatCount)
        {
                this.repeatCount = repeatCount;
        }

        /**
         * Returns the macro recorder. If this is non-null, all executed
         * actions should be forwarded to the recorder.
         */
        public InputHandler.MacroRecorder getMacroRecorder()
        {
                return recorder;
        }

        /**
         * Sets the macro recorder. If this is non-null, all executed
         * actions should be forwarded to the recorder.
         * @param recorder The macro recorder
         */
        public void setMacroRecorder(InputHandler.MacroRecorder recorder)
        {
                this.recorder = recorder;
        }

        /**
         * Returns a copy of this input handler that shares the same
         * key bindings. Setting key bindings in the copy will also
         * set them in the original.
         */
        public abstract InputHandler copy();

        /**
         * Executes the specified action, repeating and recording it as
         * necessary.
         * @param listener The action listener
         * @param source The event source
         * @param actionCommand The action command
         */
        public void executeAction(ActionListener listener, Object source,
                String actionCommand)
        {
                // create event
                ActionEvent evt = new ActionEvent(source,
                        ActionEvent.ACTION_PERFORMED,
                        actionCommand);

                // don't do anything if the action is a wrapper
                // (like EditAction.Wrapper)
                if(listener instanceof Wrapper)
                {
                        listener.actionPerformed(evt);
                        return;
                }

                // remember old values, in case action changes them
                boolean _repeat = repeat;
                int _repeatCount = getRepeatCount();

                // execute the action
                if(listener instanceof InputHandler.NonRepeatable)
                        listener.actionPerformed(evt);
                else
                {
                        for(int i = 0; i < Math.max(1,repeatCount); i++)
                                listener.actionPerformed(evt);
                }

                // do recording. Notice that we do no recording whatsoever
                // for actions that grab keys
                if(grabAction == null)
                {
                        if(recorder != null)
                        {
                                if(!(listener instanceof InputHandler.NonRecordable))
                                {
                                        if(_repeatCount != 1)
                                                recorder.actionPerformed(REPEAT,String.valueOf(_repeatCount));

                                        recorder.actionPerformed(listener,actionCommand);
                                }
                        }

                        // If repeat was true originally, clear it
                        // Otherwise it might have been set by the action, etc
                        if(_repeat)
                        {
                                repeat = false;
                                repeatCount = 0;
                        }
                }
        }

        /**
         * Returns the text area that fired the specified event.
         * @param evt The event
         */
        public static JEditTextArea getTextArea(EventObject evt)
        {
                if(evt != null)
                {
                        Object o = evt.getSource();
                        if(o instanceof Component)
                        {
                                // find the parent text area
                                Component c = (Component)o;
                                for(;;)
                                {
                                        if(c instanceof JEditTextArea)
                                                return (JEditTextArea)c;
                                        else if(c == null)
                                                break;
                                        if(c instanceof JPopupMenu)
                                                c = ((JPopupMenu)c)
                                                        .getInvoker();
                                        else
                                                c = c.getParent();
                                }
                        }
                }

                // this shouldn't happen
                System.err.println("BUG: getTextArea() returning null");
                System.err.println("Report this to Slava Pestov <sp@gjt.org>");
                return null;
        }

        // protected members

        /**
         * If a key is being grabbed, this method should be called with
         * the appropriate key event. It executes the grab action with
         * the typed character as the parameter.
         */
        protected void handleGrabAction(KeyEvent evt)
        {
                // Clear it *before* it is executed so that executeAction()
                // resets the repeat count
                ActionListener _grabAction = grabAction;
                grabAction = null;
                executeAction(_grabAction,evt.getSource(),
                        String.valueOf(evt.getKeyChar()));
        }

        // protected members
        protected ActionListener grabAction;
        protected boolean repeat;
        protected int repeatCount;
        protected InputHandler.MacroRecorder recorder;

        /**
         * If an action implements this interface, it should not be repeated.
         * Instead, it will handle the repetition itself.
         */
        public interface NonRepeatable {}

        /**
         * If an action implements this interface, it should not be recorded
         * by the macro recorder. Instead, it will do its own recording.
         */
        public interface NonRecordable {}

        /**
         * For use by EditAction.Wrapper only.
         * @since jEdit 2.2final
         */
        public interface Wrapper {}

        /**
         * Macro recorder.
         */
        public interface MacroRecorder
        {
                void actionPerformed(ActionListener listener,
                        String actionCommand);
        }

        public static class backspace implements ActionListener
        {
                public void actionPerformed(ActionEvent evt)
                {
                        JEditTextArea textArea = getTextArea(evt);

                        if(!textArea.isEditable())
                        {
                                textArea.getToolkit().beep();
                                return;
                        }

                        if(textArea.getSelectionStart()
                           != textArea.getSelectionStop())
                        {
                                textArea.setSelectedText("");
                        }
                        else
                        {
                                int caret = textArea.getCaretPosition();
                                if(caret == 0)
                                {
                                        textArea.getToolkit().beep();
                                        return;
                                }
                                try
                                {
                                        textArea.getDocument().remove(caret - 1,1);
                                }
                                catch(BadLocationException bl)
                                {
                                        bl.printStackTrace();
                                }
                        }
                }
        }

        public static class backspace_word implements ActionListener
        {
                public void actionPerformed(ActionEvent evt)
                {
                        JEditTextArea textArea = getTextArea(evt);
                        int start = textArea.getSelectionStart();
                        if(start != textArea.getSelectionStop())
                        {
                                textArea.setSelectedText("");
                        }

                        int line = textArea.getCaretLine();
                        int lineStart = textArea.getLineStartOffset(line);
                        int caret = start - lineStart;

                        String lineText = textArea.getLineText(textArea
                                .getCaretLine());

                        if(caret == 0)
                        {
                                if(lineStart == 0)
                                {
                                        textArea.getToolkit().beep();
                                        return;
                                }
                                caret--;
                        }
                        else
                        {
                                String noWordSep = (String)textArea.getDocument().getProperty("noWordSep");
                                caret = findWordStart(lineText,caret,noWordSep);
                        }

                        try
                        {
                                textArea.getDocument().remove(
                                                caret + lineStart,
                                                start - (caret + lineStart));
                        }
                        catch(BadLocationException bl)
                        {
                                bl.printStackTrace();
                        }
                }
        }

        public static class delete implements ActionListener
        {
                public void actionPerformed(ActionEvent evt)
                {
                        JEditTextArea textArea = getTextArea(evt);

                        if(!textArea.isEditable())
                        {
                                textArea.getToolkit().beep();
                                return;
                        }

                        if(textArea.getSelectionStart()
                           != textArea.getSelectionStop())
                        {
                                textArea.setSelectedText("");
                        }
                        else
                        {
                                int caret = textArea.getCaretPosition();
                                if(caret == textArea.getDocumentLength())
                                {
                                        textArea.getToolkit().beep();
                                        return;
                                }
                                try
                                {
                                        textArea.getDocument().remove(caret,1);
                                }
                                catch(BadLocationException bl)
                                {
                                        bl.printStackTrace();
                                }
                        }
                }
        }

        public static class delete_word implements ActionListener
        {
                public void actionPerformed(ActionEvent evt)
                {
                        JEditTextArea textArea = getTextArea(evt);
                        int start = textArea.getSelectionStart();
                        if(start != textArea.getSelectionStop())
                        {
                                textArea.setSelectedText("");
                        }

                        int line = textArea.getCaretLine();
                        int lineStart = textArea.getLineStartOffset(line);
                        int caret = start - lineStart;

                        String lineText = textArea.getLineText(textArea
                                .getCaretLine());

                        if(caret == lineText.length())
                        {
                                if(lineStart + caret == textArea.getDocumentLength())
                                {
                                        textArea.getToolkit().beep();
                                        return;
                                }
                                caret++;
                        }
                        else
                        {
                                String noWordSep = (String)textArea.getDocument().getProperty("noWordSep");
                                caret = findWordEnd(lineText,caret,noWordSep);
                        }

                        try
                        {
                                textArea.getDocument().remove(start,
                                        (caret + lineStart) - start);
                        }
                        catch(BadLocationException bl)
                        {
                                bl.printStackTrace();
                        }
                }
        }

        public static class end implements ActionListener
        {
                private boolean select;

                public end(boolean select)
                {
                        this.select = select;
                }

                public void actionPerformed(ActionEvent evt)
                {
                        JEditTextArea textArea = getTextArea(evt);

                        int caret = textArea.getCaretPosition();

                        int caretLine = textArea.getCaretLine();
                        
                        int lastOfLine = textArea.getLineStopOffset(caretLine) - 1;
                        int lastNonWhiteSpaceOfLine = textArea.getLineStopNonWhiteSpaceOffset(caretLine) - 1;
                        int lastVisibleLine = textArea.getFirstLine()
                                + textArea.getVisibleLines();
                        if(lastVisibleLine >= textArea.getLineCount())
                        {
                                lastVisibleLine = Math.min(textArea.getLineCount() - 1,
                                        lastVisibleLine);
                        }
                        else
                                lastVisibleLine -= (textArea.getElectricScroll() + 1);

                        int lastVisible = textArea.getLineStopOffset(lastVisibleLine) - 1;
                        int lastDocument = textArea.getDocumentLength();

                        if(caret == lastDocument && !Preferences.getBoolean(CONTEXT_AWARE_HOME_END))
                        {
                                textArea.getToolkit().beep();
                                return;
                        }
                        else if(!Boolean.TRUE.equals(textArea.getClientProperty(
                                SMART_HOME_END_PROPERTY)))
                        {
                                if (!Preferences.getBoolean(CONTEXT_AWARE_HOME_END)
                                    || caret == lastNonWhiteSpaceOfLine)
                                  caret = lastOfLine;
                                else
                                  caret = lastNonWhiteSpaceOfLine;
                        }
                        else if(caret == lastVisible)
                                caret = lastDocument;
                        else if(caret == lastOfLine)
                                caret = lastVisible;
                        else
                                caret = lastOfLine;

                        if(select)
                                textArea.select(textArea.getMarkPosition(),caret);
                        else
                                textArea.setCaretPosition(caret);
                }
        }

        public static class document_end implements ActionListener
        {
                private boolean select;

                public document_end(boolean select)
                {
                        this.select = select;
                }

                public void actionPerformed(ActionEvent evt)
                {
                        JEditTextArea textArea = getTextArea(evt);
                        if(select)
                                textArea.select(textArea.getMarkPosition(),
                                        textArea.getDocumentLength());
                        else
                                textArea.setCaretPosition(textArea
                                        .getDocumentLength());
                }
        }

        public static class home implements ActionListener
        {
                private boolean select;

                public home(boolean select)
                {
                        this.select = select;
                }

                public void actionPerformed(ActionEvent evt)
                {
                        JEditTextArea textArea = getTextArea(evt);

                        int caret = textArea.getCaretPosition();

                        int firstLine = textArea.getFirstLine();

                        int caretLine = textArea.getCaretLine();
                        
                        int firstOfLine = textArea.getLineStartOffset(caretLine);
                        int firstNonWhiteSpaceOfLine = textArea.getLineStartNonWhiteSpaceOffset(caretLine);
                        int firstVisibleLine = (firstLine == 0 ? 0 :
                                firstLine + textArea.getElectricScroll());
                        int firstVisible = textArea.getLineStartOffset(
                                firstVisibleLine);

                        if(caret == 0 && !Preferences.getBoolean(CONTEXT_AWARE_HOME_END))
                        {
                                textArea.getToolkit().beep();
                                return;
                        }
                        else if(!Boolean.TRUE.equals(textArea.getClientProperty(
                                SMART_HOME_END_PROPERTY)))
                        {
                                if (!Preferences.getBoolean(CONTEXT_AWARE_HOME_END)
                                    || caret == firstNonWhiteSpaceOfLine)
                                        caret = firstOfLine;
                                else
                                        caret = firstNonWhiteSpaceOfLine;
                        }
                        else if(caret == firstVisible)
                                caret = 0;
                        else if(caret == firstOfLine)
                                caret = firstVisible;
                        else
                                caret = firstOfLine;

                        if(select)
                                textArea.select(textArea.getMarkPosition(),caret);
                        else
                                textArea.setCaretPosition(caret);
                }
        }

        public static class document_home implements ActionListener
        {
                private boolean select;

                public document_home(boolean select)
                {
                        this.select = select;
                }

                public void actionPerformed(ActionEvent evt)
                {
                        JEditTextArea textArea = getTextArea(evt);
                        if(select)
                                textArea.select(textArea.getMarkPosition(),0);
                        else
                                textArea.setCaretPosition(0);
                }
        }

        public static class insert_break implements ActionListener
        {
                public void actionPerformed(ActionEvent evt)
                {
                        JEditTextArea textArea = getTextArea(evt);

                        if(!textArea.isEditable())
                        {
                                textArea.getToolkit().beep();
                                return;
                        }

                        textArea.setSelectedText("\n");
                }
        }

        public static class insert_tab implements ActionListener
        {
                public void actionPerformed(ActionEvent evt)
                {
                        JEditTextArea textArea = getTextArea(evt);

                        if(!textArea.isEditable())
                        {
                                textArea.getToolkit().beep();
                                return;
                        }

                        textArea.overwriteSetSelectedText("\t");
                }
        }

        public static class next_char implements ActionListener
        {
                private boolean select;

                public next_char(boolean select)
                {
                        this.select = select;
                }

                public void actionPerformed(ActionEvent evt)
                {
                        JEditTextArea textArea = getTextArea(evt);
                        int caret = textArea.getCaretPosition();

                        if(caret == textArea.getDocumentLength())
                        {
                          if (textArea.getSelectionStart() !=
                              textArea.getSelectionStop()) {
                            // just move to the end of the selection
                            textArea.select(caret, caret);
                          } else {
                            // beep at the user for being annoying
                            textArea.getToolkit().beep();
                          }

                        } else if (select) {
                          textArea.select(textArea.getMarkPosition(), caret+1);

                        } else {
                          int start = textArea.getSelectionStart();
                          int end = textArea.getSelectionStop();
                          if (start != end) {
                            textArea.select(end, end);
                          } else {
                            textArea.setCaretPosition(caret + 1);
                          }
                        }
                }
        }

        public static class next_line implements ActionListener
        {
                private boolean select;

                public next_line(boolean select)
                {
                        this.select = select;
                }

                public void actionPerformed(ActionEvent evt)
                {
                        JEditTextArea textArea = getTextArea(evt);
                        int caret = textArea.getCaretPosition();
                        int line = textArea.getCaretLine();

                        if(line == textArea.getLineCount() - 1)
                        {
                          //textArea.getToolkit().beep();
                          int doc = textArea.getDocumentLength();
                          if (select) {
                            textArea.select(textArea.getMarkPosition(), doc);
                          } else {
                            textArea.setCaretPosition(doc);
                          }
                          return;
                        }

                        int magic = textArea.getMagicCaretPosition();
                        if(magic == -1)
                        {
                                magic = textArea.offsetToX(line,
                                        caret - textArea.getLineStartOffset(line));
                        }

                        caret = textArea.getLineStartOffset(line + 1)
                                + textArea.xToOffset(line + 1,magic);
                        if(select)
                                textArea.select(textArea.getMarkPosition(),caret);
                        else
                                textArea.setCaretPosition(caret);
                        textArea.setMagicCaretPosition(magic);
                }
        }

        public static class next_page implements ActionListener
        {
                private boolean select;

                public next_page(boolean select)
                {
                        this.select = select;
                }

                public void actionPerformed(ActionEvent evt)
                {
                        JEditTextArea textArea = getTextArea(evt);
                        int lineCount = textArea.getLineCount();
                        int firstLine = textArea.getFirstLine();
                        int visibleLines = textArea.getVisibleLines();
                        int line = textArea.getCaretLine();

                        firstLine += visibleLines;

                        if(firstLine + visibleLines >= lineCount - 1)
                                firstLine = lineCount - visibleLines;

                        textArea.setFirstLine(firstLine);

                        int caret = textArea.getLineStartOffset(
                                Math.min(textArea.getLineCount() - 1,
                                line + visibleLines));
                        if(select)
                                textArea.select(textArea.getMarkPosition(),caret);
                        else
                                textArea.setCaretPosition(caret);
                }
        }

        public static class next_word implements ActionListener
        {
                private boolean select;

                public next_word(boolean select)
                {
                        this.select = select;
                }

                public void actionPerformed(ActionEvent evt)
                {
                        JEditTextArea textArea = getTextArea(evt);
                        int caret = textArea.getCaretPosition();
                        int line = textArea.getCaretLine();
                        int lineStart = textArea.getLineStartOffset(line);
                        caret -= lineStart;

                        String lineText = textArea.getLineText(textArea
                                .getCaretLine());

                        if(caret == lineText.length())
                        {
                                if(lineStart + caret == textArea.getDocumentLength())
                                {
                                        textArea.getToolkit().beep();
                                        return;
                                }
                                caret++;
                        }
                        else
                        {
                                String noWordSep = (String)textArea.getDocument().getProperty("noWordSep");
                                caret = findWordEnd(lineText,caret,noWordSep);
                        }

                        if(select)
                                textArea.select(textArea.getMarkPosition(),
                                        lineStart + caret);
                        else
                                textArea.setCaretPosition(lineStart + caret);
                }
        }

        public static class overwrite implements ActionListener
        {
                public void actionPerformed(ActionEvent evt)
                {
                        JEditTextArea textArea = getTextArea(evt);
                        textArea.setOverwriteEnabled(
                                !textArea.isOverwriteEnabled());
                }
        }

        public static class prev_char implements ActionListener
        {
                private boolean select;

                public prev_char(boolean select)
                {
                        this.select = select;
                }

                public void actionPerformed(ActionEvent evt)
                {
                        JEditTextArea textArea = getTextArea(evt);
                        int caret = textArea.getCaretPosition();
                        if(caret == 0)
                        {
                                textArea.getToolkit().beep();
                                return;
                        }

                        if (select) {
                          textArea.select(textArea.getMarkPosition(), caret-1);
                        } else {
                          int start = textArea.getSelectionStart();
                          int end = textArea.getSelectionStop();
                          if (start != end) {
                            textArea.select(start, start);
                          } else {
                            textArea.setCaretPosition(caret - 1);
                          }
                        }
                }
        }

        public static class prev_line implements ActionListener
        {
                private boolean select;

                public prev_line(boolean select)
                {
                        this.select = select;
                }

                public void actionPerformed(ActionEvent evt)
                {
                        JEditTextArea textArea = getTextArea(evt);
                        int caret = textArea.getCaretPosition();
                        int line = textArea.getCaretLine();

                        if(line == 0)
                        {
                          if (select) {
                            if (textArea.getSelectionStart() != 0) {
                              textArea.select(textArea.getMarkPosition(), 0);
                            }
                          } else {
                            textArea.setCaretPosition(0);
                          }
                          //textArea.getToolkit().beep();
                                return;
                        }

                        int magic = textArea.getMagicCaretPosition();
                        if(magic == -1)
                        {
                                magic = textArea.offsetToX(line,
                                        caret - textArea.getLineStartOffset(line));
                        }

                        caret = textArea.getLineStartOffset(line - 1)
                                + textArea.xToOffset(line - 1,magic);
                        if(select)
                                textArea.select(textArea.getMarkPosition(),caret);
                        else
                                textArea.setCaretPosition(caret);
                        textArea.setMagicCaretPosition(magic);
                }
        }

        public static class prev_page implements ActionListener
        {
                private boolean select;

                public prev_page(boolean select)
                {
                        this.select = select;
                }

                public void actionPerformed(ActionEvent evt)
                {
                        JEditTextArea textArea = getTextArea(evt);
                        int firstLine = textArea.getFirstLine();
                        int visibleLines = textArea.getVisibleLines();
                        int line = textArea.getCaretLine();

                        if(firstLine < visibleLines)
                                firstLine = visibleLines;

                        textArea.setFirstLine(firstLine - visibleLines);

                        int caret = textArea.getLineStartOffset(
                                Math.max(0,line - visibleLines));
                        if(select)
                                textArea.select(textArea.getMarkPosition(),caret);
                        else
                                textArea.setCaretPosition(caret);
                }
        }

        public static class prev_word implements ActionListener
        {
                private boolean select;

                public prev_word(boolean select)
                {
                        this.select = select;
                }

                public void actionPerformed(ActionEvent evt)
                {
                        JEditTextArea textArea = getTextArea(evt);
                        int caret = textArea.getCaretPosition();
                        int line = textArea.getCaretLine();
                        int lineStart = textArea.getLineStartOffset(line);
                        caret -= lineStart;

                        String lineText = textArea.getLineText(textArea
                                .getCaretLine());

                        if(caret == 0)
                        {
                                if(lineStart == 0)
                                {
                                        textArea.getToolkit().beep();
                                        return;
                                }
                                caret--;
                        }
                        else
                        {
                                String noWordSep = (String)textArea.getDocument().getProperty("noWordSep");
                                caret = findWordStart(lineText,caret,noWordSep);
                        }

                        if(select)
                                textArea.select(textArea.getMarkPosition(),
                                        lineStart + caret);
                        else
                                textArea.setCaretPosition(lineStart + caret);
                }
        }

        public static class repeat implements ActionListener,
                InputHandler.NonRecordable
        {
                public void actionPerformed(ActionEvent evt)
                {
                        JEditTextArea textArea = getTextArea(evt);
                        textArea.getInputHandler().setRepeatEnabled(true);
                        String actionCommand = evt.getActionCommand();
                        if(actionCommand != null)
                        {
                                textArea.getInputHandler().setRepeatCount(
                                        Integer.parseInt(actionCommand));
                        }
                }
        }

        public static class toggle_rect implements ActionListener
        {
                public void actionPerformed(ActionEvent evt)
                {
                        JEditTextArea textArea = getTextArea(evt);
                        textArea.setSelectionRectangular(
                                !textArea.isSelectionRectangular());
                }
        }
        
        
        public static class clipboard_cut implements ActionListener
        {
                public void actionPerformed(ActionEvent evt)
                {
                        getTextArea(evt).cut();
                }
        }

        
        public static class clipboard_copy implements ActionListener
        {
                public void actionPerformed(ActionEvent evt)
                {
                        getTextArea(evt).copy();
                }
        }
        
        
        public static class clipboard_paste implements ActionListener
        {
                public void actionPerformed(ActionEvent evt)
                {
                        getTextArea(evt).paste();
                }
        }


        public static class insert_char implements ActionListener,
                InputHandler.NonRepeatable
        {
                public void actionPerformed(ActionEvent evt)
                {
                        JEditTextArea textArea = getTextArea(evt);
                        String str = evt.getActionCommand();
                        int repeatCount = textArea.getInputHandler().getRepeatCount();

                        if(textArea.isEditable())
                        {
                                StringBuffer buf = new StringBuffer();
                                for(int i = 0; i < repeatCount; i++)
                                        buf.append(str);
                                textArea.overwriteSetSelectedText(buf.toString());
                        }
                        else
                        {
                                textArea.getToolkit().beep();
                        }
                }
        }
        
        
        /**
         * Locates the start of the word at the specified position.
         * Moved from TextUtilities.java [fry 121210].
         * @param line The text
         * @param pos The position
         */
        public static int findWordStart(String line, int pos, String noWordSep)
        {
                char ch = line.charAt(pos - 1);

                if(noWordSep == null)
                        noWordSep = "";
                boolean selectNoLetter = (!Character.isLetterOrDigit(ch)
                        && noWordSep.indexOf(ch) == -1);

                int wordStart = 0;
                for(int i = pos - 1; i >= 0; i--)
                {
                        ch = line.charAt(i);
                        if(selectNoLetter ^ (!Character.isLetterOrDigit(ch) &&
                                noWordSep.indexOf(ch) == -1))
                        {
                                wordStart = i + 1;
                                break;
                        }
                }

                return wordStart;
        }

        /**
         * Locates the end of the word at the specified position.
         * Moved from TextUtilities.java [fry 121210].
         * @param line The text
         * @param pos The position
         */
        public static int findWordEnd(String line, int pos, String noWordSep)
        {
                char ch = line.charAt(pos);

                if(noWordSep == null)
                        noWordSep = "";
                boolean selectNoLetter = (!Character.isLetterOrDigit(ch)
                        && noWordSep.indexOf(ch) == -1);

                int wordEnd = line.length();
                for(int i = pos; i < line.length(); i++)
                {
                        ch = line.charAt(i);
                        if(selectNoLetter ^ (!Character.isLetterOrDigit(ch) &&
                                noWordSep.indexOf(ch) == -1))
                        {
                                wordEnd = i;
                                break;
                        }
                }
                return wordEnd;
        }
}
