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 < 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 }