/*******************************************************************************
* Copyright (c) 2006-2012
* Software Technology Group, Dresden University of Technology
* DevBoost GmbH, Berlin, Amtsgericht Charlottenburg, HRB 140026
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Software Technology Group - TU Dresden, Germany;
* DevBoost GmbH - Berlin, Germany
* - initial API and implementation
******************************************************************************/
/*
* @(#)SVGTextArea.java 2.1.1 2009-03-29
*
* Copyright (c) 1996-2009 by the original authors of JHotDraw
* and all its contributors.
* All rights reserved.
*
* The copyright of this software is owned by the authors and
* contributors of the JHotDraw project ("the copyright holders").
* You may not use, copy or modify this software, except in
* accordance with the license agreement you entered into with
* the copyright holders. For details see accompanying license terms.
*/
package org.jhotdraw.samples.svg.figures;
import java.awt.*;
import java.awt.font.*;
import java.awt.geom.*;
import java.text.*;
import java.util.*;
import static org.jhotdraw.samples.svg.SVGAttributeKeys.*;
import org.jhotdraw.draw.*;
import org.jhotdraw.samples.svg.*;
import org.jhotdraw.geom.*;
/**
* SVGTextArea.
*
* @author Werner Randelshofer
* @version 2.1.1 2009-03-29 Two consecutive tab-characters in text caused
* ArrayIndexOutOfBoundsException.
* <br>2.1 2008-05-31 Added method getPreferredTextSize.
* <br>2.0.1 Rectangle returned by getDrawingArea needs to be cloned.
* <br>2.0 2007-04-14 Adapted for new AttributeKeys.TRANSFORM support.
* <br>1.0 December 9, 2006 Created.
*/
public class SVGTextAreaFigure extends SVGAttributedFigure
implements SVGFigure, TextHolderFigure {
private Rectangle2D.Double bounds = new Rectangle2D.Double();
private boolean editable = true;
private final static BasicStroke dashes = new BasicStroke(1f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL, 0f, new float[]{4f, 4f}, 0f);
/**
* This is a cached value to improve the performance of method isTextOverflow();
*/
private Boolean isTextOverflow;
/**
* This is used to perform faster drawing and hit testing.
*/
private transient Rectangle2D.Double cachedDrawingArea;
private transient Shape cachedTextShape;
/** Creates a new instance. */
public SVGTextAreaFigure() {
this("Text");
}
public SVGTextAreaFigure(String text) {
setText(text);
SVGAttributeKeys.setDefaults(this);
}
// DRAWING
@Override
protected void drawText(java.awt.Graphics2D g) {
}
protected void drawFill(Graphics2D g) {
g.fill(getTextShape());
g.draw(new Rectangle2D.Double(getBounds().x, getBounds().y, getPreferredTextSize(changingDepth).width, getPreferredTextSize(changingDepth).height));
}
protected void drawStroke(Graphics2D g) {
g.draw(getTextShape());
}
// SHAPE AND BOUNDS
public Rectangle2D.Double getBounds() {
return (Rectangle2D.Double) bounds.clone();
}
@Override
public Rectangle2D.Double getDrawingArea() {
if (cachedDrawingArea == null) {
Rectangle2D rx = getBounds();
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);
Geom.grow(r, g, g);
if (TRANSFORM.get(this) == null) {
cachedDrawingArea = r;
} else {
cachedDrawingArea = new Rectangle2D.Double();
cachedDrawingArea.setRect(TRANSFORM.get(this).createTransformedShape(r).getBounds2D());
}
}
return (Rectangle2D.Double) cachedDrawingArea.clone();
}
/**
* Checks if a Point2D.Double is inside the figure.
*/
public boolean contains(Point2D.Double p) {
if (TRANSFORM.get(this) != null) {
try {
p = (Point2D.Double) TRANSFORM.get(this).inverseTransform(p, new Point2D.Double());
} catch (NoninvertibleTransformException ex) {
ex.printStackTrace();
}
}
Rectangle2D r = getTextShape().getBounds2D();
return r.isEmpty() ? getBounds().contains(p) : r.contains(p);
}
private Shape getTextShape() {
if (cachedTextShape == null) {
GeneralPath shape;
cachedTextShape = shape = new GeneralPath();
if (getText() != null || isEditable()) {
Font font = getFont();
boolean isUnderlined = FONT_UNDERLINE.get(this);
Insets2D.Double insets = getInsets();
Rectangle2D.Double textRect = new Rectangle2D.Double(
bounds.x + insets.left,
bounds.y + insets.top,
bounds.width - insets.left - insets.right,
bounds.height - insets.top - insets.bottom);
float leftMargin = (float) textRect.x;
float rightMargin = (float) Math.max(leftMargin + 1, textRect.x + textRect.width);
float verticalPos = (float) textRect.y;
float maxVerticalPos = (float) (textRect.y + textRect.height);
if (leftMargin < rightMargin) {
float tabWidth = (float) (getTabSize() * font.getStringBounds("m", getFontRenderContext()).getWidth());
float[] tabStops = new float[(int) (textRect.width / tabWidth)];
for (int i = 0; i < tabStops.length; i++) {
tabStops[i] = (float) (textRect.x + (int) (tabWidth * (i + 1)));
}
if (getText() != null) {
String[] paragraphs = getText().split("\n");//Strings.split(getText(), '\n');
for (int i = 0; i < paragraphs.length; i++) {
if (paragraphs[i].length() == 0) {
paragraphs[i] = " ";
}
AttributedString as = new AttributedString(paragraphs[i]);
as.addAttribute(TextAttribute.FONT, font);
if (isUnderlined) {
as.addAttribute(TextAttribute.UNDERLINE, TextAttribute.UNDERLINE_LOW_ONE_PIXEL);
}
int tabCount = paragraphs[i].split("\t").length - 1;
Rectangle2D.Double paragraphBounds = appendParagraph(
shape, as.getIterator(),
verticalPos, maxVerticalPos, leftMargin, rightMargin, tabStops, tabCount);
verticalPos = (float) (paragraphBounds.y + paragraphBounds.height);
if (verticalPos > textRect.y + textRect.height) {
break;
}
}
}
}
}
}
return cachedTextShape;
}
/**
* Appends a paragraph of text at the specified y location and returns
* the bounds of the paragraph.
*
*
* @param shape Shape to which to add the glyphs of the paragraph. This
* parameter is null, if we only want to measure the size of the paragraph.
* @param styledText the text of the paragraph.
* @param verticalPos the top bound of the paragraph
* @param maxVerticalPos the bottom bound of the paragraph
* @param leftMargin the left bound of the paragraph
* @param rightMargin the right bound of the paragraph
* @param tabStops an array with tab stops
* @param tabCounts the number of entries in tabStops which contain actual
* values
* @return Returns the actual bounds of the paragraph.
*/
private Rectangle2D.Double appendParagraph(GeneralPath shape,
AttributedCharacterIterator styledText,
float verticalPos, float maxVerticalPos,
float leftMargin, float rightMargin,
float[] tabStops, int tabCount) {
// assume styledText is an AttributedCharacterIterator, and the number
// of tabs in styledText is tabCount
Rectangle2D.Double paragraphBounds = new Rectangle2D.Double(leftMargin, verticalPos, 0, 0);
int[] tabLocations = new int[tabCount + 1];
int i = 0;
for (char c = styledText.first(); c != styledText.DONE; c = styledText.next()) {
if (c == '\t') {
tabLocations[i++] = styledText.getIndex();
}
}
tabLocations[tabCount] = styledText.getEndIndex() - 1;
// Now tabLocations has an entry for every tab's offset in
// the text. For convenience, the last entry is tabLocations
// is the offset of the last character in the text.
LineBreakMeasurer measurer = new LineBreakMeasurer(styledText, getFontRenderContext());
int currentTab = 0;
while (measurer.getPosition() < styledText.getEndIndex()) {
// Lay out and draw each line. All segments on a line
// must be computed before any drawing can occur, since
// we must know the largest ascent on the line.
// TextLayouts are computed and stored in a List;
// their horizontal positions are stored in a parallel
// List.
// lineContainsText is true after first segment is drawn
boolean lineContainsText = false;
boolean lineComplete = false;
float maxAscent = 0, maxDescent = 0;
float horizontalPos = leftMargin;
LinkedList<TextLayout> layouts = new LinkedList<TextLayout>();
LinkedList<Float> penPositions = new LinkedList<Float>();
while (!lineComplete) {
float wrappingWidth = rightMargin - horizontalPos;
TextLayout layout = null;
layout =
measurer.nextLayout(wrappingWidth,
tabLocations[currentTab] + 1,
lineContainsText);
// layout can be null if lineContainsText is true
if (layout != null) {
layouts.add(layout);
penPositions.add(horizontalPos);
horizontalPos += layout.getAdvance();
maxAscent = Math.max(maxAscent, layout.getAscent());
maxDescent = Math.max(maxDescent,
layout.getDescent() + layout.getLeading());
} else {
lineComplete = true;
}
lineContainsText = true;
if (measurer.getPosition() == tabLocations[currentTab] + 1) {
currentTab++;
}
if (measurer.getPosition() == styledText.getEndIndex()) {
lineComplete = true;
} else if (tabStops.length == 0 || horizontalPos >= tabStops[tabStops.length - 1]) {
lineComplete = true;
}
if (!lineComplete) {
// move to next tab stop
int j;
for (j = 0; horizontalPos >= tabStops[j]; j++) {
}
horizontalPos = tabStops[j];
}
}
verticalPos += maxAscent;
if (verticalPos > maxVerticalPos) {
break;
}
Iterator<TextLayout> layoutEnum = layouts.iterator();
Iterator<Float> positionEnum = penPositions.iterator();
// now iterate through layouts and draw them
while (layoutEnum.hasNext()) {
TextLayout nextLayout = layoutEnum.next();
float nextPosition = positionEnum.next();
AffineTransform tx = new AffineTransform();
tx.translate(nextPosition, verticalPos);
if (shape != null) {
Shape outline = nextLayout.getOutline(tx);
shape.append(outline, false);
}
Rectangle2D layoutBounds = nextLayout.getBounds();
paragraphBounds.add(new Rectangle2D.Double(layoutBounds.getX() + nextPosition,
layoutBounds.getY() + verticalPos,
layoutBounds.getWidth(),
layoutBounds.getHeight()));
}
verticalPos += maxDescent;
}
return paragraphBounds;
}
public void setBounds(Point2D.Double anchor, Point2D.Double lead) {
bounds.x = Math.min(anchor.x, lead.x);
bounds.y = Math.min(anchor.y, lead.y);
bounds.width = Math.max(0.1, Math.abs(lead.x - anchor.x));
bounds.height = Math.max(0.1, Math.abs(lead.y - anchor.y));
invalidate();
}
/**
* Transforms the figure.
*
* @param tx the transformation.
*/
public void transform(AffineTransform tx) {
if (TRANSFORM.get(this) != null ||
(tx.getType() &
(AffineTransform.TYPE_TRANSLATION /*| AffineTransform.TYPE_MASK_SCALE*/)) !=
tx.getType()) {
if (TRANSFORM.get(this) == null) {
TRANSFORM.basicSet(this, (AffineTransform) tx.clone());
} else {
AffineTransform t = TRANSFORM.getClone(this);
t.preConcatenate(tx);
TRANSFORM.basicSet(this, t);
}
} else {
Point2D.Double anchor = getStartPoint();
Point2D.Double lead = getEndPoint();
setBounds(
(Point2D.Double) tx.transform(anchor, anchor),
(Point2D.Double) tx.transform(lead, lead));
if (FILL_GRADIENT.get(this) != null &&
!FILL_GRADIENT.get(this).isRelativeToFigureBounds()) {
Gradient g = FILL_GRADIENT.getClone(this);
g.transform(tx);
FILL_GRADIENT.basicSet(this, g);
}
if (STROKE_GRADIENT.get(this) != null &&
!STROKE_GRADIENT.get(this).isRelativeToFigureBounds()) {
Gradient g = STROKE_GRADIENT.getClone(this);
g.transform(tx);
STROKE_GRADIENT.basicSet(this, g);
}
}
invalidate();
}
public void restoreTransformTo(Object geometry) {
Object[] restoreData = (Object[]) geometry;
bounds = (Rectangle2D.Double) ((Rectangle2D.Double) restoreData[0]).clone();
TRANSFORM.basicSetClone(this, (AffineTransform) restoreData[1]);
FILL_GRADIENT.basicSetClone(this, (Gradient) restoreData[2]);
STROKE_GRADIENT.basicSetClone(this, (Gradient) restoreData[3]);
invalidate();
}
public Object getTransformRestoreData() {
return new Object[]{
bounds.clone(),
TRANSFORM.getClone(this),
FILL_GRADIENT.getClone(this),
STROKE_GRADIENT.getClone(this),};
}
// ATTRIBUTES
public String getText() {
return (String) getAttribute(TEXT);
}
public int getTextColumns() {
return (getText() == null) ? 4 : Math.max(getText().length(), 4);
}
public <T> void setAttribute(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) ||
key.equals(SVGAttributeKeys.STROKE_WIDTH) ||
key.equals(SVGAttributeKeys.STROKE_COLOR) ||
key.equals(SVGAttributeKeys.STROKE_GRADIENT)) {
invalidate();
}
super.setAttribute(key, newValue);
}
/**
* Sets the text shown by the text figure.
*/
public void setText(String newText) {
TEXT.set(this, newText);
}
/**
* Returns the insets used to draw text.
*/
public Insets2D.Double getInsets() {
double sw = (STROKE_COLOR.get(this) == null) ? 0 : Math.ceil(STROKE_WIDTH.get(this) / 2);
Insets2D.Double insets = new Insets2D.Double(0, 0, 0, 0);
return new Insets2D.Double(insets.top + sw, insets.left + sw, insets.bottom + sw, insets.right + sw);
}
public double getBaseline() {
return getFont().getLineMetrics(getText(), getFontRenderContext()).getAscent() + getInsets().top;
}
public int getTabSize() {
return 8;
}
public TextHolderFigure getLabelFor() {
return this;
}
public Font getFont() {
return SVGAttributeKeys.getFont(this);
}
public Color getTextColor() {
return FILL_COLOR.get(this);
// return TEXT_COLOR.get(this);
}
public Color getFillColor() {
return FILL_COLOR.get(this).equals(Color.white) ? Color.black : Color.WHITE;
// return FILL_COLOR.get(this);
}
public void setFontSize(float size) {
Point2D.Double p = new Point2D.Double(0, size);
AffineTransform tx = TRANSFORM.get(this);
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();
}
}
FONT_SIZE.set(this, Math.abs(p.y));
}
public float getFontSize() {
Point2D.Double p = new Point2D.Double(0, FONT_SIZE.get(this));
AffineTransform tx = TRANSFORM.get(this);
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
public boolean isEditable() {
return editable;
}
public void setEditable(boolean b) {
this.editable = b;
}
@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:
ResizeHandleKit.addResizeHandles(this, handles);
handles.add(new FontSizeHandle(this));
handles.add(new TextOverflowHandle(this));
handles.add(new LinkHandle(this));
break;
case 1:
TransformHandleKit.addTransformHandles(this, handles);
break;
default:
break;
}
return handles;
}
/**
* Returns a specialized tool for the given coordinate.
* <p>Returns null, if no specialized tool is available.
*/
public Tool getTool(Point2D.Double p) {
if (isEditable() && contains(p)) {
TextAreaEditingTool tool = new TextAreaEditingTool(this);
return tool;
}
return null;
}
// CONNECTING
public boolean canConnect() {
return false; // SVG does not support connecting
}
public Connector findConnector(Point2D.Double p, ConnectionFigure prototype) {
return null; // SVG does not support connectors
}
public Connector findCompatibleConnector(Connector c, boolean isStartConnector) {
return null; // SVG does not support connectors
}
// COMPOSITE FIGURES
// CLONING
// EVENT HANDLING
/**
* Gets the text shown by the text figure.
*/
public boolean isEmpty() {
return getText() == null || getText().length() == 0;
}
@Override
public void invalidate() {
super.invalidate();
cachedDrawingArea = null;
cachedTextShape = null;
isTextOverflow = null;
}
public boolean isTextOverflow() {
if (isTextOverflow == null) {
Insets2D.Double insets = getInsets();
isTextOverflow = getPreferredTextSize(getBounds().width - insets.left - insets.right).height > getBounds().height - insets.top - insets.bottom;
}
return isTextOverflow;
}
/**
* Returns the preferred text size of the TextAreaFigure.
* <p>
* If you want to use this method to determine the bounds of the TextAreaFigure,
* you need to add the insets of the TextAreaFigure to the size.
*
* @param maxWidth the maximal width to use. Specify Double.MAX_VALUE
* if you want the width to be unlimited.
* @return width and height needed to lay out the text.
*/
public Dimension2DDouble getPreferredTextSize(double maxWidth) {
Rectangle2D.Double textRect = new Rectangle2D.Double();
if (getText() != null) {
Font font = getFont();
boolean isUnderlined = FONT_UNDERLINE.get(this);
float leftMargin = 0;
float rightMargin = (float) maxWidth - 1;
float verticalPos = 0;
float maxVerticalPos = Float.MAX_VALUE;
if (leftMargin < rightMargin) {
float tabWidth = (float) (getTabSize() * font.getStringBounds("m", getFontRenderContext()).getWidth());
float[] tabStops = new float[(int) (textRect.width / tabWidth)];
for (int i = 0; i < tabStops.length; i++) {
tabStops[i] = (float) (textRect.x + (int) (tabWidth * (i + 1)));
}
if (getText() != null) {
String[] paragraphs = getText().split("\n");//Strings.split(getText(), '\n');
for (int i = 0; i < paragraphs.length; i++) {
if (paragraphs[i].length() == 0) {
paragraphs[i] = " ";
}
AttributedString as = new AttributedString(paragraphs[i]);
as.addAttribute(TextAttribute.FONT, font);
if (isUnderlined) {
as.addAttribute(TextAttribute.UNDERLINE, TextAttribute.UNDERLINE_LOW_ONE_PIXEL);
}
int tabCount = paragraphs[i].split("\t").length - 1;
Rectangle2D.Double paragraphBounds = appendParagraph(null, as.getIterator(), verticalPos, maxVerticalPos, leftMargin, rightMargin, tabStops, tabCount);
verticalPos = (float) (paragraphBounds.y + paragraphBounds.height);
textRect.add(paragraphBounds);
}
}
}
}
return new Dimension2DDouble(Math.abs(textRect.x) + textRect.width, Math.abs(textRect.y) + textRect.height);
}
public SVGTextAreaFigure clone() {
SVGTextAreaFigure that = (SVGTextAreaFigure) super.clone();
that.bounds = (Rectangle2D.Double) this.bounds.clone();
return that;
}
}