/* * @(#)SVGText.java * * Copyright (c) 1996-2010 The authors and contributors of JHotDraw. * You may not use, copy or modify this file, except in compliance with the * accompanying license terms. */ package org.jhotdraw.samples.svg.figures; import org.jhotdraw.geom.Insets2D; import org.jhotdraw.geom.Geom; import org.jhotdraw.geom.Dimension2DDouble; import javax.annotation.Nullable; import org.jhotdraw.draw.tool.Tool; import org.jhotdraw.draw.locator.RelativeLocator; import org.jhotdraw.draw.handle.TransformHandleKit; import org.jhotdraw.draw.handle.MoveHandle; import org.jhotdraw.draw.handle.Handle; import org.jhotdraw.draw.tool.TextEditingTool; import org.jhotdraw.draw.handle.FontSizeHandle; import java.awt.*; import java.awt.font.*; import java.awt.geom.*; import java.util.*; import org.jhotdraw.draw.*; import org.jhotdraw.draw.handle.BoundsOutlineHandle; import org.jhotdraw.samples.svg.Gradient; import org.jhotdraw.samples.svg.SVGAttributeKeys; import static org.jhotdraw.samples.svg.SVGAttributeKeys.*; /** * SVGText. * <p> * XXX - At least on Mac OS X - Always draw text using TextLayout.getOutline(), because outline * layout does not match with TextLayout.draw() output. Cache outline to improve performance. * * @author Werner Randelshofer * @version $Id$ * <br>2.1 2007-05-13 Fixed transformation issues. * <br>2.0 2007-04-14 Adapted for new AttributeKeys.TRANSFORM support. * <br>1.0 July 8, 2006 Created. */ public class SVGTextFigure extends SVGAttributedFigure implements TextHolderFigure, SVGFigure { private static final long serialVersionUID = 1L; protected Point2D.Double[] coordinates = new Point2D.Double[]{new Point2D.Double()}; protected double[] rotates = new double[]{0}; private boolean editable = true; /** * This is used to perform faster drawing and hit testing. */ @Nullable private transient Shape cachedTextShape; @Nullable private transient Rectangle2D.Double cachedBounds; @Nullable private transient Rectangle2D.Double cachedDrawingArea; /** * Creates a new instance. */ public SVGTextFigure() { this("Text"); } public SVGTextFigure(String text) { setText(text); SVGAttributeKeys.setDefaults(this); setConnectable(false); } // DRAWING @Override protected void drawText(java.awt.Graphics2D g) { } @Override protected void drawFill(Graphics2D g) { g.fill(getTextShape()); } @Override protected void drawStroke(Graphics2D g) { g.draw(getTextShape()); } // SHAPE AND BOUNDS public void setCoordinates(Point2D.Double[] coordinates) { this.coordinates = coordinates.clone(); invalidate(); } public Point2D.Double[] getCoordinates() { Point2D.Double[] c = new Point2D.Double[coordinates.length]; for (int i = 0; i < c.length; i++) { c[i] = (Point2D.Double) coordinates[i].clone(); } return c; } public void setRotates(double[] rotates) { this.rotates = rotates.clone(); invalidate(); } public double[] getRotates() { return rotates.clone(); } @Override public Rectangle2D.Double getBounds() { if (cachedBounds == null) { cachedBounds = new Rectangle2D.Double(); cachedBounds.setRect(getTextShape().getBounds2D()); String text = getText(); if (text == null || text.length() == 0) { text = " "; } FontRenderContext frc = getFontRenderContext(); HashMap<TextAttribute, Object> textAttributes = new HashMap<TextAttribute, Object>(); textAttributes.put(TextAttribute.FONT, getFont()); if (get(FONT_UNDERLINE)) { textAttributes.put(TextAttribute.UNDERLINE, TextAttribute.UNDERLINE_ON); } TextLayout textLayout = new TextLayout(text, textAttributes, frc); cachedBounds.setRect(coordinates[0].x, coordinates[0].y - textLayout.getAscent(), textLayout.getAdvance(), textLayout.getAscent()); AffineTransform tx = new AffineTransform(); tx.translate(coordinates[0].x, coordinates[0].y); switch (get(TEXT_ANCHOR)) { case END: cachedBounds.x -= textLayout.getAdvance(); break; case MIDDLE: cachedBounds.x -= textLayout.getAdvance() / 2d; break; case START: break; } tx.rotate(rotates[0]); } return (Rectangle2D.Double) cachedBounds.clone(); } @Override public Rectangle2D.Double getDrawingArea() { if (cachedDrawingArea == null) { Rectangle2D rx = getTextShape().getBounds2D(); Rectangle2D.Double r = (rx instanceof Rectangle2D.Double) ? (Rectangle2D.Double) rx : new Rectangle2D.Double(rx.getX(), rx.getY(), rx.getWidth(), rx.getHeight()); double g = SVGAttributeKeys.getPerpendicularHitGrowth(this, 1.0) + 1; Geom.grow(r, g, g); if (get(TRANSFORM) == null) { cachedDrawingArea = r; } else { cachedDrawingArea = new Rectangle2D.Double(); cachedDrawingArea.setRect(get(TRANSFORM).createTransformedShape(r).getBounds2D()); } } return (Rectangle2D.Double) cachedDrawingArea.clone(); } /** * Checks if a Point2D.Double is inside the figure. */ @Override public boolean contains(Point2D.Double p) { if (get(TRANSFORM) != null) { try { p = (Point2D.Double) get(TRANSFORM).inverseTransform(p, new Point2D.Double()); } catch (NoninvertibleTransformException ex) { ex.printStackTrace(); } } return getTextShape().getBounds2D().contains(p); } private Shape getTextShape() { if (cachedTextShape == null) { String text = getText(); if (text == null || text.length() == 0) { text = " "; } FontRenderContext frc = getFontRenderContext(); HashMap<TextAttribute, Object> textAttributes = new HashMap<TextAttribute, Object>(); textAttributes.put(TextAttribute.FONT, getFont()); if (get(FONT_UNDERLINE)) { textAttributes.put(TextAttribute.UNDERLINE, TextAttribute.UNDERLINE_ON); } TextLayout textLayout = new TextLayout(text, textAttributes, frc); AffineTransform tx = new AffineTransform(); tx.translate(coordinates[0].x, coordinates[0].y); switch (get(TEXT_ANCHOR)) { case END: tx.translate(-textLayout.getAdvance(), 0); break; case MIDDLE: tx.translate(-textLayout.getAdvance() / 2d, 0); break; case START: break; } tx.rotate(rotates[0]); /* if (get(TRANSFORM) != null) { tx.preConcatenate(get(TRANSFORM)); }*/ cachedTextShape = tx.createTransformedShape(textLayout.getOutline(tx)); cachedTextShape = textLayout.getOutline(tx); } return cachedTextShape; } @Override public void setBounds(Point2D.Double anchor, Point2D.Double lead) { coordinates = new Point2D.Double[]{ new Point2D.Double(anchor.x, anchor.y) }; rotates = new double[]{0d}; } /** * Transforms the figure. * * @param tx the transformation. */ @Override public void transform(AffineTransform tx) { if (get(TRANSFORM) != null || tx.getType() != (tx.getType() & AffineTransform.TYPE_TRANSLATION)) { if (get(TRANSFORM) == null) { set(TRANSFORM, (AffineTransform) tx.clone()); } else { AffineTransform t = TRANSFORM.getClone(this); t.preConcatenate(tx); set(TRANSFORM, t); } } else { for (int i = 0; i < coordinates.length; i++) { tx.transform(coordinates[i], coordinates[i]); } if (get(FILL_GRADIENT) != null && !get(FILL_GRADIENT).isRelativeToFigureBounds()) { Gradient g = FILL_GRADIENT.getClone(this); g.transform(tx); set(FILL_GRADIENT, g); } if (get(STROKE_GRADIENT) != null && !get(STROKE_GRADIENT).isRelativeToFigureBounds()) { Gradient g = STROKE_GRADIENT.getClone(this); g.transform(tx); set(STROKE_GRADIENT, g); } } invalidate(); } @Override public void restoreTransformTo(Object geometry) { Object[] restoreData = (Object[]) geometry; TRANSFORM.setClone(this, (AffineTransform) restoreData[0]); Point2D.Double[] restoredCoordinates = (Point2D.Double[]) restoreData[1]; for (int i = 0; i < this.coordinates.length; i++) { coordinates[i] = (Point2D.Double) restoredCoordinates[i].clone(); } FILL_GRADIENT.setClone(this, (Gradient) restoreData[2]); STROKE_GRADIENT.setClone(this, (Gradient) restoreData[3]); invalidate(); } @Override public Object getTransformRestoreData() { Point2D.Double[] restoredCoordinates = this.coordinates.clone(); for (int i = 0; i < this.coordinates.length; i++) { restoredCoordinates[i] = (Point2D.Double) this.coordinates[i].clone(); } return new Object[]{ TRANSFORM.getClone(this), restoredCoordinates, FILL_GRADIENT.getClone(this), STROKE_GRADIENT.getClone(this),}; } // ATTRIBUTES /** * Gets the text shown by the text figure. */ @Override public String getText() { return get(TEXT); } @Override public <T> void set(AttributeKey<T> key, T newValue) { if (key.equals(SVGAttributeKeys.TRANSFORM) || key.equals(SVGAttributeKeys.FONT_FACE) || key.equals(SVGAttributeKeys.FONT_BOLD) || key.equals(SVGAttributeKeys.FONT_ITALIC) || key.equals(SVGAttributeKeys.FONT_SIZE)) { invalidate(); } super.set(key, newValue); } /** * Sets the text shown by the text figure. */ @Override public void setText(String newText) { set(TEXT, newText); } @Override public boolean isEditable() { return editable; } public void setEditable(boolean b) { this.editable = b; } @Override public int getTextColumns() { //return (getText() == null) ? 4 : Math.min(getText().length(), 4); return 4; } @Override public Font getFont() { return SVGAttributeKeys.getFont(this); } @Override public Color getTextColor() { return get(FILL_COLOR); // return get(TEXT_COLOR); } @Override public Color getFillColor() { return get(FILL_COLOR) == null || get(FILL_COLOR).equals(Color.white) ? Color.black : Color.WHITE; // return get(FILL_COLOR); } @Override public void setFontSize(float size) { // put(FONT_SIZE, new Double(size)); Point2D.Double p = new Point2D.Double(0, size); AffineTransform tx = get(TRANSFORM); if (tx != null) { try { tx.inverseTransform(p, p); Point2D.Double p0 = new Point2D.Double(0, 0); tx.inverseTransform(p0, p0); p.y -= p0.y; } catch (NoninvertibleTransformException ex) { ex.printStackTrace(); } } set(FONT_SIZE, Math.abs(p.y)); } @Override public float getFontSize() { // return get(FONT_SIZE).floatValue(); Point2D.Double p = new Point2D.Double(0, get(FONT_SIZE)); AffineTransform tx = get(TRANSFORM); if (tx != null) { tx.transform(p, p); Point2D.Double p0 = new Point2D.Double(0, 0); tx.transform(p0, p0); p.y -= p0.y; /* try { tx.inverseTransform(p, p); } catch (NoninvertibleTransformException ex) { ex.printStackTrace(); }*/ } return (float) Math.abs(p.y); } // EDITING // CONNECTING @Override public void invalidate() { super.invalidate(); cachedTextShape = null; cachedBounds = null; cachedDrawingArea = null; } @Override public Dimension2DDouble getPreferredSize() { Rectangle2D.Double b = getBounds(); return new Dimension2DDouble(b.width, b.height); } @Override public Collection<Handle> createHandles(int detailLevel) { LinkedList<Handle> handles = new LinkedList<Handle>(); switch (detailLevel % 2) { case -1: // Mouse hover handles handles.add(new BoundsOutlineHandle(this, false, true)); break; case 0: handles.add(new BoundsOutlineHandle(this)); handles.add(new MoveHandle(this, RelativeLocator.northWest())); handles.add(new MoveHandle(this, RelativeLocator.northEast())); handles.add(new MoveHandle(this, RelativeLocator.southWest())); handles.add(new MoveHandle(this, RelativeLocator.southEast())); handles.add(new FontSizeHandle(this)); handles.add(new LinkHandle(this)); break; case 1: TransformHandleKit.addTransformHandles(this, handles); break; } return handles; } // EDITING /** * Returns a specialized tool for the given coordinate. * <p> * Returns null, if no specialized tool is available. */ @Override public Tool getTool(Point2D.Double p) { if (isEditable() && contains(p)) { TextEditingTool tool = new TextEditingTool(this); return tool; } return null; } @Override public double getBaseline() { return coordinates[0].y - getBounds().y; } /** * Gets the number of characters used to expand tabs. */ @Override public int getTabSize() { return 8; } @Override public TextHolderFigure getLabelFor() { return this; } @Override public Insets2D.Double getInsets() { return new Insets2D.Double(); } @Override public SVGTextFigure clone() { SVGTextFigure that = (SVGTextFigure) super.clone(); that.coordinates = new Point2D.Double[this.coordinates.length]; for (int i = 0; i < this.coordinates.length; i++) { that.coordinates[i] = (Point2D.Double) this.coordinates[i].clone(); } that.rotates = this.rotates.clone(); that.cachedBounds = null; that.cachedDrawingArea = null; that.cachedTextShape = null; return that; } @Override public boolean isEmpty() { return getText() == null || getText().length() == 0; } @Override public boolean isTextOverflow() { return false; } }