001: /*
002: * Licensed to the Apache Software Foundation (ASF) under one or more
003: * contributor license agreements. See the NOTICE file distributed with
004: * this work for additional information regarding copyright ownership.
005: * The ASF licenses this file to You under the Apache License, Version 2.0
006: * (the "License"); you may not use this file except in compliance with
007: * the License. You may obtain a copy of the License at
008: *
009: * http://www.apache.org/licenses/LICENSE-2.0
010: *
011: * Unless required by applicable law or agreed to in writing, software
012: * distributed under the License is distributed on an "AS IS" BASIS,
013: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014: * See the License for the specific language governing permissions and
015: * limitations under the License.
016: */
017: /**
018: * @author Oleg V. Khaschansky
019: * @version $Revision$
020: *
021: * @date: Jun 14, 2005
022: */package org.apache.harmony.awt.gl.font;
023:
024: import java.awt.font.TextHitInfo;
025: import java.awt.font.TextLayout;
026: import java.awt.geom.Rectangle2D;
027: import java.awt.geom.GeneralPath;
028: import java.awt.geom.Line2D;
029: import java.awt.*;
030:
031: import org.apache.harmony.awt.internal.nls.Messages;
032:
033: /**
034: * This class provides functionality for creating caret and highlight shapes
035: * (bidirectional text is also supported, but, unfortunately, not tested yet).
036: */
037: public class CaretManager {
038: private TextRunBreaker breaker;
039:
040: public CaretManager(TextRunBreaker breaker) {
041: this .breaker = breaker;
042: }
043:
044: /**
045: * Checks if TextHitInfo is not out of the text range and throws the
046: * IllegalArgumentException if it is.
047: * @param info - text hit info
048: */
049: private void checkHit(TextHitInfo info) {
050: int idx = info.getInsertionIndex();
051:
052: if (idx < 0 || idx > breaker.getCharCount()) {
053: // awt.42=TextHitInfo out of range
054: throw new IllegalArgumentException(Messages
055: .getString("awt.42")); //$NON-NLS-1$
056: }
057: }
058:
059: /**
060: * Calculates and returns visual position from the text hit info.
061: * @param hitInfo - text hit info
062: * @return visual index
063: */
064: private int getVisualFromHitInfo(TextHitInfo hitInfo) {
065: final int idx = hitInfo.getCharIndex();
066:
067: if (idx >= 0 && idx < breaker.getCharCount()) {
068: int visual = breaker.getVisualFromLogical(idx);
069: // We take next character for (LTR char + TRAILING info) and (RTL + LEADING)
070: if (hitInfo.isLeadingEdge()
071: ^ ((breaker.getLevel(idx) & 0x1) == 0x0)) {
072: visual++;
073: }
074: return visual;
075: } else if (idx < 0) {
076: return breaker.isLTR() ? 0 : breaker.getCharCount();
077: } else {
078: return breaker.isLTR() ? breaker.getCharCount() : 0;
079: }
080: }
081:
082: /**
083: * Calculates text hit info from the visual position
084: * @param visual - visual position
085: * @return text hit info
086: */
087: private TextHitInfo getHitInfoFromVisual(int visual) {
088: final boolean first = visual == 0;
089:
090: if (!(first || visual == breaker.getCharCount())) {
091: int logical = breaker.getLogicalFromVisual(visual);
092: return (breaker.getLevel(logical) & 0x1) == 0x0 ? TextHitInfo
093: .leading(logical)
094: : // LTR
095: TextHitInfo.trailing(logical); // RTL
096: } else if (first) {
097: return breaker.isLTR() ? TextHitInfo.trailing(-1)
098: : TextHitInfo.leading(breaker.getCharCount());
099: } else { // Last
100: return breaker.isLTR() ? TextHitInfo.leading(breaker
101: .getCharCount()) : TextHitInfo.trailing(-1);
102: }
103: }
104:
105: /**
106: * Creates caret info. Required for the getCaretInfo
107: * methods of the TextLayout
108: * @param hitInfo - specifies caret position
109: * @return caret info, see TextLayout.getCaretInfo documentation
110: */
111: public float[] getCaretInfo(TextHitInfo hitInfo) {
112: checkHit(hitInfo);
113: float res[] = new float[2];
114:
115: int visual = getVisualFromHitInfo(hitInfo);
116: float advance, angle;
117: TextRunSegment seg;
118:
119: if (visual < breaker.getCharCount()) {
120: int logIdx = breaker.getLogicalFromVisual(visual);
121: int segmentIdx = breaker.logical2segment[logIdx];
122: seg = breaker.runSegments.get(segmentIdx);
123: advance = seg.x
124: + seg.getAdvanceDelta(seg.getStart(), logIdx);
125: angle = seg.metrics.italicAngle;
126:
127: } else { // Last character
128: int logIdx = breaker.getLogicalFromVisual(visual - 1);
129: int segmentIdx = breaker.logical2segment[logIdx];
130: seg = breaker.runSegments.get(segmentIdx);
131: advance = seg.x
132: + seg.getAdvanceDelta(seg.getStart(), logIdx + 1);
133: }
134:
135: angle = seg.metrics.italicAngle;
136:
137: res[0] = advance;
138: res[1] = angle;
139:
140: return res;
141: }
142:
143: /**
144: * Returns the next position to the right from the current caret position
145: * @param hitInfo - current position
146: * @return next position to the right
147: */
148: public TextHitInfo getNextRightHit(TextHitInfo hitInfo) {
149: checkHit(hitInfo);
150: int visual = getVisualFromHitInfo(hitInfo);
151:
152: if (visual == breaker.getCharCount()) {
153: return null;
154: }
155:
156: TextHitInfo newInfo;
157:
158: while (visual <= breaker.getCharCount()) {
159: visual++;
160: newInfo = getHitInfoFromVisual(visual);
161:
162: if (newInfo.getCharIndex() >= breaker.logical2segment.length) {
163: return newInfo;
164: }
165:
166: if (hitInfo.getCharIndex() >= 0) { // Don't check for leftmost info
167: if (breaker.logical2segment[newInfo.getCharIndex()] != breaker.logical2segment[hitInfo
168: .getCharIndex()]) {
169: return newInfo; // We crossed segment boundary
170: }
171: }
172:
173: TextRunSegment seg = breaker.runSegments
174: .get(breaker.logical2segment[newInfo.getCharIndex()]);
175: if (!seg.charHasZeroAdvance(newInfo.getCharIndex())) {
176: return newInfo;
177: }
178: }
179:
180: return null;
181: }
182:
183: /**
184: * Returns the next position to the left from the current caret position
185: * @param hitInfo - current position
186: * @return next position to the left
187: */
188: public TextHitInfo getNextLeftHit(TextHitInfo hitInfo) {
189: checkHit(hitInfo);
190: int visual = getVisualFromHitInfo(hitInfo);
191:
192: if (visual == 0) {
193: return null;
194: }
195:
196: TextHitInfo newInfo;
197:
198: while (visual >= 0) {
199: visual--;
200: newInfo = getHitInfoFromVisual(visual);
201:
202: if (newInfo.getCharIndex() < 0) {
203: return newInfo;
204: }
205:
206: // Don't check for rightmost info
207: if (hitInfo.getCharIndex() < breaker.logical2segment.length) {
208: if (breaker.logical2segment[newInfo.getCharIndex()] != breaker.logical2segment[hitInfo
209: .getCharIndex()]) {
210: return newInfo; // We crossed segment boundary
211: }
212: }
213:
214: TextRunSegment seg = breaker.runSegments
215: .get(breaker.logical2segment[newInfo.getCharIndex()]);
216: if (!seg.charHasZeroAdvance(newInfo.getCharIndex())) {
217: return newInfo;
218: }
219: }
220:
221: return null;
222: }
223:
224: /**
225: * For each visual caret position there are two hits. For the simple LTR text one is
226: * a trailing of the previous char and another is the leading of the next char. This
227: * method returns the opposite hit for the given hit.
228: * @param hitInfo - given hit
229: * @return opposite hit
230: */
231: public TextHitInfo getVisualOtherHit(TextHitInfo hitInfo) {
232: checkHit(hitInfo);
233:
234: int idx = hitInfo.getCharIndex();
235:
236: int resIdx;
237: boolean resIsLeading;
238:
239: if (idx >= 0 && idx < breaker.getCharCount()) { // Hit info in the middle
240: int visual = breaker.getVisualFromLogical(idx);
241:
242: // Char is LTR + LEADING info
243: if (((breaker.getLevel(idx) & 0x1) == 0x0)
244: ^ hitInfo.isLeadingEdge()) {
245: visual++;
246: if (visual == breaker.getCharCount()) {
247: if (breaker.isLTR()) {
248: resIdx = breaker.getCharCount();
249: resIsLeading = true;
250: } else {
251: resIdx = -1;
252: resIsLeading = false;
253: }
254: } else {
255: resIdx = breaker.getLogicalFromVisual(visual);
256: if ((breaker.getLevel(resIdx) & 0x1) == 0x0) {
257: resIsLeading = true;
258: } else {
259: resIsLeading = false;
260: }
261: }
262: } else {
263: visual--;
264: if (visual == -1) {
265: if (breaker.isLTR()) {
266: resIdx = -1;
267: resIsLeading = false;
268: } else {
269: resIdx = breaker.getCharCount();
270: resIsLeading = true;
271: }
272: } else {
273: resIdx = breaker.getLogicalFromVisual(visual);
274: if ((breaker.getLevel(resIdx) & 0x1) == 0x0) {
275: resIsLeading = false;
276: } else {
277: resIsLeading = true;
278: }
279: }
280: }
281: } else if (idx < 0) { // before "start"
282: if (breaker.isLTR()) {
283: resIdx = breaker.getLogicalFromVisual(0);
284: resIsLeading = (breaker.getLevel(resIdx) & 0x1) == 0x0; // LTR char?
285: } else {
286: resIdx = breaker.getLogicalFromVisual(breaker
287: .getCharCount() - 1);
288: resIsLeading = (breaker.getLevel(resIdx) & 0x1) != 0x0; // RTL char?
289: }
290: } else { // idx == breaker.getCharCount()
291: if (breaker.isLTR()) {
292: resIdx = breaker.getLogicalFromVisual(breaker
293: .getCharCount() - 1);
294: resIsLeading = (breaker.getLevel(resIdx) & 0x1) != 0x0; // LTR char?
295: } else {
296: resIdx = breaker.getLogicalFromVisual(0);
297: resIsLeading = (breaker.getLevel(resIdx) & 0x1) == 0x0; // RTL char?
298: }
299: }
300:
301: return resIsLeading ? TextHitInfo.leading(resIdx) : TextHitInfo
302: .trailing(resIdx);
303: }
304:
305: public Line2D getCaretShape(TextHitInfo hitInfo, TextLayout layout) {
306: return getCaretShape(hitInfo, layout, true, false, null);
307: }
308:
309: /**
310: * Creates a caret shape.
311: * @param hitInfo - hit where to place a caret
312: * @param layout - text layout
313: * @param useItalic - unused for now, was used to create
314: * slanted carets for italic text
315: * @param useBounds - true if the cared should fit into the provided bounds
316: * @param bounds - bounds for the caret
317: * @return caret shape
318: */
319: public Line2D getCaretShape(TextHitInfo hitInfo, TextLayout layout,
320: boolean useItalic, boolean useBounds, Rectangle2D bounds) {
321: checkHit(hitInfo);
322:
323: float x1, x2, y1, y2;
324:
325: int charIdx = hitInfo.getCharIndex();
326:
327: if (charIdx >= 0 && charIdx < breaker.getCharCount()) {
328: TextRunSegment segment = breaker.runSegments
329: .get(breaker.logical2segment[charIdx]);
330: y1 = segment.metrics.descent;
331: y2 = -segment.metrics.ascent - segment.metrics.leading;
332:
333: x1 = x2 = segment.getCharPosition(charIdx)
334: + (hitInfo.isLeadingEdge() ? 0 : segment
335: .getCharAdvance(charIdx));
336: // Decided that straight cursor looks better even for italic fonts,
337: // especially combined with highlighting
338: /*
339: // Not graphics, need to check italic angle and baseline
340: if (layout.getBaseline() >= 0) {
341: if (segment.metrics.italicAngle != 0 && useItalic) {
342: x1 -= segment.metrics.italicAngle * segment.metrics.descent;
343: x2 += segment.metrics.italicAngle *
344: (segment.metrics.ascent + segment.metrics.leading);
345:
346: float baselineOffset =
347: layout.getBaselineOffsets()[layout.getBaseline()];
348: y1 += baselineOffset;
349: y2 += baselineOffset;
350: }
351: }
352: */
353: } else {
354: y1 = layout.getDescent();
355: y2 = -layout.getAscent() - layout.getLeading();
356: x1 = x2 = ((breaker.getBaseLevel() & 0x1) == 0 ^ charIdx < 0) ? layout
357: .getAdvance()
358: : 0;
359: }
360:
361: if (useBounds) {
362: y1 = (float) bounds.getMaxY();
363: y2 = (float) bounds.getMinY();
364:
365: if (x2 > bounds.getMaxX()) {
366: x1 = x2 = (float) bounds.getMaxX();
367: }
368: if (x1 < bounds.getMinX()) {
369: x1 = x2 = (float) bounds.getMinX();
370: }
371: }
372:
373: return new Line2D.Float(x1, y1, x2, y2);
374: }
375:
376: /**
377: * Creates caret shapes for the specified offset. On the boundaries where
378: * the text is changing its direction this method may return two shapes
379: * for the strong and the weak carets, in other cases it would return one.
380: * @param offset - offset in the text.
381: * @param bounds - bounds to fit the carets into
382: * @param policy - caret policy
383: * @param layout - text layout
384: * @return one or two caret shapes
385: */
386: public Shape[] getCaretShapes(int offset, Rectangle2D bounds,
387: TextLayout.CaretPolicy policy, TextLayout layout) {
388: TextHitInfo hit1 = TextHitInfo.afterOffset(offset);
389: TextHitInfo hit2 = getVisualOtherHit(hit1);
390:
391: Shape caret1 = getCaretShape(hit1, layout);
392:
393: if (getVisualFromHitInfo(hit1) == getVisualFromHitInfo(hit2)) {
394: return new Shape[] { caret1, null };
395: }
396: Shape caret2 = getCaretShape(hit2, layout);
397:
398: TextHitInfo strongHit = policy.getStrongCaret(hit1, hit2,
399: layout);
400: return strongHit.equals(hit1) ? new Shape[] { caret1, caret2 }
401: : new Shape[] { caret2, caret1 };
402: }
403:
404: /**
405: * Connects two carets to produce a highlight shape.
406: * @param caret1 - 1st caret
407: * @param caret2 - 2nd caret
408: * @return highlight shape
409: */
410: GeneralPath connectCarets(Line2D caret1, Line2D caret2) {
411: GeneralPath path = new GeneralPath(GeneralPath.WIND_NON_ZERO);
412: path.moveTo((float) caret1.getX1(), (float) caret1.getY1());
413: path.lineTo((float) caret2.getX1(), (float) caret2.getY1());
414: path.lineTo((float) caret2.getX2(), (float) caret2.getY2());
415: path.lineTo((float) caret1.getX2(), (float) caret1.getY2());
416:
417: path.closePath();
418:
419: return path;
420: }
421:
422: /**
423: * Creates a highlight shape from given two hits. This shape
424: * will always be visually contiguous
425: * @param hit1 - 1st hit
426: * @param hit2 - 2nd hit
427: * @param bounds - bounds to fit the shape into
428: * @param layout - text layout
429: * @return highlight shape
430: */
431: public Shape getVisualHighlightShape(TextHitInfo hit1,
432: TextHitInfo hit2, Rectangle2D bounds, TextLayout layout) {
433: checkHit(hit1);
434: checkHit(hit2);
435:
436: Line2D caret1 = getCaretShape(hit1, layout, false, true, bounds);
437: Line2D caret2 = getCaretShape(hit2, layout, false, true, bounds);
438:
439: return connectCarets(caret1, caret2);
440: }
441:
442: /**
443: * Suppose that the user visually selected a block of text which has
444: * several different levels (mixed RTL and LTR), so, in the logical
445: * representation of the text this selection may be not contigous.
446: * This methods returns a set of logical ranges for the arbitrary
447: * visual selection represented by two hits.
448: * @param hit1 - 1st hit
449: * @param hit2 - 2nd hit
450: * @return logical ranges for the selection
451: */
452: public int[] getLogicalRangesForVisualSelection(TextHitInfo hit1,
453: TextHitInfo hit2) {
454: checkHit(hit1);
455: checkHit(hit2);
456:
457: int visual1 = getVisualFromHitInfo(hit1);
458: int visual2 = getVisualFromHitInfo(hit2);
459:
460: if (visual1 > visual2) {
461: int tmp = visual2;
462: visual2 = visual1;
463: visual1 = tmp;
464: }
465:
466: // Max level is 255, so we don't need more than 512 entries
467: int results[] = new int[512];
468:
469: int prevLogical, logical, runStart, numRuns = 0;
470:
471: logical = runStart = prevLogical = breaker
472: .getLogicalFromVisual(visual1);
473:
474: // Get all the runs. We use the fact that direction is constant in all runs.
475: for (int i = visual1 + 1; i <= visual2; i++) {
476: logical = breaker.getLogicalFromVisual(i);
477: int diff = logical - prevLogical;
478:
479: // Start of the next run encountered
480: if (diff > 1 || diff < -1) {
481: results[(numRuns) * 2] = Math
482: .min(runStart, prevLogical);
483: results[(numRuns) * 2 + 1] = Math.max(runStart,
484: prevLogical);
485: numRuns++;
486: runStart = logical;
487: }
488:
489: prevLogical = logical;
490: }
491:
492: // The last unsaved run
493: results[(numRuns) * 2] = Math.min(runStart, logical);
494: results[(numRuns) * 2 + 1] = Math.max(runStart, logical);
495: numRuns++;
496:
497: int retval[] = new int[numRuns * 2];
498: System.arraycopy(results, 0, retval, 0, numRuns * 2);
499: return retval;
500: }
501:
502: /**
503: * Creates a highlight shape from given two endpoints in the logical
504: * representation. This shape is not always visually contiguous
505: * @param firstEndpoint - 1st logical endpoint
506: * @param secondEndpoint - 2nd logical endpoint
507: * @param bounds - bounds to fit the shape into
508: * @param layout - text layout
509: * @return highlight shape
510: */
511: public Shape getLogicalHighlightShape(int firstEndpoint,
512: int secondEndpoint, Rectangle2D bounds, TextLayout layout) {
513: GeneralPath res = new GeneralPath();
514:
515: for (int i = firstEndpoint; i <= secondEndpoint; i++) {
516: int endRun = breaker.getLevelRunLimit(i, secondEndpoint);
517: TextHitInfo hit1 = TextHitInfo.leading(i);
518: TextHitInfo hit2 = TextHitInfo.trailing(endRun - 1);
519:
520: Line2D caret1 = getCaretShape(hit1, layout, false, true,
521: bounds);
522: Line2D caret2 = getCaretShape(hit2, layout, false, true,
523: bounds);
524:
525: res.append(connectCarets(caret1, caret2), false);
526:
527: i = endRun;
528: }
529:
530: return res;
531: }
532: }
|