Source for javax.swing.text.Utilities

   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.text.BreakIterator;
  44: 
  45: /**
  46:  * A set of utilities to deal with text. This is used by several other classes
  47:  * inside this package.
  48:  *
  49:  * @author Roman Kennke (roman@ontographics.com)
  50:  */
  51: public class Utilities
  52: {
  53:   /**
  54:    * The length of the char buffer that holds the characters to be drawn.
  55:    */
  56:   private static final int BUF_LENGTH = 64;
  57: 
  58:   /**
  59:    * Creates a new <code>Utilities</code> object.
  60:    */
  61:   public Utilities()
  62:   {
  63:     // Nothing to be done here.
  64:   }
  65: 
  66:   /**
  67:    * Draws the given text segment. Contained tabs and newline characters
  68:    * are taken into account. Tabs are expanded using the
  69:    * specified {@link TabExpander}.
  70:    *
  71:    * @param s the text fragment to be drawn.
  72:    * @param x the x position for drawing.
  73:    * @param y the y position for drawing.
  74:    * @param g the {@link Graphics} context for drawing.
  75:    * @param e the {@link TabExpander} which specifies the Tab-expanding
  76:    *     technique.
  77:    * @param startOffset starting offset in the text.
  78:    * @return the x coordinate at the end of the drawn text.
  79:    */
  80:   public static final int drawTabbedText(Segment s, int x, int y, Graphics g,
  81:                                          TabExpander e, int startOffset)
  82:   {
  83:     // This buffers the chars to be drawn.
  84:     char[] buffer = s.array;
  85: 
  86: 
  87:     // The current x and y pixel coordinates.
  88:     int pixelX = x;
  89:     int pixelY = y;
  90: 
  91:     // The font metrics of the current selected font.
  92:     FontMetrics metrics = g.getFontMetrics();
  93:     int ascent = metrics.getAscent();
  94: 
  95:     int pixelWidth = 0;
  96:     int pos = s.offset;
  97:     int len = 0;
  98: 
  99:     for (int offset = s.offset; offset < (s.offset + s.count); ++offset)
 100:       {
 101:         char c = buffer[offset];
 102:         if (c == '\t' || c == '\n')
 103:           {
 104:             if (len > 0) {
 105:               g.drawChars(buffer, pos, len, pixelX, pixelY + ascent);            
 106:               pixelX += pixelWidth;
 107:               pixelWidth = 0;
 108:             }
 109:             pos = offset+1;
 110:             len = 0;
 111:           }
 112:         
 113:     switch (c)
 114:       {
 115:       case '\t':
 116:         // In case we have a tab, we just 'jump' over the tab.
 117:         // When we have no tab expander we just use the width of ' '.
 118:         if (e != null)
 119:           pixelX = (int) e.nextTabStop((float) pixelX,
 120:                        startOffset + offset - s.offset);
 121:         else
 122:           pixelX += metrics.charWidth(' ');
 123:         break;
 124:       case '\n':
 125:         // In case we have a newline, we must jump to the next line.
 126:         pixelY += metrics.getHeight();
 127:         pixelX = x;
 128:         break;
 129:       default:
 130:             ++len;
 131:         pixelWidth += metrics.charWidth(buffer[offset]);
 132:         break;
 133:       }
 134:       }
 135: 
 136:     if (len > 0)
 137:       g.drawChars(buffer, pos, len, pixelX, pixelY + ascent);            
 138:     
 139:     return pixelX;
 140:   }
 141: 
 142:   /**
 143:    * Determines the width, that the given text <code>s</code> would take
 144:    * if it was printed with the given {@link java.awt.FontMetrics} on the
 145:    * specified screen position.
 146:    * @param s the text fragment
 147:    * @param metrics the font metrics of the font to be used
 148:    * @param x the x coordinate of the point at which drawing should be done
 149:    * @param e the {@link TabExpander} to be used
 150:    * @param startOffset the index in <code>s</code> where to start
 151:    * @returns the width of the given text s. This takes tabs and newlines
 152:    * into account.
 153:    */
 154:   public static final int getTabbedTextWidth(Segment s, FontMetrics metrics,
 155:                                              int x, TabExpander e,
 156:                                              int startOffset)
 157:   {
 158:     // This buffers the chars to be drawn.
 159:     char[] buffer = s.array;
 160: 
 161:     // The current x coordinate.
 162:     int pixelX = x;
 163: 
 164:     // The current maximum width.
 165:     int maxWidth = 0;
 166: 
 167:     for (int offset = s.offset; offset < (s.offset + s.count); ++offset)
 168:       {
 169:     switch (buffer[offset])
 170:       {
 171:       case '\t':
 172:         // In case we have a tab, we just 'jump' over the tab.
 173:         // When we have no tab expander we just use the width of 'm'.
 174:         if (e != null)
 175:           pixelX = (int) e.nextTabStop((float) pixelX,
 176:                        startOffset + offset - s.offset);
 177:         else
 178:           pixelX += metrics.charWidth(' ');
 179:         break;
 180:       case '\n':
 181:         // In case we have a newline, we must 'draw'
 182:         // the buffer and jump on the next line.
 183:         pixelX += metrics.charWidth(buffer[offset]);
 184:         maxWidth = Math.max(maxWidth, pixelX - x);
 185:         pixelX = x;
 186:         break;
 187:       default:
 188:         // Here we draw the char.
 189:         pixelX += metrics.charWidth(buffer[offset]);
 190:         break;
 191:       }
 192:       }
 193: 
 194:     // Take the last line into account.
 195:     maxWidth = Math.max(maxWidth, pixelX - x);
 196: 
 197:     return maxWidth;
 198:   }
 199: 
 200:   /**
 201:    * Provides a facility to map screen coordinates into a model location. For a
 202:    * given text fragment and start location within this fragment, this method
 203:    * determines the model location so that the resulting fragment fits best
 204:    * into the span <code>[x0, x]</code>.
 205:    *
 206:    * The parameter <code>round</code> controls which model location is returned
 207:    * if the view coordinates are on a character: If <code>round</code> is
 208:    * <code>true</code>, then the result is rounded up to the next character, so
 209:    * that the resulting fragment is the smallest fragment that is larger than
 210:    * the specified span. If <code>round</code> is <code>false</code>, then the
 211:    * resulting fragment is the largest fragment that is smaller than the
 212:    * specified span.
 213:    *
 214:    * @param s the text segment
 215:    * @param fm the font metrics to use
 216:    * @param x0 the starting screen location
 217:    * @param x the target screen location at which the requested fragment should
 218:    *        end
 219:    * @param te the tab expander to use; if this is <code>null</code>, TABs are
 220:    *        expanded to one space character
 221:    * @param p0 the starting model location
 222:    * @param round if <code>true</code> round up to the next location, otherwise
 223:    *        round down to the current location
 224:    *
 225:    * @return the model location, so that the resulting fragment fits within the
 226:    *         specified span
 227:    */
 228:   public static final int getTabbedTextOffset(Segment s, FontMetrics fm, int x0,
 229:                                               int x, TabExpander te, int p0,
 230:                                               boolean round)
 231:   {
 232:     // At the end of the for loop, this holds the requested model location
 233:     int pos;
 234:     int currentX = x0;
 235: 
 236:     for (pos = p0; pos < s.count; pos++)
 237:       {
 238:         char nextChar = s.array[s.offset+pos];
 239:         if (nextChar == 0)
 240:           {
 241:             if (! round)
 242:               pos--;
 243:             break;
 244:           }
 245:         if (nextChar != '\t')
 246:           currentX += fm.charWidth(nextChar);
 247:         else
 248:           {
 249:             if (te == null)
 250:               currentX += fm.charWidth(' ');
 251:             else
 252:               currentX = (int) te.nextTabStop(currentX, pos);
 253:           }
 254:         if (currentX > x)
 255:           {
 256:             if (! round)
 257:               pos--;
 258:             break;
 259:           }
 260:       }
 261:     return pos;
 262:   }
 263: 
 264:   /**
 265:    * Provides a facility to map screen coordinates into a model location. For a
 266:    * given text fragment and start location within this fragment, this method
 267:    * determines the model location so that the resulting fragment fits best
 268:    * into the span <code>[x0, x]</code>.
 269:    *
 270:    * This method rounds up to the next location, so that the resulting fragment
 271:    * will be the smallest fragment of the text, that is greater than the
 272:    * specified span.
 273:    *
 274:    * @param s the text segment
 275:    * @param fm the font metrics to use
 276:    * @param x0 the starting screen location
 277:    * @param x the target screen location at which the requested fragment should
 278:    *        end
 279:    * @param te the tab expander to use; if this is <code>null</code>, TABs are
 280:    *        expanded to one space character
 281:    * @param p0 the starting model location
 282:    *
 283:    * @return the model location, so that the resulting fragment fits within the
 284:    *         specified span
 285:    */
 286:   public static final int getTabbedTextOffset(Segment s, FontMetrics fm, int x0,
 287:                                               int x, TabExpander te, int p0)
 288:   {
 289:     return getTabbedTextOffset(s, fm, x0, x, te, p0, true);
 290:   }
 291:   
 292:   /**
 293:    * Finds the start of the next word for the given offset.
 294:    * 
 295:    * @param c
 296:    *          the text component
 297:    * @param offs
 298:    *          the offset in the document
 299:    * @return the location in the model of the start of the next word.
 300:    * @throws BadLocationException
 301:    *           if the offset is invalid.
 302:    */
 303:   public static final int getNextWord(JTextComponent c, int offs)
 304:       throws BadLocationException
 305:   {
 306:     if (offs < 0 || offs > (c.getText().length() - 1))
 307:       throw new BadLocationException("invalid offset specified", offs);
 308:     String text = c.getText();
 309:     BreakIterator wb = BreakIterator.getWordInstance();
 310:     wb.setText(text);
 311:     int last = wb.following(offs);
 312:     int current = wb.next();
 313:     while (current != BreakIterator.DONE)
 314:       {
 315:         for (int i = last; i < current; i++)
 316:           {
 317:             // FIXME: Should use isLetter(int) and text.codePointAt(int)
 318:             // instead, but isLetter(int) isn't implemented yet
 319:             if (Character.isLetter(text.charAt(i)))
 320:               return last;
 321:           }
 322:         last = current;
 323:         current = wb.next();
 324:       }
 325:     return BreakIterator.DONE;
 326:   }
 327: 
 328:   /**
 329:    * Finds the start of the previous word for the given offset.
 330:    * 
 331:    * @param c
 332:    *          the text component
 333:    * @param offs
 334:    *          the offset in the document
 335:    * @return the location in the model of the start of the previous word.
 336:    * @throws BadLocationException
 337:    *           if the offset is invalid.
 338:    */
 339:   public static final int getPreviousWord(JTextComponent c, int offs)
 340:       throws BadLocationException
 341:   {
 342:     if (offs < 0 || offs > (c.getText().length() - 1))
 343:       throw new BadLocationException("invalid offset specified", offs);
 344:     String text = c.getText();
 345:     BreakIterator wb = BreakIterator.getWordInstance();
 346:     wb.setText(text);
 347:     int last = wb.preceding(offs);
 348:     int current = wb.previous();
 349: 
 350:     while (current != BreakIterator.DONE)
 351:       {
 352:         for (int i = last; i < offs; i++)
 353:           {
 354:             // FIXME: Should use isLetter(int) and text.codePointAt(int)
 355:             // instead, but isLetter(int) isn't implemented yet
 356:             if (Character.isLetter(text.charAt(i)))
 357:               return last;
 358:           }
 359:         last = current;
 360:         current = wb.previous();
 361:       }
 362:     return 0;
 363:   }
 364:   
 365:   /**
 366:    * Finds the start of a word for the given location.
 367:    * @param c the text component
 368:    * @param offs the offset location
 369:    * @return the location of the word beginning
 370:    * @throws BadLocationException if the offset location is invalid
 371:    */
 372:   public static final int getWordStart(JTextComponent c, int offs)
 373:       throws BadLocationException
 374:   {
 375:     if (offs < 0 || offs >= c.getText().length())
 376:       throw new BadLocationException("invalid offset specified", offs);
 377:     
 378:     String text = c.getText();
 379:     BreakIterator wb = BreakIterator.getWordInstance();
 380:     wb.setText(text);
 381:     if (wb.isBoundary(offs))
 382:       return offs;
 383:     return wb.preceding(offs);
 384:   }
 385:   
 386:   /**
 387:    * Finds the end of a word for the given location.
 388:    * @param c the text component
 389:    * @param offs the offset location
 390:    * @return the location of the word end
 391:    * @throws BadLocationException if the offset location is invalid
 392:    */
 393:   public static final int getWordEnd(JTextComponent c, int offs)
 394:       throws BadLocationException
 395:   {
 396:     if (offs < 0 || offs >= c.getText().length())
 397:       throw new BadLocationException("invalid offset specified", offs);
 398:     
 399:     String text = c.getText();
 400:     BreakIterator wb = BreakIterator.getWordInstance();
 401:     wb.setText(text);
 402:     return wb.following(offs);
 403:   }
 404:   
 405:   /**
 406:    * Get the model position of the end of the row that contains the 
 407:    * specified model position.  Return null if the given JTextComponent
 408:    * does not have a size.
 409:    * @param c the JTextComponent
 410:    * @param offs the model position
 411:    * @return the model position of the end of the row containing the given 
 412:    * offset
 413:    * @throws BadLocationException if the offset is invalid
 414:    */
 415:   public static final int getRowEnd(JTextComponent c, int offs)
 416:       throws BadLocationException
 417:   {
 418:     String text = c.getText();
 419:     if (text == null)
 420:       return -1;
 421: 
 422:     // Do a binary search for the smallest position X > offs
 423:     // such that that character at positino X is not on the same
 424:     // line as the character at position offs
 425:     int high = offs + ((text.length() - 1 - offs) / 2);
 426:     int low = offs;
 427:     int oldHigh = text.length() + 1;
 428:     while (true)
 429:       {
 430:         if (c.modelToView(high).y != c.modelToView(offs).y)
 431:           {
 432:             oldHigh = high;
 433:             high = low + ((high + 1 - low) / 2);
 434:             if (oldHigh == high)
 435:               return high - 1;
 436:           }
 437:         else
 438:           {
 439:             low = high;
 440:             high += ((oldHigh - high) / 2);
 441:             if (low == high)
 442:               return low;
 443:           }
 444:       }
 445:   }
 446:       
 447:   /**
 448:    * Get the model position of the start of the row that contains the specified
 449:    * model position. Return null if the given JTextComponent does not have a
 450:    * size.
 451:    * 
 452:    * @param c the JTextComponent
 453:    * @param offs the model position
 454:    * @return the model position of the start of the row containing the given
 455:    *         offset
 456:    * @throws BadLocationException if the offset is invalid
 457:    */
 458:   public static final int getRowStart(JTextComponent c, int offs)
 459:       throws BadLocationException
 460:   {
 461:     String text = c.getText();
 462:     if (text == null)
 463:       return -1;
 464: 
 465:     // Do a binary search for the greatest position X < offs
 466:     // such that the character at position X is not on the same
 467:     // row as the character at position offs
 468:     int high = offs;
 469:     int low = 0;
 470:     int oldLow = 0;
 471:     while (true)
 472:       {
 473:         if (c.modelToView(low).y != c.modelToView(offs).y)
 474:           {
 475:             oldLow = low;
 476:             low = high - ((high + 1 - low) / 2);
 477:             if (oldLow == low)
 478:               return low + 1;
 479:           }
 480:         else
 481:           {
 482:             high = low;
 483:             low -= ((low - oldLow) / 2);
 484:             if (low == high)
 485:               return low;
 486:           }
 487:       }
 488:   }
 489:   
 490:   /**
 491:    * Determine where to break the text in the given Segment, attempting to find
 492:    * a word boundary.
 493:    * @param s the Segment that holds the text
 494:    * @param metrics the font metrics used for calculating the break point
 495:    * @param x0 starting view location representing the start of the text
 496:    * @param x the target view location
 497:    * @param e the TabExpander used for expanding tabs (if this is null tabs
 498:    * are expanded to 1 space)
 499:    * @param startOffset the offset in the Document of the start of the text
 500:    * @return the offset at which we should break the text
 501:    */
 502:   public static final int getBreakLocation(Segment s, FontMetrics metrics,
 503:                                            int x0, int x, TabExpander e,
 504:                                            int startOffset)
 505:   {
 506:     int mark = Utilities.getTabbedTextOffset(s, metrics, x0, x, e, startOffset);
 507:     BreakIterator breaker = BreakIterator.getWordInstance();
 508:     breaker.setText(s.toString());
 509:     
 510:     // If mark is equal to the end of the string, just use that position
 511:     if (mark == s.count)
 512:       return mark;
 513:     
 514:     // Try to find a word boundary previous to the mark at which we 
 515:     // can break the text
 516:     int preceding = breaker.preceding(mark + 1);
 517:     
 518:     if (preceding != 0)
 519:       return preceding;
 520:     else
 521:       // If preceding is 0 we couldn't find a suitable word-boundary so
 522:       // just break it on the character boundary
 523:       return mark;
 524:   }
 525: }