You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
661 lines
21 KiB
661 lines
21 KiB
/* |
|
* Copyright 2007-2008 Sun Microsystems, Inc. All Rights Reserved. |
|
* |
|
* Redistribution and use in source and binary forms, with or without |
|
* modification, are permitted provided that the following conditions |
|
* are met: |
|
* |
|
* - Redistributions of source code must retain the above copyright |
|
* notice, this list of conditions and the following disclaimer. |
|
* |
|
* - Redistributions in binary form must reproduce the above copyright |
|
* notice, this list of conditions and the following disclaimer in the |
|
* documentation and/or other materials provided with the distribution. |
|
* |
|
* - Neither the name of Sun Microsystems nor the names of its |
|
* contributors may be used to endorse or promote products derived |
|
* from this software without specific prior written permission. |
|
* |
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS |
|
* IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, |
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR |
|
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, |
|
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, |
|
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR |
|
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF |
|
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING |
|
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS |
|
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
|
*/ |
|
|
|
package com.sun.swingset3.codeview; |
|
|
|
import java.awt.Color; |
|
import java.awt.Graphics; |
|
import java.awt.Insets; |
|
import java.awt.Rectangle; |
|
import java.awt.Shape; |
|
import java.util.*; |
|
import javax.swing.SwingUtilities; |
|
import javax.swing.plaf.TextUI; |
|
import javax.swing.text.BadLocationException; |
|
import javax.swing.text.Document; |
|
import javax.swing.text.Highlighter; |
|
import javax.swing.text.JTextComponent; |
|
import javax.swing.text.LayeredHighlighter; |
|
import javax.swing.text.Position; |
|
import javax.swing.text.View; |
|
|
|
/** |
|
* Implements the Highlighter interfaces. Implements a simple highlight |
|
* painter that renders in a solid color. |
|
* |
|
* @see Highlighter |
|
*/ |
|
public class SnippetHighlighter extends LayeredHighlighter { |
|
|
|
/** |
|
* Creates a new DefaultHighlighther object. |
|
*/ |
|
public SnippetHighlighter() { |
|
drawsLayeredHighlights = true; |
|
} |
|
|
|
// ---- Highlighter methods ---------------------------------------------- |
|
|
|
/** |
|
* Renders the highlights. |
|
* |
|
* @param g the graphics context |
|
*/ |
|
public void paint(Graphics g) { |
|
// PENDING(prinz) - should cull ranges not visible |
|
int len = highlights.size(); |
|
for (int i = 0; i < len; i++) { |
|
HighlightInfo info = highlights.elementAt(i); |
|
if (!(info instanceof LayeredHighlightInfo)) { |
|
// Avoid allocing unless we need it. |
|
Rectangle a = component.getBounds(); |
|
Insets insets = component.getInsets(); |
|
a.x = insets.left; |
|
a.y = insets.top; |
|
a.width -= insets.left + insets.right; |
|
a.height -= insets.top + insets.bottom; |
|
for (; i < len; i++) { |
|
info = highlights.elementAt(i); |
|
if (!(info instanceof LayeredHighlightInfo)) { |
|
Highlighter.HighlightPainter p = info.getPainter(); |
|
p.paint(g, info.getStartOffset(), info.getEndOffset(), |
|
a, component); |
|
} |
|
} |
|
} |
|
} |
|
} |
|
|
|
/** |
|
* Called when the UI is being installed into the |
|
* interface of a JTextComponent. Installs the editor, and |
|
* removes any existing highlights. |
|
* |
|
* @param c the editor component |
|
* @see Highlighter#install |
|
*/ |
|
public void install(JTextComponent c) { |
|
component = c; |
|
removeAllHighlights(); |
|
} |
|
|
|
/** |
|
* Called when the UI is being removed from the interface of |
|
* a JTextComponent. |
|
* |
|
* @param c the component |
|
* @see Highlighter#deinstall |
|
*/ |
|
public void deinstall(JTextComponent c) { |
|
component = null; |
|
} |
|
|
|
/** |
|
* Adds a highlight to the view. Returns a tag that can be used |
|
* to refer to the highlight. |
|
* |
|
* @param p0 the start offset of the range to highlight >= 0 |
|
* @param p1 the end offset of the range to highlight >= p0 |
|
* @param p the painter to use to actually render the highlight |
|
* @return an object that can be used as a tag |
|
* to refer to the highlight |
|
* @exception BadLocationException if the specified location is invalid |
|
*/ |
|
public Object addHighlight(int p0, int p1, Highlighter.HighlightPainter p) throws BadLocationException { |
|
Document doc = component.getDocument(); |
|
HighlightInfo i = (getDrawsLayeredHighlights() && |
|
(p instanceof LayeredHighlighter.LayerPainter)) ? |
|
new LayeredHighlightInfo() : new HighlightInfo(); |
|
i.painter = p; |
|
i.p0 = doc.createPosition(p0); |
|
i.p1 = doc.createPosition(p1); |
|
// For snippets, we want to make sure selection is layered ON TOP |
|
// since we add transparency to the selection color; so rather |
|
// than append the highlight, we insert it in the front. |
|
highlights.insertElementAt(i, 0); |
|
safeDamageRange(p0, p1); |
|
return i; |
|
} |
|
|
|
/** |
|
* Removes a highlight from the view. |
|
* |
|
* @param tag the reference to the highlight |
|
*/ |
|
public void removeHighlight(Object tag) { |
|
if (tag instanceof LayeredHighlightInfo) { |
|
LayeredHighlightInfo lhi = (LayeredHighlightInfo)tag; |
|
if (lhi.width > 0 && lhi.height > 0) { |
|
component.repaint(lhi.x, lhi.y, lhi.width, lhi.height); |
|
} |
|
} |
|
else { |
|
HighlightInfo info = (HighlightInfo) tag; |
|
safeDamageRange(info.p0, info.p1); |
|
} |
|
highlights.removeElement(tag); |
|
} |
|
|
|
/** |
|
* Removes all highlights. |
|
*/ |
|
public void removeAllHighlights() { |
|
TextUI mapper = component.getUI(); |
|
if (getDrawsLayeredHighlights()) { |
|
int len = highlights.size(); |
|
if (len != 0) { |
|
int minX = 0; |
|
int minY = 0; |
|
int maxX = 0; |
|
int maxY = 0; |
|
int p0 = -1; |
|
int p1 = -1; |
|
for (int i = 0; i < len; i++) { |
|
HighlightInfo hi = highlights.elementAt(i); |
|
if (hi instanceof LayeredHighlightInfo) { |
|
LayeredHighlightInfo info = (LayeredHighlightInfo)hi; |
|
minX = Math.min(minX, info.x); |
|
minY = Math.min(minY, info.y); |
|
maxX = Math.max(maxX, info.x + info.width); |
|
maxY = Math.max(maxY, info.y + info.height); |
|
} |
|
else { |
|
if (p0 == -1) { |
|
p0 = hi.p0.getOffset(); |
|
p1 = hi.p1.getOffset(); |
|
} |
|
else { |
|
p0 = Math.min(p0, hi.p0.getOffset()); |
|
p1 = Math.max(p1, hi.p1.getOffset()); |
|
} |
|
} |
|
} |
|
if (minX != maxX && minY != maxY) { |
|
component.repaint(minX, minY, maxX - minX, maxY - minY); |
|
} |
|
if (p0 != -1) { |
|
try { |
|
safeDamageRange(p0, p1); |
|
} catch (BadLocationException e) {} |
|
} |
|
highlights.removeAllElements(); |
|
} |
|
} |
|
else if (mapper != null) { |
|
int len = highlights.size(); |
|
if (len != 0) { |
|
int p0 = Integer.MAX_VALUE; |
|
int p1 = 0; |
|
for (int i = 0; i < len; i++) { |
|
HighlightInfo info = highlights.elementAt(i); |
|
p0 = Math.min(p0, info.p0.getOffset()); |
|
p1 = Math.max(p1, info.p1.getOffset()); |
|
} |
|
try { |
|
safeDamageRange(p0, p1); |
|
} catch (BadLocationException e) {} |
|
|
|
highlights.removeAllElements(); |
|
} |
|
} |
|
} |
|
|
|
/** |
|
* Changes a highlight. |
|
* |
|
* @param tag the highlight tag |
|
* @param p0 the beginning of the range >= 0 |
|
* @param p1 the end of the range >= p0 |
|
* @exception BadLocationException if the specified location is invalid |
|
*/ |
|
public void changeHighlight(Object tag, int p0, int p1) throws BadLocationException { |
|
Document doc = component.getDocument(); |
|
if (tag instanceof LayeredHighlightInfo) { |
|
LayeredHighlightInfo lhi = (LayeredHighlightInfo)tag; |
|
if (lhi.width > 0 && lhi.height > 0) { |
|
component.repaint(lhi.x, lhi.y, lhi.width, lhi.height); |
|
} |
|
// Mark the highlights region as invalid, it will reset itself |
|
// next time asked to paint. |
|
lhi.width = lhi.height = 0; |
|
lhi.p0 = doc.createPosition(p0); |
|
lhi.p1 = doc.createPosition(p1); |
|
safeDamageRange(Math.min(p0, p1), Math.max(p0, p1)); |
|
} |
|
else { |
|
HighlightInfo info = (HighlightInfo) tag; |
|
int oldP0 = info.p0.getOffset(); |
|
int oldP1 = info.p1.getOffset(); |
|
if (p0 == oldP0) { |
|
safeDamageRange(Math.min(oldP1, p1), |
|
Math.max(oldP1, p1)); |
|
} else if (p1 == oldP1) { |
|
safeDamageRange(Math.min(p0, oldP0), |
|
Math.max(p0, oldP0)); |
|
} else { |
|
safeDamageRange(oldP0, oldP1); |
|
safeDamageRange(p0, p1); |
|
} |
|
info.p0 = doc.createPosition(p0); |
|
info.p1 = doc.createPosition(p1); |
|
} |
|
} |
|
|
|
/** |
|
* Makes a copy of the highlights. Does not actually clone each highlight, |
|
* but only makes references to them. |
|
* |
|
* @return the copy |
|
* @see Highlighter#getHighlights |
|
*/ |
|
public Highlighter.Highlight[] getHighlights() { |
|
int size = highlights.size(); |
|
if (size == 0) { |
|
return noHighlights; |
|
} |
|
Highlighter.Highlight[] h = new Highlighter.Highlight[size]; |
|
highlights.copyInto(h); |
|
return h; |
|
} |
|
|
|
/** |
|
* When leaf Views (such as LabelView) are rendering they should |
|
* call into this method. If a highlight is in the given region it will |
|
* be drawn immediately. |
|
* |
|
* @param g Graphics used to draw |
|
* @param p0 starting offset of view |
|
* @param p1 ending offset of view |
|
* @param viewBounds Bounds of View |
|
* @param editor JTextComponent |
|
* @param view View instance being rendered |
|
*/ |
|
public void paintLayeredHighlights(Graphics g, int p0, int p1, |
|
Shape viewBounds, |
|
JTextComponent editor, View view) { |
|
for (int counter = highlights.size() - 1; counter >= 0; counter--) { |
|
Object tag = highlights.elementAt(counter); |
|
if (tag instanceof LayeredHighlightInfo) { |
|
LayeredHighlightInfo lhi = (LayeredHighlightInfo)tag; |
|
int start = lhi.getStartOffset(); |
|
int end = lhi.getEndOffset(); |
|
if ((p0 < start && p1 > start) || |
|
(p0 >= start && p0 < end)) { |
|
lhi.paintLayeredHighlights(g, p0, p1, viewBounds, |
|
editor, view); |
|
} |
|
} |
|
} |
|
} |
|
|
|
/** |
|
* Queues damageRange() call into event dispatch thread |
|
* to be sure that views are in consistent state. |
|
*/ |
|
private void safeDamageRange(final Position p0, final Position p1) { |
|
safeDamager.damageRange(p0, p1); |
|
} |
|
|
|
/** |
|
* Queues damageRange() call into event dispatch thread |
|
* to be sure that views are in consistent state. |
|
*/ |
|
private void safeDamageRange(int a0, int a1) throws BadLocationException { |
|
Document doc = component.getDocument(); |
|
safeDamageRange(doc.createPosition(a0), doc.createPosition(a1)); |
|
} |
|
|
|
/** |
|
* If true, highlights are drawn as the Views draw the text. That is |
|
* the Views will call into <code>paintLayeredHighlight</code> which |
|
* will result in a rectangle being drawn before the text is drawn |
|
* (if the offsets are in a highlighted region that is). For this to |
|
* work the painter supplied must be an instance of |
|
* LayeredHighlightPainter. |
|
*/ |
|
public void setDrawsLayeredHighlights(boolean newValue) { |
|
drawsLayeredHighlights = newValue; |
|
} |
|
|
|
public boolean getDrawsLayeredHighlights() { |
|
return drawsLayeredHighlights; |
|
} |
|
|
|
// ---- member variables -------------------------------------------- |
|
|
|
private final static Highlighter.Highlight[] noHighlights = |
|
new Highlighter.Highlight[0]; |
|
private final Vector<HighlightInfo> highlights = new Vector<HighlightInfo>(); |
|
private JTextComponent component; |
|
private boolean drawsLayeredHighlights; |
|
private final SafeDamager safeDamager = new SafeDamager(); |
|
|
|
|
|
/** |
|
* Default implementation of LayeredHighlighter.LayerPainter that can |
|
* be used for painting highlights. |
|
* <p> |
|
* As of 1.4 this field is final. |
|
*/ |
|
public static LayeredHighlighter.LayerPainter SnippetPainter = new SnippetHighlightPainter(null); |
|
|
|
|
|
/** |
|
* Simple highlight painter that fills a highlighted area with |
|
* a solid color. |
|
*/ |
|
public static class SnippetHighlightPainter extends LayeredHighlighter.LayerPainter { |
|
|
|
/** |
|
* Constructs a new highlight painter. If <code>c</code> is null, |
|
* the JTextComponent will be queried for its selection color. |
|
* |
|
* @param c the color for the highlight |
|
*/ |
|
public SnippetHighlightPainter(Color c) { |
|
color = c; |
|
} |
|
|
|
/** |
|
* Returns the color of the highlight. |
|
* |
|
* @return the color |
|
*/ |
|
public Color getColor() { |
|
return color; |
|
} |
|
|
|
// --- HighlightPainter methods --------------------------------------- |
|
|
|
/** |
|
* Paints a highlight. |
|
* |
|
* @param g the graphics context |
|
* @param offs0 the starting model offset >= 0 |
|
* @param offs1 the ending model offset >= offs1 |
|
* @param bounds the bounding box for the highlight |
|
* @param c the editor |
|
*/ |
|
public void paint(Graphics g, int offs0, int offs1, Shape bounds, JTextComponent c) { |
|
Rectangle alloc = bounds.getBounds(); |
|
try { |
|
// --- determine locations --- |
|
TextUI mapper = c.getUI(); |
|
Rectangle p0 = mapper.modelToView(c, offs0); |
|
Rectangle p1 = mapper.modelToView(c, offs1); |
|
|
|
// --- render --- |
|
Color color = getColor(); |
|
|
|
if (color == null) { |
|
g.setColor(c.getSelectionColor()); |
|
} |
|
else { |
|
g.setColor(color); |
|
} |
|
if (p0.y == p1.y) { |
|
// same line, render a rectangle |
|
Rectangle r = p0.union(p1); |
|
g.fillRect(r.x, r.y, r.width, r.height); |
|
} else { |
|
// different lines |
|
int p0ToMarginWidth = alloc.x + alloc.width - p0.x; |
|
g.fillRect(p0.x, p0.y, p0ToMarginWidth, p0.height); |
|
if ((p0.y + p0.height) != p1.y) { |
|
g.fillRect(alloc.x, p0.y + p0.height, alloc.width, |
|
p1.y - (p0.y + p0.height)); |
|
} |
|
g.fillRect(alloc.x, p1.y, (p1.x - alloc.x), p1.height); |
|
} |
|
} catch (BadLocationException e) { |
|
// can't render |
|
} |
|
} |
|
|
|
// --- LayerPainter methods ---------------------------- |
|
/** |
|
* Paints a portion of a highlight. |
|
* |
|
* @param g the graphics context |
|
* @param offs0 the starting model offset >= 0 |
|
* @param offs1 the ending model offset >= offs1 |
|
* @param bounds the bounding box of the view, which is not |
|
* necessarily the region to paint. |
|
* @param c the editor |
|
* @param view View painting for |
|
* @return region drawing occured in |
|
*/ |
|
public Shape paintLayer(Graphics g, int offs0, int offs1, |
|
Shape bounds, JTextComponent c, View view) { |
|
Color base = getColor(); |
|
// use transparency so selection isn't clobbered |
|
Color color = base != null? |
|
new Color(base.getRed(), base.getGreen(), base.getBlue(), |
|
255) : null; |
|
|
|
if (color == null) { |
|
g.setColor(c.getSelectionColor()); |
|
} |
|
else { |
|
g.setColor(color); |
|
} |
|
if (offs0 == view.getStartOffset() && |
|
offs1 == view.getEndOffset()) { |
|
// Contained in view, can just use bounds. |
|
Rectangle alloc; |
|
if (bounds instanceof Rectangle) { |
|
alloc = (Rectangle)bounds; |
|
} |
|
else { |
|
alloc = bounds.getBounds(); |
|
} |
|
// For CodeViewer Snippet painting, this is the paint block that is called. |
|
// We want to paint the highlight on the full width of the text pane, |
|
// so we only need to do this when the region is the beginning of a new |
|
// line. |
|
//g.fillRect(alloc.x, alloc.y, alloc.width, alloc.height); |
|
g.fillRect(alloc.x, alloc.y, c.getWidth() - alloc.x, alloc.height); |
|
|
|
} |
|
else { |
|
// Should only render part of View. |
|
try { |
|
// --- determine locations --- |
|
Shape shape = view.modelToView(offs0, Position.Bias.Forward, |
|
offs1,Position.Bias.Backward, |
|
bounds); |
|
Rectangle r = (shape instanceof Rectangle) ? |
|
(Rectangle)shape : shape.getBounds(); |
|
|
|
g.fillRect(0, r.y, c.getWidth(), r.height); |
|
return r; |
|
} catch (BadLocationException e) { |
|
// can't render |
|
} |
|
} |
|
// Only if exception |
|
return null; |
|
} |
|
|
|
private Color color; |
|
|
|
} |
|
|
|
|
|
private static class HighlightInfo implements Highlighter.Highlight { |
|
|
|
public int getStartOffset() { |
|
return p0.getOffset(); |
|
} |
|
|
|
public int getEndOffset() { |
|
return p1.getOffset(); |
|
} |
|
|
|
public Highlighter.HighlightPainter getPainter() { |
|
return painter; |
|
} |
|
|
|
Position p0; |
|
Position p1; |
|
Highlighter.HighlightPainter painter; |
|
} |
|
|
|
|
|
/** |
|
* LayeredHighlightPainter is used when a drawsLayeredHighlights is |
|
* true. It maintains a rectangle of the region to paint. |
|
*/ |
|
private static class LayeredHighlightInfo extends HighlightInfo { |
|
|
|
void union(Shape bounds) { |
|
if (bounds == null) |
|
return; |
|
|
|
Rectangle alloc; |
|
if (bounds instanceof Rectangle) { |
|
alloc = (Rectangle)bounds; |
|
} |
|
else { |
|
alloc = bounds.getBounds(); |
|
} |
|
if (width == 0 || height == 0) { |
|
x = alloc.x; |
|
y = alloc.y; |
|
width = alloc.width; |
|
height = alloc.height; |
|
} |
|
else { |
|
width = Math.max(x + width, alloc.x + alloc.width); |
|
height = Math.max(y + height, alloc.y + alloc.height); |
|
x = Math.min(x, alloc.x); |
|
width -= x; |
|
y = Math.min(y, alloc.y); |
|
height -= y; |
|
} |
|
} |
|
|
|
/** |
|
* Restricts the region based on the receivers offsets and messages |
|
* the painter to paint the region. |
|
*/ |
|
void paintLayeredHighlights(Graphics g, int p0, int p1, |
|
Shape viewBounds, JTextComponent editor, |
|
View view) { |
|
int start = getStartOffset(); |
|
int end = getEndOffset(); |
|
// Restrict the region to what we represent |
|
p0 = Math.max(start, p0); |
|
p1 = Math.min(end, p1); |
|
// Paint the appropriate region using the painter and union |
|
// the effected region with our bounds. |
|
union(((LayeredHighlighter.LayerPainter)painter).paintLayer |
|
(g, p0, p1, viewBounds, editor, view)); |
|
} |
|
|
|
int x; |
|
int y; |
|
int width; |
|
int height; |
|
} |
|
|
|
/** |
|
* This class invokes <code>mapper.damageRange</code> in |
|
* EventDispatchThread. The only one instance per Highlighter |
|
* is cretaed. When a number of ranges should be damaged |
|
* it collects them into queue and damages |
|
* them in consecutive order in <code>run</code> |
|
* call. |
|
*/ |
|
class SafeDamager implements Runnable { |
|
private final List<Position> p0 = new ArrayList<Position>(); |
|
private final List<Position> p1 = new ArrayList<Position>(); |
|
private Document lastDoc = null; |
|
|
|
/** |
|
* Executes range(s) damage and cleans range queue. |
|
*/ |
|
public synchronized void run() { |
|
if (component != null) { |
|
TextUI mapper = component.getUI(); |
|
if (mapper != null && lastDoc == component.getDocument()) { |
|
// the Document should be the same to properly |
|
// display highlights |
|
int len = p0.size(); |
|
for (int i = 0; i < len; i++){ |
|
mapper.damageRange(component, |
|
p0.get(i).getOffset(), |
|
p1.get(i).getOffset()); |
|
} |
|
} |
|
} |
|
p0.clear(); |
|
p1.clear(); |
|
|
|
// release reference |
|
lastDoc = null; |
|
} |
|
|
|
/** |
|
* Adds the range to be damaged into the range queue. If the |
|
* range queue is empty (the first call or run() was already |
|
* invoked) then adds this class instance into EventDispatch |
|
* queue. |
|
* |
|
* The method also tracks if the current document changed or |
|
* component is null. In this case it removes all ranges added |
|
* before from range queue. |
|
*/ |
|
private synchronized void damageRange(Position pos0, Position pos1) { |
|
if (component == null) { |
|
p0.clear(); |
|
lastDoc = null; |
|
return; |
|
} |
|
|
|
boolean addToQueue = p0.isEmpty(); |
|
Document curDoc = component.getDocument(); |
|
if (curDoc != lastDoc) { |
|
if (!p0.isEmpty()) { |
|
p0.clear(); |
|
p1.clear(); |
|
} |
|
lastDoc = curDoc; |
|
} |
|
p0.add(pos0); |
|
p1.add(pos1); |
|
|
|
if (addToQueue) { |
|
SwingUtilities.invokeLater(this); |
|
} |
|
} |
|
} |
|
}
|
|
|