001    /* JTextArea.java -- 
002       Copyright (C) 2004, 2005  Free Software Foundation, Inc.
003    
004    This file is part of GNU Classpath.
005    
006    GNU Classpath is free software; you can redistribute it and/or modify
007    it under the terms of the GNU General Public License as published by
008    the Free Software Foundation; either version 2, or (at your option)
009    any later version.
010    
011    GNU Classpath is distributed in the hope that it will be useful, but
012    WITHOUT ANY WARRANTY; without even the implied warranty of
013    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
014    General Public License for more details.
015    
016    You should have received a copy of the GNU General Public License
017    along with GNU Classpath; see the file COPYING.  If not, write to the
018    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
019    02110-1301 USA.
020    
021    Linking this library statically or dynamically with other modules is
022    making a combined work based on this library.  Thus, the terms and
023    conditions of the GNU General Public License cover the whole
024    combination.
025    
026    As a special exception, the copyright holders of this library give you
027    permission to link this library with independent modules to produce an
028    executable, regardless of the license terms of these independent
029    modules, and to copy and distribute the resulting executable under
030    terms of your choice, provided that you also meet, for each linked
031    independent module, the terms and conditions of the license of that
032    module.  An independent module is a module which is not derived from
033    or based on this library.  If you modify this library, you may extend
034    this exception to your version of the library, but you are not
035    obligated to do so.  If you do not wish to do so, delete this
036    exception statement from your version. */
037    
038    
039    package javax.swing;
040    
041    import java.awt.Dimension;
042    import java.awt.FontMetrics;
043    import java.awt.Rectangle;
044    
045    import javax.accessibility.AccessibleContext;
046    import javax.accessibility.AccessibleStateSet;
047    import javax.swing.text.BadLocationException;
048    import javax.swing.text.Document;
049    import javax.swing.text.Element;
050    import javax.swing.text.JTextComponent;
051    import javax.swing.text.PlainDocument;
052    import 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    
094    public 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    }