/* * 02/17/2009 * * IconRowHeader.java - Renders icons in the gutter. * 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.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.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; /** * Renderes icons in the {@link Gutter}. * * @author Robert Futrell * @version 1.0 */ class IconRowHeader extends AbstractGutterComponent implements MouseListener { /** * The icons to render. */ private List trackingIcons; /** * The width of this component. */ private 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; /** * The icon to use for bookmarks. */ private Icon currentLineIcon; /** * Used in {@link #paintComponent(Graphics)} to prevent reallocation on each paint. */ private Rectangle visibleRect; /** * Used in {@link #paintComponent(Graphics)} to prevent reallocation on each paint. */ private Insets textAreaInsets; /** * The first line in the active line range. */ private int activeLineRangeStart; /** * The end line in the active line range. */ private int activeLineRangeEnd; /** * The color used to highlight the active code block. */ private Color activeLineRangeColor = new Color(51, 153, 255); /** * Constructor. * * @param textArea * The parent text area. */ public IconRowHeader(RTextArea textArea) { super(textArea); visibleRect = new Rectangle(); width = 16; addMouseListener(this); activeLineRangeStart = activeLineRangeEnd = -1; // Must explicitly set our background color, otherwise we inherit that // of the parent Gutter. updateBackground(); } /** * 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 { Position pos = textArea.getDocument().createPosition(offs); GutterIconImpl ti = new GutterIconImpl(icon, pos); 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 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; } /** * @return the currentLineIcon */ public Icon getCurrentLineIcon() { return currentLineIcon; } /** * @param currentLineIcon * the currentLineIcon to set */ public void setCurrentLineIcon(Icon currentLineIcon) { this.currentLineIcon = currentLineIcon; } /** * @return the trackingIcons */ @SuppressWarnings("unchecked") public GutterIconInfo[] getAllTrackingIcons() { List<GutterIconImpl> retVal = new ArrayList<GutterIconImpl>(); if (trackingIcons != null) { for (int i = 0; i < trackingIcons.size(); i++) { GutterIconImpl ti = getTrackingIcon(i); retVal.add(ti); } } Collections.sort(retVal); GutterIconInfo[] array = new GutterIconInfo[retVal.size()]; return (GutterIconInfo[]) retVal.toArray(array); } /** * Returns the bookmarks known to this gutter. * * @return The bookmarks. If there are no bookmarks, an empty array is returned. */ @SuppressWarnings("unchecked") public GutterIconInfo[] getBookmarks() { List<GutterIconImpl> retVal = new ArrayList<GutterIconImpl>(); if (trackingIcons != null) { for (int i = 0; i < trackingIcons.size(); i++) { GutterIconImpl ti = getTrackingIcon(i); if (ti.getIcon() == bookmarkIcon) { retVal.add(ti); } } } Collections.sort(retVal); GutterIconInfo[] array = new GutterIconInfo[retVal.size()]; return (GutterIconInfo[]) retVal.toArray(array); } /** * {@inheritDoc} */ void handleDocumentEvent(DocumentEvent e) { int newLineCount = textArea.getLineCount(); if (newLineCount != currentLineCount) { currentLineCount = newLineCount; repaint(); } } /** * {@inheritDoc} */ public Dimension getPreferredSize() { int h = textArea != null ? textArea.getHeight() : 100; // Arbitrary return new Dimension(width, h); } private GutterIconImpl getTrackingIcon(int index) { return (GutterIconImpl) 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 GutterIconImpl[] 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 < 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 } } } GutterIconImpl[] array = new GutterIconImpl[retVal.size()]; return (GutterIconImpl[]) retVal.toArray(array); } /** * Returns whether bookmarking is enabled. * * @return Whether bookmarking is enabled. * @see #setBookmarkingEnabled(boolean) */ public boolean isBookmarkingEnabled() { return bookmarkingEnabled; } /** * {@inheritDoc} */ void lineHeightsChanged() { repaint(); } public void mouseClicked(MouseEvent e) { } public void mouseEntered(MouseEvent e) { } public void mouseExited(MouseEvent e) { } public void mousePressed(MouseEvent e) { if (e.getClickCount() == 2) { if (bookmarkingEnabled && bookmarkIcon != null) { try { int offs = textArea.viewToModel(e.getPoint()); if (offs > -1) { int line = textArea.getLineOfOffset(offs); toggleBookmark(line); } } catch (BadLocationException ble) { ble.printStackTrace(); // Never happens } } } } public void mouseReleased(MouseEvent e) { } /** * {@inheritDoc} */ 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; } g.setColor(getBackground()); g.fillRect(0, visibleRect.y, width, visibleRect.height); if (textArea.getLineWrap()) { paintComponentWrapped(g); return; } Document doc = textArea.getDocument(); Element root = doc.getDefaultRootElement(); // Get the first and last lines to paint. int cellHeight = textArea.getLineHeight(); int topLine = visibleRect.y / cellHeight; int bottomLine = Math.min(topLine + visibleRect.height / cellHeight, 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 = textArea.getInsets(textAreaInsets); if (textAreaInsets != null) { y += 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; // int lastLine = Math.min(activeLineRangeEnd, bottomLine); // int y2 = (lastLine + 1) * cellHeight; // g.fillRect(0, y1, getWidth(), y2-y1); Icon icon = getCurrentLineIcon(); if (icon != null) { int y3 = y1 + (cellHeight - icon.getIconHeight()) / 2; icon.paintIcon(this, g, 0, y3); } // 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.setColor(activeLineRangeColor); // g.drawLine(0, y1, getWidth(), y1); // } // if (lastLine == activeLineRangeEnd) { // g.setColor(activeLineRangeColor); // g.drawLine(0, y2, getWidth(), y2); // } } if (trackingIcons != null) { int lastLine = bottomLine; for (int i = trackingIcons.size() - 1; i >= 0; i--) { // Last to first GutterIconImpl ti = getTrackingIcon(i); int offs = ti.getMarkedOffset(); if (offs >= 0 && offs <= doc.getLength()) { int line = root.getElementIndex(offs); if (line <= lastLine && line > topLine - 1) { 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 - 1) { break; } } } } 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; // int lastLine = Math.min(activeLineRangeEnd, bottomLine); // int y2 = (lastLine + 1) * cellHeight; // g.fillRect(0, y1, getWidth(), y2-y1); Icon icon = getCurrentLineIcon(); if (icon != null) { int y3 = y1 + (cellHeight - icon.getIconHeight()) / 2; icon.paintIcon(this, g, 0, y3); } // 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.setColor(activeLineRangeColor); // g.drawLine(0, y1, getWidth(), y1); // } // if (lastLine == activeLineRangeEnd) { // g.setColor(activeLineRangeColor); // g.drawLine(0, y2, getWidth(), y2); // } } } /** * 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 = LineNumberList.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(); } } /** * 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(); } } /** * 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 i = trackingIcons.iterator(); i.hasNext();) { GutterIconImpl ti = (GutterIconImpl) i.next(); if (ti.getIcon() == bookmarkIcon) { i.remove(); } } } } /** * 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 the text area being displayed. This will clear any tracking icons currently displayed. * * @param textArea * The text area. */ 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 setCurrentLine(int line) throws BadLocationException { return false; } /** * 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; } GutterIconImpl[] 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} */ public void updateUI() { super.updateUI(); // Does nothing updateBackground(); } /** * Implementation of the icons rendered. * * @author Robert Futrell * @version 1.0 */ private static class GutterIconImpl implements GutterIconInfo, Comparable { private Icon icon; private Position pos; public GutterIconImpl(Icon icon, Position pos) { this.icon = icon; this.pos = pos; } public int compareTo(Object o) { if (o instanceof GutterIconImpl) { return pos.getOffset() - ((GutterIconImpl) o).getMarkedOffset(); } return -1; } public boolean equals(Object o) { return o == this; } public Icon getIcon() { return icon; } public int getMarkedOffset() { return pos.getOffset(); } public int hashCode() { return icon.hashCode(); // FindBugs } } }