001 /* PlainView.java --
002 Copyright (C) 2004, 2005, 2006 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.text;
040
041 import java.awt.Color;
042 import java.awt.Component;
043 import java.awt.Font;
044 import java.awt.FontMetrics;
045 import java.awt.Graphics;
046 import java.awt.Rectangle;
047 import java.awt.Shape;
048
049 import javax.swing.SwingUtilities;
050 import javax.swing.event.DocumentEvent;
051 import javax.swing.event.DocumentEvent.ElementChange;
052
053 public class PlainView extends View implements TabExpander
054 {
055 Color selectedColor;
056 Color unselectedColor;
057
058 /**
059 * The color that is used to draw disabled text fields.
060 */
061 Color disabledColor;
062
063 /**
064 * While painting this is the textcomponent's current start index
065 * of the selection.
066 */
067 int selectionStart;
068
069 /**
070 * While painting this is the textcomponent's current end index
071 * of the selection.
072 */
073 int selectionEnd;
074
075 Font font;
076
077 /** The length of the longest line in the Document **/
078 float maxLineLength = -1;
079
080 /** The longest line in the Document **/
081 Element longestLine = null;
082
083 protected FontMetrics metrics;
084
085 /**
086 * The instance returned by {@link #getLineBuffer()}.
087 */
088 private transient Segment lineBuffer;
089
090 /**
091 * The base offset for tab calculations.
092 */
093 private int tabBase;
094
095 /**
096 * The tab size.
097 */
098 private int tabSize;
099
100 public PlainView(Element elem)
101 {
102 super(elem);
103 }
104
105 /**
106 * @since 1.4
107 */
108 protected void updateMetrics()
109 {
110 Component component = getContainer();
111 Font font = component.getFont();
112
113 if (this.font != font)
114 {
115 this.font = font;
116 metrics = component.getFontMetrics(font);
117 tabSize = getTabSize() * metrics.charWidth('m');
118 }
119 }
120
121 /**
122 * @since 1.4
123 */
124 protected Rectangle lineToRect(Shape a, int line)
125 {
126 // Ensure metrics are up-to-date.
127 updateMetrics();
128
129 Rectangle rect = a instanceof Rectangle ? (Rectangle) a : a.getBounds();
130 int fontHeight = metrics.getHeight();
131 return new Rectangle(rect.x, rect.y + (line * fontHeight),
132 rect.width, fontHeight);
133 }
134
135 public Shape modelToView(int position, Shape a, Position.Bias b)
136 throws BadLocationException
137 {
138 // Ensure metrics are up-to-date.
139 updateMetrics();
140
141 Document document = getDocument();
142
143 // Get rectangle of the line containing position.
144 int lineIndex = getElement().getElementIndex(position);
145 Rectangle rect = lineToRect(a, lineIndex);
146 tabBase = rect.x;
147
148 // Get the rectangle for position.
149 Element line = getElement().getElement(lineIndex);
150 int lineStart = line.getStartOffset();
151 Segment segment = getLineBuffer();
152 document.getText(lineStart, position - lineStart, segment);
153 int xoffset = Utilities.getTabbedTextWidth(segment, metrics, tabBase,
154 this, lineStart);
155
156 // Calc the real rectangle.
157 rect.x += xoffset;
158 rect.width = 1;
159 rect.height = metrics.getHeight();
160
161 return rect;
162 }
163
164 /**
165 * Draws a line of text. The X and Y coordinates specify the start of
166 * the <em>baseline</em> of the line.
167 *
168 * @param lineIndex the index of the line
169 * @param g the graphics to use for drawing the text
170 * @param x the X coordinate of the baseline
171 * @param y the Y coordinate of the baseline
172 */
173 protected void drawLine(int lineIndex, Graphics g, int x, int y)
174 {
175 try
176 {
177 Element line = getElement().getElement(lineIndex);
178 int startOffset = line.getStartOffset();
179 int endOffset = line.getEndOffset() - 1;
180
181 if (selectionStart <= startOffset)
182 // Selection starts before the line ...
183 if (selectionEnd <= startOffset)
184 {
185 // end ends before the line: Draw completely unselected text.
186 drawUnselectedText(g, x, y, startOffset, endOffset);
187 }
188 else if (selectionEnd <= endOffset)
189 {
190 // and ends within the line: First part is selected,
191 // second is not.
192 x = drawSelectedText(g, x, y, startOffset, selectionEnd);
193 drawUnselectedText(g, x, y, selectionEnd, endOffset);
194 }
195 else
196 // and ends behind the line: Draw completely selected text.
197 drawSelectedText(g, x, y, startOffset, endOffset);
198 else if (selectionStart < endOffset)
199 // Selection starts within the line ..
200 if (selectionEnd < endOffset)
201 {
202 // and ends within it: First part unselected, second part
203 // selected, third part unselected.
204 x = drawUnselectedText(g, x, y, startOffset, selectionStart);
205 x = drawSelectedText(g, x, y, selectionStart, selectionEnd);
206 drawUnselectedText(g, x, y, selectionEnd, endOffset);
207 }
208 else
209 {
210 // and ends behind the line: First part unselected, second
211 // part selected.
212 x = drawUnselectedText(g, x, y, startOffset, selectionStart);
213 drawSelectedText(g, x, y, selectionStart, endOffset);
214 }
215 else
216 // Selection is behind this line: Draw completely unselected text.
217 drawUnselectedText(g, x, y, startOffset, endOffset);
218 }
219 catch (BadLocationException e)
220 {
221 AssertionError ae = new AssertionError("Unexpected bad location");
222 ae.initCause(e);
223 throw ae;
224 }
225 }
226
227 protected int drawSelectedText(Graphics g, int x, int y, int p0, int p1)
228 throws BadLocationException
229 {
230 g.setColor(selectedColor);
231 Segment segment = getLineBuffer();
232 getDocument().getText(p0, p1 - p0, segment);
233 return Utilities.drawTabbedText(segment, x, y, g, this, segment.offset);
234 }
235
236 /**
237 * Draws a chunk of unselected text.
238 *
239 * @param g the graphics to use for drawing the text
240 * @param x the X coordinate of the baseline
241 * @param y the Y coordinate of the baseline
242 * @param p0 the start position in the text model
243 * @param p1 the end position in the text model
244 *
245 * @return the X location of the end of the range
246 *
247 * @throws BadLocationException if <code>p0</code> or <code>p1</code> are
248 * invalid
249 */
250 protected int drawUnselectedText(Graphics g, int x, int y, int p0, int p1)
251 throws BadLocationException
252 {
253 JTextComponent textComponent = (JTextComponent) getContainer();
254 if (textComponent.isEnabled())
255 g.setColor(unselectedColor);
256 else
257 g.setColor(disabledColor);
258
259 Segment segment = getLineBuffer();
260 getDocument().getText(p0, p1 - p0, segment);
261 return Utilities.drawTabbedText(segment, x, y, g, this, segment.offset);
262 }
263
264 public void paint(Graphics g, Shape s)
265 {
266 // Ensure metrics are up-to-date.
267 updateMetrics();
268
269 JTextComponent textComponent = (JTextComponent) getContainer();
270
271 selectedColor = textComponent.getSelectedTextColor();
272 unselectedColor = textComponent.getForeground();
273 disabledColor = textComponent.getDisabledTextColor();
274 selectionStart = textComponent.getSelectionStart();
275 selectionEnd = textComponent.getSelectionEnd();
276
277 Rectangle rect = s instanceof Rectangle ? (Rectangle) s : s.getBounds();
278 tabBase = rect.x;
279
280 // FIXME: Text may be scrolled.
281 Document document = textComponent.getDocument();
282 Element root = getElement();
283 int height = metrics.getHeight();
284
285 // For layered highlighters we need to paint the layered highlights
286 // before painting any text.
287 LayeredHighlighter hl = null;
288 Highlighter h = textComponent.getHighlighter();
289 if (h instanceof LayeredHighlighter)
290 hl = (LayeredHighlighter) h;
291
292 int count = root.getElementCount();
293
294 // Determine first and last line inside the clip.
295 Rectangle clip = g.getClipBounds();
296 SwingUtilities.computeIntersection(rect.x, rect.y, rect.width, rect.height,
297 clip);
298 int line0 = (clip.y - rect.y) / height;
299 line0 = Math.max(0, Math.min(line0, count - 1));
300 int line1 = (clip.y + clip.height - rect.y) / height;
301 line1 = Math.max(0, Math.min(line1, count - 1));
302 int y = rect.y + metrics.getAscent() + height * line0;
303 for (int i = line0; i <= line1; i++)
304 {
305 if (hl != null)
306 {
307 Element lineEl = root.getElement(i);
308 // Exclude the trailing newline from beeing highlighted.
309 if (i == count)
310 hl.paintLayeredHighlights(g, lineEl.getStartOffset(),
311 lineEl.getEndOffset(), s, textComponent,
312 this);
313 else
314 hl.paintLayeredHighlights(g, lineEl.getStartOffset(),
315 lineEl.getEndOffset() - 1, s,
316 textComponent, this);
317 }
318 drawLine(i, g, rect.x, y);
319 y += height;
320 }
321 }
322
323 /**
324 * Returns the tab size of a tab. Checks the Document's
325 * properties for PlainDocument.tabSizeAttribute and returns it if it is
326 * defined, otherwise returns 8.
327 *
328 * @return the tab size.
329 */
330 protected int getTabSize()
331 {
332 Object tabSize = getDocument().getProperty(PlainDocument.tabSizeAttribute);
333 if (tabSize == null)
334 return 8;
335 return ((Integer)tabSize).intValue();
336 }
337
338 /**
339 * Returns the next tab stop position after a given reference position.
340 *
341 * This implementation ignores the <code>tabStop</code> argument.
342 *
343 * @param x the current x position in pixels
344 * @param tabStop the position within the text stream that the tab occured at
345 */
346 public float nextTabStop(float x, int tabStop)
347 {
348 float next = x;
349 if (tabSize != 0)
350 {
351 int numTabs = (((int) x) - tabBase) / tabSize;
352 next = tabBase + (numTabs + 1) * tabSize;
353 }
354 return next;
355 }
356
357 /**
358 * Returns the length of the longest line, used for getting the span
359 * @return the length of the longest line
360 */
361 float determineMaxLineLength()
362 {
363 // if the longest line is cached, return the cached value
364 if (maxLineLength != -1)
365 return maxLineLength;
366
367 // otherwise we have to go through all the lines and find it
368 Element el = getElement();
369 Segment seg = getLineBuffer();
370 float span = 0;
371 for (int i = 0; i < el.getElementCount(); i++)
372 {
373 Element child = el.getElement(i);
374 int start = child.getStartOffset();
375 int end = child.getEndOffset() - 1;
376 try
377 {
378 el.getDocument().getText(start, end - start, seg);
379 }
380 catch (BadLocationException ex)
381 {
382 AssertionError ae = new AssertionError("Unexpected bad location");
383 ae.initCause(ex);
384 throw ae;
385 }
386
387 if (seg == null || seg.array == null || seg.count == 0)
388 continue;
389
390 int width = metrics.charsWidth(seg.array, seg.offset, seg.count);
391 if (width > span)
392 {
393 longestLine = child;
394 span = width;
395 }
396 }
397 maxLineLength = span;
398 return maxLineLength;
399 }
400
401 public float getPreferredSpan(int axis)
402 {
403 if (axis != X_AXIS && axis != Y_AXIS)
404 throw new IllegalArgumentException();
405
406 // make sure we have the metrics
407 updateMetrics();
408
409 Element el = getElement();
410 float span;
411
412 switch (axis)
413 {
414 case X_AXIS:
415 span = determineMaxLineLength();
416 break;
417 case Y_AXIS:
418 default:
419 span = metrics.getHeight() * el.getElementCount();
420 break;
421 }
422
423 return span;
424 }
425
426 /**
427 * Maps coordinates from the <code>View</code>'s space into a position
428 * in the document model.
429 *
430 * @param x the x coordinate in the view space
431 * @param y the y coordinate in the view space
432 * @param a the allocation of this <code>View</code>
433 * @param b the bias to use
434 *
435 * @return the position in the document that corresponds to the screen
436 * coordinates <code>x, y</code>
437 */
438 public int viewToModel(float x, float y, Shape a, Position.Bias[] b)
439 {
440 Rectangle rec = a instanceof Rectangle ? (Rectangle) a : a.getBounds();
441 tabBase = rec.x;
442
443 int pos;
444 if ((int) y < rec.y)
445 // Above our area vertically. Return start offset.
446 pos = getStartOffset();
447 else if ((int) y > rec.y + rec.height)
448 // Below our area vertically. Return end offset.
449 pos = getEndOffset() - 1;
450 else
451 {
452 // Inside the allocation vertically. Determine line and X offset.
453 Document doc = getDocument();
454 Element root = doc.getDefaultRootElement();
455 int line = Math.abs(((int) y - rec.y) / metrics.getHeight());
456 if (line >= root.getElementCount())
457 pos = getEndOffset() - 1;
458 else
459 {
460 Element lineEl = root.getElement(line);
461 if (x < rec.x)
462 // To the left of the allocation area.
463 pos = lineEl.getStartOffset();
464 else if (x > rec.x + rec.width)
465 // To the right of the allocation area.
466 pos = lineEl.getEndOffset() - 1;
467 else
468 {
469 try
470 {
471 int p0 = lineEl.getStartOffset();
472 int p1 = lineEl.getEndOffset();
473 Segment s = new Segment();
474 doc.getText(p0, p1 - p0, s);
475 tabBase = rec.x;
476 pos = p0 + Utilities.getTabbedTextOffset(s, metrics,
477 tabBase, (int) x,
478 this, p0);
479 }
480 catch (BadLocationException ex)
481 {
482 // Should not happen.
483 pos = -1;
484 }
485 }
486
487 }
488 }
489 // Bias is always forward.
490 b[0] = Position.Bias.Forward;
491 return pos;
492 }
493
494 /**
495 * Since insertUpdate and removeUpdate each deal with children
496 * Elements being both added and removed, they both have to perform
497 * the same checks. So they both simply call this method.
498 * @param changes the DocumentEvent for the changes to the Document.
499 * @param a the allocation of the View.
500 * @param f the ViewFactory to use for rebuilding.
501 */
502 protected void updateDamage(DocumentEvent changes, Shape a, ViewFactory f)
503 {
504 // This happens during initialization.
505 if (metrics == null)
506 {
507 updateMetrics();
508 preferenceChanged(null, true, true);
509 return;
510 }
511
512 Element element = getElement();
513
514 // Find longest line if it hasn't been initialized yet.
515 if (longestLine == null)
516 findLongestLine(0, element.getElementCount() - 1);
517
518 ElementChange change = changes.getChange(element);
519 if (changes.getType() == DocumentEvent.EventType.INSERT)
520 {
521 // Handles character/line insertion.
522
523 // Determine if lines have been added. In this case we repaint
524 // differently.
525 boolean linesAdded = true;
526 if (change == null)
527 linesAdded = false;
528
529 // Determine the start line.
530 int start;
531 if (linesAdded)
532 start = change.getIndex();
533 else
534 start = element.getElementIndex(changes.getOffset());
535
536 // Determine the length of the updated region.
537 int length = 0;
538 if (linesAdded)
539 length = change.getChildrenAdded().length - 1;
540
541 // Update the longest line and length.
542 int oldMaxLength = (int) maxLineLength;
543 if (longestLine.getEndOffset() < changes.getOffset()
544 || longestLine.getStartOffset() > changes.getOffset()
545 + changes.getLength())
546 {
547 findLongestLine(start, start + length);
548 }
549 else
550 {
551 findLongestLine(0, element.getElementCount() - 1);
552 }
553
554 // Trigger a preference change so that the layout gets updated
555 // correctly.
556 preferenceChanged(null, maxLineLength != oldMaxLength, linesAdded);
557
558 // Damage the updated line range.
559 int endLine = start;
560 if (linesAdded)
561 endLine = element.getElementCount() - 1;
562 damageLineRange(start, endLine, a, getContainer());
563
564 }
565 else
566 {
567 // Handles character/lines removals.
568
569 // Update the longest line and length and trigger preference changed.
570 int oldMaxLength = (int) maxLineLength;
571 if (change != null)
572 {
573 // Line(s) have been removed.
574 findLongestLine(0, element.getElementCount() - 1);
575 preferenceChanged(null, maxLineLength != oldMaxLength, true);
576 }
577 else
578 {
579 // No line has been removed.
580 int lineNo = getElement().getElementIndex(changes.getOffset());
581 Element line = getElement().getElement(lineNo);
582 if (longestLine == line)
583 {
584 findLongestLine(0, element.getElementCount() - 1);
585 preferenceChanged(null, maxLineLength != oldMaxLength, false);
586 }
587 damageLineRange(lineNo, lineNo, a, getContainer());
588 }
589 }
590 }
591
592 /**
593 * This method is called when something is inserted into the Document
594 * that this View is displaying.
595 *
596 * @param changes the DocumentEvent for the changes.
597 * @param a the allocation of the View
598 * @param f the ViewFactory used to rebuild
599 */
600 public void insertUpdate(DocumentEvent changes, Shape a, ViewFactory f)
601 {
602 updateDamage(changes, a, f);
603 }
604
605 /**
606 * This method is called when something is removed from the Document
607 * that this View is displaying.
608 *
609 * @param changes the DocumentEvent for the changes.
610 * @param a the allocation of the View
611 * @param f the ViewFactory used to rebuild
612 */
613 public void removeUpdate(DocumentEvent changes, Shape a, ViewFactory f)
614 {
615 updateDamage(changes, a, f);
616 }
617
618 /**
619 * This method is called when attributes were changed in the
620 * Document in a location that this view is responsible for.
621 */
622 public void changedUpdate (DocumentEvent changes, Shape a, ViewFactory f)
623 {
624 updateDamage(changes, a, f);
625 }
626
627 /**
628 * Repaint the given line range. This is called from insertUpdate,
629 * changedUpdate, and removeUpdate when no new lines were added
630 * and no lines were removed, to repaint the line that was
631 * modified.
632 *
633 * @param line0 the start of the range
634 * @param line1 the end of the range
635 * @param a the rendering region of the host
636 * @param host the Component that uses this View (used to call repaint
637 * on that Component)
638 *
639 * @since 1.4
640 */
641 protected void damageLineRange (int line0, int line1, Shape a, Component host)
642 {
643 if (a == null)
644 return;
645
646 Rectangle rec0 = lineToRect(a, line0);
647 Rectangle rec1 = lineToRect(a, line1);
648
649 if (rec0 == null || rec1 == null)
650 // something went wrong, repaint the entire host to be safe
651 host.repaint();
652 else
653 {
654 Rectangle repaintRec = SwingUtilities.computeUnion(rec0.x, rec0.y,
655 rec0.width,
656 rec0.height, rec1);
657 host.repaint(repaintRec.x, repaintRec.y, repaintRec.width,
658 repaintRec.height);
659 }
660 }
661
662 /**
663 * Provides a {@link Segment} object, that can be used to fetch text from
664 * the document.
665 *
666 * @returna {@link Segment} object, that can be used to fetch text from
667 * the document
668 */
669 protected final Segment getLineBuffer()
670 {
671 if (lineBuffer == null)
672 lineBuffer = new Segment();
673 return lineBuffer;
674 }
675
676 /**
677 * Finds and updates the longest line in the view inside an interval of
678 * lines.
679 *
680 * @param start the start of the search interval
681 * @param end the end of the search interval
682 */
683 private void findLongestLine(int start, int end)
684 {
685 for (int i = start; i <= end; i++)
686 {
687 int w = getLineLength(i);
688 if (w > maxLineLength)
689 {
690 maxLineLength = w;
691 longestLine = getElement().getElement(i);
692 }
693 }
694 }
695
696 /**
697 * Determines the length of the specified line.
698 *
699 * @param line the number of the line
700 *
701 * @return the length of the line in pixels
702 */
703 private int getLineLength(int line)
704 {
705 Element lineEl = getElement().getElement(line);
706 Segment buffer = getLineBuffer();
707 try
708 {
709 Document doc = getDocument();
710 doc.getText(lineEl.getStartOffset(),
711 lineEl.getEndOffset() - lineEl.getStartOffset() - 1,
712 buffer);
713 }
714 catch (BadLocationException ex)
715 {
716 AssertionError err = new AssertionError("Unexpected bad location");
717 err.initCause(ex);
718 throw err;
719 }
720
721 return Utilities.getTabbedTextWidth(buffer, metrics, tabBase, this,
722 lineEl.getStartOffset());
723 }
724 }