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 &gt;= 0
  75      * @param g the <code>Graphics</code> context
  76      * @param x the starting X position &gt;= 0
  77      * @param y the starting Y position &gt;= 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 &gt;= 0
 178      * @param y the starting Y coordinate &gt;= 0
 179      * @param p0 the beginning position in the model &gt;= 0
 180      * @param p1 the ending position in the model &gt;= 0
 181      * @return the X location of the end of the range &gt;= 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 &gt;= 0
 244      * @param y the starting Y coordinate &gt;= 0
 245      * @param p0 the beginning position in the model &gt;= 0
 246      * @param p1 the ending position in the model &gt;= 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 &gt;= 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 &gt;= 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 &gt;= 0
 514      * @param y the Y coordinate &gt;= 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 &gt;= 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 &gt;= 0
 626      * @param height the height &gt;= 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 &gt;= 0
 641      * @param tabOffset the position within the text stream
 642      *   that the tab occurred at &gt;= 0.
 643      * @return the tab stop, measured in points &gt;= 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 }