/* class JTextArea * * Copyright (C) 2001 R M Pitman * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ package charvax.swing; import charva.awt.*; import charva.awt.event.KeyEvent; import charva.awt.event.MouseEvent; import charva.awt.event.ScrollEvent; import charva.awt.event.ScrollListener; import java.util.Enumeration; import java.util.Vector; /** * JTextArea is an (optionally editable) multi-line area that displays * plain text. * The JTextArea class implements the Scrollable interface, which enables * it to be placed inside a charvax.swing.JScrollPane. In fact, in the * CHARVA framework it should ALWAYS be used inside a JScrollPane, otherwise * it will be unusable (its size depends on the text it contains).<p> * <p/> * Note that, unlike the javax.swing.JTextArea, pressing the TAB key while * the keyboard focus is in the JTextArea will not cause a tab to be * inserted; instead, it will move the keyboard input focus to the next * focus-traversable component (if there is one). This is because (in * javax.swing) the user can user can use the mouse to move the keyboard * input focus away from the JTextArea, whereas CHARVA has no mouse support. */ public class JTextArea extends charvax.swing.text.JTextComponent implements charva.awt.Scrollable { /** * The default constructor creates an empty text area with 10 rows * and 10 columns. */ public JTextArea() { this( "" ); } /** * Construct a text area with 10 rows and 10 columns, and containing * the specified text. */ public JTextArea( String text_ ) { this( text_, 10, 10 ); } /** * Construct a text area wth the specified number of rows and columns, * and containing the specified text. */ public JTextArea( String text_, int rows_, int columns_ ) { setDocument( text_ ); _rows = rows_; _preferredRows = rows_; _columns = columns_; _preferredColumns = columns_; setCaretPosition( 0 ); } /** * Sets the number of columns in this JTextArea. */ public void setColumns( int columns_ ) { _columns = columns_; _preferredColumns = columns_; } /** * Returns the number of columns in this JTextArea. */ public int getColumns() { return _preferredColumns; } /** * Sets the number of rows in this JTextArea. */ public void setRows( int rows_ ) { _rows = rows_; _preferredRows = rows_; } /** * Returns the number of rows in this JTextArea. */ public int getRows() { return _preferredRows; } /** * Returns the size of this component. */ public Dimension getSize() { return new Dimension( _columns, _rows ); } public int getWidth() { return _columns; } public int getHeight() { return _rows; } /** * Appends the specified text to the end of the document. */ public synchronized void append( String text_ ) { super._document.append( text_ ); _caretPosition = super._document.length(); refresh(); } /** * Inserts the specified text at the specified position (ie at the * specified offset from the start of the document).. */ public synchronized void insert( String text_, int pos_ ) { super._document.insert( pos_, text_ ); _caretPosition = pos_ + text_.length(); refresh(); } /** * Inserts the specified character at the specified position (ie at the * specified offset from the start of the document).. */ public synchronized void insert( char ch, int pos_ ) { try { super._document.insert( pos_, ch ); _caretPosition = pos_ + 1; refresh(); } catch( StringIndexOutOfBoundsException sioobe ) { System.err.println( "Insert ch=" + ch + ", pos=" + pos_ + " in document of length" + super._document.length() + " failed" ); } } /** * Replaces the text from the specified start position to end position * with the specified text. */ public synchronized void replaceRange( String text_, int start_, int end_ ) { super._document.replace( start_, end_, text_ ); _caretPosition = start_ + text_.length(); refresh(); } /** * Sets the position of the text insertion caret for this JTextArea. */ public void setCaretPosition( int caret_ ) { super.setCaretPosition( caret_ ); refresh(); } /** * Returns the number of lines of text displayed in the JTextArea. */ public int getLineCount() { return offsetCalc( LINE_COUNT, 0 ); } /** * Returns the offset of the first character in the specified line * of text. */ public int getLineStartOffset( int line_ ) { return offsetCalc( LINE_START_OFFSET, line_ ); } /** * Returns the offset of the last character in the specified line. */ public int getLineEndOffset( int line_ ) { return offsetCalc( LINE_END_OFFSET, line_ ); } /** * Translates an offset (relative to the start of the document) * to a line number. */ public int getLineOfOffset( int offset_ ) { return offsetCalc( LINE_OF_OFFSET, offset_ ); } /** * Sets the line-wrapping policy of the JTextArea. If set to true, * lines will be wrapped if they are too long to fit within the * allocated width. If set to false, the lines will always be * unwrapped. The default value of this property is false. */ public void setLineWrap( boolean wrap_ ) { _lineWrap = wrap_; _rows = _preferredRows; _columns = _preferredColumns; } /** * Returns the line-wrapping policy of the JTextArea. If set to true, * lines will be wrapped if they are too long to fit within the * allocated width. If set to false, the lines will always be * unwrapped. */ public boolean getLineWrap() { return _lineWrap; } /** * Sets the line-wrapping style to be used if getLineWrap() is true. * If true, lines will be wrapped at word boundaries (whitespace) if * they are too long to fit in the allocated number of columns. If * false, lines will be wrapped at character boundaries. The default * value of this property is false. */ public void setWrapStyleWord( boolean wrapWord_ ) { _wrapStyleWord = wrapWord_; } /** * Returns the line-wrapping style to be used if getLineWrap() is true. * If true, lines will be wrapped at word boundaries (whitespace) if * they are too long to fit in the allocated number of columns. If * false, lines will be wrapped at character boundaries. */ public boolean getWrapStyleWord() { return _wrapStyleWord; } /** * Called by the LayoutManager. */ public Dimension minimumSize() { return getSize(); } /** * Process KeyEvents that have been generated by this JTextArea. */ public void processKeyEvent( KeyEvent ke_ ) { /* First call all KeyListener objects that may have been registered * for this component. */ super.processKeyEvent( ke_ ); /* Check if any of the KeyListeners consumed the KeyEvent. */ if( ke_.isConsumed() ) { return; } int caret = getCaretPosition(); int line = getLineOfOffset( caret ); int key = ke_.getKeyCode(); if( key == '\t' ) { getParent().nextFocus(); return; } else if( key == KeyEvent.VK_BACK_TAB ) { getParent().previousFocus(); return; } else if( key == KeyEvent.VK_LEFT && caret > 0 ) { setCaretPosition( caret - 1 ); } else if( key == KeyEvent.VK_RIGHT && caret < getDocument().length() ) { setCaretPosition( caret + 1 ); } else if( key == KeyEvent.VK_HOME ) { int lineStart = getLineStartOffset( line ); setCaretPosition( lineStart ); } else if( key == KeyEvent.VK_END ) { int lineEnd = getLineEndOffset( line ); setCaretPosition( lineEnd ); } else if( ( key == KeyEvent.VK_PAGE_UP || key == KeyEvent.VK_PAGE_DOWN ) && ( getParent() instanceof JViewport ) ) { JViewport viewport = (JViewport)getParent(); int vertical_offset = -1 * viewport.getViewPosition().y; int viewport_height = viewport.getSize().height; if( key == KeyEvent.VK_PAGE_UP ) { if( line > vertical_offset ) { line = vertical_offset; } else { line = vertical_offset - viewport_height; } line = ( line < 0 ) ? 0 : line; } else { if( line < vertical_offset + viewport_height - 1 ) { line = vertical_offset + viewport_height - 1; } else { line = vertical_offset + ( 2 * viewport_height ) - 1; } line = ( line > getLineCount() - 1 ) ? ( getLineCount() - 1 ) : line; } setCaretPosition( getLineStartOffset( line ) ); } else if( key == KeyEvent.VK_UP && line > 0 ) { int column = caret - getLineStartOffset( line ); int prevlineStart = getLineStartOffset( line - 1 ); int prevlineEnd = getLineEndOffset( line - 1 ); if( column > prevlineEnd - prevlineStart ) { column = prevlineEnd - prevlineStart; } setCaretPosition( prevlineStart + column ); } else if( key == KeyEvent.VK_DOWN && line < getLineCount() - 1 ) { int column = caret - getLineStartOffset( line ); int nextlineStart = getLineStartOffset( line + 1 ); int nextlineEnd = getLineEndOffset( line + 1 ); if( column > nextlineEnd - nextlineStart ) { column = nextlineEnd - nextlineStart; } setCaretPosition( nextlineStart + column ); } else if( super.isEditable() == false ) { Toolkit.getDefaultToolkit().beep(); } else if( key >= ' ' && key <= 0177 ) { insert( (char)key, caret ); } else if( key == KeyEvent.VK_ENTER ) { insert( '\n', caret ); } else if( key == KeyEvent.VK_BACK_SPACE && caret > 0 ) { replaceRange( "", caret - 1, caret ); } else if( key == KeyEvent.VK_DELETE && caret < getDocument().length() - 1 ) { replaceRange( "", caret, caret + 1 ); } /* If this JTextArea is contained in a JViewport, let the JViewport * do the drawing, after setting the clip rectangle. */ if( ( getParent() instanceof JViewport ) == false ) { draw( Toolkit.getDefaultToolkit() ); requestFocus(); super.requestSync(); } } /** * Process a MouseEvent that was generated by clicking the mouse * somewhere inside this JTextArea. * Clicking the mouse inside the JTextArea moves the caret position * to where the mouse was clicked. */ public void processMouseEvent( MouseEvent e_ ) { super.processMouseEvent( e_ ); if( e_.getButton() == MouseEvent.BUTTON1 && e_.getModifiers() == MouseEvent.MOUSE_CLICKED && this.isFocusTraversable() ) { /* Get the absolute origin of this component. */ Point origin = getLocationOnScreen(); Insets insets = super.getInsets(); origin.translate( insets.left, insets.top ); int line = e_.getY() - origin.y; if( line > getLineCount() - 1 ) { return; } int column = e_.getX() - origin.x; int lineStart = getLineStartOffset( line ); int lineEnd = getLineEndOffset( line ); if( column > lineEnd - lineStart ) { column = lineEnd - lineStart; } setCaretPosition( lineStart + column ); repaint(); } } /** * Implements the abstract method in charva.awt.Component. * * @param toolkit */ public void draw( Toolkit toolkit ) { Point tempCaret = null; Point caret = _caret; /* Get the absolute origin of this component. */ Point origin = getLocationOnScreen(); int colorpair = getCursesColor(); /* Start by blanking out the text area */ toolkit.blankBox( origin, getSize(), colorpair ); toolkit.setCursor( origin ); StringBuffer charBuffer = new StringBuffer(); /* Scan through the entire document, drawing each character in it. */ ScrollEvent scrollevent = null; int row = 0, col = 0; // outerloop: for( int i = 0; i < super._document.length(); i++ ) { /* At some point during the scan of the document, the * caret position should match the scan index, unless the caret * position is after the last character of the document. */ if( _caretPosition == i ) { tempCaret = new Point( col, row ); /* If the caret has changed, generate a ScrollEvent. Note * that this method may be called multiple times during the * scan; however, we must post only the last event generated. */ if( tempCaret.equals( caret ) == false ) { scrollevent = generateScrollEvent( tempCaret, new Point( col, row ) ); caret = tempCaret; } } char chr = super._document.charAt( i ); if( col < _columns ) { if( chr == '\n' ) { col = 0; row++; if( row >= _rows ) { _rows++; } if(charBuffer.length() > 0){ toolkit.addString( charBuffer.toString(), 0, colorpair ); charBuffer.setLength(0); } toolkit.setCursor( origin.addOffset( col, row ) ); } else { charBuffer.append(chr); //toolkit.addChar( chr, 0, colorpair ); col++; } } else { // We have reached the right-hand column. if(charBuffer.length() > 0){ toolkit.addString( charBuffer.toString(), 0, colorpair ); charBuffer.setLength(0); } if( _lineWrap == false ) { if( chr == '\n' ) { col = 0; row++; if( row >= _rows ) { _rows++; } toolkit.setCursor( origin.addOffset( col, row ) ); } else { toolkit.addChar( chr, 0, colorpair ); col++; _columns++; } } else { // line-wrap is true if( _wrapStyleWord == false ) { col = 0; row++; if( row >= _rows ) { _rows++; } toolkit.setCursor( origin.addOffset( col, row ) ); if( chr != '\n' ) // thanks to Chris Rogers for this { toolkit.addChar( chr, 0, colorpair ); } } else { /* We must back-track until we get to whitespace, so * that we can move the word to the next line. */ int j; for( j = 0; j < _columns; j++ ) { char tmpchr = super._document.charAt( i - j ); if( tmpchr == ' ' || tmpchr == '\t' ) { deleteEOL( toolkit, col - j, row, colorpair ); col = 0; row++; if( row >= _rows ) { _rows++; } i -= j; toolkit.setCursor( origin.addOffset( col, row ) ); break; } } if( j == _columns ) { // the word was too long if( chr == ' ' || chr == '\n' || chr == '\t' ) { col = 0; row++; if( row >= _rows ) { _rows++; } toolkit.setCursor( origin.addOffset( col, row ) ); } } } } // end if line-wrap is true } // end if we have reached the right-hand column } // end FOR loop. if(charBuffer.length() > 0){ toolkit.addString( charBuffer.toString(), 0, colorpair ); charBuffer.setLength(0); } /* Check for the case where the caret position is after the last * character of the document. */ if( _caretPosition == super._document.length() ) { tempCaret = new Point( col, row ); /* If the caret has changed, generate a ScrollEvent */ if( tempCaret.equals( caret ) == false ) { scrollevent = generateScrollEvent( tempCaret, new Point( col, row ) ); } caret = tempCaret; } /* Post a ScrollEvent, if one was generated; but only if the * caret has really changed. We have to be careful to avoid an * endless loop, where a ScrollEvent triggers a draw(), which * triggers an unnecessary ScrollEvent and so on. */ if( ( _caret.equals( caret ) == false ) && scrollevent != null ) { toolkit.getSystemEventQueue().postEvent( scrollevent ); _caret = caret; } } public void requestFocus() { /* Generate the FOCUS_GAINED event. */ super.requestFocus(); /* Get the absolute origin of this component. */ Point origin = getLocationOnScreen(); Toolkit.getDefaultToolkit().setCursor( origin.addOffset( _caret ) ); } /** * Register a ScrollListener object for this JTextArea. */ public void addScrollListener( ScrollListener sl_ ) { if( _scrollListeners == null ) { _scrollListeners = new Vector<ScrollListener>(); } _scrollListeners.add( sl_ ); } /** * Remove a ScrollListener object that is registered for this JTextArea. */ public void removeScrollListener( ScrollListener sl_ ) { if( _scrollListeners == null ) { return; } _scrollListeners.remove( sl_ ); } /** * Process scroll events generated by this JTextArea. */ public void processScrollEvent( ScrollEvent e_ ) { if( _scrollListeners != null ) { for( Enumeration<ScrollListener> e = _scrollListeners.elements(); e.hasMoreElements(); ) { ScrollListener sl = e.nextElement(); sl.scroll( e_ ); } } } /** * Returns the preferred size of the viewport for this JTextArea * when it is in a JScrollPane (this method implements the * Scrollable interface). The size is determined by the number of * rows and columns set for this JTextArea (either in the constructor * or in the setColumns() and setRows() methods). */ public Dimension getPreferredScrollableViewportSize() { return new Dimension( _preferredColumns, _preferredRows ); } public void debug( int level_ ) { for( int i = 0; i < level_; i++ ) { System.err.print( " " ); } System.err.println( "JTextArea origin=" + _origin + " size=" + getSize() ); } /** * A private helper method to delete from a specified position * until the end of the line. */ private void deleteEOL( Toolkit term_, int col_, int row_, int colorpair_ ) { Point origin = getLocationOnScreen(); term_.setCursor( origin.addOffset( col_, row_ ) ); for( int i = col_; i < _columns; i++ ) { term_.addChar( ' ', 0, colorpair_ ); } } /** * This helper method converts offset to line number and * vice versa. */ private int offsetCalc( int mode_, int value_ ) { int lineOfOffset = 0; int row = 0; if( mode_ == LINE_START_OFFSET && value_ == 0 ) { return 0; } for( int col = 0, i = 0; i < super._document.length(); i++ ) { if( mode_ == LINE_OF_OFFSET && value_ == i ) { lineOfOffset = row; } char chr = super._document.charAt( i ); if( col < _columns ) { if( chr == '\n' ) { col = 0; row++; } else { col++; } } else { // We have reached the right-hand column. if( _lineWrap == false ) { if( chr == '\n' ) { col = 0; row++; } } else { // line-wrap is true if( _wrapStyleWord == false ) { col = 0; row++; } else { /* We must back-track until we get to whitespace, so * that we can move the word to the next line. */ int j; for( j = 0; j < _columns; j++ ) { char tmpchr = super._document.charAt( i - j ); if( tmpchr == ' ' || tmpchr == '\t' ) { col = 0; row++; i -= j; break; } } if( j == _columns ) { // the word was too long if( chr == ' ' || chr == '\n' || chr == '\t' ) { col = 0; row++; } } } } // end if line-wrap is true } // end if we have reached the right-hand column if( mode_ == LINE_START_OFFSET && col == 0 && row == value_ ) { return i + 1; } else if( mode_ == LINE_END_OFFSET && col == 0 && row == value_ + 1 ) { return i; } } // end FOR loop. if( mode_ == LINE_OF_OFFSET ) { if( value_ == super._document.length() ) { return row; } else { return lineOfOffset; } } else if( mode_ == LINE_COUNT ) { return row + 1; } else if( mode_ == LINE_END_OFFSET && row == value_ ) { return super._document.length(); } else { throw new IndexOutOfBoundsException( "Invalid offset or line number: mode=" + mode_ + " value=" + value_ + " row=" + row + " doc=\"" + _document + "\"" ); } } /* Private helper method used to redraw the component if its state * has changed. */ @SuppressWarnings("unused") private void refreshOrig() { /* If this JTextArea is contained in a JViewport, the PaintEvent * that we post must request a redraw of the JViewport, so that * the JViewport can set the clipping rectangle before calling the * draw() method of this JTextArea. */ Component todraw; if( getParent() instanceof JViewport ) { todraw = getParent(); } else { todraw = this; } /* If this component is already displayed, generate a PaintEvent * and post it onto the queue. */ todraw.repaint(); } private void refresh() { /* If this JTextArea is contained in a JViewport, the PaintEvent * that we post must request a redraw of the JViewport, so that * the JViewport can set the clipping rectangle before calling the * draw() method of this JTextArea. */ Component todraw; Component parent = getParent(); if( parent == null ) { this.repaint(); } else { if( parent instanceof JViewport ) { todraw = parent; } else { todraw = this; } /* If this component is already displayed, generate a PaintEvent * and post it onto the queue. */ todraw.draw( Toolkit.getDefaultToolkit() ); } } /** * Private method, called whenever the caret changes. */ private ScrollEvent generateScrollEvent( Point tempCaret_, Point col_row_ ) { int direction; /* Determine the direction of scrolling */ if( tempCaret_.y > _caret.y ) { if( tempCaret_.x > _caret.x ) { direction = ScrollEvent.UP_LEFT; } else if( tempCaret_.x < _caret.x ) { direction = ScrollEvent.UP_RIGHT; } else { direction = ScrollEvent.UP; } } else if( tempCaret_.y < _caret.y ) { if( tempCaret_.x > _caret.x ) { direction = ScrollEvent.DOWN_LEFT; } else if( tempCaret_.x < _caret.x ) { direction = ScrollEvent.DOWN_RIGHT; } else { direction = ScrollEvent.DOWN; } } else { if( tempCaret_.x > _caret.x ) { direction = ScrollEvent.LEFT; } else { direction = ScrollEvent.RIGHT; } } return new ScrollEvent( this, direction, col_row_ ); } //==================================================================== // INSTANCE VARIABLES private int _rows; private int _columns; private int _preferredRows; private int _preferredColumns; /** * The caret is updated only when the component is drawn. */ private Point _caret = new Point( 0, 0 ); private boolean _lineWrap; private boolean _wrapStyleWord = false; /** * A list of ScrollListeners registered for this JTextArea. */ private Vector<ScrollListener> _scrollListeners = null; private static final int LINE_COUNT = 1; private static final int LINE_START_OFFSET = 2; private static final int LINE_END_OFFSET = 3; private static final int LINE_OF_OFFSET = 4; }