FileDocCategorySizeDatePackage
CaretManager.javaAPI DocAndroid 1.5 API18890Wed May 06 22:41:54 BST 2009org.apache.harmony.awt.gl.font

CaretManager.java

/*
 *  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$
 *
 * @date: Jun 14, 2005
 */

package org.apache.harmony.awt.gl.font;

import java.awt.font.TextHitInfo;
import java.awt.font.TextLayout;
import java.awt.geom.Rectangle2D;
import java.awt.geom.GeneralPath;
import java.awt.geom.Line2D;
import java.awt.*;

import org.apache.harmony.awt.internal.nls.Messages;

/**
 * This class provides functionality for creating caret and highlight shapes
 * (bidirectional text is also supported, but, unfortunately, not tested yet).
 */
public class CaretManager {
    private TextRunBreaker breaker;

    public CaretManager(TextRunBreaker breaker) {
        this.breaker = breaker;
    }

    /**
     * Checks if TextHitInfo is not out of the text range and throws the
     * IllegalArgumentException if it is.
     * @param info - text hit info
     */
    private void checkHit(TextHitInfo info) {
        int idx = info.getInsertionIndex();

        if (idx < 0 || idx > breaker.getCharCount()) {
            // awt.42=TextHitInfo out of range
            throw new IllegalArgumentException(Messages.getString("awt.42")); //$NON-NLS-1$
        }
    }

    /**
     * Calculates and returns visual position from the text hit info.
     * @param hitInfo - text hit info
     * @return visual index
     */
    private int getVisualFromHitInfo(TextHitInfo hitInfo) {
        final int idx = hitInfo.getCharIndex();

        if (idx >= 0 && idx < breaker.getCharCount()) {
            int visual = breaker.getVisualFromLogical(idx);
            // We take next character for (LTR char + TRAILING info) and (RTL + LEADING)
            if (hitInfo.isLeadingEdge() ^ ((breaker.getLevel(idx) & 0x1) == 0x0)) {
                visual++;
            }
            return visual;
        } else if (idx < 0) {
            return breaker.isLTR() ? 0: breaker.getCharCount();
        } else {
            return breaker.isLTR() ? breaker.getCharCount() : 0;
        }
    }

    /**
     * Calculates text hit info from the visual position
     * @param visual - visual position
     * @return text hit info
     */
    private TextHitInfo getHitInfoFromVisual(int visual) {
        final boolean first = visual == 0;

        if (!(first || visual == breaker.getCharCount())) {
            int logical = breaker.getLogicalFromVisual(visual);
            return (breaker.getLevel(logical) & 0x1) == 0x0 ?
                    TextHitInfo.leading(logical) : // LTR
                    TextHitInfo.trailing(logical); // RTL
        } else if (first) {
            return breaker.isLTR() ?
                    TextHitInfo.trailing(-1) :
                    TextHitInfo.leading(breaker.getCharCount());
        } else { // Last
            return breaker.isLTR() ?
                    TextHitInfo.leading(breaker.getCharCount()) :
                    TextHitInfo.trailing(-1);
        }
    }

    /**
     * Creates caret info. Required for the getCaretInfo
     * methods of the TextLayout
     * @param hitInfo - specifies caret position
     * @return caret info, see TextLayout.getCaretInfo documentation
     */
    public float[] getCaretInfo(TextHitInfo hitInfo) {
        checkHit(hitInfo);
        float res[] = new float[2];

        int visual = getVisualFromHitInfo(hitInfo);
        float advance, angle;
        TextRunSegment seg;

        if (visual < breaker.getCharCount()) {
            int logIdx = breaker.getLogicalFromVisual(visual);
            int segmentIdx = breaker.logical2segment[logIdx];
            seg = breaker.runSegments.get(segmentIdx);
            advance = seg.x + seg.getAdvanceDelta(seg.getStart(), logIdx);
            angle = seg.metrics.italicAngle;

        } else { // Last character
            int logIdx = breaker.getLogicalFromVisual(visual-1);
            int segmentIdx = breaker.logical2segment[logIdx];
            seg = breaker.runSegments.get(segmentIdx);
            advance = seg.x + seg.getAdvanceDelta(seg.getStart(), logIdx+1);
        }

        angle = seg.metrics.italicAngle;

        res[0] = advance;
        res[1] = angle;

        return res;
    }

    /**
     * Returns the next position to the right from the current caret position
     * @param hitInfo - current position
     * @return next position to the right
     */
    public TextHitInfo getNextRightHit(TextHitInfo hitInfo) {
        checkHit(hitInfo);
        int visual = getVisualFromHitInfo(hitInfo);

        if (visual == breaker.getCharCount()) {
            return null;
        }

        TextHitInfo newInfo;

        while(visual <= breaker.getCharCount()) {
            visual++;
            newInfo = getHitInfoFromVisual(visual);

            if (newInfo.getCharIndex() >= breaker.logical2segment.length) {
                return newInfo;
            }

            if (hitInfo.getCharIndex() >= 0) { // Don't check for leftmost info
                if (
                        breaker.logical2segment[newInfo.getCharIndex()] !=
                        breaker.logical2segment[hitInfo.getCharIndex()]
                ) {
                    return newInfo; // We crossed segment boundary
                }
            }

            TextRunSegment seg = breaker.runSegments.get(breaker.logical2segment[newInfo
                    .getCharIndex()]);
            if (!seg.charHasZeroAdvance(newInfo.getCharIndex())) {
                return newInfo;
            }
        }

        return null;
    }

    /**
     * Returns the next position to the left from the current caret position
     * @param hitInfo - current position
     * @return next position to the left
     */
    public TextHitInfo getNextLeftHit(TextHitInfo hitInfo) {
        checkHit(hitInfo);
        int visual = getVisualFromHitInfo(hitInfo);

        if (visual == 0) {
            return null;
        }

        TextHitInfo newInfo;

        while(visual >= 0) {
            visual--;
            newInfo = getHitInfoFromVisual(visual);

            if (newInfo.getCharIndex() < 0) {
                return newInfo;
            }

            // Don't check for rightmost info
            if (hitInfo.getCharIndex() < breaker.logical2segment.length) {
                if (
                        breaker.logical2segment[newInfo.getCharIndex()] !=
                        breaker.logical2segment[hitInfo.getCharIndex()]
                ) {
                    return newInfo; // We crossed segment boundary
                }
            }

            TextRunSegment seg = breaker.runSegments.get(breaker.logical2segment[newInfo
                    .getCharIndex()]);
            if (!seg.charHasZeroAdvance(newInfo.getCharIndex())) {
                return newInfo;
            }
        }

        return null;
    }

    /**
     * For each visual caret position there are two hits. For the simple LTR text one is
     * a trailing of the previous char and another is the leading of the next char. This
     * method returns the opposite hit for the given hit.
     * @param hitInfo - given hit
     * @return opposite hit
     */
    public TextHitInfo getVisualOtherHit(TextHitInfo hitInfo) {
        checkHit(hitInfo);

        int idx = hitInfo.getCharIndex();

        int resIdx;
        boolean resIsLeading;

        if (idx >= 0 && idx < breaker.getCharCount()) { // Hit info in the middle
            int visual = breaker.getVisualFromLogical(idx);

            // Char is LTR + LEADING info
            if (((breaker.getLevel(idx) & 0x1) == 0x0) ^ hitInfo.isLeadingEdge()) {
                visual++;
                if (visual == breaker.getCharCount()) {
                    if (breaker.isLTR()) {
                        resIdx = breaker.getCharCount();
                        resIsLeading = true;
                    } else {
                        resIdx = -1;
                        resIsLeading = false;
                    }
                } else {
                    resIdx = breaker.getLogicalFromVisual(visual);
                    if ((breaker.getLevel(resIdx) & 0x1) == 0x0) {
                        resIsLeading = true;
                    } else {
                        resIsLeading = false;
                    }
                }
            } else {
                visual--;
                if (visual == -1) {
                    if (breaker.isLTR()) {
                        resIdx = -1;
                        resIsLeading = false;
                    } else {
                        resIdx = breaker.getCharCount();
                        resIsLeading = true;
                    }
                } else {
                    resIdx = breaker.getLogicalFromVisual(visual);
                    if ((breaker.getLevel(resIdx) & 0x1) == 0x0) {
                        resIsLeading = false;
                    } else {
                        resIsLeading = true;
                    }
                }
            }
        } else if (idx < 0) { // before "start"
            if (breaker.isLTR()) {
                resIdx = breaker.getLogicalFromVisual(0);
                resIsLeading = (breaker.getLevel(resIdx) & 0x1) == 0x0; // LTR char?
            } else {
                resIdx = breaker.getLogicalFromVisual(breaker.getCharCount() - 1);
                resIsLeading = (breaker.getLevel(resIdx) & 0x1) != 0x0; // RTL char?
            }
        } else { // idx == breaker.getCharCount()
            if (breaker.isLTR()) {
                resIdx = breaker.getLogicalFromVisual(breaker.getCharCount() - 1);
                resIsLeading = (breaker.getLevel(resIdx) & 0x1) != 0x0; // LTR char?
            } else {
                resIdx = breaker.getLogicalFromVisual(0);
                resIsLeading = (breaker.getLevel(resIdx) & 0x1) == 0x0; // RTL char?
            }
        }

        return resIsLeading ? TextHitInfo.leading(resIdx) : TextHitInfo.trailing(resIdx);
    }

    public Line2D getCaretShape(TextHitInfo hitInfo, TextLayout layout) {
        return getCaretShape(hitInfo, layout, true, false, null);
    }

    /**
     * Creates a caret shape.
     * @param hitInfo - hit where to place a caret
     * @param layout - text layout
     * @param useItalic - unused for now, was used to create
     * slanted carets for italic text
     * @param useBounds - true if the cared should fit into the provided bounds
     * @param bounds - bounds for the caret
     * @return caret shape
     */
    public Line2D getCaretShape(
            TextHitInfo hitInfo, TextLayout layout,
            boolean useItalic, boolean useBounds, Rectangle2D bounds
    ) {
        checkHit(hitInfo);

        float x1, x2, y1, y2;

        int charIdx = hitInfo.getCharIndex();

        if (charIdx >= 0 && charIdx < breaker.getCharCount()) {
            TextRunSegment segment = breaker.runSegments.get(breaker.logical2segment[charIdx]);
            y1 = segment.metrics.descent;
            y2 = - segment.metrics.ascent - segment.metrics.leading;

            x1 = x2 = segment.getCharPosition(charIdx) + (hitInfo.isLeadingEdge() ?
                    0 : segment.getCharAdvance(charIdx));
            // Decided that straight cursor looks better even for italic fonts,
            // especially combined with highlighting
            /*
            // Not graphics, need to check italic angle and baseline
            if (layout.getBaseline() >= 0) {
                if (segment.metrics.italicAngle != 0 && useItalic) {
                    x1 -= segment.metrics.italicAngle * segment.metrics.descent;
                    x2 += segment.metrics.italicAngle *
                        (segment.metrics.ascent + segment.metrics.leading);

                    float baselineOffset =
                        layout.getBaselineOffsets()[layout.getBaseline()];
                    y1 += baselineOffset;
                    y2 += baselineOffset;
                }
            }
            */
        } else {
            y1 = layout.getDescent();
            y2 = - layout.getAscent() - layout.getLeading();
            x1 = x2 = ((breaker.getBaseLevel() & 0x1) == 0 ^ charIdx < 0) ?
                    layout.getAdvance() : 0;
        }

        if (useBounds) {
            y1 = (float) bounds.getMaxY();
            y2 = (float) bounds.getMinY();

            if (x2 > bounds.getMaxX()) {
                x1 = x2 = (float) bounds.getMaxX();
            }
            if (x1 < bounds.getMinX()) {
                x1 = x2 = (float) bounds.getMinX();
            }
        }

        return new Line2D.Float(x1, y1, x2, y2);
    }

    /**
     * Creates caret shapes for the specified offset. On the boundaries where
     * the text is changing its direction this method may return two shapes
     * for the strong and the weak carets, in other cases it would return one.
     * @param offset - offset in the text.
     * @param bounds - bounds to fit the carets into
     * @param policy - caret policy
     * @param layout - text layout
     * @return one or two caret shapes
     */
    public Shape[] getCaretShapes(
            int offset, Rectangle2D bounds,
            TextLayout.CaretPolicy policy, TextLayout layout
    ) {
        TextHitInfo hit1 = TextHitInfo.afterOffset(offset);
        TextHitInfo hit2 = getVisualOtherHit(hit1);

        Shape caret1 = getCaretShape(hit1, layout);

        if (getVisualFromHitInfo(hit1) == getVisualFromHitInfo(hit2)) {
            return new Shape[] {caret1, null};
        }
        Shape caret2 = getCaretShape(hit2, layout);

        TextHitInfo strongHit = policy.getStrongCaret(hit1, hit2, layout);
        return strongHit.equals(hit1) ?
                new Shape[] {caret1, caret2} :
                new Shape[] {caret2, caret1};
    }

    /**
     * Connects two carets to produce a highlight shape.
     * @param caret1 - 1st caret
     * @param caret2 - 2nd caret
     * @return highlight shape
     */
    GeneralPath connectCarets(Line2D caret1, Line2D caret2) {
        GeneralPath path = new GeneralPath(GeneralPath.WIND_NON_ZERO);
        path.moveTo((float) caret1.getX1(), (float) caret1.getY1());
        path.lineTo((float) caret2.getX1(), (float) caret2.getY1());
        path.lineTo((float) caret2.getX2(), (float) caret2.getY2());
        path.lineTo((float) caret1.getX2(), (float) caret1.getY2());

        path.closePath();

        return path;
    }

    /**
     * Creates a highlight shape from given two hits. This shape
     * will always be visually contiguous
     * @param hit1 - 1st hit
     * @param hit2 - 2nd hit
     * @param bounds - bounds to fit the shape into
     * @param layout - text layout
     * @return highlight shape
     */
    public Shape getVisualHighlightShape(
            TextHitInfo hit1, TextHitInfo hit2,
            Rectangle2D bounds, TextLayout layout
    ) {
        checkHit(hit1);
        checkHit(hit2);

        Line2D caret1 = getCaretShape(hit1, layout, false, true, bounds);
        Line2D caret2 = getCaretShape(hit2, layout, false, true, bounds);

        return connectCarets(caret1, caret2);
    }

    /**
     * Suppose that the user visually selected a block of text which has
     * several different levels (mixed RTL and LTR), so, in the logical
     * representation of the text this selection may be not contigous.
     * This methods returns a set of logical ranges for the arbitrary
     * visual selection represented by two hits.
     * @param hit1 - 1st hit
     * @param hit2 - 2nd hit
     * @return logical ranges for the selection
     */
    public int[] getLogicalRangesForVisualSelection(TextHitInfo hit1, TextHitInfo hit2) {
        checkHit(hit1);
        checkHit(hit2);

        int visual1 = getVisualFromHitInfo(hit1);
        int visual2 = getVisualFromHitInfo(hit2);

        if (visual1 > visual2) {
            int tmp = visual2;
            visual2 = visual1;
            visual1 = tmp;
        }

        // Max level is 255, so we don't need more than 512 entries
        int results[] = new int[512];

        int prevLogical, logical, runStart, numRuns = 0;

        logical = runStart = prevLogical = breaker.getLogicalFromVisual(visual1);

        // Get all the runs. We use the fact that direction is constant in all runs.
        for (int i=visual1+1; i<=visual2; i++) {
            logical = breaker.getLogicalFromVisual(i);
            int diff = logical-prevLogical;

            // Start of the next run encountered
            if (diff > 1 || diff < -1) {
                results[(numRuns)*2] = Math.min(runStart, prevLogical);
                results[(numRuns)*2 + 1] = Math.max(runStart, prevLogical);
                numRuns++;
                runStart = logical;
            }

            prevLogical = logical;
        }

        // The last unsaved run
        results[(numRuns)*2] = Math.min(runStart, logical);
        results[(numRuns)*2 + 1] = Math.max(runStart, logical);
        numRuns++;

        int retval[] = new int[numRuns*2];
        System.arraycopy(results, 0, retval, 0, numRuns*2);
        return retval;
    }

    /**
     * Creates a highlight shape from given two endpoints in the logical
     * representation. This shape is not always visually contiguous
     * @param firstEndpoint - 1st logical endpoint
     * @param secondEndpoint - 2nd logical endpoint
     * @param bounds - bounds to fit the shape into
     * @param layout - text layout
     * @return highlight shape
     */
    public Shape getLogicalHighlightShape(
            int firstEndpoint, int secondEndpoint,
            Rectangle2D bounds, TextLayout layout
    ) {
        GeneralPath res = new GeneralPath();

        for (int i=firstEndpoint; i<=secondEndpoint; i++) {
            int endRun = breaker.getLevelRunLimit(i, secondEndpoint);
            TextHitInfo hit1 = TextHitInfo.leading(i);
            TextHitInfo hit2 = TextHitInfo.trailing(endRun-1);

            Line2D caret1 = getCaretShape(hit1, layout, false, true, bounds);
            Line2D caret2 = getCaretShape(hit2, layout, false, true, bounds);

            res.append(connectCarets(caret1, caret2), false);

            i = endRun;
        }

        return res;
    }
}