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 >= 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 * @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 >= 0 404 * @param offs1 the ending model offset >= 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 >= 0 450 * @param offs1 the ending model offset >= 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 }