| /* |
| |
| Licensed to the Apache Software Foundation (ASF) under one or more |
| contributor license agreements. See the NOTICE file distributed with |
| this work for additional information regarding copyright ownership. |
| The ASF licenses this file to You under the Apache License, Version 2.0 |
| (the "License"); you may not use this file except in compliance with |
| the License. You may obtain a copy of the License at |
| |
| http://www.apache.org/licenses/LICENSE-2.0 |
| |
| Unless required by applicable law or agreed to in writing, software |
| distributed under the License is distributed on an "AS IS" BASIS, |
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| See the License for the specific language governing permissions and |
| limitations under the License. |
| |
| */ |
| package org.apache.batik.bridge; |
| |
| import java.awt.Graphics2D; |
| import java.awt.Shape; |
| import java.awt.geom.AffineTransform; |
| import java.awt.geom.GeneralPath; |
| import java.awt.geom.Point2D; |
| import java.awt.geom.Rectangle2D; |
| import java.text.AttributedCharacterIterator; |
| import java.text.CharacterIterator; |
| import java.util.List; |
| |
| import org.apache.batik.gvt.AbstractGraphicsNode; |
| import org.apache.batik.gvt.Selectable; |
| import org.apache.batik.gvt.text.AttributedCharacterSpanIterator; |
| import org.apache.batik.gvt.text.GVTAttributedCharacterIterator; |
| import org.apache.batik.gvt.text.TextPaintInfo; |
| |
| /** |
| * A graphics node that represents text. |
| * |
| * @author <a href="mailto:Thierry.Kormann@sophia.inria.fr">Thierry Kormann</a> |
| * @version $Id$ |
| */ |
| public class TextNode extends AbstractGraphicsNode implements Selectable { |
| |
| public static final |
| AttributedCharacterIterator.Attribute PAINT_INFO = |
| GVTAttributedCharacterIterator.TextAttribute.PAINT_INFO; |
| |
| /** |
| * Location of this text node (inherited, independent of explicit |
| * X and Y attributes applied to children). |
| */ |
| protected Point2D location = new Point2D.Float(0, 0); |
| |
| /** |
| * Attributed Character Iterator describing the text |
| */ |
| protected AttributedCharacterIterator aci; |
| |
| /** |
| * The text of this <code>TextNode</code>. |
| */ |
| protected String text; |
| |
| /** |
| * The begin mark. |
| */ |
| protected Mark beginMark = null; |
| |
| /** |
| * The end mark. |
| */ |
| protected Mark endMark = null; |
| |
| /** |
| * The list of text runs. |
| */ |
| protected List textRuns; |
| |
| /** |
| * The text painter used to display the text of this text node. |
| */ |
| protected TextPainter textPainter = StrokingTextPainter.getInstance(); |
| |
| /** |
| * Internal Cache: Bounds for this text node, without taking any of the |
| * rendering attributes (e.g., stroke) into account |
| */ |
| private Rectangle2D geometryBounds; |
| |
| /** |
| * Internal Cache: Primitive Bounds. |
| */ |
| private Rectangle2D primitiveBounds; |
| |
| /** |
| * Internal Cache: the outline. |
| */ |
| private Shape outline; |
| |
| /** |
| * Constructs a new empty <code>TextNode</code>. |
| */ |
| public TextNode() { |
| } |
| |
| /** |
| * Sets the text painter of this text node. If the specified text |
| * painter is null, this text node will use its default text |
| * painter (StrokingTextPainter.getInstance()). |
| * |
| * @param textPainter the text painter to use |
| */ |
| public void setTextPainter(TextPainter textPainter) { |
| if (textPainter == null) { |
| this.textPainter = StrokingTextPainter.getInstance(); |
| } else { |
| this.textPainter = textPainter; |
| } |
| } |
| |
| /** |
| * Returns the text painter of this text node. |
| */ |
| public TextPainter getTextPainter() { |
| return textPainter; |
| } |
| |
| /** |
| * Returns a list of text runs. |
| */ |
| public List getTextRuns() { |
| return textRuns; |
| } |
| |
| /** |
| * Sets the list of text runs of this text node. |
| * |
| * @param textRuns the new list of text runs |
| */ |
| public void setTextRuns(List textRuns) { |
| this.textRuns = textRuns; |
| } |
| |
| /** |
| * Returns the text of this <code>TextNode</code> as a string. |
| */ |
| public String getText() { |
| |
| if (text != null) |
| return text; |
| |
| if (aci == null) { |
| text = ""; |
| } else { |
| StringBuffer buf = new StringBuffer(aci.getEndIndex()); |
| for (char c = aci.first(); |
| c != CharacterIterator.DONE; |
| c = aci.next()) { |
| buf.append(c); |
| } |
| text = buf.toString(); |
| } |
| return text; |
| } |
| |
| /** |
| * Sets the location of this text node. |
| * |
| * @param newLocation the new location of this text node |
| */ |
| public void setLocation(Point2D newLocation){ |
| fireGraphicsNodeChangeStarted(); |
| invalidateGeometryCache(); |
| this.location = newLocation; |
| fireGraphicsNodeChangeCompleted(); |
| } |
| |
| /** |
| * Returns the location of this text node. |
| * |
| * @return the location of this text node |
| */ |
| public Point2D getLocation(){ |
| return location; |
| } |
| |
| public void swapTextPaintInfo(TextPaintInfo newInfo, |
| TextPaintInfo oldInfo) { |
| fireGraphicsNodeChangeStarted(); |
| invalidateGeometryCache(); |
| oldInfo.set(newInfo); |
| fireGraphicsNodeChangeCompleted(); |
| } |
| |
| |
| /** |
| * Sets the attributed character iterator of this text node. |
| * |
| * @param newAci the new attributed character iterator |
| */ |
| public void setAttributedCharacterIterator |
| (AttributedCharacterIterator newAci) { |
| fireGraphicsNodeChangeStarted(); |
| invalidateGeometryCache(); |
| this.aci = newAci; |
| text = null; |
| textRuns = null; |
| fireGraphicsNodeChangeCompleted(); |
| } |
| |
| /** |
| * Returns the attributed character iterator of this text node. |
| * |
| * @return the attributed character iterator |
| */ |
| public AttributedCharacterIterator getAttributedCharacterIterator(){ |
| return aci; |
| } |
| |
| // |
| // Geometric methods |
| // |
| |
| /** |
| * Invalidates this <code>TextNode</code>. This node and all its ancestors have |
| * been informed that all its cached values related to its bounds must be |
| * recomputed. |
| */ |
| protected void invalidateGeometryCache() { |
| super.invalidateGeometryCache(); |
| primitiveBounds = null; |
| geometryBounds = null; |
| outline = null; |
| } |
| |
| /** |
| * Returns the bounds of the area covered by this node's primitive paint. |
| */ |
| public Rectangle2D getPrimitiveBounds(){ |
| if (primitiveBounds == null) { |
| if (aci != null) { |
| primitiveBounds = textPainter.getBounds2D(this); |
| } |
| } |
| return primitiveBounds; |
| } |
| |
| /** |
| * Returns the bounds of the area covered by this node, without |
| * taking any of its rendering attribute into account. That is, |
| * exclusive of any clipping, masking, filtering or stroking, for |
| * example. |
| */ |
| public Rectangle2D getGeometryBounds(){ |
| if (geometryBounds == null){ |
| if (aci != null) { |
| geometryBounds = textPainter.getGeometryBounds(this); |
| } |
| } |
| return geometryBounds; |
| } |
| |
| /** |
| * Returns the bounds of the sensitive area covered by this node, |
| * This includes the stroked area but does not include the effects |
| * of clipping, masking or filtering. |
| */ |
| public Rectangle2D getSensitiveBounds() { |
| return getGeometryBounds(); |
| } |
| |
| /** |
| * Returns the outline of this node. |
| */ |
| public Shape getOutline() { |
| if (outline == null) { |
| if (aci != null) { |
| outline = textPainter.getOutline(this); |
| } |
| } |
| return outline; |
| } |
| |
| /** |
| * Return the marker for the character at index in this nodes |
| * AttributedCharacterIterator. Before Char indicates if the |
| * Marker should be considered before or after char. |
| */ |
| public Mark getMarkerForChar(int index, boolean beforeChar) { |
| return textPainter.getMark(this, index, beforeChar); |
| } |
| |
| // |
| // Selection methods |
| // |
| public void setSelection(Mark begin, Mark end) { |
| if ((begin.getTextNode() != this) || |
| (end.getTextNode() != this)) |
| throw new Error("Markers not from this TextNode"); |
| |
| beginMark = begin; |
| endMark = end; |
| } |
| |
| /** |
| * Initializes the current selection to begin with the character at (x, y). |
| * @param x the x coordinate of the start of the selection |
| * @param y the y coordinate of the start of the selection |
| */ |
| public boolean selectAt(double x, double y) { |
| beginMark = textPainter.selectAt(x, y, this); |
| return true; // assume this always changes selection, for now. |
| } |
| |
| /** |
| * Extends the current selection to the character at (x, y). |
| * @param x the x coordinate of the end of the selection |
| * @param y the y coordinate of the end of the selection |
| */ |
| public boolean selectTo(double x, double y) { |
| Mark tmpMark = textPainter.selectTo(x, y, beginMark); |
| if (tmpMark == null) |
| return false; |
| if (tmpMark != endMark) { |
| endMark = tmpMark; |
| return true; |
| } |
| return false; |
| } |
| |
| /** |
| * Selects all the text in this TextNode. The coordinates are ignored. |
| * @param x the x coordinate of the point the selection was made |
| * @param y the y coordinate of the point the selection was made |
| */ |
| public boolean selectAll(double x, double y) { |
| beginMark = textPainter.selectFirst(this); |
| endMark = textPainter.selectLast(this); |
| return true; // assume this always changes selection, for now. |
| } |
| |
| /** |
| * Gets the current text selection. |
| * |
| * @return an object containing the selected content. |
| */ |
| public Object getSelection() { |
| Object o = null; |
| if (aci == null) return o; |
| |
| int[] ranges = textPainter.getSelected(beginMark, endMark); |
| |
| // TODO: later we can return more complex things like |
| // noncontiguous selections |
| if ((ranges != null) && (ranges.length > 1)) { |
| // make sure that they are in order |
| if (ranges[0] > ranges[1]) { |
| int temp = ranges[1]; |
| ranges[1] = ranges[0]; |
| ranges[0] = temp; |
| } |
| o = new AttributedCharacterSpanIterator |
| (aci, ranges[0], ranges[1]+1); |
| } |
| return o; |
| } |
| |
| /** |
| * Returns the shape used to outline this text node. |
| * |
| * @return a Shape which encloses the current text selection. |
| */ |
| public Shape getHighlightShape() { |
| Shape highlightShape = |
| textPainter.getHighlightShape(beginMark, endMark); |
| AffineTransform t = getGlobalTransform(); |
| highlightShape = t.createTransformedShape(highlightShape); |
| return highlightShape; |
| } |
| |
| // |
| // Drawing methods |
| // |
| |
| /** |
| * Paints this node without applying Filter, Mask, Composite, and clip. |
| * |
| * @param g2d the Graphics2D to use |
| */ |
| public void primitivePaint(Graphics2D g2d) { |
| // |
| // DO NOT REMOVE: THE FOLLOWING IS A WORK AROUND |
| // A BUG IN THE JDK 1.2 RENDERING PIPELINE WHEN |
| // THE CLIP IS A RECTANGLE |
| // |
| Shape clip = g2d.getClip(); |
| if (clip != null && !(clip instanceof GeneralPath)) { |
| g2d.setClip(new GeneralPath(clip)); |
| } |
| // Paint the text |
| textPainter.paint(this, g2d); |
| } |
| |
| // |
| // Geometric methods |
| // |
| |
| /** |
| * Returns true if the specified Point2D is inside the boundary of this |
| * node, false otherwise. |
| * |
| * @param p the specified Point2D in the user space |
| */ |
| public boolean contains(Point2D p) { |
| // <!> FIXME: should put this code in TextPaint somewhere, |
| // as pointer-events support - same problem with pointer-events |
| // and ShapeNode |
| if (!super.contains(p)) { |
| return false; |
| } |
| List list = getTextRuns(); |
| // place coords in text node coordinate system |
| for (Object aList : list) { |
| StrokingTextPainter.TextRun run = |
| (StrokingTextPainter.TextRun) aList; |
| TextSpanLayout layout = run.getLayout(); |
| float x = (float) p.getX(); |
| float y = (float) p.getY(); |
| TextHit textHit = layout.hitTestChar(x, y); |
| if (textHit != null && contains(p, layout.getBounds2D())) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| protected boolean contains(Point2D p, Rectangle2D b) { |
| if (b == null || !b.contains(p)) { |
| return false; |
| } |
| switch(pointerEventType) { |
| case VISIBLE_PAINTED: |
| case VISIBLE_FILL: |
| case VISIBLE_STROKE: |
| case VISIBLE: |
| return isVisible; |
| case PAINTED: |
| case FILL: |
| case STROKE: |
| case ALL: |
| return true; |
| case NONE: |
| return false; |
| default: |
| return false; |
| } |
| } |
| |
| /** |
| * Defines where the text of a <code>TextNode</code> can be anchored |
| * relative to its location. |
| */ |
| public static final class Anchor implements java.io.Serializable { |
| |
| /** |
| * The type of the START anchor. |
| */ |
| public static final int ANCHOR_START = 0; |
| |
| /** |
| * The type of the MIDDLE anchor. |
| */ |
| public static final int ANCHOR_MIDDLE = 1; |
| |
| /** |
| * The type of the END anchor. |
| */ |
| public static final int ANCHOR_END = 2; |
| |
| /** |
| * The anchor which enables the rendered characters to be aligned such |
| * that the start of the text string is at the initial current text |
| * location. |
| */ |
| public static final Anchor START = new Anchor(ANCHOR_START); |
| |
| /** |
| * The anchor which enables the rendered characters to be aligned such |
| * that the middle of the text string is at the initial current text |
| * location. |
| */ |
| public static final Anchor MIDDLE = new Anchor(ANCHOR_MIDDLE); |
| |
| /** |
| * The anchor which enables the rendered characters to be aligned such |
| * that the end of the text string is at the initial current text |
| * location. |
| */ |
| public static final Anchor END = new Anchor(ANCHOR_END); |
| |
| /** |
| * The anchor type. |
| */ |
| private int type; |
| |
| /** |
| * No instance of this class. |
| */ |
| private Anchor(int type) { |
| this.type = type; |
| } |
| |
| /** |
| * Returns the type of this anchor. |
| */ |
| public int getType() { |
| return type; |
| } |
| |
| /** |
| * This is called by the serialization code before it returns |
| * an unserialized object. To provide for unicity of |
| * instances, the instance that was read is replaced by its |
| * static equivalent. See the serialiazation specification for |
| * further details on this method's logic. |
| */ |
| private Object readResolve() throws java.io.ObjectStreamException { |
| switch(type){ |
| case ANCHOR_START: |
| return START; |
| case ANCHOR_MIDDLE: |
| return MIDDLE; |
| case ANCHOR_END: |
| return END; |
| default: |
| throw new Error("Unknown Anchor type"); |
| } |
| } |
| } |
| } |
| |
| |