1 /* 2 * Copyright (c) 1997, 2006, Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. Oracle designates this 8 * particular file as subject to the "Classpath" exception as provided 9 * by Oracle in the LICENSE file that accompanied this code. 10 * 11 * This code is distributed in the hope that it will be useful, but WITHOUT 12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 14 * version 2 for more details (a copy is included in the LICENSE file that 15 * accompanied this code). 16 * 17 * You should have received a copy of the GNU General Public License version 18 * 2 along with this work; if not, write to the Free Software Foundation, 19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 20 * 21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 22 * or visit www.oracle.com if you need additional information or have any 23 * questions. 24 */ 25 package javax.swing.text; 26 27 import java.lang.reflect.Method; 28 29 import java.awt.Component; 30 import java.awt.Rectangle; 31 import java.awt.Graphics; 32 import java.awt.FontMetrics; 33 import java.awt.Shape; 34 import java.awt.Toolkit; 35 import java.awt.Graphics2D; 36 import java.awt.font.FontRenderContext; 37 import java.awt.font.TextLayout; 38 import java.awt.font.TextAttribute; 39 40 import java.text.*; 41 import javax.swing.JComponent; 42 import javax.swing.SwingConstants; 43 import javax.swing.text.ParagraphView.Row; 44 import sun.swing.SwingUtilities2; 45 46 /** 47 * A collection of methods to deal with various text 48 * related activities. 49 * 50 * @author Timothy Prinzing 51 */ 52 public class Utilities { 53 /** 54 * If <code>view</code>'s container is a <code>JComponent</code> it 55 * is returned, after casting. 56 */ 57 static JComponent getJComponent(View view) { 58 if (view != null) { 59 Component component = view.getContainer(); 60 if (component instanceof JComponent) { 61 return (JComponent)component; 62 } 63 } 64 return null; 65 } 66 67 /** 68 * Draws the given text, expanding any tabs that are contained 69 * using the given tab expansion technique. This particular 70 * implementation renders in a 1.1 style coordinate system 71 * where ints are used and 72dpi is assumed. 72 * 73 * @param s the source of the text 74 * @param x the X origin >= 0 75 * @param y the Y origin >= 0 76 * @param g the graphics context 77 * @param e how to expand the tabs. If this value is null, 78 * tabs will be expanded as a space character. 79 * @param startOffset starting offset of the text in the document >= 0 80 * @return the X location at the end of the rendered text 81 */ 82 public static final int drawTabbedText(Segment s, int x, int y, Graphics g, 83 TabExpander e, int startOffset) { 84 return drawTabbedText(null, s, x, y, g, e, startOffset); 85 } 86 87 /** 88 * Draws the given text, expanding any tabs that are contained 89 * using the given tab expansion technique. This particular 90 * implementation renders in a 1.1 style coordinate system 91 * where ints are used and 72dpi is assumed. 92 * 93 * @param view View requesting rendering, may be null. 94 * @param s the source of the text 95 * @param x the X origin >= 0 96 * @param y the Y origin >= 0 97 * @param g the graphics context 98 * @param e how to expand the tabs. If this value is null, 99 * tabs will be expanded as a space character. 100 * @param startOffset starting offset of the text in the document >= 0 101 * @return the X location at the end of the rendered text 102 */ 103 static final int drawTabbedText(View view, 104 Segment s, int x, int y, Graphics g, 105 TabExpander e, int startOffset) { 106 return drawTabbedText(view, s, x, y, g, e, startOffset, null); 107 } 108 109 // In addition to the previous method it can extend spaces for 110 // justification. 111 // 112 // all params are the same as in the preious method except the last 113 // one: 114 // @param justificationData justificationData for the row. 115 // if null not justification is needed 116 static final int drawTabbedText(View view, 117 Segment s, int x, int y, Graphics g, 118 TabExpander e, int startOffset, 119 int [] justificationData) { 120 JComponent component = getJComponent(view); 121 FontMetrics metrics = SwingUtilities2.getFontMetrics(component, g); 122 int nextX = x; 123 char[] txt = s.array; 124 int txtOffset = s.offset; 125 int flushLen = 0; 126 int flushIndex = s.offset; 127 int spaceAddon = 0; 128 int spaceAddonLeftoverEnd = -1; 129 int startJustifiableContent = 0; 130 int endJustifiableContent = 0; 131 if (justificationData != null) { 132 int offset = - startOffset + txtOffset; 133 View parent = null; 134 if (view != null 135 && (parent = view.getParent()) != null) { 136 offset += parent.getStartOffset(); 137 } 138 spaceAddon = 139 justificationData[Row.SPACE_ADDON]; 140 spaceAddonLeftoverEnd = 141 justificationData[Row.SPACE_ADDON_LEFTOVER_END] + offset; 142 startJustifiableContent = 143 justificationData[Row.START_JUSTIFIABLE] + offset; 144 endJustifiableContent = 145 justificationData[Row.END_JUSTIFIABLE] + offset; 146 } 147 int n = s.offset + s.count; 148 for (int i = txtOffset; i < n; i++) { 149 if (txt[i] == '\t' 150 || ((spaceAddon != 0 || i <= spaceAddonLeftoverEnd) 151 && (txt[i] == ' ') 152 && startJustifiableContent <= i 153 && i <= endJustifiableContent 154 )) { 155 if (flushLen > 0) { 156 nextX = SwingUtilities2.drawChars(component, g, txt, 157 flushIndex, flushLen, x, y); 158 flushLen = 0; 159 } 160 flushIndex = i + 1; 161 if (txt[i] == '\t') { 162 if (e != null) { 163 nextX = (int) e.nextTabStop((float) nextX, startOffset + i - txtOffset); 164 } else { 165 nextX += metrics.charWidth(' '); 166 } 167 } else if (txt[i] == ' ') { 168 nextX += metrics.charWidth(' ') + spaceAddon; 169 if (i <= spaceAddonLeftoverEnd) { 170 nextX++; 171 } 172 } 173 x = nextX; 174 } else if ((txt[i] == '\n') || (txt[i] == '\r')) { 175 if (flushLen > 0) { 176 nextX = SwingUtilities2.drawChars(component, g, txt, 177 flushIndex, flushLen, x, y); 178 flushLen = 0; 179 } 180 flushIndex = i + 1; 181 x = nextX; 182 } else { 183 flushLen += 1; 184 } 185 } 186 if (flushLen > 0) { 187 nextX = SwingUtilities2.drawChars(component, g,txt, flushIndex, 188 flushLen, x, y); 189 } 190 return nextX; 191 } 192 193 /** 194 * Determines the width of the given segment of text taking tabs 195 * into consideration. This is implemented in a 1.1 style coordinate 196 * system where ints are used and 72dpi is assumed. 197 * 198 * @param s the source of the text 199 * @param metrics the font metrics to use for the calculation 200 * @param x the X origin >= 0 201 * @param e how to expand the tabs. If this value is null, 202 * tabs will be expanded as a space character. 203 * @param startOffset starting offset of the text in the document >= 0 204 * @return the width of the text 205 */ 206 public static final int getTabbedTextWidth(Segment s, FontMetrics metrics, int x, 207 TabExpander e, int startOffset) { 208 return getTabbedTextWidth(null, s, metrics, x, e, startOffset, null); 209 } 210 211 212 // In addition to the previous method it can extend spaces for 213 // justification. 214 // 215 // all params are the same as in the preious method except the last 216 // one: 217 // @param justificationData justificationData for the row. 218 // if null not justification is needed 219 static final int getTabbedTextWidth(View view, Segment s, FontMetrics metrics, int x, 220 TabExpander e, int startOffset, 221 int[] justificationData) { 222 int nextX = x; 223 char[] txt = s.array; 224 int txtOffset = s.offset; 225 int n = s.offset + s.count; 226 int charCount = 0; 227 int spaceAddon = 0; 228 int spaceAddonLeftoverEnd = -1; 229 int startJustifiableContent = 0; 230 int endJustifiableContent = 0; 231 if (justificationData != null) { 232 int offset = - startOffset + txtOffset; 233 View parent = null; 234 if (view != null 235 && (parent = view.getParent()) != null) { 236 offset += parent.getStartOffset(); 237 } 238 spaceAddon = 239 justificationData[Row.SPACE_ADDON]; 240 spaceAddonLeftoverEnd = 241 justificationData[Row.SPACE_ADDON_LEFTOVER_END] + offset; 242 startJustifiableContent = 243 justificationData[Row.START_JUSTIFIABLE] + offset; 244 endJustifiableContent = 245 justificationData[Row.END_JUSTIFIABLE] + offset; 246 } 247 248 for (int i = txtOffset; i < n; i++) { 249 if (txt[i] == '\t' 250 || ((spaceAddon != 0 || i <= spaceAddonLeftoverEnd) 251 && (txt[i] == ' ') 252 && startJustifiableContent <= i 253 && i <= endJustifiableContent 254 )) { 255 nextX += metrics.charsWidth(txt, i-charCount, charCount); 256 charCount = 0; 257 if (txt[i] == '\t') { 258 if (e != null) { 259 nextX = (int) e.nextTabStop((float) nextX, 260 startOffset + i - txtOffset); 261 } else { 262 nextX += metrics.charWidth(' '); 263 } 264 } else if (txt[i] == ' ') { 265 nextX += metrics.charWidth(' ') + spaceAddon; 266 if (i <= spaceAddonLeftoverEnd) { 267 nextX++; 268 } 269 } 270 } else if(txt[i] == '\n') { 271 // Ignore newlines, they take up space and we shouldn't be 272 // counting them. 273 nextX += metrics.charsWidth(txt, i - charCount, charCount); 274 charCount = 0; 275 } else { 276 charCount++; 277 } 278 } 279 nextX += metrics.charsWidth(txt, n - charCount, charCount); 280 return nextX - x; 281 } 282 283 /** 284 * Determines the relative offset into the given text that 285 * best represents the given span in the view coordinate 286 * system. This is implemented in a 1.1 style coordinate 287 * system where ints are used and 72dpi is assumed. 288 * 289 * @param s the source of the text 290 * @param metrics the font metrics to use for the calculation 291 * @param x0 the starting view location representing the start 292 * of the given text >= 0. 293 * @param x the target view location to translate to an 294 * offset into the text >= 0. 295 * @param e how to expand the tabs. If this value is null, 296 * tabs will be expanded as a space character. 297 * @param startOffset starting offset of the text in the document >= 0 298 * @return the offset into the text >= 0 299 */ 300 public static final int getTabbedTextOffset(Segment s, FontMetrics metrics, 301 int x0, int x, TabExpander e, 302 int startOffset) { 303 return getTabbedTextOffset(s, metrics, x0, x, e, startOffset, true); 304 } 305 306 static final int getTabbedTextOffset(View view, Segment s, FontMetrics metrics, 307 int x0, int x, TabExpander e, 308 int startOffset, 309 int[] justificationData) { 310 return getTabbedTextOffset(view, s, metrics, x0, x, e, startOffset, true, 311 justificationData); 312 } 313 314 public static final int getTabbedTextOffset(Segment s, 315 FontMetrics metrics, 316 int x0, int x, TabExpander e, 317 int startOffset, 318 boolean round) { 319 return getTabbedTextOffset(null, s, metrics, x0, x, e, startOffset, round, null); 320 } 321 322 // In addition to the previous method it can extend spaces for 323 // justification. 324 // 325 // all params are the same as in the preious method except the last 326 // one: 327 // @param justificationData justificationData for the row. 328 // if null not justification is needed 329 static final int getTabbedTextOffset(View view, 330 Segment s, 331 FontMetrics metrics, 332 int x0, int x, TabExpander e, 333 int startOffset, 334 boolean round, 335 int[] justificationData) { 336 if (x0 >= x) { 337 // x before x0, return. 338 return 0; 339 } 340 int currX = x0; 341 int nextX = currX; 342 // s may be a shared segment, so it is copied prior to calling 343 // the tab expander 344 char[] txt = s.array; 345 int txtOffset = s.offset; 346 int txtCount = s.count; 347 int spaceAddon = 0 ; 348 int spaceAddonLeftoverEnd = -1; 349 int startJustifiableContent = 0 ; 350 int endJustifiableContent = 0; 351 if (justificationData != null) { 352 int offset = - startOffset + txtOffset; 353 View parent = null; 354 if (view != null 355 && (parent = view.getParent()) != null) { 356 offset += parent.getStartOffset(); 357 } 358 spaceAddon = 359 justificationData[Row.SPACE_ADDON]; 360 spaceAddonLeftoverEnd = 361 justificationData[Row.SPACE_ADDON_LEFTOVER_END] + offset; 362 startJustifiableContent = 363 justificationData[Row.START_JUSTIFIABLE] + offset; 364 endJustifiableContent = 365 justificationData[Row.END_JUSTIFIABLE] + offset; 366 } 367 int n = s.offset + s.count; 368 for (int i = s.offset; i < n; i++) { 369 if (txt[i] == '\t' 370 || ((spaceAddon != 0 || i <= spaceAddonLeftoverEnd) 371 && (txt[i] == ' ') 372 && startJustifiableContent <= i 373 && i <= endJustifiableContent 374 )){ 375 if (txt[i] == '\t') { 376 if (e != null) { 377 nextX = (int) e.nextTabStop((float) nextX, 378 startOffset + i - txtOffset); 379 } else { 380 nextX += metrics.charWidth(' '); 381 } 382 } else if (txt[i] == ' ') { 383 nextX += metrics.charWidth(' ') + spaceAddon; 384 if (i <= spaceAddonLeftoverEnd) { 385 nextX++; 386 } 387 } 388 } else { 389 nextX += metrics.charWidth(txt[i]); 390 } 391 if ((x >= currX) && (x < nextX)) { 392 // found the hit position... return the appropriate side 393 if ((round == false) || ((x - currX) < (nextX - x))) { 394 return i - txtOffset; 395 } else { 396 return i + 1 - txtOffset; 397 } 398 } 399 currX = nextX; 400 } 401 402 // didn't find, return end offset 403 return txtCount; 404 } 405 406 /** 407 * Adjust text offset so that the length of a resulting string as a whole 408 * fits into the specified width. 409 */ 410 static int adjustOffsetForFractionalMetrics( 411 Segment s, FontMetrics fm, int offset, int width) { 412 // Sometimes the offset returned by getTabbedTextOffset is beyond the 413 // available area, when fractional metrics are enabled. We should 414 // guard against this. 415 if (offset < s.count) { 416 while (offset > 0 && 417 fm.charsWidth(s.array, s.offset, offset + 1) > width) { 418 offset--; 419 } 420 } 421 return offset; 422 } 423 424 /** 425 * Determine where to break the given text to fit 426 * within the given span. This tries to find a word boundary. 427 * @param s the source of the text 428 * @param metrics the font metrics to use for the calculation 429 * @param x0 the starting view location representing the start 430 * of the given text. 431 * @param x the target view location to translate to an 432 * offset into the text. 433 * @param e how to expand the tabs. If this value is null, 434 * tabs will be expanded as a space character. 435 * @param startOffset starting offset in the document of the text 436 * @return the offset into the given text 437 */ 438 public static final int getBreakLocation(Segment s, FontMetrics metrics, 439 int x0, int x, TabExpander e, 440 int startOffset) { 441 char[] txt = s.array; 442 int txtOffset = s.offset; 443 int txtCount = s.count; 444 int index = Utilities.getTabbedTextOffset(s, metrics, x0, x, 445 e, startOffset, false); 446 index = adjustOffsetForFractionalMetrics(s, metrics, index, x - x0); 447 448 if (index >= txtCount - 1) { 449 return txtCount; 450 } 451 452 for (int i = txtOffset + index; i >= txtOffset; i--) { 453 char ch = txt[i]; 454 if (ch < 256) { 455 // break on whitespace 456 if (Character.isWhitespace(ch)) { 457 index = i - txtOffset + 1; 458 break; 459 } 460 } else { 461 // a multibyte char found; use BreakIterator to find line break 462 BreakIterator bit = BreakIterator.getLineInstance(); 463 bit.setText(s); 464 int breakPos = bit.preceding(i + 1); 465 if (breakPos > txtOffset) { 466 index = breakPos - txtOffset; 467 } 468 break; 469 } 470 } 471 return index; 472 } 473 474 /** 475 * Determines the starting row model position of the row that contains 476 * the specified model position. The component given must have a 477 * size to compute the result. If the component doesn't have a size 478 * a value of -1 will be returned. 479 * 480 * @param c the editor 481 * @param offs the offset in the document >= 0 482 * @return the position >= 0 if the request can be computed, otherwise 483 * a value of -1 will be returned. 484 * @exception BadLocationException if the offset is out of range 485 */ 486 public static final int getRowStart(JTextComponent c, int offs) throws BadLocationException { 487 Rectangle r = c.modelToView(offs); 488 if (r == null) { 489 return -1; 490 } 491 int lastOffs = offs; 492 int y = r.y; 493 while ((r != null) && (y == r.y)) { 494 // Skip invisible elements 495 if(r.height !=0) { 496 offs = lastOffs; 497 } 498 lastOffs -= 1; 499 r = (lastOffs >= 0) ? c.modelToView(lastOffs) : null; 500 } 501 return offs; 502 } 503 504 /** 505 * Determines the ending row model position of the row that contains 506 * the specified model position. The component given must have a 507 * size to compute the result. If the component doesn't have a size 508 * a value of -1 will be returned. 509 * 510 * @param c the editor 511 * @param offs the offset in the document >= 0 512 * @return the position >= 0 if the request can be computed, otherwise 513 * a value of -1 will be returned. 514 * @exception BadLocationException if the offset is out of range 515 */ 516 public static final int getRowEnd(JTextComponent c, int offs) throws BadLocationException { 517 Rectangle r = c.modelToView(offs); 518 if (r == null) { 519 return -1; 520 } 521 int n = c.getDocument().getLength(); 522 int lastOffs = offs; 523 int y = r.y; 524 while ((r != null) && (y == r.y)) { 525 // Skip invisible elements 526 if (r.height !=0) { 527 offs = lastOffs; 528 } 529 lastOffs += 1; 530 r = (lastOffs <= n) ? c.modelToView(lastOffs) : null; 531 } 532 return offs; 533 } 534 535 /** 536 * Determines the position in the model that is closest to the given 537 * view location in the row above. The component given must have a 538 * size to compute the result. If the component doesn't have a size 539 * a value of -1 will be returned. 540 * 541 * @param c the editor 542 * @param offs the offset in the document >= 0 543 * @param x the X coordinate >= 0 544 * @return the position >= 0 if the request can be computed, otherwise 545 * a value of -1 will be returned. 546 * @exception BadLocationException if the offset is out of range 547 */ 548 public static final int getPositionAbove(JTextComponent c, int offs, int x) throws BadLocationException { 549 int lastOffs = getRowStart(c, offs) - 1; 550 if (lastOffs < 0) { 551 return -1; 552 } 553 int bestSpan = Integer.MAX_VALUE; 554 int y = 0; 555 Rectangle r = null; 556 if (lastOffs >= 0) { 557 r = c.modelToView(lastOffs); 558 y = r.y; 559 } 560 while ((r != null) && (y == r.y)) { 561 int span = Math.abs(r.x - x); 562 if (span < bestSpan) { 563 offs = lastOffs; 564 bestSpan = span; 565 } 566 lastOffs -= 1; 567 r = (lastOffs >= 0) ? c.modelToView(lastOffs) : null; 568 } 569 return offs; 570 } 571 572 /** 573 * Determines the position in the model that is closest to the given 574 * view location in the row below. The component given must have a 575 * size to compute the result. If the component doesn't have a size 576 * a value of -1 will be returned. 577 * 578 * @param c the editor 579 * @param offs the offset in the document >= 0 580 * @param x the X coordinate >= 0 581 * @return the position >= 0 if the request can be computed, otherwise 582 * a value of -1 will be returned. 583 * @exception BadLocationException if the offset is out of range 584 */ 585 public static final int getPositionBelow(JTextComponent c, int offs, int x) throws BadLocationException { 586 int lastOffs = getRowEnd(c, offs) + 1; 587 if (lastOffs <= 0) { 588 return -1; 589 } 590 int bestSpan = Integer.MAX_VALUE; 591 int n = c.getDocument().getLength(); 592 int y = 0; 593 Rectangle r = null; 594 if (lastOffs <= n) { 595 r = c.modelToView(lastOffs); 596 y = r.y; 597 } 598 while ((r != null) && (y == r.y)) { 599 int span = Math.abs(x - r.x); 600 if (span < bestSpan) { 601 offs = lastOffs; 602 bestSpan = span; 603 } 604 lastOffs += 1; 605 r = (lastOffs <= n) ? c.modelToView(lastOffs) : null; 606 } 607 return offs; 608 } 609 610 /** 611 * Determines the start of a word for the given model location. 612 * Uses BreakIterator.getWordInstance() to actually get the words. 613 * 614 * @param c the editor 615 * @param offs the offset in the document >= 0 616 * @return the location in the model of the word start >= 0 617 * @exception BadLocationException if the offset is out of range 618 */ 619 public static final int getWordStart(JTextComponent c, int offs) throws BadLocationException { 620 Document doc = c.getDocument(); 621 Element line = getParagraphElement(c, offs); 622 if (line == null) { 623 throw new BadLocationException("No word at " + offs, offs); 624 } 625 int lineStart = line.getStartOffset(); 626 int lineEnd = Math.min(line.getEndOffset(), doc.getLength()); 627 628 Segment seg = SegmentCache.getSharedSegment(); 629 doc.getText(lineStart, lineEnd - lineStart, seg); 630 if(seg.count > 0) { 631 BreakIterator words = BreakIterator.getWordInstance(c.getLocale()); 632 words.setText(seg); 633 int wordPosition = seg.offset + offs - lineStart; 634 if(wordPosition >= words.last()) { 635 wordPosition = words.last() - 1; 636 } 637 words.following(wordPosition); 638 offs = lineStart + words.previous() - seg.offset; 639 } 640 SegmentCache.releaseSharedSegment(seg); 641 return offs; 642 } 643 644 /** 645 * Determines the end of a word for the given location. 646 * Uses BreakIterator.getWordInstance() to actually get the words. 647 * 648 * @param c the editor 649 * @param offs the offset in the document >= 0 650 * @return the location in the model of the word end >= 0 651 * @exception BadLocationException if the offset is out of range 652 */ 653 public static final int getWordEnd(JTextComponent c, int offs) throws BadLocationException { 654 Document doc = c.getDocument(); 655 Element line = getParagraphElement(c, offs); 656 if (line == null) { 657 throw new BadLocationException("No word at " + offs, offs); 658 } 659 int lineStart = line.getStartOffset(); 660 int lineEnd = Math.min(line.getEndOffset(), doc.getLength()); 661 662 Segment seg = SegmentCache.getSharedSegment(); 663 doc.getText(lineStart, lineEnd - lineStart, seg); 664 if(seg.count > 0) { 665 BreakIterator words = BreakIterator.getWordInstance(c.getLocale()); 666 words.setText(seg); 667 int wordPosition = offs - lineStart + seg.offset; 668 if(wordPosition >= words.last()) { 669 wordPosition = words.last() - 1; 670 } 671 offs = lineStart + words.following(wordPosition) - seg.offset; 672 } 673 SegmentCache.releaseSharedSegment(seg); 674 return offs; 675 } 676 677 /** 678 * Determines the start of the next word for the given location. 679 * Uses BreakIterator.getWordInstance() to actually get the words. 680 * 681 * @param c the editor 682 * @param offs the offset in the document >= 0 683 * @return the location in the model of the word start >= 0 684 * @exception BadLocationException if the offset is out of range 685 */ 686 public static final int getNextWord(JTextComponent c, int offs) throws BadLocationException { 687 int nextWord; 688 Element line = getParagraphElement(c, offs); 689 for (nextWord = getNextWordInParagraph(c, line, offs, false); 690 nextWord == BreakIterator.DONE; 691 nextWord = getNextWordInParagraph(c, line, offs, true)) { 692 693 // didn't find in this line, try the next line 694 offs = line.getEndOffset(); 695 line = getParagraphElement(c, offs); 696 } 697 return nextWord; 698 } 699 700 /** 701 * Finds the next word in the given elements text. The first 702 * parameter allows searching multiple paragraphs where even 703 * the first offset is desired. 704 * Returns the offset of the next word, or BreakIterator.DONE 705 * if there are no more words in the element. 706 */ 707 static int getNextWordInParagraph(JTextComponent c, Element line, int offs, boolean first) throws BadLocationException { 708 if (line == null) { 709 throw new BadLocationException("No more words", offs); 710 } 711 Document doc = line.getDocument(); 712 int lineStart = line.getStartOffset(); 713 int lineEnd = Math.min(line.getEndOffset(), doc.getLength()); 714 if ((offs >= lineEnd) || (offs < lineStart)) { 715 throw new BadLocationException("No more words", offs); 716 } 717 Segment seg = SegmentCache.getSharedSegment(); 718 doc.getText(lineStart, lineEnd - lineStart, seg); 719 BreakIterator words = BreakIterator.getWordInstance(c.getLocale()); 720 words.setText(seg); 721 if ((first && (words.first() == (seg.offset + offs - lineStart))) && 722 (! Character.isWhitespace(seg.array[words.first()]))) { 723 724 return offs; 725 } 726 int wordPosition = words.following(seg.offset + offs - lineStart); 727 if ((wordPosition == BreakIterator.DONE) || 728 (wordPosition >= seg.offset + seg.count)) { 729 // there are no more words on this line. 730 return BreakIterator.DONE; 731 } 732 // if we haven't shot past the end... check to 733 // see if the current boundary represents whitespace. 734 // if so, we need to try again 735 char ch = seg.array[wordPosition]; 736 if (! Character.isWhitespace(ch)) { 737 return lineStart + wordPosition - seg.offset; 738 } 739 740 // it was whitespace, try again. The assumption 741 // is that it must be a word start if the last 742 // one had whitespace following it. 743 wordPosition = words.next(); 744 if (wordPosition != BreakIterator.DONE) { 745 offs = lineStart + wordPosition - seg.offset; 746 if (offs != lineEnd) { 747 return offs; 748 } 749 } 750 SegmentCache.releaseSharedSegment(seg); 751 return BreakIterator.DONE; 752 } 753 754 755 /** 756 * Determine the start of the prev word for the given location. 757 * Uses BreakIterator.getWordInstance() to actually get the words. 758 * 759 * @param c the editor 760 * @param offs the offset in the document >= 0 761 * @return the location in the model of the word start >= 0 762 * @exception BadLocationException if the offset is out of range 763 */ 764 public static final int getPreviousWord(JTextComponent c, int offs) throws BadLocationException { 765 int prevWord; 766 Element line = getParagraphElement(c, offs); 767 for (prevWord = getPrevWordInParagraph(c, line, offs); 768 prevWord == BreakIterator.DONE; 769 prevWord = getPrevWordInParagraph(c, line, offs)) { 770 771 // didn't find in this line, try the prev line 772 offs = line.getStartOffset() - 1; 773 line = getParagraphElement(c, offs); 774 } 775 return prevWord; 776 } 777 778 /** 779 * Finds the previous word in the given elements text. The first 780 * parameter allows searching multiple paragraphs where even 781 * the first offset is desired. 782 * Returns the offset of the next word, or BreakIterator.DONE 783 * if there are no more words in the element. 784 */ 785 static int getPrevWordInParagraph(JTextComponent c, Element line, int offs) throws BadLocationException { 786 if (line == null) { 787 throw new BadLocationException("No more words", offs); 788 } 789 Document doc = line.getDocument(); 790 int lineStart = line.getStartOffset(); 791 int lineEnd = line.getEndOffset(); 792 if ((offs > lineEnd) || (offs < lineStart)) { 793 throw new BadLocationException("No more words", offs); 794 } 795 Segment seg = SegmentCache.getSharedSegment(); 796 doc.getText(lineStart, lineEnd - lineStart, seg); 797 BreakIterator words = BreakIterator.getWordInstance(c.getLocale()); 798 words.setText(seg); 799 if (words.following(seg.offset + offs - lineStart) == BreakIterator.DONE) { 800 words.last(); 801 } 802 int wordPosition = words.previous(); 803 if (wordPosition == (seg.offset + offs - lineStart)) { 804 wordPosition = words.previous(); 805 } 806 807 if (wordPosition == BreakIterator.DONE) { 808 // there are no more words on this line. 809 return BreakIterator.DONE; 810 } 811 // if we haven't shot past the end... check to 812 // see if the current boundary represents whitespace. 813 // if so, we need to try again 814 char ch = seg.array[wordPosition]; 815 if (! Character.isWhitespace(ch)) { 816 return lineStart + wordPosition - seg.offset; 817 } 818 819 // it was whitespace, try again. The assumption 820 // is that it must be a word start if the last 821 // one had whitespace following it. 822 wordPosition = words.previous(); 823 if (wordPosition != BreakIterator.DONE) { 824 return lineStart + wordPosition - seg.offset; 825 } 826 SegmentCache.releaseSharedSegment(seg); 827 return BreakIterator.DONE; 828 } 829 830 /** 831 * Determines the element to use for a paragraph/line. 832 * 833 * @param c the editor 834 * @param offs the starting offset in the document >= 0 835 * @return the element 836 */ 837 public static final Element getParagraphElement(JTextComponent c, int offs) { 838 Document doc = c.getDocument(); 839 if (doc instanceof StyledDocument) { 840 return ((StyledDocument)doc).getParagraphElement(offs); 841 } 842 Element map = doc.getDefaultRootElement(); 843 int index = map.getElementIndex(offs); 844 Element paragraph = map.getElement(index); 845 if ((offs >= paragraph.getStartOffset()) && (offs < paragraph.getEndOffset())) { 846 return paragraph; 847 } 848 return null; 849 } 850 851 static boolean isComposedTextElement(Document doc, int offset) { 852 Element elem = doc.getDefaultRootElement(); 853 while (!elem.isLeaf()) { 854 elem = elem.getElement(elem.getElementIndex(offset)); 855 } 856 return isComposedTextElement(elem); 857 } 858 859 static boolean isComposedTextElement(Element elem) { 860 AttributeSet as = elem.getAttributes(); 861 return isComposedTextAttributeDefined(as); 862 } 863 864 static boolean isComposedTextAttributeDefined(AttributeSet as) { 865 return ((as != null) && 866 (as.isDefined(StyleConstants.ComposedTextAttribute))); 867 } 868 869 /** 870 * Draws the given composed text passed from an input method. 871 * 872 * @param view View hosting text 873 * @param attr the attributes containing the composed text 874 * @param g the graphics context 875 * @param x the X origin 876 * @param y the Y origin 877 * @param p0 starting offset in the composed text to be rendered 878 * @param p1 ending offset in the composed text to be rendered 879 * @return the new insertion position 880 */ 881 static int drawComposedText(View view, AttributeSet attr, Graphics g, 882 int x, int y, int p0, int p1) 883 throws BadLocationException { 884 Graphics2D g2d = (Graphics2D)g; 885 AttributedString as = (AttributedString)attr.getAttribute( 886 StyleConstants.ComposedTextAttribute); 887 as.addAttribute(TextAttribute.FONT, g.getFont()); 888 889 if (p0 >= p1) 890 return x; 891 892 AttributedCharacterIterator aci = as.getIterator(null, p0, p1); 893 return x + (int)SwingUtilities2.drawString( 894 getJComponent(view), g2d,aci,x,y); 895 } 896 897 /** 898 * Paints the composed text in a GlyphView 899 */ 900 static void paintComposedText(Graphics g, Rectangle alloc, GlyphView v) { 901 if (g instanceof Graphics2D) { 902 Graphics2D g2d = (Graphics2D) g; 903 int p0 = v.getStartOffset(); 904 int p1 = v.getEndOffset(); 905 AttributeSet attrSet = v.getElement().getAttributes(); 906 AttributedString as = 907 (AttributedString)attrSet.getAttribute(StyleConstants.ComposedTextAttribute); 908 int start = v.getElement().getStartOffset(); 909 int y = alloc.y + alloc.height - (int)v.getGlyphPainter().getDescent(v); 910 int x = alloc.x; 911 912 //Add text attributes 913 as.addAttribute(TextAttribute.FONT, v.getFont()); 914 as.addAttribute(TextAttribute.FOREGROUND, v.getForeground()); 915 if (StyleConstants.isBold(v.getAttributes())) { 916 as.addAttribute(TextAttribute.WEIGHT, TextAttribute.WEIGHT_BOLD); 917 } 918 if (StyleConstants.isItalic(v.getAttributes())) { 919 as.addAttribute(TextAttribute.POSTURE, TextAttribute.POSTURE_OBLIQUE); 920 } 921 if (v.isUnderline()) { 922 as.addAttribute(TextAttribute.UNDERLINE, TextAttribute.UNDERLINE_ON); 923 } 924 if (v.isStrikeThrough()) { 925 as.addAttribute(TextAttribute.STRIKETHROUGH, TextAttribute.STRIKETHROUGH_ON); 926 } 927 if (v.isSuperscript()) { 928 as.addAttribute(TextAttribute.SUPERSCRIPT, TextAttribute.SUPERSCRIPT_SUPER); 929 } 930 if (v.isSubscript()) { 931 as.addAttribute(TextAttribute.SUPERSCRIPT, TextAttribute.SUPERSCRIPT_SUB); 932 } 933 934 // draw 935 AttributedCharacterIterator aci = as.getIterator(null, p0 - start, p1 - start); 936 SwingUtilities2.drawString(getJComponent(v), 937 g2d,aci,x,y); 938 } 939 } 940 941 /* 942 * Convenience function for determining ComponentOrientation. Helps us 943 * avoid having Munge directives throughout the code. 944 */ 945 static boolean isLeftToRight( java.awt.Component c ) { 946 return c.getComponentOrientation().isLeftToRight(); 947 } 948 949 950 /** 951 * Provides a way to determine the next visually represented model 952 * location that one might place a caret. Some views may not be visible, 953 * they might not be in the same order found in the model, or they just 954 * might not allow access to some of the locations in the model. 955 * <p> 956 * This implementation assumes the views are layed out in a logical 957 * manner. That is, that the view at index x + 1 is visually after 958 * the View at index x, and that the View at index x - 1 is visually 959 * before the View at x. There is support for reversing this behavior 960 * only if the passed in <code>View</code> is an instance of 961 * <code>CompositeView</code>. The <code>CompositeView</code> 962 * must then override the <code>flipEastAndWestAtEnds</code> method. 963 * 964 * @param v View to query 965 * @param pos the position to convert >= 0 966 * @param a the allocated region to render into 967 * @param direction the direction from the current position that can 968 * be thought of as the arrow keys typically found on a keyboard; 969 * this may be one of the following: 970 * <ul> 971 * <li><code>SwingConstants.WEST</code> 972 * <li><code>SwingConstants.EAST</code> 973 * <li><code>SwingConstants.NORTH</code> 974 * <li><code>SwingConstants.SOUTH</code> 975 * </ul> 976 * @param biasRet an array contain the bias that was checked 977 * @return the location within the model that best represents the next 978 * location visual position 979 * @exception BadLocationException 980 * @exception IllegalArgumentException if <code>direction</code> is invalid 981 */ 982 static int getNextVisualPositionFrom(View v, int pos, Position.Bias b, 983 Shape alloc, int direction, 984 Position.Bias[] biasRet) 985 throws BadLocationException { 986 if (v.getViewCount() == 0) { 987 // Nothing to do. 988 return pos; 989 } 990 boolean top = (direction == SwingConstants.NORTH || 991 direction == SwingConstants.WEST); 992 int retValue; 993 if (pos == -1) { 994 // Start from the first View. 995 int childIndex = (top) ? v.getViewCount() - 1 : 0; 996 View child = v.getView(childIndex); 997 Shape childBounds = v.getChildAllocation(childIndex, alloc); 998 retValue = child.getNextVisualPositionFrom(pos, b, childBounds, 999 direction, biasRet); 1000 if (retValue == -1 && !top && v.getViewCount() > 1) { 1001 // Special case that should ONLY happen if first view 1002 // isn't valid (can happen when end position is put at 1003 // beginning of line. 1004 child = v.getView(1); 1005 childBounds = v.getChildAllocation(1, alloc); 1006 retValue = child.getNextVisualPositionFrom(-1, biasRet[0], 1007 childBounds, 1008 direction, biasRet); 1009 } 1010 } 1011 else { 1012 int increment = (top) ? -1 : 1; 1013 int childIndex; 1014 if (b == Position.Bias.Backward && pos > 0) { 1015 childIndex = v.getViewIndex(pos - 1, Position.Bias.Forward); 1016 } 1017 else { 1018 childIndex = v.getViewIndex(pos, Position.Bias.Forward); 1019 } 1020 View child = v.getView(childIndex); 1021 Shape childBounds = v.getChildAllocation(childIndex, alloc); 1022 retValue = child.getNextVisualPositionFrom(pos, b, childBounds, 1023 direction, biasRet); 1024 if ((direction == SwingConstants.EAST || 1025 direction == SwingConstants.WEST) && 1026 (v instanceof CompositeView) && 1027 ((CompositeView)v).flipEastAndWestAtEnds(pos, b)) { 1028 increment *= -1; 1029 } 1030 childIndex += increment; 1031 if (retValue == -1 && childIndex >= 0 && 1032 childIndex < v.getViewCount()) { 1033 child = v.getView(childIndex); 1034 childBounds = v.getChildAllocation(childIndex, alloc); 1035 retValue = child.getNextVisualPositionFrom( 1036 -1, b, childBounds, direction, biasRet); 1037 // If there is a bias change, it is a fake position 1038 // and we should skip it. This is usually the result 1039 // of two elements side be side flowing the same way. 1040 if (retValue == pos && biasRet[0] != b) { 1041 return getNextVisualPositionFrom(v, pos, biasRet[0], 1042 alloc, direction, 1043 biasRet); 1044 } 1045 } 1046 else if (retValue != -1 && biasRet[0] != b && 1047 ((increment == 1 && child.getEndOffset() == retValue) || 1048 (increment == -1 && 1049 child.getStartOffset() == retValue)) && 1050 childIndex >= 0 && childIndex < v.getViewCount()) { 1051 // Reached the end of a view, make sure the next view 1052 // is a different direction. 1053 child = v.getView(childIndex); 1054 childBounds = v.getChildAllocation(childIndex, alloc); 1055 Position.Bias originalBias = biasRet[0]; 1056 int nextPos = child.getNextVisualPositionFrom( 1057 -1, b, childBounds, direction, biasRet); 1058 if (biasRet[0] == b) { 1059 retValue = nextPos; 1060 } 1061 else { 1062 biasRet[0] = originalBias; 1063 } 1064 } 1065 } 1066 return retValue; 1067 } 1068 }