/* * 02/24/2004 * * SyntaxView.java - The View object used by RSyntaxTextArea when word wrap is * disabled. * Copyright (C) 2004 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.rsyntaxtextarea; import java.awt.*; import javax.swing.event.*; import javax.swing.text.*; /** * The <code>javax.swing.text.View</code> object used by {@link RSyntaxTextArea} when word wrap is disabled. It * implements syntax highlighting for programming languages using the colors and font styles specified by the * <code>RSyntaxTextArea</code>. * <p> * * You don't really have to do anything to use this class, as {@link RSyntaxTextAreaUI} automatically sets the text * area's view to be an instance of this class if word wrap is disabled. * <p> * * The tokens that specify how to paint the syntax-highlighted text are gleaned from the text area's * {@link RSyntaxDocument}. * * @author Robert Futrell * @version 0.3 */ public class SyntaxView extends View implements TabExpander, TokenOrientedView, RSTAView { /** * The default font used by the text area. If this changes we need to recalculate the longest line. */ Font font; /** * Font metrics for the current font. */ protected FontMetrics metrics; /** * The current longest line. This is used to calculate the preferred width of the view. Since the calculation is * potentially expensive, we try to avoid it by stashing which line is currently the longest. */ Element longLine; float longLineWidth; private int tabSize; protected int tabBase; /** * Cached for each paint() call so each drawLine() call has access to it. */ private RSyntaxTextArea host; /** * Cached values to speed up the painting a tad. */ private int lineHeight = 0; private int ascent; private int clipStart; private int clipEnd; // /** // * The end-of-line marker. // */ // private static final char[] eolMarker = { '.' }; /** * Constructs a new <code>SyntaxView</code> wrapped around an element. * * @param elem * The element representing the text to display. */ public SyntaxView(Element elem) { super(elem); } /** * Iterate over the lines represented by the child elements of the element this view represents, looking for the * line that is the longest. The <em>longLine</em> variable is updated to represent the longest line contained. The * <em>font</em> variable is updated to indicate the font used to calculate the longest line. */ void calculateLongestLine() { Component c = getContainer(); font = c.getFont(); metrics = c.getFontMetrics(font); tabSize = getTabSize() * metrics.charWidth(' '); Element lines = getElement(); int n = lines.getElementCount(); for (int i = 0; i < n; i++) { Element line = lines.getElement(i); float w = getLineWidth(i); if (w > longLineWidth) { longLineWidth = w; longLine = line; } } } /** * Gives notification from the document that attributes were changed in a location that this view is responsible * for. * * @param changes * the change information from the associated document * @param a * the current allocation of the view * @param f * the factory to use to rebuild if the view has children * @see View#changedUpdate */ public void changedUpdate(DocumentEvent changes, Shape a, ViewFactory f) { updateDamage(changes, a, f); } /** * Repaint the given line range. * * @param line0 * The starting line number to repaint. This must be a valid line number in the model. * @param line1 * The ending line number to repaint. This must be a valid line number in the model. * @param a * The region allocated for the view to render into. * @param host * The component hosting the view (used to call repaint). */ protected void damageLineRange(int line0, int line1, Shape a, Component host) { if (a != null) { Rectangle area0 = lineToRect(a, line0); Rectangle area1 = lineToRect(a, line1); if ((area0 != null) && (area1 != null)) { Rectangle dmg = area0.union(area1); // damage. host.repaint(dmg.x, dmg.y, dmg.width, dmg.height); } else host.repaint(); } } /** * Draws the passed-in text using syntax highlighting for the current language. The tokens used to decide how to * paint the syntax highlighting are grabbed from the text area's document. * * @param token * The list of tokens to draw. * @param g * The graphics context in which to draw. * @param x * The x-coordinate at which to draw. * @param y * The y-coordinate at which to draw. * @return The x-coordinate representing the end of the painted text. */ public float drawLine(Token token, Graphics2D g, float x, float y) { float nextX = x; // The x-value at the end of our text. while (token != null && token.isPaintable() && nextX < clipEnd) { nextX = token.paint(g, nextX, y, host, this, clipStart); token = token.getNextToken(); } // NOTE: We should re-use code from Token (paintBackground()) here, // but don't because I'm just too lazy. if (host.getEOLMarkersVisible()) { g.setColor(host.getForegroundForTokenType(Token.WHITESPACE)); g.setFont(host.getFontForTokenType(Token.WHITESPACE)); g.drawString("\u00B6", nextX, y); } // Return the x-coordinate at the end of the painted text. return nextX; } /** * Calculates the width of the line represented by the given element. * * @param line * The line for which to get the length. * @param lineNumber * The line number of the specified line in the document. * @return The width of the line. */ private float getLineWidth(int lineNumber) { Token tokenList = ((RSyntaxDocument) getDocument()). getTokenListForLine(lineNumber); return RSyntaxUtilities.getTokenListWidth(tokenList, (RSyntaxTextArea) getContainer(), this); } /** * Provides a way to determine the next visually represented model location that one might place a caret. Some views * may not be visible, they might not be in the same order found in the model, or they just might not allow access * to some of the locations in the model. * * @param pos * the position to convert >= 0 * @param a * the allocated region to render into * @param direction * the direction from the current position that can be thought of as the arrow keys typically found on a * keyboard. This may be SwingConstants.WEST, SwingConstants.EAST, SwingConstants.NORTH, or * SwingConstants.SOUTH. * @return the location within the model that best represents the next location visual position. * @exception BadLocationException * @exception IllegalArgumentException * for an invalid direction */ public int getNextVisualPositionFrom(int pos, Position.Bias b, Shape a, int direction, Position.Bias[] biasRet) throws BadLocationException { return RSyntaxUtilities.getNextVisualPositionFrom(pos, b, a, direction, biasRet, this); } /** * Determines the preferred span for this view along an axis. * * @param axis * may be either View.X_AXIS or View.Y_AXIS * @return the span the view would like to be rendered into >= 0. Typically the view is told to render into the span * that is returned, although there is no guarantee. The parent may choose to resize or break the view. * @exception IllegalArgumentException * for an invalid axis */ public float getPreferredSpan(int axis) { updateMetrics(); switch (axis) { case View.X_AXIS: float span = longLineWidth + 10; // "fudge factor." if (host.getEOLMarkersVisible()) { span += metrics.charWidth('\u00B6'); } return span; case View.Y_AXIS: // We update lineHeight here as when this method is first // called, lineHeight isn't initialized. If we don't do it // here, we get no vertical scrollbar (as lineHeight==0). lineHeight = host != null ? host.getLineHeight() : lineHeight; return getElement().getElementCount() * lineHeight; default: throw new IllegalArgumentException("Invalid axis: " + axis); } } /** * Returns the tab size set for the document, defaulting to 5. * * @return The tab size. */ protected int getTabSize() { Integer i = (Integer) getDocument().getProperty( PlainDocument.tabSizeAttribute); int size = (i != null) ? i.intValue() : 5; return size; } /** * Returns a token list for the <i>physical</i> line above the physical line containing the specified offset into * the document. Note that for this plain (non-wrapped) view, this is simply the token list for the logical line * above the line containing <code>offset</code>, since lines are not wrapped. * * @param offset * The offset in question. * @return A token list for the physical (and in this view, logical) line before this one. If <code>offset</code> is * in the first line in the document, <code>null</code> is returned. */ public Token getTokenListForPhysicalLineAbove(int offset) { RSyntaxDocument document = (RSyntaxDocument) getDocument(); Element map = document.getDefaultRootElement(); int line = map.getElementIndex(offset) - 1; if (line >= 0) return document.getTokenListForLine(line); return null; } /** * Returns a token list for the <i>physical</i> line below the physical line containing the specified offset into * the document. Note that for this plain (non-wrapped) view, this is simply the token list for the logical line * below the line containing <code>offset</code>, since lines are not wrapped. * * @param offset * The offset in question. * @return A token list for the physical (and in this view, logical) line after this one. If <code>offset</code> is * in the last physical line in the document, <code>null</code> is returned. */ public Token getTokenListForPhysicalLineBelow(int offset) { RSyntaxDocument document = (RSyntaxDocument) getDocument(); Element map = document.getDefaultRootElement(); int line = map.getElementIndex(offset); int lineCount = map.getElementCount(); if (line < lineCount - 1) return document.getTokenListForLine(line + 1); return null; } /** * Gives notification that something was inserted into the document in a location that this view is responsible for. * * @param changes * The change information from the associated document. * @param a * The current allocation of the view. * @param f * The factory to use to rebuild if the view has children. */ public void insertUpdate(DocumentEvent changes, Shape a, ViewFactory f) { updateDamage(changes, a, f); } /** * Determine the rectangle that represents the given line. * * @param a * The region allocated for the view to render into * @param line * The line number to find the region of. This must be a valid line number in the model. */ protected Rectangle lineToRect(Shape a, int line) { Rectangle r = null; updateMetrics(); if (metrics != null) { Rectangle alloc = a.getBounds(); // NOTE: lineHeight is not initially set here, leading to the // current line not being highlighted when a document is first // opened. So, we set it here just in case. lineHeight = host != null ? host.getLineHeight() : lineHeight; r = new Rectangle(alloc.x, alloc.y + line * lineHeight, alloc.width, lineHeight); } return r; } /** * Provides a mapping from the document model coordinate space to the coordinate space of the view mapped to it. * * @param pos * the position to convert >= 0 * @param a * the allocated region to render into * @return the bounding box of the given position * @exception BadLocationException * if the given position does not represent a valid location in the associated document * @see View#modelToView */ public Shape modelToView(int pos, Shape a, Position.Bias b) throws BadLocationException { // line coordinates Element map = getElement(); RSyntaxDocument doc = (RSyntaxDocument) getDocument(); int lineIndex = map.getElementIndex(pos); Rectangle lineArea = lineToRect(a, lineIndex); tabBase = lineArea.x; // Used by listOffsetToView(). Token tokenList = doc.getTokenListForLine(lineIndex); // int x = (int)RSyntaxUtilities.getTokenListWidthUpTo(tokenList, // (RSyntaxTextArea)getContainer(), // this, 0, pos); // We use this method instead as it returns the actual bounding box, // not just the x-coordinate. lineArea = tokenList.listOffsetToView( (RSyntaxTextArea) getContainer(), this, pos, tabBase, lineArea); return lineArea; } /** * Provides a mapping, for a given region, from the document model coordinate space to the view coordinate space. * The specified region is created as a union of the first and last character positions. * <p> * * This is implemented to subtract the width of the second character, as this view's <code>modelToView</code> * actually returns the width of the character instead of "1" or "0" like the View implementations in * <code>javax.swing.text</code>. Thus, if we don't override this method, the <code>View</code> implementation will * return one character's width too much for its consumers (implementations of * <code>javax.swing.text.Highlighter</code>). * * @param p0 * the position of the first character (>=0) * @param b0 * The bias of the first character position, toward the previous character or the next character * represented by the offset, in case the position is a boundary of two views; <code>b0</code> will have * one of these values: * <ul> * <li> <code>Position.Bias.Forward</code> <li> <code>Position.Bias.Backward</code> * </ul> * @param p1 * the position of the last character (>=0) * @param b1 * the bias for the second character position, defined one of the legal values shown above * @param a * the area of the view, which encompasses the requested region * @return the bounding box which is a union of the region specified by the first and last character positions * @exception BadLocationException * if the given position does not represent a valid location in the associated document * @exception IllegalArgumentException * if <code>b0</code> or <code>b1</code> are not one of the legal <code>Position.Bias</code> values * listed above * @see View#viewToModel */ public Shape modelToView(int p0, Position.Bias b0, int p1, Position.Bias b1, Shape a) throws BadLocationException { Shape s0 = modelToView(p0, a, b0); Shape s1; if (p1 == getEndOffset()) { try { s1 = modelToView(p1, a, b1); } catch (BadLocationException ble) { s1 = null; } if (s1 == null) { // Assume extends left to right. Rectangle alloc = (a instanceof Rectangle) ? (Rectangle) a : a.getBounds(); s1 = new Rectangle(alloc.x + alloc.width - 1, alloc.y, 1, alloc.height); } } else { s1 = modelToView(p1, a, b1); } Rectangle r0 = s0.getBounds(); Rectangle r1 = (s1 instanceof Rectangle) ? (Rectangle) s1 : s1.getBounds(); if (r0.y != r1.y) { // If it spans lines, force it to be the width of the view. Rectangle alloc = (a instanceof Rectangle) ? (Rectangle) a : a.getBounds(); r0.x = alloc.x; r0.width = alloc.width; } r0.add(r1); // The next line is the only difference between this method and // View's implementation. We're subtracting the width of the second // character. This is because this method is used by Highlighter // implementations to get the area to "highlight", and if we don't do // this, one character too many is highlighted thanks to our // modelToView() implementation returning the actual width of the // character requested! if (p1 > p0) r0.width -= r1.width; return r0; } /** * Returns the next tab stop position after a given reference position. This implementation does not support things * like centering so it ignores the tabOffset argument. * * @param x * the current position >= 0 * @param tabOffset * the position within the text stream that the tab occurred at >= 0. * @return the tab stop, measured in points >= 0 */ public float nextTabStop(float x, int tabOffset) { if (tabSize == 0) return x; int ntabs = (((int) x) - tabBase) / tabSize; return tabBase + ((ntabs + 1) * tabSize); } /** * Actually paints the text area. Only lines that have been damaged are repainted. * * @param g * The graphics context with which to paint. * @param a * The allocated region in which to render. * @see #drawLine */ public void paint(Graphics g, Shape a) { RSyntaxDocument document = (RSyntaxDocument) getDocument(); Rectangle alloc = a.getBounds(); tabBase = alloc.x; host = (RSyntaxTextArea) getContainer(); Rectangle clip = g.getClipBounds(); // An attempt to speed things up for files with long lines. Note that // this will actually slow things down a tad for the common case of // regular-length lines, but I don't think it'll make a difference // visually. We'll see... clipStart = clip.x; clipEnd = clipStart + clip.width; lineHeight = host.getLineHeight(); ascent = host.getMaxAscent();// metrics.getAscent(); int heightBelow = (alloc.y + alloc.height) - (clip.y + clip.height); int linesBelow = Math.max(0, heightBelow / lineHeight); int heightAbove = clip.y - alloc.y; int linesAbove = Math.max(0, heightAbove / lineHeight); int linesTotal = alloc.height / lineHeight; if (alloc.height % lineHeight != 0) { linesTotal++; } Rectangle lineArea = lineToRect(a, linesAbove); int y = lineArea.y + ascent; int x = lineArea.x; Element map = getElement(); int lineCount = map.getElementCount(); int endLine = Math.min(lineCount, linesTotal - linesBelow); RSyntaxTextAreaHighlighter h = (RSyntaxTextAreaHighlighter) host.getHighlighter(); Graphics2D g2d = (Graphics2D) g; Token token; // System.err.println("Painting lines: " + linesAbove + " to " + (endLine-1)); for (int line = linesAbove; line < endLine; line++) { Element lineElement = map.getElement(line); int startOffset = lineElement.getStartOffset(); // int endOffset = (line==lineCount ? lineElement.getEndOffset()-1 : // lineElement.getEndOffset()-1); int endOffset = lineElement.getEndOffset() - 1; // Why always "-1"? h.paintLayeredHighlights(g2d, startOffset, endOffset, a, host, this); // Paint a line of text. token = document.getTokenListForLine(line); drawLine(token, g2d, x, y); y += lineHeight; } } /** * If the passed-in line is longer than the current longest line, then the longest line is updated. * * @param line * The line to test against the current longest. * @param lineNumber * The line number of the passed-in line. * @return <code>true</code> iff the current longest line was updated. */ protected boolean possiblyUpdateLongLine(Element line, int lineNumber) { float w = getLineWidth(lineNumber); if (w > longLineWidth) { longLineWidth = w; longLine = line; return true; } return false; } /** * Gives notification that something was removed from the document in a location that this view is responsible for. * * @param changes * the change information from the associated document * @param a * the current allocation of the view * @param f * the factory to use to rebuild if the view has children */ public void removeUpdate(DocumentEvent changes, Shape a, ViewFactory f) { updateDamage(changes, a, f); } public void setSize(float width, float height) { super.setSize(width, height); updateMetrics(); } /** * Repaint the region of change covered by the given document event. Damages the line that begins the range to cover * the case when the insert/remove is only on one line. If lines are added or removed, damages the whole view. The * longest line is checked to see if it has changed. */ protected void updateDamage(DocumentEvent changes, Shape a, ViewFactory f) { Component host = getContainer(); updateMetrics(); Element elem = getElement(); DocumentEvent.ElementChange ec = changes.getChange(elem); Element[] added = (ec != null) ? ec.getChildrenAdded() : null; Element[] removed = (ec != null) ? ec.getChildrenRemoved() : null; if (((added != null) && (added.length > 0)) || ((removed != null) && (removed.length > 0))) { // lines were added or removed... if (added != null) { int addedAt = ec.getIndex(); // FIXME: Is this correct????? for (int i = 0; i < added.length; i++) possiblyUpdateLongLine(added[i], addedAt + i); } if (removed != null) { for (int i = 0; i < removed.length; i++) { if (removed[i] == longLine) { longLineWidth = -1; // Must do this!! calculateLongestLine(); break; } } } preferenceChanged(null, true, true); host.repaint(); } // This occurs when syntax highlighting only changes on lines // (i.e. beginning a multiline comment). else if (changes.getType() == DocumentEvent.EventType.CHANGE) { // System.err.println("Updating the damage due to a CHANGE event..."); int startLine = changes.getOffset(); int endLine = changes.getLength(); damageLineRange(startLine, endLine, a, host); } else { Element map = getElement(); int line = map.getElementIndex(changes.getOffset()); damageLineRange(line, line, a, host); if (changes.getType() == DocumentEvent.EventType.INSERT) { // check to see if the line is longer than current // longest line. Element e = map.getElement(line); if (e == longLine) { // We must recalculate longest line's width here // because it has gotten longer. longLineWidth = getLineWidth(line); preferenceChanged(null, true, false); } else { // If long line gets updated, update the status bars too. if (possiblyUpdateLongLine(e, line)) preferenceChanged(null, true, false); } } else if (changes.getType() == DocumentEvent.EventType.REMOVE) { if (map.getElement(line) == longLine) { // removed from longest line... recalc longLineWidth = -1; // Must do this! calculateLongestLine(); preferenceChanged(null, true, false); } } } } /** * Checks to see if the font metrics and longest line are up-to-date. */ protected void updateMetrics() { host = (RSyntaxTextArea) getContainer(); Font f = host.getFont(); if (font != f) { // The font changed, we need to recalculate the longest line! // This also updates cached font and tab size. calculateLongestLine(); } } /** * Provides a mapping from the view coordinate space to the logical coordinate space of the model. * * @param fx * the X coordinate >= 0 * @param fy * the Y coordinate >= 0 * @param a * the allocated region to render into * @return the location within the model that best represents the given point in the view >= 0 */ public int viewToModel(float fx, float fy, Shape a, Position.Bias[] bias) { bias[0] = Position.Bias.Forward; Rectangle alloc = a.getBounds(); RSyntaxDocument doc = (RSyntaxDocument) getDocument(); int x = (int) fx; int y = (int) fy; // If they're asking about a view position above the area covered by // this view, then the position is assumed to be the starting position // of this view. if (y < alloc.y) { return getStartOffset(); } // If they're asking about a position below this view, the position // is assumed to be the ending position of this view. else if (y > alloc.y + alloc.height) { return getEndOffset() - 1; } // They're asking about a position within the coverage of this view // vertically. So, we figure out which line the point corresponds to. // If the line is greater than the number of lines contained, then // simply use the last line as it represents the last possible place // we can position to. else { Element map = doc.getDefaultRootElement(); int lineIndex = Math.abs((y - alloc.y) / lineHeight);// metrics.getHeight() ); if (lineIndex >= map.getElementCount()) return getEndOffset() - 1; Element line = map.getElement(lineIndex); // If the point is to the left of the line... if (x < alloc.x) return line.getStartOffset(); // If the point is to the right of the line... else if (x > alloc.x + alloc.width) return line.getEndOffset() - 1; else { // Determine the offset into the text int p0 = line.getStartOffset(); Token tokenList = doc.getTokenListForLine(lineIndex); tabBase = alloc.x; int offs = tokenList.getListOffset( (RSyntaxTextArea) getContainer(), this, tabBase, x); return offs != -1 ? offs : p0; } } // End of else. } /** * {@inheritDoc} */ public int yForLineContaining(Rectangle alloc, int offs) throws BadLocationException { // line coordinates Element map = getElement(); int line = map.getElementIndex(offs); // Rectangle lineArea = lineToRect(alloc, lineIndex); updateMetrics(); if (metrics != null) { // NOTE: lineHeight is not initially set here, leading to the // current line not being highlighted when a document is first // opened. So, we set it here just in case. lineHeight = host != null ? host.getLineHeight() : lineHeight; return alloc.y + line * lineHeight; } return -1; } }