OSDN Git Service

Imported GNU Classpath 0.90
[pf3gnuchains/gcc-fork.git] / libjava / classpath / javax / swing / text / Utilities.java
1 /* Utilities.java --
2    Copyright (C) 2004, 2005  Free Software Foundation, Inc.
3
4 This file is part of GNU Classpath.
5
6 GNU Classpath is free software; you can redistribute it and/or modify
7 it under the terms of the GNU General Public License as published by
8 the Free Software Foundation; either version 2, or (at your option)
9 any later version.
10
11 GNU Classpath is distributed in the hope that it will be useful, but
12 WITHOUT ANY WARRANTY; without even the implied warranty of
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14 General Public License for more details.
15
16 You should have received a copy of the GNU General Public License
17 along with GNU Classpath; see the file COPYING.  If not, write to the
18 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
19 02110-1301 USA.
20
21 Linking this library statically or dynamically with other modules is
22 making a combined work based on this library.  Thus, the terms and
23 conditions of the GNU General Public License cover the whole
24 combination.
25
26 As a special exception, the copyright holders of this library give you
27 permission to link this library with independent modules to produce an
28 executable, regardless of the license terms of these independent
29 modules, and to copy and distribute the resulting executable under
30 terms of your choice, provided that you also meet, for each linked
31 independent module, the terms and conditions of the license of that
32 module.  An independent module is a module which is not derived from
33 or based on this library.  If you modify this library, you may extend
34 this exception to your version of the library, but you are not
35 obligated to do so.  If you do not wish to do so, delete this
36 exception statement from your version. */
37
38
39 package javax.swing.text;
40
41 import java.awt.FontMetrics;
42 import java.awt.Graphics;
43 import java.awt.Point;
44 import java.text.BreakIterator;
45
46 /**
47  * A set of utilities to deal with text. This is used by several other classes
48  * inside this package.
49  *
50  * @author Roman Kennke (roman@ontographics.com)
51  */
52 public class Utilities
53 {
54   /**
55    * The length of the char buffer that holds the characters to be drawn.
56    */
57   private static final int BUF_LENGTH = 64;
58
59   /**
60    * Creates a new <code>Utilities</code> object.
61    */
62   public Utilities()
63   {
64     // Nothing to be done here.
65   }
66
67   /**
68    * Draws the given text segment. Contained tabs and newline characters
69    * are taken into account. Tabs are expanded using the
70    * specified {@link TabExpander}.
71    *
72    *
73    * The X and Y coordinates denote the start of the <em>baseline</em> where
74    * the text should be drawn.
75    *
76    * @param s the text fragment to be drawn.
77    * @param x the x position for drawing.
78    * @param y the y position for drawing.
79    * @param g the {@link Graphics} context for drawing.
80    * @param e the {@link TabExpander} which specifies the Tab-expanding
81    *     technique.
82    * @param startOffset starting offset in the text.
83    * @return the x coordinate at the end of the drawn text.
84    */
85   public static final int drawTabbedText(Segment s, int x, int y, Graphics g,
86                                          TabExpander e, int startOffset)
87   {
88     // This buffers the chars to be drawn.
89     char[] buffer = s.array;
90
91     // The font metrics of the current selected font.
92     FontMetrics metrics = g.getFontMetrics();
93     int ascent = metrics.getAscent();
94
95     // The current x and y pixel coordinates.
96     int pixelX = x;
97     int pixelY = y - ascent;
98
99     int pixelWidth = 0;
100     int pos = s.offset;
101     int len = 0;
102
103     for (int offset = s.offset; offset < (s.offset + s.count); ++offset)
104       {
105         char c = buffer[offset];
106         if (c == '\t' || c == '\n')
107           {
108             if (len > 0) {
109               g.drawChars(buffer, pos, len, pixelX, pixelY + ascent);            
110               pixelX += pixelWidth;
111               pixelWidth = 0;
112             }
113             pos = offset+1;
114             len = 0;
115           }
116         
117         switch (c)
118           {
119           case '\t':
120             // In case we have a tab, we just 'jump' over the tab.
121             // When we have no tab expander we just use the width of ' '.
122             if (e != null)
123               pixelX = (int) e.nextTabStop((float) pixelX,
124                                            startOffset + offset - s.offset);
125             else
126               pixelX += metrics.charWidth(' ');
127             break;
128           case '\n':
129             // In case we have a newline, we must jump to the next line.
130             pixelY += metrics.getHeight();
131             pixelX = x;
132             break;
133           default:
134             ++len;
135             pixelWidth += metrics.charWidth(buffer[offset]);
136             break;
137           }
138       }
139
140     if (len > 0)
141       g.drawChars(buffer, pos, len, pixelX, pixelY + ascent);            
142     
143     return pixelX;
144   }
145
146   /**
147    * Determines the width, that the given text <code>s</code> would take
148    * if it was printed with the given {@link java.awt.FontMetrics} on the
149    * specified screen position.
150    * @param s the text fragment
151    * @param metrics the font metrics of the font to be used
152    * @param x the x coordinate of the point at which drawing should be done
153    * @param e the {@link TabExpander} to be used
154    * @param startOffset the index in <code>s</code> where to start
155    * @returns the width of the given text s. This takes tabs and newlines
156    * into account.
157    */
158   public static final int getTabbedTextWidth(Segment s, FontMetrics metrics,
159                                              int x, TabExpander e,
160                                              int startOffset)
161   {
162     // This buffers the chars to be drawn.
163     char[] buffer = s.array;
164
165     // The current x coordinate.
166     int pixelX = x;
167
168     // The current maximum width.
169     int maxWidth = 0;
170
171     for (int offset = s.offset; offset < (s.offset + s.count); ++offset)
172       {
173         switch (buffer[offset])
174           {
175           case '\t':
176             // In case we have a tab, we just 'jump' over the tab.
177             // When we have no tab expander we just use the width of 'm'.
178             if (e != null)
179               pixelX = (int) e.nextTabStop((float) pixelX,
180                                            startOffset + offset - s.offset);
181             else
182               pixelX += metrics.charWidth(' ');
183             break;
184           case '\n':
185             // In case we have a newline, we must 'draw'
186             // the buffer and jump on the next line.
187             pixelX += metrics.charWidth(buffer[offset]);
188             maxWidth = Math.max(maxWidth, pixelX - x);
189             pixelX = x;
190             break;
191           default:
192             // Here we draw the char.
193             pixelX += metrics.charWidth(buffer[offset]);
194             break;
195           }
196       }
197
198     // Take the last line into account.
199     maxWidth = Math.max(maxWidth, pixelX - x);
200
201     return maxWidth;
202   }
203
204   /**
205    * Provides a facility to map screen coordinates into a model location. For a
206    * given text fragment and start location within this fragment, this method
207    * determines the model location so that the resulting fragment fits best
208    * into the span <code>[x0, x]</code>.
209    *
210    * The parameter <code>round</code> controls which model location is returned
211    * if the view coordinates are on a character: If <code>round</code> is
212    * <code>true</code>, then the result is rounded up to the next character, so
213    * that the resulting fragment is the smallest fragment that is larger than
214    * the specified span. If <code>round</code> is <code>false</code>, then the
215    * resulting fragment is the largest fragment that is smaller than the
216    * specified span.
217    *
218    * @param s the text segment
219    * @param fm the font metrics to use
220    * @param x0 the starting screen location
221    * @param x the target screen location at which the requested fragment should
222    *        end
223    * @param te the tab expander to use; if this is <code>null</code>, TABs are
224    *        expanded to one space character
225    * @param p0 the starting model location
226    * @param round if <code>true</code> round up to the next location, otherwise
227    *        round down to the current location
228    *
229    * @return the model location, so that the resulting fragment fits within the
230    *         specified span
231    */
232   public static final int getTabbedTextOffset(Segment s, FontMetrics fm, int x0,
233                                               int x, TabExpander te, int p0,
234                                               boolean round)
235   {
236     // At the end of the for loop, this holds the requested model location
237     int pos;
238     int currentX = x0;
239
240     for (pos = 0; pos < s.count; pos++)
241       {
242         char nextChar = s.array[s.offset+pos];
243         
244         if (nextChar == 0)
245           {
246             if (! round)
247               pos--;
248             break;
249           }
250         if (nextChar != '\t')
251           currentX += fm.charWidth(nextChar);
252         else
253           {
254             if (te == null)
255               currentX += fm.charWidth(' ');
256             else
257               currentX = (int) te.nextTabStop(currentX, pos);
258           }
259         
260         if (currentX > x)
261           {
262             if (! round)
263               pos--;
264             break;
265           }
266       }
267     
268     return pos + p0;
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     int last = wb.following(offs);
319     int current = wb.next();
320     while (current != BreakIterator.DONE)
321       {
322         for (int i = last; i < current; i++)
323           {
324             // FIXME: Should use isLetter(int) and text.codePointAt(int)
325             // instead, but isLetter(int) isn't implemented yet
326             if (Character.isLetter(text.charAt(i)))
327               return last;
328           }
329         last = current;
330         current = wb.next();
331       }
332     return BreakIterator.DONE;
333   }
334
335   /**
336    * Finds the start of the previous word for the given offset.
337    * 
338    * @param c
339    *          the text component
340    * @param offs
341    *          the offset in the document
342    * @return the location in the model of the start of the previous word.
343    * @throws BadLocationException
344    *           if the offset is invalid.
345    */
346   public static final int getPreviousWord(JTextComponent c, int offs)
347       throws BadLocationException
348   {
349     if (offs < 0 || offs > (c.getText().length() - 1))
350       throw new BadLocationException("invalid offset specified", offs);
351     String text = c.getText();
352     BreakIterator wb = BreakIterator.getWordInstance();
353     wb.setText(text);
354     int last = wb.preceding(offs);
355     int current = wb.previous();
356
357     while (current != BreakIterator.DONE)
358       {
359         for (int i = last; i < offs; i++)
360           {
361             // FIXME: Should use isLetter(int) and text.codePointAt(int)
362             // instead, but isLetter(int) isn't implemented yet
363             if (Character.isLetter(text.charAt(i)))
364               return last;
365           }
366         last = current;
367         current = wb.previous();
368       }
369     return 0;
370   }
371   
372   /**
373    * Finds the start of a word for the given location.
374    * @param c the text component
375    * @param offs the offset location
376    * @return the location of the word beginning
377    * @throws BadLocationException if the offset location is invalid
378    */
379   public static final int getWordStart(JTextComponent c, int offs)
380       throws BadLocationException
381   {
382     if (offs < 0 || offs >= c.getText().length())
383       throw new BadLocationException("invalid offset specified", offs);
384     
385     String text = c.getText();
386     BreakIterator wb = BreakIterator.getWordInstance();
387     wb.setText(text);
388     if (wb.isBoundary(offs))
389       return offs;
390     return wb.preceding(offs);
391   }
392   
393   /**
394    * Finds the end of a word for the given location.
395    * @param c the text component
396    * @param offs the offset location
397    * @return the location of the word end
398    * @throws BadLocationException if the offset location is invalid
399    */
400   public static final int getWordEnd(JTextComponent c, int offs)
401       throws BadLocationException
402   {
403     if (offs < 0 || offs >= c.getText().length())
404       throw new BadLocationException("invalid offset specified", offs);
405     
406     String text = c.getText();
407     BreakIterator wb = BreakIterator.getWordInstance();
408     wb.setText(text);
409     return wb.following(offs);
410   }
411   
412   /**
413    * Get the model position of the end of the row that contains the 
414    * specified model position.  Return null if the given JTextComponent
415    * does not have a size.
416    * @param c the JTextComponent
417    * @param offs the model position
418    * @return the model position of the end of the row containing the given 
419    * offset
420    * @throws BadLocationException if the offset is invalid
421    */
422   public static final int getRowEnd(JTextComponent c, int offs)
423       throws BadLocationException
424   {
425     String text = c.getText();
426     if (text == null)
427       return -1;
428
429     // Do a binary search for the smallest position X > offs
430     // such that that character at positino X is not on the same
431     // line as the character at position offs
432     int high = offs + ((text.length() - 1 - offs) / 2);
433     int low = offs;
434     int oldHigh = text.length() + 1;
435     while (true)
436       {
437         if (c.modelToView(high).y != c.modelToView(offs).y)
438           {
439             oldHigh = high;
440             high = low + ((high + 1 - low) / 2);
441             if (oldHigh == high)
442               return high - 1;
443           }
444         else
445           {
446             low = high;
447             high += ((oldHigh - high) / 2);
448             if (low == high)
449               return low;
450           }
451       }
452   }
453       
454   /**
455    * Get the model position of the start of the row that contains the specified
456    * model position. Return null if the given JTextComponent does not have a
457    * size.
458    * 
459    * @param c the JTextComponent
460    * @param offs the model position
461    * @return the model position of the start of the row containing the given
462    *         offset
463    * @throws BadLocationException if the offset is invalid
464    */
465   public static final int getRowStart(JTextComponent c, int offs)
466       throws BadLocationException
467   {
468     String text = c.getText();
469     if (text == null)
470       return -1;
471
472     // Do a binary search for the greatest position X < offs
473     // such that the character at position X is not on the same
474     // row as the character at position offs
475     int high = offs;
476     int low = 0;
477     int oldLow = 0;
478     while (true)
479       {
480         if (c.modelToView(low).y != c.modelToView(offs).y)
481           {
482             oldLow = low;
483             low = high - ((high + 1 - low) / 2);
484             if (oldLow == low)
485               return low + 1;
486           }
487         else
488           {
489             high = low;
490             low -= ((low - oldLow) / 2);
491             if (low == high)
492               return low;
493           }
494       }
495   }
496   
497   /**
498    * Determine where to break the text in the given Segment, attempting to find
499    * a word boundary.
500    * @param s the Segment that holds the text
501    * @param metrics the font metrics used for calculating the break point
502    * @param x0 starting view location representing the start of the text
503    * @param x the target view location
504    * @param e the TabExpander used for expanding tabs (if this is null tabs
505    * are expanded to 1 space)
506    * @param startOffset the offset in the Document of the start of the text
507    * @return the offset at which we should break the text
508    */
509   public static final int getBreakLocation(Segment s, FontMetrics metrics,
510                                            int x0, int x, TabExpander e,
511                                            int startOffset)
512   {
513     int mark = Utilities.getTabbedTextOffset(s, metrics, x0, x, e, startOffset);
514     BreakIterator breaker = BreakIterator.getWordInstance();
515     breaker.setText(s);
516
517     // If mark is equal to the end of the string, just use that position
518     if (mark == s.count + s.offset)
519       return mark;
520     
521     // Try to find a word boundary previous to the mark at which we 
522     // can break the text
523     int preceding = breaker.preceding(mark + 1);
524     
525     if (preceding != 0)
526       return preceding;
527     else
528       // If preceding is 0 we couldn't find a suitable word-boundary so
529       // just break it on the character boundary
530       return mark;
531   }
532
533   /**
534    * Returns the paragraph element in the text component <code>c</code> at
535    * the specified location <code>offset</code>.
536    *
537    * @param c the text component
538    * @param offset the offset of the paragraph element to return
539    *
540    * @return the paragraph element at <code>offset</code>
541    */
542   public static final Element getParagraphElement(JTextComponent c, int offset)
543   {
544     Document doc = c.getDocument();
545     Element par = null;
546     if (doc instanceof StyledDocument)
547       {
548         StyledDocument styledDoc = (StyledDocument) doc;
549         par = styledDoc.getParagraphElement(offset);
550       }
551     else
552       {
553         Element root = c.getDocument().getDefaultRootElement();
554         int parIndex = root.getElementIndex(offset);
555         par = root.getElement(parIndex);
556       }
557     return par;
558   }
559
560   /**
561    * Returns the document position that is closest above to the specified x
562    * coordinate in the row containing <code>offset</code>.
563    *
564    * @param c the text component
565    * @param offset the offset
566    * @param x the x coordinate
567    *
568    * @return  the document position that is closest above to the specified x
569    *          coordinate in the row containing <code>offset</code>
570    *
571    * @throws BadLocationException if <code>offset</code> is not a valid offset
572    */
573   public static final int getPositionAbove(JTextComponent c, int offset, int x)
574     throws BadLocationException
575   {
576     int offs = getRowStart(c, offset);
577     
578     if(offs == -1)
579       return -1;
580
581     // Effectively calculates the y value of the previous line.
582     Point pt = c.modelToView(offs-1).getLocation();
583     
584     pt.x = x;
585     
586     // Calculate a simple fitting offset.
587     offs = c.viewToModel(pt);
588     
589     // Find out the real x positions of the calculated character and its
590     // neighbour.
591     int offsX = c.modelToView(offs).getLocation().x;
592     int offsXNext = c.modelToView(offs+1).getLocation().x;
593     
594     // Chose the one which is nearer to us and return its offset.
595     if (Math.abs(offsX-x) <= Math.abs(offsXNext-x))
596       return offs;
597     else
598       return offs+1;
599   }
600
601   /**
602    * Returns the document position that is closest below to the specified x
603    * coordinate in the row containing <code>offset</code>.
604    *
605    * @param c the text component
606    * @param offset the offset
607    * @param x the x coordinate
608    *
609    * @return  the document position that is closest above to the specified x
610    *          coordinate in the row containing <code>offset</code>
611    *
612    * @throws BadLocationException if <code>offset</code> is not a valid offset
613    */
614   public static final int getPositionBelow(JTextComponent c, int offset, int x)
615     throws BadLocationException
616   {
617     int offs = getRowEnd(c, offset);
618     
619     if(offs == -1)
620       return -1;
621
622     // Effectively calculates the y value of the previous line.
623     Point pt = c.modelToView(offs+1).getLocation();
624     
625     pt.x = x;
626     
627     // Calculate a simple fitting offset.
628     offs = c.viewToModel(pt);
629     
630     if (offs == c.getDocument().getLength())
631       return offs;
632
633     // Find out the real x positions of the calculated character and its
634     // neighbour.
635     int offsX = c.modelToView(offs).getLocation().x;
636     int offsXNext = c.modelToView(offs+1).getLocation().x;
637     
638     // Chose the one which is nearer to us and return its offset.
639     if (Math.abs(offsX-x) <= Math.abs(offsXNext-x))
640       return offs;
641     else
642       return offs+1;
643     }
644 }