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