/*
* Copyright (c) 1997, 2011, Oracle and/or its affiliates. All rights reserved.
* ORACLE 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.
AttributedCharacterIterator.
Attribute;
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.
AttributeValues;
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
* @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 =
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 !=
CharacterIterator.
DONE;
c =
text.
next())
{
fChars[
n++] =
c;
}
text.
first();
fBidi = new
Bidi(
text);
if (
fBidi.
isLeftToRight()) {
fBidi = null;
}
text.
first();
Map<? extends
Attribute, ?>
paragraphAttrs =
text.
getAttributes();
NumericShaper shaper =
AttributeValues.
getNumericShaping(
paragraphAttrs);
if (
shaper != null) {
shaper.
shape(
fChars, 0,
fChars.length);
}
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 =
AttributeValues.
getJustification(
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);
Hashtable<
Attribute, ?>
fmap = new
Hashtable<>(5, (float)0.9);
Font dummyFont = new
Font(
fmap);
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(
fFrc,
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;
}
}