/*
* 02/17/2009
*
* IconRowHeader.java - Renders icons in the gutter.
*
* This library is distributed under a modified BSD license. See the included
* RSyntaxTextArea.License.txt file for details.
*/
package org.fife.ui.rtextarea;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Insets;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import javax.swing.Icon;
import javax.swing.JPanel;
import javax.swing.ToolTipManager;
import javax.swing.UIManager;
import javax.swing.event.DocumentEvent;
import javax.swing.text.BadLocationException;
import javax.swing.text.Document;
import javax.swing.text.Element;
import javax.swing.text.Position;
import javax.swing.text.View;
import org.fife.ui.rsyntaxtextarea.FoldingAwareIconRowHeader;
/**
* Renders icons in the {@link Gutter}. This can be used to visually mark
* lines containing syntax errors, lines with breakpoints set on them, etc.<p>
*
* This component has built-in support for displaying icons representing
* "bookmarks;" that is, lines a user can cycle through via F2 and Shift+F2.
* Bookmarked lines are toggled via Ctrl+F2, or by clicking in the icon area
* at the line to bookmark. In order to enable bookmarking, you must first
* assign an icon to represent a bookmarked line, then actually enable the
* feature. This is actually done on the parent {@link Gutter} component:<p>
*
* <pre>
* Gutter gutter = scrollPane.getGutter();
* gutter.setBookmarkIcon(new ImageIcon("bookmark.png"));
* gutter.setBookmarkingEnabled(true);
* </pre>
*
* @author Robert Futrell
* @version 1.0
* @see FoldingAwareIconRowHeader
*/
public class IconRowHeader extends AbstractGutterComponent implements MouseListener {
/**
* The icons to render.
*/
protected List<GutterIconImpl> trackingIcons;
/**
* The width of this component.
*/
protected int width;
/**
* Whether this component listens for mouse clicks and toggles "bookmark"
* icons on them.
*/
private boolean bookmarkingEnabled;
/**
* The icon to use for bookmarks.
*/
private Icon bookmarkIcon;
/**
* Used in {@link #paintComponent(Graphics)} to prevent reallocation on
* each paint.
*/
protected Rectangle visibleRect;
/**
* Used in {@link #paintComponent(Graphics)} to prevent reallocation on
* each paint.
*/
protected Insets textAreaInsets;
/**
* The first line in the active line range.
*/
protected int activeLineRangeStart;
/**
* The end line in the active line range.
*/
protected int activeLineRangeEnd;
/**
* The color used to highlight the active code block.
*/
private Color activeLineRangeColor;
/**
* Whether this component should use the gutter's background color (as
* opposed to using a LookAndFeel-dependent color, which is the default
* behavior).
*/
private boolean inheritsGutterBackground;
/**
* Constructor.
*
* @param textArea The parent text area.
*/
public IconRowHeader(RTextArea textArea) {
super(textArea);
}
/**
* Adds an icon that tracks an offset in the document, and is displayed
* adjacent to the line numbers. This is useful for marking things such
* as source code errors.
*
* @param offs The offset to track.
* @param icon The icon to display. This should be small (say 16x16).
* @return A tag for this icon.
* @throws BadLocationException If <code>offs</code> is an invalid offset
* into the text area.
* @see #removeTrackingIcon(Object)
*/
public GutterIconInfo addOffsetTrackingIcon(int offs, Icon icon)
throws BadLocationException {
return addOffsetTrackingIcon(offs, icon, null);
}
/**
* Adds an icon that tracks an offset in the document, and is displayed
* adjacent to the line numbers. This is useful for marking things such
* as source code errors.
*
* @param offs The offset to track.
* @param icon The icon to display. This should be small (say 16x16).
* @param tip A tool tip for the icon.
* @return A tag for this icon.
* @throws BadLocationException If <code>offs</code> is an invalid offset
* into the text area.
* @see #removeTrackingIcon(Object)
*/
public GutterIconInfo addOffsetTrackingIcon(int offs, Icon icon, String tip)
throws BadLocationException {
Position pos = textArea.getDocument().createPosition(offs);
GutterIconImpl ti = new GutterIconImpl(icon, pos, tip);
if (trackingIcons==null) {
trackingIcons = new ArrayList<GutterIconImpl>(1); // Usually small
}
int index = Collections.binarySearch(trackingIcons, ti);
if (index<0) {
index = -(index+1);
}
trackingIcons.add(index, ti);
repaint();
return ti;
}
/**
* Clears the active line range.
*
* @see #setActiveLineRange(int, int)
*/
public void clearActiveLineRange() {
if (activeLineRangeStart!=-1 || activeLineRangeEnd!=-1) {
activeLineRangeStart = activeLineRangeEnd = -1;
repaint();
}
}
/**
* Returns the color used to paint the active line range, if any.
*
* @return The color.
* @see #setActiveLineRangeColor(Color)
*/
public Color getActiveLineRangeColor() {
return activeLineRangeColor;
}
/**
* Returns the icon to use for bookmarks.
*
* @return The icon to use for bookmarks. If this is <code>null</code>,
* bookmarking is effectively disabled.
* @see #setBookmarkIcon(Icon)
* @see #isBookmarkingEnabled()
*/
public Icon getBookmarkIcon() {
return bookmarkIcon;
}
/**
* Returns the bookmarks known to this gutter.
*
* @return The bookmarks. If there are no bookmarks, an empty array is
* returned.
*/
public GutterIconInfo[] getBookmarks() {
List<GutterIconInfo> retVal = new ArrayList<GutterIconInfo>(1);
if (trackingIcons!=null) {
for (int i=0; i<trackingIcons.size(); i++) {
GutterIconImpl ti = getTrackingIcon(i);
if (ti.getIcon()==bookmarkIcon) {
retVal.add(ti);
}
}
}
GutterIconInfo[] array = new GutterIconInfo[retVal.size()];
return retVal.toArray(array);
}
/**
* {@inheritDoc}
*/
@Override
void handleDocumentEvent(DocumentEvent e) {
int newLineCount = textArea.getLineCount();
if (newLineCount!=currentLineCount) {
currentLineCount = newLineCount;
repaint();
}
}
/**
* {@inheritDoc}
*/
@Override
public Dimension getPreferredSize() {
int h = textArea!=null ? textArea.getHeight() : 100; // Arbitrary
return new Dimension(width, h);
}
/**
* Overridden to display the tool tip of any icons on this line.
*
* @param e The location the mouse is hovering over.
*/
@Override
public String getToolTipText(MouseEvent e) {
try {
int line = viewToModelLine(e.getPoint());
if (line>-1) {
GutterIconInfo[] infos = getTrackingIcons(line);
if (infos.length>0) {
// TODO: Display all messages?
return infos[infos.length-1].getToolTip();
}
}
} catch (BadLocationException ble) {
ble.printStackTrace(); // Never happens
}
return null;
}
protected GutterIconImpl getTrackingIcon(int index) {
return trackingIcons.get(index);
}
/**
* Returns the tracking icons at the specified line.
*
* @param line The line.
* @return The tracking icons at that line. If there are no tracking
* icons there, this will be an empty array.
* @throws BadLocationException If <code>line</code> is invalid.
*/
public GutterIconInfo[] getTrackingIcons(int line)
throws BadLocationException {
List<GutterIconInfo> retVal = new ArrayList<GutterIconInfo>(1);
if (trackingIcons!=null) {
int start = textArea.getLineStartOffset(line);
int end = textArea.getLineEndOffset(line);
if (line==textArea.getLineCount()-1) {
end++; // Hack
}
for (int i=0; i<trackingIcons.size(); i++) {
GutterIconImpl ti = getTrackingIcon(i);
int offs = ti.getMarkedOffset();
if (offs>=start && offs<end) {
retVal.add(ti);
}
else if (offs>=end) {
break; // Quit early
}
}
}
GutterIconInfo[] array = new GutterIconInfo[retVal.size()];
return retVal.toArray(array);
}
@Override
protected void init() {
super.init();
visibleRect = new Rectangle();
width = 16;
addMouseListener(this);
activeLineRangeStart = activeLineRangeEnd = -1;
setActiveLineRangeColor(null);
// Must explicitly set our background color, otherwise we inherit that
// of the parent Gutter.
updateBackground();
ToolTipManager.sharedInstance().registerComponent(this);
}
/**
* Returns whether bookmarking is enabled.
*
* @return Whether bookmarking is enabled.
* @see #setBookmarkingEnabled(boolean)
*/
public boolean isBookmarkingEnabled() {
return bookmarkingEnabled;
}
/**
* {@inheritDoc}
*/
@Override
void lineHeightsChanged() {
repaint();
}
public void mouseClicked(MouseEvent e) {
}
public void mouseEntered(MouseEvent e) {
}
public void mouseExited(MouseEvent e) {
}
public void mousePressed(MouseEvent e) {
if (bookmarkingEnabled && bookmarkIcon!=null) {
try {
int line = viewToModelLine(e.getPoint());
if (line>-1) {
toggleBookmark(line);
}
} catch (BadLocationException ble) {
ble.printStackTrace(); // Never happens
}
}
}
public void mouseReleased(MouseEvent e) {
}
/**
* {@inheritDoc}
*/
@Override
protected void paintComponent(Graphics g) {
if (textArea==null) {
return;
}
visibleRect = g.getClipBounds(visibleRect);
if (visibleRect==null) { // ???
visibleRect = getVisibleRect();
}
//System.out.println("IconRowHeader repainting: " + visibleRect);
if (visibleRect==null) {
return;
}
paintBackgroundImpl(g, visibleRect);
if (textArea.getLineWrap()) {
paintComponentWrapped(g);
return;
}
Document doc = textArea.getDocument();
Element root = doc.getDefaultRootElement();
textAreaInsets = textArea.getInsets(textAreaInsets);
if (visibleRect.y<textAreaInsets.top) {
visibleRect.height -= (textAreaInsets.top - visibleRect.y);
visibleRect.y = textAreaInsets.top;
}
// Get the first and last lines to paint.
int cellHeight = textArea.getLineHeight();
int topLine = (visibleRect.y-textAreaInsets.top)/cellHeight;
int bottomLine = Math.min(topLine+visibleRect.height/cellHeight+1,
root.getElementCount());
// Get where to start painting (top of the row).
// We need to be "scrolled up" up just enough for the missing part of
// the first line.
int y = topLine*cellHeight + textAreaInsets.top;
if ((activeLineRangeStart>=topLine&&activeLineRangeStart<=bottomLine) ||
(activeLineRangeEnd>=topLine && activeLineRangeEnd<=bottomLine) ||
(activeLineRangeStart<=topLine && activeLineRangeEnd>=bottomLine)) {
g.setColor(activeLineRangeColor);
int firstLine = Math.max(activeLineRangeStart, topLine);
int y1 = firstLine * cellHeight + textAreaInsets.top;
int lastLine = Math.min(activeLineRangeEnd, bottomLine);
int y2 = (lastLine+1) * cellHeight + textAreaInsets.top - 1;
int j = y1;
while (j<=y2) {
int yEnd = Math.min(y2, j+getWidth());
int xEnd = yEnd-j;
g.drawLine(0,j, xEnd,yEnd);
j += 2;
}
int i = 2;
while (i<getWidth()) {
int yEnd = y1 + getWidth() - i;
g.drawLine(i,y1, getWidth(),yEnd);
i += 2;
}
if (firstLine==activeLineRangeStart) {
g.drawLine(0,y1, getWidth(),y1);
}
if (lastLine==activeLineRangeEnd) {
g.drawLine(0,y2, getWidth(),y2);
}
}
if (trackingIcons!=null) {
int lastLine = bottomLine;
for (int i=trackingIcons.size()-1; i>=0; i--) { // Last to first
GutterIconInfo ti = getTrackingIcon(i);
int offs = ti.getMarkedOffset();
if (offs>=0 && offs<=doc.getLength()) {
int line = root.getElementIndex(offs);
if (line<=lastLine && line>=topLine) {
Icon icon = ti.getIcon();
if (icon!=null) {
int y2 = y + (line-topLine)*cellHeight;
y2 += (cellHeight-icon.getIconHeight())/2;
ti.getIcon().paintIcon(this, g, 0, y2);
lastLine = line-1; // Paint only 1 icon per line
}
}
else if (line<topLine) {
break;
}
}
}
}
}
/**
* Paints the background of this component.
*
* @param g The graphics context.
* @param visibleRect The visible bounds of this component.
*/
protected void paintBackgroundImpl(Graphics g, Rectangle visibleRect) {
Color bg = getBackground();
if (inheritsGutterBackground && getGutter()!=null) {
bg = getGutter().getBackground();
}
g.setColor(bg);
g.fillRect(0,visibleRect.y, width,visibleRect.height);
}
/**
* Paints icons when line wrapping is enabled.
*
* @param g The graphics context.
*/
private void paintComponentWrapped(Graphics g) {
// The variables we use are as follows:
// - visibleRect is the "visible" area of the text area; e.g.
// [0,100, 300,100+(lineCount*cellHeight)-1].
// actualTop.y is the topmost-pixel in the first logical line we
// paint. Note that we may well not paint this part of the logical
// line, as it may be broken into many physical lines, with the first
// few physical lines scrolled past. Note also that this is NOT the
// visible rect of this line number list; this line number list has
// visible rect == [0,0, insets.left-1,visibleRect.height-1].
// - offset (<=0) is the y-coordinate at which we begin painting when
// we begin painting with the first logical line. This can be
// negative, signifying that we've scrolled past the actual topmost
// part of this line.
// The algorithm is as follows:
// - Get the starting y-coordinate at which to paint. This may be
// above the first visible y-coordinate as we're in line-wrapping
// mode, but we always paint entire logical lines.
// - Paint that line's line number and highlight, if appropriate.
// Increment y to be just below the are we just painted (i.e., the
// beginning of the next logical line's view area).
// - Get the ending visual position for that line. We can now loop
// back, paint this line, and continue until our y-coordinate is
// past the last visible y-value.
// We avoid using modelToView/viewToModel where possible, as these
// methods trigger a parsing of the line into syntax tokens, which is
// costly. It's cheaper to just grab the child views' bounds.
RTextAreaUI ui = (RTextAreaUI)textArea.getUI();
View v = ui.getRootView(textArea).getView(0);
// boolean currentLineHighlighted = textArea.getHighlightCurrentLine();
Document doc = textArea.getDocument();
Element root = doc.getDefaultRootElement();
int lineCount = root.getElementCount();
int topPosition = textArea.viewToModel(
new Point(visibleRect.x,visibleRect.y));
int topLine = root.getElementIndex(topPosition);
// Compute the y at which to begin painting text, taking into account
// that 1 logical line => at least 1 physical line, so it may be that
// y<0. The computed y-value is the y-value of the top of the first
// (possibly) partially-visible view.
Rectangle visibleEditorRect = ui.getVisibleEditorRect();
Rectangle r = IconRowHeader.getChildViewBounds(v, topLine,
visibleEditorRect);
int y = r.y;
int visibleBottom = visibleRect.y + visibleRect.height;
// Get the first possibly visible icon index.
int currentIcon = -1;
if (trackingIcons!=null) {
for (int i=0; i<trackingIcons.size(); i++) {
GutterIconImpl icon = getTrackingIcon(i);
int offs = icon.getMarkedOffset();
if (offs>=0 && offs<=doc.getLength()) {
int line = root.getElementIndex(offs);
if (line>=topLine) {
currentIcon = i;
break;
}
}
}
}
// Keep painting lines until our y-coordinate is past the visible
// end of the text area.
g.setColor(getForeground());
int cellHeight = textArea.getLineHeight();
while (y < visibleBottom) {
r = getChildViewBounds(v, topLine, visibleEditorRect);
// int lineEndY = r.y+r.height;
/*
// Highlight the current line's line number, if desired.
if (currentLineHighlighted && topLine==currentLine) {
g.setColor(textArea.getCurrentLineHighlightColor());
g.fillRect(0,y, width,lineEndY-y);
g.setColor(getForeground());
}
*/
// Possibly paint an icon.
if (currentIcon>-1) {
// We want to paint the last icon added for this line.
GutterIconImpl toPaint = null;
while (currentIcon<trackingIcons.size()) {
GutterIconImpl ti = getTrackingIcon(currentIcon);
int offs = ti.getMarkedOffset();
if (offs>=0 && offs<=doc.getLength()) {
int line = root.getElementIndex(offs);
if (line==topLine) {
toPaint = ti;
}
else if (line>topLine) {
break;
}
}
currentIcon++;
}
if (toPaint!=null) {
Icon icon = toPaint.getIcon();
if (icon!=null) {
int y2 = y + (cellHeight-icon.getIconHeight())/2;
icon.paintIcon(this, g, 0, y2);
}
}
}
// The next possible y-coordinate is just after the last line
// painted.
y += r.height;
// Update topLine (we're actually using it for our "current line"
// variable now).
topLine++;
if (topLine>=lineCount)
break;
}
}
/**
* Removes the specified tracking icon.
*
* @param tag A tag for a tracking icon.
* @see #removeAllTrackingIcons()
* @see #addOffsetTrackingIcon(int, Icon)
*/
public void removeTrackingIcon(Object tag) {
if (trackingIcons!=null && trackingIcons.remove(tag)) {
repaint();
}
}
/**
* Removes all tracking icons.
*
* @see #removeTrackingIcon(Object)
* @see #addOffsetTrackingIcon(int, Icon)
*/
public void removeAllTrackingIcons() {
if (trackingIcons!=null && trackingIcons.size()>0) {
trackingIcons.clear();
repaint();
}
}
/**
* Removes all bookmark tracking icons.
*/
private void removeBookmarkTrackingIcons() {
if (trackingIcons!=null) {
for (Iterator<GutterIconImpl> i=trackingIcons.iterator();
i.hasNext(); ) {
GutterIconImpl ti = i.next();
if (ti.getIcon()==bookmarkIcon) {
i.remove();
}
}
}
}
/**
* Highlights a range of lines in the icon area.
*
* @param startLine The start of the line range.
* @param endLine The end of the line range.
* @see #clearActiveLineRange()
*/
public void setActiveLineRange(int startLine, int endLine) {
if (startLine!=activeLineRangeStart ||
endLine!=activeLineRangeEnd) {
activeLineRangeStart = startLine;
activeLineRangeEnd = endLine;
repaint();
}
}
/**
* Sets the color to use to render active line ranges.
*
* @param color The color to use. If this is null, then the default
* color is used.
* @see #getActiveLineRangeColor()
* @see Gutter#DEFAULT_ACTIVE_LINE_RANGE_COLOR
*/
public void setActiveLineRangeColor(Color color) {
if (color==null) {
color = Gutter.DEFAULT_ACTIVE_LINE_RANGE_COLOR;
}
if (!color.equals(activeLineRangeColor)) {
activeLineRangeColor = color;
repaint();
}
}
/**
* Sets the icon to use for bookmarks. Any previous bookmark icons
* are removed.
*
* @param icon The new bookmark icon. If this is <code>null</code>,
* bookmarking is effectively disabled.
* @see #getBookmarkIcon()
* @see #isBookmarkingEnabled()
*/
public void setBookmarkIcon(Icon icon) {
removeBookmarkTrackingIcons();
bookmarkIcon = icon;
repaint();
}
/**
* Sets whether bookmarking is enabled. Note that a bookmarking icon
* must be set via {@link #setBookmarkIcon(Icon)} before bookmarks are
* truly enabled.
*
* @param enabled Whether bookmarking is enabled. If this is
* <code>false</code>, any bookmark icons are removed.
* @see #isBookmarkingEnabled()
* @see #setBookmarkIcon(Icon)
*/
public void setBookmarkingEnabled(boolean enabled) {
if (enabled!=bookmarkingEnabled) {
bookmarkingEnabled = enabled;
if (!enabled) {
removeBookmarkTrackingIcons();
}
repaint();
}
}
/**
* Sets whether the icon area inherits the gutter background (as opposed
* to painting with its own, default "panel" color, which is the default).
*
* @param inherits Whether the gutter background should be used in the icon
* row header. If this is <code>false</code>, a default,
* Look-and-feel-dependent color is used.
*/
public void setInheritsGutterBackground(boolean inherits) {
if (inherits!=inheritsGutterBackground) {
inheritsGutterBackground = inherits;
repaint();
}
}
/**
* Sets the text area being displayed. This will clear any tracking
* icons currently displayed.
*
* @param textArea The text area.
*/
@Override
public void setTextArea(RTextArea textArea) {
removeAllTrackingIcons();
super.setTextArea(textArea);
}
/**
* Programatically toggles whether there is a bookmark for the specified
* line. If bookmarking is not enabled, this method does nothing.
*
* @param line The line.
* @return Whether a bookmark is now at the specified line.
* @throws BadLocationException If <code>line</code> is an invalid line
* number in the text area.
*/
public boolean toggleBookmark(int line) throws BadLocationException {
if (!isBookmarkingEnabled() || getBookmarkIcon()==null) {
return false;
}
GutterIconInfo[] icons = getTrackingIcons(line);
if (icons.length==0) {
int offs = textArea.getLineStartOffset(line);
addOffsetTrackingIcon(offs, bookmarkIcon);
return true;
}
boolean found = false;
for (int i=0; i<icons.length; i++) {
if (icons[i].getIcon()==bookmarkIcon) {
removeTrackingIcon(icons[i]);
found = true;
// Don't quit, in case they manipulate the document so > 1
// bookmark is on a single line (kind of flaky, but it
// works...). If they delete all chars in the document,
// AbstractDocument gets a little flaky with the returned line
// number for viewToModel(), so this is just us trying to save
// face a little.
}
}
if (!found) {
int offs = textArea.getLineStartOffset(line);
addOffsetTrackingIcon(offs, bookmarkIcon);
}
return !found;
}
/**
* Sets our background color to that of standard "panels" in this
* LookAndFeel. This is necessary because, otherwise, we'd inherit the
* background color of our parent component (the Gutter).
*/
private void updateBackground() {
Color bg = UIManager.getColor("Panel.background");
if (bg==null) { // UIManager properties aren't guaranteed to exist
bg = new JPanel().getBackground();
}
setBackground(bg);
}
/**
* {@inheritDoc}
*/
@Override
public void updateUI() {
super.updateUI(); // Does nothing
updateBackground();
}
/**
* Returns the line rendered at the specified location.
*
* @param p The location in this row header.
* @return The corresponding line in the editor.
* @throws BadLocationException ble If an error occurs.
*/
private int viewToModelLine(Point p) throws BadLocationException {
int offs = textArea.viewToModel(p);
return offs>-1 ? textArea.getLineOfOffset(offs) : -1;
}
/**
* Implementation of the icons rendered.
*/
private static class GutterIconImpl implements GutterIconInfo,
Comparable<GutterIconInfo> {
private Icon icon;
private Position pos;
private String toolTip;
public GutterIconImpl(Icon icon, Position pos, String toolTip) {
this.icon = icon;
this.pos = pos;
this.toolTip = toolTip;
}
public int compareTo(GutterIconInfo other) {
if (other!=null) {
return pos.getOffset() - other.getMarkedOffset();
}
return -1;
}
@Override
public boolean equals(Object o) {
return o==this;
}
public Icon getIcon() {
return icon;
}
public int getMarkedOffset() {
return pos.getOffset();
}
public String getToolTip() {
return toolTip;
}
@Override
public int hashCode() {
return icon.hashCode(); // FindBugs
}
}
}