package nodebox.graphics;
import java.awt.*;
import java.awt.font.FontRenderContext;
import java.awt.font.LineBreakMeasurer;
import java.awt.font.TextAttribute;
import java.awt.font.TextLayout;
import java.awt.geom.AffineTransform;
import java.awt.geom.Rectangle2D;
import java.text.AttributedString;
import java.util.Hashtable;
import java.util.Iterator;
public class Text extends AbstractGrob {
public enum Align {
LEFT, RIGHT, CENTER, JUSTIFY
}
private String text;
private double baseLineX, baseLineY;
private double width = 0;
private double height = 0;
private String fontName = "Helvetica";
private double fontSize = 24;
private double lineHeight = 1.2;
private Align align = Align.CENTER;
private Color fillColor = new Color();
public Text(String text, Point pt) {
this.text = text;
this.baseLineX = pt.getX();
this.baseLineY = pt.getY();
}
public Text(String text, double baseLineX, double baseLineY) {
this.text = text;
this.baseLineX = baseLineX;
this.baseLineY = baseLineY;
}
public Text(String text, Rect r) {
this.text = text;
this.baseLineX = r.getX();
this.baseLineY = r.getY();
this.width = r.getWidth();
this.height = r.getHeight();
}
public Text(String text, double x, double y, double width, double height) {
this.text = text;
this.baseLineX = x;
this.baseLineY = y;
this.width = width;
this.height = height;
}
public Text(Text other) {
super(other);
this.text = other.text;
this.baseLineX = other.baseLineX;
this.baseLineY = other.baseLineY;
this.width = other.width;
this.height = other.height;
this.fontName = other.fontName;
this.fontSize = other.fontSize;
this.lineHeight = other.lineHeight;
this.align = other.align;
fillColor = other.fillColor == null ? null : other.fillColor.clone();
}
//// Getters/setters /////
public String getText() {
return text;
}
public void setText(String text) {
this.text = text;
}
public double getBaseLineX() {
return baseLineX;
}
public void setBaseLineX(double baseLineX) {
this.baseLineX = baseLineX;
}
public double getBaseLineY() {
return baseLineY;
}
public void setBaseLineY(double baseLineY) {
this.baseLineY = baseLineY;
}
public double getWidth() {
return width;
}
public void setWidth(double width) {
this.width = width;
}
public double getHeight() {
return height;
}
public void setHeight(double height) {
this.height = height;
}
public String getFontName() {
return fontName;
}
public void setFontName(String fontName) {
this.fontName = fontName;
}
public double getFontSize() {
return fontSize;
}
public void setFontSize(double fontSize) {
this.fontSize = fontSize;
}
public Font getFont() {
Hashtable<TextAttribute, Object> m = new Hashtable<TextAttribute, Object>();
m.put(TextAttribute.FAMILY, fontName);
m.put(TextAttribute.SIZE, fontSize);
m.put(TextAttribute.KERNING, TextAttribute.KERNING_ON);
return Font.getFont(m);
}
public double getLineHeight() {
return lineHeight;
}
public void setLineHeight(double lineHeight) {
this.lineHeight = lineHeight;
}
public Align getAlign() {
return align;
}
public void setAlign(Align align) {
this.align = align;
}
public Color getFillColor() {
return fillColor;
}
public void setFillColor(Color fillColor) {
this.fillColor = fillColor;
}
//// Font management ////
public static boolean fontExists(String fontName) {
// TODO: Move getAllFonts() in static attribute.
GraphicsEnvironment env = GraphicsEnvironment.getLocalGraphicsEnvironment();
Font[] allFonts = env.getAllFonts();
for (Font font : allFonts) {
if (font.getName().equals(fontName)) {
return true;
}
}
return false;
}
//// Metrics ////
private AttributedString getStyledText(String text) {
// TODO: Find a better way to handle empty Strings (like for example paragraph line breaks)
if (text.length() == 0)
text = " ";
AttributedString attrString = new AttributedString(text);
attrString.addAttribute(TextAttribute.FONT, getFont());
if (fillColor != null)
attrString.addAttribute(TextAttribute.FOREGROUND, fillColor.getAwtColor());
if (align == Align.RIGHT) {
//attrString.addAttribute(TextAttribute.RUN_DIRECTION, TextAttribute.RUN_DIRECTION_RTL);
} else if (align == Align.CENTER) {
// TODO: Center alignment?
} else if (align == Align.JUSTIFY) {
attrString.addAttribute(TextAttribute.JUSTIFICATION, TextAttribute.JUSTIFICATION_FULL);
}
return attrString;
}
public Rect getMetrics() {
if (text == null || text.length() == 0) return new Rect();
TextLayoutIterator iterator = new TextLayoutIterator();
Rectangle2D bounds = new Rectangle2D.Double();
while (iterator.hasNext()) {
TextLayout layout = iterator.next();
// TODO: Compensate X, Y
bounds = bounds.createUnion(layout.getBounds());
}
return new Rect(bounds);
}
//// Transformations ////
protected void setupTransform(Graphics2D g) {
saveTransform(g);
AffineTransform trans = g.getTransform();
trans.concatenate(getTransform().getAffineTransform());
g.setTransform(trans);
}
public void draw(Graphics2D g) {
if (fillColor == null) return;
setupTransform(g);
if (text == null || text.length() == 0) return;
TextLayoutIterator iterator = new TextLayoutIterator();
while (iterator.hasNext()) {
TextLayout layout = iterator.next();
layout.draw(g, (float) (baseLineX + iterator.getX()), (float) (baseLineY + iterator.getY()));
}
restoreTransform(g);
}
public Path getPath() {
Path p = new Path();
p.setFillColor(fillColor == null ? null : fillColor.clone());
TextLayoutIterator iterator = new TextLayoutIterator();
while (iterator.hasNext()) {
TextLayout layout = iterator.next();
AffineTransform trans = new AffineTransform();
trans.translate(baseLineX + iterator.getX(), baseLineY + iterator.getY());
Shape shape = layout.getOutline(trans);
p.extend(shape);
}
p.transform(getTransform());
return p;
}
public boolean isEmpty() {
return text.trim().length() == 0;
}
public Rect getBounds() {
// TODO: This is correct, but creating a full path just for measuring bounds is slow.
return getPath().getBounds();
}
public Text clone() {
return new Text(this);
}
private class TextLayoutIterator implements Iterator<TextLayout> {
private double x, y;
private double ascent;
private int currentIndex = 0;
private String[] textParts;
private LineBreakMeasurer[] measurers;
private LineBreakMeasurer currentMeasurer;
private String currentText;
private boolean first;
private TextLayoutIterator() {
x = 0;
y = 0;
textParts = text.split("\n");
measurers = new LineBreakMeasurer[textParts.length];
FontRenderContext frc = new FontRenderContext(new AffineTransform(), true, true);
for (int i = 0; i < textParts.length; i++) {
AttributedString s = getStyledText(textParts[i]);
measurers[i] = new LineBreakMeasurer(s.getIterator(), frc);
}
currentMeasurer = measurers[currentIndex];
currentText = textParts[currentIndex];
first = true;
}
public boolean hasNext() {
if (currentMeasurer.getPosition() < currentText.length())
return true;
else {
currentIndex++;
if (currentIndex < textParts.length) {
currentMeasurer = measurers[currentIndex];
currentText = textParts[currentIndex];
return hasNext();
} else {
return false;
}
}
}
public TextLayout next() {
if (first) {
first = false;
} else {
y += ascent * lineHeight;
}
double layoutWidth = width == 0 ? Float.MAX_VALUE : width;
TextLayout layout = currentMeasurer.nextLayout((float) layoutWidth);
if (width == 0) {
layoutWidth = layout.getAdvance();
if (align == Align.RIGHT) {
x = -layoutWidth;
} else if (align == Align.CENTER) {
x = -layoutWidth / 2.0;
}
} else if (align == Align.RIGHT) {
x = width - layout.getAdvance();
} else if (align == Align.CENTER) {
x = (width - layout.getAdvance()) / 2.0;
} else if (align == Align.JUSTIFY) {
// Don't justify the last line.
if (currentMeasurer.getPosition() < currentText.length()) {
layout = layout.getJustifiedLayout((float) width);
}
}
ascent = layout.getAscent();
// y += layout.getDescent() + layout.getLeading() + layout.getAscent();
return layout;
}
public double getX() {
return x;
}
public double getY() {
return y;
}
public void remove() {
throw new AssertionError("This operation is not implemented");
}
}
}