/* * 02/17/2009 * * Gutter.java - Manages line numbers, icons, etc. on the left-hand side of * an RTextArea. * * 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.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.Rectangle; import java.awt.event.ComponentAdapter; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import javax.swing.Icon; import javax.swing.JComponent; import javax.swing.JPanel; import javax.swing.border.Border; 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; import org.fife.ui.rsyntaxtextarea.folding.FoldManager; /** * The gutter is the component on the left-hand side of the text area that * displays optional information such as line numbers, fold regions, and icons * (for bookmarks, debugging breakpoints, error markers, etc.).<p> * * Icons can be added on a per-line basis to visually mark syntax errors, lines * with breakpoints set on them, etc. 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)}.<p> * * In addition to support for arbitrary per-line icons, this component also * 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. In order to enable bookmarking, you must first assign * an icon to represent a bookmarked line, then actually enable the feature: * * <pre> * Gutter gutter = scrollPane.getGutter(); * gutter.setBookmarkIcon(new ImageIcon("bookmark.png")); * gutter.setBookmarkingEnabled(true); * </pre> * * @author Robert Futrell * @version 1.0 * @see GutterIconInfo */ public class Gutter extends JPanel { /** * The color used to highlight active line ranges if none is specified. */ public static final Color DEFAULT_ACTIVE_LINE_RANGE_COLOR = new Color(51, 153, 255); /** * The text area. */ private RTextArea textArea; /** * Renders line numbers. */ private LineNumberList lineNumberList; /** * The color used to render line numbers. */ private Color lineNumberColor; /** * The starting index for line numbers in the gutter. */ private int lineNumberingStartIndex; /** * The font used to render line numbers. */ private Font lineNumberFont; /** * Renders bookmark icons, breakpoints, error icons, etc. */ private IconRowHeader iconArea; /** * Whether the icon area inherits the gutter background (as opposed to * painting with its own, default "panel" color). */ private boolean iconRowHeaderInheritsGutterBackground; /** * Shows lines that are code-foldable. */ private FoldIndicator foldIndicator; /** * Listens for events in our text area. */ private TextAreaListener listener; /** * Constructor. * * @param textArea The parent text area. */ public Gutter(RTextArea textArea) { listener = new TextAreaListener(); lineNumberColor = Color.gray; lineNumberFont = RTextArea.getDefaultFont(); lineNumberingStartIndex = 1; iconRowHeaderInheritsGutterBackground = false; 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); if (this.textArea instanceof RSyntaxTextArea) { RSyntaxTextArea rsta = (RSyntaxTextArea)this.textArea; setFoldIndicatorEnabled(rsta.isCodeFoldingEnabled()); } } 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 #addLineTrackingIcon(int, Icon, String) * @see #addOffsetTrackingIcon(int, Icon) * @see #removeTrackingIcon(GutterIconInfo) */ public GutterIconInfo addLineTrackingIcon(int line, Icon icon) throws BadLocationException { return addLineTrackingIcon(line, 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 line The line to track (zero-based). * @param icon The icon to display. This should be small (say 16x16). * @param tip An optional tool tip for the icon. * @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 #addLineTrackingIcon(int, Icon) * @see #addOffsetTrackingIcon(int, Icon) * @see #removeTrackingIcon(GutterIconInfo) */ public GutterIconInfo addLineTrackingIcon(int line, Icon icon, String tip) throws BadLocationException { int offs = textArea.getLineStartOffset(line); return addOffsetTrackingIcon(offs, icon, tip); } /** * 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 #addOffsetTrackingIcon(int, Icon, String) * @see #addLineTrackingIcon(int, Icon) * @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 An optional 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 #addOffsetTrackingIcon(int, Icon) * @see #addLineTrackingIcon(int, Icon) * @see #removeTrackingIcon(GutterIconInfo) */ public GutterIconInfo addOffsetTrackingIcon(int offs, Icon icon, String tip) throws BadLocationException { return iconArea.addOffsetTrackingIcon(offs, icon, tip); } /** * Clears the active line range. * * @see #setActiveLineRange(int, int) */ private void clearActiveLineRange() { iconArea.clearActiveLineRange(); } /** * Returns the color used to paint the active line range, if any. * * @return The color. * @see #setActiveLineRangeColor(Color) */ public Color getActiveLineRangeColor() { return iconArea.getActiveLineRangeColor(); } /** * 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. * @see #toggleBookmark(int) */ 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 background color used by the (default) fold icons. * * @return The background color. * @see #setFoldBackground(Color) */ public Color getFoldBackground() { return foldIndicator.getFoldIconBackground(); } /** * Returns the foreground color of the fold indicator. * * @return The foreground color of the fold indicator. * @see #setFoldIndicatorForeground(Color) */ public Color getFoldIndicatorForeground() { return foldIndicator.getForeground(); } /** * Returns whether the icon area inherits the gutter background (as opposed * to painting with its own, default "panel" color, which is the default). * * @return Whether the gutter background is used in the icon row header. * @see #setIconRowHeaderInheritsGutterBackground(boolean) */ public boolean getIconRowHeaderInheritsGutterBackground() { return iconRowHeaderInheritsGutterBackground; } /** * Returns the color to use to paint line numbers. * * @return The color used when painting line numbers. * @see #setLineNumberColor(Color) */ public Color getLineNumberColor() { return lineNumberColor; } /** * Returns the font used for line numbers. * * @return The font used for line numbers. * @see #setLineNumberFont(Font) */ public Font getLineNumberFont() { return lineNumberFont; } /** * Returns the starting line's line number. The default value is * <code>1</code>. * * @return The index * @see #setLineNumberingStartIndex(int) */ public int getLineNumberingStartIndex() { return lineNumberingStartIndex; } /** * 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 whether tool tips are displayed showing the contents of * collapsed fold regions when the mouse hovers over a +/- icon. * * @return Whether these tool tips are displayed. * @see #setShowCollapsedRegionToolTips(boolean) */ public boolean getShowCollapsedRegionToolTips() { return foldIndicator.getShowCollapsedRegionToolTips(); } /** * 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 GutterIconInfo[] getTrackingIcons(Point p) throws BadLocationException { int offs = textArea.viewToModel(new Point(0, p.y)); int line = textArea.getLineOfOffset(offs); return iconArea.getTrackingIcons(line); } /** * Returns whether the fold indicator is enabled. * * @return Whether the fold indicator is enabled. * @see #setFoldIndicatorEnabled(boolean) */ public boolean isFoldIndicatorEnabled() { for (int i=0; i<getComponentCount(); i++) { if (getComponent(i)==foldIndicator) { return true; } } return false; } /** * 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 all tracking icons. * * @see #removeTrackingIcon(GutterIconInfo) * @see #addOffsetTrackingIcon(int, Icon) */ public void removeAllTrackingIcons() { iconArea.removeAllTrackingIcons(); } /** * 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); } /** * 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 #DEFAULT_ACTIVE_LINE_RANGE_COLOR */ public void setActiveLineRangeColor(Color color) { iconArea.setActiveLineRangeColor(color); } /** * 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 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} */ @Override 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); } /** * Sets the icons to use to represent collapsed and expanded folds. * * @param collapsedIcon The collapsed fold icon. This cannot be * <code>null</code>. * @param expandedIcon The expanded fold icon. This cannot be * <code>null</code>. */ public void setFoldIcons(Icon collapsedIcon, Icon expandedIcon) { if (foldIndicator!=null) { foldIndicator.setFoldIcons(collapsedIcon, expandedIcon); } } /** * Toggles whether the fold indicator is enabled. * * @param enabled Whether the fold indicator should be enabled. * @see #isFoldIndicatorEnabled() */ public void setFoldIndicatorEnabled(boolean enabled) { if (foldIndicator!=null) { if (enabled) { add(foldIndicator, BorderLayout.LINE_END); } else { remove(foldIndicator); } revalidate(); } } /** * Sets the background color used by the (default) fold icons. * * @param bg The new background color. * @see #getFoldBackground() */ public void setFoldBackground(Color bg) { if (bg==null) { bg = FoldIndicator.DEFAULT_FOLD_BACKGROUND; } foldIndicator.setFoldIconBackground(bg); } /** * Sets the foreground color used by the fold indicator. * * @param fg The new fold indicator foreground. * @see #getFoldIndicatorForeground() */ public void setFoldIndicatorForeground(Color fg) { if (fg==null) { fg = FoldIndicator.DEFAULT_FOREGROUND; } foldIndicator.setForeground(fg); } /** * 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 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. * @see #getIconRowHeaderInheritsGutterBackground() */ public void setIconRowHeaderInheritsGutterBackground(boolean inherits) { if (inherits!=iconRowHeaderInheritsGutterBackground) { iconRowHeaderInheritsGutterBackground = inherits; if (iconArea!=null) { iconArea.setInheritsGutterBackground(inherits); } } } /** * 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) { if (color!=null && !color.equals(lineNumberColor)) { lineNumberColor = color; if (lineNumberList!=null) { 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"); } if (!font.equals(lineNumberFont)) { lineNumberFont = font; if (lineNumberList!=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) { if (index!=lineNumberingStartIndex) { lineNumberingStartIndex = 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(); } } /** * Toggles whether tool tips should be displayed showing the contents of * collapsed fold regions when the mouse hovers over a +/- icon. * * @param show Whether to show these tool tips. * @see #getShowCollapsedRegionToolTips() */ public void setShowCollapsedRegionToolTips(boolean show) { if (foldIndicator!=null) { foldIndicator.setShowCollapsedRegionToolTips(show); } } /** * 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 (textArea!=null) { RTextAreaEditorKit kit = (RTextAreaEditorKit)textArea.getUI(). getEditorKit(textArea); if (lineNumberList==null) { lineNumberList = kit.createLineNumberList(textArea); lineNumberList.setFont(getLineNumberFont()); lineNumberList.setForeground(getLineNumberColor()); lineNumberList.setLineNumberingStartIndex( getLineNumberingStartIndex()); } else { lineNumberList.setTextArea(textArea); } if (iconArea==null) { iconArea = kit.createIconRowHeader(textArea); iconArea.setInheritsGutterBackground( getIconRowHeaderInheritsGutterBackground()); } else { iconArea.setTextArea(textArea); } if (foldIndicator==null) { foldIndicator = new FoldIndicator(textArea); } else { foldIndicator.setTextArea(textArea); } 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. * @see #getBookmarks() */ public boolean toggleBookmark(int line) throws BadLocationException { return iconArea.toggleBookmark(line); } // public void setUI(ComponentUI ui) { // // Border gutterBorder = getBorder(); // // super.setUI(ui); // // // Some LaFs, such as WebLookAndFeel, override borders even when // // they aren't UIResources. // Border border = getBorder(); // if (border != gutterBorder) { // setBorder(gutterBorder); // } // // } // // @Override public void setBorder(Border border) { if (border instanceof GutterBorder) { super.setBorder(border); } } /** * The border used by the gutter. */ private static class GutterBorder extends EmptyBorder { private Color color; private Rectangle visibleRect; public GutterBorder(int top, int left, int bottom, int right) { super(top, left, bottom, right); color = new Color(221, 221, 221); visibleRect = new Rectangle(); } public Color getColor() { return color; } @Override public void paintBorder(Component c, Graphics g, int x, int y, int width, int height) { visibleRect = g.getClipBounds(visibleRect); if (visibleRect==null) { // ??? visibleRect = ((JComponent)c).getVisibleRect(); } g.setColor(color); if (left==1) { g.drawLine(0,visibleRect.y, 0,visibleRect.y+visibleRect.height); } else { g.drawLine(width-1,visibleRect.y, width-1,visibleRect.y+visibleRect.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) {} @Override 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 rsta = (RSyntaxTextArea)textArea; rsta.addActiveLineRangeListener(this); rsta.getFoldManager().addPropertyChangeListener(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(); } } // If they toggle whether code folding is enabled... else if (RSyntaxTextArea.CODE_FOLDING_PROPERTY.equals(name)) { boolean foldingEnabled = ((Boolean)e.getNewValue()). booleanValue(); if (lineNumberList!=null) { // Its size depends on folding //lineNumberList.revalidate(); lineNumberList.updateCellWidths(); } setFoldIndicatorEnabled(foldingEnabled); } // If code folds are updated... else if (FoldManager.PROPERTY_FOLDS_UPDATED.equals(name)) { repaint(); } else if ("document".equals(name)) { // The document switched out from under us RDocument old = (RDocument)e.getOldValue(); if (old != null) { old.removeDocumentListener(this); } RDocument newDoc = (RDocument)e.getNewValue(); if (newDoc != null) { newDoc.addDocumentListener(this); } } } public void removeUpdate(DocumentEvent e) { handleDocumentEvent(e); } public void uninstall() { if (installed) { textArea.removeComponentListener(this); textArea.getDocument().removeDocumentListener(this); textArea.removePropertyChangeListener(this); if (textArea instanceof RSyntaxTextArea) { RSyntaxTextArea rsta = (RSyntaxTextArea)textArea; rsta.removeActiveLineRangeListener(this); rsta.getFoldManager().removePropertyChangeListener(this); } installed = false; } } } }