Source for javax.swing.text.Utilities

   1: /* Utilities.java --
   2:    Copyright (C) 2004, 2005, 2006  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:  * @author Robert Schuster (robertschuster@fsfe.org)
  52:  */
  53: public class Utilities
  54: {
  55:   /**
  56:    * The length of the char buffer that holds the characters to be drawn.
  57:    */
  58:   private static final int BUF_LENGTH = 64;
  59: 
  60:   /**
  61:    * Creates a new <code>Utilities</code> object.
  62:    */
  63:   public Utilities()
  64:   {
  65:     // Nothing to be done here.
  66:   }
  67: 
  68:   /**
  69:    * Draws the given text segment. Contained tabs and newline characters
  70:    * are taken into account. Tabs are expanded using the
  71:    * specified {@link TabExpander}.
  72:    *
  73:    *
  74:    * The X and Y coordinates denote the start of the <em>baseline</em> where
  75:    * the text should be drawn.
  76:    *
  77:    * @param s the text fragment to be drawn.
  78:    * @param x the x position for drawing.
  79:    * @param y the y position for drawing.
  80:    * @param g the {@link Graphics} context for drawing.
  81:    * @param e the {@link TabExpander} which specifies the Tab-expanding
  82:    *     technique.
  83:    * @param startOffset starting offset in the text.
  84:    * @return the x coordinate at the end of the drawn text.
  85:    */
  86:   public static final int drawTabbedText(Segment s, int x, int y, Graphics g,
  87:                                          TabExpander e, int startOffset)
  88:   {
  89:     // This buffers the chars to be drawn.
  90:     char[] buffer = s.array;
  91: 
  92:     // The font metrics of the current selected font.
  93:     FontMetrics metrics = g.getFontMetrics();
  94:     int ascent = metrics.getAscent();
  95: 
  96:     // The current x and y pixel coordinates.
  97:     int pixelX = x;
  98:     int pixelY = y - ascent;
  99: 
 100:     int pixelWidth = 0;
 101:     int pos = s.offset;
 102:     int len = 0;
 103:     
 104:     int end = s.offset + s.count;
 105: 
 106:     for (int offset = s.offset; offset < end; ++offset)
 107:       {
 108:         char c = buffer[offset];
 109:         if (c == '\t' || c == '\n')
 110:           {
 111:             if (len > 0) {
 112:               g.drawChars(buffer, pos, len, pixelX, pixelY + ascent);            
 113:               pixelX += pixelWidth;
 114:               pixelWidth = 0;
 115:             }
 116:             pos = offset+1;
 117:             len = 0;
 118:           }
 119:         
 120:     switch (c)
 121:       {
 122:       case '\t':
 123:         // In case we have a tab, we just 'jump' over the tab.
 124:         // When we have no tab expander we just use the width of ' '.
 125:         if (e != null)
 126:           pixelX = (int) e.nextTabStop((float) pixelX,
 127:                        startOffset + offset - s.offset);
 128:         else
 129:           pixelX += metrics.charWidth(' ');
 130:         break;
 131:       case '\n':
 132:         // In case we have a newline, we must jump to the next line.
 133:         pixelY += metrics.getHeight();
 134:         pixelX = x;
 135:         break;
 136:       default:
 137:             ++len;
 138:         pixelWidth += metrics.charWidth(buffer[offset]);
 139:         break;
 140:       }
 141:       }
 142: 
 143:     if (len > 0)
 144:       g.drawChars(buffer, pos, len, pixelX, pixelY + ascent);            
 145:     
 146:     return pixelX + pixelWidth;
 147:   }
 148: 
 149:   /**
 150:    * Determines the width, that the given text <code>s</code> would take
 151:    * if it was printed with the given {@link java.awt.FontMetrics} on the
 152:    * specified screen position.
 153:    * @param s the text fragment
 154:    * @param metrics the font metrics of the font to be used
 155:    * @param x the x coordinate of the point at which drawing should be done
 156:    * @param e the {@link TabExpander} to be used
 157:    * @param startOffset the index in <code>s</code> where to start
 158:    * @returns the width of the given text s. This takes tabs and newlines
 159:    * into account.
 160:    */
 161:   public static final int getTabbedTextWidth(Segment s, FontMetrics metrics,
 162:                                              int x, TabExpander e,
 163:                                              int startOffset)
 164:   {
 165:     // This buffers the chars to be drawn.
 166:     char[] buffer = s.array;
 167: 
 168:     // The current x coordinate.
 169:     int pixelX = x;
 170: 
 171:     // The current maximum width.
 172:     int maxWidth = 0;
 173: 
 174:     for (int offset = s.offset; offset < (s.offset + s.count); ++offset)
 175:       {
 176:     switch (buffer[offset])
 177:       {
 178:       case '\t':
 179:         // In case we have a tab, we just 'jump' over the tab.
 180:         // When we have no tab expander we just use the width of 'm'.
 181:         if (e != null)
 182:           pixelX = (int) e.nextTabStop((float) pixelX,
 183:                        startOffset + offset - s.offset);
 184:         else
 185:           pixelX += metrics.charWidth(' ');
 186:         break;
 187:       case '\n':
 188:         // In case we have a newline, we must 'draw'
 189:         // the buffer and jump on the next line.
 190:         pixelX += metrics.charWidth(buffer[offset]);
 191:         maxWidth = Math.max(maxWidth, pixelX - x);
 192:         pixelX = x;
 193:         break;
 194:       default:
 195:         // Here we draw the char.
 196:         pixelX += metrics.charWidth(buffer[offset]);
 197:         break;
 198:       }
 199:       }
 200: 
 201:     // Take the last line into account.
 202:     maxWidth = Math.max(maxWidth, pixelX - x);
 203: 
 204:     return maxWidth;
 205:   }
 206: 
 207:   /**
 208:    * Provides a facility to map screen coordinates into a model location. For a
 209:    * given text fragment and start location within this fragment, this method
 210:    * determines the model location so that the resulting fragment fits best
 211:    * into the span <code>[x0, x]</code>.
 212:    *
 213:    * The parameter <code>round</code> controls which model location is returned
 214:    * if the view coordinates are on a character: If <code>round</code> is
 215:    * <code>true</code>, then the result is rounded up to the next character, so
 216:    * that the resulting fragment is the smallest fragment that is larger than
 217:    * the specified span. If <code>round</code> is <code>false</code>, then the
 218:    * resulting fragment is the largest fragment that is smaller than the
 219:    * specified span.
 220:    *
 221:    * @param s the text segment
 222:    * @param fm the font metrics to use
 223:    * @param x0 the starting screen location
 224:    * @param x the target screen location at which the requested fragment should
 225:    *        end
 226:    * @param te the tab expander to use; if this is <code>null</code>, TABs are
 227:    *        expanded to one space character
 228:    * @param p0 the starting model location
 229:    * @param round if <code>true</code> round up to the next location, otherwise
 230:    *        round down to the current location
 231:    *
 232:    * @return the model location, so that the resulting fragment fits within the
 233:    *         specified span
 234:    */
 235:   public static final int getTabbedTextOffset(Segment s, FontMetrics fm, int x0,
 236:                                               int x, TabExpander te, int p0,
 237:                                               boolean round)
 238:   {
 239:     // At the end of the for loop, this holds the requested model location
 240:     int pos;
 241:     int currentX = x0;
 242:     int width = 0;
 243:     
 244:     for (pos = 0; pos < s.count; pos++)
 245:       {
 246:         char nextChar = s.array[s.offset+pos];
 247:         
 248:         if (nextChar == 0)
 249:             break;
 250:         
 251:         if (nextChar != '\t')
 252:           width = fm.charWidth(nextChar);
 253:         else
 254:           {
 255:             if (te == null)
 256:               width = fm.charWidth(' ');
 257:             else
 258:               width = ((int) te.nextTabStop(currentX, pos)) - currentX;
 259:           }
 260:         
 261:         if (round)
 262:           {
 263:             if (currentX + (width>>1) > x)
 264:               break;
 265:           }
 266:         else
 267:           {
 268:             if (currentX + width > x)
 269:               break;
 270:           }
 271:         
 272:         currentX += width;
 273:       }
 274: 
 275:     return pos + p0;
 276:   }
 277: 
 278:   /**
 279:    * Provides a facility to map screen coordinates into a model location. For a
 280:    * given text fragment and start location within this fragment, this method
 281:    * determines the model location so that the resulting fragment fits best
 282:    * into the span <code>[x0, x]</code>.
 283:    *
 284:    * This method rounds up to the next location, so that the resulting fragment
 285:    * will be the smallest fragment of the text, that is greater than the
 286:    * specified span.
 287:    *
 288:    * @param s the text segment
 289:    * @param fm the font metrics to use
 290:    * @param x0 the starting screen location
 291:    * @param x the target screen location at which the requested fragment should
 292:    *        end
 293:    * @param te the tab expander to use; if this is <code>null</code>, TABs are
 294:    *        expanded to one space character
 295:    * @param p0 the starting model location
 296:    *
 297:    * @return the model location, so that the resulting fragment fits within the
 298:    *         specified span
 299:    */
 300:   public static final int getTabbedTextOffset(Segment s, FontMetrics fm, int x0,
 301:                                               int x, TabExpander te, int p0)
 302:   {
 303:     return getTabbedTextOffset(s, fm, x0, x, te, p0, true);
 304:   }
 305:   
 306:   /**
 307:    * Finds the start of the next word for the given offset.
 308:    * 
 309:    * @param c
 310:    *          the text component
 311:    * @param offs
 312:    *          the offset in the document
 313:    * @return the location in the model of the start of the next word.
 314:    * @throws BadLocationException
 315:    *           if the offset is invalid.
 316:    */
 317:   public static final int getNextWord(JTextComponent c, int offs)
 318:       throws BadLocationException
 319:   {
 320:     if (offs < 0 || offs > (c.getText().length() - 1))
 321:       throw new BadLocationException("invalid offset specified", offs);
 322:     String text = c.getText();
 323:     BreakIterator wb = BreakIterator.getWordInstance();
 324:     wb.setText(text);
 325:         
 326:     int last = wb.following(offs);
 327:     int current = wb.next();
 328:     int cp;
 329: 
 330:     while (current != BreakIterator.DONE)
 331:       {
 332:         for (int i = last; i < current; i++)
 333:           {
 334:             cp = text.codePointAt(i);
 335:             
 336:             // Return the last found bound if there is a letter at the current
 337:             // location or is not whitespace (meaning it is a number or
 338:             // punctuation). The first case means that 'last' denotes the
 339:             // beginning of a word while the second case means it is the start
 340:             // of some else.
 341:             if (Character.isLetter(cp)
 342:                 || !Character.isWhitespace(cp))
 343:               return last;
 344:           }
 345:         last = current;
 346:         current = wb.next();
 347:       }
 348:     
 349:     throw new BadLocationException("no more word", offs);
 350:   }
 351: 
 352:   /**
 353:    * Finds the start of the previous word for the given offset.
 354:    * 
 355:    * @param c
 356:    *          the text component
 357:    * @param offs
 358:    *          the offset in the document
 359:    * @return the location in the model of the start of the previous word.
 360:    * @throws BadLocationException
 361:    *           if the offset is invalid.
 362:    */
 363:   public static final int getPreviousWord(JTextComponent c, int offs)
 364:       throws BadLocationException
 365:   {
 366:     if (offs < 0 || offs > (c.getText().length() - 1))
 367:       throw new BadLocationException("invalid offset specified", offs);
 368:     String text = c.getText();
 369:     BreakIterator wb = BreakIterator.getWordInstance();
 370:     wb.setText(text);
 371:     int last = wb.preceding(offs);
 372:     int current = wb.previous();
 373: 
 374:     while (current != BreakIterator.DONE)
 375:       {
 376:         for (int i = last; i < offs; i++)
 377:           {
 378:             if (Character.isLetter(text.codePointAt(i)))
 379:               return last;
 380:           }
 381:         last = current;
 382:         current = wb.previous();
 383:       }
 384:     return 0;
 385:   }
 386:   
 387:   /**
 388:    * Finds the start of a word for the given location.
 389:    * @param c the text component
 390:    * @param offs the offset location
 391:    * @return the location of the word beginning
 392:    * @throws BadLocationException if the offset location is invalid
 393:    */
 394:   public static final int getWordStart(JTextComponent c, int offs)
 395:       throws BadLocationException
 396:   {
 397:     if (offs < 0 || offs >= c.getText().length())
 398:       throw new BadLocationException("invalid offset specified", offs);
 399:     
 400:     String text = c.getText();
 401:     BreakIterator wb = BreakIterator.getWordInstance();
 402:     wb.setText(text);
 403:     if (wb.isBoundary(offs))
 404:       return offs;
 405:     return wb.preceding(offs);
 406:   }
 407:   
 408:   /**
 409:    * Finds the end of a word for the given location.
 410:    * @param c the text component
 411:    * @param offs the offset location
 412:    * @return the location of the word end
 413:    * @throws BadLocationException if the offset location is invalid
 414:    */
 415:   public static final int getWordEnd(JTextComponent c, int offs)
 416:       throws BadLocationException
 417:   {
 418:     if (offs < 0 || offs >= c.getText().length())
 419:       throw new BadLocationException("invalid offset specified", offs);
 420:     
 421:     String text = c.getText();
 422:     BreakIterator wb = BreakIterator.getWordInstance();
 423:     wb.setText(text);
 424:     return wb.following(offs);
 425:   }
 426:   
 427:   /**
 428:    * Get the model position of the end of the row that contains the 
 429:    * specified model position.  Return null if the given JTextComponent
 430:    * does not have a size.
 431:    * @param c the JTextComponent
 432:    * @param offs the model position
 433:    * @return the model position of the end of the row containing the given 
 434:    * offset
 435:    * @throws BadLocationException if the offset is invalid
 436:    */
 437:   public static final int getRowEnd(JTextComponent c, int offs)
 438:       throws BadLocationException
 439:   {
 440:     String text = c.getText();
 441:     if (text == null)
 442:       return -1;
 443: 
 444:     // Do a binary search for the smallest position X > offs
 445:     // such that that character at positino X is not on the same
 446:     // line as the character at position offs
 447:     int high = offs + ((text.length() - 1 - offs) / 2);
 448:     int low = offs;
 449:     int oldHigh = text.length() + 1;
 450:     while (true)
 451:       {
 452:         if (c.modelToView(high).y != c.modelToView(offs).y)
 453:           {
 454:             oldHigh = high;
 455:             high = low + ((high + 1 - low) / 2);
 456:             if (oldHigh == high)
 457:               return high - 1;
 458:           }
 459:         else
 460:           {
 461:             low = high;
 462:             high += ((oldHigh - high) / 2);
 463:             if (low == high)
 464:               return low;
 465:           }
 466:       }
 467:   }
 468:       
 469:   /**
 470:    * Get the model position of the start of the row that contains the specified
 471:    * model position. Return null if the given JTextComponent does not have a
 472:    * size.
 473:    * 
 474:    * @param c the JTextComponent
 475:    * @param offs the model position
 476:    * @return the model position of the start of the row containing the given
 477:    *         offset
 478:    * @throws BadLocationException if the offset is invalid
 479:    */
 480:   public static final int getRowStart(JTextComponent c, int offs)
 481:       throws BadLocationException
 482:   {
 483:     String text = c.getText();
 484:     if (text == null)
 485:       return -1;
 486: 
 487:     // Do a binary search for the greatest position X < offs
 488:     // such that the character at position X is not on the same
 489:     // row as the character at position offs
 490:     int high = offs;
 491:     int low = 0;
 492:     int oldLow = 0;
 493:     while (true)
 494:       {
 495:         if (c.modelToView(low).y != c.modelToView(offs).y)
 496:           {
 497:             oldLow = low;
 498:             low = high - ((high + 1 - low) / 2);
 499:             if (oldLow == low)
 500:               return low + 1;
 501:           }
 502:         else
 503:           {
 504:             high = low;
 505:             low -= ((low - oldLow) / 2);
 506:             if (low == high)
 507:               return low;
 508:           }
 509:       }
 510:   }
 511:   
 512:   /**
 513:    * Determine where to break the text in the given Segment, attempting to find
 514:    * a word boundary.
 515:    * @param s the Segment that holds the text
 516:    * @param metrics the font metrics used for calculating the break point
 517:    * @param x0 starting view location representing the start of the text
 518:    * @param x the target view location
 519:    * @param e the TabExpander used for expanding tabs (if this is null tabs
 520:    * are expanded to 1 space)
 521:    * @param startOffset the offset in the Document of the start of the text
 522:    * @return the offset at which we should break the text
 523:    */
 524:   public static final int getBreakLocation(Segment s, FontMetrics metrics,
 525:                                            int x0, int x, TabExpander e,
 526:                                            int startOffset)
 527:   {
 528:     int mark = Utilities.getTabbedTextOffset(s, metrics, x0, x, e, startOffset, false);
 529:     BreakIterator breaker = BreakIterator.getWordInstance();
 530:     breaker.setText(s);
 531: 
 532:     // If startOffset and s.offset differ then we need to use
 533:     // that difference two convert the offset between the two metrics. 
 534:     int shift = startOffset - s.offset;
 535:     
 536:     // If mark is equal to the end of the string, just use that position.
 537:     if (mark >= shift + s.count)
 538:       return mark;
 539:     
 540:     // Try to find a word boundary previous to the mark at which we 
 541:     // can break the text.
 542:     int preceding = breaker.preceding(mark + 1 - shift);
 543:     
 544:     if (preceding != 0)
 545:       return preceding + shift;
 546:     
 547:     // If preceding is 0 we couldn't find a suitable word-boundary so
 548:     // just break it on the character boundary
 549:     return mark;
 550:   }
 551: 
 552:   /**
 553:    * Returns the paragraph element in the text component <code>c</code> at
 554:    * the specified location <code>offset</code>.
 555:    *
 556:    * @param c the text component
 557:    * @param offset the offset of the paragraph element to return
 558:    *
 559:    * @return the paragraph element at <code>offset</code>
 560:    */
 561:   public static final Element getParagraphElement(JTextComponent c, int offset)
 562:   {
 563:     Document doc = c.getDocument();
 564:     Element par = null;
 565:     if (doc instanceof StyledDocument)
 566:       {
 567:         StyledDocument styledDoc = (StyledDocument) doc;
 568:         par = styledDoc.getParagraphElement(offset);
 569:       }
 570:     else
 571:       {
 572:         Element root = c.getDocument().getDefaultRootElement();
 573:         int parIndex = root.getElementIndex(offset);
 574:         par = root.getElement(parIndex);
 575:       }
 576:     return par;
 577:   }
 578: 
 579:   /**
 580:    * Returns the document position that is closest above to the specified x
 581:    * coordinate in the row containing <code>offset</code>.
 582:    *
 583:    * @param c the text component
 584:    * @param offset the offset
 585:    * @param x the x coordinate
 586:    *
 587:    * @return  the document position that is closest above to the specified x
 588:    *          coordinate in the row containing <code>offset</code>
 589:    *
 590:    * @throws BadLocationException if <code>offset</code> is not a valid offset
 591:    */
 592:   public static final int getPositionAbove(JTextComponent c, int offset, int x)
 593:     throws BadLocationException
 594:   {
 595:     int offs = getRowStart(c, offset);
 596:     
 597:     if(offs == -1)
 598:       return -1;
 599: 
 600:     // Effectively calculates the y value of the previous line.
 601:     Point pt = c.modelToView(offs-1).getLocation();
 602:     
 603:     pt.x = x;
 604:     
 605:     // Calculate a simple fitting offset.
 606:     offs = c.viewToModel(pt);
 607:     
 608:     // Find out the real x positions of the calculated character and its
 609:     // neighbour.
 610:     int offsX = c.modelToView(offs).getLocation().x;
 611:     int offsXNext = c.modelToView(offs+1).getLocation().x;
 612:     
 613:     // Chose the one which is nearer to us and return its offset.
 614:     if (Math.abs(offsX-x) <= Math.abs(offsXNext-x))
 615:       return offs;
 616:     else
 617:       return offs+1;
 618:   }
 619: 
 620:   /**
 621:    * Returns the document position that is closest below to the specified x
 622:    * coordinate in the row containing <code>offset</code>.
 623:    *
 624:    * @param c the text component
 625:    * @param offset the offset
 626:    * @param x the x coordinate
 627:    *
 628:    * @return  the document position that is closest above to the specified x
 629:    *          coordinate in the row containing <code>offset</code>
 630:    *
 631:    * @throws BadLocationException if <code>offset</code> is not a valid offset
 632:    */
 633:   public static final int getPositionBelow(JTextComponent c, int offset, int x)
 634:     throws BadLocationException
 635:   {
 636:     int offs = getRowEnd(c, offset);
 637:     
 638:     if(offs == -1)
 639:       return -1;
 640: 
 641:     Point pt = null;
 642:     
 643:     // Note: Some views represent the position after the last
 644:     // typed character others do not. Converting offset 3 in "a\nb"
 645:     // in a PlainView will return a valid rectangle while in a
 646:     // WrappedPlainView this will throw a BadLocationException.
 647:     // This behavior has been observed in the RI.
 648:     try
 649:       {
 650:         // Effectively calculates the y value of the next line.
 651:         pt = c.modelToView(offs+1).getLocation();
 652:       }
 653:     catch(BadLocationException ble)
 654:       {
 655:         return offset;
 656:       }
 657:     
 658:     pt.x = x;
 659:     
 660:     // Calculate a simple fitting offset.
 661:     offs = c.viewToModel(pt);
 662:     
 663:     if (offs == c.getDocument().getLength())
 664:       return offs;
 665: 
 666:     // Find out the real x positions of the calculated character and its
 667:     // neighbour.
 668:     int offsX = c.modelToView(offs).getLocation().x;
 669:     int offsXNext = c.modelToView(offs+1).getLocation().x;
 670:     
 671:     // Chose the one which is nearer to us and return its offset.
 672:     if (Math.abs(offsX-x) <= Math.abs(offsXNext-x))
 673:       return offs;
 674:     else
 675:       return offs+1;
 676:     }
 677: }