1 /* 2 * Copyright (c) 1998, 2015, 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.awt.*; 28 import java.lang.ref.SoftReference; 29 import javax.swing.event.*; 30 31 /** 32 * View of plain text (text with only one font and color) 33 * that does line-wrapping. This view expects that its 34 * associated element has child elements that represent 35 * the lines it should be wrapping. It is implemented 36 * as a vertical box that contains logical line views. 37 * The logical line views are nested classes that render 38 * the logical line as multiple physical line if the logical 39 * line is too wide to fit within the allocation. The 40 * line views draw upon the outer class for its state 41 * to reduce their memory requirements. 42 * <p> 43 * The line views do all of their rendering through the 44 * <code>drawLine</code> method which in turn does all of 45 * its rendering through the <code>drawSelectedText</code> 46 * and <code>drawUnselectedText</code> methods. This 47 * enables subclasses to easily specialize the rendering 48 * without concern for the layout aspects. 49 * 50 * @author Timothy Prinzing 51 * @see View 52 */ 53 public class WrappedPlainView extends BoxView implements TabExpander { 54 55 /** 56 * Creates a new WrappedPlainView. Lines will be wrapped 57 * on character boundaries. 58 * 59 * @param elem the element underlying the view 60 */ 61 public WrappedPlainView(Element elem) { 62 this(elem, false); 63 } 64 65 /** 66 * Creates a new WrappedPlainView. Lines can be wrapped on 67 * either character or word boundaries depending upon the 68 * setting of the wordWrap parameter. 69 * 70 * @param elem the element underlying the view 71 * @param wordWrap should lines be wrapped on word boundaries? 72 */ 73 public WrappedPlainView(Element elem, boolean wordWrap) { 74 super(elem, Y_AXIS); 75 this.wordWrap = wordWrap; 76 } 77 78 /** 79 * Returns the tab size set for the document, defaulting to 8. 80 * 81 * @return the tab size 82 */ 83 protected int getTabSize() { 84 Integer i = (Integer) getDocument().getProperty(PlainDocument.tabSizeAttribute); 85 int size = (i != null) ? i.intValue() : 8; 86 return size; 87 } 88 89 /** 90 * Renders a line of text, suppressing whitespace at the end 91 * and expanding any tabs. This is implemented to make calls 92 * to the methods <code>drawUnselectedText</code> and 93 * <code>drawSelectedText</code> so that the way selected and 94 * unselected text are rendered can be customized. 95 * 96 * @param p0 the starting document location to use >= 0 97 * @param p1 the ending document location to use >= p1 98 * @param g the graphics context 99 * @param x the starting X position >= 0 100 * @param y the starting Y position >= 0 101 * @see #drawUnselectedText 102 * @see #drawSelectedText 103 */ 104 protected void drawLine(int p0, int p1, Graphics g, int x, int y) { 105 Element lineMap = getElement(); 106 Element line = lineMap.getElement(lineMap.getElementIndex(p0)); 107 Element elem; 108 109 try { 110 if (line.isLeaf()) { 111 drawText(line, p0, p1, g, x, y); 112 } else { 113 // this line contains the composed text. 114 int idx = line.getElementIndex(p0); 115 int lastIdx = line.getElementIndex(p1); 116 for(; idx <= lastIdx; idx++) { 117 elem = line.getElement(idx); 118 int start = Math.max(elem.getStartOffset(), p0); 119 int end = Math.min(elem.getEndOffset(), p1); 120 x = drawText(elem, start, end, g, x, y); 121 } 122 } 123 } catch (BadLocationException e) { 124 throw new StateInvariantError("Can't render: " + p0 + "," + p1); 125 } 126 } 127 128 private int drawText(Element elem, int p0, int p1, Graphics g, int x, int y) throws BadLocationException { 129 p1 = Math.min(getDocument().getLength(), p1); 130 AttributeSet attr = elem.getAttributes(); 131 132 if (Utilities.isComposedTextAttributeDefined(attr)) { 133 g.setColor(unselected); 134 x = Utilities.drawComposedText(this, attr, g, x, y, 135 p0-elem.getStartOffset(), 136 p1-elem.getStartOffset()); 137 } else { 138 if (sel0 == sel1 || selected == unselected) { 139 // no selection, or it is invisible 140 x = drawUnselectedText(g, x, y, p0, p1); 141 } else if ((p0 >= sel0 && p0 <= sel1) && (p1 >= sel0 && p1 <= sel1)) { 142 x = drawSelectedText(g, x, y, p0, p1); 143 } else if (sel0 >= p0 && sel0 <= p1) { 144 if (sel1 >= p0 && sel1 <= p1) { 145 x = drawUnselectedText(g, x, y, p0, sel0); 146 x = drawSelectedText(g, x, y, sel0, sel1); 147 x = drawUnselectedText(g, x, y, sel1, p1); 148 } else { 149 x = drawUnselectedText(g, x, y, p0, sel0); 150 x = drawSelectedText(g, x, y, sel0, p1); 151 } 152 } else if (sel1 >= p0 && sel1 <= p1) { 153 x = drawSelectedText(g, x, y, p0, sel1); 154 x = drawUnselectedText(g, x, y, sel1, p1); 155 } else { 156 x = drawUnselectedText(g, x, y, p0, p1); 157 } 158 } 159 160 return x; 161 } 162 163 /** 164 * Renders the given range in the model as normal unselected 165 * text. 166 * 167 * @param g the graphics context 168 * @param x the starting X coordinate >= 0 169 * @param y the starting Y coordinate >= 0 170 * @param p0 the beginning position in the model >= 0 171 * @param p1 the ending position in the model >= p0 172 * @return the X location of the end of the range >= 0 173 * @exception BadLocationException if the range is invalid 174 */ 175 protected int drawUnselectedText(Graphics g, int x, int y, 176 int p0, int p1) throws BadLocationException { 177 g.setColor(unselected); 178 Document doc = getDocument(); 179 Segment segment = SegmentCache.getSharedSegment(); 180 doc.getText(p0, p1 - p0, segment); 181 int ret = Utilities.drawTabbedText(this, segment, x, y, g, this, p0); 182 SegmentCache.releaseSharedSegment(segment); 183 return ret; 184 } 185 186 /** 187 * Renders the given range in the model as selected text. This 188 * is implemented to render the text in the color specified in 189 * the hosting component. It assumes the highlighter will render 190 * the selected background. 191 * 192 * @param g the graphics context 193 * @param x the starting X coordinate >= 0 194 * @param y the starting Y coordinate >= 0 195 * @param p0 the beginning position in the model >= 0 196 * @param p1 the ending position in the model >= p0 197 * @return the location of the end of the range. 198 * @exception BadLocationException if the range is invalid 199 */ 200 protected int drawSelectedText(Graphics g, int x, 201 int y, int p0, int p1) throws BadLocationException { 202 g.setColor(selected); 203 Document doc = getDocument(); 204 Segment segment = SegmentCache.getSharedSegment(); 205 doc.getText(p0, p1 - p0, segment); 206 int ret = Utilities.drawTabbedText(this, segment, x, y, g, this, p0); 207 SegmentCache.releaseSharedSegment(segment); 208 return ret; 209 } 210 211 /** 212 * Gives access to a buffer that can be used to fetch 213 * text from the associated document. 214 * 215 * @return the buffer 216 */ 217 protected final Segment getLineBuffer() { 218 if (lineBuffer == null) { 219 lineBuffer = new Segment(); 220 } 221 return lineBuffer; 222 } 223 224 /** 225 * This is called by the nested wrapped line 226 * views to determine the break location. This can 227 * be reimplemented to alter the breaking behavior. 228 * It will either break at word or character boundaries 229 * depending upon the break argument given at 230 * construction. 231 * @param p0 the starting document location 232 * @param p1 the ending document location to use 233 * @return the break position 234 */ 235 protected int calculateBreakPosition(int p0, int p1) { 236 int p; 237 Segment segment = SegmentCache.getSharedSegment(); 238 loadText(segment, p0, p1); 239 int currentWidth = getWidth(); 240 if (wordWrap) { 241 p = p0 + Utilities.getBreakLocation(segment, metrics, 242 tabBase, tabBase + currentWidth, 243 this, p0); 244 } else { 245 p = p0 + Utilities.getTabbedTextOffset(segment, metrics, 246 tabBase, tabBase + currentWidth, 247 this, p0, false); 248 } 249 SegmentCache.releaseSharedSegment(segment); 250 return p; 251 } 252 253 /** 254 * Loads all of the children to initialize the view. 255 * This is called by the <code>setParent</code> method. 256 * Subclasses can reimplement this to initialize their 257 * child views in a different manner. The default 258 * implementation creates a child view for each 259 * child element. 260 * 261 * @param f the view factory 262 */ 263 protected void loadChildren(ViewFactory f) { 264 Element e = getElement(); 265 int n = e.getElementCount(); 266 if (n > 0) { 267 View[] added = new View[n]; 268 for (int i = 0; i < n; i++) { 269 added[i] = new WrappedLine(e.getElement(i)); 270 } 271 replace(0, 0, added); 272 } 273 } 274 275 /** 276 * Update the child views in response to a 277 * document event. 278 */ 279 void updateChildren(DocumentEvent e, Shape a) { 280 Element elem = getElement(); 281 DocumentEvent.ElementChange ec = e.getChange(elem); 282 if (ec != null) { 283 // the structure of this element changed. 284 Element[] removedElems = ec.getChildrenRemoved(); 285 Element[] addedElems = ec.getChildrenAdded(); 286 View[] added = new View[addedElems.length]; 287 for (int i = 0; i < addedElems.length; i++) { 288 added[i] = new WrappedLine(addedElems[i]); 289 } 290 replace(ec.getIndex(), removedElems.length, added); 291 292 // should damge a little more intelligently. 293 if (a != null) { 294 preferenceChanged(null, true, true); 295 getContainer().repaint(); 296 } 297 } 298 299 // update font metrics which may be used by the child views 300 updateMetrics(); 301 } 302 303 /** 304 * Load the text buffer with the given range 305 * of text. This is used by the fragments 306 * broken off of this view as well as this 307 * view itself. 308 */ 309 final void loadText(Segment segment, int p0, int p1) { 310 try { 311 Document doc = getDocument(); 312 doc.getText(p0, p1 - p0, segment); 313 } catch (BadLocationException bl) { 314 throw new StateInvariantError("Can't get line text"); 315 } 316 } 317 318 final void updateMetrics() { 319 Component host = getContainer(); 320 Font f = host.getFont(); 321 metrics = host.getFontMetrics(f); 322 tabSize = getTabSize() * metrics.charWidth('m'); 323 } 324 325 // --- TabExpander methods ------------------------------------------ 326 327 /** 328 * Returns the next tab stop position after a given reference position. 329 * This implementation does not support things like centering so it 330 * ignores the tabOffset argument. 331 * 332 * @param x the current position >= 0 333 * @param tabOffset the position within the text stream 334 * that the tab occurred at >= 0. 335 * @return the tab stop, measured in points >= 0 336 */ 337 public float nextTabStop(float x, int tabOffset) { 338 if (tabSize == 0) 339 return x; 340 int ntabs = ((int) x - tabBase) / tabSize; 341 return tabBase + ((ntabs + 1) * tabSize); 342 } 343 344 345 // --- View methods ------------------------------------- 346 347 /** 348 * Renders using the given rendering surface and area 349 * on that surface. This is implemented to stash the 350 * selection positions, selection colors, and font 351 * metrics for the nested lines to use. 352 * 353 * @param g the rendering surface to use 354 * @param a the allocated region to render into 355 * 356 * @see View#paint 357 */ 358 public void paint(Graphics g, Shape a) { 359 Rectangle alloc = (Rectangle) a; 360 tabBase = alloc.x; 361 JTextComponent host = (JTextComponent) getContainer(); 362 sel0 = host.getSelectionStart(); 363 sel1 = host.getSelectionEnd(); 364 unselected = (host.isEnabled()) ? 365 host.getForeground() : host.getDisabledTextColor(); 366 Caret c = host.getCaret(); 367 selected = c.isSelectionVisible() && host.getHighlighter() != null ? 368 host.getSelectedTextColor() : unselected; 369 g.setFont(host.getFont()); 370 371 // superclass paints the children 372 super.paint(g, a); 373 } 374 375 /** 376 * Sets the size of the view. This should cause 377 * layout of the view along the given axis, if it 378 * has any layout duties. 379 * 380 * @param width the width >= 0 381 * @param height the height >= 0 382 */ 383 public void setSize(float width, float height) { 384 updateMetrics(); 385 if ((int) width != getWidth()) { 386 // invalidate the view itself since the desired widths 387 // of the children will be based upon this views width. 388 preferenceChanged(null, true, true); 389 widthChanging = true; 390 } 391 super.setSize(width, height); 392 widthChanging = false; 393 } 394 395 /** 396 * Determines the preferred span for this view along an 397 * axis. This is implemented to provide the superclass 398 * behavior after first making sure that the current font 399 * metrics are cached (for the nested lines which use 400 * the metrics to determine the height of the potentially 401 * wrapped lines). 402 * 403 * @param axis may be either View.X_AXIS or View.Y_AXIS 404 * @return the span the view would like to be rendered into. 405 * Typically the view is told to render into the span 406 * that is returned, although there is no guarantee. 407 * The parent may choose to resize or break the view. 408 * @see View#getPreferredSpan 409 */ 410 public float getPreferredSpan(int axis) { 411 updateMetrics(); 412 return super.getPreferredSpan(axis); 413 } 414 415 /** 416 * Determines the minimum span for this view along an 417 * axis. This is implemented to provide the superclass 418 * behavior after first making sure that the current font 419 * metrics are cached (for the nested lines which use 420 * the metrics to determine the height of the potentially 421 * wrapped lines). 422 * 423 * @param axis may be either View.X_AXIS or View.Y_AXIS 424 * @return the span the view would like to be rendered into. 425 * Typically the view is told to render into the span 426 * that is returned, although there is no guarantee. 427 * The parent may choose to resize or break the view. 428 * @see View#getMinimumSpan 429 */ 430 public float getMinimumSpan(int axis) { 431 updateMetrics(); 432 return super.getMinimumSpan(axis); 433 } 434 435 /** 436 * Determines the maximum span for this view along an 437 * axis. This is implemented to provide the superclass 438 * behavior after first making sure that the current font 439 * metrics are cached (for the nested lines which use 440 * the metrics to determine the height of the potentially 441 * wrapped lines). 442 * 443 * @param axis may be either View.X_AXIS or View.Y_AXIS 444 * @return the span the view would like to be rendered into. 445 * Typically the view is told to render into the span 446 * that is returned, although there is no guarantee. 447 * The parent may choose to resize or break the view. 448 * @see View#getMaximumSpan 449 */ 450 public float getMaximumSpan(int axis) { 451 updateMetrics(); 452 return super.getMaximumSpan(axis); 453 } 454 455 /** 456 * Gives notification that something was inserted into the 457 * document in a location that this view is responsible for. 458 * This is implemented to simply update the children. 459 * 460 * @param e the change information from the associated document 461 * @param a the current allocation of the view 462 * @param f the factory to use to rebuild if the view has children 463 * @see View#insertUpdate 464 */ 465 public void insertUpdate(DocumentEvent e, Shape a, ViewFactory f) { 466 updateChildren(e, a); 467 468 Rectangle alloc = ((a != null) && isAllocationValid()) ? 469 getInsideAllocation(a) : null; 470 int pos = e.getOffset(); 471 View v = getViewAtPosition(pos, alloc); 472 if (v != null) { 473 v.insertUpdate(e, alloc, f); 474 } 475 } 476 477 /** 478 * Gives notification that something was removed from the 479 * document in a location that this view is responsible for. 480 * This is implemented to simply update the children. 481 * 482 * @param e 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#removeUpdate 486 */ 487 public void removeUpdate(DocumentEvent e, Shape a, ViewFactory f) { 488 updateChildren(e, a); 489 490 Rectangle alloc = ((a != null) && isAllocationValid()) ? 491 getInsideAllocation(a) : null; 492 int pos = e.getOffset(); 493 View v = getViewAtPosition(pos, alloc); 494 if (v != null) { 495 v.removeUpdate(e, alloc, f); 496 } 497 } 498 499 /** 500 * Gives notification from the document that attributes were changed 501 * in a location that this view is responsible for. 502 * 503 * @param e the change information from the associated document 504 * @param a the current allocation of the view 505 * @param f the factory to use to rebuild if the view has children 506 * @see View#changedUpdate 507 */ 508 public void changedUpdate(DocumentEvent e, Shape a, ViewFactory f) { 509 updateChildren(e, a); 510 } 511 512 // --- variables ------------------------------------------- 513 514 FontMetrics metrics; 515 Segment lineBuffer; 516 boolean widthChanging; 517 int tabBase; 518 int tabSize; 519 boolean wordWrap; 520 521 int sel0; 522 int sel1; 523 Color unselected; 524 Color selected; 525 526 527 /** 528 * Simple view of a line that wraps if it doesn't 529 * fit withing the horizontal space allocated. 530 * This class tries to be lightweight by carrying little 531 * state of it's own and sharing the state of the outer class 532 * with it's sibblings. 533 */ 534 class WrappedLine extends View { 535 536 WrappedLine(Element elem) { 537 super(elem); 538 lineCount = -1; 539 } 540 541 /** 542 * Determines the preferred span for this view along an 543 * axis. 544 * 545 * @param axis may be either X_AXIS or Y_AXIS 546 * @return the span the view would like to be rendered into. 547 * Typically the view is told to render into the span 548 * that is returned, although there is no guarantee. 549 * The parent may choose to resize or break the view. 550 * @see View#getPreferredSpan 551 */ 552 public float getPreferredSpan(int axis) { 553 switch (axis) { 554 case View.X_AXIS: 555 float width = getWidth(); 556 if (width == Integer.MAX_VALUE) { 557 // We have been initially set to MAX_VALUE, but we don't 558 // want this as our preferred. 559 return 100f; 560 } 561 return width; 562 case View.Y_AXIS: 563 if (lineCount < 0 || widthChanging) { 564 breakLines(getStartOffset()); 565 } 566 return lineCount * metrics.getHeight(); 567 default: 568 throw new IllegalArgumentException("Invalid axis: " + axis); 569 } 570 } 571 572 /** 573 * Renders using the given rendering surface and area on that 574 * surface. The view may need to do layout and create child 575 * views to enable itself to render into the given allocation. 576 * 577 * @param g the rendering surface to use 578 * @param a the allocated region to render into 579 * @see View#paint 580 */ 581 public void paint(Graphics g, Shape a) { 582 Rectangle alloc = (Rectangle) a; 583 int y = alloc.y + metrics.getAscent(); 584 int x = alloc.x; 585 586 JTextComponent host = (JTextComponent)getContainer(); 587 Highlighter h = host.getHighlighter(); 588 LayeredHighlighter dh = (h instanceof LayeredHighlighter) ? 589 (LayeredHighlighter)h : null; 590 591 int start = getStartOffset(); 592 int end = getEndOffset(); 593 int p0 = start; 594 int[] lineEnds = getLineEnds(); 595 for (int i = 0; i < lineCount; i++) { 596 int p1 = (lineEnds == null) ? end : 597 start + lineEnds[i]; 598 if (dh != null) { 599 int hOffset = (p1 == end) 600 ? (p1 - 1) 601 : p1; 602 dh.paintLayeredHighlights(g, p0, hOffset, a, host, this); 603 } 604 drawLine(p0, p1, g, x, y); 605 606 p0 = p1; 607 y += metrics.getHeight(); 608 } 609 } 610 611 /** 612 * Provides a mapping from the document model coordinate space 613 * to the coordinate space of the view mapped to it. 614 * 615 * @param pos the position to convert 616 * @param a the allocated region to render into 617 * @return the bounding box of the given position is returned 618 * @exception BadLocationException if the given position does not represent a 619 * valid location in the associated document 620 * @see View#modelToView 621 */ 622 public Shape modelToView(int pos, Shape a, Position.Bias b) 623 throws BadLocationException { 624 Rectangle alloc = a.getBounds(); 625 alloc.height = metrics.getHeight(); 626 alloc.width = 1; 627 628 int p0 = getStartOffset(); 629 if (pos < p0 || pos > getEndOffset()) { 630 throw new BadLocationException("Position out of range", pos); 631 } 632 633 int testP = (b == Position.Bias.Forward) ? pos : 634 Math.max(p0, pos - 1); 635 int line = 0; 636 int[] lineEnds = getLineEnds(); 637 if (lineEnds != null) { 638 line = findLine(testP - p0); 639 if (line > 0) { 640 p0 += lineEnds[line - 1]; 641 } 642 alloc.y += alloc.height * line; 643 } 644 645 if (pos > p0) { 646 Segment segment = SegmentCache.getSharedSegment(); 647 loadText(segment, p0, pos); 648 alloc.x += Utilities.getTabbedTextWidth(segment, metrics, 649 alloc.x, WrappedPlainView.this, p0); 650 SegmentCache.releaseSharedSegment(segment); 651 } 652 return alloc; 653 } 654 655 /** 656 * Provides a mapping from the view coordinate space to the logical 657 * coordinate space of the model. 658 * 659 * @param fx the X coordinate 660 * @param fy the Y coordinate 661 * @param a the allocated region to render into 662 * @return the location within the model that best represents the 663 * given point in the view 664 * @see View#viewToModel 665 */ 666 public int viewToModel(float fx, float fy, Shape a, Position.Bias[] bias) { 667 // PENDING(prinz) implement bias properly 668 bias[0] = Position.Bias.Forward; 669 670 Rectangle alloc = (Rectangle) a; 671 int x = (int) fx; 672 int y = (int) fy; 673 if (y < alloc.y) { 674 // above the area covered by this icon, so the position 675 // is assumed to be the start of the coverage for this view. 676 return getStartOffset(); 677 } else if (y > alloc.y + alloc.height) { 678 // below the area covered by this icon, so the position 679 // is assumed to be the end of the coverage for this view. 680 return getEndOffset() - 1; 681 } else { 682 // positioned within the coverage of this view vertically, 683 // so we figure out which line the point corresponds to. 684 // if the line is greater than the number of lines contained, then 685 // simply use the last line as it represents the last possible place 686 // we can position to. 687 alloc.height = metrics.getHeight(); 688 int line = (alloc.height > 0 ? 689 (y - alloc.y) / alloc.height : lineCount - 1); 690 if (line >= lineCount) { 691 return getEndOffset() - 1; 692 } else { 693 int p0 = getStartOffset(); 694 int p1; 695 if (lineCount == 1) { 696 p1 = getEndOffset(); 697 } else { 698 int[] lineEnds = getLineEnds(); 699 p1 = p0 + lineEnds[line]; 700 if (line > 0) { 701 p0 += lineEnds[line - 1]; 702 } 703 } 704 705 if (x < alloc.x) { 706 // point is to the left of the line 707 return p0; 708 } else if (x > alloc.x + alloc.width) { 709 // point is to the right of the line 710 return p1 - 1; 711 } else { 712 // Determine the offset into the text 713 Segment segment = SegmentCache.getSharedSegment(); 714 loadText(segment, p0, p1); 715 int n = Utilities.getTabbedTextOffset(segment, metrics, 716 alloc.x, x, 717 WrappedPlainView.this, p0); 718 SegmentCache.releaseSharedSegment(segment); 719 return Math.min(p0 + n, p1 - 1); 720 } 721 } 722 } 723 } 724 725 public void insertUpdate(DocumentEvent e, Shape a, ViewFactory f) { 726 update(e, a); 727 } 728 729 public void removeUpdate(DocumentEvent e, Shape a, ViewFactory f) { 730 update(e, a); 731 } 732 733 private void update(DocumentEvent ev, Shape a) { 734 int oldCount = lineCount; 735 breakLines(ev.getOffset()); 736 if (oldCount != lineCount) { 737 WrappedPlainView.this.preferenceChanged(this, false, true); 738 // have to repaint any views after the receiver. 739 getContainer().repaint(); 740 } else if (a != null) { 741 Component c = getContainer(); 742 Rectangle alloc = (Rectangle) a; 743 c.repaint(alloc.x, alloc.y, alloc.width, alloc.height); 744 } 745 } 746 747 /** 748 * Returns line cache. If the cache was GC'ed, recreates it. 749 * If there's no cache, returns null 750 */ 751 final int[] getLineEnds() { 752 if (lineCache == null) { 753 return null; 754 } else { 755 int[] lineEnds = lineCache.get(); 756 if (lineEnds == null) { 757 // Cache was GC'ed, so rebuild it 758 return breakLines(getStartOffset()); 759 } else { 760 return lineEnds; 761 } 762 } 763 } 764 765 /** 766 * Creates line cache if text breaks into more than one physical line. 767 * @param startPos position to start breaking from 768 * @return the cache created, ot null if text breaks into one line 769 */ 770 final int[] breakLines(int startPos) { 771 int[] lineEnds = (lineCache == null) ? null : lineCache.get(); 772 int[] oldLineEnds = lineEnds; 773 int start = getStartOffset(); 774 int lineIndex = 0; 775 if (lineEnds != null) { 776 lineIndex = findLine(startPos - start); 777 if (lineIndex > 0) { 778 lineIndex--; 779 } 780 } 781 782 int p0 = (lineIndex == 0) ? start : start + lineEnds[lineIndex - 1]; 783 int p1 = getEndOffset(); 784 while (p0 < p1) { 785 int p = calculateBreakPosition(p0, p1); 786 p0 = (p == p0) ? ++p : p; // 4410243 787 788 if (lineIndex == 0 && p0 >= p1) { 789 // do not use cache if there's only one line 790 lineCache = null; 791 lineEnds = null; 792 lineIndex = 1; 793 break; 794 } else if (lineEnds == null || lineIndex >= lineEnds.length) { 795 // we have 2+ lines, and the cache is not big enough 796 // we try to estimate total number of lines 797 double growFactor = ((double)(p1 - start) / (p0 - start)); 798 int newSize = (int)Math.ceil((lineIndex + 1) * growFactor); 799 newSize = Math.max(newSize, lineIndex + 2); 800 int[] tmp = new int[newSize]; 801 if (lineEnds != null) { 802 System.arraycopy(lineEnds, 0, tmp, 0, lineIndex); 803 } 804 lineEnds = tmp; 805 } 806 lineEnds[lineIndex++] = p0 - start; 807 } 808 809 lineCount = lineIndex; 810 if (lineCount > 1) { 811 // check if the cache is too big 812 int maxCapacity = lineCount + lineCount / 3; 813 if (lineEnds.length > maxCapacity) { 814 int[] tmp = new int[maxCapacity]; 815 System.arraycopy(lineEnds, 0, tmp, 0, lineCount); 816 lineEnds = tmp; 817 } 818 } 819 820 if (lineEnds != null && lineEnds != oldLineEnds) { 821 lineCache = new SoftReference<int[]>(lineEnds); 822 } 823 return lineEnds; 824 } 825 826 /** 827 * Binary search in the cache for line containing specified offset 828 * (which is relative to the beginning of the view). This method 829 * assumes that cache exists. 830 */ 831 private int findLine(int offset) { 832 int[] lineEnds = lineCache.get(); 833 if (offset < lineEnds[0]) { 834 return 0; 835 } else if (offset > lineEnds[lineCount - 1]) { 836 return lineCount; 837 } else { 838 return findLine(lineEnds, offset, 0, lineCount - 1); 839 } 840 } 841 842 private int findLine(int[] array, int offset, int min, int max) { 843 if (max - min <= 1) { 844 return max; 845 } else { 846 int mid = (max + min) / 2; 847 return (offset < array[mid]) ? 848 findLine(array, offset, min, mid) : 849 findLine(array, offset, mid, max); 850 } 851 } 852 853 int lineCount; 854 SoftReference<int[]> lineCache = null; 855 } 856 }