/*
* 02/17/2009
*
* Gutter.java - Manages line numbers, icons, etc. on the left-hand side of
* an RTextArea.
* 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.rtextarea;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.ComponentOrientation;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Point;
import java.awt.event.ComponentAdapter;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import javax.swing.Icon;
import javax.swing.JComponent;
import javax.swing.border.EmptyBorder;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.text.BadLocationException;
import org.fife.ui.rsyntaxtextarea.ActiveLineRangeEvent;
import org.fife.ui.rsyntaxtextarea.ActiveLineRangeListener;
import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea;
/**
* The gutter is the component on the left-hand side of the text area that displays optional information such as line
* numbers and icons (for bookmarks, debugging breakpoints, error markers, etc.).
* <p>
*
* To add icons to the gutter, you must first call {@link RTextScrollPane#setIconRowHeaderEnabled(boolean)} on the
* parent scroll pane, to make the icon area visible. Then, you can add icons that track either lines in the document,
* or offsets, via {@link #addLineTrackingIcon(int, Icon)} and {@link #addOffsetTrackingIcon(int, Icon)}, respectively.
* To remove an icon you've added, use {@link #removeTrackingIcon(GutterIconInfo)}.
*
* @author Robert Futrell
* @version 1.0
* @see GutterIconInfo
*/
public class Gutter extends JComponent {
/**
* The text area.
*/
private RTextArea textArea;
/**
* Renders line numbers.
*/
private LineNumberList lineNumberList;
/**
* Renders bookmark icons, breakpoints, error icons, etc.
*/
private IconRowHeader iconArea;
/**
* Listens for events in our text area.
*/
private TextAreaListener listener;
/**
* Constructor.
*
* @param textArea
* The parent text area.
*/
public Gutter(RTextArea textArea) {
listener = new TextAreaListener();
setTextArea(textArea);
setLayout(new BorderLayout());
if (this.textArea != null) {
// Enable line numbers our first time through if they give us
// a text area.
setLineNumbersEnabled(true);
}
setBorder(new GutterBorder(0, 0, 0, 1)); // Assume ltr
Color bg = null;
if (textArea != null) {
bg = textArea.getBackground(); // May return null if image bg
}
setBackground(bg != null ? bg : Color.WHITE);
}
/**
* 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 line
* The line to track (zero-based).
* @param icon
* The icon to display. This should be small (say 16x16).
* @return A tag for this icon. This can later be used in a call to {@link #removeTrackingIcon(GutterIconInfo)} to
* remove this icon.
* @throws BadLocationException
* If <code>offs</code> is an invalid offset into the text area.
* @see #addOffsetTrackingIcon(int, Icon)
* @see #removeTrackingIcon(GutterIconInfo)
*/
public GutterIconInfo addLineTrackingIcon(int line, Icon icon)
throws BadLocationException {
int offs = textArea.getLineStartOffset(line);
return addOffsetTrackingIcon(offs, icon);
}
/**
* 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 #addLineTrackingIcon(int, Icon)
* @see #removeTrackingIcon(GutterIconInfo)
*/
public GutterIconInfo addOffsetTrackingIcon(int offs, Icon icon)
throws BadLocationException {
return iconArea.addOffsetTrackingIcon(offs, icon);
}
/**
* Clears the active line range.
*
* @see #setActiveLineRange(int, int)
*/
private void clearActiveLineRange() {
iconArea.clearActiveLineRange();
}
/**
* 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 iconArea.getBookmarkIcon();
}
/**
* Returns the bookmarks known to this gutter.
*
* @return The bookmarks. If there are no bookmarks, an empty array is returned.
*/
public GutterIconInfo[] getBookmarks() {
return iconArea.getBookmarks();
}
/**
* Returns the color of the "border" line.
*
* @return The color.
* @see #setBorderColor(Color)
*/
public Color getBorderColor() {
return ((GutterBorder) getBorder()).getColor();
}
/**
* Returns the color to use to paint line numbers.
*
* @return The color used when painting line numbers.
* @see #setLineNumberColor(Color)
*/
public Color getLineNumberColor() {
return lineNumberList.getForeground();
}
/**
* Returns the font used for line numbers.
*
* @return The font used for line numbers.
* @see #setLineNumberFont(Font)
*/
public Font getLineNumberFont() {
return lineNumberList.getFont();
}
/**
* Returns the starting line's line number. The default value is <code>1</code>.
*
* @return The index
* @see #setLineNumberingStartIndex(int)
*/
public int getLineNumberingStartIndex() {
return lineNumberList.getLineNumberingStartIndex();
}
/**
* Returns <code>true</code> if the line numbers are enabled and visible.
*
* @return Whether or not line numbers are visible.
*/
public boolean getLineNumbersEnabled() {
for (int i = 0; i < getComponentCount(); i++) {
if (getComponent(i) == lineNumberList) {
return true;
}
}
return false;
}
/**
* Returns the tracking icons at the specified view position.
*
* @param p
* The view position.
* @return The tracking icons at that position. If there are no tracking icons there, this will be an empty array.
* @throws BadLocationException
* If <code>p</code> is invalid.
*/
public Object[] getTrackingIcons(Point p) throws BadLocationException {
int offs = textArea.viewToModel(new Point(0, p.y));
int line = textArea.getLineOfOffset(offs);
return iconArea.getTrackingIcons(line);
}
public GutterIconInfo[] getAllTrackingIcons() {
return iconArea.getAllTrackingIcons();
}
/**
* Returns whether bookmarking is enabled.
*
* @return Whether bookmarking is enabled.
* @see #setBookmarkingEnabled(boolean)
*/
public boolean isBookmarkingEnabled() {
return iconArea.isBookmarkingEnabled();
}
/**
* Returns whether the icon row header is enabled.
*
* @return Whether the icon row header is enabled.
*/
public boolean isIconRowHeaderEnabled() {
for (int i = 0; i < getComponentCount(); i++) {
if (getComponent(i) == iconArea) {
return true;
}
}
return false;
}
/**
* Removes the specified tracking icon.
*
* @param tag
* A tag for an icon in the gutter, as returned from either {@link #addLineTrackingIcon(int, Icon)} or
* {@link #addOffsetTrackingIcon(int, Icon)}.
* @see #removeAllTrackingIcons()
* @see #addLineTrackingIcon(int, Icon)
* @see #addOffsetTrackingIcon(int, Icon)
*/
public void removeTrackingIcon(GutterIconInfo tag) {
iconArea.removeTrackingIcon(tag);
}
/**
* Removes all tracking icons.
*
* @see #removeTrackingIcon(GutterIconInfo)
* @see #addOffsetTrackingIcon(int, Icon)
*/
public void removeAllTrackingIcons() {
iconArea.removeAllTrackingIcons();
}
/**
* Highlights a range of lines in the icon area. This, of course, will only be visible if the icon area is visible.
*
* @param startLine
* The start of the line range.
* @param endLine
* The end of the line range.
* @see #clearActiveLineRange()
*/
private void setActiveLineRange(int startLine, int endLine) {
iconArea.setActiveLineRange(startLine, endLine);
}
/**
* Sets the icon to use for bookmarks.
*
* @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) {
iconArea.setBookmarkIcon(icon);
}
/**
* Sets the icon to use for current Line.
*
* @param icon
* The new icon. If this is <code>null</code>, bookmarking is effectively disabled.
* @see #getBookmarkIcon()
* @see #isBookmarkingEnabled()
*/
public void setCurrentLineIcon(Icon icon) {
iconArea.setCurrentLineIcon(icon);
}
/**
* 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.
* @see #isBookmarkingEnabled()
* @see #setBookmarkIcon(Icon)
*/
public void setBookmarkingEnabled(boolean enabled) {
iconArea.setBookmarkingEnabled(enabled);
if (enabled && !isIconRowHeaderEnabled()) {
setIconRowHeaderEnabled(true);
}
}
/**
* Sets the color for the "border" line.
*
* @param color
* The new color.
* @see #getBorderColor()
*/
public void setBorderColor(Color color) {
((GutterBorder) getBorder()).setColor(color);
repaint();
}
/**
* {@inheritDoc}
*/
public void setComponentOrientation(ComponentOrientation o) {
// Reuse the border to preserve its color.
if (o.isLeftToRight()) {
((GutterBorder) getBorder()).setEdges(0, 0, 0, 1);
}
else {
((GutterBorder) getBorder()).setEdges(0, 1, 0, 0);
}
super.setComponentOrientation(o);
}
/**
* Toggles whether the icon row header (used for breakpoints, bookmarks, etc.) is enabled.
*
* @param enabled
* Whether the icon row header is enabled.
* @see #isIconRowHeaderEnabled()
*/
void setIconRowHeaderEnabled(boolean enabled) {
if (iconArea != null) {
if (enabled) {
add(iconArea, BorderLayout.LINE_START);
}
else {
remove(iconArea);
}
revalidate();
}
}
/**
* Sets the color to use to paint line numbers.
*
* @param color
* The color to use when painting line numbers.
* @see #getLineNumberColor()
*/
public void setLineNumberColor(Color color) {
lineNumberList.setForeground(color);
}
/**
* Sets the font used for line numbers.
*
* @param font
* The font to use. This cannot be <code>null</code>.
* @see #getLineNumberFont()
*/
public void setLineNumberFont(Font font) {
if (font == null) {
throw new IllegalArgumentException("font cannot be null");
}
lineNumberList.setFont(font);
}
/**
* Sets the starting line's line number. The default value is <code>1</code>. Applications can call this method to
* change this value if they are displaying a subset of lines in a file, for example.
*
* @param index
* The new index.
* @see #getLineNumberingStartIndex()
*/
public void setLineNumberingStartIndex(int index) {
lineNumberList.setLineNumberingStartIndex(index);
}
/**
* Toggles whether or not line numbers are visible.
*
* @param enabled
* Whether or not line numbers should be visible.
* @see #getLineNumbersEnabled()
*/
void setLineNumbersEnabled(boolean enabled) {
if (lineNumberList != null) {
if (enabled) {
add(lineNumberList);
}
else {
remove(lineNumberList);
}
revalidate();
}
}
/**
* Sets the text area being displayed. This will clear any tracking icons currently displayed.
*
* @param textArea
* The text area.
*/
void setTextArea(RTextArea textArea) {
if (this.textArea != null) {
listener.uninstall();
}
if (lineNumberList == null) {
lineNumberList = new LineNumberList(textArea);
}
else {
lineNumberList.setTextArea(textArea);
}
if (iconArea == null) {
iconArea = new IconRowHeader(textArea);
}
else {
iconArea.setTextArea(textArea);
}
if (textArea != null) {
listener.install(textArea);
}
this.textArea = 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 {
return iconArea.toggleBookmark(line);
}
/**
* The border used by the gutter.
*/
private static class GutterBorder extends EmptyBorder {
private Color color;
public GutterBorder(int top, int left, int bottom, int right) {
super(top, left, bottom, right);
color = new Color(221, 221, 221);
}
public Color getColor() {
return color;
}
public void paintBorder(Component c, Graphics g, int x, int y,
int width, int height) {
g.setColor(color);
if (left == 1) {
g.drawLine(0, 0, 0, height);
}
else {
g.drawLine(width - 1, 0, width - 1, height);
}
}
public void setColor(Color color) {
this.color = color;
}
public void setEdges(int top, int left, int bottom, int right) {
this.top = top;
this.left = left;
this.bottom = bottom;
this.right = right;
}
}
/**
* Listens for the text area resizing.
*/
/*
* This is necessary to keep child components the same height as the text area. The worse case is when the user
* toggles word-wrap and it changes the height of the text area. In that case, if we listen for the "lineWrap"
* property change, we get notified BEFORE the text area decides on its new size, thus we cannot resize properly. We
* listen instead for ComponentEvents so we change size after the text area has resized.
*/
private class TextAreaListener extends ComponentAdapter
implements DocumentListener, PropertyChangeListener,
ActiveLineRangeListener {
private boolean installed;
/**
* Modifies the "active line range" that is painted in this component.
*
* @param e
* Information about the new "active line range."
*/
public void activeLineRangeChanged(ActiveLineRangeEvent e) {
if (e.getMin() == -1) {
clearActiveLineRange();
}
else {
setActiveLineRange(e.getMin(), e.getMax());
}
}
public void changedUpdate(DocumentEvent e) {
}
public void componentResized(java.awt.event.ComponentEvent e) {
revalidate();
}
protected void handleDocumentEvent(DocumentEvent e) {
for (int i = 0; i < getComponentCount(); i++) {
AbstractGutterComponent agc =
(AbstractGutterComponent) getComponent(i);
agc.handleDocumentEvent(e);
}
}
public void insertUpdate(DocumentEvent e) {
handleDocumentEvent(e);
}
public void install(RTextArea textArea) {
if (installed) {
uninstall();
}
textArea.addComponentListener(this);
textArea.getDocument().addDocumentListener(this);
textArea.addPropertyChangeListener(this);
if (textArea instanceof RSyntaxTextArea) {
((RSyntaxTextArea) textArea).addActiveLineRangeListener(this);
}
installed = true;
}
public void propertyChange(PropertyChangeEvent e) {
String name = e.getPropertyName();
// If they change the text area's font, we need to update cell
// heights to match the font's height.
if ("font".equals(name) ||
RSyntaxTextArea.SYNTAX_SCHEME_PROPERTY.equals(name)) {
for (int i = 0; i < getComponentCount(); i++) {
AbstractGutterComponent agc =
(AbstractGutterComponent) getComponent(i);
agc.lineHeightsChanged();
}
}
}
public void removeUpdate(DocumentEvent e) {
handleDocumentEvent(e);
}
public void uninstall() {
if (installed) {
textArea.removeComponentListener(this);
textArea.getDocument().removeDocumentListener(this);
if (textArea instanceof RSyntaxTextArea) {
((RSyntaxTextArea) textArea).removeActiveLineRangeListener(this);
}
installed = false;
}
}
}
}