001/* JTextArea.java -- 
002   Copyright (C) 2004, 2005  Free Software Foundation, Inc.
003
004This file is part of GNU Classpath.
005
006GNU Classpath is free software; you can redistribute it and/or modify
007it under the terms of the GNU General Public License as published by
008the Free Software Foundation; either version 2, or (at your option)
009any later version.
010
011GNU Classpath is distributed in the hope that it will be useful, but
012WITHOUT ANY WARRANTY; without even the implied warranty of
013MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
014General Public License for more details.
015
016You should have received a copy of the GNU General Public License
017along with GNU Classpath; see the file COPYING.  If not, write to the
018Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
01902110-1301 USA.
020
021Linking this library statically or dynamically with other modules is
022making a combined work based on this library.  Thus, the terms and
023conditions of the GNU General Public License cover the whole
024combination.
025
026As a special exception, the copyright holders of this library give you
027permission to link this library with independent modules to produce an
028executable, regardless of the license terms of these independent
029modules, and to copy and distribute the resulting executable under
030terms of your choice, provided that you also meet, for each linked
031independent module, the terms and conditions of the license of that
032module.  An independent module is a module which is not derived from
033or based on this library.  If you modify this library, you may extend
034this exception to your version of the library, but you are not
035obligated to do so.  If you do not wish to do so, delete this
036exception statement from your version. */
037
038
039package javax.swing;
040
041import java.awt.Dimension;
042import java.awt.FontMetrics;
043import java.awt.Rectangle;
044
045import javax.accessibility.AccessibleContext;
046import javax.accessibility.AccessibleStateSet;
047import javax.swing.text.BadLocationException;
048import javax.swing.text.Document;
049import javax.swing.text.Element;
050import javax.swing.text.JTextComponent;
051import javax.swing.text.PlainDocument;
052import javax.swing.text.View;
053
054/**
055 * The <code>JTextArea</code> component provides a multi-line area for displaying
056 * and editing plain text.  The component is designed to act as a lightweight
057 * replacement for the heavyweight <code>java.awt.TextArea</code> component,
058 * which provides similar functionality using native widgets.
059 * <p>
060 *
061 * This component has additional functionality to the AWT class.  It follows
062 * the same design pattern as seen in other text components, such as
063 * <code>JTextField</code>, <code>JTextPane</code> and <code>JEditorPane</code>,
064 * and embodied in <code>JTextComponent</code>.  These classes separate the text
065 * (the model) from its appearance within the onscreen component (the view).  The
066 * text is held within a <code>javax.swing.text.Document</code> object, which can
067 * also maintain relevant style information where necessary.  As a result, it is the
068 * document that should be monitored for textual changes, via
069 * <code>DocumentEvent</code>s delivered to registered
070 * <code>DocumentListener</code>s, rather than this component.
071 * <p>
072 *
073 * Unlike <code>java.awt.TextArea</code>, <code>JTextArea</code> does not
074 * handle scrolling.  Instead, this functionality is delegated to a
075 * <code>JScrollPane</code>, which can contain the text area and handle
076 * scrolling when required.  Likewise, the word wrapping functionality
077 * of the AWT component is converted to a property of this component
078 * and the <code>rows</code> and <code>columns</code> properties
079 * are used in calculating the preferred size of the scroll pane's
080 * view port.
081 *
082 * @author Michael Koch  (konqueror@gmx.de)
083 * @author Andrew John Hughes  (gnu_andrew@member.fsf.org)
084 * @see java.awt.TextArea
085 * @see javax.swing.text.JTextComponent
086 * @see javax.swing.JTextField
087 * @see javax.swing.JTextPane
088 * @see javax.swing.JEditorPane
089 * @see javax.swing.text.Document
090 * @see javax.swing.event.DocumentEvent
091 * @see javax.swing.event.DocumentListener
092 */
093
094public class JTextArea extends JTextComponent
095{
096  /**
097   * Provides accessibility support for <code>JTextArea</code>.
098   *
099   * @author Roman Kennke (kennke@aicas.com)
100   */
101  protected class AccessibleJTextArea extends AccessibleJTextComponent
102  {
103
104    /**
105     * Creates a new <code>AccessibleJTextArea</code> object.
106     */
107    protected AccessibleJTextArea()
108    {
109      super();
110    }
111
112    /**
113     * Returns the accessible state of this <code>AccessibleJTextArea</code>.
114     *
115     * @return  the accessible state of this <code>AccessibleJTextArea</code>
116     */
117    public AccessibleStateSet getAccessibleStateSet()
118    {
119      AccessibleStateSet state = super.getAccessibleStateSet();
120      // TODO: Figure out what state must be added here to the super's state.
121      return state;
122    }
123  }
124
125  /**
126   * Compatible with Sun's JDK
127   */
128  private static final long serialVersionUID = -6141680179310439825L;
129  
130  /**
131   * The number of rows used by the component.
132   */
133  private int rows;
134
135  /**
136   * The number of columns used by the component.
137   */
138  private int columns;
139
140  /**
141   * Whether line wrapping is enabled or not.
142   */
143  private boolean lineWrap;
144
145  /**
146   * The number of characters equal to a tab within the text.
147   */
148  private int tabSize = 8;
149
150  private boolean wrapStyleWord;
151
152  /**
153   * Creates a new <code>JTextArea</code> object.
154   */
155  public JTextArea()
156  {
157    this(null, null, 0, 0);
158  }
159
160  /**
161   * Creates a new <code>JTextArea</code> object.
162   *
163   * @param text the initial text
164   */
165  public JTextArea(String text)
166  {
167    this(null, text, 0, 0);
168  }
169
170  /**
171   * Creates a new <code>JTextArea</code> object.
172   *
173   * @param rows the number of rows
174   * @param columns the number of cols
175   *
176   * @exception IllegalArgumentException if rows or columns are negative
177   */
178  public JTextArea(int rows, int columns)
179  {
180    this(null, null, rows, columns);
181  }
182
183  /**
184   * Creates a new <code>JTextArea</code> object.
185   *
186   * @param text the initial text
187   * @param rows the number of rows
188   * @param columns the number of cols
189   *
190   * @exception IllegalArgumentException if rows or columns are negative
191   */
192  public JTextArea(String text, int rows, int columns)
193  {
194    this(null, text, rows, columns);
195  }
196
197  /**
198   * Creates a new <code>JTextArea</code> object.
199   *
200   * @param doc the document model to use
201   */
202  public JTextArea(Document doc)
203  {
204    this(doc, null, 0, 0);
205  }
206
207  /**
208   * Creates a new <code>JTextArea</code> object.
209   *
210   * @param doc the document model to use
211   * @param text the initial text
212   * @param rows the number of rows
213   * @param columns the number of cols
214   *
215   * @exception IllegalArgumentException if rows or columns are negative
216   */
217  public JTextArea(Document doc, String text, int rows, int columns)
218  {
219    setDocument(doc == null ? createDefaultModel() : doc);
220    // Only explicitly setText() when there is actual text since
221    // setText() might be overridden and not expected to be called
222    // from the constructor (as in JEdit).
223    if (text != null)
224      setText(text);
225    setRows(rows);
226    setColumns(columns);
227  }
228
229  /**
230   * Appends the supplied text to the current contents
231   * of the document model.
232   *
233   * @param toAppend the text to append
234   */
235  public void append(String toAppend)
236  {
237      try
238          {
239              getDocument().insertString(getText().length(), toAppend, null);
240          }
241      catch (BadLocationException exception)
242          {
243              /* This shouldn't happen in theory -- but, if it does...  */
244              throw new RuntimeException("Unexpected exception occurred.", exception);
245          }
246      if (toAppend != null && toAppend.length() > 0)
247        revalidate();
248  }
249
250  /**
251   * Creates the default document model.
252   *
253   * @return a new default model
254   */
255  protected Document createDefaultModel()
256  {
257    return new PlainDocument();
258  }
259
260  /**
261   * Returns true if the width of this component should be forced
262   * to match the width of a surrounding view port.  When line wrapping
263   * is turned on, this method returns true.
264   *
265   * @return true if lines are wrapped.
266   */
267  public boolean getScrollableTracksViewportWidth()
268  {
269    return lineWrap ? true : super.getScrollableTracksViewportWidth();
270  }
271
272  /**
273   * Returns the increment that is needed to expose exactly one new line
274   * of text. This is implemented here to return the values of
275   * {@link #getRowHeight} and {@link #getColumnWidth}, depending on
276   * the value of the argument <code>direction</code>.
277   *
278   * @param visibleRect the view area that is visible in the viewport
279   * @param orientation either {@link SwingConstants#VERTICAL} or
280   *     {@link SwingConstants#HORIZONTAL}
281   * @param direction less than zero for up/left scrolling, greater
282   *     than zero for down/right scrolling
283   *
284   * @return the increment that is needed to expose exactly one new row
285   *     or column of text
286   *
287   * @throws IllegalArgumentException if <code>orientation</code> is invalid
288   */
289  public int getScrollableUnitIncrement(Rectangle visibleRect, int orientation,
290                                        int direction)
291  {
292    if (orientation == SwingConstants.VERTICAL)
293      return getRowHeight();
294    else if (orientation == SwingConstants.HORIZONTAL)
295      return getColumnWidth();
296    else
297      throw new IllegalArgumentException("orientation must be either "
298                                     + "javax.swing.SwingConstants.VERTICAL "
299                                     + "or "
300                                     + "javax.swing.SwingConstants.HORIZONTAL"
301                                     );
302  }
303
304  /**
305   * Returns the preferred size of that text component in the case
306   * it is embedded within a JScrollPane. This uses the column and
307   * row settings if they are explicitly set, or fall back to
308   * the superclass's behaviour.
309   *
310   * @return the preferred size of that text component in the case
311   *     it is embedded within a JScrollPane
312   */
313  public Dimension getPreferredScrollableViewportSize()
314  {
315    if ((rows > 0) && (columns > 0))
316      return new Dimension(columns * getColumnWidth(), rows * getRowHeight());
317    else
318      return super.getPreferredScrollableViewportSize();
319  }
320
321  /**
322   * Returns the UI class ID string.
323   *
324   * @return the string "TextAreaUI"
325   */
326  public String getUIClassID()
327  {
328    return "TextAreaUI";
329  }
330
331  /**
332   * Returns the current number of columns.
333   *
334   * @return number of columns
335   */
336  public int getColumns()
337  {
338    return columns;
339  }
340  
341  /**
342   * Sets the number of rows.
343   *
344   * @param columns number of columns
345   *
346   * @exception IllegalArgumentException if columns is negative
347   */
348  public void setColumns(int columns)
349  {
350    if (columns < 0)
351      throw new IllegalArgumentException();
352    
353    if (columns != this.columns)
354      {
355        this.columns = columns;
356        revalidate();
357      }
358  }
359
360  /**
361   * Returns the current number of rows.
362   *
363   * @return number of rows
364   */
365  public int getRows()
366  {
367    return rows;
368  }
369
370  /**
371   * Sets the number of rows.
372   *
373   * @param rows number of rows
374   *
375   * @exception IllegalArgumentException if rows is negative
376   */
377  public void setRows(int rows)
378  {
379    if (rows < 0)
380      throw new IllegalArgumentException();
381   
382    if (rows != this.rows)
383      {
384        this.rows = rows;
385        revalidate();
386      }
387  }
388
389  /**
390   * Checks whether line wrapping is enabled.
391   *
392   * @return <code>true</code> if line wrapping is enabled,
393   * <code>false</code> otherwise
394   */
395  public boolean getLineWrap()
396  {
397    return lineWrap;
398  }
399
400  /**
401   * Enables/disables line wrapping.
402   *
403   * @param flag <code>true</code> to enable line wrapping,
404   * <code>false</code> otherwise
405   */
406  public void setLineWrap(boolean flag)
407  {
408    if (lineWrap == flag)
409      return;
410
411    boolean oldValue = lineWrap;
412    lineWrap = flag;
413    firePropertyChange("lineWrap", oldValue, lineWrap);
414  }
415
416  /**
417   * Checks whether word style wrapping is enabled.
418   *
419   * @return <code>true</code> if word style wrapping is enabled,
420   * <code>false</code> otherwise
421   */
422  public boolean getWrapStyleWord()
423  {
424    return wrapStyleWord;
425  }
426  
427  /**
428   * Enables/Disables word style wrapping.
429   *
430   * @param flag <code>true</code> to enable word style wrapping,
431   * <code>false</code> otherwise
432   */
433  public void setWrapStyleWord(boolean flag)
434  {
435    if (wrapStyleWord == flag)
436      return;
437    
438    boolean oldValue = wrapStyleWord;
439    wrapStyleWord = flag;
440    firePropertyChange("wrapStyleWord", oldValue, wrapStyleWord);
441  }
442  
443  /**
444   * Returns the number of characters used for a tab.
445   * This defaults to 8.
446   *
447   * @return the current number of spaces used for a tab.
448   */
449  public int getTabSize()
450  {
451    return tabSize;
452  }
453
454  /**
455   * Sets the number of characters used for a tab to the
456   * supplied value.  If a change to the tab size property
457   * occurs (i.e. newSize != tabSize), a property change event
458   * is fired.
459   * 
460   * @param newSize The new number of characters to use for a tab.
461   */
462  public void setTabSize(int newSize)
463  {
464    if (tabSize == newSize)
465      return;
466    
467    int oldValue = tabSize;
468    tabSize = newSize;
469    firePropertyChange("tabSize", oldValue, tabSize);
470  }
471
472  protected int getColumnWidth()
473  {
474    FontMetrics metrics = getToolkit().getFontMetrics(getFont());
475    return metrics.charWidth('m');
476  }
477
478  public int getLineCount()
479  {
480    return getDocument().getDefaultRootElement().getElementCount();
481  }
482
483  public int getLineStartOffset(int line)
484     throws BadLocationException
485  {
486    int lineCount = getLineCount();
487    
488    if (line < 0 || line > lineCount)
489      throw new BadLocationException("Non-existing line number", line);
490
491    Element lineElem = getDocument().getDefaultRootElement().getElement(line);
492    return lineElem.getStartOffset();
493  }
494
495  public int getLineEndOffset(int line)
496     throws BadLocationException
497  {
498    int lineCount = getLineCount();
499    
500    if (line < 0 || line > lineCount)
501      throw new BadLocationException("Non-existing line number", line);
502
503    Element lineElem = getDocument().getDefaultRootElement().getElement(line);
504    return lineElem.getEndOffset();
505  }
506
507  public int getLineOfOffset(int offset)
508    throws BadLocationException
509  {
510    Document doc = getDocument();
511
512    if (offset < doc.getStartPosition().getOffset()
513        || offset >= doc.getEndPosition().getOffset())
514      throw new BadLocationException("offset outside of document", offset);
515
516    return doc.getDefaultRootElement().getElementIndex(offset);
517  }
518
519  protected int getRowHeight()
520  {
521    FontMetrics metrics = getToolkit().getFontMetrics(getFont());
522    return metrics.getHeight();
523  }
524
525  /**
526   * Inserts the supplied text at the specified position.  Nothing
527   * happens in the case that the model or the supplied string is null
528   * or of zero length.
529   *
530   * @param string The string of text to insert.
531   * @param position The position at which to insert the supplied text.
532   * @throws IllegalArgumentException if the position is &lt; 0 or greater
533   * than the length of the current text.
534   */
535  public void insert(String string, int position)
536  {
537    // Retrieve the document model.
538    Document doc = getDocument();
539      
540    // Check the model and string for validity.
541    if (doc == null
542        || string == null
543        || string.length() == 0)
544      return;
545
546    // Insert the text into the model.
547    try
548      {
549        doc.insertString(position, string, null);
550      }
551    catch (BadLocationException e)
552      {
553        throw new IllegalArgumentException("The supplied position, "
554                                           + position + ", was invalid.");
555      }
556  }
557
558  public void replaceRange(String text, int start, int end)
559  {
560    Document doc = getDocument();
561    
562    if (start > end
563        || start < doc.getStartPosition().getOffset()
564        || end >= doc.getEndPosition().getOffset())
565      throw new IllegalArgumentException();
566
567    try
568      {
569        doc.remove(start, end - start);
570        doc.insertString(start, text, null);
571      }
572    catch (BadLocationException e)
573      {
574        // This cannot happen as we check offset above.
575      }
576  }
577
578  /**
579   * Returns the preferred size for the JTextArea. This is the maximum of
580   * the size that is needed to display the content and the requested size
581   * as per {@link #getColumns} and {@link #getRows}.
582   *
583   * @return the preferred size of the JTextArea
584   */
585  public Dimension getPreferredSize()
586  {
587    int reqWidth = getColumns() * getColumnWidth();
588    int reqHeight = getRows() * getRowHeight();
589    View view = getUI().getRootView(this);
590    int neededWidth = (int) view.getPreferredSpan(View.HORIZONTAL);
591    int neededHeight = (int) view.getPreferredSpan(View.VERTICAL);
592    return new Dimension(Math.max(reqWidth, neededWidth),
593                          Math.max(reqHeight, neededHeight));
594  }
595
596  /**
597   * Returns the accessible context associated with the <code>JTextArea</code>.
598   *
599   * @return the accessible context associated with the <code>JTextArea</code>
600   */
601  public AccessibleContext getAccessibleContext()
602  {
603    if (accessibleContext == null)
604      accessibleContext = new AccessibleJTextArea();
605    return accessibleContext;
606  }
607}