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 &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 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 &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        @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 &gt;= 0
451         * @param offs1 the ending model offset &gt;= 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