blob: b18bdd53ae8494df596bbcd41a945d50df02cc65 [file] [log] [blame]
/*
* 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;
}
}