/*****************************************************************************
* Copyright (C) The Apache Software Foundation. All rights reserved. *
* ------------------------------------------------------------------------- *
* This software is published under the terms of the Apache Software License *
* version 1.1, a copy of which has been included with this distribution in *
* the LICENSE file. *
*****************************************************************************/
package org.apache.batik.gvt.renderer;
import java.awt.BasicStroke;
import java.awt.Composite;
import java.awt.Graphics2D;
import java.awt.Paint;
import java.awt.Shape;
import java.awt.Stroke;
import java.awt.font.FontRenderContext;
import java.awt.font.TextAttribute;
import java.awt.geom.AffineTransform;
import java.awt.geom.Point2D;
import java.awt.Composite;
import java.awt.geom.GeneralPath;
import java.awt.geom.Rectangle2D;
import java.text.AttributedCharacterIterator;
import java.text.AttributedString;
import java.text.CharacterIterator;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.Vector;
import org.apache.batik.gvt.GraphicsNodeRenderContext;
import org.apache.batik.gvt.TextNode;
import org.apache.batik.gvt.TextPainter;
import org.apache.batik.gvt.text.AttributedCharacterSpanIterator;
import org.apache.batik.gvt.text.GVTAttributedCharacterIterator;
import org.apache.batik.gvt.text.TextSpanLayout;
import org.apache.batik.gvt.font.GVTFont;
import org.apache.batik.gvt.font.GVTFontFamily;
import org.apache.batik.gvt.font.UnresolvedFontFamily;
import org.apache.batik.gvt.font.FontFamilyResolver;
import org.apache.batik.gvt.text.TextHit;
/**
* More sophisticated implementation of TextPainter which
* renders the attributed character iterator of a <tt>TextNode</tt>.
* <em>StrokingTextPainter includes support for stroke, fill, opacity,
* text-decoration, and other attributes.</em>
* @see org.apache.batik.gvt.TextPainter
* @see org.apache.batik.gvt.text.GVTAttributedCharacterIterator
*
* @author <a href="bill.haneman@ireland.sun.com>Bill Haneman</a>
* @version $Id: StrokingTextPainter.java,v 1.3 2001/04/29 08:22:26 dino Exp $
*/
public class StrokingTextPainter extends BasicTextPainter {
static Set extendedAtts = new HashSet();
static {
extendedAtts.add(GVTAttributedCharacterIterator.TextAttribute.TEXT_COMPOUND_DELIMITER);
extendedAtts.add(GVTAttributedCharacterIterator.TextAttribute.GVT_FONT);
}
/**
* Paints the specified attributed character iterator using the
* specified Graphics2D and rendering context.
* Note that the GraphicsNodeRenderContext contains a TextPainter
* reference.
* @see org.apache.batik.gvt.TextPainter
* @see org.apache.batik.gvt.GraphicsNodeRenderContext
* @param shape the shape to paint
* @param g2d the Graphics2D to use
* @param context the rendering context.
*/
public void paint(TextNode node, Graphics2D g2d,
GraphicsNodeRenderContext context) {
FontRenderContext frc = context.getFontRenderContext();
AttributedCharacterIterator aci = node.getAttributedCharacterIterator();
List textRuns = getTextRuns(node, aci, frc);
/*
* Text Chunks contain one or more TextRuns,
* which they create from the ACI.
* Each TextRun contains one TextLayout object.
* We render each of the TextLayout glyphsets
* in turn.
*/
for (int i = 0; i < textRuns.size(); ++i) {
paintTextRun((TextRun) textRuns.get(i), g2d, context);
}
// TODO: FONT_VARIANT, SUPERSCRIPT, SUBSCRIPT...
}
private List getTextRuns(TextNode node,
AttributedCharacterIterator aci,
FontRenderContext frc) {
List textRuns = new ArrayList();
AttributedCharacterIterator fontaci = createModifiedACIForFontMatching(node, aci);
fontaci.first();
/*
* We iterate through the spans over extended attributes,
* instantiating TextLayout elements as we go, and
* accumulate an overall advance for the text display.
*/
TextChunk chunk;
int beginChunk = 0;
do {
/*
* Text Chunks contain one or more TextRuns,
* which they create from the ACI.
*/
chunk = getTextChunk(node, fontaci, textRuns, beginChunk, frc);
/* Adjust according to text-anchor property value */
if (chunk != null) {
adjustChunkOffsets(textRuns, chunk.advance, chunk.begin, chunk.end);
beginChunk = chunk.end;
}
} while (chunk != null);
return textRuns;
}
private TextChunk getTextChunk(TextNode node,
AttributedCharacterIterator aci,
List textRuns,
int beginChunk,
FontRenderContext frc) {
int endChunk = beginChunk;
AttributedCharacterIterator runaci;
boolean inChunk = true;
Point2D advance = new Point2D.Float(0f, 0f);
Point2D location = node.getLocation();
if (aci.current() != CharacterIterator.DONE) {
int chunkStartIndex = aci.getIndex();
boolean isChunkStart = true;
do {
int start = aci.getRunStart(extendedAtts);
int end = aci.getRunLimit(extendedAtts);
runaci =
new AttributedCharacterSpanIterator(aci, start, end);
Float fx = (Float) runaci.getAttribute(
GVTAttributedCharacterIterator.TextAttribute.X);
inChunk = (isChunkStart) || (fx == null) || (fx.isNaN());
if (inChunk) {
Point2D offset = new Point2D.Float(
(float) (location.getX()+advance.getX()),
(float) (location.getY()+advance.getY()));
TextSpanLayout layout = getTextLayoutFactory().
createTextLayout(runaci, offset, frc);
if (layout.isVertical()) {
runaci = createModifiedACIForVerticalLayout(runaci);
}
TextRun run = new TextRun(layout, runaci);
textRuns.add(run);
Point2D layoutAdvance = layout.getAdvance2D();
advance = new Point2D.Float(
(float) (advance.getX()+layoutAdvance.getX()),
(float) (advance.getY()+layoutAdvance.getY()));
++endChunk;
if (aci.setIndex(end) == CharacterIterator.DONE) break;
} else {
aci.setIndex(start);
}
isChunkStart = false;
} while (inChunk);
return new TextChunk(beginChunk, endChunk, advance);
} else {
return null;
}
}
class TextChunk {
public int begin;
public int end;
public Point2D advance;
public TextChunk(int begin, int end, Point2D advance) {
this.begin = begin;
this.end = end;
this.advance = new Point2D.Float((float) advance.getX(),
(float) advance.getY());
}
}
private AttributedCharacterIterator createModifiedACIForVerticalLayout(
AttributedCharacterIterator runaci) {
AttributedString as = new AttributedString(runaci);
if (runaci.getAttribute(GVTAttributedCharacterIterator.
TextAttribute.UNDERLINE) != null) {
as.addAttribute(TextAttribute.UNDERLINE,
TextAttribute.UNDERLINE_ON);
}
if (runaci.getAttribute(GVTAttributedCharacterIterator.
TextAttribute.STRIKETHROUGH) != null) {
as.addAttribute(TextAttribute.STRIKETHROUGH,
TextAttribute.STRIKETHROUGH_ON);
}
return as.getIterator();
}
/**
* Returns a new AttributedCharacterIterator that contains resolved GVTFont
* attributes. This is then used when creating the text runs so that the
* text can be split on changes of font as well as tspans and trefs.
*
* @param node The text node that the aci belongs to.
* @param aci The aci to be modified.
*
* @return The new modified aci.
*/
private AttributedCharacterIterator createModifiedACIForFontMatching(
TextNode node, AttributedCharacterIterator aci) {
/* System.out.print("selecting font characters in: ");
for (char c = aci.first(); c != aci.DONE; c = aci.next()) {
System.out.print(c);
}
System.out.println();
*/
aci.first();
AttributedCharacterSpanIterator acsi
= new AttributedCharacterSpanIterator(aci, aci.getBeginIndex(), aci.getEndIndex());
AttributedString as = new AttributedString(acsi);
aci.first();
boolean moreChunks = true;
while (moreChunks) {
int start = aci.getRunStart(GVTAttributedCharacterIterator.TextAttribute.TEXT_COMPOUND_DELIMITER);
int end = aci.getRunLimit(GVTAttributedCharacterIterator.TextAttribute.TEXT_COMPOUND_DELIMITER);
AttributedCharacterSpanIterator runaci = new AttributedCharacterSpanIterator(aci, start, end);
Vector fontFamilies = (Vector)runaci.getAttributes().get(GVTAttributedCharacterIterator.TextAttribute.GVT_FONT_FAMILIES);
if (fontFamilies == null) {
// no font families set, just return the same aci
return aci;
}
// resolve any unresolved font families in the list
Vector resolvedFontFamilies = new Vector();
for (int i = 0; i < fontFamilies.size(); i++) {
GVTFontFamily fontFamily = (GVTFontFamily) fontFamilies.get(i);
if (fontFamily instanceof UnresolvedFontFamily) {
GVTFontFamily resolvedFontFamily = FontFamilyResolver.resolve((UnresolvedFontFamily)fontFamily);
if (resolvedFontFamily != null) {
// font family was successfully resolved
resolvedFontFamilies.add(resolvedFontFamily);
}
} else {
// already resolved
resolvedFontFamilies.add(fontFamily);
}
}
// if could not resolve at least one of the fontFamilies then use
// the default faont
if (resolvedFontFamilies.size() == 0) {
resolvedFontFamilies.add(FontFamilyResolver.defaultFont);
}
// create a list of fonts of the correct size
Float fontSizeFloat = (Float)as.getIterator().getAttribute(TextAttribute.SIZE);
float fontSize = 12;
if (fontSizeFloat != null) {
fontSize = fontSizeFloat.floatValue();
}
Vector gvtFonts = new Vector();
for (int i = 0; i < resolvedFontFamilies.size(); i++) {
GVTFont font = ((GVTFontFamily)resolvedFontFamilies.get(i)).deriveFont(fontSize, runaci);
gvtFonts.add(font);
}
// now for each char or group of chars in the string,
// find a font that can display it
int runaciLength = end-start;
boolean[] fontAssigned = new boolean[runaciLength];
for (int i = 0; i < runaciLength; i++) {
fontAssigned[i] = false;
}
for (int i = 0; i < gvtFonts.size(); i++) {
// assign this font to all characters it can display if it has
// not already been assigned
GVTFont font = (GVTFont)gvtFonts.get(i);
int currentRunIndex = runaci.getBeginIndex();
while (currentRunIndex < runaci.getEndIndex()) {
int displayUpToIndex = font.canDisplayUpTo(runaci, currentRunIndex, end);
if (displayUpToIndex == -1) {
// for each char, if not already assigned a font, assign this font to it
for (int j = currentRunIndex; j < end; j++) {
if (!fontAssigned[j - start]) {
as.addAttribute(GVTAttributedCharacterIterator.TextAttribute.GVT_FONT, font, j, j+1);
fontAssigned[j - start] = true;
}
}
currentRunIndex = runaci.getEndIndex();
} else if (displayUpToIndex > currentRunIndex) {
// could display some but not all
// for each char it can display, if not already assigned a font, assign this font to it
for (int j = currentRunIndex; j < displayUpToIndex; j++) {
if (!fontAssigned[j - start]) {
as.addAttribute(GVTAttributedCharacterIterator.TextAttribute.GVT_FONT, font, j, j+1);
fontAssigned[j - start] = true;
}
}
// set currentRunIndex to be one after the char couldn't display
currentRunIndex = displayUpToIndex+1;
} else {
// couldn't display the current char
currentRunIndex++;
}
}
}
// assign the first font to any chars haven't alreay been assigned
for (int i = 0; i < runaciLength; i++) {
if (!fontAssigned[i]) {
as.addAttribute(GVTAttributedCharacterIterator.TextAttribute.GVT_FONT,
gvtFonts.get(0), start+i, start+i+1);
}
}
if (aci.setIndex(end) == aci.DONE) {
moreChunks = false;
}
}
return as.getIterator();
}
private void adjustChunkOffsets(List textRuns, Point2D advance,
int beginChunk, int endChunk) {
for (int n=beginChunk; n<endChunk; ++n) {
TextRun r = (TextRun) textRuns.get(n);
int anchorType = r.getAnchorType();
float dx = 0f;
float dy = 0f;
switch(anchorType){
case TextNode.Anchor.ANCHOR_MIDDLE:
dx = (float) (-advance.getX()/2d);
dy = (float) (-advance.getY()/2d);
break;
case TextNode.Anchor.ANCHOR_END:
dx = (float) (-advance.getX());
dy = (float) (-advance.getY());
break;
default:
// leave untouched
}
TextSpanLayout layout = r.getLayout();
Point2D offset = layout.getOffset();
if (layout.isVertical()) {
layout.setOffset(new Point2D.Float(
(float) offset.getX(),
(float) offset.getY()+dy));
} else {
//System.out.println("offset "+offset+" shift "+dx);
layout.setOffset(new Point2D.Float(
(float) offset.getX()+dx,
(float) offset.getY()));
}
}
}
private void paintTextRun(TextRun textRun, Graphics2D g2d,
GraphicsNodeRenderContext context) {
AttributedCharacterIterator runaci = textRun.getACI();
TextSpanLayout layout = textRun.getLayout();
runaci.first();
Composite opacity = (Composite)
runaci.getAttribute(GVTAttributedCharacterIterator.
TextAttribute.OPACITY);
if (opacity != null) {
g2d.setComposite(opacity);
}
boolean underline =
(runaci.getAttribute(GVTAttributedCharacterIterator.
TextAttribute.UNDERLINE) != null);
// paint over-and-underlines first, then layer glyphs over them
if (underline && !layout.isVertical()) {
paintUnderline(textRun, g2d);
}
boolean overline =
(runaci.getAttribute(GVTAttributedCharacterIterator.
TextAttribute.OVERLINE) != null);
if (overline && !layout.isVertical()) {
paintOverline(textRun, g2d);
}
layout.draw(g2d, context);
boolean strikethrough =
(runaci.getAttribute(GVTAttributedCharacterIterator.
TextAttribute.STRIKETHROUGH) ==
GVTAttributedCharacterIterator.
TextAttribute.STRIKETHROUGH_ON);
// paint strikethrough last
if (strikethrough && !layout.isVertical()) {
paintStrikethrough(textRun, g2d);
}
}
/**
* Paints the overline for a given ACI.
*/
private void paintOverline(TextRun textRun, Graphics2D g2d) {
AttributedCharacterIterator runaci = textRun.getACI();
TextSpanLayout layout = textRun.getLayout();
java.awt.Shape overlineShape =
layout.getDecorationOutline(
TextSpanLayout.DECORATION_OVERLINE);
Paint paint = (Paint) runaci.getAttribute(
TextAttribute.FOREGROUND);
if (paint != null) {
g2d.setPaint(paint);
g2d.fill(overlineShape);
}
Stroke stroke = (Stroke) runaci.getAttribute(
GVTAttributedCharacterIterator.TextAttribute.STROKE);
paint = (Paint) runaci.getAttribute(
GVTAttributedCharacterIterator.TextAttribute.STROKE_PAINT);
if ((stroke != null) && (paint != null)) {
g2d.setStroke(stroke);
g2d.setPaint(paint);
g2d.draw(overlineShape);
}
// TODO: performance and concision
}
/**
* Paints the underline for a given ACI - does not rely on TextLayout's
* internal underlining but computes the underline manually, allowing
* the underline fill and stroke to differ from that of the text glyphs.
*/
private void paintUnderline(TextRun textRun, Graphics2D g2d) {
AttributedCharacterIterator runaci = textRun.getACI();
TextSpanLayout layout = textRun.getLayout();
Shape underlineShape = layout.getDecorationOutline(
TextSpanLayout.DECORATION_UNDERLINE);
// TODO: change getAdvance to getVisibleAdvance for
// ACIs which do not inherit their underline attribute
// (not sure how to implement this yet)
Paint paint = (Paint) runaci.getAttribute(
GVTAttributedCharacterIterator.TextAttribute.UNDERLINE_PAINT);
if (paint != null) {
g2d.setPaint(paint);
g2d.fill(underlineShape);
// System.out.println("Filling "+underlineShape+" with paint "+paint);
}
Stroke stroke = (Stroke) runaci.getAttribute(
GVTAttributedCharacterIterator.TextAttribute.UNDERLINE_STROKE);
paint = (Paint) runaci.getAttribute(
GVTAttributedCharacterIterator.
TextAttribute.UNDERLINE_STROKE_PAINT);
if ((stroke != null) && (paint != null)) {
g2d.setStroke(stroke);
g2d.setPaint(paint);
g2d.draw(underlineShape);
}
}
/**
* Paints the strikethrough line for a given ACI - does not
* rely on TextLayout's
* internal strikethrough but computes it manually.
*/
private void paintStrikethrough(TextRun textRun, Graphics2D g2d) {
AttributedCharacterIterator runaci = textRun.getACI();
TextSpanLayout layout = textRun.getLayout();
java.awt.Shape strikethroughShape =
layout.getDecorationOutline(
TextSpanLayout.DECORATION_STRIKETHROUGH);
Paint paint = (Paint) runaci.getAttribute(
TextAttribute.FOREGROUND);
if (paint != null) {
g2d.setPaint(paint);
g2d.fill(strikethroughShape);
}
Stroke stroke = (Stroke) runaci.getAttribute(
GVTAttributedCharacterIterator.TextAttribute.STROKE);
paint = (Paint) runaci.getAttribute(
GVTAttributedCharacterIterator.TextAttribute.STROKE_PAINT);
if ((stroke != null) && (paint != null)) {
g2d.setStroke(stroke);
g2d.setPaint(paint);
g2d.draw(strikethroughShape);
}
}
/*
* Get a Rectangle2D in userspace coords which encloses the textnode
* glyphs composed from an AttributedCharacterIterator.
* @param node the TextNode to measure
* @param g2d the Graphics2D to use
* @param context rendering context.
* @param includeDecoration whether to include text decoration
* in bounds computation.
* @param includeStrokeWidth whether to include the effect of stroke width
* in bounds computation.
*/
protected Rectangle2D getBounds(TextNode node,
FontRenderContext context,
boolean includeDecoration,
boolean includeStrokeWidth) {
Rectangle2D bounds;
if (includeStrokeWidth) {
Shape s = getStrokeOutline(node, context, includeDecoration);
if (s != null) {
bounds = s.getBounds2D();
} else {
bounds = getOutline(node, context, false).getBounds2D();
}
} else {
if (includeDecoration) {
bounds = getOutline(node, context, true).getBounds2D();
} else {
bounds = getOutline(node, context, false).getBounds2D();
}
}
return bounds;
}
/**
* Get a Shape in userspace coords which defines the textnode glyph outlines.
* @param node the TextNode to measure
* @param frc the font rendering context.
* @param includeDecoration whether to include text decoration
* outlines.
*/
protected Shape getOutline(TextNode node, FontRenderContext frc,
boolean includeDecoration) {
Shape outline = null;
AttributedCharacterIterator aci = node.getAttributedCharacterIterator();
// get the list of text runs
List textRuns = getTextRuns(node, aci, frc);
// for each text run, get its outline and append it to the overall outline
for (int i = 0; i < textRuns.size(); ++i) {
TextRun textRun = (TextRun)textRuns.get(i);
Shape textRunOutline = textRun.getLayout().getOutline();;
if (includeDecoration) {
AttributedCharacterIterator textRunACI = textRun.getACI();
int decorationTypes = 0;
if (textRunACI.getAttribute(GVTAttributedCharacterIterator.
TextAttribute.UNDERLINE) != null) {
decorationTypes |= TextSpanLayout.DECORATION_UNDERLINE;
}
if (textRunACI.getAttribute(GVTAttributedCharacterIterator.
TextAttribute.OVERLINE) != null) {
decorationTypes |= TextSpanLayout.DECORATION_OVERLINE;
}
if (textRunACI.getAttribute(GVTAttributedCharacterIterator.
TextAttribute.STRIKETHROUGH) != null) {
decorationTypes |= TextSpanLayout.DECORATION_STRIKETHROUGH;
}
if (decorationTypes != 0) {
if (!(textRunOutline instanceof GeneralPath)) {
textRunOutline = new GeneralPath(textRunOutline);
}
((GeneralPath) textRunOutline).setWindingRule(
GeneralPath.WIND_NON_ZERO);
((GeneralPath) textRunOutline).append(
textRun.getLayout().getDecorationOutline(decorationTypes), false);
}
}
if (outline == null) {
outline = textRunOutline;
} else {
if (!(outline instanceof GeneralPath)) {
outline = new GeneralPath(outline);
}
((GeneralPath) outline).setWindingRule(
GeneralPath.WIND_NON_ZERO);
((GeneralPath) outline).append(textRunOutline, false);
}
}
return outline;
}
protected org.apache.batik.gvt.text.Mark hitTest(
double x, double y, AttributedCharacterIterator aci,
TextNode node,
GraphicsNodeRenderContext context) {
FontRenderContext frc = context.getFontRenderContext();
// get the list of text runs
List textRuns = getTextRuns(node, aci, frc);
// for each text run, see if its been hit
for (int i = 0; i < textRuns.size(); ++i) {
TextRun textRun = (TextRun)textRuns.get(i);
TextSpanLayout layout = textRun.getLayout();
TextHit textHit = layout.hitTestChar((float) x, (float) y);
if (textHit != null && layout.getBounds().contains(x,y)) {
textHit.setTextNode(node);
textHit.setFontRenderContext(frc);
// Note that a texthit char index of -1 signals that the
// hit, though within the text element bounds, did not
// coincide with a glyph.
if ((aci != cachedACI) ||
(textHit == null) ||
(cachedHit == null) ||
((textHit.getCharIndex() != -1) &&
(textHit.getInsertionIndex() != cachedHit.getInsertionIndex()))) {
cachedMark = new BasicTextPainter.Mark(x, y, layout, textHit);
cachedACI = textRun.getACI();
cachedHit = textHit;
return cachedMark;
} // else old mark is still valid, return it.
}
}
return cachedMark;
}
/**
* Return a Shape, in the coordinate system of the text layout,
* which encloses the text selection delineated by two Mark instances.
* <em>Note: The Mark instances passed must have been instantiated by
* an instance of this enclosing TextPainter implementation.</em>
*/
public Shape getHighlightShape(org.apache.batik.gvt.text.Mark beginMark,
org.apache.batik.gvt.text.Mark endMark) {
// TODO: later we can return more complex things like
// noncontiguous selections
BasicTextPainter.Mark begin;
BasicTextPainter.Mark end;
try {
begin = (BasicTextPainter.Mark) beginMark;
end = (BasicTextPainter.Mark) endMark;
} catch (ClassCastException cce) {
throw new
Error("This Mark was not instantiated by this TextPainter class!");
}
/* if (begin == null) {
System.out.println("begin mark is null");
} else {
System.out.println("begin mark is at: " + begin.getHit().getCharIndex());
}
if (end == null) {
System.out.println("end mark is null");
} else {
System.out.println("end mark is at: " + end.getHit().getCharIndex());
}
*/
TextSpanLayout beginLayout = null;
TextSpanLayout endLayout = null;
if (begin != null && end != null) {
beginLayout = begin.getLayout();
endLayout = end.getLayout();
}
if (beginLayout == null || endLayout == null) {
return null;
}
// System.out.println("beginLayout offset is: " + beginLayout.getOffset());
// System.out.println("endLayout offset is: " + endLayout.getOffset());
if (beginLayout.getOffset().equals(endLayout.getOffset())) {
int firsthit = 0;
int lasthit = 0;
if (begin != end) {
firsthit = (begin.getHit().isLeadingEdge()) ?
begin.getHit().getCharIndex() :
begin.getHit().getCharIndex()+1;
lasthit = (end.getHit().isLeadingEdge()) ?
end.getHit().getCharIndex() :
end.getHit().getCharIndex()+1;
if (firsthit > lasthit) {
int temp = firsthit;
firsthit = lasthit;
lasthit = temp;
}
} else {
lasthit = beginLayout.getCharacterCount();
}
if (firsthit < 0) {
firsthit = 0;
}
return beginLayout.getLogicalHighlightShape(
firsthit,
lasthit);
} else {
// selection must span more than one text layout (run)
// get the list of text runs
TextNode textNode = begin.getHit().getTextNode();
FontRenderContext frc = begin.getHit().getFontRenderContext();
List textRuns = getTextRuns(textNode, textNode.getAttributedCharacterIterator(), frc);
// find out whether selection is right to left or not, ie. whether
// beginLayout is before endLayout or not
boolean leftToRight = true;
for (int i = 0; i < textRuns.size(); ++i) {
TextRun textRun = (TextRun)textRuns.get(i);
TextSpanLayout layout = textRun.getLayout();
if (layout.getOffset().equals(beginLayout.getOffset())) {
break;
}
if (layout.getOffset().equals(endLayout.getOffset())) {
leftToRight = false;
break;
}
}
/* if (leftToRight) {
System.out.println("left to right selection");
} else {
System.out.println("right to left selection");
}
*/
GeneralPath highlightedShape = new GeneralPath();
boolean startedHighlight = false;
boolean finishedHighlight = false;
// for each text run
for (int i = 0; i < textRuns.size(); ++i) {
TextRun textRun = (TextRun)textRuns.get(i);
TextSpanLayout layout = textRun.getLayout();
Shape layoutHighlightedShape = null;
if (leftToRight) {
if (layout.getOffset().equals(beginLayout.getOffset())) { // found the first layout
startedHighlight = true;
int firsthit = (begin.getHit().isLeadingEdge()) ?
begin.getHit().getCharIndex() :
begin.getHit().getCharIndex()+1;
if (firsthit < 0) {
firsthit = 0;
}
layoutHighlightedShape = layout.getLogicalHighlightShape(
firsthit, layout.getCharacterCount());
} else if (layout.getOffset().equals(endLayout.getOffset())) {
finishedHighlight = true;
int lasthit = (end.getHit().isLeadingEdge()) ?
end.getHit().getCharIndex() :
end.getHit().getCharIndex()+1;
if (lasthit < 0) {
lasthit = layout.getCharacterCount();
}
layoutHighlightedShape = layout.getLogicalHighlightShape(
0, lasthit);
} else if (startedHighlight) {
layoutHighlightedShape = layout.getLogicalHighlightShape(
0, layout.getCharacterCount());
}
} else { // right to left
if (layout.getOffset().equals(beginLayout.getOffset())) { // found the first layout
finishedHighlight = true;
int lasthit = (begin.getHit().isLeadingEdge()) ?
begin.getHit().getCharIndex() :
begin.getHit().getCharIndex()+1;
if (lasthit < 0) {
lasthit = layout.getCharacterCount();
}
layoutHighlightedShape = layout.getLogicalHighlightShape(
0, lasthit);
} else if (layout.getOffset().equals(endLayout.getOffset())) {
startedHighlight = true;
int firsthit = (end.getHit().isLeadingEdge()) ?
end.getHit().getCharIndex() :
end.getHit().getCharIndex()+1;
if (firsthit < 0) {
firsthit = 0;
}
layoutHighlightedShape = layout.getLogicalHighlightShape(
firsthit, layout.getCharacterCount());
} else if (startedHighlight) {
layoutHighlightedShape = layout.getLogicalHighlightShape(
0, layout.getCharacterCount());
}
}
// append the highlighted shape of this layout to the
// overall hightlighted shape
if (layoutHighlightedShape != null && !layoutHighlightedShape.getBounds().isEmpty()) {
highlightedShape.append(layoutHighlightedShape, false);
}
// if has appended the last highlight, then don't process any more
if (finishedHighlight) {
break;
}
}
return highlightedShape;
}
}
/**
* Inner convenience class for associating a TextLayout for
* sub-spans, and the ACI which iterates over that subspan.
*/
class TextRun {
private AttributedCharacterIterator aci;
private TextSpanLayout layout;
private int anchorType;
public TextRun(TextSpanLayout layout, AttributedCharacterIterator aci) {
this.layout = layout;
this.aci = aci;
this.aci.first();
TextNode.Anchor anchor = (TextNode.Anchor) aci.getAttribute(
GVTAttributedCharacterIterator.TextAttribute.ANCHOR_TYPE);
anchorType = TextNode.Anchor.ANCHOR_START;
if (anchor != null) {
anchorType = anchor.getType();
}
}
public AttributedCharacterIterator getACI() {
return aci;
}
public TextSpanLayout getLayout() {
return layout;
}
public int getAnchorType() {
return anchorType;
}
}
}