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