| /* |
| * 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; |
| } |
| } |