org.fife.ui.rsyntaxtextarea.ErrorStrip Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of rsyntaxtextarea Show documentation
Show all versions of rsyntaxtextarea Show documentation
RSyntaxTextArea is the syntax highlighting text editor for Swing applications. Features include syntax highlighting for 40+ languages, code folding, code completion, regex find and replace, macros, code templates, undo/redo, line numbering and bracket matching.
/*
* 08/10/2009
*
* ErrorStrip.java - A component that can visually show Parser messages (syntax
* errors, etc.) in an RSyntaxTextArea.
*
* This library is distributed under a modified BSD license. See the included
* LICENSE file for details.
*/
package org.fife.ui.rsyntaxtextarea;
import java.awt.Color;
import java.awt.Component;
import java.awt.Cursor;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.geom.AffineTransform;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.ResourceBundle;
import javax.swing.JComponent;
import javax.swing.JPanel;
import javax.swing.ToolTipManager;
import javax.swing.UIManager;
import javax.swing.event.CaretEvent;
import javax.swing.event.CaretListener;
import javax.swing.plaf.ColorUIResource;
import javax.swing.text.BadLocationException;
import org.fife.ui.rsyntaxtextarea.parser.Parser;
import org.fife.ui.rsyntaxtextarea.parser.ParserNotice;
import org.fife.ui.rsyntaxtextarea.parser.TaskTagParser.TaskNotice;
import org.fife.ui.rtextarea.RTextArea;
/**
* A component to sit alongside an {@link RSyntaxTextArea} that displays
* colored markers for locations of interest (parser errors, marked
* occurrences, etc.).
*
* ErrorStrip
s display ParserNotice
s from
* {@link Parser}s. Currently, the only way to get lines flagged in this
* component is to register a Parser
on an RSyntaxTextArea and
* return ParserNotice
s for each line to display an icon for.
* The severity of each notice must be at least the threshold set by
* {@link #setLevelThreshold(org.fife.ui.rsyntaxtextarea.parser.ParserNotice.Level)}
* to be displayed in this error strip. The default threshold is
* {@link org.fife.ui.rsyntaxtextarea.parser.ParserNotice.Level#WARNING}.
*
* An ErrorStrip
can be added to a UI like so:
*
* textArea = createTextArea();
* textArea.addParser(new MyParser(textArea)); // Identifies lines to display
* scrollPane = new RTextScrollPane(textArea, true);
* ErrorStrip es = new ErrorStrip(textArea);
* JPanel temp = new JPanel(new BorderLayout());
* temp.add(scrollPane);
* temp.add(es, BorderLayout.LINE_END);
*
*
* @author Robert Futrell
* @version 0.5
*/
// Possible improvements:
// 1. Handle marked occurrence changes & "mark all" changes separately from
// parser changes. For each property change, call a method that removes
// the notices being reloaded from the Markers (removing any Markers that
// are now "empty").
//
public class ErrorStrip extends JPanel {
/**
* The text area.
*/
private RSyntaxTextArea textArea;
/**
* Listens for events in this component.
*/
private transient Listener listener;
/**
* Whether "marked occurrences" in the text area should be shown in this
* error strip.
*/
private boolean showMarkedOccurrences;
/**
* Whether markers for "mark all" highlights should be shown in this
* error strip.
*/
private boolean showMarkAll;
/**
* Mapping of colors to brighter colors. This is kept to prevent
* unnecessary creation of the same Colors over and over.
*/
private Map brighterColors;
/**
* Only notices of this severity (or worse) will be displayed in this
* error strip.
*/
private ParserNotice.Level levelThreshold;
/**
* Whether the caret marker's location should be rendered.
*/
private boolean followCaret;
/**
* The color to use for the caret marker.
*/
private Color caretMarkerColor;
/**
* Whether to draw the caret marker before or after the parser markers.
*/
private boolean paintCaretMarkerOnTop;
/**
* Where we paint the caret marker.
*/
private int caretLineY;
/**
* The last location of the caret marker.
*/
private int lastLineY;
/**
* Generates the tool tips for markers in this error strip.
*/
private transient ErrorStripMarkerToolTipProvider markerToolTipProvider;
/**
* The preferred width of this component.
*/
private static final int PREFERRED_WIDTH = 14;
private static final ResourceBundle MSG = ResourceBundle.getBundle(
"org.fife.ui.rsyntaxtextarea.ErrorStrip");
/**
* Constructor.
*
* @param textArea The text area we are examining.
*/
public ErrorStrip(RSyntaxTextArea textArea) {
this.textArea = textArea;
listener = new Listener();
ToolTipManager.sharedInstance().registerComponent(this);
setLayout(null); // Manually layout Markers as they can overlap
addMouseListener(listener);
setShowMarkedOccurrences(true);
setShowMarkAll(true);
setLevelThreshold(ParserNotice.Level.WARNING);
setFollowCaret(true);
setCaretMarkerColor(getDefaultCaretMarkerColor());
setMarkerToolTipProvider(null); // Install default
setCaretMarkerOnTop(false);
}
/**
* Overridden so we only start listening for parser notices when this
* component (and presumably the text area) are visible.
*/
@Override
public void addNotify() {
super.addNotify();
textArea.addCaretListener(listener);
textArea.addPropertyChangeListener(
RSyntaxTextArea.PARSER_NOTICES_PROPERTY, listener);
textArea.addPropertyChangeListener(
RSyntaxTextArea.MARK_OCCURRENCES_PROPERTY, listener);
textArea.addPropertyChangeListener(
RSyntaxTextArea.MARKED_OCCURRENCES_CHANGED_PROPERTY, listener);
textArea.addPropertyChangeListener(
RSyntaxTextArea.MARK_ALL_OCCURRENCES_CHANGED_PROPERTY, listener);
refreshMarkers();
}
/**
* Manually manages layout since this component uses no layout manager.
*/
@Override
public void doLayout() {
for (int i=0; i(5); // Usually small
}
Color brighter = brighterColors.get(c);
if (brighter==null) {
// Don't use c.brighter() as it doesn't work well for blue, and
// also doesn't return something brighter "enough."
int r = possiblyBrighter(c.getRed());
int g = possiblyBrighter(c.getGreen());
int b = possiblyBrighter(c.getBlue());
brighter = new Color(r, g, b);
brighterColors.put(c, brighter);
}
return brighter;
}
/**
* returns the color to use when painting the caret marker.
*
* @return The caret marker color.
* @see #setCaretMarkerColor(Color)
*/
public Color getCaretMarkerColor() {
return caretMarkerColor;
}
/**
* Sets whether to paint the caret marker, if enabled, over the markers
* for parser notices. The default is to paint the caret marker under
* notices.
*
* @param paintOver If true, paint the caret marker over other markers.
* @see #isCaretMarkerOnTop()
*/
public void setCaretMarkerOnTop(boolean paintOver) {
if (paintOver != paintCaretMarkerOnTop) {
paintCaretMarkerOnTop = paintOver;
listener.caretUpdate(null); // Force repaint
}
}
/**
* Returns whether the caret marker is painted overtop of any other
* markers.
*
* @return True is the caret marker is drawn overtop of other markers,
* false otherwise.
* @see #setCaretMarkerOnTop(boolean)
*/
public boolean isCaretMarkerOnTop() {
return paintCaretMarkerOnTop;
}
/**
* Returns the default color for the caret marker. This is a UI
* resource so that it is updated if the LookAndFeel is updated,
* but not if the user overrides it.
*
* @return The default color.
*/
private ColorUIResource getDefaultCaretMarkerColor() {
if (RSyntaxUtilities.isLightForeground(getForeground())) {
return new ColorUIResource(textArea.getCaretColor());
}
return new ColorUIResource(Color.BLACK);
}
/**
* Returns whether the caret's position should be drawn.
*
* @return Whether the caret's position should be drawn.
* @see #setFollowCaret(boolean)
*/
public boolean getFollowCaret() {
return followCaret;
}
@Override
public Dimension getPreferredSize() {
int height = textArea.getPreferredScrollableViewportSize().height;
return new Dimension(PREFERRED_WIDTH, height);
}
/**
* Returns the minimum severity a parser notice must be for it to be
* displayed in this error strip. This will be one of the constants
* defined in the ParserNotice
class.
*
* @return The minimum severity.
* @see #setLevelThreshold(org.fife.ui.rsyntaxtextarea.parser.ParserNotice.Level)
*/
public ParserNotice.Level getLevelThreshold() {
return levelThreshold;
}
/**
* Returns whether "mark all" highlights are shown in this error strip.
*
* @return Whether markers are shown for "mark all" highlights.
* @see #setShowMarkAll(boolean)
*/
public boolean getShowMarkAll() {
return showMarkAll;
}
/**
* Returns whether marked occurrences are shown in this error strip.
*
* @return Whether marked occurrences are shown.
* @see #setShowMarkedOccurrences(boolean)
*/
public boolean getShowMarkedOccurrences() {
return showMarkedOccurrences;
}
@Override
public String getToolTipText(MouseEvent e) {
String text = null;
int line = yToLine(e.getY());
if (line>-1) {
text = MSG.getString("Line");
text = MessageFormat.format(text, line + 1);
}
return text;
}
/**
* Returns the y-offset in this component corresponding to a line in the
* text component.
*
* @param line The line.
* @return The y-offset.
* @see #yToLine(int)
*/
private int lineToY(int line, Rectangle r) {
if (r == null) {
r = new Rectangle();
}
textArea.computeVisibleRect(r);
int h = r.height;
float lineCount = textArea.getLineCount();
int lineHeight = textArea.getLineHeight();
int linesPerVisibleRect = h / lineHeight;
return Math.round((h-1) * line / Math.max(lineCount, linesPerVisibleRect));
}
/**
* Overridden to (possibly) draw the caret's position.
*
* @param g The graphics context.
*/
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
if (!paintCaretMarkerOnTop) {
paintCaretMarker((Graphics2D) g, caretLineY);
}
}
/**
* Overridden to (possibly) draw the caret's position.
*
* @param g The graphics context.
*/
@Override
protected void paintChildren(Graphics g) {
super.paintChildren(g);
if (paintCaretMarkerOnTop) {
paintCaretMarker((Graphics2D) g, caretLineY);
}
}
/**
* Sets up for painting the caret, then delegates painting to
* {@link #paintCaretMarker(java.awt.Graphics2D, int, int)}.
*/
private void paintCaretMarker(Graphics2D g, int caretLineY) {
if (caretLineY > -1) {
AffineTransform oldTransform = g.getTransform();
try {
g.translate(0, caretLineY - MARKER_HEIGHT/2);
g.setColor(getCaretMarkerColor());
paintCaretMarker(g, getWidth(), MARKER_HEIGHT);
} finally {
g.setTransform(oldTransform);
}
}
}
/**
* Paints the caret marker.
* Can be overridden to customize how the marker is drawn.
* All drawing must be kept within the rectangle
* (0, 0, width, height).
*
* @param g the graphics context to paint with
* @param width the width of the painting area
* @param height the height of the painting area
*/
protected void paintCaretMarker(Graphics2D g, int width, int height) {
final int y0 = height / 2 + (height & 1);
g.fillRect(0, y0, width, 2);
}
/**
* Paints the marker for a parser notice.
* Can be overridden to customize how the marker is drawn.
* All drawing must be kept within the rectangle
* (0, 0, width, height).
*
* @param g the graphics context
* @param notice the notice to paint
* @param width the width of the painting area
* @param height the height of the painting area
*/
protected void paintParserNoticeMarker(Graphics2D g, ParserNotice notice, int width, int height) {
Color borderColor = notice.getColor();
if (borderColor==null) {
borderColor = Color.DARK_GRAY;
}
Color fillColor = getBrighterColor(borderColor);
g.setColor(fillColor);
g.fillRect(0,0, width,height);
g.setColor(borderColor);
g.drawRect(0,0, width-1,height-1);
}
/**
* Returns a possibly brighter component for a color.
*
* @param i An RGB component for a color (0-255).
* @return A possibly brighter value for the component.
*/
private static int possiblyBrighter(int i) {
if (i<255) {
i += (int)((255-i)*0.8f);
}
return i;
}
/**
* Refreshes the markers displayed in this error strip.
*/
private void refreshMarkers() {
removeAll(); // listener is removed in Marker.removeNotify()
Map markerMap = new HashMap<>();
List notices = textArea.getParserNotices();
for (ParserNotice notice : notices) {
if (notice.getLevel().isEqualToOrWorseThan(levelThreshold) ||
(notice instanceof TaskNotice)) {
Integer key = notice.getLine();
Marker m = markerMap.get(key);
if (m==null) {
m = new Marker(notice);
m.addMouseListener(listener);
markerMap.put(key, m);
add(m);
}
else {
m.addNotice(notice);
}
}
}
if (getShowMarkedOccurrences() && textArea.getMarkOccurrences()) {
List occurrences = textArea.getMarkedOccurrences();
addMarkersForRanges(occurrences, markerMap, textArea.getMarkOccurrencesColor());
}
if (getShowMarkAll() /*&& textArea.getMarkAll()*/) {
Color markAllColor = textArea.getMarkAllHighlightColor();
List ranges = textArea.getMarkAllHighlightRanges();
addMarkersForRanges(ranges, markerMap, markAllColor);
}
revalidate();
repaint();
}
/**
* Adds markers for a list of ranges in the document.
*
* @param ranges The list of ranges in the document.
* @param markerMap A mapping from line number to Marker
.
* @param color The color to use for the markers.
*/
private void addMarkersForRanges(List ranges,
Map markerMap, Color color) {
for (DocumentRange range : ranges) {
int line;
try {
line = textArea.getLineOfOffset(range.getStartOffset());
} catch (BadLocationException ble) { // Never happens
continue;
}
ParserNotice notice = new MarkedOccurrenceNotice(range, color);
Integer key = line;
Marker m = markerMap.get(key);
if (m==null) {
m = new Marker(notice);
m.addMouseListener(listener);
markerMap.put(key, m);
add(m);
}
else {
if (!m.containsMarkedOccurrence()) {
m.addNotice(notice);
}
}
}
}
@Override
public void removeNotify() {
super.removeNotify();
textArea.removeCaretListener(listener);
textArea.removePropertyChangeListener(
RSyntaxTextArea.PARSER_NOTICES_PROPERTY, listener);
textArea.removePropertyChangeListener(
RSyntaxTextArea.MARK_OCCURRENCES_PROPERTY, listener);
textArea.removePropertyChangeListener(
RSyntaxTextArea.MARKED_OCCURRENCES_CHANGED_PROPERTY, listener);
textArea.removePropertyChangeListener(
RSyntaxTextArea.MARK_ALL_OCCURRENCES_CHANGED_PROPERTY, listener);
}
/**
* Sets the color to use when painting the caret marker.
*
* @param color The new caret marker color.
* @see #getCaretMarkerColor()
*/
public void setCaretMarkerColor(Color color) {
if (color!=null) {
caretMarkerColor = color;
listener.caretUpdate(null); // Force repaint
}
}
/**
* Toggles whether the caret's current location should be drawn.
*
* @param follow Whether the caret's current location should be followed.
* @see #getFollowCaret()
*/
public void setFollowCaret(boolean follow) {
if (followCaret!=follow) {
if (followCaret) {
repaint(0,caretLineY, getWidth(),2); // Erase
}
caretLineY = -1;
lastLineY = -1;
followCaret = follow;
listener.caretUpdate(null); // Possibly repaint
}
}
/**
* Sets the minimum severity a parser notice must be for it to be displayed
* in this error strip. This should be one of the constants defined in
* the ParserNotice
class. The default value is
* {@link org.fife.ui.rsyntaxtextarea.parser.ParserNotice.Level#WARNING}.
*
* @param level The new severity threshold.
* @see #getLevelThreshold()
* @see ParserNotice
*/
public void setLevelThreshold(ParserNotice.Level level) {
levelThreshold = level;
if (isDisplayable()) {
refreshMarkers();
}
}
/**
* Sets the provider of tool tips for markers in this error strip.
* Applications can use this method to control the content and format of
* the tool tip descriptions of line markers.
*
* @param provider The provider. If this is null
, a default
* implementation will be used.
*/
public void setMarkerToolTipProvider(ErrorStripMarkerToolTipProvider provider) {
markerToolTipProvider = provider != null ? provider :
new DefaultErrorStripMarkerToolTipProvider();
}
/**
* Sets whether "mark all" highlights are shown in this error strip.
*
* @param show Whether to show markers for "mark all" highlights.
* @see #getShowMarkAll()
*/
public void setShowMarkAll(boolean show) {
if (show!=showMarkAll) {
showMarkAll = show;
if (isDisplayable()) { // Skip this when we're first created
refreshMarkers();
}
}
}
/**
* Sets whether marked occurrences are shown in this error strip.
*
* @param show Whether to show marked occurrences.
* @see #getShowMarkedOccurrences()
*/
public void setShowMarkedOccurrences(boolean show) {
if (show!=showMarkedOccurrences) {
showMarkedOccurrences = show;
if (isDisplayable()) { // Skip this when we're first created
refreshMarkers();
}
}
}
@Override
public void updateUI() {
super.updateUI();
if (caretMarkerColor instanceof ColorUIResource) {
setCaretMarkerColor(getDefaultCaretMarkerColor());
}
}
/**
* Returns the line in the text area corresponding to a y-offset in this
* component.
*
* @param y The y-offset.
* @return The line.
* @see #lineToY(int, Rectangle)
*/
private int yToLine(int y) {
int line = -1;
int h = textArea.getVisibleRect().height;
int lineHeight = textArea.getLineHeight();
int linesPerVisibleRect = h / lineHeight;
int lineCount = textArea.getLineCount();
if (y notices) {
String text;
if (notices.size()==1) {
text = notices.get(0).getMessage();
}
else { // > 1
StringBuilder sb = new StringBuilder("");
sb.append(MSG.getString("MultipleMarkers"));
sb.append("
");
for (ParserNotice pn : notices) {
sb.append(" - ");
sb.append(pn.getMessage());
sb.append("
");
}
text = sb.toString();
}
return text;
}
}
/**
* Returns tool tip text for the markers in an {@link ErrorStrip} that
* denote one or more parser notices.
*
* @author predi
*/
public interface ErrorStripMarkerToolTipProvider {
/**
* Returns the tool tip text for a marker in an ErrorStrip
* that denotes a given list of parser notices.
*
* @param notices The list of parser notices.
* @return The tool tip text. This may be HTML. Returning
* null
will result in no tool tip being displayed.
*/
String getToolTipText(List notices);
}
/**
* Listens for events in the error strip and its markers.
*/
private class Listener extends MouseAdapter
implements PropertyChangeListener, CaretListener {
private final Rectangle r = new Rectangle();
@Override
public void caretUpdate(CaretEvent e) {
if (getFollowCaret()) {
int line = textArea.getCaretLineNumber();
caretLineY = lineToY(line, r);
if (caretLineY!=lastLineY) {
// Extend caret position to repaint rectangle around it
final int dyRectTop = MARKER_HEIGHT/2 + 1;
final int rectHeight = MARKER_HEIGHT + 3;
// Erase old position
repaint(0,lastLineY - dyRectTop, getWidth(), rectHeight);
repaint(0,caretLineY - dyRectTop, getWidth(), rectHeight);
lastLineY = caretLineY;
}
}
}
@Override
public void mouseClicked(MouseEvent e) {
Component source = (Component)e.getSource();
if (source instanceof Marker) {
((Marker)source).mouseClicked(e);
return;
}
int line = yToLine(e.getY());
if (line>-1) {
try {
int offs = textArea.getLineStartOffset(line);
textArea.setCaretPosition(offs);
RSyntaxUtilities.selectAndPossiblyCenter(textArea, new DocumentRange(offs, offs), false);
} catch (BadLocationException ble) { // Never happens
UIManager.getLookAndFeel().provideErrorFeedback(textArea);
}
}
}
@Override
public void propertyChange(PropertyChangeEvent e) {
String propName = e.getPropertyName();
// If they change whether marked occurrences are visible in editor
if (RSyntaxTextArea.MARK_OCCURRENCES_PROPERTY.equals(propName)) {
if (getShowMarkedOccurrences()) {
refreshMarkers();
}
}
// If parser notices changed.
// TODO: Don't update "mark all/occurrences" markers.
else if (RSyntaxTextArea.PARSER_NOTICES_PROPERTY.equals(propName)) {
refreshMarkers();
}
// If marked occurrences changed.
// TODO: Only update "mark occurrences" markers, not all of them.
else if (RSyntaxTextArea.MARKED_OCCURRENCES_CHANGED_PROPERTY.
equals(propName)) {
if (getShowMarkedOccurrences()) {
refreshMarkers();
}
}
// If "mark all" occurrences changed.
// TODO: Only update "mark all" markers, not all of them.
else if (RTextArea.MARK_ALL_OCCURRENCES_CHANGED_PROPERTY.
equals(propName)) {
if (getShowMarkAll()) {
refreshMarkers();
}
}
}
}
/**
* A notice that wraps a "marked occurrence" instance.
*/
private class MarkedOccurrenceNotice implements ParserNotice {
private DocumentRange range;
private Color color;
MarkedOccurrenceNotice(DocumentRange range, Color color) {
this.range = range;
this.color = color;
}
@Override
public int compareTo(ParserNotice other) {
return 0; // Value doesn't matter
}
@Override
public boolean containsPosition(int pos) {
return pos>=range.getStartOffset() && pos notices;
Marker(ParserNotice notice) {
notices = new ArrayList<>(1); // Usually just 1
addNotice(notice);
setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
setSize(getPreferredSize());
ToolTipManager.sharedInstance().registerComponent(this);
}
public void addNotice(ParserNotice notice) {
notices.add(notice);
}
public boolean containsMarkedOccurrence() {
boolean result = false;
for (ParserNotice notice : notices) {
if (notice instanceof MarkedOccurrenceNotice) {
result = true;
break;
}
}
return result;
}
public ParserNotice getHighestPriorityNotice() {
ParserNotice selectedNotice = null;
int lowestLevel = Integer.MAX_VALUE; // ERROR is 0
for (ParserNotice notice : notices) {
if (notice.getLevel().getNumericValue()-1 && len>-1) { // These values are optional
DocumentRange range = new DocumentRange(offs, offs + len);
RSyntaxUtilities.selectAndPossiblyCenter(textArea, range, true);
}
else {
int line = pn.getLine();
try {
offs = textArea.getLineStartOffset(line);
textArea.getFoldManager().ensureOffsetNotInClosedFold(offs);
textArea.setCaretPosition(offs);
} catch (BadLocationException ble) { // Never happens
UIManager.getLookAndFeel().provideErrorFeedback(textArea);
}
}
}
@Override
protected void paintComponent(Graphics g) {
final ParserNotice notice = getHighestPriorityNotice();
if (notice != null) {
paintParserNoticeMarker(
(Graphics2D) g, notice,
getWidth(), getHeight()
);
}
}
@Override
public void removeNotify() {
super.removeNotify();
ToolTipManager.sharedInstance().unregisterComponent(this);
removeMouseListener(listener);
}
public void updateLocation() {
int line = notices.get(0).getLine();
int y = lineToY(line - 1, null); // ParserNotices are 1-based
setLocation(2, y);
}
}
}