| /* |
| * (C) Copyright IBM Corp. 1998-2004. All Rights Reserved. |
| * |
| * The program is provided "as is" without any warranty express or |
| * implied, including the warranty of non-infringement and the implied |
| * warranties of merchantibility and fitness for a particular purpose. |
| * IBM will not be liable for any damages suffered by you as a result |
| * of using the Program. In no event will IBM be liable for any |
| * special, indirect or consequential damages or lost profits even if |
| * IBM has been advised of the possibility of their occurrence. IBM |
| * will not be liable for any third party claims against you. |
| */ |
| // Requires Java2 |
| // Revision: 70 1.38 richtext/AsyncFormatter.java, richtext, richtext |
| |
| package com.ibm.richtext.textformat; |
| |
| import java.awt.Graphics; |
| import java.awt.Rectangle; |
| import java.awt.Point; |
| import java.awt.Color; |
| |
| import java.text.BreakIterator; |
| import java.text.CharacterIterator; |
| |
| import java.util.Hashtable; |
| |
| import com.ibm.richtext.textlayout.attributes.AttributeMap; |
| import com.ibm.richtext.textlayout.attributes.TextAttribute; |
| |
| import com.ibm.richtext.styledtext.MConstText; |
| |
| import com.ibm.richtext.textlayout.Graphics2DConversion; |
| |
| ///*JDK12IMPORTS |
| import java.awt.Graphics2D; |
| import java.awt.font.LineBreakMeasurer; |
| import java.awt.font.FontRenderContext; |
| //JDK12IMPORTS*/ |
| |
| /*JDK11IMPORTS |
| import com.ibm.richtext.textlayout.Graphics2D; |
| import com.ibm.richtext.textlayout.LineBreakMeasurer; |
| import com.ibm.richtext.textlayout.FontRenderContext; |
| JDK11IMPORTS*/ |
| |
| /* |
| Change history: |
| |
| 7/25/96 - |
| |
| 8/15/96 |
| Fixed bug in textOffsetToPoint (fPixHeight wasn't added to negative heights). {jbr} |
| |
| 8/19/96 |
| Removed references to JustificationStyle constants, and moved them into MFormatter {sfb} |
| |
| 8/23/96 |
| Modified findLineAt and getLineContaining - added optimization in search loop. Also, |
| they were failing when fLTPosEnd+1 == fLTNegStart. Fixed. |
| |
| 8/26/96 |
| Moved FormatDaemon stuff into this class. |
| |
| 9/11/96 |
| Shortened line returned from textOffsetToPoint by 1 pixel |
| |
| 9/23/96 |
| textOffsetToPoint line length restored (see above). drawText() now draws only lines |
| which fall in rectangle param. |
| |
| 9/26/96 |
| whitespace at end of line is used for caret positioning |
| |
| 10/4/96 |
| Added static TextBox method. |
| |
| 10/8/96 |
| Line 1300 - less than changed to less than or equal in textOffsetToPoint. Watch for |
| hangs in formatText. |
| |
| 10/9/96 |
| Changed sync. model. fFormatInBackground is used to start/stop bg formatting |
| |
| 10/17/96 |
| Added new flags: fLineInc, fFillInc for bidi support. updateFormat, pointToTextOffset, getBoundingRect, |
| and findNewInsertionOffset should now function correctly for non-Roman documents. Nothing else has been |
| modified to support intl text. |
| |
| 10/21/96 |
| Pushed paragraph formatting and caret positioning into LineLayout. In process of pushing |
| highlighting into LineLayout. |
| |
| 10/24/96 |
| Now getting paragraph styles from paragraph buffer. |
| |
| 7/7/97 |
| Up-arrow doesn't move to beginning of text if you're on the first line. |
| |
| 7/1/98 |
| No longer intersecting damaged rect with view rect in updateFormat. |
| */ |
| |
| /** |
| * This class implements MFormatter. It maintains a table of |
| * <tt>LayoutInfo</tt> instances which contain layout information |
| * for each line in the text. This class formats lines on demand, |
| * and creates a low-priority thread to format text in the background. |
| * Note that, at times, some text may not have been formatted, especially |
| * if the text is large. |
| * <p> |
| * The line table is an array of <tt>LayoutInfo</tt> objects, which expands as needed to hold |
| * all computed lines in the text. The line table consists of three |
| * regions: a "positive" region, a "gap," and a "negative" region. |
| * In the positive region, character and graphics offsets are positive |
| * integers, computed from the beginning of the text / display. In the |
| * gap, line table entries are null. New lines may be inserted into the gap. |
| * In the negative region, character and graphics offsets are negative; |
| * their absolute values indicate distances from the end of the text / display. |
| * The <tt>fLTPosEnd</tt> member gives the index in the line table of the |
| * last positive entry. The <tt>fLTNegStart</tt> gives the index of first |
| * negative entry. If there are no negative entries, <tt>fLTNegStart</tt> is |
| * equal to <tt>fLTSize</tt>, the size of the line table. |
| * <p> |
| * Changes to the line table occur only in the <tt>formatText()</tt> method. |
| * This method calls <tt>LineLayout.layout()</tt> for each line to format. |
| * |
| * @author John Raley |
| * |
| * @see MFormatter |
| * @see LineLayout |
| * @see LayoutContext |
| * @see LayoutInfo |
| */ |
| |
| final class AsyncFormatter extends MFormatter implements Runnable |
| { |
| static final String COPYRIGHT = |
| "(C) Copyright IBM Corp. 1998-1999 - All Rights Reserved"; |
| /** |
| * Text to format. |
| */ |
| private MConstText fText; |
| |
| /** |
| * Default values. |
| */ |
| private AttributeMap fDefaultValues; |
| |
| /** |
| * Font resolver. |
| */ |
| private FontResolver fFontResolver; |
| |
| /** |
| * Default character metric - used to set height of empty paragraphs |
| */ |
| private DefaultCharacterMetric fDefaultCharMetric; |
| |
| /** |
| * Length to which lines are formatted. |
| */ |
| private int fLineDim; |
| |
| /** |
| * Table of formatted lines. |
| */ |
| private LayoutInfo fLineTable[]; |
| |
| /** |
| * Size of line table. |
| */ |
| private int fLTSize = 10; // initial size must be > 0 |
| |
| /** |
| * Index of last positive entry in line table. |
| */ |
| private int fLTPosEnd; |
| |
| /** |
| * Index of first negative entry in line table. |
| */ |
| private int fLTNegStart; |
| |
| /** |
| * Length of text on which negative line offsets are based. |
| * @see #formatText |
| */ |
| private int fLTCurTextLen; |
| |
| /** |
| * Length of formatted text in fill direction, in pixels. |
| */ |
| private int fPixHeight; |
| |
| /** |
| * Length of formatted text including pseudoline. |
| */ |
| private int fFullPixHeight; |
| |
| private int fMinX; |
| private int fMaxX; |
| |
| /** |
| * <tt>true</tt> if lines should be formatted to fit line dimension. |
| */ |
| private boolean fWrap; |
| |
| /** |
| * <tt>true</tt> if characters run horizontally. |
| */ |
| private boolean fHLine = true; |
| |
| /** |
| * <tt>true</tt> if characters run from from low to high coordinates on line. |
| */ |
| private boolean fLineInc = true; |
| |
| /** |
| * <tt>true</tt> if lines run from low to high coordinates within page. |
| */ |
| private boolean fFillInc = true; |
| |
| /** |
| * Value returned from <tt>findLineAt()</tt> |
| * if pixel height precedes topmost line. |
| */ |
| private static final int kBeforeFirstLine = -2; |
| |
| /** |
| * Value returned from <tt>findLineAt()</tt> and <tt>getLineContaining()</tt> |
| * if offset / pixel height is after all existing lines. |
| */ |
| private static final int kAfterLastLine = -1; |
| |
| /** |
| * Thread which invokes formatter in the background. |
| */ |
| private Thread fDaemon; |
| |
| /** |
| * FontRenderContext to measure with. Currently not settable after |
| * construction. |
| */ |
| private FontRenderContext fFontRenderContext; |
| |
| /** |
| * Controls whether background formatting can run. |
| */ |
| private boolean fBgFormatAllowed = false; |
| |
| /** |
| * Cached line break object. |
| */ |
| private BreakIterator fLineBreak = null; |
| |
| /** |
| * Cached LineBreakMeasurer. |
| */ |
| private LineBreakMeasurer fCachedMeasurer = null; |
| private int fCachedMeasurerStart; |
| private int fCachedMeasurerLimit; |
| |
| // Some JDK's (Sun's 1.2.2) throw exceptions from |
| // LineBreakMeasurer.insertChar and deleteChar. This class |
| // detects this condition and doesn't use these two methods |
| // if they throw exceptions. |
| private static boolean fgCacheMeasurers = true; |
| |
| /** |
| * Current text time stamp. Used to maintain modification invariant. |
| */ |
| private int fCurTimeStamp; |
| |
| /** |
| * Cache of ParagraphRenderers. |
| */ |
| private Hashtable fRendererCache = new Hashtable(); |
| |
| /** |
| * Draw text inside a rectangle. Does not cache any formatting information. |
| * This is convenient for small amounts of text; comparable to TETextBox on the Mac. |
| * <p> |
| * @param text the text to draw |
| * @param g Graphics on which to draw |
| * @param drawRect rectangle in which text will be drawn |
| * @param fillInc if true, lines run from low to high coordinates in page. |
| * @param hLine if true, characters run horizontally within a line. |
| */ |
| |
| private static boolean isParagraphSeparator(char ch) { |
| |
| return ch == '\n' || ch == '\u2029'; |
| } |
| |
| /** |
| * Create an <tt>AsyncFormatter</tt>. |
| * @param text the text to format |
| * @param lineBound length to which lines are foramtted |
| * @param wrap <tt>true</tt> if text should be "line wrapped" (formatted to fit destination area) |
| */ |
| AsyncFormatter(MConstText text, |
| AttributeMap defaultValues, |
| int lineBound, |
| boolean wrap, |
| Graphics g) |
| { |
| fText = text; |
| fDefaultValues = defaultValues; |
| fFontResolver = new FontResolver(fDefaultValues); |
| |
| fLineDim = lineBound; |
| fWrap = wrap; |
| Graphics2D g2d = Graphics2DConversion.getGraphics2D(g); |
| fFontRenderContext = g2d.getFontRenderContext(); |
| |
| fDefaultCharMetric = new DefaultCharacterMetric(fFontResolver, |
| fFontRenderContext); |
| fLTCurTextLen = text.length(); |
| removeAllLines(); |
| |
| fDaemon = new Thread(this); |
| fDaemon.start(); |
| } |
| |
| public AttributeMap getDefaultValues() { |
| |
| return fDefaultValues; |
| } |
| |
| public void checkTimeStamp() { |
| String admonition = "Probably, you modified " + |
| "the text before calling stopBackgroundFormatting()."; |
| |
| if (fText.getTimeStamp() != fCurTimeStamp) { |
| throw new Error("Time stamp is out of sync. " + admonition); |
| } |
| if (fText.length() != fLTCurTextLen) { |
| throw new Error("Length changed unexpectedly. " + |
| "fText.length()="+fText.length()+"; "+ |
| "fLTCurTextLen="+fLTCurTextLen+"; "+ |
| "formatter="+this+"; "+ |
| "text="+fText); |
| } |
| } |
| |
| /** |
| * Specify whether to wrap lines using the line dimension. |
| * @param wrap if <tt>true</tt> lines will be wrapped; otherwise new lines will only be |
| * started when a newline is encountered. |
| */ |
| public synchronized void setWrap(boolean wrap) |
| { |
| if (wrap != fWrap) { |
| fWrap = wrap; |
| removeAllLines(); |
| } |
| } |
| |
| /** |
| * Return true if lines are wrapped using the line dimension. |
| * @see #setWrap |
| */ |
| public synchronized boolean wrap() |
| { |
| return fWrap; |
| } |
| |
| /** |
| * Specify the lineBound in pixels. If line wrapping is on, lines |
| * will be wrapped to this value. |
| * <p> |
| * |
| * @param lineBound the distance, in pixels, used to wrap lines. |
| */ |
| public synchronized void setLineBound(int lineBound) |
| { |
| if (fLineDim != lineBound) { |
| fLineDim = lineBound; |
| if (fWrap) { |
| removeAllLines(); |
| } |
| } |
| } |
| |
| /** |
| * Return the number of pixels along the line dimension. |
| */ |
| public synchronized int lineBound() |
| { |
| return fLineDim; |
| } |
| |
| /** |
| * Return <tt>true</tt> if characters run from low to high coordinates on line. |
| */ |
| private boolean lineInc() |
| { |
| return fLineInc; |
| } |
| |
| /** |
| * Return <tt>true</tt> if lines run from low to high coordinates on page. |
| */ |
| private boolean fillInc() |
| { |
| return fFillInc; |
| } |
| |
| /** |
| * Remove all lines in the line table. Used after an operation that |
| * invalidates all existing lines, such as changing line wrapping or the |
| * line dim. |
| */ |
| private synchronized void removeAllLines() |
| { |
| fCurTimeStamp = fText.getTimeStamp(); |
| stopBackgroundFormatting(); |
| |
| fMinX = 0; |
| fMaxX = fLineDim; |
| |
| fLineTable = new LayoutInfo[fLTSize]; // fLTSize must be > 0 |
| fLTNegStart = fLTSize; |
| fLTPosEnd = 0; |
| |
| fLineTable[0] = pseudoLineInfo(null, 0); |
| |
| fPixHeight = fLineTable[0].getHeight(); // ??? or should it be zero? |
| fFullPixHeight = fPixHeight; |
| |
| // format at least one line: |
| formatToHeight(fPixHeight + 1); |
| |
| enableBGFormat(); |
| } |
| |
| /** |
| * Fill the layout info with information appropriate to the pseudoline. |
| */ |
| private synchronized LayoutInfo pseudoLineInfo(LayoutInfo info, int offset) |
| { |
| AttributeMap st = fText.paragraphStyleAt(fLTCurTextLen); // ??? if text is empty or this is the end of the text, what happens? |
| ParagraphRenderer renderer = getRendererFor(st); |
| info = renderer.layout(fText, |
| info, |
| (LineBreakMeasurer)null, |
| fFontRenderContext, |
| offset, |
| offset, |
| fLineDim, |
| fLineDim); |
| |
| return info; |
| } |
| |
| /** |
| * Return the index of the last valid line in the line table. |
| */ |
| private int lastLine() |
| { |
| return (fLTNegStart == fLTSize) ? fLTPosEnd : fLTSize - 1; |
| } |
| |
| /** |
| * Shift line table such that <tt>lastPos</tt> is the last positive |
| * entry in the table. <b>NOTE: <tt>lastPos</tt> must be a valid line!</b> |
| * <p> |
| * @param lastPos the index of the line which will become the last positive |
| * entry in the line table |
| */ |
| private void shiftTableTo(int lastPos) |
| { |
| LayoutInfo li; |
| |
| while (lastPos < fLTPosEnd) { // shift +'s to -'s |
| li = fLineTable[fLTPosEnd]; |
| fLineTable[fLTPosEnd--] = null; |
| |
| li.makeRelativeToEnd(fLTCurTextLen, fPixHeight); |
| |
| fLineTable[--fLTNegStart] = li; |
| } |
| |
| while (lastPos >= fLTNegStart) { // shift -'s to +'s |
| li = fLineTable[fLTNegStart]; |
| fLineTable[fLTNegStart++] = null; |
| |
| li.makeRelativeToBeginning(fLTCurTextLen, fPixHeight); |
| |
| fLineTable[++fLTPosEnd] = li; |
| } |
| } |
| |
| /** |
| * Increase the size of the line table. |
| */ |
| private void expandLineTable() |
| { |
| // This just doubles the size of the line table. |
| |
| LayoutInfo newLineTable[] = new LayoutInfo[fLineTable.length * 2]; |
| int newNegStart = newLineTable.length - (fLineTable.length - fLTNegStart); |
| |
| System.arraycopy(fLineTable, 0, newLineTable, 0, fLTPosEnd + 1); |
| System.arraycopy(fLineTable, fLTNegStart, newLineTable, newNegStart, fLTSize - fLTNegStart); |
| |
| fLTNegStart = newNegStart; |
| fLTSize = newLineTable.length; |
| fLineTable = newLineTable; |
| } |
| |
| /** |
| * Return the index of the line containing the pixel position <tt>fillCoord</tt>. |
| * If fillCoord exceeds the bottom of the text, return kAfterLastLine. |
| * If fillCoord is less than the top of the text, return kBeforeFirstLine. |
| * <p> |
| * @param fillCoord "height" of line to locate. |
| */ |
| private int findLineAt(int fillCoord) |
| { |
| int low, high, mid; |
| int lowStart, highStart, midStart; |
| |
| if (fillCoord >= fPixHeight) |
| return kAfterLastLine; |
| else if (fillCoord < 0) |
| return kBeforeFirstLine; |
| |
| if ((fLTNegStart < fLTSize) && (fillCoord >= fLineTable[fLTNegStart].getGraphicStart(fPixHeight))) { |
| fillCoord -= fPixHeight; |
| |
| low = fLTNegStart; |
| high = fLTSize; |
| highStart = 0; |
| } |
| else { |
| low = 0; |
| high = fLTPosEnd + 1; |
| highStart = fLineTable[fLTPosEnd].getGraphicStart(0) + fLineTable[fLTPosEnd].getHeight(); |
| } |
| lowStart = fLineTable[low].getGraphicStart(0); |
| |
| do { |
| if (lowStart == highStart) |
| return low; |
| |
| mid = low + (fillCoord - lowStart) / (highStart - lowStart) * (high - low); |
| midStart = fLineTable[mid].getGraphicStart(0); |
| |
| if (midStart > fillCoord) { |
| high = mid; |
| highStart = fLineTable[high].getGraphicStart(0); |
| } |
| else if (midStart + fLineTable[mid].getHeight() <= fillCoord) { |
| low = mid + 1; |
| lowStart = fLineTable[low].getGraphicStart(0); |
| } |
| else |
| return mid; |
| |
| } while (low < high); |
| |
| return 0; |
| } |
| |
| /** |
| * Return the index of the first character in the line. |
| * @param line the internal index of the line (direct index into linetable). |
| */ |
| private int lineCharStartInternal(int line) |
| { |
| return fLineTable[line].getCharStart(fLTCurTextLen); |
| } |
| |
| /** |
| * Return the index of the character following the last character in the line. |
| * @param line the internal index of the line (direct index into linetable). |
| */ |
| private int lineCharLimitInternal(int line) |
| { |
| return lineCharStartInternal(line) + fLineTable[line].getCharLength(); |
| } |
| |
| /** |
| * Return the graphic start of the line, unadjusted for fill direction. |
| * @param line the internal index of the line (direct index into linetable). |
| */ |
| private int lineGraphicStartInternal(int line) |
| { |
| return fLineTable[line].getGraphicStart(fPixHeight); |
| } |
| |
| /** |
| * Return the graphic limit of the line, unadjusted for fill direction. |
| * @param line the internal index of the line (direct index into linetable). |
| */ |
| private int lineGraphicLimitInternal(int line) |
| { |
| return lineGraphicStartInternal(line) + fLineTable[line].getHeight(); |
| } |
| |
| /** |
| * Return the offset of the first character which has not been formatted. |
| * If all text has been formatted, return the current text length. |
| */ |
| private int lastLineCharStop() |
| { |
| return lineCharLimitInternal(lastLine()); |
| } |
| |
| /** |
| * Return a 'valid' line containing offset. This differs from getLineContaining in |
| * this maps kAfterLastLine to lastLine(), so that the result is always a valid |
| * linetable index. |
| */ |
| private int getValidLineContaining(TextOffset offset) { |
| |
| return getValidLineContaining(offset.fOffset, offset.fPlacement); |
| } |
| |
| /** |
| * Return a 'valid' line containing offset. This differs from getLineContaining in |
| * this maps kAfterLastLine to lastLine(), so that the result is always a valid |
| * linetable index. |
| */ |
| private int getValidLineContaining(int insOffset, boolean placement) |
| { |
| int line = getLineContaining(insOffset, placement); |
| if (line == kAfterLastLine) |
| line = lastLine(); |
| else if (line == kBeforeFirstLine) |
| throw new IllegalArgumentException("Debug: getLineContaining returned kBeforeFirstLine"); |
| |
| return line; |
| } |
| |
| /** |
| * Return index of line containing <tt>offset</tt>. |
| * ??? If offset is after last formatted line, returns kAfterLastLine. Is that good? |
| * <p> |
| * @param offset the offset whose line should be located |
| * @returns line containing <tt>offset</tt> |
| */ |
| private int getLineContaining(TextOffset offset) { |
| |
| return getLineContaining(offset.fOffset, offset.fPlacement); |
| } |
| |
| private int getLineContaining(int insOffset, boolean placement) |
| { |
| int pos = insOffset; |
| if (placement == TextOffset.BEFORE_OFFSET && pos > 0) |
| --pos; |
| |
| if (pos < 0) { |
| throw new IllegalArgumentException("Debug: getLineContaining offset < 0: " + pos); |
| } |
| |
| if (pos >= lastLineCharStop()) { |
| return emptyParagraphAtEndOfText()? kAfterLastLine : lastLine(); |
| } |
| |
| int low, high, mid; |
| int lowStart, highStart, midStart; |
| |
| if ((fLTNegStart < fLTSize) && (pos >= fLineTable[fLTNegStart].getCharStart(fLTCurTextLen))) { |
| pos -= fLTCurTextLen; |
| |
| low = fLTNegStart; |
| high = fLTSize; |
| highStart = 0; |
| } |
| else { |
| low = 0; |
| high = fLTPosEnd + 1; |
| highStart = fLineTable[fLTPosEnd].getCharStart(0) + fLineTable[fLTPosEnd].getCharLength(); |
| } |
| lowStart = fLineTable[low].getCharStart(0); |
| |
| do { |
| if (highStart == lowStart) { |
| return low; |
| } |
| |
| mid = low + (pos - lowStart) / (highStart - lowStart) * (high - low); |
| midStart = fLineTable[mid].getCharStart(0); |
| |
| if (midStart > pos) { |
| high = mid; |
| highStart = fLineTable[high].getCharStart(0); |
| } |
| else if (midStart + fLineTable[mid].getCharLength() <= pos) { |
| low = mid + 1; |
| lowStart = fLineTable[low].getCharStart(0); |
| } |
| else { |
| return mid; |
| } |
| |
| } while (low < high); |
| |
| return 0; |
| } |
| |
| /** |
| * Display text in drawArea. Does not reformat text. |
| * <p> |
| * @param g the Graphics object in which to draw |
| * @param drawArea the rectangle, in g's coordinate system, in which to draw |
| * @param origin the top-left corner of the text, in g's coordinate system |
| */ |
| public synchronized void draw(Graphics g, Rectangle drawArea, Point origin) |
| { |
| draw(g, drawArea, origin, null, null, null); |
| } |
| |
| public synchronized void draw(Graphics g, Rectangle drawArea, Point origin, |
| TextOffset selStart, TextOffset selStop, Color highlight) { |
| |
| checkTimeStamp(); |
| Graphics2D g2d = Graphics2DConversion.getGraphics2D(g); |
| |
| // Get starting and ending fill 'heights.' |
| |
| int startFill; |
| int endFill; |
| |
| if (fFillInc) |
| startFill = drawArea.y - origin.y; |
| else |
| startFill = origin.y - (drawArea.y + drawArea.height); |
| |
| endFill = startFill + drawArea.height; |
| |
| // We're drawing one more line than necessary when we update because of a |
| // selection change. But we're drawing the right amount of lines when we |
| // refresh the whole display. This affects rendering speed significantly, |
| // and creating a new paragraph renderer for each line doesn't help either. |
| // For now, I'm going to subtract one from the fill height, on the theory |
| // that we're picking up the extra line because of a one-pixel slop. |
| // This seems to work, although perhaps if one pixel of a line at the |
| // bottom should draw, it won't. |
| |
| --endFill; |
| |
| // Format to ending fill height, so line table is valid for all lines we need to draw. |
| |
| formatToHeight(endFill); |
| |
| // Get starting and ending lines for fill height. If the start of the fill is after the last line, |
| // or the end of the fill is before the first line, return. |
| |
| int curLine = findLineAt(startFill); |
| if (curLine == kAfterLastLine) |
| return; |
| else if (curLine == kBeforeFirstLine) |
| curLine = 0; |
| |
| int lastLine = findLineAt(endFill); |
| if (lastLine == kBeforeFirstLine) |
| return; |
| else if (lastLine == kAfterLastLine) |
| lastLine = lastLine(); |
| |
| // Get the base coordinates (lineX, lineY) for the starting line. |
| |
| int lineX, lineY; |
| |
| int gStart = lineGraphicStartInternal(curLine); |
| |
| if (fHLine) { |
| if (fLineInc) |
| lineX = origin.x; |
| else |
| lineX = origin.x - fLineDim; |
| if (fFillInc) |
| lineY = origin.y + gStart; |
| else |
| lineY = origin.y - (gStart + fLineTable[curLine].getHeight()); |
| } |
| else { |
| if (fLineInc) |
| lineY = origin.y; |
| else |
| lineY = origin.y - fLineDim; |
| if (fFillInc) |
| lineX = origin.x + gStart; |
| else |
| lineX = origin.x - (gStart + fLineTable[curLine].getHeight()); |
| } |
| |
| // Iterate through lines, drawing each one and incrementing the base coordinate by the line height. |
| |
| |
| for (; curLine <= lastLine; curLine++) { |
| // Adjust curLine around gap in line table. |
| if ((curLine > fLTPosEnd) && (curLine < fLTNegStart)) |
| curLine = fLTNegStart; |
| |
| fLineTable[curLine].renderWithHighlight(fLTCurTextLen, g2d, fLineDim, lineX, lineY, selStart, selStop, highlight); |
| |
| // Increment line base for next iteration. |
| int lineInc = fLineTable[curLine].getHeight(); |
| if (fFillInc) { |
| if (fHLine) |
| lineY += lineInc; |
| else |
| lineX += lineInc; |
| } else { |
| if (fHLine) |
| lineY -= lineInc; |
| else |
| lineX -= lineInc; |
| } |
| } |
| } |
| |
| /** |
| * Format text to given height. |
| * @param height the height to which text will be formatted. |
| */ |
| public synchronized void formatToHeight(int reqHeight) |
| { |
| checkTimeStamp(); |
| if (reqHeight <= fPixHeight) // already formatted to this height |
| return; |
| |
| if (fText.length() == lastLineCharStop()) // already formatted all the text |
| return; |
| |
| // +++ should disable update thread here |
| |
| if (fLTNegStart < fLTSize) |
| shiftTableTo(fLTSize - 1); |
| |
| formatText(0, 0, reqHeight, false); |
| } |
| |
| /** |
| * Format text to given offset. |
| * @param offset the offset to which text will be formatted. |
| */ |
| private void formatToOffset(TextOffset offset) |
| { |
| formatToOffset(offset.fOffset, offset.fPlacement); |
| } |
| |
| private synchronized void formatToOffset(int offset, boolean placement) { |
| |
| checkTimeStamp(); |
| int llcs = lastLineCharStop(); |
| if (llcs < fLTCurTextLen) { |
| int limit = offset; |
| if (placement == TextOffset.AFTER_OFFSET) // format to past character offset is associated with |
| limit++; |
| if (limit >= llcs) { // ??? would '>' be ok instead or '>='? |
| if (limit > fLTCurTextLen) |
| limit = fLTCurTextLen; |
| |
| shiftTableTo(lastLine()); |
| formatText(llcs, limit - llcs, Integer.MAX_VALUE, true); |
| } |
| } |
| } |
| |
| /** |
| * Reformat text after a change. |
| * After the formatter's text changes, call this method to reformat. Does |
| * not redraw. |
| * @param afStart the offset into the text where modification began; ie, the |
| * first character in the text which is "different" in some way. Does not |
| * have to be nonnegative. |
| * @param afLength the number of new or changed characters in the text. Should never |
| * be less than 0. |
| * @param viewRect the Rectangle in which the text will be displayed. This is needed for |
| * returning the "damaged" area - the area of the screen in which the text must be redrawn. |
| * @param origin the top-left corner of the text, in the display's coordinate system |
| * @returns a <tt>Rectangle</tt> which specifies the area in which text must be |
| * redrawn to reflect the change to the text. |
| */ |
| public Rectangle updateFormat(final int afStart, |
| final int afLength, |
| Rectangle viewRect, |
| Point origin) |
| { |
| if (afStart < 0) { |
| throw new IllegalArgumentException("Debug: updateFormat afStart < 0: " + afStart); |
| } |
| if (fBgFormatAllowed) { |
| throw new IllegalArgumentException("Background formatting should have been disabled"); |
| } |
| fCurTimeStamp = fText.getTimeStamp(); |
| |
| int curLine = getValidLineContaining(afStart, TextOffset.AFTER_OFFSET); |
| int lineStartPos = lineCharStartInternal(curLine); |
| |
| // optimize by finding out whether change occurred |
| // after first word break on curline |
| |
| int firstPossibleBreak; |
| |
| if (lineStartPos < fText.length()) { |
| |
| if (fLineBreak == null) { |
| fLineBreak = BreakIterator.getLineInstance(); |
| } |
| CharacterIterator charIter = fText.createCharacterIterator(); |
| charIter.setIndex(lineStartPos); |
| fLineBreak.setText(charIter); |
| |
| firstPossibleBreak = fLineBreak.following(lineStartPos); |
| } |
| else |
| firstPossibleBreak = afStart; |
| |
| if ((curLine > 0) && (firstPossibleBreak == BreakIterator.DONE || afStart <= firstPossibleBreak)) { |
| curLine--; |
| if (curLine < fLTNegStart && curLine > fLTPosEnd) |
| curLine = fLTPosEnd; |
| } |
| |
| shiftTableTo(curLine); |
| |
| int pixHeight; // after the formatText call, at least pixHeight text must be formatted |
| |
| if (fHLine) { |
| if (fFillInc) |
| pixHeight = viewRect.y + viewRect.height - origin.y; |
| else |
| pixHeight = origin.y - viewRect.y; |
| } |
| else { |
| if (fFillInc) |
| pixHeight = viewRect.x + viewRect.width - origin.x; |
| else |
| pixHeight = origin.x - viewRect.x; |
| } |
| |
| Rectangle r = formatText(afStart, afLength, pixHeight, false); |
| |
| //dumpLineTable(); |
| |
| if ((fPixHeight < pixHeight) && (fLTNegStart < fLTSize) && (fLTCurTextLen > lastLineCharStop())) { |
| shiftTableTo(lastLine()); |
| Rectangle s = formatText(0, 0, pixHeight, false); |
| r = r.union(s); |
| } |
| |
| intlRect(origin, r); |
| //System.out.println("Damaged rect: "+r+"; origin: "+origin); |
| |
| // don't need to synchronized here, b/c the daemon shouldn't be running when |
| // this is executing |
| |
| if (fText.length() < lastLineCharStop()) |
| enableBGFormat(); |
| else |
| stopBackgroundFormatting(); |
| |
| //dumpLineTable(); |
| |
| return r; |
| } |
| |
| private LineBreakMeasurer makeMeasurer(int paragraphStart, int paragraphLimit) { |
| |
| MTextIterator iter = new MTextIterator(fText, |
| fFontResolver, |
| paragraphStart, |
| paragraphLimit); |
| LineBreakMeasurer measurer = new LineBreakMeasurer(iter, fFontRenderContext); |
| if (fgCacheMeasurers) { |
| fCachedMeasurerStart = paragraphStart; |
| fCachedMeasurerLimit = paragraphLimit; |
| fCachedMeasurer = measurer; |
| } |
| return measurer; |
| } |
| |
| /** |
| * Compute text format. This method calculates text format; it can be |
| * called for various purposes: to reformat text after an edit, to |
| * format text to a particular height, or to format text up to a |
| * particular offset. |
| * <p> |
| * The calling method must ensure that <tt>fLineTable</tt> has been shifted |
| * such that the last positive line is where the formatting operation will |
| * begin. |
| * <p> |
| * Called by: <tt>formatToHeight()</tt>, <tt>updateFormat()</tt>, |
| * <tt>textOffsetToPoint()</tt>, <tt>getBoundingRect()</tt> |
| * @param afStart the offset of the first character in the text which has changed |
| * @param afLength the number of new or changed characters in the text |
| * @param reqHeight the pixel height to which text must be formatted. Ignored |
| * if <tt>formatAllNewText</tt> is <tt>true</tt>, or if old lines remain in the |
| * line table after all changed text has been formatted. |
| * @param seekOffsetAtEnd if <tt>true</tt>, formatting continues until the line |
| * containing afStart+afLength has been formatted. If false, formatting may stop |
| * when reqHeight has been reached. This parameter should be <tt>true</tt> <b>only</b> |
| * if the object of the formatting operation is to extend formatting to a particular |
| * offset within the text; it should be <tt>false</tt> everywhere else. |
| * @returns a rectangle, relative to the top-left of the text, which encloses the |
| * screen area whose appearance has changed due to the reformatting. |
| */ |
| |
| private Rectangle formatText(int afStart, final int afLength, int reqHeight, boolean seekOffsetAtEnd) |
| { |
| /* assumes line table shifted such that first line to format is |
| last positive line */ |
| |
| if (afLength < 0) { |
| throw new IllegalArgumentException("afLength < 0. afLength=" + afLength); |
| } |
| |
| int newTextEnd = afStart + afLength; |
| |
| final int newCurTextLen = fText.length(); |
| |
| // variable not used int oldPixHeight = fPixHeight; |
| int oldFullPixHeight = fFullPixHeight; |
| fPixHeight -= fLineTable[fLTPosEnd].getHeight(); |
| |
| int curGraphicStart = fLineTable[fLTPosEnd].getGraphicStart(fPixHeight); |
| int curLineStart = fLineTable[fLTPosEnd].getCharStart(newCurTextLen); |
| |
| int curParagraphStart = fText.paragraphStart(curLineStart); |
| int curParagraphLimit = Integer.MIN_VALUE; // dummy value |
| |
| int damageStart = curGraphicStart; |
| |
| ParagraphRenderer renderer = null; |
| LineBreakMeasurer measurer = null; |
| |
| // try to use cached LineBreakMeasurer if possible |
| if (fCachedMeasurer != null && |
| curParagraphStart == fCachedMeasurerStart) { |
| |
| curParagraphLimit = fText.paragraphLimit(curParagraphStart); |
| |
| try { |
| if (newCurTextLen - fLTCurTextLen == 1 && afLength == 1) { |
| if (curParagraphLimit == fCachedMeasurerLimit+1) { |
| MTextIterator iter = new MTextIterator(fText, |
| fFontResolver, |
| curParagraphStart, |
| curParagraphLimit); |
| fCachedMeasurer.insertChar(iter, afStart); |
| fCachedMeasurerLimit += 1; |
| measurer = fCachedMeasurer; |
| } |
| } |
| else if (fLTCurTextLen - newCurTextLen == 1 && afLength == 0) { |
| if (fCachedMeasurerLimit > fCachedMeasurerStart + 1 && |
| curParagraphLimit == fCachedMeasurerLimit-1) { |
| MTextIterator iter = new MTextIterator(fText, |
| fFontResolver, |
| curParagraphStart, |
| curParagraphLimit); |
| fCachedMeasurer.deleteChar(iter, afStart); |
| fCachedMeasurerLimit -= 1; |
| measurer = fCachedMeasurer; |
| } |
| } |
| } |
| catch(ArrayIndexOutOfBoundsException e) { |
| fCachedMeasurer = null; |
| fgCacheMeasurers = false; |
| } |
| |
| if (measurer != null) { |
| // need to set up renderer since the paragraph update in the |
| // formatting loop will not happen |
| AttributeMap style = fText.paragraphStyleAt(curParagraphStart); |
| renderer = getRendererFor(style); |
| measurer.setPosition(curLineStart); |
| } |
| } |
| |
| if (measurer == null) { |
| // trigger paragraph update at start of formatting loop |
| curParagraphLimit = curParagraphStart; |
| curParagraphStart = 0; |
| } |
| |
| fLTCurTextLen = newCurTextLen; |
| |
| while (true) { |
| // System.out.println("line: " + fLTPosEnd + ", cls: " + curLineStart); |
| |
| |
| if (curLineStart >= curParagraphLimit) { |
| curParagraphStart = curParagraphLimit; |
| curParagraphLimit = fText.paragraphLimit(curParagraphStart); |
| |
| AttributeMap style = fText.paragraphStyleAt(curParagraphStart); |
| renderer = getRendererFor(style); |
| |
| if (curParagraphStart < curParagraphLimit) { |
| measurer = makeMeasurer(curParagraphStart, curParagraphLimit); |
| measurer.setPosition(curLineStart); |
| } |
| else { |
| measurer = null; |
| } |
| } |
| |
| { |
| boolean haveOldDirection = fLineTable[fLTPosEnd] != null; |
| boolean oldDirection = false; // dummy value for compiler |
| if (haveOldDirection) { |
| oldDirection = fLineTable[fLTPosEnd].isLeftToRight(); |
| } |
| |
| fLineTable[fLTPosEnd] = renderer.layout(fText, |
| fLineTable[fLTPosEnd], |
| measurer, |
| fFontRenderContext, |
| curParagraphStart, |
| curParagraphLimit, |
| fWrap ? fLineDim : Integer.MAX_VALUE, |
| fLineDim); |
| if (haveOldDirection) { |
| if (fLineTable[fLTPosEnd].isLeftToRight() != oldDirection) { |
| newTextEnd = Math.max(newTextEnd, curParagraphLimit); |
| } |
| } |
| } |
| |
| { |
| LayoutInfo theLine = fLineTable[fLTPosEnd]; |
| |
| theLine.setGraphicStart(curGraphicStart); |
| curGraphicStart += theLine.getHeight(); |
| |
| fPixHeight += theLine.getHeight(); |
| curLineStart += theLine.getCharLength(); |
| |
| if (!fWrap) { |
| int lineWidth = theLine.getTotalAdvance() + theLine.getLeadingMargin(); |
| if (theLine.isLeftToRight()) { |
| if (fMaxX < lineWidth) { |
| fMaxX = lineWidth; |
| } |
| } |
| else { |
| if (fLineDim-lineWidth < fMinX) { |
| fMinX = fLineDim-lineWidth; |
| } |
| } |
| } |
| } |
| /* |
| Next, discard obsolete lines. A line is obsolete if it |
| contains new text or text which has been formatted. |
| */ |
| |
| while (fLTNegStart < fLTSize) { |
| int linePos = fLineTable[fLTNegStart].getCharStart(newCurTextLen); |
| if (linePos >= curLineStart && linePos >= newTextEnd) |
| break; |
| |
| // System.out.println("delete neg line: " + fLTNegStart); |
| fPixHeight -= fLineTable[fLTNegStart].getHeight(); |
| fLineTable[fLTNegStart++] = null; |
| } |
| |
| int stopAt; |
| if (fLTNegStart < fLTSize) |
| stopAt = fLineTable[fLTNegStart].getCharStart(newCurTextLen); |
| else |
| stopAt = newCurTextLen; |
| |
| /* |
| Now, if exit conditions aren't met, create a new line. |
| */ |
| |
| if (seekOffsetAtEnd) { |
| if ((curLineStart >= newTextEnd) && (fLTNegStart == fLTSize)) { |
| // System.out.println("break 1"); |
| break; |
| } |
| } |
| else { |
| if (curLineStart >= stopAt) { |
| // System.out.println("curLineStart: " + curLineStart + " >= stopAt: " + stopAt); |
| break; |
| } |
| else if (fLTNegStart==fLTSize && fPixHeight >= reqHeight) { |
| // System.out.println("break 3"); |
| break; |
| } |
| } |
| |
| if (fLTPosEnd + 1 == fLTNegStart) |
| expandLineTable(); |
| |
| fLineTable[++fLTPosEnd] = null; // will be created by Renderer |
| } |
| //System.out.print("\n"); |
| |
| if (newCurTextLen == 0) { |
| fLineTable[0] = pseudoLineInfo(fLineTable[0], 0); |
| fPixHeight = fLineTable[0].getHeight(); |
| } |
| fFullPixHeight = fPixHeight; |
| |
| if (isParaBreakBefore(newCurTextLen)) { |
| fFullPixHeight += lastCharHeight(); |
| } |
| /* |
| System.out.println("curLineStart: " + curLineStart + |
| ", fLTPosEnd: " + fLTPosEnd + |
| ", fLTNegStart: " + fLTNegStart + |
| ", fLTSize: " + fLTSize); |
| |
| System.out.println("oldFullPixHeight: " + oldFullPixHeight + ", newFullPixHeight: " + fFullPixHeight); |
| */ |
| int damageLength; |
| if (fFullPixHeight == oldFullPixHeight) { |
| damageLength = fLineTable[fLTPosEnd].getGraphicStart(fPixHeight) |
| + fLineTable[fLTPosEnd].getHeight() - damageStart; |
| } |
| else { |
| damageLength = Math.max(fFullPixHeight, oldFullPixHeight); |
| } |
| |
| return new Rectangle(fMinX, damageStart, fMaxX-fMinX, damageLength); |
| } |
| |
| private void dumpLineTable() |
| { |
| int i; |
| |
| System.out.println("fLTCurTextLen=" + fLTCurTextLen + " " ); |
| for (i=0; i<= fLTPosEnd; i++) |
| System.out.println("Line " + i + " starts at " |
| + fLineTable[i].getCharStart(fLTCurTextLen) |
| + " and extends " + fLineTable[i].getCharLength()); |
| |
| for (i=fLTNegStart; i< fLTSize; i++) |
| System.out.println("Line " + (i-fLTNegStart+fLTPosEnd+1) + " starts at " |
| + fLineTable[i].getCharStart(fLTCurTextLen) |
| + " and extends " + fLineTable[i].getCharLength()); |
| } |
| |
| public synchronized int minX() { |
| |
| return fMinX; |
| } |
| |
| /** |
| * Return the horizontal extent of the text, in pixels. |
| * <p> |
| * This returns an approximation based on the currently formatted text. |
| */ |
| public synchronized int maxX() |
| { |
| checkTimeStamp(); |
| |
| return fMaxX; |
| } |
| |
| /** |
| * Return the height of the last character in the text. |
| * |
| * This is used for the 'extra height' needed to display a caret at the end of the text when the |
| * text is empty or ends with a newline. |
| */ |
| private int lastCharHeight() |
| { |
| int charIndex = lastLineCharStop() - 1; |
| AttributeMap st = fText.characterStyleAt(charIndex); |
| DefaultCharacterMetric.Metric metric = fDefaultCharMetric.getMetricForStyle(st); |
| |
| int height = metric.getAscent(); |
| height += metric.getDescent(); |
| height += metric.getLeading(); |
| |
| return height; |
| } |
| |
| /** |
| * Return true if the character at pos is a paragraph separator. |
| */ |
| private boolean isParaBreakBefore(int pos) |
| { |
| return pos > 0 && (fText.at(pos - 1) == '\u2029' || fText.at(pos - 1) == '\n'); |
| // we really need to take look at this and determine what this function |
| // should be doing. What I've got here right now is a temporary implementation. |
| } |
| |
| public synchronized int minY() { |
| |
| return 0; |
| } |
| |
| /** |
| * Return the vertical extent of the text, in pixels. |
| * <p> |
| * This returns an approximation based on the currently formatted text. |
| */ |
| public synchronized int maxY() |
| { |
| checkTimeStamp(); |
| |
| int numChars = lastLineCharStop(); |
| |
| int pixHeight = fPixHeight; |
| if (numChars == fLTCurTextLen && isParaBreakBefore(fLTCurTextLen)) { |
| pixHeight += lastCharHeight(); |
| } |
| |
| if (numChars != 0) |
| return pixHeight * fText.length() / numChars; |
| else |
| return 0; |
| } |
| |
| /** |
| * Return the actual pixel length of the text which has been formatted. |
| */ |
| public synchronized int formattedHeight() |
| { |
| checkTimeStamp(); |
| return fPixHeight; |
| } |
| |
| /** |
| * There are two modes for dealing with carriage returns at the end of a line. In the 'infinite width' |
| * mode, the last character is considered to have infinite width. Thus if the point is past the 'real' |
| * end of the line, the offset is the position before that last character, and the offset is associated |
| * with that character (placement after). In the 'actual width' mode, the offset is positioned after |
| * that character, but still associated with it (placement before). |
| */ |
| |
| private TextOffset lineDimToOffset(TextOffset result, int line, int lineX, int lineY, TextOffset anchor, boolean infiniteMode) |
| { |
| // temporarily adjust line info to remove the negative char starts used in the line table. |
| // then call through to the paragraph renderer to get the offset. Don't put line end |
| // optimization here, let the renderer do it (perhaps it does fancy stuff with the margins). |
| |
| LayoutInfo lineInfo = fLineTable[line]; |
| |
| result = lineInfo.pixelToOffset(fLTCurTextLen, result, fLineDim, lineX, lineY); |
| |
| if (infiniteMode && |
| (result.fOffset > lineInfo.getCharStart(fLTCurTextLen)) && |
| isParaBreakBefore(result.fOffset) && |
| (anchor == null || anchor.fOffset == result.fOffset - 1)) { |
| |
| result.setOffset(result.fOffset - 1, TextOffset.AFTER_OFFSET); |
| } |
| |
| return result; |
| } |
| |
| /** |
| * Given a screen location p, return the offset of the character in the text nearest to p. |
| */ |
| public synchronized TextOffset pointToTextOffset(TextOffset result, int px, int py, Point origin, TextOffset anchor, boolean infiniteMode) |
| { |
| checkTimeStamp(); |
| if (result == null) |
| result = new TextOffset(); |
| |
| int fillD; |
| |
| if (fHLine) |
| fillD = py - origin.y; |
| else |
| fillD = px - origin.x; |
| |
| if (!fFillInc) |
| fillD = -fillD; |
| |
| if (fillD < 0) { |
| result.setOffset(0, TextOffset.AFTER_OFFSET); |
| return result; |
| } |
| |
| formatToHeight(fillD); |
| |
| if (fillD >= fPixHeight) { |
| boolean bias = fLTCurTextLen==0? TextOffset.AFTER_OFFSET : TextOffset.BEFORE_OFFSET; |
| result.setOffset(fLTCurTextLen, bias); |
| return result; |
| } |
| |
| int line = findLineAt(fillD); // always a valid line |
| int gStart = lineGraphicStartInternal(line); |
| |
| int lineX, lineY; // upper-left corner of line |
| if (fHLine) { |
| lineX = origin.x; |
| lineY = fFillInc? origin.y + gStart : origin.y - (gStart + fLineTable[line].getHeight()); |
| } |
| else { |
| lineY = origin.y; |
| lineX = fFillInc? origin.x + gStart : origin.x - (gStart + fLineTable[line].getHeight()); |
| } |
| |
| return lineDimToOffset(result, line, px - lineX, py - lineY, anchor, infiniteMode); |
| } |
| |
| private boolean emptyParagraphAtEndOfText() { |
| |
| return fLTCurTextLen > 0 && |
| isParagraphSeparator(fText.at(fLTCurTextLen-1)); |
| } |
| |
| /** |
| * Return true if the offset designates a point on the pseudoline following a paragraph |
| * separator at the end of text. This is true if the offset is the end of text |
| * and the last character in the text is a paragraph separator. |
| */ |
| private boolean afterLastParagraph(TextOffset offset) |
| { |
| return offset.fOffset == fLTCurTextLen && |
| emptyParagraphAtEndOfText(); |
| } |
| |
| /** |
| * Given an offset, return the Rectangle bounding the caret at the offset. |
| * @param offset an offset into the text |
| * @param origin the top-left corner of the text, in the display's coordinate system |
| * @return a Rectangle bounding the caret. |
| */ |
| public synchronized Rectangle getCaretRect(TextOffset offset, Point origin) { |
| |
| Rectangle r = new Rectangle(); |
| getCaretRect(r, offset, origin); |
| return r; |
| } |
| |
| private void getCaretRect(Rectangle r, TextOffset offset, Point origin) { |
| |
| checkTimeStamp(); |
| formatToOffset(offset); |
| |
| if (afterLastParagraph(offset)) { |
| int pseudoLineHeight = lastCharHeight(); |
| if (fHLine) { |
| int lineY = fFillInc ? origin.y + fPixHeight : origin.y - fPixHeight - pseudoLineHeight; |
| r.setBounds(origin.x, lineY, 0, pseudoLineHeight); |
| } |
| else { |
| int lineX = fFillInc? origin.x + fPixHeight : origin.x - fPixHeight - pseudoLineHeight; |
| r.setBounds(lineX, origin.y, pseudoLineHeight, 0); |
| } |
| return; |
| } |
| |
| int line = getValidLineContaining(offset); |
| |
| int gStart = lineGraphicStartInternal(line); |
| |
| int lineX, lineY; |
| |
| if (fHLine) { |
| lineX = origin.x; |
| if (fFillInc) |
| lineY = origin.y + gStart; |
| else |
| lineY = origin.y - (gStart + fLineTable[line].getHeight()); |
| } |
| else { |
| lineY = origin.y; |
| if (fFillInc) |
| lineX = origin.x + gStart; |
| else |
| lineX = origin.x - (gStart + fLineTable[line].getHeight()); |
| } |
| |
| Rectangle bounds = fLineTable[line].caretBounds(fText, fLTCurTextLen, fLineDim, offset.fOffset, lineX, lineY); |
| |
| r.setBounds(bounds); |
| } |
| |
| /** |
| * Draw the caret(s) associated with the given offset into the given Graphics. |
| * @param g the Graphics to draw into |
| * @param offset the offset in the text for which the caret is drawn |
| * @param origin the top-left corner of the text, in the display's coordinate system |
| * @param strongCaretColor the color of the strong caret |
| * @param weakCaretColor the color of the weak caret (if any) |
| */ |
| public synchronized void drawCaret(Graphics g, |
| TextOffset offset, |
| Point origin, |
| Color strongCaretColor, |
| Color weakCaretColor) { |
| |
| checkTimeStamp(); |
| Graphics2D g2d = Graphics2DConversion.getGraphics2D(g); |
| formatToOffset(offset); |
| |
| LayoutInfo line; |
| int gStart; |
| |
| if (afterLastParagraph(offset)) { |
| gStart = fPixHeight; |
| line = pseudoLineInfo(null, offset.fOffset); |
| } |
| else { |
| int lineIndex = getValidLineContaining(offset); |
| gStart = lineGraphicStartInternal(lineIndex); |
| line = fLineTable[lineIndex]; |
| } |
| |
| int lineX, lineY; |
| |
| if (fHLine) { |
| lineX = origin.x; |
| if (fFillInc) |
| lineY = origin.y + gStart; |
| else |
| lineY = origin.y - (gStart + line.getHeight()); |
| } |
| else { |
| lineY = origin.y; |
| if (fFillInc) |
| lineX = origin.x + gStart; |
| else |
| lineX = origin.x - (gStart + line.getHeight()); |
| } |
| |
| line.renderCaret(fText, fLTCurTextLen, g2d, fLineDim, lineX, lineY, |
| offset.fOffset, strongCaretColor, weakCaretColor); |
| } |
| |
| /** |
| * Given two offsets in the text, return a rectangle which encloses the lines containing the offsets. |
| * Offsets do not need to be ordered or nonnegative. |
| * @param offset1,offset2 offsets into the text |
| * @param origin the top-left corner of the text, in the display's coordinate system |
| * @returns a <tt>Rectangle</tt>, relative to <tt>origin</tt>, which encloses the lines containing the offsets |
| */ |
| public synchronized Rectangle getBoundingRect(TextOffset offset1, |
| TextOffset offset2, |
| Point origin, |
| boolean tight) { |
| |
| Rectangle r = new Rectangle(); |
| getBoundingRect(r, offset1, offset2, origin, tight); |
| return r; |
| } |
| |
| /* |
| Transform r from "text" coordinates to "screen" coordinates. |
| */ |
| |
| private void intlRect(Point origin, Rectangle r) { |
| |
| int lineOrig, fillOrig; |
| |
| if (fHLine) { |
| lineOrig = origin.x; |
| fillOrig = origin.y; |
| } |
| else { |
| lineOrig = origin.y; |
| fillOrig = origin.x; |
| } |
| |
| if (fLineInc) |
| r.x += lineOrig; |
| else |
| r.x = lineOrig - (r.x + r.width); |
| |
| if (fFillInc) |
| r.y += fillOrig; |
| else |
| r.y = fillOrig - (r.y + r.height); |
| |
| |
| if (!fHLine) { |
| int t = r.x; |
| r.x = r.y; |
| r.y = t; |
| t = r.width; |
| r.width = r.height; |
| r.height = t; |
| } |
| } |
| |
| |
| public synchronized void getBoundingRect(Rectangle r, |
| TextOffset offset1, |
| TextOffset offset2, |
| Point origin, |
| boolean tight) |
| { |
| checkTimeStamp(); |
| if (offset1.equals(offset2)) { |
| getCaretRect(r, offset1, origin); |
| return; |
| } |
| if (offset1.greaterThan(offset2)) { |
| TextOffset t; t = offset1; offset1 = offset2; offset2 = t; |
| } |
| |
| formatToOffset(offset2); |
| |
| int line = getValidLineContaining(offset1); |
| r.y = lineGraphicStartInternal(line); |
| |
| int gLimit; |
| boolean sameLine = false; |
| |
| if (afterLastParagraph(offset2)) |
| gLimit = fPixHeight + lastCharHeight(); |
| else { |
| int line2 = getValidLineContaining(offset2); |
| gLimit = lineGraphicLimitInternal(line2); |
| sameLine = (line == line2); |
| } |
| |
| r.height = gLimit - r.y; |
| |
| if (sameLine && tight==TIGHT) { |
| Rectangle rt = new Rectangle(); |
| getCaretRect(rt, offset1, origin); |
| r.setBounds(rt); |
| if (!offset1.equals(offset2)) { |
| getCaretRect(rt, offset2, origin); |
| r.add(rt); |
| } |
| } |
| else { |
| r.x = fMinX; |
| r.width = fMaxX - fMinX; |
| intlRect(origin, r); |
| } |
| |
| // System.out.print("gbr: " + r.x + ", " + r.y + ", " + r.width + ", " + r.height); |
| |
| // System.out.println(" --> " + r.x + ", " + r.y + ", " + r.width + ", " + r.height); |
| } |
| |
| /** |
| * Compute the offset resulting from moving from a previous offset in direction dir. |
| * For arrow keys. |
| * @param result the offset to modify and return. may be null, if so a new offset is allocated, modified, and returned. |
| * @param previousOffset the insertion offset prior to the arrow key press. |
| * @param direction the direction of the arrow key (eUp, eDown, eLeft, or eRight) |
| * @returns new offset based on direction and previous offset. |
| */ |
| public synchronized TextOffset findInsertionOffset(TextOffset result, TextOffset prevOffset, short dir) |
| { |
| return findNewInsertionOffset(result, prevOffset, prevOffset, dir); |
| } |
| |
| /** |
| * Transform key direction: after this step, "left" means previous glyph, "right" means next glyph, |
| *"up" means previous line, "down" means next line |
| */ |
| private short remapArrowKey(short dir) { |
| |
| if (!fLineInc) { |
| if (dir == eLeft) |
| dir = eRight; |
| else if (dir == eRight) |
| dir = eLeft; |
| } |
| |
| if (!fFillInc) { |
| if (dir == eUp) |
| dir = eDown; |
| else if (dir == eDown) |
| dir = eUp; |
| } |
| |
| if (!fHLine) { |
| if (dir == eLeft) |
| dir = eUp; |
| else if (dir == eRight) |
| dir = eDown; |
| else if (dir == eUp) |
| dir = eLeft; |
| else if (dir == eDown) |
| dir = eRight; |
| } |
| |
| return dir; |
| } |
| |
| /** |
| * Compute the offset resulting from moving from a previous offset, starting at an original offset, in direction dir. |
| * For arrow keys. Use this for "smart" up/down keys. |
| * @param result the offset to modify and return. May be null, if so a new offset is allocated, modified, and returned. |
| * @param origOffset the offset at which an up-down arrow key sequence began. |
| * @param prevOffset the insertion offset prior to the arrow key press |
| * @param dir the direction of the arrow key (eUp, eDown, eLeft, or eRight) |
| * @returns new offset based on direction, original offset, and previous offset. |
| */ |
| public synchronized TextOffset findNewInsertionOffset(TextOffset result, TextOffset origOffset, TextOffset prevOffset, short dir) |
| { |
| checkTimeStamp(); |
| if (result == null) |
| result = new TextOffset(); |
| |
| dir = remapArrowKey(dir); |
| |
| // assume that text at origOffset and prevOffset has already been formatted |
| |
| if (dir == eLeft || dir == eRight) { |
| formatToOffset(prevOffset); |
| int line = getValidLineContaining(prevOffset); |
| |
| result.fPlacement = TextOffset.AFTER_OFFSET; |
| result.fOffset = fLineTable[line].getNextOffset(fLTCurTextLen, prevOffset.fOffset, dir); |
| if (result.fOffset < 0) { |
| result.fOffset = 0; |
| } |
| else if (result.fOffset >= fLTCurTextLen) { |
| result.setOffset(fLTCurTextLen, TextOffset.BEFORE_OFFSET); |
| } |
| } |
| else { |
| int distOnLine; |
| |
| if (afterLastParagraph(origOffset)) |
| distOnLine = 0; |
| else { |
| int line = getValidLineContaining(origOffset); |
| |
| distOnLine = fLineTable[line].strongCaretBaselinePosition(fLTCurTextLen, fLineDim, origOffset.fOffset); |
| } |
| |
| // get prevOffset's line |
| int line; |
| if (afterLastParagraph(prevOffset)) |
| line = lastLine() + 1; |
| else { |
| line = getLineContaining(prevOffset); |
| |
| if (dir == eDown && (line == kAfterLastLine || line == lastLine()) |
| && (lastLineCharStop() < fText.length())) { |
| shiftTableTo(lastLine()); |
| formatText(lastLineCharStop(), 1, Integer.MAX_VALUE, true); |
| line = getLineContaining(prevOffset); |
| } |
| |
| if (line == kBeforeFirstLine) |
| line = 0; |
| else if (line == kAfterLastLine) |
| line = lastLine(); |
| } |
| |
| if (dir == eUp) |
| line--; |
| else if (dir == eDown) |
| line++; |
| else |
| throw new IllegalArgumentException("Debug: Illegal direction parameter in findNewInsertionOffset"); |
| |
| if (line < 0) { |
| //result.setOffset(0, TextOffset.AFTER_OFFSET); |
| result.assign(prevOffset); |
| } |
| else if (line > lastLine()) { |
| result.setOffset(fLTCurTextLen, TextOffset.BEFORE_OFFSET); |
| } |
| else { |
| if (fLineTable[line] == null) |
| line = (dir == eUp)? fLTPosEnd : fLTNegStart; |
| |
| // anchor is null since we never want a position after newline. If we used the real anchor, |
| // we might not ignore the newline even though infiniteMode is true. |
| lineDimToOffset(result, line, distOnLine, 0, null, true); |
| } |
| } |
| |
| // System.out.println("fnio prev: " + prevOffset + ", new: " + result); |
| |
| return result; |
| } |
| |
| public synchronized void stopBackgroundFormatting() |
| { |
| checkTimeStamp(); |
| fBgFormatAllowed = false; |
| } |
| |
| private synchronized void enableBGFormat() |
| { |
| try { |
| fBgFormatAllowed = true; |
| notify(); |
| } |
| catch (IllegalMonitorStateException e) { |
| } |
| } |
| |
| private int lineIndexToNumber(int lineIndex) { |
| |
| if (lineIndex <= fLTPosEnd) { |
| return lineIndex; |
| } |
| else { |
| return lineIndex - (fLTNegStart-fLTPosEnd-1); |
| } |
| } |
| |
| private int lineNumberToIndex(int lineNumber) { |
| |
| if (lineNumber <= fLTPosEnd) { |
| return lineNumber; |
| } |
| else { |
| return lineNumber + (fLTNegStart-fLTPosEnd-1); |
| } |
| } |
| |
| private void formatToLineNumber(int lineNumber) { |
| |
| while (lastLineCharStop() < fLTCurTextLen && |
| lineNumber >= lineIndexToNumber(fLTSize)) { |
| // could be smarter and choose larger amounts for |
| // larger lines, but probably not worth the effort |
| formatToHeight(fPixHeight + kPixIncrement); |
| } |
| } |
| |
| private static final boolean STRICT = true; |
| private static final boolean LENIENT = false; |
| |
| /** |
| * Insure that at least lineNumber lines exist, doing |
| * extra formatting if necessary. |
| * Throws exception if lineNumber is not valid. |
| * @param strict if STRICT, only lines [0...maxLineNumber()] |
| * are permitted. If LENIENT, maxLineNumber()+1 is |
| * the greatest valid value. |
| */ |
| private void validateLineNumber(int lineNumber, boolean strict) { |
| |
| formatToLineNumber(lineNumber); |
| |
| int maxNumber = lineIndexToNumber(fLTSize); |
| if (strict == STRICT) { |
| maxNumber -= 1; |
| } |
| |
| if (lineNumber > maxNumber+1 || |
| (lineNumber == maxNumber+1 && !emptyParagraphAtEndOfText())) { |
| throw new IllegalArgumentException("Invalid line number: " + lineNumber); |
| } |
| } |
| |
| public synchronized int getLineCount() { |
| |
| // format all text: |
| formatToHeight(Integer.MAX_VALUE); |
| |
| int lineCount = lineIndexToNumber(fLTSize); |
| |
| if (emptyParagraphAtEndOfText()) { |
| lineCount += 1; |
| } |
| |
| return lineCount; |
| } |
| |
| public synchronized int lineContaining(int charIndex) { |
| |
| formatToOffset(charIndex, TextOffset.AFTER_OFFSET); |
| |
| boolean placement = TextOffset.AFTER_OFFSET; |
| if (charIndex == fLTCurTextLen && charIndex > 0) { |
| placement = emptyParagraphAtEndOfText()? TextOffset.AFTER_OFFSET : |
| TextOffset.BEFORE_OFFSET; |
| } |
| |
| return lineContaining(charIndex, placement); |
| } |
| |
| public synchronized int lineContaining(TextOffset offset) { |
| |
| formatToOffset(offset); |
| |
| if (afterLastParagraph(offset)) { |
| return lineIndexToNumber(fLTSize); |
| } |
| |
| return lineContaining(offset.fOffset, offset.fPlacement); |
| } |
| |
| private int lineContaining(int off, boolean placement) { |
| |
| int line = off==0? 0 : getLineContaining(off, placement); |
| |
| if (line == kAfterLastLine) { |
| line = fLTSize; |
| } |
| else if (line == kBeforeFirstLine) { |
| throw new Error("lineContaining got invalid result from getLineContaining()."); |
| } |
| |
| return lineIndexToNumber(line); |
| } |
| |
| public synchronized int lineRangeLow(int lineNumber) { |
| |
| validateLineNumber(lineNumber, STRICT); |
| int index = lineNumberToIndex(lineNumber); |
| |
| if (index == fLTSize) { |
| if (emptyParagraphAtEndOfText()) { |
| return lastLineCharStop(); |
| } |
| } |
| |
| if (index >= fLTSize) { |
| throw new IllegalArgumentException("lineNumber is invalid."); |
| } |
| else { |
| return lineCharStartInternal(index); |
| } |
| } |
| |
| public synchronized int lineRangeLimit(int lineNumber) { |
| |
| validateLineNumber(lineNumber, STRICT); |
| int index = lineNumberToIndex(lineNumber); |
| |
| if (index == fLTSize) { |
| if (emptyParagraphAtEndOfText()) { |
| return lastLineCharStop(); |
| } |
| } |
| |
| if (index >= fLTSize) { |
| throw new IllegalArgumentException("lineNumber is invalid."); |
| } |
| else { |
| return lineCharLimitInternal(index); |
| } |
| } |
| |
| /** |
| * Return the number of the line at the given graphic height. |
| * If height is greater than full height, return line count. |
| */ |
| public synchronized int lineAtHeight(int height) { |
| |
| if (height >= fPixHeight) { |
| |
| int line = getLineCount(); |
| if (height < fFullPixHeight) { |
| line -= 1; |
| } |
| return line; |
| } |
| else if (height < 0) { |
| return -1; |
| } |
| else { |
| return lineIndexToNumber(findLineAt(height)); |
| } |
| } |
| |
| public synchronized int lineGraphicStart(int lineNumber) { |
| |
| checkTimeStamp(); |
| validateLineNumber(lineNumber, LENIENT); |
| |
| int index = lineNumberToIndex(lineNumber); |
| |
| if (index < fLTSize) { |
| return lineGraphicStartInternal(index); |
| } |
| else { |
| if (index == fLTSize+1) { |
| return fFullPixHeight; |
| } |
| else { |
| return fPixHeight; |
| } |
| } |
| } |
| |
| public synchronized boolean lineIsLeftToRight(int lineNumber) { |
| |
| validateLineNumber(lineNumber, STRICT); |
| |
| int index = lineNumberToIndex(lineNumber); |
| |
| if (index < fLTSize) { |
| return fLineTable[index].isLeftToRight(); |
| } |
| else { |
| AttributeMap st = fText.paragraphStyleAt(fLTCurTextLen); |
| return !TextAttribute.RUN_DIRECTION_RTL.equals(st.get(TextAttribute.RUN_DIRECTION)); |
| } |
| } |
| |
| /** |
| * Number of pixels by which to advance formatting in the background. |
| */ |
| private static final int kPixIncrement = 100; |
| |
| /** |
| * Time to sleep between background formatting operations. |
| */ |
| private static final int kInterval = 100; |
| |
| /** |
| * Perform periodic background formatting. |
| */ |
| public void run() |
| { |
| while (true) { |
| synchronized (this) { |
| while(!fBgFormatAllowed) { |
| try { |
| wait(); |
| } catch(InterruptedException e) { |
| } |
| } |
| |
| checkTimeStamp(); |
| formatToHeight(fPixHeight + kPixIncrement); |
| |
| if (lastLineCharStop() == fLTCurTextLen) { |
| stopBackgroundFormatting(); |
| } |
| } |
| |
| try { |
| Thread.sleep(kInterval); |
| } |
| catch(InterruptedException e) { |
| } |
| } |
| } |
| |
| private ParagraphRenderer getRendererFor(AttributeMap s) { |
| |
| // Note: eventually we could let clients put their own renderers |
| // on the text. |
| BidiParagraphRenderer renderer = (BidiParagraphRenderer) fRendererCache.get(s); |
| if (renderer == null) { |
| renderer = new BidiParagraphRenderer(fDefaultValues.addAttributes(s), fDefaultCharMetric); |
| fRendererCache.put(s, renderer); |
| } |
| return renderer; |
| } |
| |
| } |