Revision: 591
Author: allain.lalonde
Date: Mon Jul 27 16:09:53 2009
Log: Starting to refactor PStyledText. It's a beast. Too bad we don't have
any code that really puts it through its paces.
http://code.google.com/p/piccolo2d/source/detail?r=591
Modified:
/piccolo2d.java/trunk/extras/src/main/java/edu/umd/cs/piccolox/nodes/PStyledText.java
=======================================
---
/piccolo2d.java/trunk/extras/src/main/java/edu/umd/cs/piccolox/nodes/PStyledText.java
Mon Jul 27 15:02:47 2009
+++
/piccolo2d.java/trunk/extras/src/main/java/edu/umd/cs/piccolox/nodes/PStyledText.java
Mon Jul 27 16:09:53 2009
@@ -42,6 +42,7 @@
import java.text.AttributedCharacterIterator;
import java.text.AttributedString;
import java.util.ArrayList;
+import java.util.Iterator;
import java.util.List;
import java.util.StringTokenizer;
@@ -132,16 +133,8 @@
}
public void syncWithDocument() {
- // The paragraph start and end indices
- ArrayList pEnds = null;
-
- // The current position in the specified range
- int pos = 0;
-
// First get the actual text and stick it in an Attributed String
-
stringContents = new ArrayList();
- pEnds = new ArrayList();
String documentString;
try {
@@ -149,78 +142,31 @@
}
catch (BadLocationException e) {
// Since this the location we're providing comes from directly
- // querying the document, this is impossible in a single
threaded model
+ // querying the document, this is impossible in a single
threaded
+ // model
return;
}
-
- StringTokenizer tokenizer = new
StringTokenizer(documentString, "\n", true);
-
- // lastNewLine is used to detect the case when two newlines follow
- // in direct succession
- // & lastNewLine should be true to start in case the first
character
- // is a newline
- boolean lastNewLine = true;
- for (int i = 0; tokenizer.hasMoreTokens(); i++) {
- String token = tokenizer.nextToken();
-
- // If the token
- if (token.equals("\n")) {
- if (lastNewLine) {
- stringContents.add(new AttributedString(" "));
- pEnds.add(new RunInfo(pos, pos + 1));
-
- pos = pos + 1;
-
- lastNewLine = true;
- }
- else {
- pos = pos + 1;
-
- lastNewLine = true;
- }
- }
- // If the token is empty - create an attributed string with a
- // single space
- // since LineBreakMeasurers don't work with an empty string
- // - note that this case should only arise if the document is
- // empty
- else if (token.equals("")) {
- stringContents.add(new AttributedString(" "));
- pEnds.add(new RunInfo(pos, pos));
-
- lastNewLine = false;
- }
- // This is the normal case - where we have some text
- else {
- stringContents.add(new AttributedString(token));
- pEnds.add(new RunInfo(pos, pos + token.length()));
-
- // Increment the position
- pos = pos + token.length();
-
- lastNewLine = false;
- }
- }
-
- // Add one more newline if the last character was a newline
- if (lastNewLine) {
- stringContents.add(new AttributedString(" "));
- pEnds.add(new RunInfo(pos, pos + 1));
-
- lastNewLine = false;
- }
-
- // The default style context - which will be reused
- StyleContext style = StyleContext.getDefaultStyleContext();
-
- RunInfo pEnd = null;
- for (int i = 0; i < stringContents.size(); i++) {
- pEnd = (RunInfo) pEnds.get(i);
- pos = pEnd.runStart;
+
+ // The paragraph start and end indices
+ ArrayList pEnds = extractParagraphRanges(documentString);
+
+ // The default style context - which will be reused
+ StyleContext styleContext = StyleContext.getDefaultStyleContext();
+
+ int pos;
+ RunInfo paragraphRange = null;
+
+ AttributedString attributedString;
+
+ Iterator contentIterator = stringContents.iterator();
+ Iterator paragraphIterator = pEnds.iterator();
+ while (contentIterator.hasNext() && paragraphIterator.hasNext()) {
+ paragraphRange = (RunInfo) paragraphIterator.next();
+ attributedString = (AttributedString) contentIterator.next();
+ pos = paragraphRange.startIndex;
// The current element will be used as a temp variable while
- // searching
- // for the leaf element at the current position
+ // searching for the leaf element at the current position
Element curElement = null;
// Small assumption here that there is one root element - can
fix
@@ -229,151 +175,194 @@
// If the string is length 0 then we just need to add the
attributes
// once
- if (pEnd.runStart != pEnd.runLimit) {
- // OK, now we loop until we find all the leaf elements in
the
- // range
- while (pos < pEnd.runLimit) {
-
- // Before each pass, start at the root
- curElement = rootElement;
-
- // Now we descend the hierarchy until we get to a leaf
- while (!curElement.isLeaf()) {
- curElement =
curElement.getElement(curElement.getElementIndex(pos));
- }
-
- // These are the mandatory attributes
-
+ if (paragraphRange.isEmpty()) {
+ curElement = drillDownFromRoot(pos, rootElement);
+
+ // These are the mandatory attributes
+ AttributeSet attributes = curElement.getAttributes();
+ Color foreground = styleContext.getForeground(attributes);
+
+ attributedString.addAttribute(TextAttribute.FOREGROUND,
foreground, (int) Math.max(0, curElement
+ .getStartOffset()
+ - paragraphRange.startIndex), (int)
Math.min(paragraphRange.length(), curElement.getEndOffset()
+ - paragraphRange.startIndex));
+
+ // These are the optional attributes
+ Font font = extractFont(styleContext, pos, rootElement,
attributes);
+ applyFontAttribute(paragraphRange, attributedString,
curElement, font);
+ applyBackgroundAttribute(styleContext, paragraphRange,
attributedString, curElement, attributes);
+ applyUnderlineAttribute(paragraphRange, attributedString,
curElement, attributes);
+ applyStrikeThroughAttribute(paragraphRange,
attributedString, curElement, attributes);
+ }
+ else {
+ // OK, now we loop until we find all the leaf elements in
the
+ // range
+ while (pos < paragraphRange.endIndex) {
+ curElement = drillDownFromRoot(pos, rootElement);
+
+ // These are the mandatory attributes
AttributeSet attributes = curElement.getAttributes();
- Color foreground = style.getForeground(attributes);
-
- ((AttributedString)
stringContents.get(i)).addAttribute(TextAttribute.FOREGROUND, foreground,
- (int) Math.max(0, curElement.getStartOffset()
- pEnd.runStart), (int) Math.min(
- pEnd.runLimit - pEnd.runStart,
curElement.getEndOffset() - pEnd.runStart));
-
- Font font =
(attributes.isDefined(StyleConstants.FontSize) || attributes
- .isDefined(StyleConstants.FontFamily)) ?
style.getFont(attributes) : null;
- if (font == null) {
- if (document instanceof DefaultStyledDocument) {
- font = style.getFont(((DefaultStyledDocument)
document).getCharacterElement(pos)
- .getAttributes());
- if (font == null) {
- font =
style.getFont(((DefaultStyledDocument) document).getParagraphElement(pos)
- .getAttributes());
- }
- if (font == null) {
- font =
style.getFont(rootElement.getAttributes());
- }
- }
- else {
- font =
style.getFont(rootElement.getAttributes());
- }
- }
- if (font != null) {
- ((AttributedString)
stringContents.get(i)).addAttribute(TextAttribute.FONT, font, (int) Math
- .max(0, curElement.getStartOffset() -
pEnd.runStart), (int) Math.min(pEnd.runLimit
- - pEnd.runStart, curElement.getEndOffset()
- pEnd.runStart));
- }
+ Color foreground =
styleContext.getForeground(attributes);
+
+
attributedString.addAttribute(TextAttribute.FOREGROUND, foreground, (int)
Math.max(0, curElement
+ .getStartOffset()
+ - paragraphRange.startIndex), (int)
Math.min(paragraphRange.length(), curElement
+ .getEndOffset()
+ - paragraphRange.startIndex));
+
+ Font font = extractFont(styleContext, pos,
rootElement, attributes);
+ applyFontAttribute(paragraphRange, attributedString,
curElement, font);
// These are the optional attributes
- Color background =
(attributes.isDefined(StyleConstants.Background)) ? style
- .getBackground(attributes) : null;
- if (background != null) {
- ((AttributedString)
stringContents.get(i)).addAttribute(TextAttribute.BACKGROUND, background,
- (int) Math.max(0,
curElement.getStartOffset() - pEnd.runStart), (int) Math.min(
- pEnd.runLimit - pEnd.runStart,
curElement.getEndOffset() - pEnd.runStart));
- }
-
- boolean underline =
StyleConstants.isUnderline(attributes);
- if (underline) {
- ((AttributedString)
stringContents.get(i)).addAttribute(TextAttribute.UNDERLINE, Boolean.TRUE,
- (int) Math.max(0,
curElement.getStartOffset() - pEnd.runStart), (int) Math.min(
- pEnd.runLimit - pEnd.runStart,
curElement.getEndOffset() - pEnd.runStart));
- }
-
- boolean strikethrough =
StyleConstants.isStrikeThrough(attributes);
- if (strikethrough) {
- ((AttributedString)
stringContents.get(i)).addAttribute(TextAttribute.STRIKETHROUGH,
- Boolean.TRUE, (int) Math.max(0,
curElement.getStartOffset() - pEnd.runStart),
- (int) Math
- .min(pEnd.runLimit -
pEnd.runStart, curElement.getEndOffset() - pEnd.runStart));
- }
+ applyBackgroundAttribute(styleContext, paragraphRange,
attributedString, curElement, attributes);
+
+ applyUnderlineAttribute(paragraphRange,
attributedString, curElement, attributes);
+
+ applyStrikeThroughAttribute(paragraphRange,
attributedString, curElement, attributes);
// And set the position to the end of the given
attribute
pos = curElement.getEndOffset();
}
}
- else {
- // Before each pass, start at the root
- curElement = rootElement;
-
- // Now we descend the hierarchy until we get to a leaf
- while (!curElement.isLeaf()) {
- curElement =
curElement.getElement(curElement.getElementIndex(pos));
- }
-
- // These are the mandatory attributes
-
- AttributeSet attributes = curElement.getAttributes();
- Color foreground = style.getForeground(attributes);
-
- ((AttributedString)
stringContents.get(i)).addAttribute(TextAttribute.FOREGROUND, foreground,
- (int) Math.max(0, curElement.getStartOffset() -
pEnd.runStart), (int) Math.min(pEnd.runLimit
- - pEnd.runStart, curElement.getEndOffset()
- pEnd.runStart));
-
- // These are the optional attributes
-
- Font font = (attributes.isDefined(StyleConstants.FontSize)
|| attributes
- .isDefined(StyleConstants.FontFamily)) ?
style.getFont(attributes) : null;
+ }
+
+ recomputeLayout();
+ }
+
+ private Element drillDownFromRoot(int pos, Element rootElement) {
+ Element curElement;
+ // Before each pass, start at the root
+ curElement = rootElement;
+
+ // Now we descend the hierarchy until we get to a leaf
+ while (!curElement.isLeaf()) {
+ curElement =
curElement.getElement(curElement.getElementIndex(pos));
+ }
+ return curElement;
+ }
+
+ private void applyFontAttribute(RunInfo paragraphRange,
AttributedString attributedString, Element curElement,
+ Font font) {
+ if (font != null) {
+ attributedString.addAttribute(TextAttribute.FONT, font, (int)
Math.max(0, curElement.getStartOffset()
+ - paragraphRange.startIndex), (int)
Math.min(paragraphRange.endIndex - paragraphRange.startIndex,
+ curElement.getEndOffset() -
paragraphRange.startIndex));
+ }
+ }
+
+ private void applyStrikeThroughAttribute(RunInfo paragraphRange,
AttributedString attributedString,
+ Element curElement, AttributeSet attributes) {
+ boolean strikethrough = StyleConstants.isStrikeThrough(attributes);
+ if (strikethrough) {
+ attributedString.addAttribute(TextAttribute.STRIKETHROUGH,
Boolean.TRUE, (int) Math.max(0, curElement
+ .getStartOffset()
+ - paragraphRange.startIndex), (int)
Math.min(paragraphRange.endIndex - paragraphRange.startIndex,
+ curElement.getEndOffset() -
paragraphRange.startIndex));
+ }
+ }
+
+ private void applyUnderlineAttribute(RunInfo paragraphRange,
AttributedString attributedString, Element curElement,
+ AttributeSet attributes) {
+ boolean underline = StyleConstants.isUnderline(attributes);
+ if (underline) {
+ attributedString.addAttribute(TextAttribute.UNDERLINE,
Boolean.TRUE, (int) Math.max(0, curElement
+ .getStartOffset()
+ - paragraphRange.startIndex), (int)
Math.min(paragraphRange.endIndex - paragraphRange.startIndex,
+ curElement.getEndOffset() -
paragraphRange.startIndex));
+ }
+ }
+
+ private void applyBackgroundAttribute(StyleContext style, RunInfo
paragraphRange,
+ AttributedString attributedString, Element curElement,
AttributeSet attributes) {
+ Color background =
(attributes.isDefined(StyleConstants.Background)) ?
style.getBackground(attributes) : null;
+ if (background != null) {
+ attributedString.addAttribute(TextAttribute.BACKGROUND,
background, (int) Math.max(0, curElement
+ .getStartOffset()
+ - paragraphRange.startIndex), (int)
Math.min(paragraphRange.endIndex - paragraphRange.startIndex,
+ curElement.getEndOffset() -
paragraphRange.startIndex));
+ }
+ }
+
+ private Font extractFont(StyleContext style, int pos, Element
rootElement, AttributeSet attributes) {
+ Font font = (attributes.isDefined(StyleConstants.FontSize) ||
attributes.isDefined(StyleConstants.FontFamily)) ? style
+ .getFont(attributes)
+ : null;
+ if (font == null) {
+ if (document instanceof DefaultStyledDocument) {
+ font = style.getFont(((DefaultStyledDocument)
document).getCharacterElement(pos).getAttributes());
if (font == null) {
- if (document instanceof DefaultStyledDocument) {
- font = style.getFont(((DefaultStyledDocument)
document).getCharacterElement(pos)
- .getAttributes());
- if (font == null) {
- font = style.getFont(((DefaultStyledDocument)
document).getParagraphElement(pos)
- .getAttributes());
- }
- if (font == null) {
- font =
style.getFont(rootElement.getAttributes());
- }
- }
- else {
- font = style.getFont(rootElement.getAttributes());
- }
- }
-
- if (font != null) {
- ((AttributedString)
stringContents.get(i)).addAttribute(TextAttribute.FONT, font, (int)
Math.max(0,
- curElement.getStartOffset() - pEnd.runStart),
(int) Math.min(pEnd.runLimit - pEnd.runStart,
- curElement.getEndOffset() - pEnd.runStart));
- }
-
- Color background =
(attributes.isDefined(StyleConstants.Background)) ?
style.getBackground(attributes)
- : null;
- if (background != null) {
- ((AttributedString)
stringContents.get(i)).addAttribute(TextAttribute.BACKGROUND, background,
- (int) Math.max(0, curElement.getStartOffset()
- pEnd.runStart), (int) Math.min(
- pEnd.runLimit - pEnd.runStart,
curElement.getEndOffset() - pEnd.runStart));
- }
-
- boolean underline = StyleConstants.isUnderline(attributes);
- if (underline) {
- ((AttributedString)
stringContents.get(i)).addAttribute(TextAttribute.UNDERLINE, Boolean.TRUE,
- (int) Math.max(0, curElement.getStartOffset()
- pEnd.runStart), (int) Math.min(
- pEnd.runLimit - pEnd.runStart,
curElement.getEndOffset() - pEnd.runStart));
- }
-
- boolean strikethrough =
StyleConstants.isStrikeThrough(attributes);
- if (strikethrough) {
- ((AttributedString)
stringContents.get(i)).addAttribute(TextAttribute.STRIKETHROUGH,
Boolean.TRUE,
- (int) Math.max(0, curElement.getStartOffset()
- pEnd.runStart), (int) Math.min(
- pEnd.runLimit - pEnd.runStart,
curElement.getEndOffset() - pEnd.runStart));
- }
+ font = style.getFont(((DefaultStyledDocument)
document).getParagraphElement(pos).getAttributes());
+ }
+ if (font == null) {
+ font = style.getFont(rootElement.getAttributes());
+ }
+ }
+ else {
+ font = style.getFont(rootElement.getAttributes());
+ }
+ }
+ return font;
+ }
+
+ private ArrayList extractParagraphRanges(String documentString) {
+ // The paragraph start and end indices
+ ArrayList paragraphRanges = new ArrayList();
+
+ // The current position in the specified range
+ int pos = 0;
+
+ StringTokenizer tokenizer = new
StringTokenizer(documentString, "\n", true);
+
+ // lastNewLine is used to detect the case when two newlines follow
+ // in direct succession
+ // & lastNewLine should be true to start in case the first
character
+ // is a newline
+ boolean lastNewLine = true;
+
+ for (int i = 0; tokenizer.hasMoreTokens(); i++) {
+ String token = tokenizer.nextToken();
+
+ // If the token
+ if (token.equals("\n")) {
+ if (lastNewLine) {
+ stringContents.add(new AttributedString(" "));
+ paragraphRanges.add(new RunInfo(pos, pos + 1));
+ }
+
+ pos = pos + 1;
+
+ lastNewLine = true;
+ }
+ // If the token is empty - create an attributed string with a
+ // single space since LineBreakMeasurers don't work with an
empty
+ // string
+ // - note that this case should only arise if the document is
empty
+ else if (token.equals("")) {
+ stringContents.add(new AttributedString(" "));
+ paragraphRanges.add(new RunInfo(pos, pos));
+
+ lastNewLine = false;
+ }
+ // This is the normal case - where we have some text
+ else {
+ stringContents.add(new AttributedString(token));
+ paragraphRanges.add(new RunInfo(pos, pos +
token.length()));
+
+ // Increment the position
+ pos = pos + token.length();
+
+ lastNewLine = false;
}
}
- recomputeLayout();
+ // Add one more newline if the last character was a newline
+ if (lastNewLine) {
+ stringContents.add(new AttributedString(" "));
+ paragraphRanges.add(new RunInfo(pos, pos + 1));
+ }
+
+ return paragraphRanges;
}
/**
@@ -391,30 +380,17 @@
double textWidth = 0;
double textHeight = 0;
- for (int i = 0; i < stringContents.size(); i++) {
-
- AttributedString ats = (AttributedString)
stringContents.get(i);
+ Iterator contentIterator = stringContents.iterator();
+
+ while (contentIterator.hasNext()) {
+ AttributedString ats = (AttributedString)
contentIterator.next();
AttributedCharacterIterator itr = ats.getIterator();
LineBreakMeasurer measurer;
ArrayList breakList = null;
- // First we have to do an initial pass with a
LineBreakMeasurer to
- // find out where Swing is going to break the lines - i.e.
- // because it doesn't use fractional metrics
-
measurer = new LineBreakMeasurer(itr, SWING_FRC);
- breakList = new ArrayList();
- while (measurer.getPosition() < itr.getEndIndex()) {
- if (constrainWidthToTextWidth) {
- measurer.nextLayout(Float.MAX_VALUE);
- }
- else {
- measurer.nextLayout((float) Math.ceil(getWidth() -
insets.left - insets.right));
- }
-
- breakList.add(new Integer(measurer.getPosition()));
- }
+ breakList = extractLineBreaks(itr, measurer);
measurer = new LineBreakMeasurer(itr,
PPaintContext.RENDER_QUALITY_HIGH_FRC);
@@ -475,20 +451,43 @@
lines = (LineInfo[]) linesList.toArray(new LineInfo[0]);
- if (constrainWidthToTextWidth || constrainHeightToTextHeight) {
- double newWidth = getWidth();
- double newHeight = getHeight();
-
+ constrainDimensionsIfNeeded(textWidth, textHeight);
+ }
+
+ private void constrainDimensionsIfNeeded(double textWidth, double
textHeight) {
+ if (!constrainWidthToTextWidth && !constrainHeightToTextHeight)
+ return;
+
+ double newWidth = getWidth();
+ double newHeight = getHeight();
+
+ if (constrainWidthToTextWidth) {
+ newWidth = textWidth + insets.left + insets.right;
+ }
+
+ if (constrainHeightToTextHeight) {
+ newHeight = Math.max(textHeight, getInitialFontHeight()) +
insets.top + insets.bottom;
+ }
+
+ super.setBounds(getX(), getY(), newWidth, newHeight);
+ }
+
+ // Because swing doesn't use fractional font metrics by default, we use
+ // LineBreakMeasurer to find out where Swing is going to break them
+ private ArrayList extractLineBreaks(AttributedCharacterIterator itr,
LineBreakMeasurer measurer) {
+ ArrayList breakList;
+ breakList = new ArrayList();
+ while (measurer.getPosition() < itr.getEndIndex()) {
if (constrainWidthToTextWidth) {
- newWidth = textWidth + insets.left + insets.right;
- }
-
- if (constrainHeightToTextHeight) {
- newHeight = Math.max(textHeight, getInitialFontHeight()) +
insets.top + insets.bottom;
+ measurer.nextLayout(Float.MAX_VALUE);
+ }
+ else {
+ measurer.nextLayout((float) Math.ceil(getWidth() -
insets.left - insets.right));
}
- super.setBounds(getX(), getY(), newWidth, newHeight);
- }
+ breakList.add(new Integer(measurer.getPosition()));
+ }
+ return breakList;
}
/**
@@ -518,13 +517,14 @@
}
protected void paint(PPaintContext paintContext) {
+ if (lines == null || lines.length == 0)
+ return;
+
float x = (float) (getX() + insets.left);
float y = (float) (getY() + insets.top);
float bottomY = (float) (getY() + getHeight() - insets.bottom);
- if (lines == null || lines.length == 0) {
- return;
- }
+
Graphics2D g2 = paintContext.getGraphics();
@@ -534,27 +534,27 @@
}
float curX;
+ LineInfo lineInfo;
for (int i = 0; i < lines.length; i++) {
- y += lines[i].maxAscent;
+ lineInfo = lines[i];
+ y += lineInfo.maxAscent;
curX = x;
if (bottomY < y) {
return;
}
- for (int j = 0; j < lines[i].segments.size(); j++) {
- SegmentInfo sInfo = (SegmentInfo) lines[i].segments.get(j);
+ for (int j = 0; j < lineInfo.segments.size(); j++) {
+ SegmentInfo sInfo = (SegmentInfo) lineInfo.segments.get(j);
float width = sInfo.layout.getAdvance();
-
+
if (sInfo.background != null) {
g2.setPaint(sInfo.background);
- g2.fill(new Rectangle2D.Double(curX, y -
lines[i].maxAscent, width, lines[i].maxAscent
- + lines[i].maxDescent + lines[i].leading));
+ g2.fill(new Rectangle2D.Double(curX, y -
lineInfo.maxAscent, width, lineInfo.maxAscent
+ + lineInfo.maxDescent + lineInfo.leading));
}
- if (sInfo.font != null) {
- g2.setFont(sInfo.font);
- }
+ sInfo.applyFont(g2);
// Manually set the paint - this is specified in the
// AttributedString but seems to be
@@ -566,16 +566,16 @@
// Draw the underline and the strikethrough after the text
if (sInfo.underline != null) {
- paintLine.setLine(x, y + lines[i].maxDescent / 2, x +
width, y + lines[i].maxDescent / 2);
+ paintLine.setLine(x, y + lineInfo.maxDescent / 2, x +
width, y + lineInfo.maxDescent / 2);
g2.draw(paintLine);
}
curX = curX + width;
}
- y += lines[i].maxDescent + lines[i].leading;
- }
- }
+ y += lineInfo.maxDescent + lineInfo.leading;
+ }
+ }
public void fullPaint(PPaintContext paintContext) {
if (!editing) {
@@ -631,18 +631,26 @@
}
/**
- * Simple class to represent an integer run
+ * Simple class to represent an range within the document
*/
protected static class RunInfo {
- public int runStart;
- public int runLimit;
+ public int startIndex;
+ public int endIndex;
public RunInfo() {
}
public RunInfo(int runStart, int runLimit) {
- this.runStart = runStart;
- this.runLimit = runLimit;
+ this.startIndex = runStart;
+ this.endIndex = runLimit;
+ }
+
+ public boolean isEmpty() {
+ return startIndex == endIndex;
+ }
+
+ public int length() {
+ return endIndex - startIndex;
}
}
@@ -680,5 +688,11 @@
public SegmentInfo() {
}
+
+ public void applyFont(Graphics2D g2) {
+ if (font != null) {
+ g2.setFont(font);
+ }
+ }
}
}
--~--~---------~--~----~------------~-------~--~----~
Piccolo2D Developers Group: http://groups.google.com/group/piccolo2d-dev?hl=en
-~----------~----~----~----~------~----~------~--~---