1 /* 2 * Copyright (c) 1997, 2013, Oracle and/or its affiliates. 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. Oracle designates this 8 * particular file as subject to the "Classpath" exception as provided 9 * by Oracle 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 Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 22 * or visit www.oracle.com if you need additional information or have any 23 * 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 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 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 }