/* * 08/06/2004 * * WrappedSyntaxView.java - Test implementation of WrappedSyntaxView that * is also aware of RSyntaxTextArea's different fonts per token type. * * This library is distributed under a modified BSD license. See the included * RSyntaxTextArea.License.txt file for details. */ package org.fife.ui.rsyntaxtextarea; import java.awt.*; import javax.swing.text.*; import javax.swing.text.Position.Bias; import javax.swing.event.*; import org.fife.ui.rsyntaxtextarea.TokenUtils.TokenSubList; import org.fife.ui.rsyntaxtextarea.folding.Fold; import org.fife.ui.rsyntaxtextarea.folding.FoldManager; import org.fife.ui.rtextarea.Gutter; /** * The view used by {@link RSyntaxTextArea} when word wrap is enabled. * * @author Robert Futrell * @version 0.2 */ public class WrappedSyntaxView extends BoxView implements TabExpander, RSTAView { boolean widthChanging; int tabBase; int tabSize; /** * This is reused to keep from allocating/deallocating. */ private Segment s, drawSeg; /** * Another variable initialized once to keep from allocating/deallocating. */ private Rectangle tempRect; /** * Cached for each paint() call so each drawView() call has access to it. */ private RSyntaxTextArea host; private FontMetrics metrics; private TokenImpl tempToken; private TokenImpl lineCountTempToken; // /** // * The end-of-line marker. // */ // private static final char[] eolMarker = { '.' }; /** * The width of this view cannot be below this amount, as if the width * is ever 0 (really a bug), we'll go into an infinite loop. */ private static final int MIN_WIDTH = 20; /** * Creates a new WrappedSyntaxView. Lines will be wrapped * on character boundaries. * * @param elem the element underlying the view */ public WrappedSyntaxView(Element elem) { super(elem, Y_AXIS); tempToken = new TokenImpl(); s = new Segment(); drawSeg = new Segment(); tempRect = new Rectangle(); lineCountTempToken = new TokenImpl(); } /** * This is called by the nested wrapped line * views to determine the break location. This can * be reimplemented to alter the breaking behavior. * It will either break at word or character boundaries * depending upon the break argument given at * construction. */ protected int calculateBreakPosition(int p0, Token tokenList, float x0) { //System.err.println("------ beginning calculateBreakPosition() --------"); int p = p0; RSyntaxTextArea textArea = (RSyntaxTextArea)getContainer(); float currentWidth = getWidth(); if (currentWidth==Integer.MAX_VALUE) currentWidth = getPreferredSpan(X_AXIS); // Make sure width>0; this is a huge hack to fix a bug where // loading text into an RTextArea before it is visible if word wrap // is enabled causes an infinite loop in calculateBreakPosition() // because of the 0-width! We cannot simply check in setSize() // because the width is set to 0 somewhere else too somehow... currentWidth = Math.max(currentWidth, MIN_WIDTH); Token t = tokenList; while (t!=null && t.isPaintable()) { // FIXME: Replace the code below with the commented-out line below. This will // allow long tokens to be broken at embedded spaces (such as MLC's). But it // currently throws BadLocationExceptions sometimes... float tokenWidth = t.getWidth(textArea, this, x0); if (tokenWidth>currentWidth) { // If the current token alone is too long for this line, // break at a character boundary. if (p==p0) { return t.getOffsetBeforeX(textArea, this, 0, currentWidth); } // Return the first non-whitespace char (i.e., don't start // off the continuation of a wrapped line with whitespace). return t.isWhitespace() ? p+t.length() : p; //return getBreakLocation(t, fm, x0, currentWidth, this); } currentWidth -= tokenWidth; x0 += tokenWidth; p += t.length(); //System.err.println("*** *** *** token fit entirely (width==" + tokenWidth + "), adding " + t.textCount + " to p, now p==" + p); t = t.getNextToken(); } //System.err.println("... ... whole line fits; returning p==" + p); //System.err.println("------ ending calculateBreakPosition() --------"); // return p; return p + 1; } //private int getBreakLocation(Token t, FontMetrics fm, int x0, int x, // TabExpander e) { // Segment s = new Segment(); // s.array = t.text; // s.offset = t.getTextOffset(); // s.count = t.textCount; // return t.offset + Utilities.getBreakLocation(s, fm, x0, x, e, t.offset); //} /** * Gives notification from the document that attributes were changed * in a location that this view is responsible for. * * @param e 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 */ @Override public void changedUpdate(DocumentEvent e, Shape a, ViewFactory f) { updateChildren(e, a); } /** * Sets the allocation rectangle for a given line's view, but sets the * y value to the passed-in value. This should be used instead of * {@link #childAllocation(int, Rectangle)} since it allows you to account * for hidden lines in collapsed fold regions. * * @param line * @param y * @param alloc */ private void childAllocation2(int line, int y, Rectangle alloc) { alloc.x += getOffset(X_AXIS, line); alloc.y += y; alloc.width = getSpan(X_AXIS, line); alloc.height = getSpan(Y_AXIS, line); // FIXME: This is required due to a bug that I can't track down. The // top margin is being added twice somewhere in wrapped views, so we // have to adjust for it here. Insets margin = host.getMargin(); if (margin!=null) { alloc.y -= margin.top; } } /** * Draws a single view (i.e., a line of text for a wrapped view), * wrapping the text onto multiple lines if necessary. * * @param painter The painter to use to render tokens. * @param g The graphics context in which to paint. * @param r The rectangle in which to paint. * @param view The <code>View</code> to paint. * @param fontHeight The height of the font being used. * @param y The y-coordinate at which to begin painting. */ protected void drawView(TokenPainter painter, Graphics2D g, Rectangle r, View view, int fontHeight, int y, int line) { float x = r.x; LayeredHighlighter h = (LayeredHighlighter)host.getHighlighter(); RSyntaxDocument document = (RSyntaxDocument)getDocument(); Element map = getElement(); int p0 = view.getStartOffset(); int lineNumber = map.getElementIndex(p0); int p1 = view.getEndOffset();// - 1; setSegment(p0,p1-1, document, drawSeg); //System.err.println("drawSeg=='" + drawSeg + "' (p0/p1==" + p0 + "/" + p1 + ")"); int start = p0 - drawSeg.offset; Token token = document.getTokenListForLine(lineNumber); // If this line is an empty line, then the token list is simply a // null token. In this case, the line highlight will be skipped in // the loop below, so unfortunately we must manually do it here. if (token!=null && token.getType()==Token.NULL) { h.paintLayeredHighlights(g, p0,p1, r, host, this); return; } // Loop through all tokens in this view and paint them! while (token!=null && token.isPaintable()) { int p = calculateBreakPosition(p0, token, x); x = r.x; h.paintLayeredHighlights(g, p0,p, r, host, this); while (token!=null && token.isPaintable() && token.getEndOffset()-1<p) {//<=p) { boolean paintBG = host.getPaintTokenBackgrounds(line, y); x = painter.paint(token, g, x,y, host, this, 0, paintBG); token = token.getNextToken(); } if (token!=null && token.isPaintable() && token.getOffset()<p) { int tokenOffset = token.getOffset(); tempToken.set(drawSeg.array, tokenOffset-start, p-1-start, tokenOffset, token.getType()); tempToken.setLanguageIndex(token.getLanguageIndex()); boolean paintBG = host.getPaintTokenBackgrounds(line, y); painter.paint(tempToken, g, x,y, host, this, 0, paintBG); tempToken.copyFrom(token); tempToken.makeStartAt(p); token = new TokenImpl(tempToken); } p0 = (p==p0) ? p1 : p; y += fontHeight; } // End of while (token!=null && token.isPaintable()). // 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", x, y-fontHeight); } } /** * Draws a single view (i.e., a line of text for a wrapped view), wrapping * the text onto multiple lines if necessary. Any selected text is * rendered with the editor's "selected text" color. * * @param painter The painter to use to render tokens. * @param g The graphics context in which to paint. * @param r The rectangle in which to paint. * @param view The <code>View</code> to paint. * @param fontHeight The height of the font being used. * @param y The y-coordinate at which to begin painting. * @param selStart The start of the selection. * @param selEnd The end of the selection. */ protected void drawViewWithSelection(TokenPainter painter, Graphics2D g, Rectangle r, View view, int fontHeight, int y, int selStart, int selEnd) { float x = r.x; boolean useSTC = host.getUseSelectedTextColor(); LayeredHighlighter h = (LayeredHighlighter)host.getHighlighter(); RSyntaxDocument document = (RSyntaxDocument)getDocument(); Element map = getElement(); int p0 = view.getStartOffset(); int lineNumber = map.getElementIndex(p0); int p1 = view.getEndOffset();// - 1; setSegment(p0,p1-1, document, drawSeg); //System.err.println("drawSeg=='" + drawSeg + "' (p0/p1==" + p0 + "/" + p1 + ")"); int start = p0 - drawSeg.offset; Token token = document.getTokenListForLine(lineNumber); // If this line is an empty line, then the token list is simply a // null token. In this case, the line highlight will be skipped in // the loop below, so unfortunately we must manually do it here. if (token!=null && token.getType()==Token.NULL) { h.paintLayeredHighlights(g, p0,p1, r, host, this); return; } // Loop through all tokens in this view and paint them! while (token!=null && token.isPaintable()) { int p = calculateBreakPosition(p0, token, x); x = r.x; h.paintLayeredHighlights(g, p0,p, r, host, this); while (token!=null && token.isPaintable() && token.getEndOffset()-1<p) {//<=p) { // Selection starts in this token if (token.containsPosition(selStart)) { if (selStart>token.getOffset()) { tempToken.copyFrom(token); tempToken.textCount = selStart - tempToken.getOffset(); x = painter.paint(tempToken,g,x,y,host, this); tempToken.textCount = token.length(); tempToken.makeStartAt(selStart); // Clone required since token and tempToken must be // different tokens for else statement below token = new TokenImpl(tempToken); } int selCount = Math.min(token.length(), selEnd-token.getOffset()); if (selCount==token.length()) { x = painter.paintSelected(token, g, x,y, host, this, useSTC); } else { tempToken.copyFrom(token); tempToken.textCount = selCount; x = painter.paintSelected(tempToken, g, x,y, host, this, useSTC); tempToken.textCount = token.length(); tempToken.makeStartAt(token.getOffset() + selCount); token = tempToken; x = painter.paint(token, g, x,y, host, this); } } // Selection ends in this token else if (token.containsPosition(selEnd)) { tempToken.copyFrom(token); tempToken.textCount = selEnd - tempToken.getOffset(); x = painter.paintSelected(tempToken, g, x,y, host, this, useSTC); tempToken.textCount = token.length(); tempToken.makeStartAt(selEnd); token = tempToken; x = painter.paint(token, g, x,y, host, this); } // This token is entirely selected else if (token.getOffset()>=selStart && token.getEndOffset()<=selEnd) { x = painter.paintSelected(token, g, x,y, host, this,useSTC); } // This token is entirely unselected else { x = painter.paint(token, g, x,y, host, this); } token = token.getNextToken(); } // If there's a token that's going to be split onto the next line if (token!=null && token.isPaintable() && token.getOffset()<p) { int tokenOffset = token.getOffset(); Token orig = token; token = new TokenImpl(drawSeg, tokenOffset-start, p-1-start, tokenOffset, token.getType(), token.getLanguageIndex()); token.setLanguageIndex(token.getLanguageIndex()); // Selection starts in this token if (token.containsPosition(selStart)) { if (selStart>token.getOffset()) { tempToken.copyFrom(token); tempToken.textCount = selStart - tempToken.getOffset(); x = painter.paint(tempToken,g,x,y,host, this); tempToken.textCount = token.length(); tempToken.makeStartAt(selStart); // Clone required since token and tempToken must be // different tokens for else statement below token = new TokenImpl(tempToken); } int selCount = Math.min(token.length(), selEnd-token.getOffset()); if (selCount==token.length()) { x = painter.paintSelected(token, g, x,y, host, this, useSTC); } else { tempToken.copyFrom(token); tempToken.textCount = selCount; x = painter.paintSelected(tempToken, g, x,y, host, this, useSTC); tempToken.textCount = token.length(); tempToken.makeStartAt(token.getOffset() + selCount); token = tempToken; x = painter.paint(token, g, x,y, host, this); } } // Selection ends in this token else if (token.containsPosition(selEnd)) { tempToken.copyFrom(token); tempToken.textCount = selEnd - tempToken.getOffset(); x = painter.paintSelected(tempToken, g, x,y, host, this, useSTC); tempToken.textCount = token.length(); tempToken.makeStartAt(selEnd); token = tempToken; x = painter.paint(token, g, x,y, host, this); } // This token is entirely selected else if (token.getOffset()>=selStart && token.getEndOffset()<=selEnd) { x = painter.paintSelected(token, g, x,y, host, this,useSTC); } // This token is entirely unselected else { x = painter.paint(token, g, x,y, host, this); } token = new TokenImpl(orig); ((TokenImpl)token).makeStartAt(p); } p0 = (p==p0) ? p1 : p; y += fontHeight; } // End of while (token!=null && token.isPaintable()). // 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", x, y-fontHeight); } } /** * Fetches the allocation for the given child view.<p> * Overridden to account for code folding. * * @param index The index of the child, >= 0 &&< getViewCount(). * @param a The allocation to this view * @return The allocation to the child; or <code>null</code> if * <code>a</code> is <code>null</code>; or <code>null</code> if the * layout is invalid */ @Override public Shape getChildAllocation(int index, Shape a) { if (a != null) { Shape ca = getChildAllocationImpl(index, a); if ((ca != null) && (!isAllocationValid())) { // The child allocation may not have been set yet. Rectangle r = (ca instanceof Rectangle) ? (Rectangle) ca : ca .getBounds(); if ((r.width == 0) && (r.height == 0)) { return null; } } return ca; } return null; } /** * Fetches the allocation for the given child view to render into.<p> * Overridden to account for lines hidden by collapsed folded regions. * * @param line The index of the child, >= 0 &&< getViewCount() * @param a The allocation to this view * @return The allocation to the child */ public Shape getChildAllocationImpl(int line, Shape a) { Rectangle alloc = getInsideAllocation(a); host = (RSyntaxTextArea)getContainer(); FoldManager fm = host.getFoldManager(); int y = alloc.y; // TODO: Make cached getOffset() calls for Y_AXIS valid even for // folding, to speed this up! for (int i=0; i<line; i++) { y += getSpan(Y_AXIS, i); Fold fold = fm.getFoldForLine(i); if (fold!=null && fold.isCollapsed()) { i += fold.getCollapsedLineCount(); } } childAllocation2(line, y, alloc); return alloc; } /** * Determines the maximum span for this view along an * axis. This is implemented to provide the superclass * behavior after first making sure that the current font * metrics are cached (for the nested lines which use * the metrics to determine the height of the potentially * wrapped lines). * * @param axis may be either View.X_AXIS or View.Y_AXIS * @return the span the view would like to be rendered into. * 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. * @see View#getMaximumSpan */ @Override public float getMaximumSpan(int axis) { updateMetrics(); float span = super.getPreferredSpan(axis); if (axis==View.X_AXIS) { // EOL marker span += metrics.charWidth('\u00b6'); // metrics set in updateMetrics } return span; } /** * Determines the minimum span for this view along an * axis. This is implemented to provide the superclass * behavior after first making sure that the current font * metrics are cached (for the nested lines which use * the metrics to determine the height of the potentially * wrapped lines). * * @param axis may be either View.X_AXIS or View.Y_AXIS * @return the span the view would like to be rendered into. * 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. * @see View#getMinimumSpan */ @Override public float getMinimumSpan(int axis) { updateMetrics(); float span = super.getPreferredSpan(axis); if (axis==View.X_AXIS) { // EOL marker span += metrics.charWidth('\u00b6'); // metrics set in updateMetrics } return span; } /** * Determines the preferred span for this view along an * axis. This is implemented to provide the superclass * behavior after first making sure that the current font * metrics are cached (for the nested lines which use * the metrics to determine the height of the potentially * wrapped lines). * * @param axis may be either View.X_AXIS or View.Y_AXIS * @return the span the view would like to be rendered into. * 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. * @see View#getPreferredSpan */ @Override public float getPreferredSpan(int axis) { updateMetrics(); float span = 0; if (axis==View.X_AXIS) { // Add EOL marker span = super.getPreferredSpan(axis); span += metrics.charWidth('\u00b6'); // metrics set in updateMetrics } else { span = super.getPreferredSpan(axis); host = (RSyntaxTextArea)getContainer(); if (host.isCodeFoldingEnabled()) { // TODO: Cache y-offsets again to speed this up //System.out.println("y-axis baby"); int lineCount = host.getLineCount(); FoldManager fm = host.getFoldManager(); for (int i=0; i<lineCount; i++) { if (fm.isLineHidden(i)) { span -= getSpan(View.Y_AXIS, i); } } } } return span; } /** * 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; } /** * Overridden to allow for folded regions. */ @Override protected View getViewAtPoint(int x, int y, Rectangle alloc) { int lineCount = getViewCount(); int curY = alloc.y + getOffset(Y_AXIS, 0); // Always at least 1 line host = (RSyntaxTextArea)getContainer(); FoldManager fm = host.getFoldManager(); for (int line=1; line<lineCount; line++) { int span = getSpan(Y_AXIS, line-1); if (y<curY+span) { childAllocation2(line-1, curY, alloc); return getView(line-1); } curY += span; Fold fold = fm.getFoldForLine(line-1); if (fold!=null && fold.isCollapsed()) { line += fold.getCollapsedLineCount(); } } // Not found - return last line's view. childAllocation2(lineCount - 1, curY, alloc); return getView(lineCount - 1); } /** * Gives notification that something was inserted into the * document in a location that this view is responsible for. * This is implemented to simply update the children. * * @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#insertUpdate */ @Override public void insertUpdate(DocumentEvent changes, Shape a, ViewFactory f) { updateChildren(changes, a); Rectangle alloc = ((a != null) && isAllocationValid()) ? getInsideAllocation(a) : null; int pos = changes.getOffset(); View v = getViewAtPosition(pos, alloc); if (v != null) v.insertUpdate(changes, alloc, f); } /** * Loads all of the children to initialize the view. * This is called by the <code>setParent</code> method. * Subclasses can re-implement this to initialize their * child views in a different manner. The default * implementation creates a child view for each * child element. * * @param f the view factory */ @Override protected void loadChildren(ViewFactory f) { Element e = getElement(); int n = e.getElementCount(); if (n > 0) { View[] added = new View[n]; for (int i = 0; i < n; i++) added[i] = new WrappedLine(e.getElement(i)); replace(0, 0, added); } } @Override public Shape modelToView(int pos, Shape a, Position.Bias b) throws BadLocationException { if (! isAllocationValid()) { Rectangle alloc = a.getBounds(); setSize(alloc.width, alloc.height); } boolean isBackward = (b == Position.Bias.Backward); int testPos = (isBackward) ? Math.max(0, pos - 1) : pos; if(isBackward && testPos < getStartOffset()) { return null; } int vIndex = getViewIndexAtPosition(testPos); if ((vIndex != -1) && (vIndex < getViewCount())) { View v = getView(vIndex); if(v != null && testPos >= v.getStartOffset() && testPos < v.getEndOffset()) { Shape childShape = getChildAllocation(vIndex, a); if (childShape == null) { // We are likely invalid, fail. return null; } Shape retShape = v.modelToView(pos, childShape, b); if(retShape == null && v.getEndOffset() == pos) { if(++vIndex < getViewCount()) { v = getView(vIndex); retShape = v.modelToView(pos, getChildAllocation(vIndex, a), b); } } return retShape; } } throw new BadLocationException("Position not represented by view", pos); } /** * 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 */ @Override 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); } /** * Paints the word-wrapped text. * * @param g The graphics context in which to paint. * @param a The shape (usually a rectangle) in which to paint. */ @Override public void paint(Graphics g, Shape a) { Rectangle alloc = (a instanceof Rectangle) ? (Rectangle)a : a.getBounds(); tabBase = alloc.x; Graphics2D g2d = (Graphics2D)g; host = (RSyntaxTextArea)getContainer(); int ascent = host.getMaxAscent(); int fontHeight = host.getLineHeight(); FoldManager fm = host.getFoldManager(); TokenPainter painter = host.getTokenPainter(); Element root = getElement(); // Whether token styles should always be painted, even in selections int selStart = host.getSelectionStart(); int selEnd = host.getSelectionEnd(); int curLine = host.getCaretLineNumber(); int n = getViewCount(); // Number of lines. int x = alloc.x + getLeftInset(); tempRect.y = alloc.y + getTopInset(); Rectangle clip = g.getClipBounds(); for (int i = 0; i < n; i++) { tempRect.x = x + getOffset(X_AXIS, i); //tempRect.y = y + getOffset(Y_AXIS, i); tempRect.width = getSpan(X_AXIS, i); tempRect.height = getSpan(Y_AXIS, i); //System.err.println("For line " + i + ": tempRect==" + tempRect); if (tempRect.intersects(clip)) { Element lineElement = root.getElement(i); int startOffset = lineElement.getStartOffset(); int endOffset = lineElement.getEndOffset()-1; // Why always "-1"? View view = getView(i); if (selStart==selEnd || startOffset>=selEnd || endOffset<selStart) { drawView(painter, g2d, alloc, view, fontHeight, tempRect.y+ascent, i); } else { //System.out.println("Drawing line with selection: " + i); drawViewWithSelection(painter, g2d, alloc, view, fontHeight, tempRect.y+ascent, selStart, selEnd); } } tempRect.y += tempRect.height; Fold possibleFold = fm.getFoldForLine(i); if (possibleFold!=null && possibleFold.isCollapsed()) { i += possibleFold.getCollapsedLineCount(); // Visible indicator of collapsed lines Color c = RSyntaxUtilities.getFoldedLineBottomColor(host); if (c!=null) { g.setColor(c); g.drawLine(x,tempRect.y-1, host.getWidth(),tempRect.y-1); } } } } /** * Gives notification that something was removed from the * document in a location that this view is responsible for. * This is implemented to simply update the children. * * @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#removeUpdate */ @Override public void removeUpdate(DocumentEvent changes, Shape a, ViewFactory f) { updateChildren(changes, a); Rectangle alloc = ((a != null) && isAllocationValid()) ? getInsideAllocation(a) : null; int pos = changes.getOffset(); View v = getViewAtPosition(pos, alloc); if (v != null) v.removeUpdate(changes, alloc, f); } /** * Makes a <code>Segment</code> point to the text in our * document between the given positions. Note that the positions MUST be * valid positions in the document. * * @param p0 The first position in the document. * @param p1 The second position in the document. * @param document The document from which you want to get the text. * @param seg The segment in which to load the text. */ private void setSegment(int p0, int p1, Document document, Segment seg) { try { //System.err.println("... in setSharedSegment, p0/p1==" + p0 + "/" + p1); document.getText(p0, p1-p0, seg); //System.err.println("... in setSharedSegment: s=='" + s + "'; line/numLines==" + line + "/" + numLines); } catch (BadLocationException ble) { // Never happens ble.printStackTrace(); } } /** * Sets the size of the view. This should cause layout of the view along * the given axis, if it has any layout duties. * * @param width the width >= 0 * @param height the height >= 0 */ @Override public void setSize(float width, float height) { updateMetrics(); if ((int) width != getWidth()) { // invalidate the view itself since the childrens // desired widths will be based upon this views width. preferenceChanged(null, true, true); widthChanging = true; } super.setSize(width, height); widthChanging = false; } /** * Update the child views in response to a * document event. */ void updateChildren(DocumentEvent e, Shape a) { Element elem = getElement(); DocumentEvent.ElementChange ec = e.getChange(elem); // This occurs when syntax highlighting only changes on lines // (i.e. beginning a multiline comment). if (e.getType()==DocumentEvent.EventType.CHANGE) { //System.err.println("Updating the damage due to a CHANGE event..."); // FIXME: Make me repaint more intelligently. getContainer().repaint(); //damageLineRange(startLine,endLine, a, host); } else if (ec != null) { // the structure of this element changed. Element[] removedElems = ec.getChildrenRemoved(); Element[] addedElems = ec.getChildrenAdded(); View[] added = new View[addedElems.length]; for (int i = 0; i < addedElems.length; i++) added[i] = new WrappedLine(addedElems[i]); //System.err.println("Replacing " + removedElems.length + // " children with " + addedElems.length); replace(ec.getIndex(), removedElems.length, added); // should damge a little more intelligently. if (a != null) { preferenceChanged(null, true, true); getContainer().repaint(); } } // update font metrics which may be used by the child views updateMetrics(); } final void updateMetrics() { Component host = getContainer(); Font f = host.getFont(); metrics = host.getFontMetrics(f); // Metrics for the default font. tabSize = getTabSize() * metrics.charWidth('m'); } @Override public int viewToModel(float x, float y, Shape a, Position.Bias[] bias) { int offs = -1; if (! isAllocationValid()) { Rectangle alloc = a.getBounds(); setSize(alloc.width, alloc.height); } // Get the child view for the line at (x,y), and ask it for the // specific offset. Rectangle alloc = getInsideAllocation(a); View v = getViewAtPoint((int) x, (int) y, alloc); if (v != null) { offs = v.viewToModel(x, y, alloc, bias); } // Code folding may have hidden the last line. If so, return the last // visible offset instead of the last offset. if (host.isCodeFoldingEnabled() && v==getView(getViewCount()-1) && offs==v.getEndOffset()-1) { offs = host.getLastVisibleOffset(); } return offs; } /** * {@inheritDoc} */ public int yForLine(Rectangle alloc, int line) throws BadLocationException { return yForLineContaining(alloc, getElement().getElement(line).getStartOffset()); //return alloc.y + getOffset(Y_AXIS, line); } /** * {@inheritDoc} */ public int yForLineContaining(Rectangle alloc, int offs) throws BadLocationException { if (isAllocationValid()) { // TODO: make cached Y_AXIS offsets valid even with folding enabled // to speed this back up! Rectangle r = (Rectangle)modelToView(offs, alloc, Bias.Forward); if (r!=null) { if (host.isCodeFoldingEnabled()) { int line = host.getLineOfOffset(offs); FoldManager fm = host.getFoldManager(); if (fm.isLineHidden(line)) { return -1; } } return r.y; } } return -1; } /** * Simple view of a line that wraps if it doesn't * fit within the horizontal space allocated. * This class tries to be lightweight by carrying little * state of it's own and sharing the state of the outer class * with it's siblings. */ class WrappedLine extends View { int nlines; WrappedLine(Element elem) { super(elem); } /** * Calculate the number of lines that will be rendered * by logical line when it is wrapped. */ final int calculateLineCount() { int nlines = 0; int startOffset = getStartOffset(); int p1 = getEndOffset(); // Get the token list for this line so we don't have to keep // recomputing it if this logical line spans multiple physical // lines. RSyntaxTextArea textArea = (RSyntaxTextArea)getContainer(); RSyntaxDocument doc = (RSyntaxDocument)getDocument(); Element map = doc.getDefaultRootElement(); int line = map.getElementIndex(startOffset); Token tokenList = doc.getTokenListForLine(line); float x0 = 0;// FIXME: should be alloc.x!! alloc.x;//0; //System.err.println(">>> calculateLineCount: " + startOffset + "-" + p1); for (int p0=startOffset; p0<p1; ) { //System.err.println("... ... " + p0 + ", " + p1); nlines += 1; TokenSubList subList = TokenUtils.getSubTokenList(tokenList, p0, WrappedSyntaxView.this, textArea, x0, lineCountTempToken); x0 = subList!=null ? subList.x : x0; tokenList = subList!=null ? subList.tokenList : null; int p = calculateBreakPosition(p0, tokenList, x0); //System.err.println("... ... ... break position p==" + p); p0 = (p == p0) ? ++p : p; // this is the fix of #4410243 // we check on situation when // width is too small and // break position is calculated // incorrectly. //System.err.println("... ... ... new p0==" + p0); } /* int numLines = 0; try { numLines = textArea.getLineCount(); } catch (BadLocationException ble) { ble.printStackTrace(); } System.err.println(">>> >>> calculated number of lines for this view (line " + line + "/" + numLines + ": " + nlines); */ return nlines; } /** * Determines the preferred span for this view along an * axis. * * @param axis may be either X_AXIS or Y_AXIS * @return the span the view would like to be rendered into. * 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. * @see View#getPreferredSpan */ @Override public float getPreferredSpan(int axis) { switch (axis) { case View.X_AXIS: float width = getWidth(); if (width == Integer.MAX_VALUE) { // We have been initially set to MAX_VALUE, but we don't // want this as our preferred. return 100f; } return width; case View.Y_AXIS: if (nlines == 0 || widthChanging) nlines = calculateLineCount(); int h = nlines * ((RSyntaxTextArea)getContainer()).getLineHeight(); return h; default: throw new IllegalArgumentException("Invalid axis: " + axis); } } /** * Renders using the given rendering surface and area on that * surface. The view may need to do layout and create child * views to enable itself to render into the given allocation. * * @param g the rendering surface to use * @param a the allocated region to render into * @see View#paint */ @Override public void paint(Graphics g, Shape a) { // This is done by drawView() above. } /** * 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 * @param a the allocated region to render into * @return the bounding box of the given position is returned * @exception BadLocationException if the given position does not * represent a valid location in the associated document. */ @Override public Shape modelToView(int pos, Shape a, Position.Bias b) throws BadLocationException { //System.err.println("--- begin modelToView ---"); Rectangle alloc = a.getBounds(); RSyntaxTextArea textArea = (RSyntaxTextArea)getContainer(); alloc.height = textArea.getLineHeight();//metrics.getHeight(); alloc.width = 1; int p0 = getStartOffset(); int p1 = getEndOffset(); int testP = (b == Position.Bias.Forward) ? pos : Math.max(p0, pos - 1); // Get the token list for this line so we don't have to keep // recomputing it if this logical line spans multiple physical // lines. RSyntaxDocument doc = (RSyntaxDocument)getDocument(); Element map = doc.getDefaultRootElement(); int line = map.getElementIndex(p0); Token tokenList = doc.getTokenListForLine(line); float x0 = alloc.x;//0; while (p0 < p1) { TokenSubList subList = TokenUtils.getSubTokenList(tokenList, p0, WrappedSyntaxView.this, textArea, x0, lineCountTempToken); x0 = subList!=null ? subList.x : x0; tokenList = subList!=null ? subList.tokenList : null; int p = calculateBreakPosition(p0, tokenList, x0); if ((pos >= p0) && (testP<p)) {//pos < p)) { // it's in this line alloc = RSyntaxUtilities.getLineWidthUpTo( textArea, s, p0, pos, WrappedSyntaxView.this, alloc, alloc.x); //System.err.println("--- end modelToView ---"); return alloc; } //if (p == p1 && pos == p1) { if (p==p1-1 && pos==p1-1) { // Wants end. if (pos > p0) { alloc = RSyntaxUtilities.getLineWidthUpTo( textArea, s, p0, pos, WrappedSyntaxView.this, alloc, alloc.x); } //System.err.println("--- end modelToView ---"); return alloc; } p0 = (p == p0) ? p1 : p; //System.err.println("... ... Incrementing y"); alloc.y += alloc.height; } throw new BadLocationException(null, pos); } /** * Provides a mapping from the view coordinate space to the logical * coordinate space of the model. * * @param fx the X coordinate * @param fy the Y coordinate * @param a the allocated region to render into * @return the location within the model that best represents the * given point in the view * @see View#viewToModel */ @Override public int viewToModel(float fx, float fy, Shape a, Position.Bias[] bias) { // PENDING(prinz) implement bias properly bias[0] = Position.Bias.Forward; Rectangle alloc = (Rectangle) a; RSyntaxDocument doc = (RSyntaxDocument)getDocument(); int x = (int) fx; int y = (int) fy; if (y < alloc.y) { // above the area covered by this icon, so the the position // is assumed to be the start of the coverage for this view. return getStartOffset(); } else if (y > alloc.y + alloc.height) { // below the area covered by this icon, so the the position // is assumed to be the end of the coverage for this view. return getEndOffset() - 1; } else { // positioned 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. RSyntaxTextArea textArea = (RSyntaxTextArea)getContainer(); alloc.height = textArea.getLineHeight(); int p1 = getEndOffset(); // Get the token list for this line so we don't have to keep // recomputing it if this logical line spans multiple // physical lines. Element map = doc.getDefaultRootElement(); int p0 = getStartOffset(); int line = map.getElementIndex(p0); Token tlist = doc.getTokenListForLine(line); // Look at each physical line-chunk of this logical line. while (p0<p1) { // We can always use alloc.x since we always break // lines so they start at the beginning of a physical // line. TokenSubList subList = TokenUtils.getSubTokenList(tlist, p0, WrappedSyntaxView.this, textArea, alloc.x, lineCountTempToken); tlist = subList!=null ? subList.tokenList : null; int p = calculateBreakPosition(p0, tlist, alloc.x); // If desired view position is in this physical chunk. if ((y>=alloc.y) && (y<(alloc.y+alloc.height))) { // Point is to the left of the line if (x < alloc.x) { return p0; } // Point is to the right of the line else if (x > alloc.x + alloc.width) { return p - 1; } // Point is in this physical line! else { // Start at alloc.x since this chunk starts // at the beginning of a physical line. int n = tlist.getListOffset(textArea, WrappedSyntaxView.this, alloc.x, x); // NOTE: We needed to add the max() with // p0 as getTokenListForLine returns -1 // for empty lines (just a null token). // How did this work before? // FIXME: Have null tokens have their // offset but a -1 length. return Math.max(Math.min(n, p1-1), p0); } // End of else. } // End of if ((y>=alloc.y) && ... p0 = (p == p0) ? p1 : p; alloc.y += alloc.height; } // End of while (p0<p1). return getEndOffset() - 1; } // End of else. } private void handleDocumentEvent(DocumentEvent e, Shape a, ViewFactory f) { int n = calculateLineCount(); if (this.nlines != n) { this.nlines = n; WrappedSyntaxView.this.preferenceChanged(this, false, true); // have to repaint any views after the receiver. RSyntaxTextArea textArea = (RSyntaxTextArea)getContainer(); textArea.repaint(); // Must also revalidate container so gutter components, such // as line numbers, get updated for this line's new height Gutter gutter = RSyntaxUtilities.getGutter(textArea); if (gutter!=null) { gutter.revalidate(); gutter.repaint(); } } else if (a != null) { Component c = getContainer(); Rectangle alloc = (Rectangle) a; c.repaint(alloc.x, alloc.y, alloc.width, alloc.height); } } @Override public void insertUpdate(DocumentEvent e, Shape a, ViewFactory f) { handleDocumentEvent(e, a, f); } @Override public void removeUpdate(DocumentEvent e, Shape a, ViewFactory f) { handleDocumentEvent(e, a, f); } } }