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.awt.Point;
  44: import java.awt.Rectangle;
  45: import java.text.BreakIterator;
  46: 
  47: import javax.swing.SwingConstants;
  48: 
  49: /**
  50:  * A set of utilities to deal with text. This is used by several other classes
  51:  * inside this package.
  52:  *
  53:  * @author Roman Kennke (roman@ontographics.com)
  54:  */
  55: public class Utilities
  56: {
  57:   /**
  58:    * The length of the char buffer that holds the characters to be drawn.
  59:    */
  60:   private static final int BUF_LENGTH = 64;
  61: 
  62:   /**
  63:    * Creates a new <code>Utilities</code> object.
  64:    */
  65:   public Utilities()
  66:   {
  67:     // Nothing to be done here.
  68:   }
  69: 
  70:   /**
  71:    * Draws the given text segment. Contained tabs and newline characters
  72:    * are taken into account. Tabs are expanded using the
  73:    * specified {@link TabExpander}.
  74:    *
  75:    * @param s the text fragment to be drawn.
  76:    * @param x the x position for drawing.
  77:    * @param y the y position for drawing.
  78:    * @param g the {@link Graphics} context for drawing.
  79:    * @param e the {@link TabExpander} which specifies the Tab-expanding
  80:    *     technique.
  81:    * @param startOffset starting offset in the text.
  82:    * @return the x coordinate at the end of the drawn text.
  83:    */
  84:   public static final int drawTabbedText(Segment s, int x, int y, Graphics g,
  85:                                          TabExpander e, int startOffset)
  86:   {
  87:     // This buffers the chars to be drawn.
  88:     char[] buffer = s.array;
  89: 
  90: 
  91:     // The current x and y pixel coordinates.
  92:     int pixelX = x;
  93:     int pixelY = y;
  94: 
  95:     // The font metrics of the current selected font.
  96:     FontMetrics metrics = g.getFontMetrics();
  97:     int ascent = metrics.getAscent();
  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 = p0; pos < s.count; pos++)
 241:       {
 242:         char nextChar = s.array[s.offset+pos];
 243:         if (nextChar == 0)
 244:           {
 245:             if (! round)
 246:               pos--;
 247:             break;
 248:           }
 249:         if (nextChar != '\t')
 250:           currentX += fm.charWidth(nextChar);
 251:         else
 252:           {
 253:             if (te == null)
 254:               currentX += fm.charWidth(' ');
 255:             else
 256:               currentX = (int) te.nextTabStop(currentX, pos);
 257:           }
 258:         if (currentX > x)
 259:           {
 260:             if (! round)
 261:               pos--;
 262:             break;
 263:           }
 264:       }
 265:     return pos;
 266:   }
 267: 
 268:   /**
 269:    * Provides a facility to map screen coordinates into a model location. For a
 270:    * given text fragment and start location within this fragment, this method
 271:    * determines the model location so that the resulting fragment fits best
 272:    * into the span <code>[x0, x]</code>.
 273:    *
 274:    * This method rounds up to the next location, so that the resulting fragment
 275:    * will be the smallest fragment of the text, that is greater than the
 276:    * specified span.
 277:    *
 278:    * @param s the text segment
 279:    * @param fm the font metrics to use
 280:    * @param x0 the starting screen location
 281:    * @param x the target screen location at which the requested fragment should
 282:    *        end
 283:    * @param te the tab expander to use; if this is <code>null</code>, TABs are
 284:    *        expanded to one space character
 285:    * @param p0 the starting model location
 286:    *
 287:    * @return the model location, so that the resulting fragment fits within the
 288:    *         specified span
 289:    */
 290:   public static final int getTabbedTextOffset(Segment s, FontMetrics fm, int x0,
 291:                                               int x, TabExpander te, int p0)
 292:   {
 293:     return getTabbedTextOffset(s, fm, x0, x, te, p0, true);
 294:   }
 295:   
 296:   /**
 297:    * Finds the start of the next word for the given offset.
 298:    * 
 299:    * @param c
 300:    *          the text component
 301:    * @param offs
 302:    *          the offset in the document
 303:    * @return the location in the model of the start of the next word.
 304:    * @throws BadLocationException
 305:    *           if the offset is invalid.
 306:    */
 307:   public static final int getNextWord(JTextComponent c, int offs)
 308:       throws BadLocationException
 309:   {
 310:     if (offs < 0 || offs > (c.getText().length() - 1))
 311:       throw new BadLocationException("invalid offset specified", offs);
 312:     String text = c.getText();
 313:     BreakIterator wb = BreakIterator.getWordInstance();
 314:     wb.setText(text);
 315:     int last = wb.following(offs);
 316:     int current = wb.next();
 317:     while (current != BreakIterator.DONE)
 318:       {
 319:         for (int i = last; i < current; i++)
 320:           {
 321:             // FIXME: Should use isLetter(int) and text.codePointAt(int)
 322:             // instead, but isLetter(int) isn't implemented yet
 323:             if (Character.isLetter(text.charAt(i)))
 324:               return last;
 325:           }
 326:         last = current;
 327:         current = wb.next();
 328:       }
 329:     return BreakIterator.DONE;
 330:   }
 331: 
 332:   /**
 333:    * Finds the start of the previous word for the given offset.
 334:    * 
 335:    * @param c
 336:    *          the text component
 337:    * @param offs
 338:    *          the offset in the document
 339:    * @return the location in the model of the start of the previous word.
 340:    * @throws BadLocationException
 341:    *           if the offset is invalid.
 342:    */
 343:   public static final int getPreviousWord(JTextComponent c, int offs)
 344:       throws BadLocationException
 345:   {
 346:     if (offs < 0 || offs > (c.getText().length() - 1))
 347:       throw new BadLocationException("invalid offset specified", offs);
 348:     String text = c.getText();
 349:     BreakIterator wb = BreakIterator.getWordInstance();
 350:     wb.setText(text);
 351:     int last = wb.preceding(offs);
 352:     int current = wb.previous();
 353: 
 354:     while (current != BreakIterator.DONE)
 355:       {
 356:         for (int i = last; i < offs; i++)
 357:           {
 358:             // FIXME: Should use isLetter(int) and text.codePointAt(int)
 359:             // instead, but isLetter(int) isn't implemented yet
 360:             if (Character.isLetter(text.charAt(i)))
 361:               return last;
 362:           }
 363:         last = current;
 364:         current = wb.previous();
 365:       }
 366:     return 0;
 367:   }
 368:   
 369:   /**
 370:    * Finds the start of a word for the given location.
 371:    * @param c the text component
 372:    * @param offs the offset location
 373:    * @return the location of the word beginning
 374:    * @throws BadLocationException if the offset location is invalid
 375:    */
 376:   public static final int getWordStart(JTextComponent c, int offs)
 377:       throws BadLocationException
 378:   {
 379:     if (offs < 0 || offs >= c.getText().length())
 380:       throw new BadLocationException("invalid offset specified", offs);
 381:     
 382:     String text = c.getText();
 383:     BreakIterator wb = BreakIterator.getWordInstance();
 384:     wb.setText(text);
 385:     if (wb.isBoundary(offs))
 386:       return offs;
 387:     return wb.preceding(offs);
 388:   }
 389:   
 390:   /**
 391:    * Finds the end of a word for the given location.
 392:    * @param c the text component
 393:    * @param offs the offset location
 394:    * @return the location of the word end
 395:    * @throws BadLocationException if the offset location is invalid
 396:    */
 397:   public static final int getWordEnd(JTextComponent c, int offs)
 398:       throws BadLocationException
 399:   {
 400:     if (offs < 0 || offs >= c.getText().length())
 401:       throw new BadLocationException("invalid offset specified", offs);
 402:     
 403:     String text = c.getText();
 404:     BreakIterator wb = BreakIterator.getWordInstance();
 405:     wb.setText(text);
 406:     return wb.following(offs);
 407:   }
 408:   
 409:   /**
 410:    * Get the model position of the end of the row that contains the 
 411:    * specified model position.  Return null if the given JTextComponent
 412:    * does not have a size.
 413:    * @param c the JTextComponent
 414:    * @param offs the model position
 415:    * @return the model position of the end of the row containing the given 
 416:    * offset
 417:    * @throws BadLocationException if the offset is invalid
 418:    */
 419:   public static final int getRowEnd(JTextComponent c, int offs)
 420:       throws BadLocationException
 421:   {
 422:     String text = c.getText();
 423:     if (text == null)
 424:       return -1;
 425: 
 426:     // Do a binary search for the smallest position X > offs
 427:     // such that that character at positino X is not on the same
 428:     // line as the character at position offs
 429:     int high = offs + ((text.length() - 1 - offs) / 2);
 430:     int low = offs;
 431:     int oldHigh = text.length() + 1;
 432:     while (true)
 433:       {
 434:         if (c.modelToView(high).y != c.modelToView(offs).y)
 435:           {
 436:             oldHigh = high;
 437:             high = low + ((high + 1 - low) / 2);
 438:             if (oldHigh == high)
 439:               return high - 1;
 440:           }
 441:         else
 442:           {
 443:             low = high;
 444:             high += ((oldHigh - high) / 2);
 445:             if (low == high)
 446:               return low;
 447:           }
 448:       }
 449:   }
 450:       
 451:   /**
 452:    * Get the model position of the start of the row that contains the specified
 453:    * model position. Return null if the given JTextComponent does not have a
 454:    * size.
 455:    * 
 456:    * @param c the JTextComponent
 457:    * @param offs the model position
 458:    * @return the model position of the start of the row containing the given
 459:    *         offset
 460:    * @throws BadLocationException if the offset is invalid
 461:    */
 462:   public static final int getRowStart(JTextComponent c, int offs)
 463:       throws BadLocationException
 464:   {
 465:     String text = c.getText();
 466:     if (text == null)
 467:       return -1;
 468: 
 469:     // Do a binary search for the greatest position X < offs
 470:     // such that the character at position X is not on the same
 471:     // row as the character at position offs
 472:     int high = offs;
 473:     int low = 0;
 474:     int oldLow = 0;
 475:     while (true)
 476:       {
 477:         if (c.modelToView(low).y != c.modelToView(offs).y)
 478:           {
 479:             oldLow = low;
 480:             low = high - ((high + 1 - low) / 2);
 481:             if (oldLow == low)
 482:               return low + 1;
 483:           }
 484:         else
 485:           {
 486:             high = low;
 487:             low -= ((low - oldLow) / 2);
 488:             if (low == high)
 489:               return low;
 490:           }
 491:       }
 492:   }
 493:   
 494:   /**
 495:    * Determine where to break the text in the given Segment, attempting to find
 496:    * a word boundary.
 497:    * @param s the Segment that holds the text
 498:    * @param metrics the font metrics used for calculating the break point
 499:    * @param x0 starting view location representing the start of the text
 500:    * @param x the target view location
 501:    * @param e the TabExpander used for expanding tabs (if this is null tabs
 502:    * are expanded to 1 space)
 503:    * @param startOffset the offset in the Document of the start of the text
 504:    * @return the offset at which we should break the text
 505:    */
 506:   public static final int getBreakLocation(Segment s, FontMetrics metrics,
 507:                                            int x0, int x, TabExpander e,
 508:                                            int startOffset)
 509:   {
 510:     int mark = Utilities.getTabbedTextOffset(s, metrics, x0, x, e, startOffset);
 511:     BreakIterator breaker = BreakIterator.getWordInstance();
 512:     breaker.setText(s.toString());
 513:     
 514:     // If mark is equal to the end of the string, just use that position
 515:     if (mark == s.count)
 516:       return mark;
 517:     
 518:     // Try to find a word boundary previous to the mark at which we 
 519:     // can break the text
 520:     int preceding = breaker.preceding(mark + 1);
 521:     
 522:     if (preceding != 0)
 523:       return preceding;
 524:     else
 525:       // If preceding is 0 we couldn't find a suitable word-boundary so
 526:       // just break it on the character boundary
 527:       return mark;
 528:   }
 529: 
 530:   /**
 531:    * Returns the paragraph element in the text component <code>c</code> at
 532:    * the specified location <code>offset</code>.
 533:    *
 534:    * @param c the text component
 535:    * @param offset the offset of the paragraph element to return
 536:    *
 537:    * @return the paragraph element at <code>offset</code>
 538:    */
 539:   public static final Element getParagraphElement(JTextComponent c, int offset)
 540:   {
 541:     Document doc = c.getDocument();
 542:     Element par = null;
 543:     if (doc instanceof StyledDocument)
 544:       {
 545:         StyledDocument styledDoc = (StyledDocument) doc;
 546:         par = styledDoc.getParagraphElement(offset);
 547:       }
 548:     else
 549:       {
 550:         Element root = c.getDocument().getDefaultRootElement();
 551:         int parIndex = root.getElementIndex(offset);
 552:         par = root.getElement(parIndex);
 553:       }
 554:     return par;
 555:   }
 556: 
 557:   /**
 558:    * Returns the document position that is closest above to the specified x
 559:    * coordinate in the row containing <code>offset</code>.
 560:    *
 561:    * @param c the text component
 562:    * @param offset the offset
 563:    * @param x the x coordinate
 564:    *
 565:    * @return  the document position that is closest above to the specified x
 566:    *          coordinate in the row containing <code>offset</code>
 567:    *
 568:    * @throws BadLocationException if <code>offset</code> is not a valid offset
 569:    */
 570:   public static final int getPositionAbove(JTextComponent c, int offset, int x)
 571:     throws BadLocationException
 572:   {
 573:     View rootView = c.getUI().getRootView(c);
 574:     Rectangle r = c.modelToView(offset);
 575:     int offs = c.viewToModel(new Point(x, r.y));
 576:     int pos = rootView.getNextVisualPositionFrom(c, offs,
 577:                                                  Position.Bias.Forward,
 578:                                                  SwingConstants.NORTH,
 579:                                                  new Position.Bias[1]);
 580:     return pos;
 581:   }
 582: 
 583:   /**
 584:    * Returns the document position that is closest below to the specified x
 585:    * coordinate in the row containing <code>offset</code>.
 586:    *
 587:    * @param c the text component
 588:    * @param offset the offset
 589:    * @param x the x coordinate
 590:    *
 591:    * @return  the document position that is closest above to the specified x
 592:    *          coordinate in the row containing <code>offset</code>
 593:    *
 594:    * @throws BadLocationException if <code>offset</code> is not a valid offset
 595:    */
 596:   public static final int getPositionBelow(JTextComponent c, int offset, int x)
 597:     throws BadLocationException
 598:   {
 599:     View rootView = c.getUI().getRootView(c);
 600:     Rectangle r = c.modelToView(offset);
 601:     int offs = c.viewToModel(new Point(x, r.y));
 602:     int pos = rootView.getNextVisualPositionFrom(c, offs,
 603:                                                  Position.Bias.Forward,
 604:                                                  SwingConstants.SOUTH,
 605:                                                  new Position.Bias[1]);
 606:     return pos;
 607:   }
 608: }