1 /* 2 * Copyright (c) 1997, 2017, 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.awt.font.FontRenderContext; 29 import java.awt.geom.Rectangle2D; 30 import java.security.AccessController; 31 import java.security.PrivilegedAction; 32 import java.util.Objects; 33 import javax.swing.event.*; 34 import java.lang.ref.SoftReference; 35 import java.util.HashMap; 36 37 /** 38 * Implements View interface for a simple multi-line text view 39 * that has text in one font and color. The view represents each 40 * child element as a line of text. 41 * 42 * @author Timothy Prinzing 43 * @see View 44 */ 45 public class PlainView extends View implements TabExpander { 46 47 /** 48 * Constructs a new PlainView wrapped on an element. 49 * 50 * @param elem the element 51 */ 52 public PlainView(Element elem) { 53 super(elem); 54 } 55 56 /** 57 * Returns the tab size set for the document, defaulting to 8. 58 * 59 * @return the tab size 60 */ 61 protected int getTabSize() { 62 Integer i = (Integer) getDocument().getProperty(PlainDocument.tabSizeAttribute); 63 int size = (i != null) ? i.intValue() : 8; 64 return size; 65 } 66 67 /** 68 * Renders a line of text, suppressing whitespace at the end 69 * and expanding any tabs. This is implemented to make calls 70 * to the methods <code>drawUnselectedText</code> and 71 * <code>drawSelectedText</code> so that the way selected and 72 * unselected text are rendered can be customized. 73 * 74 * @param lineIndex the line to draw >= 0 75 * @param g the <code>Graphics</code> context 76 * @param x the starting X position >= 0 77 * @param y the starting Y position >= 0 78 * @see #drawUnselectedText 79 * @see #drawSelectedText 80 * 81 * @deprecated replaced by 82 * {@link #drawLine(int, Graphics2D, float, float)} 83 */ 84 @Deprecated(since = "9") 85 protected void drawLine(int lineIndex, Graphics g, int x, int y) { 86 drawLineImpl(lineIndex, g, x, y); 87 } 88 89 private void drawLineImpl(int lineIndex, Graphics g, float x, float y) { 90 Element line = getElement().getElement(lineIndex); 91 Element elem; 92 93 try { 94 if (line.isLeaf()) { 95 drawElement(lineIndex, line, g, x, y); 96 } else { 97 // this line contains the composed text. 98 int count = line.getElementCount(); 99 for(int i = 0; i < count; i++) { 100 elem = line.getElement(i); 101 x = drawElement(lineIndex, elem, g, x, y); 102 } 103 } 104 } catch (BadLocationException e) { 105 throw new StateInvariantError("Can't render line: " + lineIndex); 106 } 107 } 108 109 /** 110 * Renders a line of text, suppressing whitespace at the end 111 * and expanding any tabs. This is implemented to make calls 112 * to the methods {@code drawUnselectedText} and 113 * {@code drawSelectedText} so that the way selected and 114 * unselected text are rendered can be customized. 115 * 116 * @param lineIndex the line to draw {@code >= 0} 117 * @param g the {@code Graphics} context 118 * @param x the starting X position {@code >= 0} 119 * @param y the starting Y position {@code >= 0} 120 * @see #drawUnselectedText 121 * @see #drawSelectedText 122 * 123 * @since 9 124 */ 125 protected void drawLine(int lineIndex, Graphics2D g, float x, float y) { 126 drawLineImpl(lineIndex, g, x, y); 127 } 128 129 private float drawElement(int lineIndex, Element elem, Graphics g, 130 float x, float y) 131 throws BadLocationException 132 { 133 int p0 = elem.getStartOffset(); 134 int p1 = elem.getEndOffset(); 135 p1 = Math.min(getDocument().getLength(), p1); 136 137 if (lineIndex == 0) { 138 x += firstLineOffset; 139 } 140 AttributeSet attr = elem.getAttributes(); 141 if (Utilities.isComposedTextAttributeDefined(attr)) { 142 g.setColor(unselected); 143 x = Utilities.drawComposedText(this, attr, g, x, y, 144 p0-elem.getStartOffset(), 145 p1-elem.getStartOffset()); 146 } else { 147 if (sel0 == sel1 || selected == unselected) { 148 // no selection, or it is invisible 149 x = callDrawUnselectedText(g, x, y, p0, p1); 150 } else if ((p0 >= sel0 && p0 <= sel1) && (p1 >= sel0 && p1 <= sel1)) { 151 x = callDrawSelectedText(g, x, y, p0, p1); 152 } else if (sel0 >= p0 && sel0 <= p1) { 153 if (sel1 >= p0 && sel1 <= p1) { 154 x = callDrawUnselectedText(g, x, y, p0, sel0); 155 x = callDrawSelectedText(g, x, y, sel0, sel1); 156 x = callDrawUnselectedText(g, x, y, sel1, p1); 157 } else { 158 x = callDrawUnselectedText(g, x, y, p0, sel0); 159 x = callDrawSelectedText(g, x, y, sel0, p1); 160 } 161 } else if (sel1 >= p0 && sel1 <= p1) { 162 x = callDrawSelectedText(g, x, y, p0, sel1); 163 x = callDrawUnselectedText(g, x, y, sel1, p1); 164 } else { 165 x = callDrawUnselectedText(g, x, y, p0, p1); 166 } 167 } 168 169 return x; 170 } 171 172 /** 173 * Renders the given range in the model as normal unselected 174 * text. Uses the foreground or disabled color to render the text. 175 * 176 * @param g the graphics context 177 * @param x the starting X coordinate >= 0 178 * @param y the starting Y coordinate >= 0 179 * @param p0 the beginning position in the model >= 0 180 * @param p1 the ending position in the model >= 0 181 * @return the X location of the end of the range >= 0 182 * @exception BadLocationException if the range is invalid 183 * 184 * @deprecated replaced by 185 * {@link #drawUnselectedText(Graphics2D, float, float, int, int)} 186 */ 187 @Deprecated(since = "9") 188 protected int drawUnselectedText(Graphics g, int x, int y, 189 int p0, int p1) throws BadLocationException { 190 return (int) drawUnselectedTextImpl(g, x, y, p0, p1, false); 191 } 192 193 private float callDrawUnselectedText(Graphics g, float x, float y, 194 int p0, int p1) 195 throws BadLocationException 196 { 197 return drawUnselectedTextOverridden && (g instanceof Graphics2D) 198 ? drawUnselectedText((Graphics2D) g, x, y, p0, p1) 199 : drawUnselectedText(g, (int) x, (int) y, p0, p1); 200 } 201 202 private float drawUnselectedTextImpl(Graphics g, float x, float y, 203 int p0, int p1, 204 boolean useFPAPI) 205 throws BadLocationException 206 { 207 g.setColor(unselected); 208 Document doc = getDocument(); 209 Segment s = SegmentCache.getSharedSegment(); 210 doc.getText(p0, p1 - p0, s); 211 float ret = Utilities.drawTabbedText(this, s, x, y, g, this, p0, null, 212 useFPAPI); 213 SegmentCache.releaseSharedSegment(s); 214 return ret; 215 } 216 217 /** 218 * Renders the given range in the model as normal unselected 219 * text. Uses the foreground or disabled color to render the text. 220 * 221 * @param g the graphics context 222 * @param x the starting X coordinate {@code >= 0} 223 * @param y the starting Y coordinate {@code >= 0} 224 * @param p0 the beginning position in the model {@code >= 0} 225 * @param p1 the ending position in the model {@code >= 0} 226 * @return the X location of the end of the range {@code >= 0} 227 * @exception BadLocationException if the range is invalid 228 * 229 * @since 9 230 */ 231 protected float drawUnselectedText(Graphics2D g, float x, float y, 232 int p0, int p1) throws BadLocationException { 233 return drawUnselectedTextImpl(g, x, y, p0, p1, true); 234 } 235 236 /** 237 * Renders the given range in the model as selected text. This 238 * is implemented to render the text in the color specified in 239 * the hosting component. It assumes the highlighter will render 240 * the selected background. 241 * 242 * @param g the graphics context 243 * @param x the starting X coordinate >= 0 244 * @param y the starting Y coordinate >= 0 245 * @param p0 the beginning position in the model >= 0 246 * @param p1 the ending position in the model >= 0 247 * @return the location of the end of the range 248 * @exception BadLocationException if the range is invalid 249 * 250 * @deprecated replaced by 251 * {@link #drawSelectedText(Graphics2D, float, float, int, int)} 252 */ 253 @Deprecated(since = "9") 254 protected int drawSelectedText(Graphics g, int x, 255 int y, int p0, int p1) 256 throws BadLocationException 257 { 258 return (int) drawSelectedTextImpl(g, x, y, p0, p1, false); 259 } 260 261 float callDrawSelectedText(Graphics g, float x, float y, 262 int p0, int p1) 263 throws BadLocationException 264 { 265 return drawSelectedTextOverridden && g instanceof Graphics2D 266 ? drawSelectedText((Graphics2D) g, x, y, p0, p1) 267 : drawSelectedText(g, (int) x, (int) y, p0, p1); 268 } 269 270 private float drawSelectedTextImpl(Graphics g, float x, float y, 271 int p0, int p1, 272 boolean useFPAPI) 273 throws BadLocationException 274 { 275 g.setColor(selected); 276 Document doc = getDocument(); 277 Segment s = SegmentCache.getSharedSegment(); 278 doc.getText(p0, p1 - p0, s); 279 float ret = Utilities.drawTabbedText(this, s, x, y, g, this, p0, null, 280 useFPAPI); 281 SegmentCache.releaseSharedSegment(s); 282 return ret; 283 } 284 285 /** 286 * Renders the given range in the model as selected text. This 287 * is implemented to render the text in the color specified in 288 * the hosting component. It assumes the highlighter will render 289 * the selected background. 290 * 291 * @param g the graphics context 292 * @param x the starting X coordinate {@code >= 0} 293 * @param y the starting Y coordinate {@code >= 0} 294 * @param p0 the beginning position in the model {@code >= 0} 295 * @param p1 the ending position in the model {@code >= 0} 296 * @return the location of the end of the range 297 * @exception BadLocationException if the range is invalid 298 * 299 * @since 9 300 */ 301 protected float drawSelectedText(Graphics2D g, float x, 302 float y, int p0, int p1) throws BadLocationException { 303 return drawSelectedTextImpl(g, x, y, p0, p1, true); 304 } 305 306 /** 307 * Gives access to a buffer that can be used to fetch 308 * text from the associated document. 309 * 310 * @return the buffer 311 */ 312 protected final Segment getLineBuffer() { 313 if (lineBuffer == null) { 314 lineBuffer = new Segment(); 315 } 316 return lineBuffer; 317 } 318 319 /** 320 * Checks to see if the font metrics and longest line 321 * are up-to-date. 322 * 323 * @since 1.4 324 */ 325 protected void updateMetrics() { 326 Component host = getContainer(); 327 Font f = host.getFont(); 328 FontMetrics fm = (font == null) ? null : host.getFontMetrics(font); 329 if (font != f || !Objects.equals(metrics, fm)) { 330 // The font changed, we need to recalculate the 331 // longest line. 332 calculateLongestLine(); 333 if (useFloatingPointAPI) { 334 FontRenderContext frc = metrics.getFontRenderContext(); 335 float tabWidth = (float) font.getStringBounds("m", frc).getWidth(); 336 tabSize = getTabSize() * tabWidth; 337 } else { 338 tabSize = getTabSize() * metrics.charWidth('m'); 339 } 340 } 341 } 342 343 // ---- View methods ---------------------------------------------------- 344 345 /** 346 * Determines the preferred span for this view along an 347 * axis. 348 * 349 * @param axis may be either View.X_AXIS or View.Y_AXIS 350 * @return the span the view would like to be rendered into >= 0. 351 * Typically the view is told to render into the span 352 * that is returned, although there is no guarantee. 353 * The parent may choose to resize or break the view. 354 * @exception IllegalArgumentException for an invalid axis 355 */ 356 public float getPreferredSpan(int axis) { 357 updateMetrics(); 358 switch (axis) { 359 case View.X_AXIS: 360 return getLineWidth(longLine); 361 case View.Y_AXIS: 362 return getElement().getElementCount() * metrics.getHeight(); 363 default: 364 throw new IllegalArgumentException("Invalid axis: " + axis); 365 } 366 } 367 368 /** 369 * Renders using the given rendering surface and area on that surface. 370 * The view may need to do layout and create child views to enable 371 * itself to render into the given allocation. 372 * 373 * @param g the rendering surface to use 374 * @param a the allocated region to render into 375 * 376 * @see View#paint 377 */ 378 public void paint(Graphics g, Shape a) { 379 Shape originalA = a; 380 a = adjustPaintRegion(a); 381 Rectangle alloc = (Rectangle) a; 382 tabBase = alloc.x; 383 JTextComponent host = (JTextComponent) getContainer(); 384 Highlighter h = host.getHighlighter(); 385 g.setFont(host.getFont()); 386 sel0 = host.getSelectionStart(); 387 sel1 = host.getSelectionEnd(); 388 unselected = (host.isEnabled()) ? 389 host.getForeground() : host.getDisabledTextColor(); 390 Caret c = host.getCaret(); 391 selected = c.isSelectionVisible() && h != null ? 392 host.getSelectedTextColor() : unselected; 393 updateMetrics(); 394 395 // If the lines are clipped then we don't expend the effort to 396 // try and paint them. Since all of the lines are the same height 397 // with this object, determination of what lines need to be repainted 398 // is quick. 399 Rectangle clip = g.getClipBounds(); 400 int fontHeight = metrics.getHeight(); 401 int heightBelow = (alloc.y + alloc.height) - (clip.y + clip.height); 402 int heightAbove = clip.y - alloc.y; 403 int linesBelow, linesAbove, linesTotal; 404 405 if (fontHeight > 0) { 406 linesBelow = Math.max(0, heightBelow / fontHeight); 407 linesAbove = Math.max(0, heightAbove / fontHeight); 408 linesTotal = alloc.height / fontHeight; 409 if (alloc.height % fontHeight != 0) { 410 linesTotal++; 411 } 412 } else { 413 linesBelow = linesAbove = linesTotal = 0; 414 } 415 416 // update the visible lines 417 Rectangle lineArea = lineToRect(a, linesAbove); 418 int y = lineArea.y + metrics.getAscent(); 419 int x = lineArea.x; 420 Element map = getElement(); 421 int lineCount = map.getElementCount(); 422 int endLine = Math.min(lineCount, linesTotal - linesBelow); 423 lineCount--; 424 LayeredHighlighter dh = (h instanceof LayeredHighlighter) ? 425 (LayeredHighlighter)h : null; 426 for (int line = linesAbove; line < endLine; line++) { 427 if (dh != null) { 428 Element lineElement = map.getElement(line); 429 if (line == lineCount) { 430 dh.paintLayeredHighlights(g, lineElement.getStartOffset(), 431 lineElement.getEndOffset(), 432 originalA, host, this); 433 } 434 else { 435 dh.paintLayeredHighlights(g, lineElement.getStartOffset(), 436 lineElement.getEndOffset() - 1, 437 originalA, host, this); 438 } 439 } 440 if (drawLineOverridden && (g instanceof Graphics2D)) { 441 drawLine(line, (Graphics2D) g, (float) x, (float) y); 442 } else { 443 drawLine(line, g, x, y); 444 } 445 y += fontHeight; 446 if (line == 0) { 447 // This should never really happen, in so far as if 448 // firstLineOffset is non 0, there should only be one 449 // line of text. 450 x -= firstLineOffset; 451 } 452 } 453 } 454 455 /** 456 * Should return a shape ideal for painting based on the passed in 457 * Shape <code>a</code>. This is useful if painting in a different 458 * region. The default implementation returns <code>a</code>. 459 */ 460 Shape adjustPaintRegion(Shape a) { 461 return a; 462 } 463 464 /** 465 * Provides a mapping from the document model coordinate space 466 * to the coordinate space of the view mapped to it. 467 * 468 * @param pos the position to convert >= 0 469 * @param a the allocated region to render into 470 * @return the bounding box of the given position 471 * @exception BadLocationException if the given position does not 472 * represent a valid location in the associated document 473 * @see View#modelToView 474 */ 475 @SuppressWarnings("deprecation") 476 public Shape modelToView(int pos, Shape a, Position.Bias b) throws BadLocationException { 477 // line coordinates 478 Document doc = getDocument(); 479 Element map = getElement(); 480 int lineIndex = map.getElementIndex(pos); 481 if (lineIndex < 0) { 482 return lineToRect(a, 0); 483 } 484 Rectangle lineArea = lineToRect(a, lineIndex); 485 486 // determine span from the start of the line 487 tabBase = lineArea.x; 488 Element line = map.getElement(lineIndex); 489 int p0 = line.getStartOffset(); 490 Segment s = SegmentCache.getSharedSegment(); 491 doc.getText(p0, pos - p0, s); 492 493 if (useFloatingPointAPI) { 494 float xOffs = Utilities.getTabbedTextWidth(s, metrics, (float) tabBase, this, p0); 495 SegmentCache.releaseSharedSegment(s); 496 return new Rectangle2D.Float(lineArea.x + xOffs, lineArea.y, 1, metrics.getHeight()); 497 } 498 499 int xOffs = Utilities.getTabbedTextWidth(s, metrics, tabBase, this,p0); 500 SegmentCache.releaseSharedSegment(s); 501 502 // fill in the results and return 503 lineArea.x += xOffs; 504 lineArea.width = 1; 505 lineArea.height = metrics.getHeight(); 506 return lineArea; 507 } 508 509 /** 510 * Provides a mapping from the view coordinate space to the logical 511 * coordinate space of the model. 512 * 513 * @param x the X coordinate >= 0 514 * @param y the Y coordinate >= 0 515 * @param a the allocated region to render into 516 * @return the location within the model that best represents the 517 * given point in the view >= 0 518 * @see View#viewToModel 519 */ 520 public int viewToModel(float x, float y, Shape a, Position.Bias[] bias) { 521 // PENDING(prinz) properly calculate bias 522 bias[0] = Position.Bias.Forward; 523 524 Rectangle alloc = a.getBounds(); 525 Document doc = getDocument(); 526 527 if (y < alloc.y) { 528 // above the area covered by this icon, so the position 529 // is assumed to be the start of the coverage for this view. 530 return getStartOffset(); 531 } else if (y > alloc.y + alloc.height) { 532 // below the area covered by this icon, so the position 533 // is assumed to be the end of the coverage for this view. 534 return getEndOffset() - 1; 535 } else { 536 // positioned within the coverage of this view vertically, 537 // so we figure out which line the point corresponds to. 538 // if the line is greater than the number of lines contained, then 539 // simply use the last line as it represents the last possible place 540 // we can position to. 541 Element map = doc.getDefaultRootElement(); 542 int fontHeight = metrics.getHeight(); 543 int lineIndex = (fontHeight > 0 ? 544 (int)Math.abs((y - alloc.y) / fontHeight) : 545 map.getElementCount() - 1); 546 if (lineIndex >= map.getElementCount()) { 547 return getEndOffset() - 1; 548 } 549 Element line = map.getElement(lineIndex); 550 int dx = 0; 551 if (lineIndex == 0) { 552 alloc.x += firstLineOffset; 553 alloc.width -= firstLineOffset; 554 } 555 if (x < alloc.x) { 556 // point is to the left of the line 557 return line.getStartOffset(); 558 } else if (x > alloc.x + alloc.width) { 559 // point is to the right of the line 560 return line.getEndOffset() - 1; 561 } else { 562 // Determine the offset into the text 563 try { 564 int p0 = line.getStartOffset(); 565 int p1 = line.getEndOffset() - 1; 566 Segment s = SegmentCache.getSharedSegment(); 567 doc.getText(p0, p1 - p0, s); 568 tabBase = alloc.x; 569 int offs = p0 + Utilities.getTabbedTextOffset(s, metrics, 570 tabBase, x, this, p0, true); 571 SegmentCache.releaseSharedSegment(s); 572 return offs; 573 } catch (BadLocationException e) { 574 // should not happen 575 return -1; 576 } 577 } 578 } 579 } 580 581 /** 582 * Gives notification that something was inserted into the document 583 * in a location that this view is responsible for. 584 * 585 * @param changes the change information from the associated document 586 * @param a the current allocation of the view 587 * @param f the factory to use to rebuild if the view has children 588 * @see View#insertUpdate 589 */ 590 public void insertUpdate(DocumentEvent changes, Shape a, ViewFactory f) { 591 updateDamage(changes, a, f); 592 } 593 594 /** 595 * Gives notification that something was removed from the document 596 * in a location that this view is responsible for. 597 * 598 * @param changes the change information from the associated document 599 * @param a the current allocation of the view 600 * @param f the factory to use to rebuild if the view has children 601 * @see View#removeUpdate 602 */ 603 public void removeUpdate(DocumentEvent changes, Shape a, ViewFactory f) { 604 updateDamage(changes, a, f); 605 } 606 607 /** 608 * Gives notification from the document that attributes were changed 609 * in a location that this view is responsible for. 610 * 611 * @param changes the change information from the associated document 612 * @param a the current allocation of the view 613 * @param f the factory to use to rebuild if the view has children 614 * @see View#changedUpdate 615 */ 616 public void changedUpdate(DocumentEvent changes, Shape a, ViewFactory f) { 617 updateDamage(changes, a, f); 618 } 619 620 /** 621 * Sets the size of the view. This should cause 622 * layout of the view along the given axis, if it 623 * has any layout duties. 624 * 625 * @param width the width >= 0 626 * @param height the height >= 0 627 */ 628 public void setSize(float width, float height) { 629 super.setSize(width, height); 630 updateMetrics(); 631 } 632 633 // --- TabExpander methods ------------------------------------------ 634 635 /** 636 * Returns the next tab stop position after a given reference position. 637 * This implementation does not support things like centering so it 638 * ignores the tabOffset argument. 639 * 640 * @param x the current position >= 0 641 * @param tabOffset the position within the text stream 642 * that the tab occurred at >= 0. 643 * @return the tab stop, measured in points >= 0 644 */ 645 public float nextTabStop(float x, int tabOffset) { 646 if (tabSize == 0) { 647 return x; 648 } 649 int ntabs = ((int) x - tabBase) / (int)tabSize; 650 return tabBase + ((ntabs + 1) * tabSize); 651 } 652 653 // --- local methods ------------------------------------------------ 654 655 /** 656 * Repaint the region of change covered by the given document 657 * event. Damages the line that begins the range to cover 658 * the case when the insert/remove is only on one line. 659 * If lines are added or removed, damages the whole 660 * view. The longest line is checked to see if it has 661 * changed. 662 * 663 * @param changes the change information from the associated document 664 * @param a the current allocation of the view 665 * @param f the factory to use to rebuild if the view has children 666 * @since 1.4 667 */ 668 protected void updateDamage(DocumentEvent changes, Shape a, ViewFactory f) { 669 Component host = getContainer(); 670 updateMetrics(); 671 Element elem = getElement(); 672 DocumentEvent.ElementChange ec = changes.getChange(elem); 673 674 Element[] added = (ec != null) ? ec.getChildrenAdded() : null; 675 Element[] removed = (ec != null) ? ec.getChildrenRemoved() : null; 676 if (((added != null) && (added.length > 0)) || 677 ((removed != null) && (removed.length > 0))) { 678 // lines were added or removed... 679 if (added != null) { 680 int currWide = getLineWidth(longLine); 681 for (int i = 0; i < added.length; i++) { 682 int w = getLineWidth(added[i]); 683 if (w > currWide) { 684 currWide = w; 685 longLine = added[i]; 686 } 687 } 688 } 689 if (removed != null) { 690 for (int i = 0; i < removed.length; i++) { 691 if (removed[i] == longLine) { 692 calculateLongestLine(); 693 break; 694 } 695 } 696 } 697 preferenceChanged(null, true, true); 698 host.repaint(); 699 } else { 700 Element map = getElement(); 701 int line = map.getElementIndex(changes.getOffset()); 702 damageLineRange(line, line, a, host); 703 if (changes.getType() == DocumentEvent.EventType.INSERT) { 704 // check to see if the line is longer than current 705 // longest line. 706 int w = getLineWidth(longLine); 707 Element e = map.getElement(line); 708 if (e == longLine) { 709 preferenceChanged(null, true, false); 710 } else if (getLineWidth(e) > w) { 711 longLine = e; 712 preferenceChanged(null, true, false); 713 } 714 } else if (changes.getType() == DocumentEvent.EventType.REMOVE) { 715 if (map.getElement(line) == longLine) { 716 // removed from longest line... recalc 717 calculateLongestLine(); 718 preferenceChanged(null, true, false); 719 } 720 } 721 } 722 } 723 724 /** 725 * Repaint the given line range. 726 * 727 * @param host the component hosting the view (used to call repaint) 728 * @param a the region allocated for the view to render into 729 * @param line0 the starting line number to repaint. This must 730 * be a valid line number in the model. 731 * @param line1 the ending line number to repaint. This must 732 * be a valid line number in the model. 733 * @since 1.4 734 */ 735 protected void damageLineRange(int line0, int line1, Shape a, Component host) { 736 if (a != null) { 737 Rectangle area0 = lineToRect(a, line0); 738 Rectangle area1 = lineToRect(a, line1); 739 if ((area0 != null) && (area1 != null)) { 740 Rectangle damage = area0.union(area1); 741 host.repaint(damage.x, damage.y, damage.width, damage.height); 742 } else { 743 host.repaint(); 744 } 745 } 746 } 747 748 /** 749 * Determine the rectangle that represents the given line. 750 * 751 * @param a the region allocated for the view to render into 752 * @param line the line number to find the region of. This must 753 * be a valid line number in the model. 754 * @return the rectangle that represents the given line 755 * @since 1.4 756 */ 757 protected Rectangle lineToRect(Shape a, int line) { 758 Rectangle r = null; 759 updateMetrics(); 760 if (metrics != null) { 761 Rectangle alloc = a.getBounds(); 762 if (line == 0) { 763 alloc.x += firstLineOffset; 764 alloc.width -= firstLineOffset; 765 } 766 r = new Rectangle(alloc.x, alloc.y + (line * metrics.getHeight()), 767 alloc.width, metrics.getHeight()); 768 } 769 return r; 770 } 771 772 /** 773 * Iterate over the lines represented by the child elements 774 * of the element this view represents, looking for the line 775 * that is the longest. The <em>longLine</em> variable is updated to 776 * represent the longest line contained. The <em>font</em> variable 777 * is updated to indicate the font used to calculate the 778 * longest line. 779 */ 780 private void calculateLongestLine() { 781 Component c = getContainer(); 782 font = c.getFont(); 783 metrics = c.getFontMetrics(font); 784 Document doc = getDocument(); 785 Element lines = getElement(); 786 int n = lines.getElementCount(); 787 int maxWidth = -1; 788 for (int i = 0; i < n; i++) { 789 Element line = lines.getElement(i); 790 int w = getLineWidth(line); 791 if (w > maxWidth) { 792 maxWidth = w; 793 longLine = line; 794 } 795 } 796 } 797 798 /** 799 * Calculate the width of the line represented by 800 * the given element. It is assumed that the font 801 * and font metrics are up-to-date. 802 */ 803 @SuppressWarnings("deprecation") 804 private int getLineWidth(Element line) { 805 if (line == null) { 806 return 0; 807 } 808 int p0 = line.getStartOffset(); 809 int p1 = line.getEndOffset(); 810 int w; 811 Segment s = SegmentCache.getSharedSegment(); 812 try { 813 line.getDocument().getText(p0, p1 - p0, s); 814 w = Utilities.getTabbedTextWidth(s, metrics, tabBase, this, p0); 815 } catch (BadLocationException ble) { 816 w = 0; 817 } 818 SegmentCache.releaseSharedSegment(s); 819 return w; 820 } 821 822 static boolean getFPMethodOverridden(Class<?> cls, String method, 823 FPMethodArgs methodArgs) { 824 HashMap<FPMethodItem, Boolean> map = null; 825 boolean initialized = methodsOverriddenMapRef != null 826 && (map = methodsOverriddenMapRef.get()) != null; 827 828 if (!initialized) { 829 map = new HashMap<>(); 830 methodsOverriddenMapRef = new SoftReference<>(map); 831 } 832 833 FPMethodItem key = new FPMethodItem(cls, method); 834 Boolean isFPMethodOverridden = map.get(key); 835 if (isFPMethodOverridden == null) { 836 isFPMethodOverridden = checkFPMethodOverridden(cls, method, methodArgs); 837 map.put(key, isFPMethodOverridden); 838 } 839 return isFPMethodOverridden; 840 } 841 842 private static boolean checkFPMethodOverridden(final Class<?> className, 843 final String methodName, 844 final FPMethodArgs methodArgs) { 845 846 return AccessController 847 .doPrivileged(new PrivilegedAction<Boolean>() { 848 @Override 849 public Boolean run() { 850 return isFPMethodOverridden(methodName, className, 851 methodArgs.getMethodArguments(false), 852 methodArgs.getMethodArguments(true)); 853 } 854 }); 855 } 856 857 private static boolean isFPMethodOverridden(String method, 858 Class<?> cls, 859 Class<?>[] intTypes, 860 Class<?>[] fpTypes) 861 { 862 Module thisModule = PlainView.class.getModule(); 863 while (!thisModule.equals(cls.getModule())) { 864 try { 865 cls.getDeclaredMethod(method, fpTypes); 866 return true; 867 } catch (Exception e1) { 868 try { 869 cls.getDeclaredMethod(method, intTypes); 870 return false; 871 } catch (Exception e2) { 872 cls = cls.getSuperclass(); 873 } 874 } 875 } 876 return true; 877 } 878 879 enum FPMethodArgs { 880 881 IGNN, 882 IIGNN, 883 GNNII, 884 GNNC; 885 886 public Class<?>[] getMethodArguments(boolean isFPType) { 887 Class<?> N = (isFPType) ? Float.TYPE : Integer.TYPE; 888 Class<?> G = (isFPType) ? Graphics2D.class : Graphics.class; 889 switch (this) { 890 case IGNN: 891 return new Class<?>[]{Integer.TYPE, G, N, N}; 892 case IIGNN: 893 return new Class<?>[]{Integer.TYPE, Integer.TYPE, G, N, N}; 894 case GNNII: 895 return new Class<?>[]{G, N, N, Integer.TYPE, Integer.TYPE}; 896 case GNNC: 897 return new Class<?>[]{G, N, N, Character.TYPE}; 898 default: 899 throw new RuntimeException("Unknown method arguments!"); 900 } 901 } 902 } 903 904 private static class FPMethodItem { 905 906 final Class<?> className; 907 final String methodName; 908 909 public FPMethodItem(Class<?> className, String methodName) { 910 this.className = className; 911 this.methodName = methodName; 912 } 913 914 @Override 915 public boolean equals(Object obj) { 916 if (obj instanceof FPMethodItem) { 917 FPMethodItem that = (FPMethodItem) obj; 918 return this.className.equals(that.className) 919 && this.methodName.equals(that.methodName); 920 } 921 return false; 922 } 923 924 @Override 925 public int hashCode() { 926 return 31 * methodName.hashCode() + className.hashCode(); 927 } 928 } 929 930 // --- member variables ----------------------------------------------- 931 932 /** 933 * Font metrics for the current font. 934 */ 935 protected FontMetrics metrics; 936 937 /** 938 * The current longest line. This is used to calculate 939 * the preferred width of the view. Since the calculation 940 * is potentially expensive we try to avoid it by stashing 941 * which line is currently the longest. 942 */ 943 Element longLine; 944 945 /** 946 * Font used to calculate the longest line... if this 947 * changes we need to recalculate the longest line 948 */ 949 Font font; 950 951 Segment lineBuffer; 952 float tabSize; 953 int tabBase; 954 955 int sel0; 956 int sel1; 957 Color unselected; 958 Color selected; 959 960 /** 961 * Offset of where to draw the first character on the first line. 962 * This is a hack and temporary until we can better address the problem 963 * of text measuring. This field is actually never set directly in 964 * PlainView, but by FieldView. 965 */ 966 int firstLineOffset; 967 968 private static SoftReference<HashMap<FPMethodItem, Boolean>> methodsOverriddenMapRef; 969 final boolean drawLineOverridden = 970 getFPMethodOverridden(getClass(), "drawLine", FPMethodArgs.IGNN); 971 final boolean drawSelectedTextOverridden = 972 getFPMethodOverridden(getClass(), "drawSelectedText", FPMethodArgs.GNNII); 973 final boolean drawUnselectedTextOverridden = 974 getFPMethodOverridden(getClass(), "drawUnselectedText", FPMethodArgs.GNNII); 975 final boolean useFloatingPointAPI = 976 drawUnselectedTextOverridden || drawSelectedTextOverridden; 977 }