| /* |
| * 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; |
| } |
| } |