1 /*
2 * Copyright 1997-2006 Sun Microsystems, Inc. All Rights Reserved.
3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4 *
5 * This code is free software; you can redistribute it and/or modify it
6 * under the terms of the GNU General Public License version 2 only, as
7 * published by the Free Software Foundation. Sun designates this
8 * particular file as subject to the "Classpath" exception as provided
9 * by Sun in the LICENSE file that accompanied this code.
10 *
11 * This code is distributed in the hope that it will be useful, but WITHOUT
12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
14 * version 2 for more details (a copy is included in the LICENSE file that
15 * accompanied this code).
16 *
17 * You should have received a copy of the GNU General Public License version
18 * 2 along with this work; if not, write to the Free Software Foundation,
19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20 *
21 * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa Clara,
22 * CA 95054 USA or visit www.sun.com if you need additional information or
23 * have any questions.
24 */
25 package javax.swing.text;
26
27 import java.util.Vector;
28 import java.util.Properties;
29 import java.awt;
30 import javax.swing.event;
31
32 /**
33 * Implements View interface for a simple multi-line text view
34 * that has text in one font and color. The view represents each
35 * child element as a line of text.
36 *
37 * @author Timothy Prinzing
38 * @see View
39 */
40 public class PlainView extends View implements TabExpander {
41
42 /**
43 * Constructs a new PlainView wrapped on an element.
44 *
45 * @param elem the element
46 */
47 public PlainView(Element elem) {
48 super(elem);
49 }
50
51 /**
52 * Returns the tab size set for the document, defaulting to 8.
53 *
54 * @return the tab size
55 */
56 protected int getTabSize() {
57 Integer i = (Integer) getDocument().getProperty(PlainDocument.tabSizeAttribute);
58 int size = (i != null) ? i.intValue() : 8;
59 return size;
60 }
61
62 /**
63 * Renders a line of text, suppressing whitespace at the end
64 * and expanding any tabs. This is implemented to make calls
65 * to the methods <code>drawUnselectedText</code> and
66 * <code>drawSelectedText</code> so that the way selected and
67 * unselected text are rendered can be customized.
68 *
69 * @param lineIndex the line to draw >= 0
70 * @param g the <code>Graphics</code> context
71 * @param x the starting X position >= 0
72 * @param y the starting Y position >= 0
73 * @see #drawUnselectedText
74 * @see #drawSelectedText
75 */
76 protected void drawLine(int lineIndex, Graphics g, int x, int y) {
77 Element line = getElement().getElement(lineIndex);
78 Element elem;
79
80 try {
81 if (line.isLeaf()) {
82 drawElement(lineIndex, line, g, x, y);
83 } else {
84 // this line contains the composed text.
85 int count = line.getElementCount();
86 for(int i = 0; i < count; i++) {
87 elem = line.getElement(i);
88 x = drawElement(lineIndex, elem, g, x, y);
89 }
90 }
91 } catch (BadLocationException e) {
92 throw new StateInvariantError("Can't render line: " + lineIndex);
93 }
94 }
95
96 private int drawElement(int lineIndex, Element elem, Graphics g, int x, int y) throws BadLocationException {
97 int p0 = elem.getStartOffset();
98 int p1 = elem.getEndOffset();
99 p1 = Math.min(getDocument().getLength(), p1);
100
101 if (lineIndex == 0) {
102 x += firstLineOffset;
103 }
104 AttributeSet attr = elem.getAttributes();
105 if (Utilities.isComposedTextAttributeDefined(attr)) {
106 g.setColor(unselected);
107 x = Utilities.drawComposedText(this, attr, g, x, y,
108 p0-elem.getStartOffset(),
109 p1-elem.getStartOffset());
110 } else {
111 if (sel0 == sel1 || selected == unselected) {
112 // no selection, or it is invisible
113 x = drawUnselectedText(g, x, y, p0, p1);
114 } else if ((p0 >= sel0 && p0 <= sel1) && (p1 >= sel0 && p1 <= sel1)) {
115 x = drawSelectedText(g, x, y, p0, p1);
116 } else if (sel0 >= p0 && sel0 <= p1) {
117 if (sel1 >= p0 && sel1 <= p1) {
118 x = drawUnselectedText(g, x, y, p0, sel0);
119 x = drawSelectedText(g, x, y, sel0, sel1);
120 x = drawUnselectedText(g, x, y, sel1, p1);
121 } else {
122 x = drawUnselectedText(g, x, y, p0, sel0);
123 x = drawSelectedText(g, x, y, sel0, p1);
124 }
125 } else if (sel1 >= p0 && sel1 <= p1) {
126 x = drawSelectedText(g, x, y, p0, sel1);
127 x = drawUnselectedText(g, x, y, sel1, p1);
128 } else {
129 x = drawUnselectedText(g, x, y, p0, p1);
130 }
131 }
132
133 return x;
134 }
135
136 /**
137 * Renders the given range in the model as normal unselected
138 * text. Uses the foreground or disabled color to render the text.
139 *
140 * @param g the graphics context
141 * @param x the starting X coordinate >= 0
142 * @param y the starting Y coordinate >= 0
143 * @param p0 the beginning position in the model >= 0
144 * @param p1 the ending position in the model >= 0
145 * @return the X location of the end of the range >= 0
146 * @exception BadLocationException if the range is invalid
147 */
148 protected int drawUnselectedText(Graphics g, int x, int y,
149 int p0, int p1) throws BadLocationException {
150 g.setColor(unselected);
151 Document doc = getDocument();
152 Segment s = SegmentCache.getSharedSegment();
153 doc.getText(p0, p1 - p0, s);
154 int ret = Utilities.drawTabbedText(this, s, x, y, g, this, p0);
155 SegmentCache.releaseSharedSegment(s);
156 return ret;
157 }
158
159 /**
160 * Renders the given range in the model as selected text. This
161 * is implemented to render the text in the color specified in
162 * the hosting component. It assumes the highlighter will render
163 * the selected background.
164 *
165 * @param g the graphics context
166 * @param x the starting X coordinate >= 0
167 * @param y the starting Y coordinate >= 0
168 * @param p0 the beginning position in the model >= 0
169 * @param p1 the ending position in the model >= 0
170 * @return the location of the end of the range
171 * @exception BadLocationException if the range is invalid
172 */
173 protected int drawSelectedText(Graphics g, int x,
174 int y, int p0, int p1) throws BadLocationException {
175 g.setColor(selected);
176 Document doc = getDocument();
177 Segment s = SegmentCache.getSharedSegment();
178 doc.getText(p0, p1 - p0, s);
179 int ret = Utilities.drawTabbedText(this, s, x, y, g, this, p0);
180 SegmentCache.releaseSharedSegment(s);
181 return ret;
182 }
183
184 /**
185 * Gives access to a buffer that can be used to fetch
186 * text from the associated document.
187 *
188 * @return the buffer
189 */
190 protected final Segment getLineBuffer() {
191 if (lineBuffer == null) {
192 lineBuffer = new Segment();
193 }
194 return lineBuffer;
195 }
196
197 /**
198 * Checks to see if the font metrics and longest line
199 * are up-to-date.
200 *
201 * @since 1.4
202 */
203 protected void updateMetrics() {
204 Component host = getContainer();
205 Font f = host.getFont();
206 if (font != f) {
207 // The font changed, we need to recalculate the
208 // longest line.
209 calculateLongestLine();
210 tabSize = getTabSize() * metrics.charWidth('m');
211 }
212 }
213
214 // ---- View methods ----------------------------------------------------
215
216 /**
217 * Determines the preferred span for this view along an
218 * axis.
219 *
220 * @param axis may be either View.X_AXIS or View.Y_AXIS
221 * @return the span the view would like to be rendered into >= 0.
222 * Typically the view is told to render into the span
223 * that is returned, although there is no guarantee.
224 * The parent may choose to resize or break the view.
225 * @exception IllegalArgumentException for an invalid axis
226 */
227 public float getPreferredSpan(int axis) {
228 updateMetrics();
229 switch (axis) {
230 case View.X_AXIS:
231 return getLineWidth(longLine);
232 case View.Y_AXIS:
233 return getElement().getElementCount() * metrics.getHeight();
234 default:
235 throw new IllegalArgumentException("Invalid axis: " + axis);
236 }
237 }
238
239 /**
240 * Renders using the given rendering surface and area on that surface.
241 * The view may need to do layout and create child views to enable
242 * itself to render into the given allocation.
243 *
244 * @param g the rendering surface to use
245 * @param a the allocated region to render into
246 *
247 * @see View#paint
248 */
249 public void paint(Graphics g, Shape a) {
250 Shape originalA = a;
251 a = adjustPaintRegion(a);
252 Rectangle alloc = (Rectangle) a;
253 tabBase = alloc.x;
254 JTextComponent host = (JTextComponent) getContainer();
255 Highlighter h = host.getHighlighter();
256 g.setFont(host.getFont());
257 sel0 = host.getSelectionStart();
258 sel1 = host.getSelectionEnd();
259 unselected = (host.isEnabled()) ?
260 host.getForeground() : host.getDisabledTextColor();
261 Caret c = host.getCaret();
262 selected = c.isSelectionVisible() && h != null ?
263 host.getSelectedTextColor() : unselected;
264 updateMetrics();
265
266 // If the lines are clipped then we don't expend the effort to
267 // try and paint them. Since all of the lines are the same height
268 // with this object, determination of what lines need to be repainted
269 // is quick.
270 Rectangle clip = g.getClipBounds();
271 int fontHeight = metrics.getHeight();
272 int heightBelow = (alloc.y + alloc.height) - (clip.y + clip.height);
273 int heightAbove = clip.y - alloc.y;
274 int linesBelow, linesAbove, linesTotal;
275
276 if (fontHeight > 0) {
277 linesBelow = Math.max(0, heightBelow / fontHeight);
278 linesAbove = Math.max(0, heightAbove / fontHeight);
279 linesTotal = alloc.height / fontHeight;
280 if (alloc.height % fontHeight != 0) {
281 linesTotal++;
282 }
283 } else {
284 linesBelow = linesAbove = linesTotal = 0;
285 }
286
287 // update the visible lines
288 Rectangle lineArea = lineToRect(a, linesAbove);
289 int y = lineArea.y + metrics.getAscent();
290 int x = lineArea.x;
291 Element map = getElement();
292 int lineCount = map.getElementCount();
293 int endLine = Math.min(lineCount, linesTotal - linesBelow);
294 lineCount--;
295 LayeredHighlighter dh = (h instanceof LayeredHighlighter) ?
296 (LayeredHighlighter)h : null;
297 for (int line = linesAbove; line < endLine; line++) {
298 if (dh != null) {
299 Element lineElement = map.getElement(line);
300 if (line == lineCount) {
301 dh.paintLayeredHighlights(g, lineElement.getStartOffset(),
302 lineElement.getEndOffset(),
303 originalA, host, this);
304 }
305 else {
306 dh.paintLayeredHighlights(g, lineElement.getStartOffset(),
307 lineElement.getEndOffset() - 1,
308 originalA, host, this);
309 }
310 }
311 drawLine(line, g, x, y);
312 y += fontHeight;
313 if (line == 0) {
314 // This should never really happen, in so far as if
315 // firstLineOffset is non 0, there should only be one
316 // line of text.
317 x -= firstLineOffset;
318 }
319 }
320 }
321
322 /**
323 * Should return a shape ideal for painting based on the passed in
324 * Shape <code>a</code>. This is useful if painting in a different
325 * region. The default implementation returns <code>a</code>.
326 */
327 Shape adjustPaintRegion(Shape a) {
328 return a;
329 }
330
331 /**
332 * Provides a mapping from the document model coordinate space
333 * to the coordinate space of the view mapped to it.
334 *
335 * @param pos the position to convert >= 0
336 * @param a the allocated region to render into
337 * @return the bounding box of the given position
338 * @exception BadLocationException if the given position does not
339 * represent a valid location in the associated document
340 * @see View#modelToView
341 */
342 public Shape modelToView(int pos, Shape a, Position.Bias b) throws BadLocationException {
343 // line coordinates
344 Document doc = getDocument();
345 Element map = getElement();
346 int lineIndex = map.getElementIndex(pos);
347 if (lineIndex < 0) {
348 return lineToRect(a, 0);
349 }
350 Rectangle lineArea = lineToRect(a, lineIndex);
351
352 // determine span from the start of the line
353 tabBase = lineArea.x;
354 Element line = map.getElement(lineIndex);
355 int p0 = line.getStartOffset();
356 Segment s = SegmentCache.getSharedSegment();
357 doc.getText(p0, pos - p0, s);
358 int xOffs = Utilities.getTabbedTextWidth(s, metrics, tabBase, this,p0);
359 SegmentCache.releaseSharedSegment(s);
360
361 // fill in the results and return
362 lineArea.x += xOffs;
363 lineArea.width = 1;
364 lineArea.height = metrics.getHeight();
365 return lineArea;
366 }
367
368 /**
369 * Provides a mapping from the view coordinate space to the logical
370 * coordinate space of the model.
371 *
372 * @param fx the X coordinate >= 0
373 * @param fy the Y coordinate >= 0
374 * @param a the allocated region to render into
375 * @return the location within the model that best represents the
376 * given point in the view >= 0
377 * @see View#viewToModel
378 */
379 public int viewToModel(float fx, float fy, Shape a, Position.Bias[] bias) {
380 // PENDING(prinz) properly calculate bias
381 bias[0] = Position.Bias.Forward;
382
383 Rectangle alloc = a.getBounds();
384 Document doc = getDocument();
385 int x = (int) fx;
386 int y = (int) fy;
387 if (y < alloc.y) {
388 // above the area covered by this icon, so the the position
389 // is assumed to be the start of the coverage for this view.
390 return getStartOffset();
391 } else if (y > alloc.y + alloc.height) {
392 // below the area covered by this icon, so the the position
393 // is assumed to be the end of the coverage for this view.
394 return getEndOffset() - 1;
395 } else {
396 // positioned within the coverage of this view vertically,
397 // so we figure out which line the point corresponds to.
398 // if the line is greater than the number of lines contained, then
399 // simply use the last line as it represents the last possible place
400 // we can position to.
401 Element map = doc.getDefaultRootElement();
402 int fontHeight = metrics.getHeight();
403 int lineIndex = (fontHeight > 0 ?
404 Math.abs((y - alloc.y) / fontHeight) :
405 map.getElementCount() - 1);
406 if (lineIndex >= map.getElementCount()) {
407 return getEndOffset() - 1;
408 }
409 Element line = map.getElement(lineIndex);
410 int dx = 0;
411 if (lineIndex == 0) {
412 alloc.x += firstLineOffset;
413 alloc.width -= firstLineOffset;
414 }
415 if (x < alloc.x) {
416 // point is to the left of the line
417 return line.getStartOffset();
418 } else if (x > alloc.x + alloc.width) {
419 // point is to the right of the line
420 return line.getEndOffset() - 1;
421 } else {
422 // Determine the offset into the text
423 try {
424 int p0 = line.getStartOffset();
425 int p1 = line.getEndOffset() - 1;
426 Segment s = SegmentCache.getSharedSegment();
427 doc.getText(p0, p1 - p0, s);
428 tabBase = alloc.x;
429 int offs = p0 + Utilities.getTabbedTextOffset(s, metrics,
430 tabBase, x, this, p0);
431 SegmentCache.releaseSharedSegment(s);
432 return offs;
433 } catch (BadLocationException e) {
434 // should not happen
435 return -1;
436 }
437 }
438 }
439 }
440
441 /**
442 * Gives notification that something was inserted into the document
443 * in a location that this view is responsible for.
444 *
445 * @param changes the change information from the associated document
446 * @param a the current allocation of the view
447 * @param f the factory to use to rebuild if the view has children
448 * @see View#insertUpdate
449 */
450 public void insertUpdate(DocumentEvent changes, Shape a, ViewFactory f) {
451 updateDamage(changes, a, f);
452 }
453
454 /**
455 * Gives notification that something was removed from the document
456 * in a location that this view is responsible for.
457 *
458 * @param changes the change information from the associated document
459 * @param a the current allocation of the view
460 * @param f the factory to use to rebuild if the view has children
461 * @see View#removeUpdate
462 */
463 public void removeUpdate(DocumentEvent changes, Shape a, ViewFactory f) {
464 updateDamage(changes, a, f);
465 }
466
467 /**
468 * Gives notification from the document that attributes were changed
469 * in a location that this view is responsible for.
470 *
471 * @param changes the change information from the associated document
472 * @param a the current allocation of the view
473 * @param f the factory to use to rebuild if the view has children
474 * @see View#changedUpdate
475 */
476 public void changedUpdate(DocumentEvent changes, Shape a, ViewFactory f) {
477 updateDamage(changes, a, f);
478 }
479
480 /**
481 * Sets the size of the view. This should cause
482 * layout of the view along the given axis, if it
483 * has any layout duties.
484 *
485 * @param width the width >= 0
486 * @param height the height >= 0
487 */
488 public void setSize(float width, float height) {
489 super.setSize(width, height);
490 updateMetrics();
491 }
492
493 // --- TabExpander methods ------------------------------------------
494
495 /**
496 * Returns the next tab stop position after a given reference position.
497 * This implementation does not support things like centering so it
498 * ignores the tabOffset argument.
499 *
500 * @param x the current position >= 0
501 * @param tabOffset the position within the text stream
502 * that the tab occurred at >= 0.
503 * @return the tab stop, measured in points >= 0
504 */
505 public float nextTabStop(float x, int tabOffset) {
506 if (tabSize == 0) {
507 return x;
508 }
509 int ntabs = (((int) x) - tabBase) / tabSize;
510 return tabBase + ((ntabs + 1) * tabSize);
511 }
512
513 // --- local methods ------------------------------------------------
514
515 /**
516 * Repaint the region of change covered by the given document
517 * event. Damages the line that begins the range to cover
518 * the case when the insert/remove is only on one line.
519 * If lines are added or removed, damages the whole
520 * view. The longest line is checked to see if it has
521 * changed.
522 *
523 * @since 1.4
524 */
525 protected void updateDamage(DocumentEvent changes, Shape a, ViewFactory f) {
526 Component host = getContainer();
527 updateMetrics();
528 Element elem = getElement();
529 DocumentEvent.ElementChange ec = changes.getChange(elem);
530
531 Element[] added = (ec != null) ? ec.getChildrenAdded() : null;
532 Element[] removed = (ec != null) ? ec.getChildrenRemoved() : null;
533 if (((added != null) && (added.length > 0)) ||
534 ((removed != null) && (removed.length > 0))) {
535 // lines were added or removed...
536 if (added != null) {
537 int currWide = getLineWidth(longLine);
538 for (int i = 0; i < added.length; i++) {
539 int w = getLineWidth(added[i]);
540 if (w > currWide) {
541 currWide = w;
542 longLine = added[i];
543 }
544 }
545 }
546 if (removed != null) {
547 for (int i = 0; i < removed.length; i++) {
548 if (removed[i] == longLine) {
549 calculateLongestLine();
550 break;
551 }
552 }
553 }
554 preferenceChanged(null, true, true);
555 host.repaint();
556 } else {
557 Element map = getElement();
558 int line = map.getElementIndex(changes.getOffset());
559 damageLineRange(line, line, a, host);
560 if (changes.getType() == DocumentEvent.EventType.INSERT) {
561 // check to see if the line is longer than current
562 // longest line.
563 int w = getLineWidth(longLine);
564 Element e = map.getElement(line);
565 if (e == longLine) {
566 preferenceChanged(null, true, false);
567 } else if (getLineWidth(e) > w) {
568 longLine = e;
569 preferenceChanged(null, true, false);
570 }
571 } else if (changes.getType() == DocumentEvent.EventType.REMOVE) {
572 if (map.getElement(line) == longLine) {
573 // removed from longest line... recalc
574 calculateLongestLine();
575 preferenceChanged(null, true, false);
576 }
577 }
578 }
579 }
580
581 /**
582 * Repaint the given line range.
583 *
584 * @param host the component hosting the view (used to call repaint)
585 * @param a the region allocated for the view to render into
586 * @param line0 the starting line number to repaint. This must
587 * be a valid line number in the model.
588 * @param line1 the ending line number to repaint. This must
589 * be a valid line number in the model.
590 * @since 1.4
591 */
592 protected void damageLineRange(int line0, int line1, Shape a, Component host) {
593 if (a != null) {
594 Rectangle area0 = lineToRect(a, line0);
595 Rectangle area1 = lineToRect(a, line1);
596 if ((area0 != null) && (area1 != null)) {
597 Rectangle damage = area0.union(area1);
598 host.repaint(damage.x, damage.y, damage.width, damage.height);
599 } else {
600 host.repaint();
601 }
602 }
603 }
604
605 /**
606 * Determine the rectangle that represents the given line.
607 *
608 * @param a the region allocated for the view to render into
609 * @param line the line number to find the region of. This must
610 * be a valid line number in the model.
611 * @since 1.4
612 */
613 protected Rectangle lineToRect(Shape a, int line) {
614 Rectangle r = null;
615 updateMetrics();
616 if (metrics != null) {
617 Rectangle alloc = a.getBounds();
618 if (line == 0) {
619 alloc.x += firstLineOffset;
620 alloc.width -= firstLineOffset;
621 }
622 r = new Rectangle(alloc.x, alloc.y + (line * metrics.getHeight()),
623 alloc.width, metrics.getHeight());
624 }
625 return r;
626 }
627
628 /**
629 * Iterate over the lines represented by the child elements
630 * of the element this view represents, looking for the line
631 * that is the longest. The <em>longLine</em> variable is updated to
632 * represent the longest line contained. The <em>font</em> variable
633 * is updated to indicate the font used to calculate the
634 * longest line.
635 */
636 private void calculateLongestLine() {
637 Component c = getContainer();
638 font = c.getFont();
639 metrics = c.getFontMetrics(font);
640 Document doc = getDocument();
641 Element lines = getElement();
642 int n = lines.getElementCount();
643 int maxWidth = -1;
644 for (int i = 0; i < n; i++) {
645 Element line = lines.getElement(i);
646 int w = getLineWidth(line);
647 if (w > maxWidth) {
648 maxWidth = w;
649 longLine = line;
650 }
651 }
652 }
653
654 /**
655 * Calculate the width of the line represented by
656 * the given element. It is assumed that the font
657 * and font metrics are up-to-date.
658 */
659 private int getLineWidth(Element line) {
660 if (line == null) {
661 return 0;
662 }
663 int p0 = line.getStartOffset();
664 int p1 = line.getEndOffset();
665 int w;
666 Segment s = SegmentCache.getSharedSegment();
667 try {
668 line.getDocument().getText(p0, p1 - p0, s);
669 w = Utilities.getTabbedTextWidth(s, metrics, tabBase, this, p0);
670 } catch (BadLocationException ble) {
671 w = 0;
672 }
673 SegmentCache.releaseSharedSegment(s);
674 return w;
675 }
676
677 // --- member variables -----------------------------------------------
678
679 /**
680 * Font metrics for the current font.
681 */
682 protected FontMetrics metrics;
683
684 /**
685 * The current longest line. This is used to calculate
686 * the preferred width of the view. Since the calculation
687 * is potentially expensive we try to avoid it by stashing
688 * which line is currently the longest.
689 */
690 Element longLine;
691
692 /**
693 * Font used to calculate the longest line... if this
694 * changes we need to recalculate the longest line
695 */
696 Font font;
697
698 Segment lineBuffer;
699 int tabSize;
700 int tabBase;
701
702 int sel0;
703 int sel1;
704 Color unselected;
705 Color selected;
706
707 /**
708 * Offset of where to draw the first character on the first line.
709 * This is a hack and temporary until we can better address the problem
710 * of text measuring. This field is actually never set directly in
711 * PlainView, but by FieldView.
712 */
713 int firstLineOffset;
714
715 }