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