/* * 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. */ /** * @author Oleg V. Khaschansky * @version $Revision$ */ package java.awt.font; import java.awt.Font; import java.awt.Graphics2D; import java.awt.Shape; import java.awt.geom.AffineTransform; import java.awt.geom.Rectangle2D; import java.awt.geom.GeneralPath; import java.text.AttributedCharacterIterator; import java.text.AttributedString; import java.util.Map; import org.apache.harmony.awt.gl.font.BasicMetrics; import org.apache.harmony.awt.gl.font.CaretManager; import org.apache.harmony.awt.gl.font.TextMetricsCalculator; import org.apache.harmony.awt.gl.font.TextRunBreaker; import org.apache.harmony.awt.internal.nls.Messages; /** * The TextLayout class defines the graphical representation of character data. * This class provides method for obtaining information about cursor positioning * and movement, split cursors for text with different directions, logical and * visual highlighting, multiple baselines, hits, justification, ascent, * descent, and advance, and rendering. A TextLayout object can be rendered * using Graphics context. * * @since Android 1.0 */ public final class TextLayout implements Cloneable { /** * The CaretPolicy class provides a policy for obtaining the caret location. * The single getStrongCaret method specifies the policy. */ public static class CaretPolicy { /** * Instantiates a new CaretPolicy. */ public CaretPolicy() { // Nothing to do } /** * Returns whichever of the two specified TextHitInfo objects has the * stronger caret (higher character level) in the specified TextLayout. * * @param hit1 * the first TextHitInfo of the specified TextLayout. * @param hit2 * the second TextHitInfo of the specified TextLayout. * @param layout * the TextLayout. * @return the TextHitInfo with the stronger caret. */ public TextHitInfo getStrongCaret(TextHitInfo hit1, TextHitInfo hit2, TextLayout layout) { // Stronger hit is the one with greater level. // If the level is same, leading edge is stronger. int level1 = layout.getCharacterLevel(hit1.getCharIndex()); int level2 = layout.getCharacterLevel(hit2.getCharIndex()); if (level1 == level2) { return (hit2.isLeadingEdge() && (!hit1.isLeadingEdge())) ? hit2 : hit1; } return level1 > level2 ? hit1 : hit2; } } /** * The Constant DEFAULT_CARET_POLICY indicates the default caret policy. */ public static final TextLayout.CaretPolicy DEFAULT_CARET_POLICY = new CaretPolicy(); /** * The breaker. */ private TextRunBreaker breaker; /** * The metrics valid. */ private boolean metricsValid = false; /** * The tmc. */ private TextMetricsCalculator tmc; /** * The metrics. */ private BasicMetrics metrics; /** * The caret manager. */ private CaretManager caretManager; /** * The justification width. */ float justificationWidth = -1; /** * Instantiates a new TextLayout object from the specified string and Font. * * @param string * the string to be displayed. * @param font * the font of the text. * @param frc * the FontRenderContext object for obtaining information about a * graphics device. */ public TextLayout(String string, Font font, FontRenderContext frc) { if (string == null) { // awt.01='{0}' parameter is null throw new IllegalArgumentException(Messages.getString("awt.01", "string")); //$NON-NLS-1$ //$NON-NLS-2$ } if (font == null) { // awt.01='{0}' parameter is null throw new IllegalArgumentException(Messages.getString("awt.01", "font")); //$NON-NLS-1$ //$NON-NLS-2$ } if (string.length() == 0) { // awt.02='{0}' parameter has zero length throw new IllegalArgumentException(Messages.getString("awt.02", "string")); //$NON-NLS-1$ //$NON-NLS-2$ } AttributedString as = new AttributedString(string); as.addAttribute(TextAttribute.FONT, font); this.breaker = new TextRunBreaker(as.getIterator(), frc); caretManager = new CaretManager(breaker); } /** * Instantiates a new TextLayout from the specified text and a map of * attributes. * * @param string * the string to be displayed. * @param attributes * the attributes to be used for obtaining the text style. * @param frc * the FontRenderContext object for obtaining information about a * graphics device. */ public TextLayout(String string, Map<? extends java.text.AttributedCharacterIterator.Attribute, ?> attributes, FontRenderContext frc) { if (string == null) { // awt.01='{0}' parameter is null throw new IllegalArgumentException(Messages.getString("awt.01", "string")); //$NON-NLS-1$ //$NON-NLS-2$ } if (attributes == null) { // awt.01='{0}' parameter is null throw new IllegalArgumentException(Messages.getString("awt.01", "attributes")); //$NON-NLS-1$ //$NON-NLS-2$ } if (string.length() == 0) { // awt.02='{0}' parameter has zero length throw new IllegalArgumentException(Messages.getString("awt.02", "string")); //$NON-NLS-1$ //$NON-NLS-2$ } AttributedString as = new AttributedString(string); as.addAttributes(attributes, 0, string.length()); this.breaker = new TextRunBreaker(as.getIterator(), frc); caretManager = new CaretManager(breaker); } /** * Instantiates a new TextLayout from the AttributedCharacterIterator. * * @param text * the AttributedCharacterIterator. * @param frc * the FontRenderContext object for obtaining information about a * graphics device. */ public TextLayout(AttributedCharacterIterator text, FontRenderContext frc) { if (text == null) { // awt.03='{0}' iterator parameter is null throw new IllegalArgumentException(Messages.getString("awt.03", "text")); //$NON-NLS-1$ //$NON-NLS-2$ } if (text.getBeginIndex() == text.getEndIndex()) { // awt.04='{0}' iterator parameter has zero length throw new IllegalArgumentException(Messages.getString("awt.04", "text")); //$NON-NLS-1$ //$NON-NLS-2$ } this.breaker = new TextRunBreaker(text, frc); caretManager = new CaretManager(breaker); } /** * Instantiates a new text layout. * * @param breaker * the breaker. */ TextLayout(TextRunBreaker breaker) { this.breaker = breaker; caretManager = new CaretManager(this.breaker); } /** * Returns a hash code of this TextLayout object. * * @return a hash code of this TextLayout object. */ @Override public int hashCode() { return breaker.hashCode(); } /** * Returns a copy of this object. * * @return a copy of this object. */ @Override protected Object clone() { TextLayout res = new TextLayout((TextRunBreaker)breaker.clone()); if (justificationWidth >= 0) { res.handleJustify(justificationWidth); } return res; } /** * Compares this TextLayout object to the specified TextLayout object. * * @param layout * the TextLayout object to be compared. * @return true, if this TextLayout object is equal to the specified * TextLayout object, false otherwise. */ public boolean equals(TextLayout layout) { if (layout == null) { return false; } return this.breaker.equals(layout.breaker); } /** * Compares this TextLayout object to the specified Object. * * @param obj * the Object to be compared. * @return true, if this TextLayout object is equal to the specified Object, * false otherwise. */ @Override public boolean equals(Object obj) { return obj instanceof TextLayout ? equals((TextLayout)obj) : false; } /** * Gets the string representation for this TextLayout. * * @return the string representation for this TextLayout. */ @Override public String toString() { // what for? return super.toString(); } /** * Draws this TextLayout at the specified location with the specified * Graphics2D context. * * @param g2d * the Graphics2D object which renders this TextLayout. * @param x * the X coordinate of the TextLayout origin. * @param y * the Y coordinate of the TextLayout origin. */ public void draw(Graphics2D g2d, float x, float y) { updateMetrics(); breaker.drawSegments(g2d, x, y); } /** * Update metrics. */ private void updateMetrics() { if (!metricsValid) { breaker.createAllSegments(); tmc = new TextMetricsCalculator(breaker); metrics = tmc.createMetrics(); metricsValid = true; } } /** * Gets the advance of this TextLayout object. * * @return the advance of this TextLayout object. */ public float getAdvance() { updateMetrics(); return metrics.getAdvance(); } /** * Gets the ascent of this TextLayout object. * * @return the ascent of this TextLayout object. */ public float getAscent() { updateMetrics(); return metrics.getAscent(); } /** * Gets the baseline of this TextLayout object. * * @return the baseline of this TextLayout object. */ public byte getBaseline() { updateMetrics(); return (byte)metrics.getBaseLineIndex(); } /** * Gets the float array of offsets for the baselines which are used in this * TextLayout. * * @return the float array of offsets for the baselines which are used in * this TextLayout. */ public float[] getBaselineOffsets() { updateMetrics(); return tmc.getBaselineOffsets(); } /** * Gets the black box bounds of the characters in the specified area. The * black box bounds is an Shape which contains all bounding boxes of all the * glyphs of the characters between firstEndpoint and secondEndpoint * parameters values. * * @param firstEndpoint * the first point of the area. * @param secondEndpoint * the second point of the area. * @return the Shape which contains black box bounds. */ public Shape getBlackBoxBounds(int firstEndpoint, int secondEndpoint) { updateMetrics(); if (firstEndpoint < secondEndpoint) { return breaker.getBlackBoxBounds(firstEndpoint, secondEndpoint); } return breaker.getBlackBoxBounds(secondEndpoint, firstEndpoint); } /** * Gets the bounds of this TextLayout. * * @return the bounds of this TextLayout. */ public Rectangle2D getBounds() { updateMetrics(); return breaker.getVisualBounds(); } /** * Gets information about the caret of the specified TextHitInfo. * * @param hitInfo * the TextHitInfo. * @return the information about the caret of the specified TextHitInfo. */ public float[] getCaretInfo(TextHitInfo hitInfo) { updateMetrics(); return caretManager.getCaretInfo(hitInfo); } /** * Gets information about the caret of the specified TextHitInfo of a * character in this TextLayout. * * @param hitInfo * the TextHitInfo of a character in this TextLayout. * @param bounds * the bounds to which the caret info is constructed. * @return the caret of the specified TextHitInfo. */ public float[] getCaretInfo(TextHitInfo hitInfo, Rectangle2D bounds) { updateMetrics(); return caretManager.getCaretInfo(hitInfo); } /** * Gets a Shape which represents the caret of the specified TextHitInfo in * the bounds of this TextLayout. * * @param hitInfo * the TextHitInfo. * @param bounds * the bounds to which the caret info is constructed. * @return the Shape which represents the caret. */ public Shape getCaretShape(TextHitInfo hitInfo, Rectangle2D bounds) { updateMetrics(); return caretManager.getCaretShape(hitInfo, this); } /** * Gets a Shape which represents the caret of the specified TextHitInfo in * the bounds of this TextLayout. * * @param hitInfo * the TextHitInfo. * @return the Shape which represents the caret. */ public Shape getCaretShape(TextHitInfo hitInfo) { updateMetrics(); return caretManager.getCaretShape(hitInfo, this); } /** * Gets two Shapes for the strong and weak carets with default caret policy * and null bounds: the first element is the strong caret, the second is the * weak caret or null. * * @param offset * an offset in the TextLayout. * @return an array of two Shapes corresponded to the strong and weak * carets. */ public Shape[] getCaretShapes(int offset) { return getCaretShapes(offset, null, TextLayout.DEFAULT_CARET_POLICY); } /** * Gets two Shapes for the strong and weak carets with the default caret * policy: the first element is the strong caret, the second is the weak * caret or null. * * @param offset * an offset in the TextLayout. * @param bounds * the bounds to which to extend the carets. * @return an array of two Shapes corresponded to the strong and weak * carets. */ public Shape[] getCaretShapes(int offset, Rectangle2D bounds) { return getCaretShapes(offset, bounds, TextLayout.DEFAULT_CARET_POLICY); } /** * Gets two Shapes for the strong and weak carets: the first element is the * strong caret, the second is the weak caret or null. * * @param offset * an offset in the TextLayout. * @param bounds * the bounds to which to extend the carets. * @param policy * the specified CaretPolicy. * @return an array of two Shapes corresponded to the strong and weak * carets. */ public Shape[] getCaretShapes(int offset, Rectangle2D bounds, TextLayout.CaretPolicy policy) { if (offset < 0 || offset > breaker.getCharCount()) { // awt.195=Offset is out of bounds throw new IllegalArgumentException(Messages.getString("awt.195")); //$NON-NLS-1$ } updateMetrics(); return caretManager.getCaretShapes(offset, bounds, policy, this); } /** * Gets the number of characters in this TextLayout. * * @return the number of characters in this TextLayout. */ public int getCharacterCount() { return breaker.getCharCount(); } /** * Gets the level of the character with the specified index. * * @param index * the specified index of the character. * @return the level of the character. */ public byte getCharacterLevel(int index) { if (index == -1 || index == getCharacterCount()) { return (byte)breaker.getBaseLevel(); } return breaker.getLevel(index); } /** * Gets the descent of this TextLayout. * * @return the descent of this TextLayout. */ public float getDescent() { updateMetrics(); return metrics.getDescent(); } /** * Gets the TextLayout wich is justified with the specified width related to * this TextLayout. * * @param justificationWidth * the width which is used for justification. * @return a TextLayout justified to the specified width. * @throws Error * the error occures if this TextLayout has been already * justified. */ public TextLayout getJustifiedLayout(float justificationWidth) throws Error { float justification = breaker.getJustification(); if (justification < 0) { // awt.196=Justification impossible, layout already justified throw new Error(Messages.getString("awt.196")); //$NON-NLS-1$ } else if (justification == 0) { return this; } TextLayout justifiedLayout = new TextLayout((TextRunBreaker)breaker.clone()); justifiedLayout.handleJustify(justificationWidth); return justifiedLayout; } /** * Gets the leading of this TextLayout. * * @return the leading of this TextLayout. */ public float getLeading() { updateMetrics(); return metrics.getLeading(); } /** * Gets a Shape representing the logical selection betweeen the specified * endpoints and extended to the natural bounds of this TextLayout. * * @param firstEndpoint * the first selected endpoint within the area of characters * @param secondEndpoint * the second selected endpoint within the area of characters * @return a Shape represented the logical selection betweeen the specified * endpoints. */ public Shape getLogicalHighlightShape(int firstEndpoint, int secondEndpoint) { updateMetrics(); return getLogicalHighlightShape(firstEndpoint, secondEndpoint, breaker.getLogicalBounds()); } /** * Gets a Shape representing the logical selection betweeen the specified * endpoints and extended to the specified bounds of this TextLayout. * * @param firstEndpoint * the first selected endpoint within the area of characters * @param secondEndpoint * the second selected endpoint within the area of characters * @param bounds * the specified bounds of this TextLayout. * @return a Shape represented the logical selection betweeen the specified * endpoints. */ public Shape getLogicalHighlightShape(int firstEndpoint, int secondEndpoint, Rectangle2D bounds) { updateMetrics(); if (firstEndpoint > secondEndpoint) { if (secondEndpoint < 0 || firstEndpoint > breaker.getCharCount()) { // awt.197=Endpoints are out of range throw new IllegalArgumentException(Messages.getString("awt.197")); //$NON-NLS-1$ } return caretManager.getLogicalHighlightShape(secondEndpoint, firstEndpoint, bounds, this); } if (firstEndpoint < 0 || secondEndpoint > breaker.getCharCount()) { // awt.197=Endpoints are out of range throw new IllegalArgumentException(Messages.getString("awt.197")); //$NON-NLS-1$ } return caretManager.getLogicalHighlightShape(firstEndpoint, secondEndpoint, bounds, this); } /** * Gets the logical ranges of text which corresponds to a visual selection. * * @param hit1 * the first endpoint of the visual range. * @param hit2 * the second endpoint of the visual range. * @return the logical ranges of text which corresponds to a visual * selection. */ public int[] getLogicalRangesForVisualSelection(TextHitInfo hit1, TextHitInfo hit2) { return caretManager.getLogicalRangesForVisualSelection(hit1, hit2); } /** * Gets the TextHitInfo for the next caret to the left (or up at the end of * the line) of the specified offset. * * @param offset * the offset in this TextLayout. * @return the TextHitInfo for the next caret to the left (or up at the end * of the line) of the specified hit, or null if there is no hit. */ public TextHitInfo getNextLeftHit(int offset) { return getNextLeftHit(offset, DEFAULT_CARET_POLICY); } /** * Gets the TextHitInfo for the next caret to the left (or up at the end of * the line) of the specified hit. * * @param hitInfo * the initial hit. * @return the TextHitInfo for the next caret to the left (or up at the end * of the line) of the specified hit, or null if there is no hit. */ public TextHitInfo getNextLeftHit(TextHitInfo hitInfo) { breaker.createAllSegments(); return caretManager.getNextLeftHit(hitInfo); } /** * Gets the TextHitInfo for the next caret to the left (or up at the end of * the line) of the specified offset, given the specified caret policy. * * @param offset * the offset in this TextLayout. * @param policy * the policy to be used for obtaining the strong caret. * @return the TextHitInfo for the next caret to the left of the specified * offset, or null if there is no hit. */ public TextHitInfo getNextLeftHit(int offset, TextLayout.CaretPolicy policy) { if (offset < 0 || offset > breaker.getCharCount()) { // awt.195=Offset is out of bounds throw new IllegalArgumentException(Messages.getString("awt.195")); //$NON-NLS-1$ } TextHitInfo hit = TextHitInfo.afterOffset(offset); TextHitInfo strongHit = policy.getStrongCaret(hit, hit.getOtherHit(), this); TextHitInfo nextLeftHit = getNextLeftHit(strongHit); if (nextLeftHit != null) { return policy.getStrongCaret(getVisualOtherHit(nextLeftHit), nextLeftHit, this); } return null; } /** * Gets the TextHitInfo for the next caret to the right (or down at the end * of the line) of the specified hit. * * @param hitInfo * the initial hit. * @return the TextHitInfo for the next caret to the right (or down at the * end of the line) of the specified hit, or null if there is no * hit. */ public TextHitInfo getNextRightHit(TextHitInfo hitInfo) { breaker.createAllSegments(); return caretManager.getNextRightHit(hitInfo); } /** * Gets the TextHitInfo for the next caret to the right (or down at the end * of the line) of the specified offset. * * @param offset * the offset in this TextLayout. * @return the TextHitInfo for the next caret to the right of the specified * offset, or null if there is no hit. */ public TextHitInfo getNextRightHit(int offset) { return getNextRightHit(offset, DEFAULT_CARET_POLICY); } /** * Gets the TextHitInfo for the next caret to the right (or down at the end * of the line) of the specified offset, given the specified caret policy. * * @param offset * the offset in this TextLayout. * @param policy * the policy to be used for obtaining the strong caret. * @return the TextHitInfo for the next caret to the right of the specified * offset, or null if there is no hit. */ public TextHitInfo getNextRightHit(int offset, TextLayout.CaretPolicy policy) { if (offset < 0 || offset > breaker.getCharCount()) { // awt.195=Offset is out of bounds throw new IllegalArgumentException(Messages.getString("awt.195")); //$NON-NLS-1$ } TextHitInfo hit = TextHitInfo.afterOffset(offset); TextHitInfo strongHit = policy.getStrongCaret(hit, hit.getOtherHit(), this); TextHitInfo nextRightHit = getNextRightHit(strongHit); if (nextRightHit != null) { return policy.getStrongCaret(getVisualOtherHit(nextRightHit), nextRightHit, this); } return null; } /** * Gets the outline of this TextLayout as a Shape. * * @param xform * the AffineTransform to be used to transform the outline before * returning it, or null if no transformation is desired. * @return the outline of this TextLayout as a Shape. */ public Shape getOutline(AffineTransform xform) { breaker.createAllSegments(); GeneralPath outline = breaker.getOutline(); if (outline != null && xform != null) { outline.transform(xform); } return outline; } /** * Gets the visible advance of this TextLayout which is defined as diffence * between leading (advance) and trailing whitespace. * * @return the visible advance of this TextLayout. */ public float getVisibleAdvance() { updateMetrics(); // Trailing whitespace _SHOULD_ be reordered (Unicode spec) to // base direction, so it is also trailing // in logical representation. We use this fact. int lastNonWhitespace = breaker.getLastNonWhitespace(); if (lastNonWhitespace < 0) { return 0; } else if (lastNonWhitespace == getCharacterCount() - 1) { return getAdvance(); } else if (justificationWidth >= 0) { // Layout is justified return justificationWidth; } else { breaker.pushSegments(breaker.getACI().getBeginIndex(), lastNonWhitespace + breaker.getACI().getBeginIndex() + 1); breaker.createAllSegments(); float visAdvance = tmc.createMetrics().getAdvance(); breaker.popSegments(); return visAdvance; } } /** * Gets a Shape which corresponds to the highlighted (selected) area based * on two hit locations within the text and extends to the bounds. * * @param hit1 * the first text hit location. * @param hit2 * the second text hit location. * @param bounds * the rectangle that the highlighted area should be extended or * restricted to. * @return a Shape which corresponds to the highlighted (selected) area. */ public Shape getVisualHighlightShape(TextHitInfo hit1, TextHitInfo hit2, Rectangle2D bounds) { return caretManager.getVisualHighlightShape(hit1, hit2, bounds, this); } /** * Gets a Shape which corresponds to the highlighted (selected) area based * on two hit locations within the text. * * @param hit1 * the first text hit location. * @param hit2 * the second text hit location. * @return a Shape which corresponds to the highlighted (selected) area. */ public Shape getVisualHighlightShape(TextHitInfo hit1, TextHitInfo hit2) { breaker.createAllSegments(); return caretManager.getVisualHighlightShape(hit1, hit2, breaker.getLogicalBounds(), this); } /** * Gets the TextHitInfo for a hit on the opposite side of the specified * hit's caret. * * @param hitInfo * the specified TextHitInfo. * @return the TextHitInfo for a hit on the opposite side of the specified * hit's caret. */ public TextHitInfo getVisualOtherHit(TextHitInfo hitInfo) { return caretManager.getVisualOtherHit(hitInfo); } /** * Justifies the text; this method should be overridden by subclasses. * * @param justificationWidth * the width for justification. */ protected void handleJustify(float justificationWidth) { float justification = breaker.getJustification(); if (justification < 0) { // awt.196=Justification impossible, layout already justified throw new IllegalStateException(Messages.getString("awt.196")); //$NON-NLS-1$ } else if (justification == 0) { return; } float gap = (justificationWidth - getVisibleAdvance()) * justification; breaker.justify(gap); this.justificationWidth = justificationWidth; // Correct metrics tmc = new TextMetricsCalculator(breaker); tmc.correctAdvance(metrics); } /** * Returns a TextHitInfo object that gives information on which division * point (between two characters) is corresponds to a hit (such as a mouse * click) at the specified coordinates. * * @param x * the X coordinate in this TextLayout. * @param y * the Y coordinate in this TextLayout. TextHitInfo object * corresponding to the given coordinates within the text. * @return the information about the character at the specified position. */ public TextHitInfo hitTestChar(float x, float y) { return hitTestChar(x, y, getBounds()); } /** * Returns a TextHitInfo object that gives information on which division * point (between two characters) is corresponds to a hit (such as a mouse * click) at the specified coordinates within the specified text rectangle. * * @param x * the X coordinate in this TextLayout. * @param y * the Y coordinate in this TextLayout. * @param bounds * the bounds of the text area. TextHitInfo object corresponding * to the given coordinates within the text. * @return the information about the character at the specified position. */ public TextHitInfo hitTestChar(float x, float y, Rectangle2D bounds) { if (x > bounds.getMaxX()) { return breaker.isLTR() ? TextHitInfo.trailing(breaker.getCharCount() - 1) : TextHitInfo .leading(0); } if (x < bounds.getMinX()) { return breaker.isLTR() ? TextHitInfo.leading(0) : TextHitInfo.trailing(breaker .getCharCount() - 1); } return breaker.hitTest(x, y); } /** * Returns true if this TextLayout has a "left to right" direction. * * @return true if this TextLayout has a "left to right" direction, false if * this TextLayout has a "right to left" direction. */ public boolean isLeftToRight() { return breaker.isLTR(); } /** * Returns true if this TextLayout is vertical, false otherwise. * * @return true if this TextLayout is vertical, false if horizontal. */ public boolean isVertical() { return false; } }