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 */ 25package javax.swing.text; 26 27import java.util.Vector; 28import java.awt.*; 29import javax.swing.plaf.*; 30import 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 */ 39public 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 static final 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 @SuppressWarnings("deprecation") 409 public void paint(Graphics g, int offs0, int offs1, Shape bounds, JTextComponent c) { 410 Rectangle alloc = bounds.getBounds(); 411 try { 412 // --- determine locations --- 413 TextUI mapper = c.getUI(); 414 Rectangle p0 = mapper.modelToView(c, offs0); 415 Rectangle p1 = mapper.modelToView(c, offs1); 416 417 // --- render --- 418 Color color = getColor(); 419 420 if (color == null) { 421 g.setColor(c.getSelectionColor()); 422 } 423 else { 424 g.setColor(color); 425 } 426 if (p0.y == p1.y) { 427 // same line, render a rectangle 428 Rectangle r = p0.union(p1); 429 g.fillRect(r.x, r.y, r.width, r.height); 430 } else { 431 // different lines 432 int p0ToMarginWidth = alloc.x + alloc.width - p0.x; 433 g.fillRect(p0.x, p0.y, p0ToMarginWidth, p0.height); 434 if ((p0.y + p0.height) != p1.y) { 435 g.fillRect(alloc.x, p0.y + p0.height, alloc.width, 436 p1.y - (p0.y + p0.height)); 437 } 438 g.fillRect(alloc.x, p1.y, (p1.x - alloc.x), p1.height); 439 } 440 } catch (BadLocationException e) { 441 // can't render 442 } 443 } 444 445 // --- LayerPainter methods ---------------------------- 446 /** 447 * Paints a portion of a highlight. 448 * 449 * @param g the graphics context 450 * @param offs0 the starting model offset >= 0 451 * @param offs1 the ending model offset >= offs1 452 * @param bounds the bounding box of the view, which is not 453 * necessarily the region to paint. 454 * @param c the editor 455 * @param view View painting for 456 * @return region drawing occurred in 457 */ 458 public Shape paintLayer(Graphics g, int offs0, int offs1, 459 Shape bounds, JTextComponent c, View view) { 460 Color color = getColor(); 461 462 if (color == null) { 463 g.setColor(c.getSelectionColor()); 464 } 465 else { 466 g.setColor(color); 467 } 468 469 Rectangle r; 470 471 if (offs0 == view.getStartOffset() && 472 offs1 == view.getEndOffset()) { 473 // Contained in view, can just use bounds. 474 if (bounds instanceof Rectangle) { 475 r = (Rectangle) bounds; 476 } 477 else { 478 r = bounds.getBounds(); 479 } 480 } 481 else { 482 // Should only render part of View. 483 try { 484 // --- determine locations --- 485 Shape shape = view.modelToView(offs0, Position.Bias.Forward, 486 offs1,Position.Bias.Backward, 487 bounds); 488 r = (shape instanceof Rectangle) ? 489 (Rectangle)shape : shape.getBounds(); 490 } catch (BadLocationException e) { 491 // can't render 492 r = null; 493 } 494 } 495 496 if (r != null) { 497 // If we are asked to highlight, we should draw something even 498 // if the model-to-view projection is of zero width (6340106). 499 r.width = Math.max(r.width, 1); 500 501 g.fillRect(r.x, r.y, r.width, r.height); 502 } 503 504 return r; 505 } 506 507 private Color color; 508 509 } 510 511 512 class HighlightInfo implements Highlighter.Highlight { 513 514 public int getStartOffset() { 515 return p0.getOffset(); 516 } 517 518 public int getEndOffset() { 519 return p1.getOffset(); 520 } 521 522 public Highlighter.HighlightPainter getPainter() { 523 return painter; 524 } 525 526 Position p0; 527 Position p1; 528 Highlighter.HighlightPainter painter; 529 } 530 531 532 /** 533 * LayeredHighlightPainter is used when a drawsLayeredHighlights is 534 * true. It maintains a rectangle of the region to paint. 535 */ 536 class LayeredHighlightInfo extends HighlightInfo { 537 538 void union(Shape bounds) { 539 if (bounds == null) 540 return; 541 542 Rectangle alloc; 543 if (bounds instanceof Rectangle) { 544 alloc = (Rectangle)bounds; 545 } 546 else { 547 alloc = bounds.getBounds(); 548 } 549 if (width == 0 || height == 0) { 550 x = alloc.x; 551 y = alloc.y; 552 width = alloc.width; 553 height = alloc.height; 554 } 555 else { 556 width = Math.max(x + width, alloc.x + alloc.width); 557 height = Math.max(y + height, alloc.y + alloc.height); 558 x = Math.min(x, alloc.x); 559 width -= x; 560 y = Math.min(y, alloc.y); 561 height -= y; 562 } 563 } 564 565 /** 566 * Restricts the region based on the receivers offsets and messages 567 * the painter to paint the region. 568 */ 569 void paintLayeredHighlights(Graphics g, int p0, int p1, 570 Shape viewBounds, JTextComponent editor, 571 View view) { 572 int start = getStartOffset(); 573 int end = getEndOffset(); 574 // Restrict the region to what we represent 575 p0 = Math.max(start, p0); 576 p1 = Math.min(end, p1); 577 // Paint the appropriate region using the painter and union 578 // the effected region with our bounds. 579 union(((LayeredHighlighter.LayerPainter)painter).paintLayer 580 (g, p0, p1, viewBounds, editor, view)); 581 } 582 583 int x; 584 int y; 585 int width; 586 int height; 587 } 588 589 /** 590 * This class invokes <code>mapper.damageRange</code> in 591 * EventDispatchThread. The only one instance per Highlighter 592 * is cretaed. When a number of ranges should be damaged 593 * it collects them into queue and damages 594 * them in consecutive order in <code>run</code> 595 * call. 596 */ 597 class SafeDamager implements Runnable { 598 private Vector<Position> p0 = new Vector<Position>(10); 599 private Vector<Position> p1 = new Vector<Position>(10); 600 private Document lastDoc = null; 601 602 /** 603 * Executes range(s) damage and cleans range queue. 604 */ 605 public synchronized void run() { 606 if (component != null) { 607 TextUI mapper = component.getUI(); 608 if (mapper != null && lastDoc == component.getDocument()) { 609 // the Document should be the same to properly 610 // display highlights 611 int len = p0.size(); 612 for (int i = 0; i < len; i++){ 613 mapper.damageRange(component, 614 p0.get(i).getOffset(), 615 p1.get(i).getOffset()); 616 } 617 } 618 } 619 p0.clear(); 620 p1.clear(); 621 622 // release reference 623 lastDoc = null; 624 } 625 626 /** 627 * Adds the range to be damaged into the range queue. If the 628 * range queue is empty (the first call or run() was already 629 * invoked) then adds this class instance into EventDispatch 630 * queue. 631 * 632 * The method also tracks if the current document changed or 633 * component is null. In this case it removes all ranges added 634 * before from range queue. 635 */ 636 public synchronized void damageRange(Position pos0, Position pos1) { 637 if (component == null) { 638 p0.clear(); 639 lastDoc = null; 640 return; 641 } 642 643 boolean addToQueue = p0.isEmpty(); 644 Document curDoc = component.getDocument(); 645 if (curDoc != lastDoc) { 646 if (!p0.isEmpty()) { 647 p0.clear(); 648 p1.clear(); 649 } 650 lastDoc = curDoc; 651 } 652 p0.add(pos0); 653 p1.add(pos1); 654 655 if (addToQueue) { 656 SwingUtilities.invokeLater(this); 657 } 658 } 659 } 660} 661