diff options
Diffstat (limited to 'awt/org/apache/harmony/awt/gl/font/CaretManager.java')
-rw-r--r-- | awt/org/apache/harmony/awt/gl/font/CaretManager.java | 530 |
1 files changed, 530 insertions, 0 deletions
diff --git a/awt/org/apache/harmony/awt/gl/font/CaretManager.java b/awt/org/apache/harmony/awt/gl/font/CaretManager.java new file mode 100644 index 0000000..b18bdd5 --- /dev/null +++ b/awt/org/apache/harmony/awt/gl/font/CaretManager.java @@ -0,0 +1,530 @@ +/* + * 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; + } +} |