Frames | No Frames |
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: import javax.swing.text.Position.Bias; 47: 48: /** 49: * A set of utilities to deal with text. This is used by several other classes 50: * inside this package. 51: * 52: * @author Roman Kennke (roman@ontographics.com) 53: * @author Robert Schuster (robertschuster@fsfe.org) 54: */ 55: public class Utilities 56: { 57: 58: /** 59: * Creates a new <code>Utilities</code> object. 60: */ 61: public Utilities() 62: { 63: // Nothing to be done here. 64: } 65: 66: /** 67: * Draws the given text segment. Contained tabs and newline characters 68: * are taken into account. Tabs are expanded using the 69: * specified {@link TabExpander}. 70: * 71: * 72: * The X and Y coordinates denote the start of the <em>baseline</em> where 73: * the text should be drawn. 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: // The font metrics of the current selected font. 91: FontMetrics metrics = g.getFontMetrics(); 92: 93: int ascent = metrics.getAscent(); 94: 95: // The current x and y pixel coordinates. 96: int pixelX = x; 97: 98: int pos = s.offset; 99: 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: }