org.fife.ui.rtextarea.IconRowHeader Maven / Gradle / Ivy
/*
* 02/17/2009
*
* IconRowHeader.java - Renders icons in the gutter.
*
* This library is distributed under a modified BSD license. See the included
* LICENSE 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.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;
/**
* Renders icons in the {@link Gutter}. This can be used to visually mark
* lines containing syntax errors, lines with breakpoints set on them, etc.
*
* 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:
*
*
* Gutter gutter = scrollPane.getGutter();
* gutter.setBookmarkIcon(new ImageIcon("bookmark.png"));
* gutter.setBookmarkingEnabled(true);
*
*
* @author Robert Futrell
* @version 1.0
* @see org.fife.ui.rsyntaxtextarea.FoldingAwareIconRowHeader
*/
public class IconRowHeader extends AbstractGutterComponent implements MouseListener {
/**
* The icons to render.
*/
protected List 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 offs
is an invalid offset
* into the text area.
* @see #removeTrackingIcon(GutterIconInfo)
*/
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 offs
is an invalid offset
* into the text area.
* @see #removeTrackingIcon(GutterIconInfo)
*/
public GutterIconInfo addOffsetTrackingIcon(int offs, Icon icon, String tip)
throws BadLocationException {
// Despite its documentation, AbstractDocument does *not* throw BLEs
// when creating sticky positions for offsets that do not exist.
// We must check for that ourselves.
if (offs < 0 || offs > textArea.getDocument().getLength()) {
throw new BadLocationException("Offset " + offs + " not in " +
"required range of 0-" + textArea.getDocument().getLength(),
offs);
}
Position pos = textArea.getDocument().createPosition(offs);
GutterIconImpl ti = new GutterIconImpl(icon, pos, tip);
if (trackingIcons==null) {
trackingIcons = new ArrayList<>(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 null
,
* 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 retVal = new ArrayList<>(1);
if (trackingIcons!=null) {
for (int i=0; i-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 line
is invalid.
*/
public GutterIconInfo[] getTrackingIcons(int line)
throws BadLocationException {
List retVal = new ArrayList<>(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=start && 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();
}
@Override
public void mouseClicked(MouseEvent e) {
}
@Override
public void mouseEntered(MouseEvent e) {
}
@Override
public void mouseExited(MouseEvent e) {
}
@Override
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
}
}
}
@Override
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=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=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 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=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=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(GutterIconInfo tag) {
if (trackingIcons!=null && trackingIcons.remove(tag)) {
repaint();
}
}
/**
* Removes all tracking icons.
*
* @see #removeTrackingIcon(GutterIconInfo)
* @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) {
trackingIcons.removeIf(ti -> ti.getIcon() == bookmarkIcon);
}
}
/**
* 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 null
,
* 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
* false
, 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 false
, 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 line
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 (GutterIconInfo icon : icons) {
if (icon.getIcon() == bookmarkIcon) {
removeTrackingIcon(icon);
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 {
private Icon icon;
private Position pos;
private String toolTip;
GutterIconImpl(Icon icon, Position pos, String toolTip) {
this.icon = icon;
this.pos = pos;
this.toolTip = toolTip;
}
@Override
public int compareTo(GutterIconInfo other) {
if (other!=null) {
return pos.getOffset() - other.getMarkedOffset();
}
return -1;
}
@Override
public boolean equals(Object o) {
return o==this;
}
@Override
public Icon getIcon() {
return icon;
}
@Override
public int getMarkedOffset() {
return pos.getOffset();
}
@Override
public String getToolTip() {
return toolTip;
}
@Override
public int hashCode() {
return icon.hashCode(); // FindBugs
}
}
}