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