/*
* 04/23/2009
*
* RSyntaxTextAreaHighlighter.java - Highlighter for RTextAreas.
* Copyright (C) 2009 Robert Futrell
* robert_futrell at users.sourceforge.net
* http://fifesoft.com/rsyntaxtextarea
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
*/
package org.fife.ui.rsyntaxtextarea;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Insets;
import java.awt.Rectangle;
import java.awt.Shape;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import javax.swing.plaf.TextUI;
import javax.swing.plaf.basic.BasicTextUI.BasicHighlighter;
import javax.swing.text.BadLocationException;
import javax.swing.text.Document;
import javax.swing.text.Element;
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;
import org.fife.ui.rsyntaxtextarea.parser.Parser;
import org.fife.ui.rsyntaxtextarea.parser.ParserNotice;
import org.fife.ui.rtextarea.RTextArea;
/**
* The highlighter implementation used by {@link RSyntaxTextArea}s. It knows to always paint "marked occurrences"
* highlights below selection highlights, and squiggle underline highlights above all other highlights.
* <p>
*
* Most of this code is copied from javax.swing.text.DefaultHighlighter; unfortunately, we cannot re-use much of it
* since it is package private.
*
* @author Robert Futrell
* @version 1.0
*/
public class RSyntaxTextAreaHighlighter extends BasicHighlighter {
/**
* The text component we are the highlighter for.
*/
private RTextArea textArea;
/**
* Marked occurrences in the document (to be painted separately from other highlights).
*/
private List markedOccurrences;
/**
* Highlights from document parsers. These should be painted "on top of" all other highlights to ensure they are
* always above the selection.
*/
private List parserHighlights;
/**
* The default color used for parser notices when none is specified.
*/
private static final Color DEFAULT_PARSER_NOTICE_COLOR = Color.RED;
/**
* Constructor.
*/
public RSyntaxTextAreaHighlighter() {
markedOccurrences = new ArrayList();
parserHighlights = new ArrayList(0); // Often unused
}
/**
* Adds a special "marked occurrence" highlight.
*
* @param start
* @param end
* @param p
* @return
* @throws BadLocationException
* @see {@link #removeMarkOccurrencesHighlight(Object)}
*/
Object addMarkedOccurrenceHighlight(int start, int end,
MarkOccurrencesHighlightPainter p) throws BadLocationException {
Document doc = textArea.getDocument();
TextUI mapper = textArea.getUI();
// Always layered highlights for marked occurrences.
HighlightInfo i = new LayeredHighlightInfo();
i.painter = p;
i.p0 = doc.createPosition(start);
// HACK: Use "end-1" to prevent chars the user types at the "end" of
// the highlight to be absorbed into the highlight (default Highlight
// behavior).
i.p1 = doc.createPosition(end - 1);
markedOccurrences.add(i);
mapper.damageRange(textArea, start, end);
return i;
}
/**
* Adds a special "marked occurrence" highlight.
*
* @param notice
* The notice from a {@link Parser}.
* @return A tag with which to reference the highlight.
* @throws BadLocationException
* @see {@link #clearParserHighlights()}
*/
Object addParserHighlight(ParserNotice notice, HighlightPainter p)
throws BadLocationException {
Document doc = textArea.getDocument();
TextUI mapper = textArea.getUI();
int start = notice.getOffset();
int end = 0;
if (start == -1) { // Could just define an invalid line number
int line = notice.getLine();
Element root = doc.getDefaultRootElement();
if (line >= 0 && line < root.getElementCount()) {
Element elem = root.getElement(line);
start = elem.getStartOffset();
end = elem.getEndOffset();
}
}
else {
end = start + notice.getLength();
}
// Always layered highlights for parser highlights.
HighlightInfo i = new LayeredHighlightInfo();
i.painter = p;
i.p0 = doc.createPosition(start);
i.p1 = doc.createPosition(end);
i.notice = notice;// i.color = notice.getColor();
parserHighlights.add(i);
mapper.damageRange(textArea, start, end);
return i;
}
/**
* Removes all parser highlights.
*
* @see #addParserHighlight(int, int, Color, javax.swing.text.Highlighter.HighlightPainter)
*/
void clearParserHighlights() {
for (int i = 0; i < parserHighlights.size(); i++) {
Object tag = parserHighlights.get(i);
if (tag instanceof LayeredHighlightInfo) {
LayeredHighlightInfo lhi = (LayeredHighlightInfo) tag;
if (lhi.width > 0 && lhi.height > 0) {
textArea.repaint(lhi.x, lhi.y, lhi.width, lhi.height);
}
}
else {
HighlightInfo info = (HighlightInfo) tag;
TextUI ui = textArea.getUI();
ui.damageRange(textArea, info.getStartOffset(), info.getEndOffset());
// safeDamageRange(info.p0, info.p1);
}
}
parserHighlights.clear();
}
/**
* Removes all of the highlights for a specific parser.
*
* @param parser
* The parser.
*/
public void clearParserHighlights(Parser parser) {
for (Iterator i = parserHighlights.iterator(); i.hasNext();) {
HighlightInfo info = (HighlightInfo) i.next();
if (info.notice.getParser() == parser) {
if (info instanceof LayeredHighlightInfo) {
LayeredHighlightInfo lhi = (LayeredHighlightInfo) info;
if (lhi.width > 0 && lhi.height > 0) {
textArea.repaint(lhi.x, lhi.y, lhi.width, lhi.height);
}
}
else {
TextUI ui = textArea.getUI();
ui.damageRange(textArea, info.getStartOffset(), info.getEndOffset());
// safeDamageRange(info.p0, info.p1);
}
i.remove();
}
}
}
/**
* {@inheritDoc}
*/
public void deinstall(JTextComponent c) {
this.textArea = null;
markedOccurrences.clear();
parserHighlights.clear();
}
/**
* Returns a list of "marked occurrences" in the text area. If there are no marked occurrences, this will be an
* empty list.
*
* @return The list of marked occurrences.
*/
public List getMarkedOccurrences() {
List list = new ArrayList(markedOccurrences.size());
for (Iterator i = markedOccurrences.iterator(); i.hasNext();) {
HighlightInfo info = (HighlightInfo) i.next();
int start = info.getStartOffset();
int end = info.getEndOffset() + 1; // HACK
DocumentRange range = new DocumentRangeImpl(start, end);
list.add(range);
}
return list;
}
/**
* {@inheritDoc}
*/
public void install(JTextComponent c) {
super.install(c);
this.textArea = (RTextArea) c;
}
/**
* Renders the highlights.
*
* @param g
* the graphics context
*/
public void paint(Graphics g) {
paintList(g, markedOccurrences);
super.paint(g);
paintList(g, parserHighlights);
}
private void paintList(Graphics g, List highlights) {
int len = highlights.size();
for (int i = 0; i < len; i++) {
HighlightInfo info = (HighlightInfo) highlights.get(i);
if (!(info instanceof LayeredHighlightInfo)) {
// Avoid allocating unless we need it.
Rectangle a = textArea.getBounds();
Insets insets = textArea.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 = (HighlightInfo) markedOccurrences.get(i);
if (!(info instanceof LayeredHighlightInfo)) {
Color c = info.getColor();
Highlighter.HighlightPainter p = info.getPainter();
if (c != null && p instanceof ChangeableColorHighlightPainter) {
((ChangeableColorHighlightPainter) p).setColor(c);
}
p.paint(g, info.getStartOffset(), info.getEndOffset(),
a, textArea);
}
}
}
}
}
/**
* 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) {
paintListLayered(g, p0, p1, viewBounds, editor, view, markedOccurrences);
super.paintLayeredHighlights(g, p0, p1, viewBounds, editor, view);
paintListLayered(g, p0, p1, viewBounds, editor, view, parserHighlights);
}
private void paintListLayered(Graphics g, int p0, int p1, Shape viewBounds,
JTextComponent editor, View view, List highlights) {
for (int i = highlights.size() - 1; i >= 0; i--) {
Object tag = highlights.get(i);
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);
}
}
}
}
private void removeListHighlight(List list, Object tag) {
if (tag instanceof LayeredHighlightInfo) {
LayeredHighlightInfo lhi = (LayeredHighlightInfo) tag;
if (lhi.width > 0 && lhi.height > 0) {
textArea.repaint(lhi.x, lhi.y, lhi.width, lhi.height);
}
}
else {
HighlightInfo info = (HighlightInfo) tag;
TextUI ui = textArea.getUI();
ui.damageRange(textArea, info.getStartOffset(), info.getEndOffset());
// safeDamageRange(info.p0, info.p1);
}
list.remove(tag);
}
/**
* Removes a "marked occurrences" highlight from the view.
*
* @param tag
* The reference to the highlight
* @see #addMarkedOccurrenceHighlight(int, int, javax.swing.text.Highlighter.HighlightPainter)
*/
void removeMarkOccurrencesHighlight(Object tag) {
removeListHighlight(markedOccurrences, tag);
}
/**
* Removes a parser highlight from this view.
*
* @param tag
* The reference to the highlight.
* @see #addParserHighlight(int, int, Color, javax.swing.text.Highlighter.HighlightPainter)
*/
void removeParserHighlight(Object tag) {
removeListHighlight(parserHighlights, tag);
}
private static class DocumentRangeImpl implements DocumentRange {
private int startOffs;
private int endOffs;
public DocumentRangeImpl(int startOffs, int endOffs) {
this.startOffs = startOffs;
this.endOffs = endOffs;
}
public int getEndOffset() {
return endOffs;
}
public int getStartOffset() {
return startOffs;
}
}
private static class HighlightInfo implements Highlighter.Highlight {
private Position p0;
private Position p1;
protected Highlighter.HighlightPainter painter;
private ParserNotice notice;// Color color; // Used only by Parser highlights.
public Color getColor() {
// return color;
Color color = null;
if (notice != null) {
color = notice.getColor();
if (color == null) {
color = DEFAULT_PARSER_NOTICE_COLOR;
}
}
return color;
}
public int getStartOffset() {
return p0.getOffset();
}
public int getEndOffset() {
return p1.getOffset();
}
public Highlighter.HighlightPainter getPainter() {
return painter;
}
}
private static class LayeredHighlightInfo extends HighlightInfo {
private int x;
private int y;
private int width;
private int height;
void union(Shape bounds) {
if (bounds == null) {
return;
}
Rectangle alloc = (bounds instanceof Rectangle) ?
(Rectangle) bounds : 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);
if (getColor() != null &&
(painter instanceof ChangeableColorHighlightPainter)) {
((ChangeableColorHighlightPainter) painter).setColor(getColor());
}
// 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));
}
}
}