blob: a954e647182e3e4089afca2392de6ffe42468f98 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.netbeans.core.output2;
import java.util.HashMap;
import java.util.Map;
import javax.swing.*;
import javax.swing.text.*;
import java.awt.*;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import static javax.swing.SwingConstants.EAST;
import static javax.swing.SwingConstants.NORTH;
import static javax.swing.SwingConstants.SOUTH;
import static javax.swing.SwingConstants.WEST;
import javax.swing.text.Position.Bias;
import org.openide.awt.GraphicsUtils;
import org.openide.util.Exceptions;
/**
* A custom Swing text View which supports line wrapping. The default Swing
* line wrapping code is not appropriate for our purposes - particularly, it
* will iterate the entire buffer multiple times to determine break positions.
* Since it would defeat the purpose of using a memory mapped file to have to
* pull the entire thing into memory every time it's painted or its size should
* be calculated, we have this class instead.
* <p>
* All position/line calculations this view does are based on the integer array
* of line offsets kept by the writer's Lines object.
*
* @author Tim Boudreau, Martin Entlicher
*/
public class WrappedTextView extends View implements TabExpander {
static final int TAB_SIZE = 8; // The default tab size
/**
* The component we will paint
*/
private JTextComponent comp;
/**
* Precalculated number of characters per line
*/
private int charsPerLine = 80;
/**
* Precalculated font descent, used to adjust the bounding rectangle of
* characters as returned by modelToView. This is added to the y position
* of character rectangles in modelToView() so painting the selection
* includes the complete character and does not interfere with the line above.
*/
private int fontDescent = 4;
/**
* A scratch Segment object to avoid allocation while painting lines
*/
private static final Segment SEGMENT = new Segment();
/**
* Precalculated width (in pixels) we are to paint into, the end being the wrap point
*/
private int width = 0;
/**
* Flag indicating we need to recalculate metrics before painting
*/
private boolean changed = true;
/**
* Precalculated width of a single character (assumes fixed width font).
*/
private int charWidth = 12;
/**
* Precalculated height of a single character (assumes fixed width font).
*/
private int charHeight = 7;
/**
* A scratchpad int array
*/
static final int[] ln = new int[3];
/**
* Flag indicating that the antialiasing flag is set on the Graphics object.
* We do a somewhat prettier arrow if it is.
*/
private boolean aa = false;
static final Color arrowColor = new Color (80, 162, 80);
int tabSize;
int tabBase;
private int tabOffsetX = 0;
private final PropertyChangeListener propertyChangeListener;
public WrappedTextView(Element elem, JTextComponent comp,
PropertyChangeListener propertyChangeListener1) {
super(elem);
this.comp = comp;
this.propertyChangeListener = propertyChangeListener1;
}
public float getPreferredSpan(int axis) {
OutputDocument doc = odoc();
float result = 0;
if (doc != null) {
updateWidth();
switch (axis) {
case X_AXIS :
result = charsPerLine;
break;
case Y_AXIS :
result = doc.getLines().getLogicalLineCountIfWrappedAt(charsPerLine) * charHeight + fontDescent;
break;
default :
throw new IllegalArgumentException (Integer.toString(axis));
}
}
return result;
}
@Override
public float getMinimumSpan(int axis) {
return getPreferredSpan(axis);
}
@Override
public float getMaximumSpan(int axis) {
return getPreferredSpan(axis);
}
float viewWidth = -1;
@Override
public void setSize(float width, float height) {
super.setSize(width, height);
if (viewWidth != width) {
viewWidth = width;
updateMetrics();
}
}
private int getTabSize() {
//Integer i = (Integer) getDocument().getProperty(PlainDocument.tabSizeAttribute);
//int size = (i != null) ? i.intValue() : TAB_SIZE;
//return size;
return TAB_SIZE;
}
@Override
public float nextTabStop(float x, int tabOffset) {
if (tabSize == 0) {
return x;
}
int ntabs = (((int) x) - margin() + tabOffsetX) / tabSize;
return margin() + ((ntabs + 1) * tabSize) - tabOffsetX;
}
void updateMetrics() {
Font font = comp.getFont();
FontMetrics fm = comp.getFontMetrics(font);
charWidth = fm.charWidth('m'); //NOI18N
charHeight = fm.getHeight();
fontDescent = fm.getMaxDescent();
Graphics2D g2d = ((Graphics2D) comp.getGraphics());
if (g2d != null) {
aa = g2d.getRenderingHint(RenderingHints.KEY_ANTIALIASING) ==
RenderingHints.VALUE_ANTIALIAS_ON;
}
tabSize = getTabSize() * charWidth;
updateWidth();
}
/**
* Get the component's document as an instance of OutputDocument, if it
* is one, returning null if it is not (briefly it will not be after the
* editor kit has been installed - this is unavoidable).
*
* @return An instance of OutputDocument or null.
*/
private OutputDocument odoc() {
Document doc = comp.getDocument();
if (doc instanceof OutputDocument) {
return (OutputDocument) doc;
}
return null;
}
private void updateWidth() {
int oldCharPerWidth = charsPerLine;
if (comp.getParent() instanceof JViewport) {
JViewport jv = (JViewport) comp.getParent();
width = jv.getExtentSize().width - (aa ? 18 : 17);
} else {
width = comp.getWidth() - (aa ? 18 : 17);
}
if (width < 0) {
width = 0;
}
charsPerLine = width / charWidth;
if (charsPerLine != oldCharPerWidth) {
propertyChangeListener.propertyChange(new PropertyChangeEvent(this,
"charsPerLine", oldCharPerWidth, charsPerLine)); //NOI18N
}
}
/**
* Get the left hand margin required for printing line wrap decorations.
*
* @return A margin in pixels
*/
private static int margin() {
return 9;
}
public void paint(Graphics g, Shape allocation) {
GraphicsUtils.configureDefaultRenderingHints(g);
comp.getHighlighter().paint(g);
tabBase = ((Rectangle) allocation).x + margin();
OutputDocument doc = odoc();
if (doc != null) {
Rectangle clip = g.getClipBounds();
clip.y = Math.max (0, clip.y - charHeight);
clip.height += charHeight * 2;
int lineCount = doc.getElementCount();
if (lineCount == 0) {
return;
}
ln[0] = clip.y / charHeight;
Lines lines = doc.getLines();
lines.toPhysicalLineIndex(ln, charsPerLine);
int firstline = ln[0];
g.setColor (comp.getForeground());
Segment seg = SwingUtilities.isEventDispatchThread() ? SEGMENT : new Segment();
int selStart = comp.getSelectionStart();
int selEnd = comp.getSelectionEnd();
int y = (clip.y - (clip.y % charHeight) + charHeight);
int maxVisibleChars = ((clip.height + charHeight - 1) / charHeight) * charsPerLine;
try {
for (int i = firstline; i < lines.getLineCount(); i++) {
if (y > clip.y + clip.height) {
return;
}
int visibleLine = lines.realToVisibleLine(i);
if (visibleLine < 0) {
continue;
}
int lineStart = doc.getLineStart(i);
int lineEnd = doc.getLineEnd (i);
int length = lineEnd - lineStart;
if (length == 0) {
y += charHeight;
continue;
}
length = lines.lengthWithTabs(i);
LineInfo info = lines.getLineInfo(i);
// get number of logical lines
int logicalLines = length <= charsPerLine ? 1 :
(charsPerLine == 0 ? length : (length + charsPerLine - 1) / charsPerLine);
// get current (first which we will draw) logical line
int currLogicalLine = (i == firstline && logicalLines > 0 && ln[1] > 0 ) ? ln[1] : 0;
int charpos = 0;
//int tabOverLine = 0; // 0 or 1
int charsWithTabs = 0;
int arrowDrawn = currLogicalLine - 1;
int x = 0;
int remainCharsOnLogicalLine = charsPerLine;
int logLineOffset;
if (currLogicalLine > 0) {
// shift lineStart to position of first logical line that will be drawn
// we have lineStart - offset of the beginning of the physical line
// we have to add (currLogicalLine * charsPerLine) characters with expanded TABs
// this corresponds to a different real number of characters
logLineOffset = currLogicalLine * charsPerLine;
int[] tabShiftPtr = new int[] { 0 };
logLineOffset = lines.getNumPhysicalChars(lineStart, logLineOffset, tabShiftPtr);
lineStart += logLineOffset;
if (tabShiftPtr[0] > 0) {
//tabOverLine = 1;
remainCharsOnLogicalLine -= tabShiftPtr[0];
x = tabShiftPtr[0] * charWidth;
charsWithTabs += tabShiftPtr[0];
}
} else {
logLineOffset = 0;
}
// limit number of chars needed by estimation of maximum number of chars we need to repaint
length = Math.min(maxVisibleChars, length - logLineOffset);
int sourceLength = Math.min(maxVisibleChars, lineEnd - lineStart);
// get just small part of document we need (no need to get e.g. whole 10 MB line)
doc.getText(lineStart, sourceLength, seg);
tabOffsetX = charWidth * currLogicalLine * charsPerLine; //logLineOffset;
for (LineInfo.Segment ls : info.getLineSegments()) {
if (ls.getEnd() < logLineOffset) {
continue;
}
g.setColor(ls.getColor());
int shift = 0;
while (charpos < ls.getEnd() - logLineOffset && currLogicalLine < logicalLines) {
int lenToDraw = Math.min(remainCharsOnLogicalLine, ls.getEnd() - logLineOffset - charpos);
int charsToDraw = lenToDraw;
if (lenToDraw > 0) {
charsToDraw = getCharsForLengthWithTabs(seg.array, charpos, currLogicalLine * charsPerLine + shift, lenToDraw, remainCharsOnLogicalLine);// - tabOverLine;
if (currLogicalLine != logicalLines - 1 && arrowDrawn != currLogicalLine) {
arrowDrawn = currLogicalLine;
drawArrow(g, y, currLogicalLine == logicalLines - 2);
}
Color bg = ls.getCustomBackground();
drawText(seg, g, x, y, lineStart, charpos, selStart, charsToDraw, selEnd, bg);
if (ls.getListener() != null) {
underline(g, seg, charpos, charsToDraw, x, y);
}
}
lenToDraw = getCharLengthWithTabs(seg.array, charpos, currLogicalLine * charsPerLine + shift, charsToDraw);
charpos += charsToDraw;
if (charsToDraw == 0) {
break; // Prevent livelock, see bug 230840.
}
charsWithTabs += lenToDraw;
remainCharsOnLogicalLine -= lenToDraw;
x += lenToDraw * charWidth;
shift += lenToDraw;
//tabOverLine = (remainCharsOnLogicalLine < 0) ? 1 : 0;
while(remainCharsOnLogicalLine <= 0) {
shift = -remainCharsOnLogicalLine;
remainCharsOnLogicalLine += charsPerLine;
currLogicalLine++;
x = shift * charWidth;
tabOffsetX += charWidth * (charsPerLine);// + shift);
y += charHeight;
if (y > clip.y + clip.height) {
return;
}
if (shift > 0) {
if (selStart != selEnd) {
int realPos = lineStart + charpos;
int a = Math.max(selStart, realPos);
int b = Math.min(selEnd, realPos + charsToDraw);
if (a < b) {
drawSelection(g, 0, x, y);
}
}
}
}
}
}
if (charsPerLine == 0 || charsWithTabs % charsPerLine != 0) {
y += charHeight;
}
}
tabOffsetX = 0;
} catch (BadLocationException e) {
Exceptions.printStackTrace(e);
}
}
}
/**
* Draw text
*
* @param seg A Segment object containing the text
* @param g The graphics context
* @param y The baseline in the graphics context
* @param lineStart The character position at which the line starts
* @param charpos The current character position within the segment
* @param selStart The character index at which the selected range, if any, starts
* @param lenToDraw The number of characters we'll draw before we're outside the clip rectangle
* @param selEnd The end of the selected range of text, if any
*/
private void drawText(Segment seg, Graphics g, int x, int y, int lineStart,
int charpos, int selStart, int lenToDraw, int selEnd, Color bg) {
Color clr = g.getColor();
if (selStart != selEnd) {
int realPos = lineStart + charpos;
int a = Math.max(selStart, realPos);
int b = Math.min(selEnd, realPos + lenToDraw);
if (a < b) {
realPos = odoc().getLines().getNumLogicalChars(lineStart, realPos - lineStart) + lineStart;
a = odoc().getLines().getNumLogicalChars(lineStart, a - lineStart) + lineStart;
b = odoc().getLines().getNumLogicalChars(lineStart, b - lineStart) + lineStart;
int start = x + margin() + (a - realPos) * charWidth;
int len = (b - a) * charWidth;
int w = charsPerLine * charWidth;
if (start - margin() + len > w) {
len = w - start + margin();
}
g.setColor (comp.getSelectionColor());
g.fillRect (start, y + fontDescent - charHeight, len, charHeight);
g.setColor (clr);
}
}
//g.drawChars(seg.array, charpos, lenToDraw, margin() + x, y);
int count = seg.count;
int offset = seg.offset;
seg.count = lenToDraw;
seg.offset = charpos;
drawTextBackground(g, clr, bg, selStart != selEnd, seg, x, y, charpos);
Utilities.drawTabbedText(seg, margin() + x, y, g, this, charpos);
seg.count = count;
seg.offset = offset;
}
private void drawTextBackground(Graphics g, Color fg, Color bg,
boolean selection, Segment seg, int x, int y, int charpos) {
if (bg != null && !selection) {
int w = Utilities.getTabbedTextWidth(
seg, g.getFontMetrics(), x, this, charpos);
int h = g.getFontMetrics().getHeight();
g.setColor(bg);
g.fillRect(x + margin(), y - h + g.getFontMetrics().getDescent(),
w, h);
}
g.setColor(fg);
}
private void drawSelection(Graphics g, int x1, int x2, int y) {
Color c = g.getColor();
g.setColor (comp.getSelectionColor());
g.fillRect (x1 + margin(), y + fontDescent - charHeight, x2 - x1, charHeight);
g.setColor (c);
}
private void underline(Graphics g, Segment seg, int charpos, int lenToDraw, int x, int y) {
if (!ExtPlainView.isLinkUndeliningEnabled(this)) {
return;
}
int underlineStart = margin() + x;
FontMetrics fm = g.getFontMetrics();
int underlineEnd = underlineStart + fm.charsWidth(seg.array, charpos, lenToDraw);
int underlineShift = fm.getDescent() - 1;
g.drawLine (underlineStart, y + underlineShift, underlineEnd, y + underlineShift);
}
/**
* Draw the decorations used with wrapped lines.
*
* @param g A graphics to paint into
* @param y The y coordinate of the line as a font baseline position
*/
private void drawArrow (Graphics g, int y, boolean drawHead) {
Color c = g.getColor();
g.setColor (arrowColor());
int w = width + 15;
y+=2;
int rpos = aa ? 8 : 4;
if (aa) {
g.drawArc(w - rpos, y - (charHeight / 2), rpos + 1, charHeight, 265, 185);
w++;
} else {
g.drawLine (w-rpos, y - (charHeight / 2), w, y - (charHeight / 2));
g.drawLine (w, y - (charHeight / 2)+1, w, y + (charHeight / 2) - 1);
g.drawLine (w-rpos, y + (charHeight / 2), w, y + (charHeight / 2));
}
if (drawHead) {
rpos = aa ? 7 : 8;
int[] xpoints = new int[] {
w - rpos,
w - rpos + 5,
w - rpos + 5,
};
int[] ypoints = new int[] {
y + (charHeight / 2),
y + (charHeight / 2) - 5,
y + (charHeight / 2) + 5,
};
g.fillPolygon(xpoints, ypoints, 3);
}
g.setColor (arrowColor());
g.drawLine (1, y - (charHeight / 2), 5, y - (charHeight / 2));
g.drawLine (1, y - (charHeight / 2), 1, y + (charHeight / 2));
g.drawLine (1, y + (charHeight / 2), 5, y + (charHeight / 2));
g.setColor (c);
}
/**
* Get the color used for the line wrap arrow
*
* @return The arrow color
*/
private static Color arrowColor() {
return arrowColor;
}
public Shape modelToView(int pos, Shape a, Position.Bias b) throws BadLocationException {
Rectangle result = new Rectangle();
result.setBounds (0, 0, charWidth, charHeight);
OutputDocument od = odoc();
if (od != null) {
int line, start;
synchronized (od.getLines().readLock()) {
line = Math.max(0, od.getLines().getLineAt(pos));
start = od.getLineStart(line);
}
int column = pos - start;
column = od.getLines().getNumLogicalChars(start, column);
int row = od.getLines().getLogicalLineCountAbove(line, charsPerLine);
int end = getLineEnd(line, od.getLines());
int len = od.getLines().getNumLogicalChars(start, end - start);
//#104307
if ((column >= charsPerLine)
&& charsPerLine != 0) {
row += (column % charsPerLine == 0 && column == len)
? column / charsPerLine - 1
: column / charsPerLine;
column = (column % charsPerLine == 0 && column == len)
? charsPerLine
: column % charsPerLine;
}
result.y = (row * charHeight) + fontDescent;
result.x = margin() + (column * charWidth);
// System.err.println(pos + "@" + result.x + "," + result.y + " line " + line + " start " + start + " row " + row + " col " + column);
}
return result;
}
public int viewToModel(float x, float y, Shape a, Position.Bias[] biasReturn) {
OutputDocument od = odoc();
if (od != null) {
int ix = Math.max((int) x - margin(), 0);
int iy = (int) y - fontDescent;
ln[0] = (iy / charHeight);
od.getLines().toPhysicalLineIndex(ln, charsPerLine);
int logicalLine = ln[0];
int wraps = ln[2] - 1;
int totalLines = od.getLines().getLineCount();
if (totalLines == 0) {
return 0;
}
if (logicalLine >= totalLines) {
return od.getLength();
}
int lineStart = od.getLineStart(logicalLine);
int lineLength = od.getLines().lengthWithTabs(logicalLine);
int lineEnd = lineStart + lineLength;//od.getLineEnd(logicalLine);
int column = ix / charWidth;
if (column > lineLength) {
column = lineLength;
}
int result = wraps > 0 ?
Math.min(lineEnd, lineStart + (ln[1] * charsPerLine) + column)
: Math.min(lineStart + column, lineEnd);
Lines lines = od.getLines();
result = lines.getNumPhysicalChars(lineStart, result - lineStart, null) + lineStart;
result = Math.min (od.getLength(), result);
return result;
/* System.err.println ("ViewToModel " + ix + "," + iy + " = " + result + " physical ln " + physicalLine +
" logical ln " + logicalLine + " on wrap line " + ln[1] + " of " + wraps + " charsPerLine " +
charsPerLine + " column " + column + " line length " + lineLength);
// System.err.println ("v2m: [" + ix + "," + iy + "] = " + result);
*/
} else {
return 0;
}
}
private int getCharLengthWithTabs(char[] array, int charpos, int tabLineOffset, int lenToDraw) {
int n = Math.min(array.length, charpos + lenToDraw);
int tabExpand = 0;
for (int i = charpos; i < n; i++) {
if ('\t' == array[i]) {
int numSpaces = TAB_SIZE - (((i - charpos + tabLineOffset) + tabExpand) % TAB_SIZE);
tabExpand += numSpaces - 1;
lenToDraw += numSpaces - 1;
}
}
return lenToDraw;
}
private int getCharsForLengthWithTabs(char[] array, int charpos, int tabLineOffset, int lenToDraw, int length) {
int n = Math.min(array.length, charpos + lenToDraw);
int lengthWithTab = 0;
int tabExpand = 0;
int i;
for (i = charpos; i < n && lengthWithTab < length; i++) {
if ('\t' == array[i]) {
int numSpaces = TAB_SIZE - (((i - charpos + tabLineOffset) + tabExpand) % TAB_SIZE);
tabExpand += numSpaces - 1;
lengthWithTab += numSpaces;
} else {
lengthWithTab++;
}
}
if (lengthWithTab > length && i > (charpos + 1) && array[i-1] != '\t') {
i--;
}
return i - charpos;
}
/**
* Replaces usage of slow
* Utilities.getPositionAbove()/Utilities.getPositionBelow(), skips hidden
* lines.
*/
@Override
public int getNextVisualPositionFrom(int pos, Bias b, Shape a,
int direction, Bias[] biasRet) throws BadLocationException {
Element elem = getElement();
if (pos == -1) {
pos = (direction == SOUTH || direction == EAST)
? getStartOffset()
: (getEndOffset() - 1);
}
int lineIndex;
int visibleLineIndex;
int origLineIndex;
PositionInfo pi;
Lines lines = odoc().getLines();
switch (direction) {
case NORTH:
pi = getPositionInfo(pos);
if (pi.lineIndex > 0 || pi.innerRowIndex > 0) {
if (pi.innerRowIndex > 0) {
return jumpInLine(lines, pi, direction);
}
return jumpToLine(lines, pi, direction);
}
break;
case SOUTH:
pi = getPositionInfo(pos);
if (pi.innerRowIndex < pi.innerRowsCount - 1) {
return jumpInLine(lines, pi, direction);
}
visibleLineIndex = lines.realToVisibleLine(pi.lineIndex);
if (visibleLineIndex < elem.getElementCount() - 1) {
return jumpToLine(lines, pi, direction);
}
break;
case WEST:
origLineIndex = lines.getLineAt(pos);
pos = Math.max(0, pos - 1);
lineIndex = lines.getLineAt(pos);
if (origLineIndex != lineIndex) {
int origVisibleLine = lines.realToVisibleLine(origLineIndex);
pos = elem.getElement(origVisibleLine - 1).getEndOffset() - 1;
}
break;
case EAST:
origLineIndex = lines.getLineAt(pos);
pos = Math.min(pos + 1, elem.getEndOffset() - 1);
lineIndex = lines.getLineAt(pos);
if (origLineIndex != lineIndex) {
int origVisibleLine = lines.realToVisibleLine(origLineIndex);
pos = elem.getElement(origVisibleLine + 1).getStartOffset();
}
break;
default:
throw new IllegalArgumentException("Bad direction"); //NOI18N
}
return pos;
}
/**
* Get info about a line. The offset of the returned position info will be
* set to the first character of that line.
*/
private PositionInfo getLineInfo(int lineIndex) {
Lines lines = odoc().getLines();
int lineStart = lines.getLineStart(lineIndex);
return getPositionInfo(lines, lineIndex, lineStart, lineStart);
}
/**
* Get info about a position and containing line.
*/
private PositionInfo getPositionInfo(int offset) {
Lines lines = odoc().getLines();
int lineIndex = lines.getLineAt(offset);
int lineStart = lines.getLineStart(lineIndex);
return getPositionInfo(lines, lineIndex, lineStart, offset);
}
/**
* Get info about a position. Should not be called directly.
*/
private PositionInfo getPositionInfo(Lines lines, int lineIndex,
int lineStart, int offset) {
PositionInfo pi = new PositionInfo();
pi.offset = offset;
pi.lineIndex = lineIndex;
pi.lineStart = lineStart;
pi.lineEnd = getLineEnd(pi.lineIndex, lines);
int lineLen = pi.lineEnd - pi.lineStart;
int column = offset - pi.lineStart;
int logicalColumn = lines.getNumLogicalChars(pi.lineStart, column);
pi.logicalLineLength = lines.getNumLogicalChars(pi.lineStart, lineLen);
int innerRowsCount = (pi.logicalLineLength / charsPerLine)
+ (pi.logicalLineLength % charsPerLine > 0 ? 1 : 0);
pi.innerRowsCount = Math.max(1, innerRowsCount);
pi.innerRowIndex = logicalColumn / charsPerLine
- (pi.logicalLineLength > 0 && logicalColumn == pi.logicalLineLength
&& logicalColumn % charsPerLine == 0
? 1 : 0);
if (lineLen > 0 && pi.lineEnd == offset
&& pi.logicalLineLength % charsPerLine == 0) {
// handle last char in line
pi.innerColumn = charsPerLine - (pi.innerRowsCount > 1 ? 1 : 0);
} else {
pi.innerColumn = lines.getNumLogicalChars(
pi.lineStart, offset - pi.lineStart) % charsPerLine;
}
return pi;
}
/**
* Jump to appropriate position from a position in a neighboring line. The
* line above/below is assumed to exist.
*
* @param pi Source position.
* @param direction SwingConstants.NORTH for jumping to line above,
* SwingConstants.SOUTH for line below.
* @param lines The lines object.
*/
private int jumpToLine(Lines lines, PositionInfo pi, int direction) {
assert direction == NORTH || direction == SOUTH;
int newRealLine = findNearestVisibleLine(lines, pi.lineIndex, direction);
if (newRealLine < 0) {
return pi.offset;
}
PositionInfo targetLine = getLineInfo(newRealLine);
int newInnerRow = direction == NORTH
? targetLine.innerRowsCount - 1
: 0;
int logicalLineStart = targetLine.lineStart + lines.getNumPhysicalChars(
targetLine.lineStart, newInnerRow * charsPerLine, null);
int physicalColumn = lines.getNumPhysicalChars(logicalLineStart,
pi.innerColumn, null);
int physicalPos = fixPhysicalPosition(lines, logicalLineStart
+ physicalColumn, newInnerRow, targetLine.lineStart);
return Math.min(physicalPos, targetLine.lineEnd);
}
/**
* Find the nearest visible from {@code realLineIndex}.
*
* @param lines Info about lines.
* @param realLineIndex Real index of line above/below which the first
* visible line should be found.
* @param direction SwingConstants.SOUTH or SwingConstants.NORTH.
* @return Real line index of the nearest visible line, or -1 if no such
* line exists.
*/
private int findNearestVisibleLine(Lines lines, int realLineIndex,
int direction) {
assert direction == SOUTH || direction == NORTH;
int inc = direction == SOUTH ? 1 : -1;
int visibleLine = lines.realToVisibleLine(realLineIndex);
if (visibleLine < 0) {
// the source line is not visible, let's search
for (int i = realLineIndex + inc; i >= 0
&& i < lines.getLineCount(); i += inc) {
if (lines.realToVisibleLine(i) >= 0) {
return i;
}
}
return -1;
} else {
return lines.visibleToRealLine(visibleLine + inc);
}
}
/**
* Jump to a logical line in a physical line.
*
* @param pi Source position.
* @param lines Lines object.
* @param direction Direction, SwingConstants.NORTH or SwingConstants.SOUTH.
*/
private int jumpInLine(Lines lines, PositionInfo pi, int direction) {
assert direction == NORTH || direction == SOUTH;
assert pi.innerRowIndex > 0 || direction == SOUTH;
assert pi.innerRowIndex + 1 < pi.innerRowsCount || direction == NORTH;
int newRow = pi.innerRowIndex + ((direction == SOUTH) ? 1 : -1);
int newLogicalColumn = newRow * charsPerLine + pi.innerColumn;
int newPos = pi.lineStart + lines.getNumPhysicalChars(pi.lineStart,
newLogicalColumn, null);
newPos = fixPhysicalPosition(lines, newPos, newRow, pi.lineStart);
return Math.min(pi.lineEnd, Math.max(pi.lineStart, newPos));
}
/**
* When computing physical position from logical position (including tabs),
* and the position is inside a tab, the tab offset is returned. It is a
* problem if start of the tab is in another line than the position inside
* the tab. This method checks it and corrects the physical position.
*
* @param lines Lines object.
* @param pos Physical position.
* @param dir Direction, SOUTH or NORTH.
* @param newRow Index or inner row in which the position should be placed.
* @param lineStart Start offset of the physical line.
*
* @return Position that is sure to be in the correct inner line.
*/
private int fixPhysicalPosition(Lines lines, int pos, int newRow,
int lineStart) {
//check that new pos is really in the next logical inner line
int newLogicalLineStart = newRow * charsPerLine;
int computedLogicalColumn = lines.getNumLogicalChars(lineStart,
pos - lineStart);
if (computedLogicalColumn < newLogicalLineStart) {
return pos + 1;
}
return pos;
}
/**
* Find position of the last char in line.
*/
private int getLineEnd(int realLineIndex, Lines lines) {
synchronized (lines.readLock()) {
return realLineIndex + 1 < lines.getLineCount()
? lines.getLineStart(realLineIndex + 1) - 1
: lines.getCharCount();
}
}
/**
* Simple data structure for short-term storing of information about a
* position in a wrapped line.
*/
private static final class PositionInfo {
/**
* Physical character offset. Not counting tabs.
*/
public int offset;
/**
* Real line index. Counting invisible lines.
*/
public int lineIndex;
/**
* Start offset of the line.
*/
public int lineStart;
/**
* End offset of the line.
*/
public int lineEnd;
/**
* Logical length (expanded tabs) of the line.
*/
public int logicalLineLength;
/**
* Number of inner wrapped rows for the real row.
*/
public int innerRowsCount;
/**
* Index of inner row that contains the position.
*/
public int innerRowIndex;
/**
* Column in the inner row at which the position is displayed.
*/
public int innerColumn;
}
}