All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.fife.ui.rsyntaxtextarea.ErrorStrip Maven / Gradle / Ivy

Go to download

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.

There is a newer version: 3.5.2
Show newest version
/*
 * 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.).

* * ErrorStrips display ParserNotices 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 ParserNotices 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); } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy