1 /*
   2  * Copyright (c) 1997, 2015, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 package javax.swing.text;
  26 
  27 import java.util.Vector;
  28 import java.awt.*;
  29 import javax.swing.plaf.*;
  30 import javax.swing.*;
  31 
  32 /**
  33  * Implements the Highlighter interfaces.  Implements a simple highlight
  34  * painter that renders in a solid color.
  35  *
  36  * @author  Timothy Prinzing
  37  * @see     Highlighter
  38  */
  39 public class DefaultHighlighter extends LayeredHighlighter {
  40 
  41     /**
  42      * Creates a new DefaultHighlighther object.
  43      */
  44     public DefaultHighlighter() {
  45         drawsLayeredHighlights = true;
  46     }
  47 
  48     // ---- Highlighter methods ----------------------------------------------
  49 
  50     /**
  51      * Renders the highlights.
  52      *
  53      * @param g the graphics context
  54      */
  55     public void paint(Graphics g) {
  56         // PENDING(prinz) - should cull ranges not visible
  57         int len = highlights.size();
  58         for (int i = 0; i < len; i++) {
  59             HighlightInfo info = highlights.elementAt(i);
  60             if (!(info instanceof LayeredHighlightInfo)) {
  61                 // Avoid allocing unless we need it.
  62                 Rectangle a = component.getBounds();
  63                 Insets insets = component.getInsets();
  64                 a.x = insets.left;
  65                 a.y = insets.top;
  66                 a.width -= insets.left + insets.right;
  67                 a.height -= insets.top + insets.bottom;
  68                 for (; i < len; i++) {
  69                     info = highlights.elementAt(i);
  70                     if (!(info instanceof LayeredHighlightInfo)) {
  71                         Highlighter.HighlightPainter p = info.getPainter();
  72                         p.paint(g, info.getStartOffset(), info.getEndOffset(),
  73                                 a, component);
  74                     }
  75                 }
  76             }
  77         }
  78     }
  79 
  80     /**
  81      * Called when the UI is being installed into the
  82      * interface of a JTextComponent.  Installs the editor, and
  83      * removes any existing highlights.
  84      *
  85      * @param c the editor component
  86      * @see Highlighter#install
  87      */
  88     public void install(JTextComponent c) {
  89         component = c;
  90         removeAllHighlights();
  91     }
  92 
  93     /**
  94      * Called when the UI is being removed from the interface of
  95      * a JTextComponent.
  96      *
  97      * @param c the component
  98      * @see Highlighter#deinstall
  99      */
 100     public void deinstall(JTextComponent c) {
 101         component = null;
 102     }
 103 
 104     /**
 105      * Adds a highlight to the view.  Returns a tag that can be used
 106      * to refer to the highlight.
 107      *
 108      * @param p0   the start offset of the range to highlight &gt;= 0
 109      * @param p1   the end offset of the range to highlight &gt;= p0
 110      * @param p    the painter to use to actually render the highlight
 111      * @return     an object that can be used as a tag
 112      *   to refer to the highlight
 113      * @exception BadLocationException if the specified location is invalid
 114      */
 115     public Object addHighlight(int p0, int p1, Highlighter.HighlightPainter p) throws BadLocationException {
 116         if (p0 < 0) {
 117             throw new BadLocationException("Invalid start offset", p0);
 118         }
 119 
 120         if (p1 < p0) {
 121             throw new BadLocationException("Invalid end offset", p1);
 122         }
 123 
 124         Document doc = component.getDocument();
 125         HighlightInfo i = (getDrawsLayeredHighlights() &&
 126                            (p instanceof LayeredHighlighter.LayerPainter)) ?
 127                           new LayeredHighlightInfo() : new HighlightInfo();
 128         i.painter = p;
 129         i.p0 = doc.createPosition(p0);
 130         i.p1 = doc.createPosition(p1);
 131         highlights.addElement(i);
 132         safeDamageRange(p0, p1);
 133         return i;
 134     }
 135 
 136     /**
 137      * Removes a highlight from the view.
 138      *
 139      * @param tag the reference to the highlight
 140      */
 141     public void removeHighlight(Object tag) {
 142         if (tag instanceof LayeredHighlightInfo) {
 143             LayeredHighlightInfo lhi = (LayeredHighlightInfo)tag;
 144             if (lhi.width > 0 && lhi.height > 0) {
 145                 component.repaint(lhi.x, lhi.y, lhi.width, lhi.height);
 146             }
 147         }
 148         else {
 149             HighlightInfo info = (HighlightInfo) tag;
 150             safeDamageRange(info.p0, info.p1);
 151         }
 152         highlights.removeElement(tag);
 153     }
 154 
 155     /**
 156      * Removes all highlights.
 157      */
 158     public void removeAllHighlights() {
 159         TextUI mapper = component.getUI();
 160         if (getDrawsLayeredHighlights()) {
 161             int len = highlights.size();
 162             if (len != 0) {
 163                 int minX = 0;
 164                 int minY = 0;
 165                 int maxX = 0;
 166                 int maxY = 0;
 167                 int p0 = -1;
 168                 int p1 = -1;
 169                 for (int i = 0; i < len; i++) {
 170                     HighlightInfo hi = highlights.elementAt(i);
 171                     if (hi instanceof LayeredHighlightInfo) {
 172                         LayeredHighlightInfo info = (LayeredHighlightInfo)hi;
 173                         minX = Math.min(minX, info.x);
 174                         minY = Math.min(minY, info.y);
 175                         maxX = Math.max(maxX, info.x + info.width);
 176                         maxY = Math.max(maxY, info.y + info.height);
 177                     }
 178                     else {
 179                         if (p0 == -1) {
 180                             p0 = hi.p0.getOffset();
 181                             p1 = hi.p1.getOffset();
 182                         }
 183                         else {
 184                             p0 = Math.min(p0, hi.p0.getOffset());
 185                             p1 = Math.max(p1, hi.p1.getOffset());
 186                         }
 187                     }
 188                 }
 189                 if (minX != maxX && minY != maxY) {
 190                     component.repaint(minX, minY, maxX - minX, maxY - minY);
 191                 }
 192                 if (p0 != -1) {
 193                     try {
 194                         safeDamageRange(p0, p1);
 195                     } catch (BadLocationException e) {}
 196                 }
 197                 highlights.removeAllElements();
 198             }
 199         }
 200         else if (mapper != null) {
 201             int len = highlights.size();
 202             if (len != 0) {
 203                 int p0 = Integer.MAX_VALUE;
 204                 int p1 = 0;
 205                 for (int i = 0; i < len; i++) {
 206                     HighlightInfo info = highlights.elementAt(i);
 207                     p0 = Math.min(p0, info.p0.getOffset());
 208                     p1 = Math.max(p1, info.p1.getOffset());
 209                 }
 210                 try {
 211                     safeDamageRange(p0, p1);
 212                 } catch (BadLocationException e) {}
 213 
 214                 highlights.removeAllElements();
 215             }
 216         }
 217     }
 218 
 219     /**
 220      * Changes a highlight.
 221      *
 222      * @param tag the highlight tag
 223      * @param p0 the beginning of the range &gt;= 0
 224      * @param p1 the end of the range &gt;= p0
 225      * @exception BadLocationException if the specified location is invalid
 226      */
 227     public void changeHighlight(Object tag, int p0, int p1) throws BadLocationException {
 228         if (p0 < 0) {
 229             throw new BadLocationException("Invalid beginning of the range", p0);
 230         }
 231 
 232         if (p1 < p0) {
 233             throw new BadLocationException("Invalid end of the range", p1);
 234         }
 235 
 236         Document doc = component.getDocument();
 237         if (tag instanceof LayeredHighlightInfo) {
 238             LayeredHighlightInfo lhi = (LayeredHighlightInfo)tag;
 239             if (lhi.width > 0 && lhi.height > 0) {
 240                 component.repaint(lhi.x, lhi.y, lhi.width, lhi.height);
 241             }
 242             // Mark the highlights region as invalid, it will reset itself
 243             // next time asked to paint.
 244             lhi.width = lhi.height = 0;
 245             lhi.p0 = doc.createPosition(p0);
 246             lhi.p1 = doc.createPosition(p1);
 247             safeDamageRange(Math.min(p0, p1), Math.max(p0, p1));
 248         }
 249         else {
 250             HighlightInfo info = (HighlightInfo) tag;
 251             int oldP0 = info.p0.getOffset();
 252             int oldP1 = info.p1.getOffset();
 253             if (p0 == oldP0) {
 254                 safeDamageRange(Math.min(oldP1, p1),
 255                                    Math.max(oldP1, p1));
 256             } else if (p1 == oldP1) {
 257                 safeDamageRange(Math.min(p0, oldP0),
 258                                    Math.max(p0, oldP0));
 259             } else {
 260                 safeDamageRange(oldP0, oldP1);
 261                 safeDamageRange(p0, p1);
 262             }
 263             info.p0 = doc.createPosition(p0);
 264             info.p1 = doc.createPosition(p1);
 265         }
 266     }
 267 
 268     /**
 269      * Makes a copy of the highlights.  Does not actually clone each highlight,
 270      * but only makes references to them.
 271      *
 272      * @return the copy
 273      * @see Highlighter#getHighlights
 274      */
 275     public Highlighter.Highlight[] getHighlights() {
 276         int size = highlights.size();
 277         if (size == 0) {
 278             return noHighlights;
 279         }
 280         Highlighter.Highlight[] h = new Highlighter.Highlight[size];
 281         highlights.copyInto(h);
 282         return h;
 283     }
 284 
 285     /**
 286      * When leaf Views (such as LabelView) are rendering they should
 287      * call into this method. If a highlight is in the given region it will
 288      * be drawn immediately.
 289      *
 290      * @param g Graphics used to draw
 291      * @param p0 starting offset of view
 292      * @param p1 ending offset of view
 293      * @param viewBounds Bounds of View
 294      * @param editor JTextComponent
 295      * @param view View instance being rendered
 296      */
 297     public void paintLayeredHighlights(Graphics g, int p0, int p1,
 298                                        Shape viewBounds,
 299                                        JTextComponent editor, View view) {
 300         for (int counter = highlights.size() - 1; counter >= 0; counter--) {
 301             HighlightInfo tag = highlights.elementAt(counter);
 302             if (tag instanceof LayeredHighlightInfo) {
 303                 LayeredHighlightInfo lhi = (LayeredHighlightInfo)tag;
 304                 int start = lhi.getStartOffset();
 305                 int end = lhi.getEndOffset();
 306                 if ((p0 < start && p1 > start) ||
 307                     (p0 >= start && p0 < end)) {
 308                     lhi.paintLayeredHighlights(g, p0, p1, viewBounds,
 309                                                editor, view);
 310                 }
 311             }
 312         }
 313     }
 314 
 315     /**
 316      * Queues damageRange() call into event dispatch thread
 317      * to be sure that views are in consistent state.
 318      */
 319     private void safeDamageRange(final Position p0, final Position p1) {
 320         safeDamager.damageRange(p0, p1);
 321     }
 322 
 323     /**
 324      * Queues damageRange() call into event dispatch thread
 325      * to be sure that views are in consistent state.
 326      */
 327     private void safeDamageRange(int a0, int a1) throws BadLocationException {
 328         Document doc = component.getDocument();
 329         safeDamageRange(doc.createPosition(a0), doc.createPosition(a1));
 330     }
 331 
 332     /**
 333      * If true, highlights are drawn as the Views draw the text. That is
 334      * the Views will call into <code>paintLayeredHighlight</code> which
 335      * will result in a rectangle being drawn before the text is drawn
 336      * (if the offsets are in a highlighted region that is). For this to
 337      * work the painter supplied must be an instance of
 338      * LayeredHighlightPainter.
 339      * @param newValue the new value
 340      */
 341     public void setDrawsLayeredHighlights(boolean newValue) {
 342         drawsLayeredHighlights = newValue;
 343     }
 344 
 345     /**
 346      * Return the draw layered highlights.
 347      * @return the draw layered highlights
 348      */
 349     public boolean getDrawsLayeredHighlights() {
 350         return drawsLayeredHighlights;
 351     }
 352 
 353     // ---- member variables --------------------------------------------
 354 
 355     private final static Highlighter.Highlight[] noHighlights =
 356             new Highlighter.Highlight[0];
 357     private Vector<HighlightInfo> highlights = new Vector<HighlightInfo>();
 358     private JTextComponent component;
 359     private boolean drawsLayeredHighlights;
 360     private SafeDamager safeDamager = new SafeDamager();
 361 
 362 
 363     /**
 364      * Default implementation of LayeredHighlighter.LayerPainter that can
 365      * be used for painting highlights.
 366      * <p>
 367      * As of 1.4 this field is final.
 368      */
 369     public static final LayeredHighlighter.LayerPainter DefaultPainter = new DefaultHighlightPainter(null);
 370 
 371 
 372     /**
 373      * Simple highlight painter that fills a highlighted area with
 374      * a solid color.
 375      */
 376     public static class DefaultHighlightPainter extends LayeredHighlighter.LayerPainter {
 377 
 378         /**
 379          * Constructs a new highlight painter. If <code>c</code> is null,
 380          * the JTextComponent will be queried for its selection color.
 381          *
 382          * @param c the color for the highlight
 383          */
 384         public DefaultHighlightPainter(Color c) {
 385             color = c;
 386         }
 387 
 388         /**
 389          * Returns the color of the highlight.
 390          *
 391          * @return the color
 392          */
 393         public Color getColor() {
 394             return color;
 395         }
 396 
 397         // --- HighlightPainter methods ---------------------------------------
 398 
 399         /**
 400          * Paints a highlight.
 401          *
 402          * @param g the graphics context
 403          * @param offs0 the starting model offset &gt;= 0
 404          * @param offs1 the ending model offset &gt;= offs1
 405          * @param bounds the bounding box for the highlight
 406          * @param c the editor
 407          */
 408         public void paint(Graphics g, int offs0, int offs1, Shape bounds, JTextComponent c) {
 409             Rectangle alloc = bounds.getBounds();
 410             try {
 411                 // --- determine locations ---
 412                 TextUI mapper = c.getUI();
 413                 Rectangle p0 = mapper.modelToView(c, offs0);
 414                 Rectangle p1 = mapper.modelToView(c, offs1);
 415 
 416                 // --- render ---
 417                 Color color = getColor();
 418 
 419                 if (color == null) {
 420                     g.setColor(c.getSelectionColor());
 421                 }
 422                 else {
 423                     g.setColor(color);
 424                 }
 425                 if (p0.y == p1.y) {
 426                     // same line, render a rectangle
 427                     Rectangle r = p0.union(p1);
 428                     g.fillRect(r.x, r.y, r.width, r.height);
 429                 } else {
 430                     // different lines
 431                     int p0ToMarginWidth = alloc.x + alloc.width - p0.x;
 432                     g.fillRect(p0.x, p0.y, p0ToMarginWidth, p0.height);
 433                     if ((p0.y + p0.height) != p1.y) {
 434                         g.fillRect(alloc.x, p0.y + p0.height, alloc.width,
 435                                    p1.y - (p0.y + p0.height));
 436                     }
 437                     g.fillRect(alloc.x, p1.y, (p1.x - alloc.x), p1.height);
 438                 }
 439             } catch (BadLocationException e) {
 440                 // can't render
 441             }
 442         }
 443 
 444         // --- LayerPainter methods ----------------------------
 445         /**
 446          * Paints a portion of a highlight.
 447          *
 448          * @param g the graphics context
 449          * @param offs0 the starting model offset &gt;= 0
 450          * @param offs1 the ending model offset &gt;= offs1
 451          * @param bounds the bounding box of the view, which is not
 452          *        necessarily the region to paint.
 453          * @param c the editor
 454          * @param view View painting for
 455          * @return region drawing occurred in
 456          */
 457         public Shape paintLayer(Graphics g, int offs0, int offs1,
 458                                 Shape bounds, JTextComponent c, View view) {
 459             Color color = getColor();
 460 
 461             if (color == null) {
 462                 g.setColor(c.getSelectionColor());
 463             }
 464             else {
 465                 g.setColor(color);
 466             }
 467 
 468             Rectangle r;
 469 
 470             if (offs0 == view.getStartOffset() &&
 471                 offs1 == view.getEndOffset()) {
 472                 // Contained in view, can just use bounds.
 473                 if (bounds instanceof Rectangle) {
 474                     r = (Rectangle) bounds;
 475                 }
 476                 else {
 477                     r = bounds.getBounds();
 478                 }
 479             }
 480             else {
 481                 // Should only render part of View.
 482                 try {
 483                     // --- determine locations ---
 484                     Shape shape = view.modelToView(offs0, Position.Bias.Forward,
 485                                                    offs1,Position.Bias.Backward,
 486                                                    bounds);
 487                     r = (shape instanceof Rectangle) ?
 488                                   (Rectangle)shape : shape.getBounds();
 489                 } catch (BadLocationException e) {
 490                     // can't render
 491                     r = null;
 492                 }
 493             }
 494 
 495             if (r != null) {
 496                 // If we are asked to highlight, we should draw something even
 497                 // if the model-to-view projection is of zero width (6340106).
 498                 r.width = Math.max(r.width, 1);
 499 
 500                 g.fillRect(r.x, r.y, r.width, r.height);
 501             }
 502 
 503             return r;
 504         }
 505 
 506         private Color color;
 507 
 508     }
 509 
 510 
 511     class HighlightInfo implements Highlighter.Highlight {
 512 
 513         public int getStartOffset() {
 514             return p0.getOffset();
 515         }
 516 
 517         public int getEndOffset() {
 518             return p1.getOffset();
 519         }
 520 
 521         public Highlighter.HighlightPainter getPainter() {
 522             return painter;
 523         }
 524 
 525         Position p0;
 526         Position p1;
 527         Highlighter.HighlightPainter painter;
 528     }
 529 
 530 
 531     /**
 532      * LayeredHighlightPainter is used when a drawsLayeredHighlights is
 533      * true. It maintains a rectangle of the region to paint.
 534      */
 535     class LayeredHighlightInfo extends HighlightInfo {
 536 
 537         void union(Shape bounds) {
 538             if (bounds == null)
 539                 return;
 540 
 541             Rectangle alloc;
 542             if (bounds instanceof Rectangle) {
 543                 alloc = (Rectangle)bounds;
 544             }
 545             else {
 546                 alloc = bounds.getBounds();
 547             }
 548             if (width == 0 || height == 0) {
 549                 x = alloc.x;
 550                 y = alloc.y;
 551                 width = alloc.width;
 552                 height = alloc.height;
 553             }
 554             else {
 555                 width = Math.max(x + width, alloc.x + alloc.width);
 556                 height = Math.max(y + height, alloc.y + alloc.height);
 557                 x = Math.min(x, alloc.x);
 558                 width -= x;
 559                 y = Math.min(y, alloc.y);
 560                 height -= y;
 561             }
 562         }
 563 
 564         /**
 565          * Restricts the region based on the receivers offsets and messages
 566          * the painter to paint the region.
 567          */
 568         void paintLayeredHighlights(Graphics g, int p0, int p1,
 569                                     Shape viewBounds, JTextComponent editor,
 570                                     View view) {
 571             int start = getStartOffset();
 572             int end = getEndOffset();
 573             // Restrict the region to what we represent
 574             p0 = Math.max(start, p0);
 575             p1 = Math.min(end, p1);
 576             // Paint the appropriate region using the painter and union
 577             // the effected region with our bounds.
 578             union(((LayeredHighlighter.LayerPainter)painter).paintLayer
 579                   (g, p0, p1, viewBounds, editor, view));
 580         }
 581 
 582         int x;
 583         int y;
 584         int width;
 585         int height;
 586     }
 587 
 588     /**
 589      * This class invokes <code>mapper.damageRange</code> in
 590      * EventDispatchThread. The only one instance per Highlighter
 591      * is cretaed. When a number of ranges should be damaged
 592      * it collects them into queue and damages
 593      * them in consecutive order in <code>run</code>
 594      * call.
 595      */
 596     class SafeDamager implements Runnable {
 597         private Vector<Position> p0 = new Vector<Position>(10);
 598         private Vector<Position> p1 = new Vector<Position>(10);
 599         private Document lastDoc = null;
 600 
 601         /**
 602          * Executes range(s) damage and cleans range queue.
 603          */
 604         public synchronized void run() {
 605             if (component != null) {
 606                 TextUI mapper = component.getUI();
 607                 if (mapper != null && lastDoc == component.getDocument()) {
 608                     // the Document should be the same to properly
 609                     // display highlights
 610                     int len = p0.size();
 611                     for (int i = 0; i < len; i++){
 612                         mapper.damageRange(component,
 613                                 p0.get(i).getOffset(),
 614                                 p1.get(i).getOffset());
 615                     }
 616                 }
 617             }
 618             p0.clear();
 619             p1.clear();
 620 
 621             // release reference
 622             lastDoc = null;
 623         }
 624 
 625         /**
 626          * Adds the range to be damaged into the range queue. If the
 627          * range queue is empty (the first call or run() was already
 628          * invoked) then adds this class instance into EventDispatch
 629          * queue.
 630          *
 631          * The method also tracks if the current document changed or
 632          * component is null. In this case it removes all ranges added
 633          * before from range queue.
 634          */
 635         public synchronized void damageRange(Position pos0, Position pos1) {
 636             if (component == null) {
 637                 p0.clear();
 638                 lastDoc = null;
 639                 return;
 640             }
 641 
 642             boolean addToQueue = p0.isEmpty();
 643             Document curDoc = component.getDocument();
 644             if (curDoc != lastDoc) {
 645                 if (!p0.isEmpty()) {
 646                     p0.clear();
 647                     p1.clear();
 648                 }
 649                 lastDoc = curDoc;
 650             }
 651             p0.add(pos0);
 652             p1.add(pos1);
 653 
 654             if (addToQueue) {
 655                 SwingUtilities.invokeLater(this);
 656             }
 657         }
 658     }
 659 }