001/* Utilities.java -- 002 Copyright (C) 2004, 2005, 2006 Free Software Foundation, Inc. 003 004This file is part of GNU Classpath. 005 006GNU Classpath is free software; you can redistribute it and/or modify 007it under the terms of the GNU General Public License as published by 008the Free Software Foundation; either version 2, or (at your option) 009any later version. 010 011GNU Classpath is distributed in the hope that it will be useful, but 012WITHOUT ANY WARRANTY; without even the implied warranty of 013MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 014General Public License for more details. 015 016You should have received a copy of the GNU General Public License 017along with GNU Classpath; see the file COPYING. If not, write to the 018Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 01902110-1301 USA. 020 021Linking this library statically or dynamically with other modules is 022making a combined work based on this library. Thus, the terms and 023conditions of the GNU General Public License cover the whole 024combination. 025 026As a special exception, the copyright holders of this library give you 027permission to link this library with independent modules to produce an 028executable, regardless of the license terms of these independent 029modules, and to copy and distribute the resulting executable under 030terms of your choice, provided that you also meet, for each linked 031independent module, the terms and conditions of the license of that 032module. An independent module is a module which is not derived from 033or based on this library. If you modify this library, you may extend 034this exception to your version of the library, but you are not 035obligated to do so. If you do not wish to do so, delete this 036exception statement from your version. */ 037 038 039package javax.swing.text; 040 041import java.awt.FontMetrics; 042import java.awt.Graphics; 043import java.awt.Point; 044import java.text.BreakIterator; 045 046import javax.swing.text.Position.Bias; 047 048/** 049 * A set of utilities to deal with text. This is used by several other classes 050 * inside this package. 051 * 052 * @author Roman Kennke (roman@ontographics.com) 053 * @author Robert Schuster (robertschuster@fsfe.org) 054 */ 055public class Utilities 056{ 057 058 /** 059 * Creates a new <code>Utilities</code> object. 060 */ 061 public Utilities() 062 { 063 // Nothing to be done here. 064 } 065 066 /** 067 * Draws the given text segment. Contained tabs and newline characters 068 * are taken into account. Tabs are expanded using the 069 * specified {@link TabExpander}. 070 * 071 * 072 * The X and Y coordinates denote the start of the <em>baseline</em> where 073 * the text should be drawn. 074 * 075 * @param s the text fragment to be drawn. 076 * @param x the x position for drawing. 077 * @param y the y position for drawing. 078 * @param g the {@link Graphics} context for drawing. 079 * @param e the {@link TabExpander} which specifies the Tab-expanding 080 * technique. 081 * @param startOffset starting offset in the text. 082 * @return the x coordinate at the end of the drawn text. 083 */ 084 public static final int drawTabbedText(Segment s, int x, int y, Graphics g, 085 TabExpander e, int startOffset) 086 { 087 // This buffers the chars to be drawn. 088 char[] buffer = s.array; 089 090 // The font metrics of the current selected font. 091 FontMetrics metrics = g.getFontMetrics(); 092 093 int ascent = metrics.getAscent(); 094 095 // The current x and y pixel coordinates. 096 int pixelX = x; 097 098 int pos = s.offset; 099 int len = 0; 100 101 int end = s.offset + s.count; 102 103 for (int offset = s.offset; offset < end; ++offset) 104 { 105 char c = buffer[offset]; 106 switch (c) 107 { 108 case '\t': 109 if (len > 0) { 110 g.drawChars(buffer, pos, len, pixelX, y); 111 pixelX += metrics.charsWidth(buffer, pos, len); 112 len = 0; 113 } 114 pos = offset+1; 115 if (e != null) 116 pixelX = (int) e.nextTabStop((float) pixelX, startOffset + offset 117 - s.offset); 118 else 119 pixelX += metrics.charWidth(' '); 120 x = pixelX; 121 break; 122 case '\n': 123 case '\r': 124 if (len > 0) { 125 g.drawChars(buffer, pos, len, pixelX, y); 126 pixelX += metrics.charsWidth(buffer, pos, len); 127 len = 0; 128 } 129 x = pixelX; 130 break; 131 default: 132 len += 1; 133 } 134 } 135 136 if (len > 0) 137 { 138 g.drawChars(buffer, pos, len, pixelX, y); 139 pixelX += metrics.charsWidth(buffer, pos, len); 140 } 141 142 return pixelX; 143 } 144 145 /** 146 * Determines the width, that the given text <code>s</code> would take 147 * if it was printed with the given {@link java.awt.FontMetrics} on the 148 * specified screen position. 149 * @param s the text fragment 150 * @param metrics the font metrics of the font to be used 151 * @param x the x coordinate of the point at which drawing should be done 152 * @param e the {@link TabExpander} to be used 153 * @param startOffset the index in <code>s</code> where to start 154 * @returns the width of the given text s. This takes tabs and newlines 155 * into account. 156 */ 157 public static final int getTabbedTextWidth(Segment s, FontMetrics metrics, 158 int x, TabExpander e, 159 int startOffset) 160 { 161 // This buffers the chars to be drawn. 162 char[] buffer = s.array; 163 164 // The current x coordinate. 165 int pixelX = x; 166 167 // The current maximum width. 168 int maxWidth = 0; 169 170 int end = s.offset + s.count; 171 int count = 0; 172 for (int offset = s.offset; offset < end; 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(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.charsWidth(buffer, offset - count, count); 189 count = 0; 190 break; 191 default: 192 count++; 193 } 194 } 195 196 // Take the last line into account. 197 pixelX += metrics.charsWidth(buffer, end - count, count); 198 199 return pixelX - x; 200 } 201 202 /** 203 * Provides a facility to map screen coordinates into a model location. For a 204 * given text fragment and start location within this fragment, this method 205 * determines the model location so that the resulting fragment fits best 206 * into the span <code>[x0, x]</code>. 207 * 208 * The parameter <code>round</code> controls which model location is returned 209 * if the view coordinates are on a character: If <code>round</code> is 210 * <code>true</code>, then the result is rounded up to the next character, so 211 * that the resulting fragment is the smallest fragment that is larger than 212 * the specified span. If <code>round</code> is <code>false</code>, then the 213 * resulting fragment is the largest fragment that is smaller than the 214 * specified span. 215 * 216 * @param s the text segment 217 * @param fm the font metrics to use 218 * @param x0 the starting screen location 219 * @param x the target screen location at which the requested fragment should 220 * end 221 * @param te the tab expander to use; if this is <code>null</code>, TABs are 222 * expanded to one space character 223 * @param p0 the starting model location 224 * @param round if <code>true</code> round up to the next location, otherwise 225 * round down to the current location 226 * 227 * @return the model location, so that the resulting fragment fits within the 228 * specified span 229 */ 230 public static final int getTabbedTextOffset(Segment s, FontMetrics fm, int x0, 231 int x, TabExpander te, int p0, 232 boolean round) 233 { 234 int found = s.count; 235 int currentX = x0; 236 int nextX = currentX; 237 238 int end = s.offset + s.count; 239 for (int pos = s.offset; pos < end && found == s.count; pos++) 240 { 241 char nextChar = s.array[pos]; 242 243 if (nextChar != '\t') 244 nextX += fm.charWidth(nextChar); 245 else 246 { 247 if (te == null) 248 nextX += fm.charWidth(' '); 249 else 250 nextX += ((int) te.nextTabStop(nextX, p0 + pos - s.offset)); 251 } 252 253 if (x >= currentX && x < nextX) 254 { 255 // Found position. 256 if ((! round) || ((x - currentX) < (nextX - x))) 257 { 258 found = pos - s.offset; 259 } 260 else 261 { 262 found = pos + 1 - s.offset; 263 } 264 } 265 currentX = nextX; 266 } 267 268 return found; 269 } 270 271 /** 272 * Provides a facility to map screen coordinates into a model location. For a 273 * given text fragment and start location within this fragment, this method 274 * determines the model location so that the resulting fragment fits best 275 * into the span <code>[x0, x]</code>. 276 * 277 * This method rounds up to the next location, so that the resulting fragment 278 * will be the smallest fragment of the text, that is greater than the 279 * specified span. 280 * 281 * @param s the text segment 282 * @param fm the font metrics to use 283 * @param x0 the starting screen location 284 * @param x the target screen location at which the requested fragment should 285 * end 286 * @param te the tab expander to use; if this is <code>null</code>, TABs are 287 * expanded to one space character 288 * @param p0 the starting model location 289 * 290 * @return the model location, so that the resulting fragment fits within the 291 * specified span 292 */ 293 public static final int getTabbedTextOffset(Segment s, FontMetrics fm, int x0, 294 int x, TabExpander te, int p0) 295 { 296 return getTabbedTextOffset(s, fm, x0, x, te, p0, true); 297 } 298 299 /** 300 * Finds the start of the next word for the given offset. 301 * 302 * @param c 303 * the text component 304 * @param offs 305 * the offset in the document 306 * @return the location in the model of the start of the next word. 307 * @throws BadLocationException 308 * if the offset is invalid. 309 */ 310 public static final int getNextWord(JTextComponent c, int offs) 311 throws BadLocationException 312 { 313 if (offs < 0 || offs > (c.getText().length() - 1)) 314 throw new BadLocationException("invalid offset specified", offs); 315 String text = c.getText(); 316 BreakIterator wb = BreakIterator.getWordInstance(); 317 wb.setText(text); 318 319 int last = wb.following(offs); 320 int current = wb.next(); 321 int cp; 322 323 while (current != BreakIterator.DONE) 324 { 325 for (int i = last; i < current; i++) 326 { 327 cp = text.codePointAt(i); 328 329 // Return the last found bound if there is a letter at the current 330 // location or is not whitespace (meaning it is a number or 331 // punctuation). The first case means that 'last' denotes the 332 // beginning of a word while the second case means it is the start 333 // of something else. 334 if (Character.isLetter(cp) 335 || !Character.isWhitespace(cp)) 336 return last; 337 } 338 last = current; 339 current = wb.next(); 340 } 341 342 throw new BadLocationException("no more words", offs); 343 } 344 345 /** 346 * Finds the start of the previous word for the given offset. 347 * 348 * @param c 349 * the text component 350 * @param offs 351 * the offset in the document 352 * @return the location in the model of the start of the previous word. 353 * @throws BadLocationException 354 * if the offset is invalid. 355 */ 356 public static final int getPreviousWord(JTextComponent c, int offs) 357 throws BadLocationException 358 { 359 String text = c.getText(); 360 361 if (offs <= 0 || offs > text.length()) 362 throw new BadLocationException("invalid offset specified", offs); 363 364 BreakIterator wb = BreakIterator.getWordInstance(); 365 wb.setText(text); 366 int last = wb.preceding(offs); 367 int current = wb.previous(); 368 int cp; 369 370 while (current != BreakIterator.DONE) 371 { 372 for (int i = last; i < offs; i++) 373 { 374 cp = text.codePointAt(i); 375 376 // Return the last found bound if there is a letter at the current 377 // location or is not whitespace (meaning it is a number or 378 // punctuation). The first case means that 'last' denotes the 379 // beginning of a word while the second case means it is the start 380 // of some else. 381 if (Character.isLetter(cp) 382 || !Character.isWhitespace(cp)) 383 return last; 384 } 385 last = current; 386 current = wb.previous(); 387 } 388 389 return 0; 390 } 391 392 /** 393 * Finds the start of a word for the given location. 394 * @param c the text component 395 * @param offs the offset location 396 * @return the location of the word beginning 397 * @throws BadLocationException if the offset location is invalid 398 */ 399 public static final int getWordStart(JTextComponent c, int offs) 400 throws BadLocationException 401 { 402 String text = c.getText(); 403 404 if (offs < 0 || offs > text.length()) 405 throw new BadLocationException("invalid offset specified", offs); 406 407 BreakIterator wb = BreakIterator.getWordInstance(); 408 wb.setText(text); 409 410 if (wb.isBoundary(offs)) 411 return offs; 412 413 return wb.preceding(offs); 414 } 415 416 /** 417 * Finds the end of a word for the given location. 418 * @param c the text component 419 * @param offs the offset location 420 * @return the location of the word end 421 * @throws BadLocationException if the offset location is invalid 422 */ 423 public static final int getWordEnd(JTextComponent c, int offs) 424 throws BadLocationException 425 { 426 if (offs < 0 || offs >= c.getText().length()) 427 throw new BadLocationException("invalid offset specified", offs); 428 429 String text = c.getText(); 430 BreakIterator wb = BreakIterator.getWordInstance(); 431 wb.setText(text); 432 return wb.following(offs); 433 } 434 435 /** 436 * Get the model position of the end of the row that contains the 437 * specified model position. Return null if the given JTextComponent 438 * does not have a size. 439 * @param c the JTextComponent 440 * @param offs the model position 441 * @return the model position of the end of the row containing the given 442 * offset 443 * @throws BadLocationException if the offset is invalid 444 */ 445 public static final int getRowEnd(JTextComponent c, int offs) 446 throws BadLocationException 447 { 448 String text = c.getText(); 449 if (text == null) 450 return -1; 451 452 // Do a binary search for the smallest position X > offs 453 // such that that character at positino X is not on the same 454 // line as the character at position offs 455 int high = offs + ((text.length() - 1 - offs) / 2); 456 int low = offs; 457 int oldHigh = text.length() + 1; 458 while (true) 459 { 460 if (c.modelToView(high).y != c.modelToView(offs).y) 461 { 462 oldHigh = high; 463 high = low + ((high + 1 - low) / 2); 464 if (oldHigh == high) 465 return high - 1; 466 } 467 else 468 { 469 low = high; 470 high += ((oldHigh - high) / 2); 471 if (low == high) 472 return low; 473 } 474 } 475 } 476 477 /** 478 * Get the model position of the start of the row that contains the specified 479 * model position. Return null if the given JTextComponent does not have a 480 * size. 481 * 482 * @param c the JTextComponent 483 * @param offs the model position 484 * @return the model position of the start of the row containing the given 485 * offset 486 * @throws BadLocationException if the offset is invalid 487 */ 488 public static final int getRowStart(JTextComponent c, int offs) 489 throws BadLocationException 490 { 491 String text = c.getText(); 492 if (text == null) 493 return -1; 494 495 // Do a binary search for the greatest position X < offs 496 // such that the character at position X is not on the same 497 // row as the character at position offs 498 int high = offs; 499 int low = 0; 500 int oldLow = 0; 501 while (true) 502 { 503 if (c.modelToView(low).y != c.modelToView(offs).y) 504 { 505 oldLow = low; 506 low = high - ((high + 1 - low) / 2); 507 if (oldLow == low) 508 return low + 1; 509 } 510 else 511 { 512 high = low; 513 low -= ((low - oldLow) / 2); 514 if (low == high) 515 return low; 516 } 517 } 518 } 519 520 /** 521 * Determine where to break the text in the given Segment, attempting to find 522 * a word boundary. 523 * @param s the Segment that holds the text 524 * @param metrics the font metrics used for calculating the break point 525 * @param x0 starting view location representing the start of the text 526 * @param x the target view location 527 * @param e the TabExpander used for expanding tabs (if this is null tabs 528 * are expanded to 1 space) 529 * @param startOffset the offset in the Document of the start of the text 530 * @return the offset at which we should break the text 531 */ 532 public static final int getBreakLocation(Segment s, FontMetrics metrics, 533 int x0, int x, TabExpander e, 534 int startOffset) 535 { 536 int mark = Utilities.getTabbedTextOffset(s, metrics, x0, x, e, startOffset, 537 false); 538 int breakLoc = mark; 539 // If mark is equal to the end of the string, just use that position. 540 if (mark < s.count - 1) 541 { 542 for (int i = s.offset + mark; i >= s.offset; i--) 543 { 544 char ch = s.array[i]; 545 if (ch < 256) 546 { 547 // For ASCII simply scan backwards for whitespace. 548 if (Character.isWhitespace(ch)) 549 { 550 breakLoc = i - s.offset + 1; 551 break; 552 } 553 } 554 else 555 { 556 // Only query BreakIterator for complex chars. 557 BreakIterator bi = BreakIterator.getLineInstance(); 558 bi.setText(s); 559 int pos = bi.preceding(i + 1); 560 if (pos > s.offset) 561 { 562 breakLoc = breakLoc - s.offset; 563 } 564 break; 565 } 566 } 567 } 568 return breakLoc; 569 } 570 571 /** 572 * Returns the paragraph element in the text component <code>c</code> at 573 * the specified location <code>offset</code>. 574 * 575 * @param c the text component 576 * @param offset the offset of the paragraph element to return 577 * 578 * @return the paragraph element at <code>offset</code> 579 */ 580 public static final Element getParagraphElement(JTextComponent c, int offset) 581 { 582 Document doc = c.getDocument(); 583 Element par = null; 584 if (doc instanceof StyledDocument) 585 { 586 StyledDocument styledDoc = (StyledDocument) doc; 587 par = styledDoc.getParagraphElement(offset); 588 } 589 else 590 { 591 Element root = c.getDocument().getDefaultRootElement(); 592 int parIndex = root.getElementIndex(offset); 593 par = root.getElement(parIndex); 594 } 595 return par; 596 } 597 598 /** 599 * Returns the document position that is closest above to the specified x 600 * coordinate in the row containing <code>offset</code>. 601 * 602 * @param c the text component 603 * @param offset the offset 604 * @param x the x coordinate 605 * 606 * @return the document position that is closest above to the specified x 607 * coordinate in the row containing <code>offset</code> 608 * 609 * @throws BadLocationException if <code>offset</code> is not a valid offset 610 */ 611 public static final int getPositionAbove(JTextComponent c, int offset, int x) 612 throws BadLocationException 613 { 614 int offs = getRowStart(c, offset); 615 616 if(offs == -1) 617 return -1; 618 619 // Effectively calculates the y value of the previous line. 620 Point pt = c.modelToView(offs-1).getLocation(); 621 622 pt.x = x; 623 624 // Calculate a simple fitting offset. 625 offs = c.viewToModel(pt); 626 627 // Find out the real x positions of the calculated character and its 628 // neighbour. 629 int offsX = c.modelToView(offs).getLocation().x; 630 int offsXNext = c.modelToView(offs+1).getLocation().x; 631 632 // Chose the one which is nearer to us and return its offset. 633 if (Math.abs(offsX-x) <= Math.abs(offsXNext-x)) 634 return offs; 635 else 636 return offs+1; 637 } 638 639 /** 640 * Returns the document position that is closest below to the specified x 641 * coordinate in the row containing <code>offset</code>. 642 * 643 * @param c the text component 644 * @param offset the offset 645 * @param x the x coordinate 646 * 647 * @return the document position that is closest above to the specified x 648 * coordinate in the row containing <code>offset</code> 649 * 650 * @throws BadLocationException if <code>offset</code> is not a valid offset 651 */ 652 public static final int getPositionBelow(JTextComponent c, int offset, int x) 653 throws BadLocationException 654 { 655 int offs = getRowEnd(c, offset); 656 657 if(offs == -1) 658 return -1; 659 660 Point pt = null; 661 662 // Note: Some views represent the position after the last 663 // typed character others do not. Converting offset 3 in "a\nb" 664 // in a PlainView will return a valid rectangle while in a 665 // WrappedPlainView this will throw a BadLocationException. 666 // This behavior has been observed in the RI. 667 try 668 { 669 // Effectively calculates the y value of the next line. 670 pt = c.modelToView(offs+1).getLocation(); 671 } 672 catch(BadLocationException ble) 673 { 674 return offset; 675 } 676 677 pt.x = x; 678 679 // Calculate a simple fitting offset. 680 offs = c.viewToModel(pt); 681 682 if (offs == c.getDocument().getLength()) 683 return offs; 684 685 // Find out the real x positions of the calculated character and its 686 // neighbour. 687 int offsX = c.modelToView(offs).getLocation().x; 688 int offsXNext = c.modelToView(offs+1).getLocation().x; 689 690 // Chose the one which is nearer to us and return its offset. 691 if (Math.abs(offsX-x) <= Math.abs(offsXNext-x)) 692 return offs; 693 else 694 return offs+1; 695 } 696 697 /** This is an internal helper method which is used by the 698 * <code>javax.swing.text</code> package. It simply delegates the 699 * call to a method with the same name on the <code>NavigationFilter</code> 700 * of the provided <code>JTextComponent</code> (if it has one) or its UI. 701 * 702 * If the underlying method throws a <code>BadLocationException</code> it 703 * will be swallowed and the initial offset is returned. 704 */ 705 static int getNextVisualPositionFrom(JTextComponent t, int offset, int direction) 706 { 707 NavigationFilter nf = t.getNavigationFilter(); 708 709 try 710 { 711 return (nf != null) 712 ? nf.getNextVisualPositionFrom(t, 713 offset, 714 Bias.Forward, 715 direction, 716 new Position.Bias[1]) 717 : t.getUI().getNextVisualPositionFrom(t, 718 offset, 719 Bias.Forward, 720 direction, 721 new Position.Bias[1]); 722 } 723 catch (BadLocationException ble) 724 { 725 return offset; 726 } 727 728 } 729 730}