1 /* 2 * Copyright (c) 1998, 2011, 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 */ 232 protected int calculateBreakPosition(int p0, int p1) { 233 int p; 234 Segment segment = SegmentCache.getSharedSegment(); 235 loadText(segment, p0, p1); 236 int currentWidth = getWidth(); 237 if (wordWrap) { 238 p = p0 + Utilities.getBreakLocation(segment, metrics, 239 tabBase, tabBase + currentWidth, 240 this, p0); 241 } else { 242 p = p0 + Utilities.getTabbedTextOffset(segment, metrics, 243 tabBase, tabBase + currentWidth, 244 this, p0, false); 245 } 246 SegmentCache.releaseSharedSegment(segment); 247 return p; 248 } 249 250 /** 251 * Loads all of the children to initialize the view. 252 * This is called by the <code>setParent</code> method. 253 * Subclasses can reimplement this to initialize their 254 * child views in a different manner. The default 255 * implementation creates a child view for each 256 * child element. 257 * 258 * @param f the view factory 259 */ 260 protected void loadChildren(ViewFactory f) { 261 Element e = getElement(); 262 int n = e.getElementCount(); 263 if (n > 0) { 264 View[] added = new View[n]; 265 for (int i = 0; i < n; i++) { 266 added[i] = new WrappedLine(e.getElement(i)); 267 } 268 replace(0, 0, added); 269 } 270 } 271 272 /** 273 * Update the child views in response to a 274 * document event. 275 */ 276 void updateChildren(DocumentEvent e, Shape a) { 277 Element elem = getElement(); 278 DocumentEvent.ElementChange ec = e.getChange(elem); 279 if (ec != null) { 280 // the structure of this element changed. 281 Element[] removedElems = ec.getChildrenRemoved(); 282 Element[] addedElems = ec.getChildrenAdded(); 283 View[] added = new View[addedElems.length]; 284 for (int i = 0; i < addedElems.length; i++) { 285 added[i] = new WrappedLine(addedElems[i]); 286 } 287 replace(ec.getIndex(), removedElems.length, added); 288 289 // should damge a little more intelligently. 290 if (a != null) { 291 preferenceChanged(null, true, true); 292 getContainer().repaint(); 293 } 294 } 295 296 // update font metrics which may be used by the child views 297 updateMetrics(); 298 } 299 300 /** 301 * Load the text buffer with the given range 302 * of text. This is used by the fragments 303 * broken off of this view as well as this 304 * view itself. 305 */ 306 final void loadText(Segment segment, int p0, int p1) { 307 try { 308 Document doc = getDocument(); 309 doc.getText(p0, p1 - p0, segment); 310 } catch (BadLocationException bl) { 311 throw new StateInvariantError("Can't get line text"); 312 } 313 } 314 315 final void updateMetrics() { 316 Component host = getContainer(); 317 Font f = host.getFont(); 318 metrics = host.getFontMetrics(f); 319 tabSize = getTabSize() * metrics.charWidth('m'); 320 } 321 322 // --- TabExpander methods ------------------------------------------ 323 324 /** 325 * Returns the next tab stop position after a given reference position. 326 * This implementation does not support things like centering so it 327 * ignores the tabOffset argument. 328 * 329 * @param x the current position >= 0 330 * @param tabOffset the position within the text stream 331 * that the tab occurred at >= 0. 332 * @return the tab stop, measured in points >= 0 333 */ 334 public float nextTabStop(float x, int tabOffset) { 335 if (tabSize == 0) 336 return x; 337 int ntabs = ((int) x - tabBase) / tabSize; 338 return tabBase + ((ntabs + 1) * tabSize); 339 } 340 341 342 // --- View methods ------------------------------------- 343 344 /** 345 * Renders using the given rendering surface and area 346 * on that surface. This is implemented to stash the 347 * selection positions, selection colors, and font 348 * metrics for the nested lines to use. 349 * 350 * @param g the rendering surface to use 351 * @param a the allocated region to render into 352 * 353 * @see View#paint 354 */ 355 public void paint(Graphics g, Shape a) { 356 Rectangle alloc = (Rectangle) a; 357 tabBase = alloc.x; 358 JTextComponent host = (JTextComponent) getContainer(); 359 sel0 = host.getSelectionStart(); 360 sel1 = host.getSelectionEnd(); 361 unselected = (host.isEnabled()) ? 362 host.getForeground() : host.getDisabledTextColor(); 363 Caret c = host.getCaret(); 364 selected = c.isSelectionVisible() && host.getHighlighter() != null ? 365 host.getSelectedTextColor() : unselected; 366 g.setFont(host.getFont()); 367 368 // superclass paints the children 369 super.paint(g, a); 370 } 371 372 /** 373 * Sets the size of the view. This should cause 374 * layout of the view along the given axis, if it 375 * has any layout duties. 376 * 377 * @param width the width >= 0 378 * @param height the height >= 0 379 */ 380 public void setSize(float width, float height) { 381 updateMetrics(); 382 if ((int) width != getWidth()) { 383 // invalidate the view itself since the childrens 384 // desired widths will be based upon this views width. 385 preferenceChanged(null, true, true); 386 widthChanging = true; 387 } 388 super.setSize(width, height); 389 widthChanging = false; 390 } 391 392 /** 393 * Determines the preferred span for this view along an 394 * axis. This is implemented to provide the superclass 395 * behavior after first making sure that the current font 396 * metrics are cached (for the nested lines which use 397 * the metrics to determine the height of the potentially 398 * wrapped lines). 399 * 400 * @param axis may be either View.X_AXIS or View.Y_AXIS 401 * @return the span the view would like to be rendered into. 402 * Typically the view is told to render into the span 403 * that is returned, although there is no guarantee. 404 * The parent may choose to resize or break the view. 405 * @see View#getPreferredSpan 406 */ 407 public float getPreferredSpan(int axis) { 408 updateMetrics(); 409 return super.getPreferredSpan(axis); 410 } 411 412 /** 413 * Determines the minimum span for this view along an 414 * axis. This is implemented to provide the superclass 415 * behavior after first making sure that the current font 416 * metrics are cached (for the nested lines which use 417 * the metrics to determine the height of the potentially 418 * wrapped lines). 419 * 420 * @param axis may be either View.X_AXIS or View.Y_AXIS 421 * @return the span the view would like to be rendered into. 422 * Typically the view is told to render into the span 423 * that is returned, although there is no guarantee. 424 * The parent may choose to resize or break the view. 425 * @see View#getMinimumSpan 426 */ 427 public float getMinimumSpan(int axis) { 428 updateMetrics(); 429 return super.getMinimumSpan(axis); 430 } 431 432 /** 433 * Determines the maximum span for this view along an 434 * axis. This is implemented to provide the superclass 435 * behavior after first making sure that the current font 436 * metrics are cached (for the nested lines which use 437 * the metrics to determine the height of the potentially 438 * wrapped lines). 439 * 440 * @param axis may be either View.X_AXIS or View.Y_AXIS 441 * @return the span the view would like to be rendered into. 442 * Typically the view is told to render into the span 443 * that is returned, although there is no guarantee. 444 * The parent may choose to resize or break the view. 445 * @see View#getMaximumSpan 446 */ 447 public float getMaximumSpan(int axis) { 448 updateMetrics(); 449 return super.getMaximumSpan(axis); 450 } 451 452 /** 453 * Gives notification that something was inserted into the 454 * document in a location that this view is responsible for. 455 * This is implemented to simply update the children. 456 * 457 * @param e the change information from the associated document 458 * @param a the current allocation of the view 459 * @param f the factory to use to rebuild if the view has children 460 * @see View#insertUpdate 461 */ 462 public void insertUpdate(DocumentEvent e, Shape a, ViewFactory f) { 463 updateChildren(e, a); 464 465 Rectangle alloc = ((a != null) && isAllocationValid()) ? 466 getInsideAllocation(a) : null; 467 int pos = e.getOffset(); 468 View v = getViewAtPosition(pos, alloc); 469 if (v != null) { 470 v.insertUpdate(e, alloc, f); 471 } 472 } 473 474 /** 475 * Gives notification that something was removed from the 476 * document in a location that this view is responsible for. 477 * This is implemented to simply update the children. 478 * 479 * @param e the change information from the associated document 480 * @param a the current allocation of the view 481 * @param f the factory to use to rebuild if the view has children 482 * @see View#removeUpdate 483 */ 484 public void removeUpdate(DocumentEvent e, Shape a, ViewFactory f) { 485 updateChildren(e, a); 486 487 Rectangle alloc = ((a != null) && isAllocationValid()) ? 488 getInsideAllocation(a) : null; 489 int pos = e.getOffset(); 490 View v = getViewAtPosition(pos, alloc); 491 if (v != null) { 492 v.removeUpdate(e, alloc, f); 493 } 494 } 495 496 /** 497 * Gives notification from the document that attributes were changed 498 * in a location that this view is responsible for. 499 * 500 * @param e the change information from the associated document 501 * @param a the current allocation of the view 502 * @param f the factory to use to rebuild if the view has children 503 * @see View#changedUpdate 504 */ 505 public void changedUpdate(DocumentEvent e, Shape a, ViewFactory f) { 506 updateChildren(e, a); 507 } 508 509 // --- variables ------------------------------------------- 510 511 FontMetrics metrics; 512 Segment lineBuffer; 513 boolean widthChanging; 514 int tabBase; 515 int tabSize; 516 boolean wordWrap; 517 518 int sel0; 519 int sel1; 520 Color unselected; 521 Color selected; 522 523 524 /** 525 * Simple view of a line that wraps if it doesn't 526 * fit withing the horizontal space allocated. 527 * This class tries to be lightweight by carrying little 528 * state of it's own and sharing the state of the outer class 529 * with it's sibblings. 530 */ 531 class WrappedLine extends View { 532 533 WrappedLine(Element elem) { 534 super(elem); 535 lineCount = -1; 536 } 537 538 /** 539 * Determines the preferred span for this view along an 540 * axis. 541 * 542 * @param axis may be either X_AXIS or Y_AXIS 543 * @return the span the view would like to be rendered into. 544 * Typically the view is told to render into the span 545 * that is returned, although there is no guarantee. 546 * The parent may choose to resize or break the view. 547 * @see View#getPreferredSpan 548 */ 549 public float getPreferredSpan(int axis) { 550 switch (axis) { 551 case View.X_AXIS: 552 float width = getWidth(); 553 if (width == Integer.MAX_VALUE) { 554 // We have been initially set to MAX_VALUE, but we don't 555 // want this as our preferred. 556 return 100f; 557 } 558 return width; 559 case View.Y_AXIS: 560 if (lineCount < 0 || widthChanging) { 561 breakLines(getStartOffset()); 562 } 563 return lineCount * metrics.getHeight(); 564 default: 565 throw new IllegalArgumentException("Invalid axis: " + axis); 566 } 567 } 568 569 /** 570 * Renders using the given rendering surface and area on that 571 * surface. The view may need to do layout and create child 572 * views to enable itself to render into the given allocation. 573 * 574 * @param g the rendering surface to use 575 * @param a the allocated region to render into 576 * @see View#paint 577 */ 578 public void paint(Graphics g, Shape a) { 579 Rectangle alloc = (Rectangle) a; 580 int y = alloc.y + metrics.getAscent(); 581 int x = alloc.x; 582 583 JTextComponent host = (JTextComponent)getContainer(); 584 Highlighter h = host.getHighlighter(); 585 LayeredHighlighter dh = (h instanceof LayeredHighlighter) ? 586 (LayeredHighlighter)h : null; 587 588 int start = getStartOffset(); 589 int end = getEndOffset(); 590 int p0 = start; 591 int[] lineEnds = getLineEnds(); 592 for (int i = 0; i < lineCount; i++) { 593 int p1 = (lineEnds == null) ? end : 594 start + lineEnds[i]; 595 if (dh != null) { 596 int hOffset = (p1 == end) 597 ? (p1 - 1) 598 : p1; 599 dh.paintLayeredHighlights(g, p0, hOffset, a, host, this); 600 } 601 drawLine(p0, p1, g, x, y); 602 603 p0 = p1; 604 y += metrics.getHeight(); 605 } 606 } 607 608 /** 609 * Provides a mapping from the document model coordinate space 610 * to the coordinate space of the view mapped to it. 611 * 612 * @param pos the position to convert 613 * @param a the allocated region to render into 614 * @return the bounding box of the given position is returned 615 * @exception BadLocationException if the given position does not represent a 616 * valid location in the associated document 617 * @see View#modelToView 618 */ 619 public Shape modelToView(int pos, Shape a, Position.Bias b) 620 throws BadLocationException { 621 Rectangle alloc = a.getBounds(); 622 alloc.height = metrics.getHeight(); 623 alloc.width = 1; 624 625 int p0 = getStartOffset(); 626 if (pos < p0 || pos > getEndOffset()) { 627 throw new BadLocationException("Position out of range", pos); 628 } 629 630 int testP = (b == Position.Bias.Forward) ? pos : 631 Math.max(p0, pos - 1); 632 int line = 0; 633 int[] lineEnds = getLineEnds(); 634 if (lineEnds != null) { 635 line = findLine(testP - p0); 636 if (line > 0) { 637 p0 += lineEnds[line - 1]; 638 } 639 alloc.y += alloc.height * line; 640 } 641 642 if (pos > p0) { 643 Segment segment = SegmentCache.getSharedSegment(); 644 loadText(segment, p0, pos); 645 alloc.x += Utilities.getTabbedTextWidth(segment, metrics, 646 alloc.x, WrappedPlainView.this, p0); 647 SegmentCache.releaseSharedSegment(segment); 648 } 649 return alloc; 650 } 651 652 /** 653 * Provides a mapping from the view coordinate space to the logical 654 * coordinate space of the model. 655 * 656 * @param fx the X coordinate 657 * @param fy the Y coordinate 658 * @param a the allocated region to render into 659 * @return the location within the model that best represents the 660 * given point in the view 661 * @see View#viewToModel 662 */ 663 public int viewToModel(float fx, float fy, Shape a, Position.Bias[] bias) { 664 // PENDING(prinz) implement bias properly 665 bias[0] = Position.Bias.Forward; 666 667 Rectangle alloc = (Rectangle) a; 668 int x = (int) fx; 669 int y = (int) fy; 670 if (y < alloc.y) { 671 // above the area covered by this icon, so the the position 672 // is assumed to be the start of the coverage for this view. 673 return getStartOffset(); 674 } else if (y > alloc.y + alloc.height) { 675 // below the area covered by this icon, so the the position 676 // is assumed to be the end of the coverage for this view. 677 return getEndOffset() - 1; 678 } else { 679 // positioned within the coverage of this view vertically, 680 // so we figure out which line the point corresponds to. 681 // if the line is greater than the number of lines contained, then 682 // simply use the last line as it represents the last possible place 683 // we can position to. 684 alloc.height = metrics.getHeight(); 685 int line = (alloc.height > 0 ? 686 (y - alloc.y) / alloc.height : lineCount - 1); 687 if (line >= lineCount) { 688 return getEndOffset() - 1; 689 } else { 690 int p0 = getStartOffset(); 691 int p1; 692 if (lineCount == 1) { 693 p1 = getEndOffset(); 694 } else { 695 int[] lineEnds = getLineEnds(); 696 p1 = p0 + lineEnds[line]; 697 if (line > 0) { 698 p0 += lineEnds[line - 1]; 699 } 700 } 701 702 if (x < alloc.x) { 703 // point is to the left of the line 704 return p0; 705 } else if (x > alloc.x + alloc.width) { 706 // point is to the right of the line 707 return p1 - 1; 708 } else { 709 // Determine the offset into the text 710 Segment segment = SegmentCache.getSharedSegment(); 711 loadText(segment, p0, p1); 712 int n = Utilities.getTabbedTextOffset(segment, metrics, 713 alloc.x, x, 714 WrappedPlainView.this, p0); 715 SegmentCache.releaseSharedSegment(segment); 716 return Math.min(p0 + n, p1 - 1); 717 } 718 } 719 } 720 } 721 722 public void insertUpdate(DocumentEvent e, Shape a, ViewFactory f) { 723 update(e, a); 724 } 725 726 public void removeUpdate(DocumentEvent e, Shape a, ViewFactory f) { 727 update(e, a); 728 } 729 730 private void update(DocumentEvent ev, Shape a) { 731 int oldCount = lineCount; 732 breakLines(ev.getOffset()); 733 if (oldCount != lineCount) { 734 WrappedPlainView.this.preferenceChanged(this, false, true); 735 // have to repaint any views after the receiver. 736 getContainer().repaint(); 737 } else if (a != null) { 738 Component c = getContainer(); 739 Rectangle alloc = (Rectangle) a; 740 c.repaint(alloc.x, alloc.y, alloc.width, alloc.height); 741 } 742 } 743 744 /** 745 * Returns line cache. If the cache was GC'ed, recreates it. 746 * If there's no cache, returns null 747 */ 748 final int[] getLineEnds() { 749 if (lineCache == null) { 750 return null; 751 } else { 752 int[] lineEnds = lineCache.get(); 753 if (lineEnds == null) { 754 // Cache was GC'ed, so rebuild it 755 return breakLines(getStartOffset()); 756 } else { 757 return lineEnds; 758 } 759 } 760 } 761 762 /** 763 * Creates line cache if text breaks into more than one physical line. 764 * @param startPos position to start breaking from 765 * @return the cache created, ot null if text breaks into one line 766 */ 767 final int[] breakLines(int startPos) { 768 int[] lineEnds = (lineCache == null) ? null : lineCache.get(); 769 int[] oldLineEnds = lineEnds; 770 int start = getStartOffset(); 771 int lineIndex = 0; 772 if (lineEnds != null) { 773 lineIndex = findLine(startPos - start); 774 if (lineIndex > 0) { 775 lineIndex--; 776 } 777 } 778 779 int p0 = (lineIndex == 0) ? start : start + lineEnds[lineIndex - 1]; 780 int p1 = getEndOffset(); 781 while (p0 < p1) { 782 int p = calculateBreakPosition(p0, p1); 783 p0 = (p == p0) ? ++p : p; // 4410243 784 785 if (lineIndex == 0 && p0 >= p1) { 786 // do not use cache if there's only one line 787 lineCache = null; 788 lineEnds = null; 789 lineIndex = 1; 790 break; 791 } else if (lineEnds == null || lineIndex >= lineEnds.length) { 792 // we have 2+ lines, and the cache is not big enough 793 // we try to estimate total number of lines 794 double growFactor = ((double)(p1 - start) / (p0 - start)); 795 int newSize = (int)Math.ceil((lineIndex + 1) * growFactor); 796 newSize = Math.max(newSize, lineIndex + 2); 797 int[] tmp = new int[newSize]; 798 if (lineEnds != null) { 799 System.arraycopy(lineEnds, 0, tmp, 0, lineIndex); 800 } 801 lineEnds = tmp; 802 } 803 lineEnds[lineIndex++] = p0 - start; 804 } 805 806 lineCount = lineIndex; 807 if (lineCount > 1) { 808 // check if the cache is too big 809 int maxCapacity = lineCount + lineCount / 3; 810 if (lineEnds.length > maxCapacity) { 811 int[] tmp = new int[maxCapacity]; 812 System.arraycopy(lineEnds, 0, tmp, 0, lineCount); 813 lineEnds = tmp; 814 } 815 } 816 817 if (lineEnds != null && lineEnds != oldLineEnds) { 818 lineCache = new SoftReference<int[]>(lineEnds); 819 } 820 return lineEnds; 821 } 822 823 /** 824 * Binary search in the cache for line containing specified offset 825 * (which is relative to the beginning of the view). This method 826 * assumes that cache exists. 827 */ 828 private int findLine(int offset) { 829 int[] lineEnds = lineCache.get(); 830 if (offset < lineEnds[0]) { 831 return 0; 832 } else if (offset > lineEnds[lineCount - 1]) { 833 return lineCount; 834 } else { 835 return findLine(lineEnds, offset, 0, lineCount - 1); 836 } 837 } 838 839 private int findLine(int[] array, int offset, int min, int max) { 840 if (max - min <= 1) { 841 return max; 842 } else { 843 int mid = (max + min) / 2; 844 return (offset < array[mid]) ? 845 findLine(array, offset, min, mid) : 846 findLine(array, offset, mid, max); 847 } 848 } 849 850 int lineCount; 851 SoftReference<int[]> lineCache = null; 852 } 853 }