001 /* Utilities.java --
002 Copyright (C) 2004, 2005, 2006 Free Software Foundation, Inc.
003
004 This file is part of GNU Classpath.
005
006 GNU Classpath is free software; you can redistribute it and/or modify
007 it under the terms of the GNU General Public License as published by
008 the Free Software Foundation; either version 2, or (at your option)
009 any later version.
010
011 GNU Classpath is distributed in the hope that it will be useful, but
012 WITHOUT ANY WARRANTY; without even the implied warranty of
013 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
014 General Public License for more details.
015
016 You should have received a copy of the GNU General Public License
017 along with GNU Classpath; see the file COPYING. If not, write to the
018 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
019 02110-1301 USA.
020
021 Linking this library statically or dynamically with other modules is
022 making a combined work based on this library. Thus, the terms and
023 conditions of the GNU General Public License cover the whole
024 combination.
025
026 As a special exception, the copyright holders of this library give you
027 permission to link this library with independent modules to produce an
028 executable, regardless of the license terms of these independent
029 modules, and to copy and distribute the resulting executable under
030 terms of your choice, provided that you also meet, for each linked
031 independent module, the terms and conditions of the license of that
032 module. An independent module is a module which is not derived from
033 or based on this library. If you modify this library, you may extend
034 this exception to your version of the library, but you are not
035 obligated to do so. If you do not wish to do so, delete this
036 exception statement from your version. */
037
038
039 package javax.swing.text;
040
041 import java.awt.FontMetrics;
042 import java.awt.Graphics;
043 import java.awt.Point;
044 import java.text.BreakIterator;
045
046 import javax.swing.text.Position.Bias;
047
048 /**
049 * A set of utilities to deal with text. This is used by several other classes
050 * inside this package.
051 *
052 * @author Roman Kennke (roman@ontographics.com)
053 * @author Robert Schuster (robertschuster@fsfe.org)
054 */
055 public class Utilities
056 {
057
058 /**
059 * Creates a new <code>Utilities</code> object.
060 */
061 public Utilities()
062 {
063 // Nothing to be done here.
064 }
065
066 /**
067 * Draws the given text segment. Contained tabs and newline characters
068 * are taken into account. Tabs are expanded using the
069 * specified {@link TabExpander}.
070 *
071 *
072 * The X and Y coordinates denote the start of the <em>baseline</em> where
073 * the text should be drawn.
074 *
075 * @param s the text fragment to be drawn.
076 * @param x the x position for drawing.
077 * @param y the y position for drawing.
078 * @param g the {@link Graphics} context for drawing.
079 * @param e the {@link TabExpander} which specifies the Tab-expanding
080 * technique.
081 * @param startOffset starting offset in the text.
082 * @return the x coordinate at the end of the drawn text.
083 */
084 public static final int drawTabbedText(Segment s, int x, int y, Graphics g,
085 TabExpander e, int startOffset)
086 {
087 // This buffers the chars to be drawn.
088 char[] buffer = s.array;
089
090 // The font metrics of the current selected font.
091 FontMetrics metrics = g.getFontMetrics();
092
093 int ascent = metrics.getAscent();
094
095 // The current x and y pixel coordinates.
096 int pixelX = x;
097
098 int pos = s.offset;
099 int len = 0;
100
101 int end = s.offset + s.count;
102
103 for (int offset = s.offset; offset < end; ++offset)
104 {
105 char c = buffer[offset];
106 switch (c)
107 {
108 case '\t':
109 if (len > 0) {
110 g.drawChars(buffer, pos, len, pixelX, y);
111 pixelX += metrics.charsWidth(buffer, pos, len);
112 len = 0;
113 }
114 pos = offset+1;
115 if (e != null)
116 pixelX = (int) e.nextTabStop((float) pixelX, startOffset + offset
117 - s.offset);
118 else
119 pixelX += metrics.charWidth(' ');
120 x = pixelX;
121 break;
122 case '\n':
123 case '\r':
124 if (len > 0) {
125 g.drawChars(buffer, pos, len, pixelX, y);
126 pixelX += metrics.charsWidth(buffer, pos, len);
127 len = 0;
128 }
129 x = pixelX;
130 break;
131 default:
132 len += 1;
133 }
134 }
135
136 if (len > 0)
137 {
138 g.drawChars(buffer, pos, len, pixelX, y);
139 pixelX += metrics.charsWidth(buffer, pos, len);
140 }
141
142 return pixelX;
143 }
144
145 /**
146 * Determines the width, that the given text <code>s</code> would take
147 * if it was printed with the given {@link java.awt.FontMetrics} on the
148 * specified screen position.
149 * @param s the text fragment
150 * @param metrics the font metrics of the font to be used
151 * @param x the x coordinate of the point at which drawing should be done
152 * @param e the {@link TabExpander} to be used
153 * @param startOffset the index in <code>s</code> where to start
154 * @returns the width of the given text s. This takes tabs and newlines
155 * into account.
156 */
157 public static final int getTabbedTextWidth(Segment s, FontMetrics metrics,
158 int x, TabExpander e,
159 int startOffset)
160 {
161 // This buffers the chars to be drawn.
162 char[] buffer = s.array;
163
164 // The current x coordinate.
165 int pixelX = x;
166
167 // The current maximum width.
168 int maxWidth = 0;
169
170 int end = s.offset + s.count;
171 int count = 0;
172 for (int offset = s.offset; offset < end; offset++)
173 {
174 switch (buffer[offset])
175 {
176 case '\t':
177 // In case we have a tab, we just 'jump' over the tab.
178 // When we have no tab expander we just use the width of 'm'.
179 if (e != null)
180 pixelX = (int) e.nextTabStop(pixelX,
181 startOffset + offset - s.offset);
182 else
183 pixelX += metrics.charWidth(' ');
184 break;
185 case '\n':
186 // In case we have a newline, we must 'draw'
187 // the buffer and jump on the next line.
188 pixelX += metrics.charsWidth(buffer, offset - count, count);
189 count = 0;
190 break;
191 default:
192 count++;
193 }
194 }
195
196 // Take the last line into account.
197 pixelX += metrics.charsWidth(buffer, end - count, count);
198
199 return pixelX - x;
200 }
201
202 /**
203 * Provides a facility to map screen coordinates into a model location. For a
204 * given text fragment and start location within this fragment, this method
205 * determines the model location so that the resulting fragment fits best
206 * into the span <code>[x0, x]</code>.
207 *
208 * The parameter <code>round</code> controls which model location is returned
209 * if the view coordinates are on a character: If <code>round</code> is
210 * <code>true</code>, then the result is rounded up to the next character, so
211 * that the resulting fragment is the smallest fragment that is larger than
212 * the specified span. If <code>round</code> is <code>false</code>, then the
213 * resulting fragment is the largest fragment that is smaller than the
214 * specified span.
215 *
216 * @param s the text segment
217 * @param fm the font metrics to use
218 * @param x0 the starting screen location
219 * @param x the target screen location at which the requested fragment should
220 * end
221 * @param te the tab expander to use; if this is <code>null</code>, TABs are
222 * expanded to one space character
223 * @param p0 the starting model location
224 * @param round if <code>true</code> round up to the next location, otherwise
225 * round down to the current location
226 *
227 * @return the model location, so that the resulting fragment fits within the
228 * specified span
229 */
230 public static final int getTabbedTextOffset(Segment s, FontMetrics fm, int x0,
231 int x, TabExpander te, int p0,
232 boolean round)
233 {
234 int found = s.count;
235 int currentX = x0;
236 int nextX = currentX;
237
238 int end = s.offset + s.count;
239 for (int pos = s.offset; pos < end && found == s.count; pos++)
240 {
241 char nextChar = s.array[pos];
242
243 if (nextChar != '\t')
244 nextX += fm.charWidth(nextChar);
245 else
246 {
247 if (te == null)
248 nextX += fm.charWidth(' ');
249 else
250 nextX += ((int) te.nextTabStop(nextX, p0 + pos - s.offset));
251 }
252
253 if (x >= currentX && x < nextX)
254 {
255 // Found position.
256 if ((! round) || ((x - currentX) < (nextX - x)))
257 {
258 found = pos - s.offset;
259 }
260 else
261 {
262 found = pos + 1 - s.offset;
263 }
264 }
265 currentX = nextX;
266 }
267
268 return found;
269 }
270
271 /**
272 * Provides a facility to map screen coordinates into a model location. For a
273 * given text fragment and start location within this fragment, this method
274 * determines the model location so that the resulting fragment fits best
275 * into the span <code>[x0, x]</code>.
276 *
277 * This method rounds up to the next location, so that the resulting fragment
278 * will be the smallest fragment of the text, that is greater than the
279 * specified span.
280 *
281 * @param s the text segment
282 * @param fm the font metrics to use
283 * @param x0 the starting screen location
284 * @param x the target screen location at which the requested fragment should
285 * end
286 * @param te the tab expander to use; if this is <code>null</code>, TABs are
287 * expanded to one space character
288 * @param p0 the starting model location
289 *
290 * @return the model location, so that the resulting fragment fits within the
291 * specified span
292 */
293 public static final int getTabbedTextOffset(Segment s, FontMetrics fm, int x0,
294 int x, TabExpander te, int p0)
295 {
296 return getTabbedTextOffset(s, fm, x0, x, te, p0, true);
297 }
298
299 /**
300 * Finds the start of the next word for the given offset.
301 *
302 * @param c
303 * the text component
304 * @param offs
305 * the offset in the document
306 * @return the location in the model of the start of the next word.
307 * @throws BadLocationException
308 * if the offset is invalid.
309 */
310 public static final int getNextWord(JTextComponent c, int offs)
311 throws BadLocationException
312 {
313 if (offs < 0 || offs > (c.getText().length() - 1))
314 throw new BadLocationException("invalid offset specified", offs);
315 String text = c.getText();
316 BreakIterator wb = BreakIterator.getWordInstance();
317 wb.setText(text);
318
319 int last = wb.following(offs);
320 int current = wb.next();
321 int cp;
322
323 while (current != BreakIterator.DONE)
324 {
325 for (int i = last; i < current; i++)
326 {
327 cp = text.codePointAt(i);
328
329 // Return the last found bound if there is a letter at the current
330 // location or is not whitespace (meaning it is a number or
331 // punctuation). The first case means that 'last' denotes the
332 // beginning of a word while the second case means it is the start
333 // of something else.
334 if (Character.isLetter(cp)
335 || !Character.isWhitespace(cp))
336 return last;
337 }
338 last = current;
339 current = wb.next();
340 }
341
342 throw new BadLocationException("no more words", offs);
343 }
344
345 /**
346 * Finds the start of the previous word for the given offset.
347 *
348 * @param c
349 * the text component
350 * @param offs
351 * the offset in the document
352 * @return the location in the model of the start of the previous word.
353 * @throws BadLocationException
354 * if the offset is invalid.
355 */
356 public static final int getPreviousWord(JTextComponent c, int offs)
357 throws BadLocationException
358 {
359 String text = c.getText();
360
361 if (offs <= 0 || offs > text.length())
362 throw new BadLocationException("invalid offset specified", offs);
363
364 BreakIterator wb = BreakIterator.getWordInstance();
365 wb.setText(text);
366 int last = wb.preceding(offs);
367 int current = wb.previous();
368 int cp;
369
370 while (current != BreakIterator.DONE)
371 {
372 for (int i = last; i < offs; i++)
373 {
374 cp = text.codePointAt(i);
375
376 // Return the last found bound if there is a letter at the current
377 // location or is not whitespace (meaning it is a number or
378 // punctuation). The first case means that 'last' denotes the
379 // beginning of a word while the second case means it is the start
380 // of some else.
381 if (Character.isLetter(cp)
382 || !Character.isWhitespace(cp))
383 return last;
384 }
385 last = current;
386 current = wb.previous();
387 }
388
389 return 0;
390 }
391
392 /**
393 * Finds the start of a word for the given location.
394 * @param c the text component
395 * @param offs the offset location
396 * @return the location of the word beginning
397 * @throws BadLocationException if the offset location is invalid
398 */
399 public static final int getWordStart(JTextComponent c, int offs)
400 throws BadLocationException
401 {
402 String text = c.getText();
403
404 if (offs < 0 || offs > text.length())
405 throw new BadLocationException("invalid offset specified", offs);
406
407 BreakIterator wb = BreakIterator.getWordInstance();
408 wb.setText(text);
409
410 if (wb.isBoundary(offs))
411 return offs;
412
413 return wb.preceding(offs);
414 }
415
416 /**
417 * Finds the end of a word for the given location.
418 * @param c the text component
419 * @param offs the offset location
420 * @return the location of the word end
421 * @throws BadLocationException if the offset location is invalid
422 */
423 public static final int getWordEnd(JTextComponent c, int offs)
424 throws BadLocationException
425 {
426 if (offs < 0 || offs >= c.getText().length())
427 throw new BadLocationException("invalid offset specified", offs);
428
429 String text = c.getText();
430 BreakIterator wb = BreakIterator.getWordInstance();
431 wb.setText(text);
432 return wb.following(offs);
433 }
434
435 /**
436 * Get the model position of the end of the row that contains the
437 * specified model position. Return null if the given JTextComponent
438 * does not have a size.
439 * @param c the JTextComponent
440 * @param offs the model position
441 * @return the model position of the end of the row containing the given
442 * offset
443 * @throws BadLocationException if the offset is invalid
444 */
445 public static final int getRowEnd(JTextComponent c, int offs)
446 throws BadLocationException
447 {
448 String text = c.getText();
449 if (text == null)
450 return -1;
451
452 // Do a binary search for the smallest position X > offs
453 // such that that character at positino X is not on the same
454 // line as the character at position offs
455 int high = offs + ((text.length() - 1 - offs) / 2);
456 int low = offs;
457 int oldHigh = text.length() + 1;
458 while (true)
459 {
460 if (c.modelToView(high).y != c.modelToView(offs).y)
461 {
462 oldHigh = high;
463 high = low + ((high + 1 - low) / 2);
464 if (oldHigh == high)
465 return high - 1;
466 }
467 else
468 {
469 low = high;
470 high += ((oldHigh - high) / 2);
471 if (low == high)
472 return low;
473 }
474 }
475 }
476
477 /**
478 * Get the model position of the start of the row that contains the specified
479 * model position. Return null if the given JTextComponent does not have a
480 * size.
481 *
482 * @param c the JTextComponent
483 * @param offs the model position
484 * @return the model position of the start of the row containing the given
485 * offset
486 * @throws BadLocationException if the offset is invalid
487 */
488 public static final int getRowStart(JTextComponent c, int offs)
489 throws BadLocationException
490 {
491 String text = c.getText();
492 if (text == null)
493 return -1;
494
495 // Do a binary search for the greatest position X < offs
496 // such that the character at position X is not on the same
497 // row as the character at position offs
498 int high = offs;
499 int low = 0;
500 int oldLow = 0;
501 while (true)
502 {
503 if (c.modelToView(low).y != c.modelToView(offs).y)
504 {
505 oldLow = low;
506 low = high - ((high + 1 - low) / 2);
507 if (oldLow == low)
508 return low + 1;
509 }
510 else
511 {
512 high = low;
513 low -= ((low - oldLow) / 2);
514 if (low == high)
515 return low;
516 }
517 }
518 }
519
520 /**
521 * Determine where to break the text in the given Segment, attempting to find
522 * a word boundary.
523 * @param s the Segment that holds the text
524 * @param metrics the font metrics used for calculating the break point
525 * @param x0 starting view location representing the start of the text
526 * @param x the target view location
527 * @param e the TabExpander used for expanding tabs (if this is null tabs
528 * are expanded to 1 space)
529 * @param startOffset the offset in the Document of the start of the text
530 * @return the offset at which we should break the text
531 */
532 public static final int getBreakLocation(Segment s, FontMetrics metrics,
533 int x0, int x, TabExpander e,
534 int startOffset)
535 {
536 int mark = Utilities.getTabbedTextOffset(s, metrics, x0, x, e, startOffset,
537 false);
538 int breakLoc = mark;
539 // If mark is equal to the end of the string, just use that position.
540 if (mark < s.count - 1)
541 {
542 for (int i = s.offset + mark; i >= s.offset; i--)
543 {
544 char ch = s.array[i];
545 if (ch < 256)
546 {
547 // For ASCII simply scan backwards for whitespace.
548 if (Character.isWhitespace(ch))
549 {
550 breakLoc = i - s.offset + 1;
551 break;
552 }
553 }
554 else
555 {
556 // Only query BreakIterator for complex chars.
557 BreakIterator bi = BreakIterator.getLineInstance();
558 bi.setText(s);
559 int pos = bi.preceding(i + 1);
560 if (pos > s.offset)
561 {
562 breakLoc = breakLoc - s.offset;
563 }
564 break;
565 }
566 }
567 }
568 return breakLoc;
569 }
570
571 /**
572 * Returns the paragraph element in the text component <code>c</code> at
573 * the specified location <code>offset</code>.
574 *
575 * @param c the text component
576 * @param offset the offset of the paragraph element to return
577 *
578 * @return the paragraph element at <code>offset</code>
579 */
580 public static final Element getParagraphElement(JTextComponent c, int offset)
581 {
582 Document doc = c.getDocument();
583 Element par = null;
584 if (doc instanceof StyledDocument)
585 {
586 StyledDocument styledDoc = (StyledDocument) doc;
587 par = styledDoc.getParagraphElement(offset);
588 }
589 else
590 {
591 Element root = c.getDocument().getDefaultRootElement();
592 int parIndex = root.getElementIndex(offset);
593 par = root.getElement(parIndex);
594 }
595 return par;
596 }
597
598 /**
599 * Returns the document position that is closest above to the specified x
600 * coordinate in the row containing <code>offset</code>.
601 *
602 * @param c the text component
603 * @param offset the offset
604 * @param x the x coordinate
605 *
606 * @return the document position that is closest above to the specified x
607 * coordinate in the row containing <code>offset</code>
608 *
609 * @throws BadLocationException if <code>offset</code> is not a valid offset
610 */
611 public static final int getPositionAbove(JTextComponent c, int offset, int x)
612 throws BadLocationException
613 {
614 int offs = getRowStart(c, offset);
615
616 if(offs == -1)
617 return -1;
618
619 // Effectively calculates the y value of the previous line.
620 Point pt = c.modelToView(offs-1).getLocation();
621
622 pt.x = x;
623
624 // Calculate a simple fitting offset.
625 offs = c.viewToModel(pt);
626
627 // Find out the real x positions of the calculated character and its
628 // neighbour.
629 int offsX = c.modelToView(offs).getLocation().x;
630 int offsXNext = c.modelToView(offs+1).getLocation().x;
631
632 // Chose the one which is nearer to us and return its offset.
633 if (Math.abs(offsX-x) <= Math.abs(offsXNext-x))
634 return offs;
635 else
636 return offs+1;
637 }
638
639 /**
640 * Returns the document position that is closest below to the specified x
641 * coordinate in the row containing <code>offset</code>.
642 *
643 * @param c the text component
644 * @param offset the offset
645 * @param x the x coordinate
646 *
647 * @return the document position that is closest above to the specified x
648 * coordinate in the row containing <code>offset</code>
649 *
650 * @throws BadLocationException if <code>offset</code> is not a valid offset
651 */
652 public static final int getPositionBelow(JTextComponent c, int offset, int x)
653 throws BadLocationException
654 {
655 int offs = getRowEnd(c, offset);
656
657 if(offs == -1)
658 return -1;
659
660 Point pt = null;
661
662 // Note: Some views represent the position after the last
663 // typed character others do not. Converting offset 3 in "a\nb"
664 // in a PlainView will return a valid rectangle while in a
665 // WrappedPlainView this will throw a BadLocationException.
666 // This behavior has been observed in the RI.
667 try
668 {
669 // Effectively calculates the y value of the next line.
670 pt = c.modelToView(offs+1).getLocation();
671 }
672 catch(BadLocationException ble)
673 {
674 return offset;
675 }
676
677 pt.x = x;
678
679 // Calculate a simple fitting offset.
680 offs = c.viewToModel(pt);
681
682 if (offs == c.getDocument().getLength())
683 return offs;
684
685 // Find out the real x positions of the calculated character and its
686 // neighbour.
687 int offsX = c.modelToView(offs).getLocation().x;
688 int offsXNext = c.modelToView(offs+1).getLocation().x;
689
690 // Chose the one which is nearer to us and return its offset.
691 if (Math.abs(offsX-x) <= Math.abs(offsXNext-x))
692 return offs;
693 else
694 return offs+1;
695 }
696
697 /** This is an internal helper method which is used by the
698 * <code>javax.swing.text</code> package. It simply delegates the
699 * call to a method with the same name on the <code>NavigationFilter</code>
700 * of the provided <code>JTextComponent</code> (if it has one) or its UI.
701 *
702 * If the underlying method throws a <code>BadLocationException</code> it
703 * will be swallowed and the initial offset is returned.
704 */
705 static int getNextVisualPositionFrom(JTextComponent t, int offset, int direction)
706 {
707 NavigationFilter nf = t.getNavigationFilter();
708
709 try
710 {
711 return (nf != null)
712 ? nf.getNextVisualPositionFrom(t,
713 offset,
714 Bias.Forward,
715 direction,
716 new Position.Bias[1])
717 : t.getUI().getNextVisualPositionFrom(t,
718 offset,
719 Bias.Forward,
720 direction,
721 new Position.Bias[1]);
722 }
723 catch (BadLocationException ble)
724 {
725 return offset;
726 }
727
728 }
729
730 }