/*
* JEditTextArea.java - jEdit's text component
* 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 ise.antelope.app.jedit;

import javax.swing.event.*;
import javax.swing.text.*;
import javax.swing.undo.*;
import javax.swing.*;
import java.awt.datatransfer.*;
import java.awt.event.*;
import java.awt.*;
import java.util.Enumeration;
import java.util.Vector;

/**
 * jEdit's text area component. It is more suited for editing program
 * source code than JEditorPane, because it drops the unnecessary features
 * (images, variable-width lines, and so on) and adds a whole bunch of
 * useful goodies such as:
 * <ul>
 * <li>More flexible key binding scheme
 * <li>Supports macro recorders
 * <li>Rectangular selection
 * <li>Bracket highlighting
 * <li>Syntax highlighting
 * <li>Command repetition
 * <li>Block caret can be enabled
 * </ul>
 * It is also faster and doesn't have as many problems. It can be used
 * in other applications; the only other part of jEdit it depends on is
 * the syntax package.<p>
 *
 * To use it in your app, treat it like any other component, for example:
 * <pre>JEditTextArea ta = new JEditTextArea();
 * ta.setTokenMarker(new JavaTokenMarker());
 * ta.setText("public class Test {\n"
 *     + "    public static void main(String[] args) {\n"
 *     + "        System.out.println(\"Hello World\");\n"
 *     + "    }\n"
 *     + "}");</pre>
 *
 * @author Slava Pestov
 * @version $Id: JEditTextArea.java 98 2004-05-24 04:10:50Z danson $
 */
public class JEditTextArea extends JComponent {
    /**
     * Adding components with this name to the text area will place
     * them left of the horizontal scroll bar. In jEdit, the status
     * bar is added this way.
     */
    public static String LEFT_OF_SCROLLBAR = "los";

    /**
     * Creates a new JEditTextArea with the default settings.
     */
    public JEditTextArea() {
        this( TextAreaDefaults.getDefaults() );
    }

    /**
     * Creates a new JEditTextArea with the specified settings.
     * @param defaults The default settings
     */
    public JEditTextArea( TextAreaDefaults defaults ) {
        // Enable the necessary events
        enableEvents( AWTEvent.KEY_EVENT_MASK );

        // Initialize some misc. stuff
        painter = new TextAreaPainter( this, defaults );
        documentHandler = new DocumentHandler();
        listenerList = new EventListenerList();
        caretEvent = new MutableCaretEvent();
        lineSegment = new Segment();
        bracketLine = bracketPosition = -1;
        blink = true;

        // Initialize the GUI
        setLayout( new ScrollLayout() );
        add( CENTER, painter );
        add( RIGHT, vertical = new JScrollBar( JScrollBar.VERTICAL ) );
        add( BOTTOM, horizontal = new JScrollBar( JScrollBar.HORIZONTAL ) );

        // Add some event listeners
        vertical.addAdjustmentListener( new AdjustHandler() );
        horizontal.addAdjustmentListener( new AdjustHandler() );
        painter.addComponentListener( new ComponentHandler() );
        painter.addMouseListener( new MouseHandler() );
        painter.addMouseMotionListener( new DragHandler() );
        painter.addMouseWheelListener(new MouseWheelHandler());
        vertical.addMouseWheelListener(new MouseWheelHandler());
        addFocusListener( new FocusHandler() );

        // Load the defaults
        setInputHandler( defaults.inputHandler );
        setDocument( defaults.document );
        editable = defaults.editable;
        caretVisible = defaults.caretVisible;
        caretBlinks = defaults.caretBlinks;
        electricScroll = defaults.electricScroll;

        popup = defaults.popup;

        // We don't seem to get the initial focus event?
        focusedComponent = this;
    }
    
    /**
     * Returns if this component can be traversed by pressing
     * the Tab key. This returns true.
     * @deprecated
     */
    public final boolean isManagingFocus() {
        return true;
    }

    /**
     * Returns the object responsible for painting this text area.
     */
    public final TextAreaPainter getPainter() {
        return painter;
    }

    /**
     * Returns the input handler.
     */
    public final InputHandler getInputHandler() {
        return inputHandler;
    }

    /**
     * Sets the input handler.
     * @param inputHandler The new input handler
     */
    public void setInputHandler( InputHandler inputHandler ) {
        this.inputHandler = inputHandler;
    }

    /**
     * Returns true if the caret is blinking, false otherwise.
     */
    public final boolean isCaretBlinkEnabled() {
        return caretBlinks;
    }

    /**
     * Toggles caret blinking.
     * @param caretBlinks True if the caret should blink, false otherwise
     */
    public void setCaretBlinkEnabled( boolean caretBlinks ) {
        this.caretBlinks = caretBlinks;
        if ( !caretBlinks )
            blink = false;

        painter.invalidateSelectedLines();
    }

    /**
     * Returns true if the caret is visible, false otherwise.
     */
    public final boolean isCaretVisible() {
        return ( !caretBlinks || blink ) && caretVisible;
    }

    /**
     * Sets if the caret should be visible.
     * @param caretVisible True if the caret should be visible, false
     * otherwise
     */
    public void setCaretVisible( boolean caretVisible ) {
        this.caretVisible = caretVisible;
        blink = true;

        painter.invalidateSelectedLines();
    }

    /**
     * Blinks the caret.
     */
    public final void blinkCaret() {
        if ( caretBlinks ) {
            blink = !blink;
            painter.invalidateSelectedLines();
        }
        else
            blink = true;
    }

    /**
     * Returns the number of lines from the top and button of the
     * text area that are always visible.
     */
    public final int getElectricScroll() {
        return electricScroll;
    }

    /**
     * Sets the number of lines from the top and bottom of the text
     * area that are always visible
     * @param electricScroll The number of lines always visible from
     * the top or bottom
     */
    public final void setElectricScroll( int electricScroll ) {
        this.electricScroll = electricScroll;
    }

    /**
     * Updates the state of the scroll bars. This should be called
     * if the number of lines in the document changes, or when the
     * size of the text are changes.
     */
    public void updateScrollBars() {
        if ( vertical != null && visibleLines != 0 ) {
            vertical.setValues( firstLine, visibleLines, 0, getLineCount() );
            vertical.setUnitIncrement( 2 );
            vertical.setBlockIncrement( visibleLines );
        }

        int width = painter.getWidth();
        if ( horizontal != null && width != 0 ) {
            horizontal.setValues( -horizontalOffset, width, 0, width * 5 );
            horizontal.setUnitIncrement( painter.getFontMetrics()
                    .charWidth( 'w' ) );
            horizontal.setBlockIncrement( width / 2 );
        }
    }

    /**
     * Returns the line displayed at the text area's origin.
     */
    public final int getFirstLine() {
        return firstLine;
    }

    /**
     * Sets the line displayed at the text area's origin without
     * updating the scroll bars.
     */
    public void setFirstLine( int firstLine ) {
        if ( firstLine == this.firstLine )
            return ;
        int oldFirstLine = this.firstLine;
        this.firstLine = firstLine;
        if ( firstLine != vertical.getValue() )
            updateScrollBars();
        painter.repaint();
    }

    /**
     * Returns the number of lines visible in this text area.
     */
    public final int getVisibleLines() {
        return visibleLines;
    }

    /**
     * Recalculates the number of visible lines. This should not
     * be called directly.
     */
    public final void recalculateVisibleLines() {
        if ( painter == null )
            return ;
        int height = painter.getHeight();
        int lineHeight = painter.getFontMetrics().getHeight();
        int oldVisibleLines = visibleLines;
        visibleLines = height / lineHeight;
        updateScrollBars();
    }

    /**
     * Returns the horizontal offset of drawn lines.
     */
    public final int getHorizontalOffset() {
        return horizontalOffset;
    }

    /**
     * Sets the horizontal offset of drawn lines. This can be used to
     * implement horizontal scrolling.
     * @param horizontalOffset offset The new horizontal offset
     */
    public void setHorizontalOffset( int horizontalOffset ) {
        if ( horizontalOffset == this.horizontalOffset )
            return ;
        this.horizontalOffset = horizontalOffset;
        if ( horizontalOffset != horizontal.getValue() )
            updateScrollBars();
        painter.repaint();
    }

    /**
     * A fast way of changing both the first line and horizontal
     * offset.
     * @param firstLine The new first line
     * @param horizontalOffset The new horizontal offset
     * @return True if any of the values were changed, false otherwise
     */
    public boolean setOrigin( int firstLine, int horizontalOffset ) {
        boolean changed = false;
        int oldFirstLine = this.firstLine;

        if ( horizontalOffset != this.horizontalOffset ) {
            this.horizontalOffset = horizontalOffset;
            changed = true;
        }

        if ( firstLine != this.firstLine ) {
            this.firstLine = firstLine;
            changed = true;
        }

        if ( changed ) {
            updateScrollBars();
            painter.repaint();
        }

        return changed;
    }

    /**
     * Ensures that the caret is visible by scrolling the text area if
     * necessary.
     * @return True if scrolling was actually performed, false if the
     * caret was already visible
     */
    public boolean scrollToCaret() {
        int line = getCaretLine();
        int lineStart = getLineStartOffset( line );
        int offset = Math.max( 0, Math.min( getLineLength( line ) - 1,
                getCaretPosition() - lineStart ) );

        return scrollTo( line, offset );
    }

    /**
     * Ensures that the specified line and offset is visible by scrolling
     * the text area if necessary.
     * @param line The line to scroll to
     * @param offset The offset in the line to scroll to
     * @return True if scrolling was actually performed, false if the
     * line and offset was already visible
     */
    public boolean scrollTo( int line, int offset ) {
        // visibleLines == 0 before the component is realized
        // we can't do any proper scrolling then, so we have
        // this hack...
        if ( visibleLines == 0 ) {
            setFirstLine( Math.max( 0, line - electricScroll ) );
            return true;
        }

        int newFirstLine = firstLine;
        int newHorizontalOffset = horizontalOffset;

        if ( line < firstLine + electricScroll ) {
            newFirstLine = Math.max( 0, line - electricScroll );
        }
        else
            if ( line + electricScroll >= firstLine + visibleLines ) {
                newFirstLine = ( line - visibleLines ) + electricScroll + 1;
                if ( newFirstLine + visibleLines >= getLineCount() )
                    newFirstLine = getLineCount() - visibleLines;
                if ( newFirstLine < 0 )
                    newFirstLine = 0;
            }

        int x = _offsetToX( line, offset );
        int width = painter.getFontMetrics().charWidth( 'w' );

        if ( x < 0 ) {
            newHorizontalOffset = Math.min( 0, horizontalOffset
                    - x + width + 5 );
        }
        else
            if ( x + width >= painter.getWidth() ) {
                newHorizontalOffset = horizontalOffset +
                        ( painter.getWidth() - x ) - width - 5;
            }

        return setOrigin( newFirstLine, newHorizontalOffset );
    }

    /**
     * Converts a line index to a y co-ordinate.
     * @param line The line
     */
    public int lineToY( int line ) {
        FontMetrics fm = painter.getFontMetrics();
        return ( line - firstLine ) * fm.getHeight()
               - ( fm.getLeading() + fm.getMaxDescent() );
    }

    /**
     * Converts a y co-ordinate to a line index.
     * @param y The y co-ordinate
     */
    public int yToLine( int y ) {
        FontMetrics fm = painter.getFontMetrics();
        int height = fm.getHeight();
        return Math.max( 0, Math.min( getLineCount() - 1,
                y / height + firstLine ) );
    }

    /**
     * Converts an offset in a line into an x co-ordinate. This is a
     * slow version that can be used any time.
     * @param line The line
     * @param offset The offset, from the start of the line
     */
    public final int offsetToX( int line, int offset ) {
        // don't use cached tokens
        painter.currentLineTokens = null;
        return _offsetToX( line, offset );
    }

    /**
     * Converts an offset in a line into an x co-ordinate. This is a
     * fast version that should only be used if no changes were made
     * to the text since the last repaint.
     * @param line The line
     * @param offset The offset, from the start of the line
     */
    public int _offsetToX( int line, int offset ) {
        TokenMarker tokenMarker = getTokenMarker();

        /* Use painter's cached info for speed */
        FontMetrics fm = painter.getFontMetrics();

        getLineText( line, lineSegment );

        int segmentOffset = lineSegment.offset;
        int x = horizontalOffset;

        /* If syntax coloring is disabled, do simple translation */
        if ( tokenMarker == null ) {
            lineSegment.count = offset;
            return x + Utilities.getTabbedTextWidth( lineSegment,
                    fm, x, painter, 0 );
        }
        /* If syntax coloring is enabled, we have to do this because
         * tokens can vary in width */
        else {
            Token tokens;
            if ( painter.currentLineIndex == line
                    && painter.currentLineTokens != null )
                tokens = painter.currentLineTokens;
            else {
                painter.currentLineIndex = line;
                tokens = painter.currentLineTokens
                        = tokenMarker.markTokens( lineSegment, line );
            }

            Toolkit toolkit = painter.getToolkit();
            Font defaultFont = painter.getFont();
            SyntaxStyle[] styles = painter.getStyles();

            for ( ;; ) {
                byte id = tokens.id;
                if ( id == Token.END ) {
                    return x;
                }

                if ( id == Token.NULL )
                    fm = painter.getFontMetrics();
                else
                    fm = styles[ id ].getFontMetrics( defaultFont );

                int length = tokens.length;

                if ( offset + segmentOffset < lineSegment.offset + length ) {
                    lineSegment.count = offset - ( lineSegment.offset - segmentOffset );
                    return x + Utilities.getTabbedTextWidth(
                               lineSegment, fm, x, painter, 0 );
                }
                else {
                    lineSegment.count = length;
                    x += Utilities.getTabbedTextWidth(
                             lineSegment, fm, x, painter, 0 );
                    lineSegment.offset += length;
                }
                tokens = tokens.next;
            }
        }
    }

    /**
     * Converts an x co-ordinate to an offset within a line.
     * @param line The line
     * @param x The x co-ordinate
     */
    public int xToOffset( int line, int x ) {
        TokenMarker tokenMarker = getTokenMarker();

        /* Use painter's cached info for speed */
        FontMetrics fm = painter.getFontMetrics();

        getLineText( line, lineSegment );

        char[] segmentArray = lineSegment.array;
        int segmentOffset = lineSegment.offset;
        int segmentCount = lineSegment.count;

        int width = horizontalOffset;

        if ( tokenMarker == null ) {
            for ( int i = 0; i < segmentCount; i++ ) {
                char c = segmentArray[ i + segmentOffset ];
                int charWidth;
                if ( c == '\t' )
                    charWidth = ( int ) painter.nextTabStop( width, i )
                            - width;
                else
                    charWidth = fm.charWidth( c );

                if ( painter.isBlockCaretEnabled() ) {
                    if ( x - charWidth <= width )
                        return i;
                }
                else {
                    if ( x - charWidth / 2 <= width )
                        return i;
                }

                width += charWidth;
            }

            return segmentCount;
        }
        else {
            Token tokens;
            if ( painter.currentLineIndex == line && painter
                    .currentLineTokens != null )
                tokens = painter.currentLineTokens;
            else {
                painter.currentLineIndex = line;
                tokens = painter.currentLineTokens
                        = tokenMarker.markTokens( lineSegment, line );
            }

            int offset = 0;
            Toolkit toolkit = painter.getToolkit();
            Font defaultFont = painter.getFont();
            SyntaxStyle[] styles = painter.getStyles();

            for ( ;; ) {
                byte id = tokens.id;
                if ( id == Token.END )
                    return offset;

                if ( id == Token.NULL )
                    fm = painter.getFontMetrics();
                else
                    fm = styles[ id ].getFontMetrics( defaultFont );

                int length = tokens.length;

                for ( int i = 0; i < length; i++ ) {
                    char c = segmentArray[ segmentOffset + offset + i ];
                    int charWidth;
                    if ( c == '\t' )
                        charWidth = ( int ) painter.nextTabStop( width, offset + i )
                                - width;
                    else
                        charWidth = fm.charWidth( c );

                    if ( painter.isBlockCaretEnabled() ) {
                        if ( x - charWidth <= width )
                            return offset + i;
                    }
                    else {
                        if ( x - charWidth / 2 <= width )
                            return offset + i;
                    }

                    width += charWidth;
                }

                offset += length;
                tokens = tokens.next;
            }
        }
    }

    /**
     * Converts a point to an offset, from the start of the text.
     * @param x The x co-ordinate of the point
     * @param y The y co-ordinate of the point
     */
    public int xyToOffset( int x, int y ) {
        int line = yToLine( y );
        int start = getLineStartOffset( line );
        return start + xToOffset( line, x );
    }

    /**
     * Returns the document this text area is editing.
     */
    public final Document getDocument() {
        return document;
    }

    /**
     * Sets the document this text area is editing.
     * @param document The document
     */
    public void setDocument( SyntaxDocument document ) {
        if ( this.document == document )
            return ;
        if ( this.document != null )
            this.document.removeDocumentListener( documentHandler );
        this.document = document;

        select( 0, 0 );
        updateScrollBars();
        painter.repaint();

        undoManager = new UndoManager();
        document.addUndoableEditListener( undoManager );
        documentHandler = new DocumentHandler();
        document.addDocumentListener( documentHandler );
        undoManager.discardAllEdits();
    }

    private UndoManager undoManager = null;
    public void undo() {
        try {
            if ( undoManager.canUndo() )
                undoManager.undo();
        }
        catch ( CannotUndoException e ) {
            Toolkit.getDefaultToolkit().beep();
        }
    }

    public void redo() {
        try {
            if ( undoManager.canRedo() )
                undoManager.redo();
        }
        catch ( CannotRedoException e ) {
            Toolkit.getDefaultToolkit().beep();
        }
    }

    public boolean canUndo() {
        return undoManager.canUndo();
    }

    public boolean canRedo() {
        return undoManager.canRedo();
    }

    /**
     * Returns the document's token marker. Equivalent to calling
     * <code>getDocument().getTokenMarker()</code>.
     */
    public final TokenMarker getTokenMarker() {
        return document.getTokenMarker();
    }

    /**
     * Sets the document's token marker. Equivalent to caling
     * <code>getDocument().setTokenMarker()</code>.
     * @param tokenMarker The token marker
     */
    public final void setTokenMarker( TokenMarker tokenMarker ) {
        document.setTokenMarker( tokenMarker );
    }

    /**
     * Returns the length of the document. Equivalent to calling
     * <code>getDocument().getLength()</code>.
     */
    public final int getDocumentLength() {
        return document.getLength();
    }

    /**
     * Returns the number of lines in the document.
     */
    public final int getLineCount() {
        return document.getDefaultRootElement().getElementCount();
    }

    /**
     * Returns the line containing the specified offset.
     * @param offset The offset
     */
    public final int getLineOfOffset( int offset ) {
        return document.getDefaultRootElement().getElementIndex( offset );
    }

    /**
     * Returns the start offset of the specified line.
     * @param line The line
     * @return The start offset of the specified line, or -1 if the line is
     * invalid
     */
    public int getLineStartOffset( int line ) {
        Element lineElement = document.getDefaultRootElement()
                .getElement( line );
        if ( lineElement == null )
            return -1;
        else
            return lineElement.getStartOffset();
    }

    /**
     * Returns the end offset of the specified line.
     * @param line The line
     * @return The end offset of the specified line, or -1 if the line is
     * invalid.
     */
    public int getLineEndOffset( int line ) {
        Element lineElement = document.getDefaultRootElement()
                .getElement( line );
        if ( lineElement == null )
            return -1;
        else
            return lineElement.getEndOffset();
    }

    /**
     * Returns the length of the specified line.
     * @param line The line
     */
    public int getLineLength( int line ) {
        Element lineElement = document.getDefaultRootElement()
                .getElement( line );
        if ( lineElement == null )
            return -1;
        else
            return lineElement.getEndOffset()
                   - lineElement.getStartOffset() - 1;
    }

    /**
     * Returns the entire text of this text area.
     */
    public String getText() {
        try {
            return document.getText( 0, document.getLength() );
        }
        catch ( BadLocationException bl ) {
            bl.printStackTrace();
            return null;
        }
    }

    /**
     * Sets the entire text of this text area.
     */
    public void setText( String text ) {
        try {
            document.beginCompoundEdit();
            document.remove( 0, document.getLength() );
            document.insertString( 0, text, null );
        }
        catch ( BadLocationException bl ) {
            bl.printStackTrace();
        }
        finally {
            document.endCompoundEdit();
        }
    }

    /**
     * Returns the specified substring of the document.
     * @param start The start offset
     * @param len The length of the substring
     * @return The substring, or null if the offsets are invalid
     */
    public final String getText( int start, int len ) {
        try {
            return document.getText( start, len );
        }
        catch ( BadLocationException bl ) {
            bl.printStackTrace();
            return null;
        }
    }

    /**
     * Copies the specified substring of the document into a segment.
     * If the offsets are invalid, the segment will contain a null string.
     * @param start The start offset
     * @param len The length of the substring
     * @param segment The segment
     */
    public final void getText( int start, int len, Segment segment ) {
        try {
            document.getText( start, len, segment );
        }
        catch ( BadLocationException bl ) {
            bl.printStackTrace();
            segment.offset = segment.count = 0;
        }
    }

    /**
     * Returns the text on the specified line.
     * @param lineIndex The line
     * @return The text, or null if the line is invalid
     */
    public final String getLineText( int lineIndex ) {
        int start = getLineStartOffset( lineIndex );
        return getText( start, getLineEndOffset( lineIndex ) - start - 1 );
    }

    /**
     * Copies the text on the specified line into a segment. If the line
     * is invalid, the segment will contain a null string.
     * @param lineIndex The line
     */
    public final void getLineText( int lineIndex, Segment segment ) {
        int start = getLineStartOffset( lineIndex );
        getText( start, getLineEndOffset( lineIndex ) - start - 1, segment );
    }

    /**
     * Returns the selection start offset.
     */
    public final int getSelectionStart() {
        return selectionStart;
    }

    /**
     * Returns the offset where the selection starts on the specified
     * line.
     */
    public int getSelectionStart( int line ) {
        if ( line == selectionStartLine )
            return selectionStart;
        else
            if ( rectSelect ) {
                Element map = document.getDefaultRootElement();
                int start = selectionStart - map.getElement( selectionStartLine )
                        .getStartOffset();

                Element lineElement = map.getElement( line );
                int lineStart = lineElement.getStartOffset();
                int lineEnd = lineElement.getEndOffset() - 1;
                return Math.min( lineEnd, lineStart + start );
            }
            else
                return getLineStartOffset( line );
    }

    /**
     * Returns the selection start line.
     */
    public final int getSelectionStartLine() {
        return selectionStartLine;
    }

    /**
     * Sets the selection start. The new selection will be the new
     * selection start and the old selection end.
     * @param selectionStart The selection start
     * @see #select(int,int)
     */
    public final void setSelectionStart( int selectionStart ) {
        select( selectionStart, selectionEnd );
    }

    /**
     * Returns the selection end offset.
     */
    public final int getSelectionEnd() {
        return selectionEnd;
    }

    /**
     * Returns the offset where the selection ends on the specified
     * line.
     */
    public int getSelectionEnd( int line ) {
        if ( line == selectionEndLine )
            return selectionEnd;
        else
            if ( rectSelect ) {
                Element map = document.getDefaultRootElement();
                int end = selectionEnd - map.getElement( selectionEndLine )
                        .getStartOffset();

                Element lineElement = map.getElement( line );
                int lineStart = lineElement.getStartOffset();
                int lineEnd = lineElement.getEndOffset() - 1;
                return Math.min( lineEnd, lineStart + end );
            }
            else
                return getLineEndOffset( line ) - 1;
    }

    /**
     * Returns the selection end line.
     */
    public final int getSelectionEndLine() {
        return selectionEndLine;
    }

    /**
     * Sets the selection end. The new selection will be the old
     * selection start and the bew selection end.
     * @param selectionEnd The selection end
     * @see #select(int,int)
     */
    public final void setSelectionEnd( int selectionEnd ) {
        select( selectionStart, selectionEnd );
    }

    /**
     * Returns the caret position. This will either be the selection
     * start or the selection end, depending on which direction the
     * selection was made in.
     */
    public final int getCaretPosition() {
        return ( biasLeft ? selectionStart : selectionEnd );
    }

    /**
     * Returns the caret line.
     */
    public final int getCaretLine() {
        return ( biasLeft ? selectionStartLine : selectionEndLine );
    }

    /**
     * Returns the mark position. This will be the opposite selection
     * bound to the caret position.
     * @see #getCaretPosition()
     */
    public final int getMarkPosition() {
        return ( biasLeft ? selectionEnd : selectionStart );
    }

    /**
     * Returns the mark line.
     */
    public final int getMarkLine() {
        return ( biasLeft ? selectionEndLine : selectionStartLine );
    }

    /**
     * Sets the caret position. The new selection will consist of the
     * caret position only (hence no text will be selected)
     * @param caret The caret position
     * @see #select(int,int)
     */
    public final void setCaretPosition( int caret ) {
        select( caret, caret );
    }

    /**
     * Selects all text in the document.
     */
    public final void selectAll() {
        select( 0, getDocumentLength() );
    }

    /**
     * Moves the mark to the caret position.
     */
    public final void selectNone() {
        select( getCaretPosition(), getCaretPosition() );
    }

    /**
     * Selects from the start offset to the end offset. This is the
     * general selection method used by all other selecting methods.
     * The caret position will be start if start &lt; end, and end
     * if end &gt; start.
     * @param start The start offset
     * @param end The end offset
     */
    public void select( int start, int end ) {
        int newStart, newEnd;
        boolean newBias;
        if ( start <= end ) {
            newStart = start;
            newEnd = end;
            newBias = false;
        }
        else {
            newStart = end;
            newEnd = start;
            newBias = true;
        }

        if ( newStart < 0 || newEnd > getDocumentLength() ) {
            throw new IllegalArgumentException( "Bounds out of"
                    + " range: " + newStart + "," +
                    newEnd );
        }

        // If the new position is the same as the old, we don't
        // do all this crap, however we still do the stuff at
        // the end (clearing magic position, scrolling)
        if ( newStart != selectionStart || newEnd != selectionEnd
                || newBias != biasLeft ) {
            int newStartLine = getLineOfOffset( newStart );
            int newEndLine = getLineOfOffset( newEnd );

            if ( painter.isBracketHighlightEnabled() ) {
                if ( bracketLine != -1 )
                    painter.invalidateLine( bracketLine );
                updateBracketHighlight( end );
                if ( bracketLine != -1 )
                    painter.invalidateLine( bracketLine );
            }

            painter.invalidateLineRange( selectionStartLine, selectionEndLine );
            painter.invalidateLineRange( newStartLine, newEndLine );

            document.addUndoableEdit( new CaretUndo(
                        selectionStart, selectionEnd ) );

            selectionStart = newStart;
            selectionEnd = newEnd;
            selectionStartLine = newStartLine;
            selectionEndLine = newEndLine;
            biasLeft = newBias;

            fireCaretEvent();
        }

        // When the user is typing, etc, we don't want the caret
        // to blink
        blink = true;
        caretTimer.restart();

        // Disable rectangle select if selection start = selection end
        if ( selectionStart == selectionEnd )
            rectSelect = false;

        // Clear the `magic' caret position used by up/down
        magicCaret = -1;

        scrollToCaret();
    }

    /**
     * Returns the selected text, or null if no selection is active.
     */
    public final String getSelectedText() {
        if ( selectionStart == selectionEnd )
            return null;

        if ( rectSelect ) {
            // Return each row of the selection on a new line

            Element map = document.getDefaultRootElement();

            int start = selectionStart - map.getElement( selectionStartLine )
                    .getStartOffset();
            int end = selectionEnd - map.getElement( selectionEndLine )
                    .getStartOffset();

            // Certain rectangles satisfy this condition...
            if ( end < start ) {
                int tmp = end;
                end = start;
                start = tmp;
            }

            StringBuffer buf = new StringBuffer();
            Segment seg = new Segment();

            for ( int i = selectionStartLine; i <= selectionEndLine; i++ ) {
                Element lineElement = map.getElement( i );
                int lineStart = lineElement.getStartOffset();
                int lineEnd = lineElement.getEndOffset() - 1;
                int lineLen = lineEnd - lineStart;

                lineStart = Math.min( lineStart + start, lineEnd );
                lineLen = Math.min( end - start, lineEnd - lineStart );

                getText( lineStart, lineLen, seg );
                buf.append( seg.array, seg.offset, seg.count );

                if ( i != selectionEndLine )
                    buf.append( '\n' );
            }

            return buf.toString();
        }
        else {
            return getText( selectionStart,
                    selectionEnd - selectionStart );
        }
    }

    /**
     * Replaces the selection with the specified text.
     * @param selectedText The replacement text for the selection
     */
    public void setSelectedText( String selectedText ) {
        if ( !editable ) {
            throw new InternalError( "Text component"
                    + " read only" );
        }

        document.beginCompoundEdit();

        try {
            if ( rectSelect ) {
                Element map = document.getDefaultRootElement();

                int start = selectionStart - map.getElement( selectionStartLine )
                        .getStartOffset();
                int end = selectionEnd - map.getElement( selectionEndLine )
                        .getStartOffset();

                // Certain rectangles satisfy this condition...
                if ( end < start ) {
                    int tmp = end;
                    end = start;
                    start = tmp;
                }

                int lastNewline = 0;
                int currNewline = 0;

                for ( int i = selectionStartLine; i <= selectionEndLine; i++ ) {
                    Element lineElement = map.getElement( i );
                    int lineStart = lineElement.getStartOffset();
                    int lineEnd = lineElement.getEndOffset() - 1;
                    int rectStart = Math.min( lineEnd, lineStart + start );

                    document.remove( rectStart, Math.min( lineEnd - rectStart,
                            end - start ) );

                    if ( selectedText == null )
                        continue;

                    currNewline = selectedText.indexOf( '\n', lastNewline );
                    if ( currNewline == -1 )
                        currNewline = selectedText.length();

                    document.insertString( rectStart, selectedText
                            .substring( lastNewline, currNewline ), null );

                    lastNewline = Math.min( selectedText.length(),
                            currNewline + 1 );
                }

                if ( selectedText != null &&
                        currNewline != selectedText.length() ) {
                    int offset = map.getElement( selectionEndLine )
                            .getEndOffset() - 1;
                    document.insertString( offset, "\n", null );
                    document.insertString( offset + 1, selectedText
                            .substring( currNewline + 1 ), null );
                }
            }
            else {
                document.remove( selectionStart,
                        selectionEnd - selectionStart );
                if ( selectedText != null ) {
                    document.insertString( selectionStart,
                            selectedText, null );
                }
            }
        }
        catch ( BadLocationException bl ) {
            bl.printStackTrace();
            throw new InternalError( "Cannot replace"
                    + " selection" );
        }
        // No matter what happends... stops us from leaving document
        // in a bad state
        finally {
            document.endCompoundEdit();
        }

        setCaretPosition( selectionEnd );
    }

    /**
     * Returns true if this text area is editable, false otherwise.
     */
    public final boolean isEditable() {
        return editable;
    }

    /**
     * Sets if this component is editable.
     * @param editable True if this text area should be editable,
     * false otherwise
     */
    public final void setEditable( boolean editable ) {
        this.editable = editable;
    }

    /**
     * Returns the right click popup menu.
     */
    public final JPopupMenu getRightClickPopup() {
        return popup;
    }

    /**
     * Sets the right click popup menu.
     * @param popup The popup
     */
    public final void setRightClickPopup( JPopupMenu popup ) {
        this.popup = popup;
    }

    /**
     * Returns the `magic' caret position. This can be used to preserve
     * the column position when moving up and down lines.
     */
    public final int getMagicCaretPosition() {
        return magicCaret;
    }

    /**
     * Sets the `magic' caret position. This can be used to preserve
     * the column position when moving up and down lines.
     * @param magicCaret The magic caret position
     */
    public final void setMagicCaretPosition( int magicCaret ) {
        this.magicCaret = magicCaret;
    }

    /**
     * Similar to <code>setSelectedText()</code>, but overstrikes the
     * appropriate number of characters if overwrite mode is enabled.
     * @param str The string
     * @see #setSelectedText(String)
     * @see #isOverwriteEnabled()
     */
    public void overwriteSetSelectedText( String str ) {
        // Don't overstrike if there is a selection
        if ( !overwrite || selectionStart != selectionEnd ) {
            setSelectedText( str );
            return ;
        }

        // Don't overstrike if we're on the end of
        // the line
        int caret = getCaretPosition();
        int caretLineEnd = getLineEndOffset( getCaretLine() );
        if ( caretLineEnd - caret <= str.length() ) {
            setSelectedText( str );
            return ;
        }

        document.beginCompoundEdit();

        try {
            document.remove( caret, str.length() );
            document.insertString( caret, str, null );
        }
        catch ( BadLocationException bl ) {
            bl.printStackTrace();
        }
        finally {
            document.endCompoundEdit();
        }
    }

    /**
     * Returns true if overwrite mode is enabled, false otherwise.
     */
    public final boolean isOverwriteEnabled() {
        return overwrite;
    }

    /**
     * Sets if overwrite mode should be enabled.
     * @param overwrite True if overwrite mode should be enabled,
     * false otherwise.
     */
    public final void setOverwriteEnabled( boolean overwrite ) {
        this.overwrite = overwrite;
        painter.invalidateSelectedLines();
    }

    /**
     * Returns true if the selection is rectangular, false otherwise.
     */
    public final boolean isSelectionRectangular() {
        return rectSelect;
    }

    /**
     * Sets if the selection should be rectangular.
     * @param overwrite True if the selection should be rectangular,
     * false otherwise.
     */
    public final void setSelectionRectangular( boolean rectSelect ) {
        this.rectSelect = rectSelect;
        painter.invalidateSelectedLines();
    }

    /**
     * Returns the position of the highlighted bracket (the bracket
     * matching the one before the caret)
     */
    public final int getBracketPosition() {
        return bracketPosition;
    }

    /**
     * Returns the line of the highlighted bracket (the bracket
     * matching the one before the caret)
     */
    public final int getBracketLine() {
        return bracketLine;
    }

    /**
     * Adds a caret change listener to this text area.
     * @param listener The listener
     */
    public final void addCaretListener( CaretListener listener ) {
        listenerList.add( CaretListener.class, listener );
    }

    /**
     * Removes a caret change listener from this text area.
     * @param listener The listener
     */
    public final void removeCaretListener( CaretListener listener ) {
        listenerList.remove( CaretListener.class, listener );
    }

    /**
     * Deletes the selected text from the text area and places it
     * into the clipboard.
     */
    public void cut() {
        if ( editable ) {
            copy();
            setSelectedText( "" );
        }
    }

    /**
     * Places the selected text into the clipboard.
     */
    public void copy() {
        if ( selectionStart != selectionEnd ) {
            Clipboard clipboard = getToolkit().getSystemClipboard();

            String selection = getSelectedText();

            int repeatCount = inputHandler.getRepeatCount();
            StringBuffer buf = new StringBuffer();
            for ( int i = 0; i < repeatCount; i++ )
                buf.append( selection );

            clipboard.setContents( new StringSelection( buf.toString() ), null );
        }
    }

    /**
     * Inserts the clipboard contents into the text.
     */
    public void paste() {
        if ( editable ) {
            Clipboard clipboard = getToolkit().getSystemClipboard();
            try {
                // The MacOS MRJ doesn't convert \r to \n,
                // so do it here
                String selection = ( ( String ) clipboard
                        .getContents( this ).getTransferData(
                            DataFlavor.stringFlavor ) )
                        .replace( '\r', '\n' );

                int repeatCount = inputHandler.getRepeatCount();
                StringBuffer buf = new StringBuffer();
                for ( int i = 0; i < repeatCount; i++ )
                    buf.append( selection );
                selection = buf.toString();
                setSelectedText( selection );
            }
            catch ( Exception e ) {
                getToolkit().beep();
                System.err.println( "Clipboard does not"
                        + " contain a string" );
            }
        }
    }

    /**
     * Called by the AWT when this component is removed from it's parent.
     * This stops clears the currently focused component.
     */
    public void removeNotify() {
        super.removeNotify();
        if ( focusedComponent == this )
            focusedComponent = null;
    }

    /**
     * Forwards key events directly to the input handler.
     * This is slightly faster than using a KeyListener
     * because some Swing overhead is avoided.
     */
    public void processKeyEvent( KeyEvent evt ) {
        if ( inputHandler == null )
            return ;
        switch ( evt.getID() ) {
            case KeyEvent.KEY_TYPED:
                inputHandler.keyTyped( evt );
                break;
            case KeyEvent.KEY_PRESSED:
                inputHandler.keyPressed( evt );
                break;
            case KeyEvent.KEY_RELEASED:
                inputHandler.keyReleased( evt );
                break;
        }
    }

    // protected members
    protected static String CENTER = "center";
    protected static String RIGHT = "right";
    protected static String BOTTOM = "bottom";

    protected static JEditTextArea focusedComponent;
    protected static Timer caretTimer;

    protected TextAreaPainter painter;

    protected JPopupMenu popup;

    protected EventListenerList listenerList;
    protected MutableCaretEvent caretEvent;

    protected boolean caretBlinks;
    protected boolean caretVisible;
    protected boolean blink;

    protected boolean editable;

    protected int firstLine;
    protected int visibleLines;
    protected int electricScroll;

    protected int horizontalOffset;

    protected JScrollBar vertical;
    protected JScrollBar horizontal;
    protected boolean scrollBarsInitialized;

    protected InputHandler inputHandler;
    protected SyntaxDocument document;
    protected DocumentHandler documentHandler;

    protected Segment lineSegment;

    protected int selectionStart;
    protected int selectionStartLine;
    protected int selectionEnd;
    protected int selectionEndLine;
    protected boolean biasLeft;

    protected int bracketPosition;
    protected int bracketLine;

    protected int magicCaret;
    protected boolean overwrite;
    protected boolean rectSelect;

    protected void fireCaretEvent() {
        Object[] listeners = listenerList.getListenerList();
        for ( int i = listeners.length - 2; i >= 0; i-- ) {
            if ( listeners[ i ] == CaretListener.class ) {
                ( ( CaretListener ) listeners[ i + 1 ] ).caretUpdate( caretEvent );
            }
        }
    }

    protected void updateBracketHighlight( int newCaretPosition ) {
        if ( newCaretPosition == 0 ) {
            bracketPosition = bracketLine = -1;
            return ;
        }

        try {
            int offset = TextUtilities.findMatchingBracket(
                        document, newCaretPosition - 1 );
            if ( offset != -1 ) {
                bracketLine = getLineOfOffset( offset );
                bracketPosition = offset - getLineStartOffset( bracketLine );
                return ;
            }
        }
        catch ( BadLocationException bl ) {
            bl.printStackTrace();
        }

        bracketLine = bracketPosition = -1;
    }

    protected void documentChanged( DocumentEvent evt ) {
        DocumentEvent.ElementChange ch = evt.getChange(
                    document.getDefaultRootElement() );

        int count;
        if ( ch == null )
            count = 0;
        else
            count = ch.getChildrenAdded().length -
                    ch.getChildrenRemoved().length;

        int line = getLineOfOffset( evt.getOffset() );
        if ( count == 0 ) {
            painter.invalidateLine( line );
        }
        // do magic stuff
        else
            if ( line < firstLine ) {
                setFirstLine( firstLine + count );
            }
        // end of magic stuff
            else {
                painter.invalidateLineRange( line, firstLine + visibleLines );
                updateScrollBars();
            }
    }

    class ScrollLayout implements LayoutManager {
        public void addLayoutComponent( String name, Component comp ) {
            if ( name.equals( CENTER ) )
                center = comp;
            else
                if ( name.equals( RIGHT ) )
                    right = comp;
                else
                    if ( name.equals( BOTTOM ) )
                        bottom = comp;
                    else
                        if ( name.equals( LEFT_OF_SCROLLBAR ) )
                            leftOfScrollBar.addElement( comp );
        }

        public void removeLayoutComponent( Component comp ) {
            if ( center == comp )
                center = null;
            if ( right == comp )
                right = null;
            if ( bottom == comp )
                bottom = null;
            else
                leftOfScrollBar.removeElement( comp );
        }

        public Dimension preferredLayoutSize( Container parent ) {
            Dimension dim = new Dimension();
            Insets insets = getInsets();
            dim.width = insets.left + insets.right;
            dim.height = insets.top + insets.bottom;

            Dimension centerPref = center.getPreferredSize();
            dim.width += centerPref.width;
            dim.height += centerPref.height;
            Dimension rightPref = right.getPreferredSize();
            dim.width += rightPref.width;
            Dimension bottomPref = bottom.getPreferredSize();
            dim.height += bottomPref.height;

            return dim;
        }

        public Dimension minimumLayoutSize( Container parent ) {
            Dimension dim = new Dimension();
            Insets insets = getInsets();
            dim.width = insets.left + insets.right;
            dim.height = insets.top + insets.bottom;

            Dimension centerPref = center.getMinimumSize();
            dim.width += centerPref.width;
            dim.height += centerPref.height;
            Dimension rightPref = right.getMinimumSize();
            dim.width += rightPref.width;
            Dimension bottomPref = bottom.getMinimumSize();
            dim.height += bottomPref.height;

            return dim;
        }

        public void layoutContainer( Container parent ) {
            Dimension size = parent.getSize();
            Insets insets = parent.getInsets();
            int itop = insets.top;
            int ileft = insets.left;
            int ibottom = insets.bottom;
            int iright = insets.right;

            int rightWidth = right.getPreferredSize().width;
            int bottomHeight = bottom.getPreferredSize().height;
            int centerWidth = size.width - rightWidth - ileft - iright;
            int centerHeight = size.height - bottomHeight - itop - ibottom;

            center.setBounds(
                ileft,
                itop,
                centerWidth,
                centerHeight );

            right.setBounds(
                ileft + centerWidth,
                itop,
                rightWidth,
                centerHeight );

            // Lay out all status components, in order
            Enumeration status = leftOfScrollBar.elements();
            while ( status.hasMoreElements() ) {
                Component comp = ( Component ) status.nextElement();
                Dimension dim = comp.getPreferredSize();
                comp.setBounds( ileft,
                        itop + centerHeight,
                        dim.width,
                        bottomHeight );
                ileft += dim.width;
            }

            bottom.setBounds(
                ileft,
                itop + centerHeight,
                size.width - rightWidth - ileft - iright,
                bottomHeight );
        }

        // private members
        private Component center;
        private Component right;
        private Component bottom;
        private Vector leftOfScrollBar = new Vector();
    }

    static class CaretBlinker implements ActionListener {
        public void actionPerformed( ActionEvent evt ) {
            if ( focusedComponent != null
                    && focusedComponent.hasFocus() )
                focusedComponent.blinkCaret();
        }
    }

    class MutableCaretEvent extends CaretEvent {
        MutableCaretEvent() {
            super( JEditTextArea.this );
        }

        public int getDot() {
            return getCaretPosition();
        }

        public int getMark() {
            return getMarkPosition();
        }
    }

    class AdjustHandler implements AdjustmentListener {
        public void adjustmentValueChanged( final AdjustmentEvent evt ) {
            if ( !scrollBarsInitialized )
                return ;

            // If this is not done, mousePressed events accumilate
            // and the result is that scrolling doesn't stop after
            // the mouse is released
            SwingUtilities.invokeLater( new Runnable() {
                        public void run() {
                            if ( evt.getAdjustable() == vertical )
                                setFirstLine( vertical.getValue() );
                            else
                                setHorizontalOffset( -horizontal.getValue() );
                        }
                    }
                                      );
        }
    }

    class ComponentHandler extends ComponentAdapter {
        public void componentResized( ComponentEvent evt ) {
            recalculateVisibleLines();
            scrollBarsInitialized = true;
        }
    }

    class DocumentHandler implements DocumentListener {
        public void insertUpdate( DocumentEvent evt ) {
            documentChanged( evt );

            int offset = evt.getOffset();
            int length = evt.getLength();

            int newStart;
            int newEnd;

            if ( selectionStart > offset || ( selectionStart
                    == selectionEnd && selectionStart == offset ) )
                newStart = selectionStart + length;
            else
                newStart = selectionStart;

            if ( selectionEnd >= offset )
                newEnd = selectionEnd + length;
            else
                newEnd = selectionEnd;

            select( newStart, newEnd );
        }

        public void removeUpdate( DocumentEvent evt ) {
            documentChanged( evt );

            int offset = evt.getOffset();
            int length = evt.getLength();

            int newStart;
            int newEnd;

            if ( selectionStart > offset ) {
                if ( selectionStart > offset + length )
                    newStart = selectionStart - length;
                else
                    newStart = offset;
            }
            else
                newStart = selectionStart;

            if ( selectionEnd > offset ) {
                if ( selectionEnd > offset + length )
                    newEnd = selectionEnd - length;
                else
                    newEnd = offset;
            }
            else
                newEnd = selectionEnd;

            select( newStart, newEnd );
        }

        public void changedUpdate( DocumentEvent evt ) {
            // no-op
        }
    }

    class DragHandler implements MouseMotionListener {
        public void mouseDragged( MouseEvent evt ) {
            if ( popup != null && popup.isVisible() )
                return ;

            setSelectionRectangular( ( evt.getModifiers()
                    & InputEvent.CTRL_MASK ) != 0 );
            select( getMarkPosition(), xyToOffset( evt.getX(), evt.getY() ) );
        }

    public void mouseMoved( MouseEvent evt ) {}
    }

    class FocusHandler implements FocusListener {
        public void focusGained( FocusEvent evt ) {
            setCaretVisible( true );
            focusedComponent = JEditTextArea.this;
        }

        public void focusLost( FocusEvent evt ) {
            setCaretVisible( false );
            focusedComponent = null;
        }
    }

    class MouseHandler extends MouseAdapter {
        public void mousePressed( MouseEvent evt ) {
            requestFocus();

            // Focus events not fired sometimes?
            setCaretVisible( true );
            focusedComponent = JEditTextArea.this;

            if ( ( evt.getModifiers() & InputEvent.BUTTON3_MASK ) != 0
                    && popup != null ) {
                popup.show( painter, evt.getX(), evt.getY() );
                return ;
            }

            int line = yToLine( evt.getY() );
            int offset = xToOffset( line, evt.getX() );
            int dot = getLineStartOffset( line ) + offset;

            switch ( evt.getClickCount() ) {
                case 1:
                    doSingleClick( evt, line, offset, dot );
                    break;
                case 2:
                    // It uses the bracket matching stuff, so
                    // it can throw a BLE
                    try {
                        doDoubleClick( evt, line, offset, dot );
                    }
                    catch ( BadLocationException bl ) {
                        bl.printStackTrace();
                    }
                    break;
                case 3:
                    doTripleClick( evt, line, offset, dot );
                    break;
            }
        }

        private void doSingleClick( MouseEvent evt, int line,
                int offset, int dot ) {
            if ( ( evt.getModifiers() & InputEvent.SHIFT_MASK ) != 0 ) {
                rectSelect = ( evt.getModifiers() & InputEvent.CTRL_MASK ) != 0;
                select( getMarkPosition(), dot );
            }
            else
                setCaretPosition( dot );
        }

        private void doDoubleClick( MouseEvent evt, int line,
                int offset, int dot ) throws BadLocationException {
            // Ignore empty lines
            if ( getLineLength( line ) == 0 )
                return ;

            try {
                int bracket = TextUtilities.findMatchingBracket(
                            document, Math.max( 0, dot - 1 ) );
                if ( bracket != -1 ) {
                    int mark = getMarkPosition();
                    // Hack
                    if ( bracket > mark ) {
                        bracket++;
                        mark--;
                    }
                    select( mark, bracket );
                    return ;
                }
            }
            catch ( BadLocationException bl ) {
                bl.printStackTrace();
            }

            // Ok, it's not a bracket... select the word
            String lineText = getLineText( line );
            char ch = lineText.charAt( Math.max( 0, offset - 1 ) );

            String noWordSep = ( String ) document.getProperty( "noWordSep" );
            if ( noWordSep == null )
                noWordSep = "";

            // If the user clicked on a non-letter char,
            // we select the surrounding non-letters
            boolean selectNoLetter = ( !Character
                    .isLetterOrDigit( ch )
                    && noWordSep.indexOf( ch ) == -1 );

            int wordStart = 0;

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

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

            int lineStart = getLineStartOffset( line );
            select( lineStart + wordStart, lineStart + wordEnd );

            /*
            String lineText = getLineText(line);
            String noWordSep = (String)document.getProperty("noWordSep");
            int wordStart = TextUtilities.findWordStart(lineText,offset,noWordSep);
            int wordEnd = TextUtilities.findWordEnd(lineText,offset,noWordSep);

            int lineStart = getLineStartOffset(line);
            select(lineStart + wordStart,lineStart + wordEnd);
            */
        }

        private void doTripleClick( MouseEvent evt, int line,
                int offset, int dot ) {
            select( getLineStartOffset( line ), getLineEndOffset( line ) - 1 );
        }
    }

    class MouseWheelHandler implements MouseWheelListener {
        public void mouseWheelMoved(MouseWheelEvent me) {
            if (me.getScrollType() == MouseWheelEvent.WHEEL_UNIT_SCROLL) {
                int totalScrollAmount = me.getUnitsToScroll() * vertical.getUnitIncrement();
                vertical.setValue(vertical.getValue() + totalScrollAmount);
            }
        }
    }

    class CaretUndo extends AbstractUndoableEdit {
        private int start;
        private int end;

        CaretUndo( int start, int end ) {
            this.start = start;
            this.end = end;
        }

        public boolean isSignificant() {
            return false;
        }

        public String getPresentationName() {
            return "caret move";
        }

        public void undo() throws CannotUndoException {
            super.undo();

            select( start, end );
        }

        public void redo() throws CannotRedoException {
            super.redo();

            select( start, end );
        }

        public boolean addEdit( UndoableEdit edit ) {
            if ( edit instanceof CaretUndo ) {
                CaretUndo cedit = ( CaretUndo ) edit;
                start = cedit.start;
                end = cedit.end;
                cedit.die();

                return true;
            }
            else
                return false;
        }
    }

    static
    {
        caretTimer = new Timer( 500, new CaretBlinker() );
        caretTimer.setInitialDelay( 500 );
        caretTimer.start();
    }
}
