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