blob: 68c7c2269336d6cf265930fc9c9044506fee1550 [file] [log] [blame]
J. Duke319a3b92007-12-01 00:00:00 +00001/*
2 * Copyright 1997-2006 Sun Microsystems, Inc. All Rights Reserved.
3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4 *
5 * This code is free software; you can redistribute it and/or modify it
6 * under the terms of the GNU General Public License version 2 only, as
7 * published by the Free Software Foundation. Sun designates this
8 * particular file as subject to the "Classpath" exception as provided
9 * by Sun in the LICENSE file that accompanied this code.
10 *
11 * This code is distributed in the hope that it will be useful, but WITHOUT
12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
14 * version 2 for more details (a copy is included in the LICENSE file that
15 * accompanied this code).
16 *
17 * You should have received a copy of the GNU General Public License version
18 * 2 along with this work; if not, write to the Free Software Foundation,
19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20 *
21 * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa Clara,
22 * CA 95054 USA or visit www.sun.com if you need additional information or
23 * have any questions.
24 */
25package javax.swing.text;
26
27import java.lang.reflect.Method;
28
29import java.awt.Component;
30import java.awt.Rectangle;
31import java.awt.Graphics;
32import java.awt.FontMetrics;
33import java.awt.Shape;
34import java.awt.Toolkit;
35import java.awt.Graphics2D;
36import java.awt.font.FontRenderContext;
37import java.awt.font.TextLayout;
38import java.awt.font.TextAttribute;
39
40import java.text.*;
41import javax.swing.JComponent;
42import javax.swing.SwingConstants;
43import javax.swing.text.ParagraphView.Row;
44import sun.swing.SwingUtilities2;
45
46/**
47 * A collection of methods to deal with various text
48 * related activities.
49 *
50 * @author Timothy Prinzing
51 */
52public class Utilities {
53 /**
54 * If <code>view</code>'s container is a <code>JComponent</code> it
55 * is returned, after casting.
56 */
57 static JComponent getJComponent(View view) {
58 if (view != null) {
59 Component component = view.getContainer();
60 if (component instanceof JComponent) {
61 return (JComponent)component;
62 }
63 }
64 return null;
65 }
66
67 /**
68 * Draws the given text, expanding any tabs that are contained
69 * using the given tab expansion technique. This particular
70 * implementation renders in a 1.1 style coordinate system
71 * where ints are used and 72dpi is assumed.
72 *
73 * @param s the source of the text
74 * @param x the X origin >= 0
75 * @param y the Y origin >= 0
76 * @param g the graphics context
77 * @param e how to expand the tabs. If this value is null,
78 * tabs will be expanded as a space character.
79 * @param startOffset starting offset of the text in the document >= 0
80 * @return the X location at the end of the rendered text
81 */
82 public static final int drawTabbedText(Segment s, int x, int y, Graphics g,
83 TabExpander e, int startOffset) {
84 return drawTabbedText(null, s, x, y, g, e, startOffset);
85 }
86
87 /**
88 * Draws the given text, expanding any tabs that are contained
89 * using the given tab expansion technique. This particular
90 * implementation renders in a 1.1 style coordinate system
91 * where ints are used and 72dpi is assumed.
92 *
93 * @param view View requesting rendering, may be null.
94 * @param s the source of the text
95 * @param x the X origin >= 0
96 * @param y the Y origin >= 0
97 * @param g the graphics context
98 * @param e how to expand the tabs. If this value is null,
99 * tabs will be expanded as a space character.
100 * @param startOffset starting offset of the text in the document >= 0
101 * @return the X location at the end of the rendered text
102 */
103 static final int drawTabbedText(View view,
104 Segment s, int x, int y, Graphics g,
105 TabExpander e, int startOffset) {
106 return drawTabbedText(view, s, x, y, g, e, startOffset, null);
107 }
108
109 // In addition to the previous method it can extend spaces for
110 // justification.
111 //
112 // all params are the same as in the preious method except the last
113 // one:
114 // @param justificationData justificationData for the row.
115 // if null not justification is needed
116 static final int drawTabbedText(View view,
117 Segment s, int x, int y, Graphics g,
118 TabExpander e, int startOffset,
119 int [] justificationData) {
120 JComponent component = getJComponent(view);
121 FontMetrics metrics = SwingUtilities2.getFontMetrics(component, g);
122 int nextX = x;
123 char[] txt = s.array;
124 int txtOffset = s.offset;
125 int flushLen = 0;
126 int flushIndex = s.offset;
127 int spaceAddon = 0;
128 int spaceAddonLeftoverEnd = -1;
129 int startJustifiableContent = 0;
130 int endJustifiableContent = 0;
131 if (justificationData != null) {
132 int offset = - startOffset + txtOffset;
133 View parent = null;
134 if (view != null
135 && (parent = view.getParent()) != null) {
136 offset += parent.getStartOffset();
137 }
138 spaceAddon =
139 justificationData[Row.SPACE_ADDON];
140 spaceAddonLeftoverEnd =
141 justificationData[Row.SPACE_ADDON_LEFTOVER_END] + offset;
142 startJustifiableContent =
143 justificationData[Row.START_JUSTIFIABLE] + offset;
144 endJustifiableContent =
145 justificationData[Row.END_JUSTIFIABLE] + offset;
146 }
147 int n = s.offset + s.count;
148 for (int i = txtOffset; i < n; i++) {
149 if (txt[i] == '\t'
150 || ((spaceAddon != 0 || i <= spaceAddonLeftoverEnd)
151 && (txt[i] == ' ')
152 && startJustifiableContent <= i
153 && i <= endJustifiableContent
154 )) {
155 if (flushLen > 0) {
156 nextX = SwingUtilities2.drawChars(component, g, txt,
157 flushIndex, flushLen, x, y);
158 flushLen = 0;
159 }
160 flushIndex = i + 1;
161 if (txt[i] == '\t') {
162 if (e != null) {
163 nextX = (int) e.nextTabStop((float) nextX, startOffset + i - txtOffset);
164 } else {
165 nextX += metrics.charWidth(' ');
166 }
167 } else if (txt[i] == ' ') {
168 nextX += metrics.charWidth(' ') + spaceAddon;
169 if (i <= spaceAddonLeftoverEnd) {
170 nextX++;
171 }
172 }
173 x = nextX;
174 } else if ((txt[i] == '\n') || (txt[i] == '\r')) {
175 if (flushLen > 0) {
176 nextX = SwingUtilities2.drawChars(component, g, txt,
177 flushIndex, flushLen, x, y);
178 flushLen = 0;
179 }
180 flushIndex = i + 1;
181 x = nextX;
182 } else {
183 flushLen += 1;
184 }
185 }
186 if (flushLen > 0) {
187 nextX = SwingUtilities2.drawChars(component, g,txt, flushIndex,
188 flushLen, x, y);
189 }
190 return nextX;
191 }
192
193 /**
194 * Determines the width of the given segment of text taking tabs
195 * into consideration. This is implemented in a 1.1 style coordinate
196 * system where ints are used and 72dpi is assumed.
197 *
198 * @param s the source of the text
199 * @param metrics the font metrics to use for the calculation
200 * @param x the X origin >= 0
201 * @param e how to expand the tabs. If this value is null,
202 * tabs will be expanded as a space character.
203 * @param startOffset starting offset of the text in the document >= 0
204 * @return the width of the text
205 */
206 public static final int getTabbedTextWidth(Segment s, FontMetrics metrics, int x,
207 TabExpander e, int startOffset) {
208 return getTabbedTextWidth(null, s, metrics, x, e, startOffset, null);
209 }
210
211
212 // In addition to the previous method it can extend spaces for
213 // justification.
214 //
215 // all params are the same as in the preious method except the last
216 // one:
217 // @param justificationData justificationData for the row.
218 // if null not justification is needed
219 static final int getTabbedTextWidth(View view, Segment s, FontMetrics metrics, int x,
220 TabExpander e, int startOffset,
221 int[] justificationData) {
222 int nextX = x;
223 char[] txt = s.array;
224 int txtOffset = s.offset;
225 int n = s.offset + s.count;
226 int charCount = 0;
227 int spaceAddon = 0;
228 int spaceAddonLeftoverEnd = -1;
229 int startJustifiableContent = 0;
230 int endJustifiableContent = 0;
231 if (justificationData != null) {
232 int offset = - startOffset + txtOffset;
233 View parent = null;
234 if (view != null
235 && (parent = view.getParent()) != null) {
236 offset += parent.getStartOffset();
237 }
238 spaceAddon =
239 justificationData[Row.SPACE_ADDON];
240 spaceAddonLeftoverEnd =
241 justificationData[Row.SPACE_ADDON_LEFTOVER_END] + offset;
242 startJustifiableContent =
243 justificationData[Row.START_JUSTIFIABLE] + offset;
244 endJustifiableContent =
245 justificationData[Row.END_JUSTIFIABLE] + offset;
246 }
247
248 for (int i = txtOffset; i < n; i++) {
249 if (txt[i] == '\t'
250 || ((spaceAddon != 0 || i <= spaceAddonLeftoverEnd)
251 && (txt[i] == ' ')
252 && startJustifiableContent <= i
253 && i <= endJustifiableContent
254 )) {
255 nextX += metrics.charsWidth(txt, i-charCount, charCount);
256 charCount = 0;
257 if (txt[i] == '\t') {
258 if (e != null) {
259 nextX = (int) e.nextTabStop((float) nextX,
260 startOffset + i - txtOffset);
261 } else {
262 nextX += metrics.charWidth(' ');
263 }
264 } else if (txt[i] == ' ') {
265 nextX += metrics.charWidth(' ') + spaceAddon;
266 if (i <= spaceAddonLeftoverEnd) {
267 nextX++;
268 }
269 }
270 } else if(txt[i] == '\n') {
271 // Ignore newlines, they take up space and we shouldn't be
272 // counting them.
273 nextX += metrics.charsWidth(txt, i - charCount, charCount);
274 charCount = 0;
275 } else {
276 charCount++;
277 }
278 }
279 nextX += metrics.charsWidth(txt, n - charCount, charCount);
280 return nextX - x;
281 }
282
283 /**
284 * Determines the relative offset into the given text that
285 * best represents the given span in the view coordinate
286 * system. This is implemented in a 1.1 style coordinate
287 * system where ints are used and 72dpi is assumed.
288 *
289 * @param s the source of the text
290 * @param metrics the font metrics to use for the calculation
291 * @param x0 the starting view location representing the start
292 * of the given text >= 0.
293 * @param x the target view location to translate to an
294 * offset into the text >= 0.
295 * @param e how to expand the tabs. If this value is null,
296 * tabs will be expanded as a space character.
297 * @param startOffset starting offset of the text in the document >= 0
298 * @return the offset into the text >= 0
299 */
300 public static final int getTabbedTextOffset(Segment s, FontMetrics metrics,
301 int x0, int x, TabExpander e,
302 int startOffset) {
303 return getTabbedTextOffset(s, metrics, x0, x, e, startOffset, true);
304 }
305
306 static final int getTabbedTextOffset(View view, Segment s, FontMetrics metrics,
307 int x0, int x, TabExpander e,
308 int startOffset,
309 int[] justificationData) {
310 return getTabbedTextOffset(view, s, metrics, x0, x, e, startOffset, true,
311 justificationData);
312 }
313
314 public static final int getTabbedTextOffset(Segment s,
315 FontMetrics metrics,
316 int x0, int x, TabExpander e,
317 int startOffset,
318 boolean round) {
319 return getTabbedTextOffset(null, s, metrics, x0, x, e, startOffset, round, null);
320 }
321
322 // In addition to the previous method it can extend spaces for
323 // justification.
324 //
325 // all params are the same as in the preious method except the last
326 // one:
327 // @param justificationData justificationData for the row.
328 // if null not justification is needed
329 static final int getTabbedTextOffset(View view,
330 Segment s,
331 FontMetrics metrics,
332 int x0, int x, TabExpander e,
333 int startOffset,
334 boolean round,
335 int[] justificationData) {
336 if (x0 >= x) {
337 // x before x0, return.
338 return 0;
339 }
340 int currX = x0;
341 int nextX = currX;
342 // s may be a shared segment, so it is copied prior to calling
343 // the tab expander
344 char[] txt = s.array;
345 int txtOffset = s.offset;
346 int txtCount = s.count;
347 int spaceAddon = 0 ;
348 int spaceAddonLeftoverEnd = -1;
349 int startJustifiableContent = 0 ;
350 int endJustifiableContent = 0;
351 if (justificationData != null) {
352 int offset = - startOffset + txtOffset;
353 View parent = null;
354 if (view != null
355 && (parent = view.getParent()) != null) {
356 offset += parent.getStartOffset();
357 }
358 spaceAddon =
359 justificationData[Row.SPACE_ADDON];
360 spaceAddonLeftoverEnd =
361 justificationData[Row.SPACE_ADDON_LEFTOVER_END] + offset;
362 startJustifiableContent =
363 justificationData[Row.START_JUSTIFIABLE] + offset;
364 endJustifiableContent =
365 justificationData[Row.END_JUSTIFIABLE] + offset;
366 }
367 int n = s.offset + s.count;
368 for (int i = s.offset; i < n; i++) {
369 if (txt[i] == '\t'
370 || ((spaceAddon != 0 || i <= spaceAddonLeftoverEnd)
371 && (txt[i] == ' ')
372 && startJustifiableContent <= i
373 && i <= endJustifiableContent
374 )){
375 if (txt[i] == '\t') {
376 if (e != null) {
377 nextX = (int) e.nextTabStop((float) nextX,
378 startOffset + i - txtOffset);
379 } else {
380 nextX += metrics.charWidth(' ');
381 }
382 } else if (txt[i] == ' ') {
383 nextX += metrics.charWidth(' ') + spaceAddon;
384 if (i <= spaceAddonLeftoverEnd) {
385 nextX++;
386 }
387 }
388 } else {
389 nextX += metrics.charWidth(txt[i]);
390 }
391 if ((x >= currX) && (x < nextX)) {
392 // found the hit position... return the appropriate side
393 if ((round == false) || ((x - currX) < (nextX - x))) {
394 return i - txtOffset;
395 } else {
396 return i + 1 - txtOffset;
397 }
398 }
399 currX = nextX;
400 }
401
402 // didn't find, return end offset
403 return txtCount;
404 }
405
406 /**
407 * Determine where to break the given text to fit
408 * within the given span. This tries to find a word boundary.
409 * @param s the source of the text
410 * @param metrics the font metrics to use for the calculation
411 * @param x0 the starting view location representing the start
412 * of the given text.
413 * @param x the target view location to translate to an
414 * offset into the text.
415 * @param e how to expand the tabs. If this value is null,
416 * tabs will be expanded as a space character.
417 * @param startOffset starting offset in the document of the text
418 * @return the offset into the given text
419 */
420 public static final int getBreakLocation(Segment s, FontMetrics metrics,
421 int x0, int x, TabExpander e,
422 int startOffset) {
423 char[] txt = s.array;
424 int txtOffset = s.offset;
425 int txtCount = s.count;
426 int index = Utilities.getTabbedTextOffset(s, metrics, x0, x,
427 e, startOffset, false);
428
429
430 if (index >= txtCount - 1) {
431 return txtCount;
432 }
433
434 for (int i = txtOffset + index; i >= txtOffset; i--) {
435 char ch = txt[i];
436 if (ch < 256) {
437 // break on whitespace
438 if (Character.isWhitespace(ch)) {
439 index = i - txtOffset + 1;
440 break;
441 }
442 } else {
443 // a multibyte char found; use BreakIterator to find line break
444 BreakIterator bit = BreakIterator.getLineInstance();
445 bit.setText(s);
446 int breakPos = bit.preceding(i + 1);
447 if (breakPos > txtOffset) {
448 index = breakPos - txtOffset;
449 }
450 break;
451 }
452 }
453 return index;
454 }
455
456 /**
457 * Determines the starting row model position of the row that contains
458 * the specified model position. The component given must have a
459 * size to compute the result. If the component doesn't have a size
460 * a value of -1 will be returned.
461 *
462 * @param c the editor
463 * @param offs the offset in the document >= 0
464 * @return the position >= 0 if the request can be computed, otherwise
465 * a value of -1 will be returned.
466 * @exception BadLocationException if the offset is out of range
467 */
468 public static final int getRowStart(JTextComponent c, int offs) throws BadLocationException {
469 Rectangle r = c.modelToView(offs);
470 if (r == null) {
471 return -1;
472 }
473 int lastOffs = offs;
474 int y = r.y;
475 while ((r != null) && (y == r.y)) {
476 // Skip invisible elements
477 if(r.height !=0) {
478 offs = lastOffs;
479 }
480 lastOffs -= 1;
481 r = (lastOffs >= 0) ? c.modelToView(lastOffs) : null;
482 }
483 return offs;
484 }
485
486 /**
487 * Determines the ending row model position of the row that contains
488 * the specified model position. The component given must have a
489 * size to compute the result. If the component doesn't have a size
490 * a value of -1 will be returned.
491 *
492 * @param c the editor
493 * @param offs the offset in the document >= 0
494 * @return the position >= 0 if the request can be computed, otherwise
495 * a value of -1 will be returned.
496 * @exception BadLocationException if the offset is out of range
497 */
498 public static final int getRowEnd(JTextComponent c, int offs) throws BadLocationException {
499 Rectangle r = c.modelToView(offs);
500 if (r == null) {
501 return -1;
502 }
503 int n = c.getDocument().getLength();
504 int lastOffs = offs;
505 int y = r.y;
506 while ((r != null) && (y == r.y)) {
507 // Skip invisible elements
508 if (r.height !=0) {
509 offs = lastOffs;
510 }
511 lastOffs += 1;
512 r = (lastOffs <= n) ? c.modelToView(lastOffs) : null;
513 }
514 return offs;
515 }
516
517 /**
518 * Determines the position in the model that is closest to the given
519 * view location in the row above. The component given must have a
520 * size to compute the result. If the component doesn't have a size
521 * a value of -1 will be returned.
522 *
523 * @param c the editor
524 * @param offs the offset in the document >= 0
525 * @param x the X coordinate >= 0
526 * @return the position >= 0 if the request can be computed, otherwise
527 * a value of -1 will be returned.
528 * @exception BadLocationException if the offset is out of range
529 */
530 public static final int getPositionAbove(JTextComponent c, int offs, int x) throws BadLocationException {
531 int lastOffs = getRowStart(c, offs) - 1;
532 if (lastOffs < 0) {
533 return -1;
534 }
535 int bestSpan = Integer.MAX_VALUE;
536 int y = 0;
537 Rectangle r = null;
538 if (lastOffs >= 0) {
539 r = c.modelToView(lastOffs);
540 y = r.y;
541 }
542 while ((r != null) && (y == r.y)) {
543 int span = Math.abs(r.x - x);
544 if (span < bestSpan) {
545 offs = lastOffs;
546 bestSpan = span;
547 }
548 lastOffs -= 1;
549 r = (lastOffs >= 0) ? c.modelToView(lastOffs) : null;
550 }
551 return offs;
552 }
553
554 /**
555 * Determines the position in the model that is closest to the given
556 * view location in the row below. The component given must have a
557 * size to compute the result. If the component doesn't have a size
558 * a value of -1 will be returned.
559 *
560 * @param c the editor
561 * @param offs the offset in the document >= 0
562 * @param x the X coordinate >= 0
563 * @return the position >= 0 if the request can be computed, otherwise
564 * a value of -1 will be returned.
565 * @exception BadLocationException if the offset is out of range
566 */
567 public static final int getPositionBelow(JTextComponent c, int offs, int x) throws BadLocationException {
568 int lastOffs = getRowEnd(c, offs) + 1;
569 if (lastOffs <= 0) {
570 return -1;
571 }
572 int bestSpan = Integer.MAX_VALUE;
573 int n = c.getDocument().getLength();
574 int y = 0;
575 Rectangle r = null;
576 if (lastOffs <= n) {
577 r = c.modelToView(lastOffs);
578 y = r.y;
579 }
580 while ((r != null) && (y == r.y)) {
581 int span = Math.abs(x - r.x);
582 if (span < bestSpan) {
583 offs = lastOffs;
584 bestSpan = span;
585 }
586 lastOffs += 1;
587 r = (lastOffs <= n) ? c.modelToView(lastOffs) : null;
588 }
589 return offs;
590 }
591
592 /**
593 * Determines the start of a word for the given model location.
594 * Uses BreakIterator.getWordInstance() to actually get the words.
595 *
596 * @param c the editor
597 * @param offs the offset in the document >= 0
598 * @return the location in the model of the word start >= 0
599 * @exception BadLocationException if the offset is out of range
600 */
601 public static final int getWordStart(JTextComponent c, int offs) throws BadLocationException {
602 Document doc = c.getDocument();
603 Element line = getParagraphElement(c, offs);
604 if (line == null) {
605 throw new BadLocationException("No word at " + offs, offs);
606 }
607 int lineStart = line.getStartOffset();
608 int lineEnd = Math.min(line.getEndOffset(), doc.getLength());
609
610 Segment seg = SegmentCache.getSharedSegment();
611 doc.getText(lineStart, lineEnd - lineStart, seg);
612 if(seg.count > 0) {
613 BreakIterator words = BreakIterator.getWordInstance(c.getLocale());
614 words.setText(seg);
615 int wordPosition = seg.offset + offs - lineStart;
616 if(wordPosition >= words.last()) {
617 wordPosition = words.last() - 1;
618 }
619 words.following(wordPosition);
620 offs = lineStart + words.previous() - seg.offset;
621 }
622 SegmentCache.releaseSharedSegment(seg);
623 return offs;
624 }
625
626 /**
627 * Determines the end of a word for the given location.
628 * Uses BreakIterator.getWordInstance() to actually get the words.
629 *
630 * @param c the editor
631 * @param offs the offset in the document >= 0
632 * @return the location in the model of the word end >= 0
633 * @exception BadLocationException if the offset is out of range
634 */
635 public static final int getWordEnd(JTextComponent c, int offs) throws BadLocationException {
636 Document doc = c.getDocument();
637 Element line = getParagraphElement(c, offs);
638 if (line == null) {
639 throw new BadLocationException("No word at " + offs, offs);
640 }
641 int lineStart = line.getStartOffset();
642 int lineEnd = Math.min(line.getEndOffset(), doc.getLength());
643
644 Segment seg = SegmentCache.getSharedSegment();
645 doc.getText(lineStart, lineEnd - lineStart, seg);
646 if(seg.count > 0) {
647 BreakIterator words = BreakIterator.getWordInstance(c.getLocale());
648 words.setText(seg);
649 int wordPosition = offs - lineStart + seg.offset;
650 if(wordPosition >= words.last()) {
651 wordPosition = words.last() - 1;
652 }
653 offs = lineStart + words.following(wordPosition) - seg.offset;
654 }
655 SegmentCache.releaseSharedSegment(seg);
656 return offs;
657 }
658
659 /**
660 * Determines the start of the next word for the given location.
661 * Uses BreakIterator.getWordInstance() to actually get the words.
662 *
663 * @param c the editor
664 * @param offs the offset in the document >= 0
665 * @return the location in the model of the word start >= 0
666 * @exception BadLocationException if the offset is out of range
667 */
668 public static final int getNextWord(JTextComponent c, int offs) throws BadLocationException {
669 int nextWord;
670 Element line = getParagraphElement(c, offs);
671 for (nextWord = getNextWordInParagraph(c, line, offs, false);
672 nextWord == BreakIterator.DONE;
673 nextWord = getNextWordInParagraph(c, line, offs, true)) {
674
675 // didn't find in this line, try the next line
676 offs = line.getEndOffset();
677 line = getParagraphElement(c, offs);
678 }
679 return nextWord;
680 }
681
682 /**
683 * Finds the next word in the given elements text. The first
684 * parameter allows searching multiple paragraphs where even
685 * the first offset is desired.
686 * Returns the offset of the next word, or BreakIterator.DONE
687 * if there are no more words in the element.
688 */
689 static int getNextWordInParagraph(JTextComponent c, Element line, int offs, boolean first) throws BadLocationException {
690 if (line == null) {
691 throw new BadLocationException("No more words", offs);
692 }
693 Document doc = line.getDocument();
694 int lineStart = line.getStartOffset();
695 int lineEnd = Math.min(line.getEndOffset(), doc.getLength());
696 if ((offs >= lineEnd) || (offs < lineStart)) {
697 throw new BadLocationException("No more words", offs);
698 }
699 Segment seg = SegmentCache.getSharedSegment();
700 doc.getText(lineStart, lineEnd - lineStart, seg);
701 BreakIterator words = BreakIterator.getWordInstance(c.getLocale());
702 words.setText(seg);
703 if ((first && (words.first() == (seg.offset + offs - lineStart))) &&
704 (! Character.isWhitespace(seg.array[words.first()]))) {
705
706 return offs;
707 }
708 int wordPosition = words.following(seg.offset + offs - lineStart);
709 if ((wordPosition == BreakIterator.DONE) ||
710 (wordPosition >= seg.offset + seg.count)) {
711 // there are no more words on this line.
712 return BreakIterator.DONE;
713 }
714 // if we haven't shot past the end... check to
715 // see if the current boundary represents whitespace.
716 // if so, we need to try again
717 char ch = seg.array[wordPosition];
718 if (! Character.isWhitespace(ch)) {
719 return lineStart + wordPosition - seg.offset;
720 }
721
722 // it was whitespace, try again. The assumption
723 // is that it must be a word start if the last
724 // one had whitespace following it.
725 wordPosition = words.next();
726 if (wordPosition != BreakIterator.DONE) {
727 offs = lineStart + wordPosition - seg.offset;
728 if (offs != lineEnd) {
729 return offs;
730 }
731 }
732 SegmentCache.releaseSharedSegment(seg);
733 return BreakIterator.DONE;
734 }
735
736
737 /**
738 * Determine the start of the prev word for the given location.
739 * Uses BreakIterator.getWordInstance() to actually get the words.
740 *
741 * @param c the editor
742 * @param offs the offset in the document >= 0
743 * @return the location in the model of the word start >= 0
744 * @exception BadLocationException if the offset is out of range
745 */
746 public static final int getPreviousWord(JTextComponent c, int offs) throws BadLocationException {
747 int prevWord;
748 Element line = getParagraphElement(c, offs);
749 for (prevWord = getPrevWordInParagraph(c, line, offs);
750 prevWord == BreakIterator.DONE;
751 prevWord = getPrevWordInParagraph(c, line, offs)) {
752
753 // didn't find in this line, try the prev line
754 offs = line.getStartOffset() - 1;
755 line = getParagraphElement(c, offs);
756 }
757 return prevWord;
758 }
759
760 /**
761 * Finds the previous word in the given elements text. The first
762 * parameter allows searching multiple paragraphs where even
763 * the first offset is desired.
764 * Returns the offset of the next word, or BreakIterator.DONE
765 * if there are no more words in the element.
766 */
767 static int getPrevWordInParagraph(JTextComponent c, Element line, int offs) throws BadLocationException {
768 if (line == null) {
769 throw new BadLocationException("No more words", offs);
770 }
771 Document doc = line.getDocument();
772 int lineStart = line.getStartOffset();
773 int lineEnd = line.getEndOffset();
774 if ((offs > lineEnd) || (offs < lineStart)) {
775 throw new BadLocationException("No more words", offs);
776 }
777 Segment seg = SegmentCache.getSharedSegment();
778 doc.getText(lineStart, lineEnd - lineStart, seg);
779 BreakIterator words = BreakIterator.getWordInstance(c.getLocale());
780 words.setText(seg);
781 if (words.following(seg.offset + offs - lineStart) == BreakIterator.DONE) {
782 words.last();
783 }
784 int wordPosition = words.previous();
785 if (wordPosition == (seg.offset + offs - lineStart)) {
786 wordPosition = words.previous();
787 }
788
789 if (wordPosition == BreakIterator.DONE) {
790 // there are no more words on this line.
791 return BreakIterator.DONE;
792 }
793 // if we haven't shot past the end... check to
794 // see if the current boundary represents whitespace.
795 // if so, we need to try again
796 char ch = seg.array[wordPosition];
797 if (! Character.isWhitespace(ch)) {
798 return lineStart + wordPosition - seg.offset;
799 }
800
801 // it was whitespace, try again. The assumption
802 // is that it must be a word start if the last
803 // one had whitespace following it.
804 wordPosition = words.previous();
805 if (wordPosition != BreakIterator.DONE) {
806 return lineStart + wordPosition - seg.offset;
807 }
808 SegmentCache.releaseSharedSegment(seg);
809 return BreakIterator.DONE;
810 }
811
812 /**
813 * Determines the element to use for a paragraph/line.
814 *
815 * @param c the editor
816 * @param offs the starting offset in the document >= 0
817 * @return the element
818 */
819 public static final Element getParagraphElement(JTextComponent c, int offs) {
820 Document doc = c.getDocument();
821 if (doc instanceof StyledDocument) {
822 return ((StyledDocument)doc).getParagraphElement(offs);
823 }
824 Element map = doc.getDefaultRootElement();
825 int index = map.getElementIndex(offs);
826 Element paragraph = map.getElement(index);
827 if ((offs >= paragraph.getStartOffset()) && (offs < paragraph.getEndOffset())) {
828 return paragraph;
829 }
830 return null;
831 }
832
833 static boolean isComposedTextElement(Document doc, int offset) {
834 Element elem = doc.getDefaultRootElement();
835 while (!elem.isLeaf()) {
836 elem = elem.getElement(elem.getElementIndex(offset));
837 }
838 return isComposedTextElement(elem);
839 }
840
841 static boolean isComposedTextElement(Element elem) {
842 AttributeSet as = elem.getAttributes();
843 return isComposedTextAttributeDefined(as);
844 }
845
846 static boolean isComposedTextAttributeDefined(AttributeSet as) {
847 return ((as != null) &&
848 (as.isDefined(StyleConstants.ComposedTextAttribute)));
849 }
850
851 /**
852 * Draws the given composed text passed from an input method.
853 *
854 * @param view View hosting text
855 * @param attr the attributes containing the composed text
856 * @param g the graphics context
857 * @param x the X origin
858 * @param y the Y origin
859 * @param p0 starting offset in the composed text to be rendered
860 * @param p1 ending offset in the composed text to be rendered
861 * @return the new insertion position
862 */
863 static int drawComposedText(View view, AttributeSet attr, Graphics g,
864 int x, int y, int p0, int p1)
865 throws BadLocationException {
866 Graphics2D g2d = (Graphics2D)g;
867 AttributedString as = (AttributedString)attr.getAttribute(
868 StyleConstants.ComposedTextAttribute);
869 as.addAttribute(TextAttribute.FONT, g.getFont());
870
871 if (p0 >= p1)
872 return x;
873
874 AttributedCharacterIterator aci = as.getIterator(null, p0, p1);
875 return x + (int)SwingUtilities2.drawString(
876 getJComponent(view), g2d,aci,x,y);
877 }
878
879 /**
880 * Paints the composed text in a GlyphView
881 */
882 static void paintComposedText(Graphics g, Rectangle alloc, GlyphView v) {
883 if (g instanceof Graphics2D) {
884 Graphics2D g2d = (Graphics2D) g;
885 int p0 = v.getStartOffset();
886 int p1 = v.getEndOffset();
887 AttributeSet attrSet = v.getElement().getAttributes();
888 AttributedString as =
889 (AttributedString)attrSet.getAttribute(StyleConstants.ComposedTextAttribute);
890 int start = v.getElement().getStartOffset();
891 int y = alloc.y + alloc.height - (int)v.getGlyphPainter().getDescent(v);
892 int x = alloc.x;
893
894 //Add text attributes
895 as.addAttribute(TextAttribute.FONT, v.getFont());
896 as.addAttribute(TextAttribute.FOREGROUND, v.getForeground());
897 if (StyleConstants.isBold(v.getAttributes())) {
898 as.addAttribute(TextAttribute.WEIGHT, TextAttribute.WEIGHT_BOLD);
899 }
900 if (StyleConstants.isItalic(v.getAttributes())) {
901 as.addAttribute(TextAttribute.POSTURE, TextAttribute.POSTURE_OBLIQUE);
902 }
903 if (v.isUnderline()) {
904 as.addAttribute(TextAttribute.UNDERLINE, TextAttribute.UNDERLINE_ON);
905 }
906 if (v.isStrikeThrough()) {
907 as.addAttribute(TextAttribute.STRIKETHROUGH, TextAttribute.STRIKETHROUGH_ON);
908 }
909 if (v.isSuperscript()) {
910 as.addAttribute(TextAttribute.SUPERSCRIPT, TextAttribute.SUPERSCRIPT_SUPER);
911 }
912 if (v.isSubscript()) {
913 as.addAttribute(TextAttribute.SUPERSCRIPT, TextAttribute.SUPERSCRIPT_SUB);
914 }
915
916 // draw
917 AttributedCharacterIterator aci = as.getIterator(null, p0 - start, p1 - start);
918 SwingUtilities2.drawString(getJComponent(v),
919 g2d,aci,x,y);
920 }
921 }
922
923 /*
924 * Convenience function for determining ComponentOrientation. Helps us
925 * avoid having Munge directives throughout the code.
926 */
927 static boolean isLeftToRight( java.awt.Component c ) {
928 return c.getComponentOrientation().isLeftToRight();
929 }
930
931
932 /**
933 * Provides a way to determine the next visually represented model
934 * location that one might place a caret. Some views may not be visible,
935 * they might not be in the same order found in the model, or they just
936 * might not allow access to some of the locations in the model.
937 * <p>
938 * This implementation assumes the views are layed out in a logical
939 * manner. That is, that the view at index x + 1 is visually after
940 * the View at index x, and that the View at index x - 1 is visually
941 * before the View at x. There is support for reversing this behavior
942 * only if the passed in <code>View</code> is an instance of
943 * <code>CompositeView</code>. The <code>CompositeView</code>
944 * must then override the <code>flipEastAndWestAtEnds</code> method.
945 *
946 * @param v View to query
947 * @param pos the position to convert >= 0
948 * @param a the allocated region to render into
949 * @param direction the direction from the current position that can
950 * be thought of as the arrow keys typically found on a keyboard;
951 * this may be one of the following:
952 * <ul>
953 * <li><code>SwingConstants.WEST</code>
954 * <li><code>SwingConstants.EAST</code>
955 * <li><code>SwingConstants.NORTH</code>
956 * <li><code>SwingConstants.SOUTH</code>
957 * </ul>
958 * @param biasRet an array contain the bias that was checked
959 * @return the location within the model that best represents the next
960 * location visual position
961 * @exception BadLocationException
962 * @exception IllegalArgumentException if <code>direction</code> is invalid
963 */
964 static int getNextVisualPositionFrom(View v, int pos, Position.Bias b,
965 Shape alloc, int direction,
966 Position.Bias[] biasRet)
967 throws BadLocationException {
968 if (v.getViewCount() == 0) {
969 // Nothing to do.
970 return pos;
971 }
972 boolean top = (direction == SwingConstants.NORTH ||
973 direction == SwingConstants.WEST);
974 int retValue;
975 if (pos == -1) {
976 // Start from the first View.
977 int childIndex = (top) ? v.getViewCount() - 1 : 0;
978 View child = v.getView(childIndex);
979 Shape childBounds = v.getChildAllocation(childIndex, alloc);
980 retValue = child.getNextVisualPositionFrom(pos, b, childBounds,
981 direction, biasRet);
982 if (retValue == -1 && !top && v.getViewCount() > 1) {
983 // Special case that should ONLY happen if first view
984 // isn't valid (can happen when end position is put at
985 // beginning of line.
986 child = v.getView(1);
987 childBounds = v.getChildAllocation(1, alloc);
988 retValue = child.getNextVisualPositionFrom(-1, biasRet[0],
989 childBounds,
990 direction, biasRet);
991 }
992 }
993 else {
994 int increment = (top) ? -1 : 1;
995 int childIndex;
996 if (b == Position.Bias.Backward && pos > 0) {
997 childIndex = v.getViewIndex(pos - 1, Position.Bias.Forward);
998 }
999 else {
1000 childIndex = v.getViewIndex(pos, Position.Bias.Forward);
1001 }
1002 View child = v.getView(childIndex);
1003 Shape childBounds = v.getChildAllocation(childIndex, alloc);
1004 retValue = child.getNextVisualPositionFrom(pos, b, childBounds,
1005 direction, biasRet);
1006 if ((direction == SwingConstants.EAST ||
1007 direction == SwingConstants.WEST) &&
1008 (v instanceof CompositeView) &&
1009 ((CompositeView)v).flipEastAndWestAtEnds(pos, b)) {
1010 increment *= -1;
1011 }
1012 childIndex += increment;
1013 if (retValue == -1 && childIndex >= 0 &&
1014 childIndex < v.getViewCount()) {
1015 child = v.getView(childIndex);
1016 childBounds = v.getChildAllocation(childIndex, alloc);
1017 retValue = child.getNextVisualPositionFrom(
1018 -1, b, childBounds, direction, biasRet);
1019 // If there is a bias change, it is a fake position
1020 // and we should skip it. This is usually the result
1021 // of two elements side be side flowing the same way.
1022 if (retValue == pos && biasRet[0] != b) {
1023 return getNextVisualPositionFrom(v, pos, biasRet[0],
1024 alloc, direction,
1025 biasRet);
1026 }
1027 }
1028 else if (retValue != -1 && biasRet[0] != b &&
1029 ((increment == 1 && child.getEndOffset() == retValue) ||
1030 (increment == -1 &&
1031 child.getStartOffset() == retValue)) &&
1032 childIndex >= 0 && childIndex < v.getViewCount()) {
1033 // Reached the end of a view, make sure the next view
1034 // is a different direction.
1035 child = v.getView(childIndex);
1036 childBounds = v.getChildAllocation(childIndex, alloc);
1037 Position.Bias originalBias = biasRet[0];
1038 int nextPos = child.getNextVisualPositionFrom(
1039 -1, b, childBounds, direction, biasRet);
1040 if (biasRet[0] == b) {
1041 retValue = nextPos;
1042 }
1043 else {
1044 biasRet[0] = originalBias;
1045 }
1046 }
1047 }
1048 return retValue;
1049 }
1050}