1 /*
   2  * Copyright (c) 1997, 2008, 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 >= 0
 109      * @param p1   the end offset of the range to highlight >= 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 >= 0
 224      * @param p1 the end of the range >= 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      */
 340     public void setDrawsLayeredHighlights(boolean newValue) {
 341         drawsLayeredHighlights = newValue;
 342     }
 343 
 344     public boolean getDrawsLayeredHighlights() {
 345         return drawsLayeredHighlights;
 346     }
 347 
 348     // ---- member variables --------------------------------------------
 349 
 350     private final static Highlighter.Highlight[] noHighlights =
 351             new Highlighter.Highlight[0];
 352     private Vector<HighlightInfo> highlights = new Vector<HighlightInfo>();
 353     private JTextComponent component;
 354     private boolean drawsLayeredHighlights;
 355     private SafeDamager safeDamager = new SafeDamager();
 356 
 357 
 358     /**
 359      * Default implementation of LayeredHighlighter.LayerPainter that can
 360      * be used for painting highlights.
 361      * <p>
 362      * As of 1.4 this field is final.
 363      */
 364     public static final LayeredHighlighter.LayerPainter DefaultPainter = new DefaultHighlightPainter(null);
 365 
 366 
 367     /**
 368      * Simple highlight painter that fills a highlighted area with
 369      * a solid color.
 370      */
 371     public static class DefaultHighlightPainter extends LayeredHighlighter.LayerPainter {
 372 
 373         /**
 374          * Constructs a new highlight painter. If <code>c</code> is null,
 375          * the JTextComponent will be queried for its selection color.
 376          *
 377          * @param c the color for the highlight
 378          */
 379         public DefaultHighlightPainter(Color c) {
 380             color = c;
 381         }
 382 
 383         /**
 384          * Returns the color of the highlight.
 385          *
 386          * @return the color
 387          */
 388         public Color getColor() {
 389             return color;
 390         }
 391 
 392         // --- HighlightPainter methods ---------------------------------------
 393 
 394         /**
 395          * Paints a highlight.
 396          *
 397          * @param g the graphics context
 398          * @param offs0 the starting model offset >= 0
 399          * @param offs1 the ending model offset >= offs1
 400          * @param bounds the bounding box for the highlight
 401          * @param c the editor
 402          */
 403         public void paint(Graphics g, int offs0, int offs1, Shape bounds, JTextComponent c) {
 404             Rectangle alloc = bounds.getBounds();
 405             try {
 406                 // --- determine locations ---
 407                 TextUI mapper = c.getUI();
 408                 Rectangle p0 = mapper.modelToView(c, offs0);
 409                 Rectangle p1 = mapper.modelToView(c, offs1);
 410 
 411                 // --- render ---
 412                 Color color = getColor();
 413 
 414                 if (color == null) {
 415                     g.setColor(c.getSelectionColor());
 416                 }
 417                 else {
 418                     g.setColor(color);
 419                 }
 420                 if (p0.y == p1.y) {
 421                     // same line, render a rectangle
 422                     Rectangle r = p0.union(p1);
 423                     g.fillRect(r.x, r.y, r.width, r.height);
 424                 } else {
 425                     // different lines
 426                     int p0ToMarginWidth = alloc.x + alloc.width - p0.x;
 427                     g.fillRect(p0.x, p0.y, p0ToMarginWidth, p0.height);
 428                     if ((p0.y + p0.height) != p1.y) {
 429                         g.fillRect(alloc.x, p0.y + p0.height, alloc.width,
 430                                    p1.y - (p0.y + p0.height));
 431                     }
 432                     g.fillRect(alloc.x, p1.y, (p1.x - alloc.x), p1.height);
 433                 }
 434             } catch (BadLocationException e) {
 435                 // can't render
 436             }
 437         }
 438 
 439         // --- LayerPainter methods ----------------------------
 440         /**
 441          * Paints a portion of a highlight.
 442          *
 443          * @param g the graphics context
 444          * @param offs0 the starting model offset >= 0
 445          * @param offs1 the ending model offset >= offs1
 446          * @param bounds the bounding box of the view, which is not
 447          *        necessarily the region to paint.
 448          * @param c the editor
 449          * @param view View painting for
 450          * @return region drawing occured in
 451          */
 452         public Shape paintLayer(Graphics g, int offs0, int offs1,
 453                                 Shape bounds, JTextComponent c, View view) {
 454             Color color = getColor();
 455 
 456             if (color == null) {
 457                 g.setColor(c.getSelectionColor());
 458             }
 459             else {
 460                 g.setColor(color);
 461             }
 462 
 463             Rectangle r;
 464 
 465             if (offs0 == view.getStartOffset() &&
 466                 offs1 == view.getEndOffset()) {
 467                 // Contained in view, can just use bounds.
 468                 if (bounds instanceof Rectangle) {
 469                     r = (Rectangle) bounds;
 470                 }
 471                 else {
 472                     r = bounds.getBounds();
 473                 }
 474             }
 475             else {
 476                 // Should only render part of View.
 477                 try {
 478                     // --- determine locations ---
 479                     Shape shape = view.modelToView(offs0, Position.Bias.Forward,
 480                                                    offs1,Position.Bias.Backward,
 481                                                    bounds);
 482                     r = (shape instanceof Rectangle) ?
 483                                   (Rectangle)shape : shape.getBounds();
 484                 } catch (BadLocationException e) {
 485                     // can't render
 486                     r = null;
 487                 }
 488             }
 489 
 490             if (r != null) {
 491                 // If we are asked to highlight, we should draw something even
 492                 // if the model-to-view projection is of zero width (6340106).
 493                 r.width = Math.max(r.width, 1);
 494 
 495                 g.fillRect(r.x, r.y, r.width, r.height);
 496             }
 497 
 498             return r;
 499         }
 500 
 501         private Color color;
 502 
 503     }
 504 
 505 
 506     class HighlightInfo implements Highlighter.Highlight {
 507 
 508         public int getStartOffset() {
 509             return p0.getOffset();
 510         }
 511 
 512         public int getEndOffset() {
 513             return p1.getOffset();
 514         }
 515 
 516         public Highlighter.HighlightPainter getPainter() {
 517             return painter;
 518         }
 519 
 520         Position p0;
 521         Position p1;
 522         Highlighter.HighlightPainter painter;
 523     }
 524 
 525 
 526     /**
 527      * LayeredHighlightPainter is used when a drawsLayeredHighlights is
 528      * true. It maintains a rectangle of the region to paint.
 529      */
 530     class LayeredHighlightInfo extends HighlightInfo {
 531 
 532         void union(Shape bounds) {
 533             if (bounds == null)
 534                 return;
 535 
 536             Rectangle alloc;
 537             if (bounds instanceof Rectangle) {
 538                 alloc = (Rectangle)bounds;
 539             }
 540             else {
 541                 alloc = bounds.getBounds();
 542             }
 543             if (width == 0 || height == 0) {
 544                 x = alloc.x;
 545                 y = alloc.y;
 546                 width = alloc.width;
 547                 height = alloc.height;
 548             }
 549             else {
 550                 width = Math.max(x + width, alloc.x + alloc.width);
 551                 height = Math.max(y + height, alloc.y + alloc.height);
 552                 x = Math.min(x, alloc.x);
 553                 width -= x;
 554                 y = Math.min(y, alloc.y);
 555                 height -= y;
 556             }
 557         }
 558 
 559         /**
 560          * Restricts the region based on the receivers offsets and messages
 561          * the painter to paint the region.
 562          */
 563         void paintLayeredHighlights(Graphics g, int p0, int p1,
 564                                     Shape viewBounds, JTextComponent editor,
 565                                     View view) {
 566             int start = getStartOffset();
 567             int end = getEndOffset();
 568             // Restrict the region to what we represent
 569             p0 = Math.max(start, p0);
 570             p1 = Math.min(end, p1);
 571             // Paint the appropriate region using the painter and union
 572             // the effected region with our bounds.
 573             union(((LayeredHighlighter.LayerPainter)painter).paintLayer
 574                   (g, p0, p1, viewBounds, editor, view));
 575         }
 576 
 577         int x;
 578         int y;
 579         int width;
 580         int height;
 581     }
 582 
 583     /**
 584      * This class invokes <code>mapper.damageRange</code> in
 585      * EventDispatchThread. The only one instance per Highlighter
 586      * is cretaed. When a number of ranges should be damaged
 587      * it collects them into queue and damages
 588      * them in consecutive order in <code>run</code>
 589      * call.
 590      */
 591     class SafeDamager implements Runnable {
 592         private Vector<Position> p0 = new Vector<Position>(10);
 593         private Vector<Position> p1 = new Vector<Position>(10);
 594         private Document lastDoc = null;
 595 
 596         /**
 597          * Executes range(s) damage and cleans range queue.
 598          */
 599         public synchronized void run() {
 600             if (component != null) {
 601                 TextUI mapper = component.getUI();
 602                 if (mapper != null && lastDoc == component.getDocument()) {
 603                     // the Document should be the same to properly
 604                     // display highlights
 605                     int len = p0.size();
 606                     for (int i = 0; i < len; i++){
 607                         mapper.damageRange(component,
 608                                 p0.get(i).getOffset(),
 609                                 p1.get(i).getOffset());
 610                     }
 611                 }
 612             }
 613             p0.clear();
 614             p1.clear();
 615 
 616             // release reference
 617             lastDoc = null;
 618         }
 619 
 620         /**
 621          * Adds the range to be damaged into the range queue. If the
 622          * range queue is empty (the first call or run() was already
 623          * invoked) then adds this class instance into EventDispatch
 624          * queue.
 625          *
 626          * The method also tracks if the current document changed or
 627          * component is null. In this case it removes all ranges added
 628          * before from range queue.
 629          */
 630         public synchronized void damageRange(Position pos0, Position pos1) {
 631             if (component == null) {
 632                 p0.clear();
 633                 lastDoc = null;
 634                 return;
 635             }
 636 
 637             boolean addToQueue = p0.isEmpty();
 638             Document curDoc = component.getDocument();
 639             if (curDoc != lastDoc) {
 640                 if (!p0.isEmpty()) {
 641                     p0.clear();
 642                     p1.clear();
 643                 }
 644                 lastDoc = curDoc;
 645             }
 646             p0.add(pos0);
 647             p1.add(pos1);
 648 
 649             if (addToQueue) {
 650                 SwingUtilities.invokeLater(this);
 651             }
 652         }
 653     }
 654 }