- /*
- * @(#)TextMeasurer.java 1.39 03/12/19
- *
- * Copyright 2004 Sun Microsystems, Inc. All rights reserved.
- * SUN PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
- */
-
- /*
- * (C) Copyright Taligent, Inc. 1996 - 1997, All Rights Reserved
- * (C) Copyright IBM Corp. 1996 - 1998, All Rights Reserved
- *
- * The original version of this source code and documentation is
- * copyrighted and owned by Taligent, Inc., a wholly-owned subsidiary
- * of IBM. These materials are provided under terms of a License
- * Agreement between Taligent and Sun. This technology is protected
- * by multiple US and International patents.
- *
- * This notice and attribution to Taligent may not be removed.
- * Taligent is a registered trademark of Taligent, Inc.
- *
- */
-
- package java.awt.font;
-
- import java.awt.Font;
-
- import java.text.AttributedCharacterIterator;
- import java.text.AttributedString;
- import java.text.Bidi;
- import java.text.BreakIterator;
- import java.text.CharacterIterator;
-
- import java.awt.font.FontRenderContext;
-
- import java.util.Hashtable;
- import java.util.Map;
-
- import sun.font.BidiUtils;
- import sun.font.TextLineComponent;
- import sun.font.TextLabelFactory;
- import sun.font.FontResolver;
-
- /**
- * The <code>TextMeasurer</code> class provides the primitive operations
- * needed for line break: measuring up to a given advance, determining the
- * advance of a range of characters, and generating a
- * <code>TextLayout</code> for a range of characters. It also provides
- * methods for incremental editing of paragraphs.
- * <p>
- * A <code>TextMeasurer</code> object is constructed with an
- * {@link java.text.AttributedCharacterIterator AttributedCharacterIterator}
- * representing a single paragraph of text. The value returned by the
- * {@link AttributedCharacterIterator#getBeginIndex() getBeginIndex}
- * method of <code>AttributedCharacterIterator</code>
- * defines the absolute index of the first character. The value
- * returned by the
- * {@link AttributedCharacterIterator#getEndIndex() getEndIndex}
- * method of <code>AttributedCharacterIterator</code> defines the index
- * past the last character. These values define the range of indexes to
- * use in calls to the <code>TextMeasurer</code>. For example, calls to
- * get the advance of a range of text or the line break of a range of text
- * must use indexes between the beginning and end index values. Calls to
- * {@link #insertChar(java.text.AttributedCharacterIterator, int) insertChar}
- * and
- * {@link #deleteChar(java.text.AttributedCharacterIterator, int) deleteChar}
- * reset the <code>TextMeasurer</code> to use the beginning index and end
- * index of the <code>AttributedCharacterIterator</code> passed in those calls.
- * <p>
- * Most clients will use the more convenient <code>LineBreakMeasurer</code>,
- * which implements the standard line break policy (placing as many words
- * as will fit on each line).
- *
- * @author John Raley
- * @version 1.31, 04/20/01
- * @see LineBreakMeasurer
- * @since 1.3
- */
-
- public final class TextMeasurer implements Cloneable {
-
- // Number of lines to format to.
- private static float EST_LINES = (float) 2.1;
-
- /*
- static {
- String s = System.getProperty("estLines");
- if (s != null) {
- try {
- Float f = new Float(s);
- EST_LINES = f.floatValue();
- }
- catch(NumberFormatException e) {
- }
- }
- //System.out.println("EST_LINES="+EST_LINES);
- }
- */
-
- private FontRenderContext fFrc;
-
- private int fStart;
-
- // characters in source text
- private char[] fChars;
-
- // Bidi for this paragraph
- private Bidi fBidi;
-
- // Levels array for chars in this paragraph - needed to reorder
- // trailing counterdirectional whitespace
- private byte[] fLevels;
-
- // line components in logical order
- private TextLineComponent[] fComponents;
-
- // index where components begin
- private int fComponentStart;
-
- // index where components end
- private int fComponentLimit;
-
- private boolean haveLayoutWindow;
-
- // used to find valid starting points for line components
- private BreakIterator fLineBreak = null;
- private CharArrayIterator charIter = null;
- int layoutCount = 0;
- int layoutCharCount = 0;
-
- // paragraph, with resolved fonts and styles
- private StyledParagraph fParagraph;
-
- // paragraph data - same across all layouts
- private boolean fIsDirectionLTR;
- private byte fBaseline;
- private float[] fBaselineOffsets;
- private float fJustifyRatio = 1;
-
- /**
- * Constructs a <code>TextMeasurer</code> from the source text.
- * The source text should be a single entire paragraph.
- * @param text the source paragraph. Cannot be null.
- * @param frc the information about a graphics device which is needed
- * to measure the text correctly. Cannot be null.
- */
- public TextMeasurer(AttributedCharacterIterator text, FontRenderContext frc) {
-
- fFrc = frc;
- initAll(text);
- }
-
- protected Object clone() {
- TextMeasurer other;
- try {
- other = (TextMeasurer) super.clone();
- }
- catch(CloneNotSupportedException e) {
- throw new Error();
- }
- if (fComponents != null) {
- other.fComponents = (TextLineComponent[]) fComponents.clone();
- }
- return other;
- }
-
- private void invalidateComponents() {
- fComponentStart = fComponentLimit = fChars.length;
- fComponents = null;
- haveLayoutWindow = false;
- }
-
- /**
- * Initialize state, including fChars array, direction, and
- * fBidi.
- */
- private void initAll(AttributedCharacterIterator text) {
-
- fStart = text.getBeginIndex();
-
- // extract chars
- fChars = new char[text.getEndIndex() - fStart];
-
- int n = 0;
- for (char c = text.first(); c != text.DONE; c = text.next()) {
- fChars[n++] = c;
- }
-
- text.first();
-
- fBidi = new Bidi(text);
- if (fBidi.isLeftToRight()) {
- fBidi = null;
- }
-
- text.first();
- Map paragraphAttrs = text.getAttributes();
- if (paragraphAttrs != null) {
- try {
- NumericShaper shaper = (NumericShaper)paragraphAttrs.get(TextAttribute.NUMERIC_SHAPING);
- if (shaper != null) {
- shaper.shape(fChars, 0, fChars.length);
- }
- }
- catch (ClassCastException e) {
- }
- }
-
- fParagraph = new StyledParagraph(text, fChars);
-
- // set paragraph attributes
- {
- // If there's an embedded graphic at the start of the
- // paragraph, look for the first non-graphic character
- // and use it and its font to initialize the paragraph.
- // If not, use the first graphic to initialize.
- fJustifyRatio = TextLine.getJustifyRatio(paragraphAttrs);
-
- boolean haveFont = TextLine.advanceToFirstFont(text);
-
- if (haveFont) {
- Font defaultFont = TextLine.getFontAtCurrentPos(text);
- int charsStart = text.getIndex() - text.getBeginIndex();
- LineMetrics lm = defaultFont.getLineMetrics(fChars, charsStart, charsStart+1, fFrc);
- fBaseline = (byte) lm.getBaselineIndex();
- fBaselineOffsets = lm.getBaselineOffsets();
- }
- else {
- // hmmm what to do here? Just try to supply reasonable
- // values I guess.
-
- GraphicAttribute graphic = (GraphicAttribute)
- paragraphAttrs.get(TextAttribute.CHAR_REPLACEMENT);
- fBaseline = TextLayout.getBaselineFromGraphic(graphic);
- Font dummyFont = new Font(new Hashtable(5, (float)0.9));
- LineMetrics lm = dummyFont.getLineMetrics(" ", 0, 1, fFrc);
- fBaselineOffsets = lm.getBaselineOffsets();
- }
- fBaselineOffsets = TextLine.getNormalizedOffsets(fBaselineOffsets, fBaseline);
- }
-
- invalidateComponents();
- }
-
- /**
- * Generate components for the paragraph. fChars, fBidi should have been
- * initialized already.
- */
- private void generateComponents(int startingAt, int endingAt) {
-
- if (collectStats) {
- formattedChars += (endingAt-startingAt);
- }
- int layoutFlags = 0; // no extra info yet, bidi determines run and line direction
- TextLabelFactory factory = new TextLabelFactory(fFrc, fChars, fBidi, layoutFlags);
-
- int[] charsLtoV = null;
-
- if (fBidi != null) {
- fLevels = BidiUtils.getLevels(fBidi);
- int[] charsVtoL = BidiUtils.createVisualToLogicalMap(fLevels);
- charsLtoV = BidiUtils.createInverseMap(charsVtoL);
- fIsDirectionLTR = fBidi.baseIsLeftToRight();
- }
- else {
- fLevels = null;
- fIsDirectionLTR = true;
- }
-
- try {
- fComponents = TextLine.getComponents(
- fParagraph, fChars, startingAt, endingAt, charsLtoV, fLevels, factory);
- }
- catch(IllegalArgumentException e) {
- System.out.println("startingAt="+startingAt+"; endingAt="+endingAt);
- System.out.println("fComponentLimit="+fComponentLimit);
- throw e;
- }
-
- fComponentStart = startingAt;
- fComponentLimit = endingAt;
- //debugFormatCount += (endingAt-startingAt);
- }
-
- private int calcLineBreak(final int pos, final float maxAdvance) {
-
- // either of these statements removes the bug:
- //generateComponents(0, fChars.length);
- //generateComponents(pos, fChars.length);
-
- int startPos = pos;
- float width = maxAdvance;
-
- int tlcIndex;
- int tlcStart = fComponentStart;
-
- for (tlcIndex = 0; tlcIndex < fComponents.length; tlcIndex++) {
- int gaLimit = tlcStart + fComponents[tlcIndex].getNumCharacters();
- if (gaLimit > startPos) {
- break;
- }
- else {
- tlcStart = gaLimit;
- }
- }
-
- // tlcStart is now the start of the tlc at tlcIndex
-
- for (; tlcIndex < fComponents.length; tlcIndex++) {
-
- TextLineComponent tlc = fComponents[tlcIndex];
- int numCharsInGa = tlc.getNumCharacters();
-
- int lineBreak = tlc.getLineBreakIndex(startPos - tlcStart, width);
- if (lineBreak == numCharsInGa && tlcIndex < fComponents.length) {
- width -= tlc.getAdvanceBetween(startPos - tlcStart, lineBreak);
- tlcStart += numCharsInGa;
- startPos = tlcStart;
- }
- else {
- return tlcStart + lineBreak;
- }
- }
-
- if (fComponentLimit < fChars.length) {
- // format more text and try again
- //if (haveLayoutWindow) {
- // outOfWindow++;
- //}
-
- generateComponents(pos, fChars.length);
- return calcLineBreak(pos, maxAdvance);
- }
-
- return fChars.length;
- }
-
- /**
- * According to the Unicode Bidirectional Behavior specification
- * (Unicode Standard 2.0, section 3.11), whitespace at the ends
- * of lines which would naturally flow against the base direction
- * must be made to flow with the line direction, and moved to the
- * end of the line. This method returns the start of the sequence
- * of trailing whitespace characters to move to the end of a
- * line taken from the given range.
- */
- private int trailingCdWhitespaceStart(int startPos, int limitPos) {
-
- if (fLevels != null) {
- // Back up over counterdirectional whitespace
- final byte baseLevel = (byte) (fIsDirectionLTR? 0 : 1);
- for (int cdWsStart = limitPos; --cdWsStart >= startPos;) {
- if ((fLevels[cdWsStart] % 2) == baseLevel ||
- Character.getDirectionality(fChars[cdWsStart]) != Character.DIRECTIONALITY_WHITESPACE) {
- return ++cdWsStart;
- }
- }
- }
-
- return startPos;
- }
-
- private TextLineComponent[] makeComponentsOnRange(int startPos,
- int limitPos) {
-
- // sigh I really hate to do this here since it's part of the
- // bidi algorithm.
- // cdWsStart is the start of the trailing counterdirectional
- // whitespace
- final int cdWsStart = trailingCdWhitespaceStart(startPos, limitPos);
-
- int tlcIndex;
- int tlcStart = fComponentStart;
-
- for (tlcIndex = 0; tlcIndex < fComponents.length; tlcIndex++) {
- int gaLimit = tlcStart + fComponents[tlcIndex].getNumCharacters();
- if (gaLimit > startPos) {
- break;
- }
- else {
- tlcStart = gaLimit;
- }
- }
-
- // tlcStart is now the start of the tlc at tlcIndex
-
- int componentCount;
- {
- boolean split = false;
- int compStart = tlcStart;
- int lim=tlcIndex;
- for (boolean cont=true; cont; lim++) {
- int gaLimit = compStart + fComponents[lim].getNumCharacters();
- if (cdWsStart > Math.max(compStart, startPos)
- && cdWsStart < Math.min(gaLimit, limitPos)) {
- split = true;
- }
- if (gaLimit >= limitPos) {
- cont=false;
- }
- else {
- compStart = gaLimit;
- }
- }
- componentCount = lim-tlcIndex;
- if (split) {
- componentCount++;
- }
- }
-
- TextLineComponent[] components = new TextLineComponent[componentCount];
- int newCompIndex = 0;
- int linePos = startPos;
-
- int breakPt = cdWsStart;
-
- int subsetFlag;
- if (breakPt == startPos) {
- subsetFlag = fIsDirectionLTR? TextLineComponent.LEFT_TO_RIGHT :
- TextLineComponent.RIGHT_TO_LEFT;
- breakPt = limitPos;
- }
- else {
- subsetFlag = TextLineComponent.UNCHANGED;
- }
-
- while (linePos < limitPos) {
-
- int compLength = fComponents[tlcIndex].getNumCharacters();
- int tlcLimit = tlcStart + compLength;
-
- int start = Math.max(linePos, tlcStart);
- int limit = Math.min(breakPt, tlcLimit);
-
- components[newCompIndex++] = fComponents[tlcIndex].getSubset(
- start-tlcStart,
- limit-tlcStart,
- subsetFlag);
- linePos += (limit-start);
- if (linePos == breakPt) {
- breakPt = limitPos;
- subsetFlag = fIsDirectionLTR? TextLineComponent.LEFT_TO_RIGHT :
- TextLineComponent.RIGHT_TO_LEFT;
- }
- if (linePos == tlcLimit) {
- tlcIndex++;
- tlcStart = tlcLimit;
- }
- }
-
- return components;
- }
-
- private TextLine makeTextLineOnRange(int startPos, int limitPos) {
-
- int[] charsLtoV = null;
- byte[] charLevels = null;
-
- if (fBidi != null) {
- Bidi lineBidi = fBidi.createLineBidi(startPos, limitPos);
- charLevels = BidiUtils.getLevels(lineBidi);
- int[] charsVtoL = BidiUtils.createVisualToLogicalMap(charLevels);
- charsLtoV = BidiUtils.createInverseMap(charsVtoL);
- }
-
- TextLineComponent[] components = makeComponentsOnRange(startPos, limitPos);
-
- return new TextLine(components,
- fBaselineOffsets,
- fChars,
- startPos,
- limitPos,
- charsLtoV,
- charLevels,
- fIsDirectionLTR);
-
- }
-
- private void ensureComponents(int start, int limit) {
-
- if (start < fComponentStart || limit > fComponentLimit) {
- generateComponents(start, limit);
- }
- }
-
- private void makeLayoutWindow(int localStart) {
-
- int compStart = localStart;
- int compLimit = fChars.length;
-
- // If we've already gone past the layout window, format to end of paragraph
- if (layoutCount > 0 && !haveLayoutWindow) {
- float avgLineLength = Math.max(layoutCharCount / layoutCount, 1);
- compLimit = Math.min(localStart + (int)(avgLineLength*EST_LINES), fChars.length);
- }
-
- if (localStart > 0 || compLimit < fChars.length) {
- if (charIter == null) {
- charIter = new CharArrayIterator(fChars);
- }
- else {
- charIter.reset(fChars);
- }
- if (fLineBreak == null) {
- fLineBreak = BreakIterator.getLineInstance();
- }
- fLineBreak.setText(charIter);
- if (localStart > 0) {
- if (!fLineBreak.isBoundary(localStart)) {
- compStart = fLineBreak.preceding(localStart);
- }
- }
- if (compLimit < fChars.length) {
- if (!fLineBreak.isBoundary(compLimit)) {
- compLimit = fLineBreak.following(compLimit);
- }
- }
- }
-
- ensureComponents(compStart, compLimit);
- haveLayoutWindow = true;
- }
-
- /**
- * Returns the index of the first character which will not fit on
- * on a line beginning at <code>start</code> and possible
- * measuring up to <code>maxAdvance</code> in graphical width.
- *
- * @param start the character index at which to start measuring.
- * <code>start</code> is an absolute index, not relative to the
- * start of the paragraph
- * @param maxAdvance the graphical width in which the line must fit
- * @return the index after the last character that will fit
- * on a line beginning at <code>start</code>, which is not longer
- * than <code>maxAdvance</code> in graphical width
- * @throws IllegalArgumentException if <code>start</code> is
- * less than the beginning of the paragraph.
- */
- public int getLineBreakIndex(int start, float maxAdvance) {
-
- int localStart = start - fStart;
-
- if (!haveLayoutWindow ||
- localStart < fComponentStart ||
- localStart >= fComponentLimit) {
- makeLayoutWindow(localStart);
- }
-
- return calcLineBreak(localStart, maxAdvance) + fStart;
- }
-
- /**
- * Returns the graphical width of a line beginning at <code>start</code>
- * and including characters up to <code>limit</code>.
- * <code>start</code> and <code>limit</code> are absolute indices,
- * not relative to the start of the paragraph.
- *
- * @param start the character index at which to start measuring
- * @param limit the character index at which to stop measuring
- * @return the graphical width of a line beginning at <code>start</code>
- * and including characters up to <code>limit</code>
- * @throws IndexOutOfBoundsException if <code>limit</code> is less
- * than <code>start</code>
- * @throws IllegalArgumentException if <code>start</code> or
- * <code>limit</code> is not between the beginning of
- * the paragraph and the end of the paragraph.
- */
- public float getAdvanceBetween(int start, int limit) {
-
- int localStart = start - fStart;
- int localLimit = limit - fStart;
-
- ensureComponents(localStart, localLimit);
- TextLine line = makeTextLineOnRange(localStart, localLimit);
- return line.getMetrics().advance;
- // could cache line in case getLayout is called with same start, limit
- }
-
- /**
- * Returns a <code>TextLayout</code> on the given character range.
- *
- * @param start the index of the first character
- * @param limit the index after the last character. Must be greater
- * than <code>start</code>
- * @return a <code>TextLayout</code> for the characters beginning at
- * <code>start</code> up to (but not including) <code>limit</code>
- * @throws IndexOutOfBoundsException if <code>limit</code> is less
- * than <code>start</code>
- * @throws IllegalArgumentException if <code>start</code> or
- * <code>limit</code> is not between the beginning of
- * the paragraph and the end of the paragraph.
- */
- public TextLayout getLayout(int start, int limit) {
-
- int localStart = start - fStart;
- int localLimit = limit - fStart;
-
- ensureComponents(localStart, localLimit);
- TextLine textLine = makeTextLineOnRange(localStart, localLimit);
-
- if (localLimit < fChars.length) {
- layoutCharCount += limit-start;
- layoutCount++;
- }
-
- return new TextLayout(textLine,
- fBaseline,
- fBaselineOffsets,
- fJustifyRatio);
- }
-
- private int formattedChars = 0;
- private static boolean wantStats = false;/*"true".equals(System.getProperty("collectStats"));*/
- private boolean collectStats = false;
-
- private void printStats() {
- System.out.println("formattedChars: " + formattedChars);
- //formattedChars = 0;
- collectStats = false;
- }
-
- /**
- * Updates the <code>TextMeasurer</code> after a single character has
- * been inserted
- * into the paragraph currently represented by this
- * <code>TextMeasurer</code>. After this call, this
- * <code>TextMeasurer</code> is equivalent to a new
- * <code>TextMeasurer</code> created from the text; however, it will
- * usually be more efficient to update an existing
- * <code>TextMeasurer</code> than to create a new one from scratch.
- *
- * @param newParagraph the text of the paragraph after performing
- * the insertion. Cannot be null.
- * @param insertPos the position in the text where the character was
- * inserted. Must not be less than the start of
- * <code>newParagraph</code>, and must be less than the end of
- * <code>newParagraph</code>.
- * @throws IndexOutOfBoundsException if <code>insertPos</code> is less
- * than the start of <code>newParagraph</code> or greater than
- * or equal to the end of <code>newParagraph</code>
- * @throws NullPointerException if <code>newParagraph</code> is
- * <code>null</code>
- */
- public void insertChar(AttributedCharacterIterator newParagraph, int insertPos) {
-
- if (collectStats) {
- printStats();
- }
- if (wantStats) {
- collectStats = true;
- }
-
- fStart = newParagraph.getBeginIndex();
- int end = newParagraph.getEndIndex();
- if (end - fStart != fChars.length+1) {
- initAll(newParagraph);
- }
-
- char[] newChars = new char[end-fStart];
- int newCharIndex = insertPos - fStart;
- System.arraycopy(fChars, 0, newChars, 0, newCharIndex);
-
- char newChar = newParagraph.setIndex(insertPos);
- newChars[newCharIndex] = newChar;
- System.arraycopy(fChars,
- newCharIndex,
- newChars,
- newCharIndex+1,
- end-insertPos-1);
- fChars = newChars;
-
- if (fBidi != null || Bidi.requiresBidi(newChars, newCharIndex, newCharIndex + 1) ||
- newParagraph.getAttribute(TextAttribute.BIDI_EMBEDDING) != null) {
-
- fBidi = new Bidi(newParagraph);
- if (fBidi.isLeftToRight()) {
- fBidi = null;
- }
- }
-
- fParagraph = StyledParagraph.insertChar(newParagraph,
- fChars,
- insertPos,
- fParagraph);
- invalidateComponents();
- }
-
- /**
- * Updates the <code>TextMeasurer</code> after a single character has
- * been deleted
- * from the paragraph currently represented by this
- * <code>TextMeasurer</code>. After this call, this
- * <code>TextMeasurer</code> is equivalent to a new <code>TextMeasurer</code>
- * created from the text; however, it will usually be more efficient
- * to update an existing <code>TextMeasurer</code> than to create a new one
- * from scratch.
- *
- * @param newParagraph the text of the paragraph after performing
- * the deletion. Cannot be null.
- * @param deletePos the position in the text where the character was removed.
- * Must not be less than
- * the start of <code>newParagraph</code>, and must not be greater than the
- * end of <code>newParagraph</code>.
- * @throws IndexOutOfBoundsException if <code>deletePos</code> is
- * less than the start of <code>newParagraph</code> or greater
- * than the end of <code>newParagraph</code>
- * @throws NullPointerException if <code>newParagraph</code> is
- * <code>null</code>
- */
- public void deleteChar(AttributedCharacterIterator newParagraph, int deletePos) {
-
- fStart = newParagraph.getBeginIndex();
- int end = newParagraph.getEndIndex();
- if (end - fStart != fChars.length-1) {
- initAll(newParagraph);
- }
-
- char[] newChars = new char[end-fStart];
- int changedIndex = deletePos-fStart;
-
- System.arraycopy(fChars, 0, newChars, 0, deletePos-fStart);
- System.arraycopy(fChars, changedIndex+1, newChars, changedIndex, end-deletePos);
- fChars = newChars;
-
- if (fBidi != null) {
- fBidi = new Bidi(newParagraph);
- if (fBidi.isLeftToRight()) {
- fBidi = null;
- }
- }
-
- fParagraph = StyledParagraph.deleteChar(newParagraph,
- fChars,
- deletePos,
- fParagraph);
- invalidateComponents();
- }
-
- /**
- * NOTE: This method is only for LineBreakMeasurer's use. It is package-
- * private because it returns internal data.
- */
- char[] getChars() {
-
- return fChars;
- }
- }