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    }