Package org.fife.ui.rsyntaxtextarea

Source Code of org.fife.ui.rsyntaxtextarea.WrappedSyntaxView

/*
* 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) {

    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) {
        x = painter.paint(token, g, x,y, host, this);
        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());
        painter.paint(tempToken, g, x,y, host, this);
        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;

    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);
          }
          else {
            tempToken.copyFrom(token);
            tempToken.textCount = selCount;
            x = painter.paintSelected(tempToken, g, x,y, host, this);
            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);
          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);
        }

        // 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());

        // 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);
          }
          else {
            tempToken.copyFrom(token);
            tempToken.textCount = selCount;
            x = painter.paintSelected(tempToken, g, x,y, host, this);
            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);
          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);
        }

        // 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();
    boolean useSelectedTextColor = host.getUseSelectedTextColor();

    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 (!useSelectedTextColor || selStart==selEnd ||
            (startOffset>=selEnd || endOffset<selStart)) {
          drawView(painter, g2d, alloc, view, fontHeight,
              tempRect.y+ascent);
        }
        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);
    }

  }


}
TOP

Related Classes of org.fife.ui.rsyntaxtextarea.WrappedSyntaxView

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.