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