001/* Utilities.java --
002   Copyright (C) 2004, 2005, 2006  Free Software Foundation, Inc.
003
004This file is part of GNU Classpath.
005
006GNU Classpath is free software; you can redistribute it and/or modify
007it under the terms of the GNU General Public License as published by
008the Free Software Foundation; either version 2, or (at your option)
009any later version.
010
011GNU Classpath is distributed in the hope that it will be useful, but
012WITHOUT ANY WARRANTY; without even the implied warranty of
013MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
014General Public License for more details.
015
016You should have received a copy of the GNU General Public License
017along with GNU Classpath; see the file COPYING.  If not, write to the
018Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
01902110-1301 USA.
020
021Linking this library statically or dynamically with other modules is
022making a combined work based on this library.  Thus, the terms and
023conditions of the GNU General Public License cover the whole
024combination.
025
026As a special exception, the copyright holders of this library give you
027permission to link this library with independent modules to produce an
028executable, regardless of the license terms of these independent
029modules, and to copy and distribute the resulting executable under
030terms of your choice, provided that you also meet, for each linked
031independent module, the terms and conditions of the license of that
032module.  An independent module is a module which is not derived from
033or based on this library.  If you modify this library, you may extend
034this exception to your version of the library, but you are not
035obligated to do so.  If you do not wish to do so, delete this
036exception statement from your version. */
037
038
039package javax.swing.text;
040
041import java.awt.FontMetrics;
042import java.awt.Graphics;
043import java.awt.Point;
044import java.text.BreakIterator;
045
046import 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 */
055public 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}