package com.kreative.paint.document.draw;
import java.awt.AlphaComposite;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.FontMetrics;
import java.awt.Graphics2D;
import java.awt.Shape;
import java.awt.SystemColor;
import java.awt.font.FontRenderContext;
import java.awt.font.LineBreakMeasurer;
import java.awt.font.TextAttribute;
import java.awt.font.TextHitInfo;
import java.awt.font.TextLayout;
import java.awt.geom.AffineTransform;
import java.awt.geom.Line2D;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.text.AttributedCharacterIterator;
import java.text.AttributedString;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import com.kreative.paint.document.undo.Atom;
public class TextDrawObject extends DrawObject {
private double x, y, wrapWidth;
private String text;
private int cursorStart, cursorEnd;
private double a, w, h;
public TextDrawObject(PaintSettings ps, double x, double y, double wrapWidth, String text) {
super(ps);
this.x = x;
this.y = y;
this.wrapWidth = wrapWidth;
this.text = normalize(text);
this.cursorStart = -1;
this.cursorEnd = -1;
a = w = h = 0;
}
private TextDrawObject(TextDrawObject o) {
super(o);
this.x = o.x;
this.y = o.y;
this.wrapWidth = o.wrapWidth;
this.text = o.text;
this.cursorStart = -1;
this.cursorEnd = -1;
a = w = h = 0;
}
@Override
public TextDrawObject clone() {
return new TextDrawObject(this);
}
@Override
protected void notifyDrawObjectListeners(int id) {
a = w = h = 0;
super.notifyDrawObjectListeners(id);
}
@Override
protected Shape getBoundaryImpl() {
switch (ps.textAlignment) {
case CENTER: return new Rectangle2D.Double(x - w / 2, y - a, w, h);
case RIGHT: return new Rectangle2D.Double(x - w, y - a, w, h);
default: return new Rectangle2D.Double(x, y - a, w, h);
}
}
@Override
protected Shape getHitAreaImpl() {
switch (ps.textAlignment) {
case CENTER: return new Rectangle2D.Double(x - w / 2, y - a, w, h);
case RIGHT: return new Rectangle2D.Double(x - w, y - a, w, h);
default: return new Rectangle2D.Double(x, y - a, w, h);
}
}
@Override
protected Object getControlState() {
return new double[]{ x, y, wrapWidth };
}
@Override
protected void setControlState(Object o) {
double[] state = (double[])o;
x = state[0];
y = state[1];
wrapWidth = state[2];
}
@Override
public int getControlPointCount() {
return 1;
}
@Override
protected ControlPoint getControlPointImpl(int i) {
return new ControlPoint(ControlPointType.BASELINE, x, y);
}
@Override
protected List<ControlPoint> getControlPointsImpl() {
List<ControlPoint> cpts = new ArrayList<ControlPoint>();
cpts.add(new ControlPoint(ControlPointType.BASELINE, x, y));
return cpts;
}
@Override
protected Collection<Line2D> getControlLinesImpl() {
return null;
}
@Override
protected int setControlPointImpl(int i, double x, double y) {
this.x = x;
this.y = y;
return i;
}
@Override
protected Point2D getLocationImpl() {
return new Point2D.Double(x, y);
}
@Override
protected void setLocationImpl(double x, double y) {
this.x = x;
this.y = y;
}
private static final Color CURSOR_COLOR = new Color(0x80808080, true);
private static final Color HIGHLIGHT_COLOR = new Color(SystemColor.textHighlight.getRGB());
@Override
protected void paintImpl(Graphics2D g) {
g.translate(x, y);
paintText(g);
}
private void paintText(Graphics2D g) {
FontRenderContext frc = g.getFontRenderContext();
FontMetrics fm = g.getFontMetrics(ps.textFont);
double lx = 0, ly = 0;
int cs = Math.min(cursorStart, cursorEnd);
int ce = Math.max(cursorStart, cursorEnd);
String[] ss = text.split("\n");
a = w = h = 0;
for (String s : ss) {
if (s.length() < 1) {
if (cs == 0 && ce == 0) {
g.setPaint(CURSOR_COLOR);
g.setComposite(AlphaComposite.SrcOver);
g.setStroke(new BasicStroke(1));
g.drawLine(
0, (int)Math.round(ly - fm.getAscent()),
0, (int)Math.round(ly + fm.getDescent())
);
}
ly += fm.getHeight();
a = Math.max(a, fm.getAscent());
h += fm.getHeight();
cs--; ce--;
} else {
AttributedString as = new AttributedString(s);
as.addAttribute(TextAttribute.FONT, ps.textFont);
AttributedCharacterIterator aci = as.getIterator();
LineBreakMeasurer lbm = new LineBreakMeasurer(aci, frc);
lbm.setPosition(aci.getBeginIndex());
while (lbm.getPosition() < aci.getEndIndex()) {
int smin = lbm.getPosition();
TextLayout tl = lbm.nextLayout((float)wrapWidth);
int smax = lbm.getPosition();
switch (ps.textAlignment) {
case CENTER: lx = -tl.getAdvance() / 2; break;
case RIGHT: lx = -tl.getAdvance(); break;
default: lx = 0; break;
}
if (ce >= smin && cs <= smax) {
if (cs == ce) {
int lcs = Math.min(Math.max(smin, cs), smax);
g.setPaint(CURSOR_COLOR);
g.setComposite(AlphaComposite.SrcOver);
g.setStroke(new BasicStroke(1));
Shape[] carets = tl.getCaretShapes(lcs - smin);
for (Shape caret : carets) {
if (caret != null) {
Shape sh = AffineTransform.getTranslateInstance(lx, ly).createTransformedShape(caret);
g.draw(sh);
}
}
} else {
int lcs = Math.min(Math.max(smin, cs), smax);
int lce = Math.min(Math.max(smin, ce), smax);
if (lcs != lce) {
g.setPaint(HIGHLIGHT_COLOR);
g.setComposite(AlphaComposite.SrcOver);
g.setStroke(new BasicStroke(1));
Shape highlight = tl.getLogicalHighlightShape(lcs - smin, lce - smin);
if (highlight != null) {
Shape sh = AffineTransform.getTranslateInstance(lx, ly).createTransformedShape(highlight);
g.fill(sh);
}
}
}
}
ps.applyFill(g);
tl.draw(g, (float)lx, (float)ly);
ly += tl.getAscent() + tl.getDescent() + tl.getLeading();
a = Math.max(a, tl.getAscent());
w = Math.max(w, tl.getAdvance());
h += tl.getAscent() + tl.getDescent() + tl.getLeading();
}
cs -= s.length() + 1;
ce -= s.length() + 1;
}
}
if (cs == ce && cs >= 0) {
ly += fm.getHeight() * cs;
a = Math.max(a, fm.getAscent());
h += fm.getHeight() * cs;
g.setPaint(CURSOR_COLOR);
g.setComposite(AlphaComposite.SrcOver);
g.setStroke(new BasicStroke(1));
g.drawLine(
0, (int)Math.round(ly - fm.getAscent()),
0, (int)Math.round(ly + fm.getDescent())
);
}
}
public int getCursorIndexOfLocation(Graphics2D g, double x, double y) {
if (tx != null) {
try {
Point2D p = new Point2D.Double(x, y);
tx.inverseTransform(p, p);
x = p.getX(); y = p.getY();
} catch (Exception e) {
return -1;
}
}
return getCursorIndexOfLocationImpl(g, x - this.x, y - this.y);
}
private int getCursorIndexOfLocationImpl(Graphics2D gr, double mx, double my) {
FontRenderContext frc = gr.getFontRenderContext();
FontMetrics fm = gr.getFontMetrics(ps.textFont);
if (my < -fm.getAscent()) return 0;
double lx = 0, ly = 0; int cidx = 0;
String[] ss = text.split("\n");
for (String s : ss) {
if (s.length() < 1) {
if (my <= ly + fm.getDescent()) return cidx;
ly += fm.getHeight();
cidx++;
} else {
AttributedString as = new AttributedString(s);
as.addAttribute(TextAttribute.FONT, ps.textFont);
AttributedCharacterIterator aci = as.getIterator();
LineBreakMeasurer lbm = new LineBreakMeasurer(aci, frc);
lbm.setPosition(aci.getBeginIndex());
while (lbm.getPosition() < aci.getEndIndex()) {
int smin = lbm.getPosition();
TextLayout tl = lbm.nextLayout((float)wrapWidth);
int smax = lbm.getPosition();
if (my <= ly + tl.getDescent()) {
switch (ps.textAlignment) {
case CENTER: lx = -tl.getAdvance() / 2; break;
case RIGHT: lx = -tl.getAdvance(); break;
default: lx = 0; break;
}
if (mx <= lx) return cidx + smin;
if (mx >= lx + tl.getAdvance()) return cidx + smax;
TextHitInfo thi = tl.hitTestChar(
(float)mx, (float)ly,
new Rectangle2D.Double(
lx, ly - tl.getAscent(), tl.getAdvance(),
tl.getAscent() + tl.getDescent()
)
);
return cidx + thi.getInsertionIndex();
}
ly += tl.getAscent() + tl.getDescent() + tl.getLeading();
}
cidx += s.length() + 1;
}
}
while (cidx <= text.length()) {
if (my <= ly + fm.getDescent()) return cidx;
ly += fm.getHeight();
cidx++;
}
return text.length();
}
public Point2D getLocationOfCursorIndex(Graphics2D g, int index) {
Point2D p = getLocationOfCursorIndexImpl(g, index);
p.setLocation(p.getX() + x, p.getY() + y);
if (tx != null) tx.transform(p, p);
return p;
}
private Point2D getLocationOfCursorIndexImpl(Graphics2D gr, int ci) {
FontRenderContext frc = gr.getFontRenderContext();
FontMetrics fm = gr.getFontMetrics(ps.textFont);
double lx = 0, ly = 0;
String[] ss = text.split("\n");
for (String s : ss) {
if (s.length() < 1) {
if (ci == 0) return new Point2D.Double(0, ly);
ly += fm.getHeight();
ci--;
} else {
AttributedString as = new AttributedString(s);
as.addAttribute(TextAttribute.FONT, ps.textFont);
AttributedCharacterIterator aci = as.getIterator();
LineBreakMeasurer lbm = new LineBreakMeasurer(aci, frc);
lbm.setPosition(aci.getBeginIndex());
while (lbm.getPosition() < aci.getEndIndex()) {
int smin = lbm.getPosition();
TextLayout tl = lbm.nextLayout((float)wrapWidth);
int smax = lbm.getPosition();
switch (ps.textAlignment) {
case CENTER: lx = -tl.getAdvance() / 2; break;
case RIGHT: lx = -tl.getAdvance(); break;
default: lx = 0; break;
}
if (ci >= smin && ci <= smax) {
int lcs = Math.min(Math.max(smin, ci), smax);
Shape[] carets = tl.getCaretShapes(lcs - smin);
for (Shape caret : carets) {
if (caret != null) {
Shape sh = AffineTransform.getTranslateInstance(lx, ly).createTransformedShape(caret);
double cx = sh.getBounds2D().getCenterX();
return new Point2D.Double(cx, ly);
}
}
}
ly += tl.getAscent() + tl.getDescent() + tl.getLeading();
}
ci -= s.length() + 1;
}
}
ly += fm.getHeight() * ci;
return new Point2D.Double(0, ly);
}
public double getX() { return x; }
public double getY() { return y; }
public double getWrapWidth() { return wrapWidth; }
private static class WrapWidthAtom implements Atom {
private TextDrawObject d;
private double oldWrapWidth;
private double newWrapWidth;
public WrapWidthAtom(TextDrawObject d, double wrapWidth) {
this.d = d;
this.oldWrapWidth = d.wrapWidth;
this.newWrapWidth = wrapWidth;
}
@Override
public boolean canBuildUpon(Atom prev) {
return (prev instanceof WrapWidthAtom)
&& (((WrapWidthAtom)prev).d == this.d);
}
@Override
public Atom buildUpon(Atom prev) {
this.oldWrapWidth = ((WrapWidthAtom)prev).oldWrapWidth;
return this;
}
@Override
public void redo() {
d.wrapWidth = newWrapWidth;
d.notifyDrawObjectListeners(DrawObjectEvent.DRAW_OBJECT_IMPLEMENTATION_PROPERTY_CHANGED);
}
@Override
public void undo() {
d.wrapWidth = oldWrapWidth;
d.notifyDrawObjectListeners(DrawObjectEvent.DRAW_OBJECT_IMPLEMENTATION_PROPERTY_CHANGED);
}
}
public void setWrapWidth(double wrapWidth) {
if (this.wrapWidth == wrapWidth) return;
if (history != null) history.add(new WrapWidthAtom(this, wrapWidth));
this.wrapWidth = wrapWidth;
this.notifyDrawObjectListeners(DrawObjectEvent.DRAW_OBJECT_IMPLEMENTATION_PROPERTY_CHANGED);
}
public String getText() {
return text;
}
public String getSelectedText() {
if (cursorStart < 0 || cursorEnd < 0) return text;
int l = text.length();
int s = Math.min(cursorStart, cursorEnd);
int e = Math.max(cursorStart, cursorEnd);
if (s < 0) s = 0; if (s > l) s = l;
if (e < 0) e = 0; if (e > l) e = l;
return text.substring(s, e);
}
public int getCursorStart() {
return cursorStart;
}
public int getCursorEnd() {
return cursorEnd;
}
public boolean hasSelection() {
return (cursorStart >= 0 && cursorEnd >= 0);
}
private static class TextAtom implements Atom {
private TextDrawObject d;
private String oldText;
private int oldSelStart, oldSelEnd;
private String newText;
private int newSelStart, newSelEnd;
public TextAtom(TextDrawObject d, String text, int selStart, int selEnd) {
this.d = d;
this.oldText = d.text;
this.oldSelStart = d.cursorStart;
this.oldSelEnd = d.cursorEnd;
this.newText = text;
this.newSelStart = selStart;
this.newSelEnd = selEnd;
}
@Override
public boolean canBuildUpon(Atom prev) {
return (prev instanceof TextAtom)
&& (((TextAtom)prev).d == this.d);
}
@Override
public Atom buildUpon(Atom prev) {
this.oldText = ((TextAtom)prev).oldText;
this.oldSelStart = ((TextAtom)prev).oldSelStart;
this.oldSelEnd = ((TextAtom)prev).oldSelEnd;
return this;
}
@Override
public void redo() {
d.text = newText;
d.cursorStart = newSelStart;
d.cursorEnd = newSelEnd;
d.notifyDrawObjectListeners(DrawObjectEvent.DRAW_OBJECT_IMPLEMENTATION_PROPERTY_CHANGED);
}
@Override
public void undo() {
d.text = oldText;
d.cursorStart = oldSelStart;
d.cursorEnd = oldSelEnd;
d.notifyDrawObjectListeners(DrawObjectEvent.DRAW_OBJECT_IMPLEMENTATION_PROPERTY_CHANGED);
}
}
public void setText(String text) {
text = normalize(text);
if (this.text.equals(text)) return;
if (history != null) history.add(new TextAtom(this, text, -1, -1));
this.text = text;
this.cursorStart = -1;
this.cursorEnd = -1;
this.notifyDrawObjectListeners(DrawObjectEvent.DRAW_OBJECT_IMPLEMENTATION_PROPERTY_CHANGED);
}
public void setSelectedText(String text) {
if (cursorStart < 0 || cursorEnd < 0) {
setText(text);
} else {
int l = this.text.length();
int s = Math.min(cursorStart, cursorEnd);
int e = Math.max(cursorStart, cursorEnd);
if (s < 0) s = 0; if (s > l) s = l;
if (e < 0) e = 0; if (e > l) e = l;
text = normalize(text);
String newText = this.text.substring(0, s) + text + this.text.substring(e);
if (history != null) history.add(new TextAtom(this, newText, s + text.length(), s + text.length()));
this.text = newText;
this.cursorStart = s + text.length();
this.cursorEnd = s + text.length();
this.notifyDrawObjectListeners(DrawObjectEvent.DRAW_OBJECT_IMPLEMENTATION_PROPERTY_CHANGED);
}
}
public void setCursorStart(int cursorStart) {
if (this.cursorStart == cursorStart) return;
if (history != null) history.add(new TextAtom(this, text, cursorStart, cursorEnd));
this.cursorStart = cursorStart;
this.notifyDrawObjectListeners(DrawObjectEvent.DRAW_OBJECT_IMPLEMENTATION_PROPERTY_CHANGED);
}
public void setCursorEnd(int cursorEnd) {
if (this.cursorEnd == cursorEnd) return;
if (history != null) history.add(new TextAtom(this, text, cursorStart, cursorEnd));
this.cursorEnd = cursorEnd;
this.notifyDrawObjectListeners(DrawObjectEvent.DRAW_OBJECT_IMPLEMENTATION_PROPERTY_CHANGED);
}
public void setCursor(int cursorStart, int cursorEnd) {
if (this.cursorStart == cursorStart && this.cursorEnd == cursorEnd) return;
if (history != null) history.add(new TextAtom(this, text, cursorStart, cursorEnd));
this.cursorStart = cursorStart;
this.cursorEnd = cursorEnd;
this.notifyDrawObjectListeners(DrawObjectEvent.DRAW_OBJECT_IMPLEMENTATION_PROPERTY_CHANGED);
}
public void clearSelection() {
setCursor(-1, -1);
}
public void deleteBackward() {
if (cursorStart < 0 || cursorEnd < 0) {
setText(null);
} else if (cursorStart != cursorEnd) {
setSelectedText(null);
} else if (cursorStart > 0) {
String newText = text.substring(0, cursorStart - 1) + text.substring(cursorStart);
if (history != null) history.add(new TextAtom(this, newText, cursorStart - 1, cursorStart - 1));
this.text = newText;
this.cursorStart--;
this.cursorEnd--;
this.notifyDrawObjectListeners(DrawObjectEvent.DRAW_OBJECT_IMPLEMENTATION_PROPERTY_CHANGED);
}
}
public void deleteForward() {
if (cursorStart < 0 || cursorEnd < 0) {
setText(null);
} else if (cursorStart != cursorEnd) {
setSelectedText(null);
} else if (cursorStart < text.length()) {
String newText = text.substring(0, cursorStart) + text.substring(cursorStart + 1);
if (history != null) history.add(new TextAtom(this, newText, cursorStart, cursorStart));
this.text = newText;
this.notifyDrawObjectListeners(DrawObjectEvent.DRAW_OBJECT_IMPLEMENTATION_PROPERTY_CHANGED);
}
}
public void moveCursor(Graphics2D g, int lineDelta, int charDelta, boolean shiftDown) {
if (cursorStart < 0 || cursorEnd < 0) return;
if (lineDelta == 0 && charDelta == 0) return;
boolean gTemp = (g == null);
if (gTemp) g = new BufferedImage(8, 8, BufferedImage.TYPE_INT_ARGB).createGraphics();
int lineHeight = g.getFontMetrics(ps.textFont).getHeight();
int cs = cursorStart;
int ce = cursorEnd;
if (lineDelta != 0) {
Point2D p = getLocationOfCursorIndexImpl(g, (lineDelta > 0 || shiftDown) ? ce : cs);
ce = getCursorIndexOfLocationImpl(g, p.getX(), p.getY() + lineHeight * lineDelta);
if (!shiftDown) cs = ce;
}
if (charDelta != 0) {
ce = ((charDelta > 0 || shiftDown) ? ce : cs) + charDelta;
if (ce < 0) ce = 0; if (ce > text.length()) ce = text.length();
if (!shiftDown) cs = ce;
}
setCursor(cs, ce);
if (gTemp) g.dispose();
}
private static String normalize(String text) {
if (text == null) return "";
return text.replaceAll("\r\n|\r|\n|\u2028|\u2029", "\n");
}
}