1 /* 2 * Copyright (c) 1997, 2011, 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 int offset = ((round == false) || ((x - currX) < (nextX - x))) ? 394 (i - txtOffset) : (i + 1 - txtOffset); 395 // the length of the string measured as a whole may differ from 396 // the sum of individual character lengths, for example if 397 // fractional metrics are enabled; and we must guard from this. 398 while (metrics.charsWidth(txt, txtOffset, offset + 1) > (x - x0)) { 399 offset--; 400 } 401 return (offset < 0 ? 0 : offset); 402 } 403 currX = nextX; 404 } 405 406 // didn't find, return end offset 407 return txtCount; 408 } 409 410 /** 411 * Determine where to break the given text to fit 412 * within the given span. This tries to find a word boundary. 413 * @param s the source of the text 414 * @param metrics the font metrics to use for the calculation 415 * @param x0 the starting view location representing the start 416 * of the given text. 417 * @param x the target view location to translate to an 418 * offset into the text. 419 * @param e how to expand the tabs. If this value is null, 420 * tabs will be expanded as a space character. 421 * @param startOffset starting offset in the document of the text 422 * @return the offset into the given text 423 */ 424 public static final int getBreakLocation(Segment s, FontMetrics metrics, 425 int x0, int x, TabExpander e, 426 int startOffset) { 427 char[] txt = s.array; 428 int txtOffset = s.offset; 429 int txtCount = s.count; 430 int index = Utilities.getTabbedTextOffset(s, metrics, x0, x, 431 e, startOffset, false); 432 433 if (index >= txtCount - 1) { 434 return txtCount; 435 } 436 437 for (int i = txtOffset + index; i >= txtOffset; i--) { 438 char ch = txt[i]; 439 if (ch < 256) { 440 // break on whitespace 441 if (Character.isWhitespace(ch)) { 442 index = i - txtOffset + 1; 443 break; 444 } 445 } else { 446 // a multibyte char found; use BreakIterator to find line break 447 BreakIterator bit = BreakIterator.getLineInstance(); 448 bit.setText(s); 449 int breakPos = bit.preceding(i + 1); 450 if (breakPos > txtOffset) { 451 index = breakPos - txtOffset; 452 } 453 break; 454 } 455 } 456 return index; 457 } 458 459 /** 460 * Determines the starting row model position of the row that contains 461 * the specified model position. The component given must have a 462 * size to compute the result. If the component doesn't have a size 463 * a value of -1 will be returned. 464 * 465 * @param c the editor 466 * @param offs the offset in the document >= 0 467 * @return the position >= 0 if the request can be computed, otherwise 468 * a value of -1 will be returned. 469 * @exception BadLocationException if the offset is out of range 470 */ 471 public static final int getRowStart(JTextComponent c, int offs) throws BadLocationException { 472 Rectangle r = c.modelToView(offs); 473 if (r == null) { 474 return -1; 475 } 476 int lastOffs = offs; 477 int y = r.y; 478 while ((r != null) && (y == r.y)) { 479 // Skip invisible elements 480 if(r.height !=0) { 481 offs = lastOffs; 482 } 483 lastOffs -= 1; 484 r = (lastOffs >= 0) ? c.modelToView(lastOffs) : null; 485 } 486 return offs; 487 } 488 489 /** 490 * Determines the ending row model position of the row that contains 491 * the specified model position. The component given must have a 492 * size to compute the result. If the component doesn't have a size 493 * a value of -1 will be returned. 494 * 495 * @param c the editor 496 * @param offs the offset in the document >= 0 497 * @return the position >= 0 if the request can be computed, otherwise 498 * a value of -1 will be returned. 499 * @exception BadLocationException if the offset is out of range 500 */ 501 public static final int getRowEnd(JTextComponent c, int offs) throws BadLocationException { 502 Rectangle r = c.modelToView(offs); 503 if (r == null) { 504 return -1; 505 } 506 int n = c.getDocument().getLength(); 507 int lastOffs = offs; 508 int y = r.y; 509 while ((r != null) && (y == r.y)) { 510 // Skip invisible elements 511 if (r.height !=0) { 512 offs = lastOffs; 513 } 514 lastOffs += 1; 515 r = (lastOffs <= n) ? c.modelToView(lastOffs) : null; 516 } 517 return offs; 518 } 519 520 /** 521 * Determines the position in the model that is closest to the given 522 * view location in the row above. The component given must have a 523 * size to compute the result. If the component doesn't have a size 524 * a value of -1 will be returned. 525 * 526 * @param c the editor 527 * @param offs the offset in the document >= 0 528 * @param x the X coordinate >= 0 529 * @return the position >= 0 if the request can be computed, otherwise 530 * a value of -1 will be returned. 531 * @exception BadLocationException if the offset is out of range 532 */ 533 public static final int getPositionAbove(JTextComponent c, int offs, int x) throws BadLocationException { 534 int lastOffs = getRowStart(c, offs) - 1; 535 if (lastOffs < 0) { 536 return -1; 537 } 538 int bestSpan = Integer.MAX_VALUE; 539 int y = 0; 540 Rectangle r = null; 541 if (lastOffs >= 0) { 542 r = c.modelToView(lastOffs); 543 y = r.y; 544 } 545 while ((r != null) && (y == r.y)) { 546 int span = Math.abs(r.x - x); 547 if (span < bestSpan) { 548 offs = lastOffs; 549 bestSpan = span; 550 } 551 lastOffs -= 1; 552 r = (lastOffs >= 0) ? c.modelToView(lastOffs) : null; 553 } 554 return offs; 555 } 556 557 /** 558 * Determines the position in the model that is closest to the given 559 * view location in the row below. The component given must have a 560 * size to compute the result. If the component doesn't have a size 561 * a value of -1 will be returned. 562 * 563 * @param c the editor 564 * @param offs the offset in the document >= 0 565 * @param x the X coordinate >= 0 566 * @return the position >= 0 if the request can be computed, otherwise 567 * a value of -1 will be returned. 568 * @exception BadLocationException if the offset is out of range 569 */ 570 public static final int getPositionBelow(JTextComponent c, int offs, int x) throws BadLocationException { 571 int lastOffs = getRowEnd(c, offs) + 1; 572 if (lastOffs <= 0) { 573 return -1; 574 } 575 int bestSpan = Integer.MAX_VALUE; 576 int n = c.getDocument().getLength(); 577 int y = 0; 578 Rectangle r = null; 579 if (lastOffs <= n) { 580 r = c.modelToView(lastOffs); 581 y = r.y; 582 } 583 while ((r != null) && (y == r.y)) { 584 int span = Math.abs(x - r.x); 585 if (span < bestSpan) { 586 offs = lastOffs; 587 bestSpan = span; 588 } 589 lastOffs += 1; 590 r = (lastOffs <= n) ? c.modelToView(lastOffs) : null; 591 } 592 return offs; 593 } 594 595 /** 596 * Determines the start of a word for the given model location. 597 * Uses BreakIterator.getWordInstance() to actually get the words. 598 * 599 * @param c the editor 600 * @param offs the offset in the document >= 0 601 * @return the location in the model of the word start >= 0 602 * @exception BadLocationException if the offset is out of range 603 */ 604 public static final int getWordStart(JTextComponent c, int offs) throws BadLocationException { 605 Document doc = c.getDocument(); 606 Element line = getParagraphElement(c, offs); 607 if (line == null) { 608 throw new BadLocationException("No word at " + offs, offs); 609 } 610 int lineStart = line.getStartOffset(); 611 int lineEnd = Math.min(line.getEndOffset(), doc.getLength()); 612 613 Segment seg = SegmentCache.getSharedSegment(); 614 doc.getText(lineStart, lineEnd - lineStart, seg); 615 if(seg.count > 0) { 616 BreakIterator words = BreakIterator.getWordInstance(c.getLocale()); 617 words.setText(seg); 618 int wordPosition = seg.offset + offs - lineStart; 619 if(wordPosition >= words.last()) { 620 wordPosition = words.last() - 1; 621 } 622 words.following(wordPosition); 623 offs = lineStart + words.previous() - seg.offset; 624 } 625 SegmentCache.releaseSharedSegment(seg); 626 return offs; 627 } 628 629 /** 630 * Determines the end of a word for the given location. 631 * Uses BreakIterator.getWordInstance() to actually get the words. 632 * 633 * @param c the editor 634 * @param offs the offset in the document >= 0 635 * @return the location in the model of the word end >= 0 636 * @exception BadLocationException if the offset is out of range 637 */ 638 public static final int getWordEnd(JTextComponent c, int offs) throws BadLocationException { 639 Document doc = c.getDocument(); 640 Element line = getParagraphElement(c, offs); 641 if (line == null) { 642 throw new BadLocationException("No word at " + offs, offs); 643 } 644 int lineStart = line.getStartOffset(); 645 int lineEnd = Math.min(line.getEndOffset(), doc.getLength()); 646 647 Segment seg = SegmentCache.getSharedSegment(); 648 doc.getText(lineStart, lineEnd - lineStart, seg); 649 if(seg.count > 0) { 650 BreakIterator words = BreakIterator.getWordInstance(c.getLocale()); 651 words.setText(seg); 652 int wordPosition = offs - lineStart + seg.offset; 653 if(wordPosition >= words.last()) { 654 wordPosition = words.last() - 1; 655 } 656 offs = lineStart + words.following(wordPosition) - seg.offset; 657 } 658 SegmentCache.releaseSharedSegment(seg); 659 return offs; 660 } 661 662 /** 663 * Determines the start of the next word for the given location. 664 * Uses BreakIterator.getWordInstance() to actually get the words. 665 * 666 * @param c the editor 667 * @param offs the offset in the document >= 0 668 * @return the location in the model of the word start >= 0 669 * @exception BadLocationException if the offset is out of range 670 */ 671 public static final int getNextWord(JTextComponent c, int offs) throws BadLocationException { 672 int nextWord; 673 Element line = getParagraphElement(c, offs); 674 for (nextWord = getNextWordInParagraph(c, line, offs, false); 675 nextWord == BreakIterator.DONE; 676 nextWord = getNextWordInParagraph(c, line, offs, true)) { 677 678 // didn't find in this line, try the next line 679 offs = line.getEndOffset(); 680 line = getParagraphElement(c, offs); 681 } 682 return nextWord; 683 } 684 685 /** 686 * Finds the next word in the given elements text. The first 687 * parameter allows searching multiple paragraphs where even 688 * the first offset is desired. 689 * Returns the offset of the next word, or BreakIterator.DONE 690 * if there are no more words in the element. 691 */ 692 static int getNextWordInParagraph(JTextComponent c, Element line, int offs, boolean first) throws BadLocationException { 693 if (line == null) { 694 throw new BadLocationException("No more words", offs); 695 } 696 Document doc = line.getDocument(); 697 int lineStart = line.getStartOffset(); 698 int lineEnd = Math.min(line.getEndOffset(), doc.getLength()); 699 if ((offs >= lineEnd) || (offs < lineStart)) { 700 throw new BadLocationException("No more words", offs); 701 } 702 Segment seg = SegmentCache.getSharedSegment(); 703 doc.getText(lineStart, lineEnd - lineStart, seg); 704 BreakIterator words = BreakIterator.getWordInstance(c.getLocale()); 705 words.setText(seg); 706 if ((first && (words.first() == (seg.offset + offs - lineStart))) && 707 (! Character.isWhitespace(seg.array[words.first()]))) { 708 709 return offs; 710 } 711 int wordPosition = words.following(seg.offset + offs - lineStart); 712 if ((wordPosition == BreakIterator.DONE) || 713 (wordPosition >= seg.offset + seg.count)) { 714 // there are no more words on this line. 715 return BreakIterator.DONE; 716 } 717 // if we haven't shot past the end... check to 718 // see if the current boundary represents whitespace. 719 // if so, we need to try again 720 char ch = seg.array[wordPosition]; 721 if (! Character.isWhitespace(ch)) { 722 return lineStart + wordPosition - seg.offset; 723 } 724 725 // it was whitespace, try again. The assumption 726 // is that it must be a word start if the last 727 // one had whitespace following it. 728 wordPosition = words.next(); 729 if (wordPosition != BreakIterator.DONE) { 730 offs = lineStart + wordPosition - seg.offset; 731 if (offs != lineEnd) { 732 return offs; 733 } 734 } 735 SegmentCache.releaseSharedSegment(seg); 736 return BreakIterator.DONE; 737 } 738 739 740 /** 741 * Determine the start of the prev word for the given location. 742 * Uses BreakIterator.getWordInstance() to actually get the words. 743 * 744 * @param c the editor 745 * @param offs the offset in the document >= 0 746 * @return the location in the model of the word start >= 0 747 * @exception BadLocationException if the offset is out of range 748 */ 749 public static final int getPreviousWord(JTextComponent c, int offs) throws BadLocationException { 750 int prevWord; 751 Element line = getParagraphElement(c, offs); 752 for (prevWord = getPrevWordInParagraph(c, line, offs); 753 prevWord == BreakIterator.DONE; 754 prevWord = getPrevWordInParagraph(c, line, offs)) { 755 756 // didn't find in this line, try the prev line 757 offs = line.getStartOffset() - 1; 758 line = getParagraphElement(c, offs); 759 } 760 return prevWord; 761 } 762 763 /** 764 * Finds the previous word in the given elements text. The first 765 * parameter allows searching multiple paragraphs where even 766 * the first offset is desired. 767 * Returns the offset of the next word, or BreakIterator.DONE 768 * if there are no more words in the element. 769 */ 770 static int getPrevWordInParagraph(JTextComponent c, Element line, int offs) throws BadLocationException { 771 if (line == null) { 772 throw new BadLocationException("No more words", offs); 773 } 774 Document doc = line.getDocument(); 775 int lineStart = line.getStartOffset(); 776 int lineEnd = line.getEndOffset(); 777 if ((offs > lineEnd) || (offs < lineStart)) { 778 throw new BadLocationException("No more words", offs); 779 } 780 Segment seg = SegmentCache.getSharedSegment(); 781 doc.getText(lineStart, lineEnd - lineStart, seg); 782 BreakIterator words = BreakIterator.getWordInstance(c.getLocale()); 783 words.setText(seg); 784 if (words.following(seg.offset + offs - lineStart) == BreakIterator.DONE) { 785 words.last(); 786 } 787 int wordPosition = words.previous(); 788 if (wordPosition == (seg.offset + offs - lineStart)) { 789 wordPosition = words.previous(); 790 } 791 792 if (wordPosition == BreakIterator.DONE) { 793 // there are no more words on this line. 794 return BreakIterator.DONE; 795 } 796 // if we haven't shot past the end... check to 797 // see if the current boundary represents whitespace. 798 // if so, we need to try again 799 char ch = seg.array[wordPosition]; 800 if (! Character.isWhitespace(ch)) { 801 return lineStart + wordPosition - seg.offset; 802 } 803 804 // it was whitespace, try again. The assumption 805 // is that it must be a word start if the last 806 // one had whitespace following it. 807 wordPosition = words.previous(); 808 if (wordPosition != BreakIterator.DONE) { 809 return lineStart + wordPosition - seg.offset; 810 } 811 SegmentCache.releaseSharedSegment(seg); 812 return BreakIterator.DONE; 813 } 814 815 /** 816 * Determines the element to use for a paragraph/line. 817 * 818 * @param c the editor 819 * @param offs the starting offset in the document >= 0 820 * @return the element 821 */ 822 public static final Element getParagraphElement(JTextComponent c, int offs) { 823 Document doc = c.getDocument(); 824 if (doc instanceof StyledDocument) { 825 return ((StyledDocument)doc).getParagraphElement(offs); 826 } 827 Element map = doc.getDefaultRootElement(); 828 int index = map.getElementIndex(offs); 829 Element paragraph = map.getElement(index); 830 if ((offs >= paragraph.getStartOffset()) && (offs < paragraph.getEndOffset())) { 831 return paragraph; 832 } 833 return null; 834 } 835 836 static boolean isComposedTextElement(Document doc, int offset) { 837 Element elem = doc.getDefaultRootElement(); 838 while (!elem.isLeaf()) { 839 elem = elem.getElement(elem.getElementIndex(offset)); 840 } 841 return isComposedTextElement(elem); 842 } 843 844 static boolean isComposedTextElement(Element elem) { 845 AttributeSet as = elem.getAttributes(); 846 return isComposedTextAttributeDefined(as); 847 } 848 849 static boolean isComposedTextAttributeDefined(AttributeSet as) { 850 return ((as != null) && 851 (as.isDefined(StyleConstants.ComposedTextAttribute))); 852 } 853 854 /** 855 * Draws the given composed text passed from an input method. 856 * 857 * @param view View hosting text 858 * @param attr the attributes containing the composed text 859 * @param g the graphics context 860 * @param x the X origin 861 * @param y the Y origin 862 * @param p0 starting offset in the composed text to be rendered 863 * @param p1 ending offset in the composed text to be rendered 864 * @return the new insertion position 865 */ 866 static int drawComposedText(View view, AttributeSet attr, Graphics g, 867 int x, int y, int p0, int p1) 868 throws BadLocationException { 869 Graphics2D g2d = (Graphics2D)g; 870 AttributedString as = (AttributedString)attr.getAttribute( 871 StyleConstants.ComposedTextAttribute); 872 as.addAttribute(TextAttribute.FONT, g.getFont()); 873 874 if (p0 >= p1) 875 return x; 876 877 AttributedCharacterIterator aci = as.getIterator(null, p0, p1); 878 return x + (int)SwingUtilities2.drawString( 879 getJComponent(view), g2d,aci,x,y); 880 } 881 882 /** 883 * Paints the composed text in a GlyphView 884 */ 885 static void paintComposedText(Graphics g, Rectangle alloc, GlyphView v) { 886 if (g instanceof Graphics2D) { 887 Graphics2D g2d = (Graphics2D) g; 888 int p0 = v.getStartOffset(); 889 int p1 = v.getEndOffset(); 890 AttributeSet attrSet = v.getElement().getAttributes(); 891 AttributedString as = 892 (AttributedString)attrSet.getAttribute(StyleConstants.ComposedTextAttribute); 893 int start = v.getElement().getStartOffset(); 894 int y = alloc.y + alloc.height - (int)v.getGlyphPainter().getDescent(v); 895 int x = alloc.x; 896 897 //Add text attributes 898 as.addAttribute(TextAttribute.FONT, v.getFont()); 899 as.addAttribute(TextAttribute.FOREGROUND, v.getForeground()); 900 if (StyleConstants.isBold(v.getAttributes())) { 901 as.addAttribute(TextAttribute.WEIGHT, TextAttribute.WEIGHT_BOLD); 902 } 903 if (StyleConstants.isItalic(v.getAttributes())) { 904 as.addAttribute(TextAttribute.POSTURE, TextAttribute.POSTURE_OBLIQUE); 905 } 906 if (v.isUnderline()) { 907 as.addAttribute(TextAttribute.UNDERLINE, TextAttribute.UNDERLINE_ON); 908 } 909 if (v.isStrikeThrough()) { 910 as.addAttribute(TextAttribute.STRIKETHROUGH, TextAttribute.STRIKETHROUGH_ON); 911 } 912 if (v.isSuperscript()) { 913 as.addAttribute(TextAttribute.SUPERSCRIPT, TextAttribute.SUPERSCRIPT_SUPER); 914 } 915 if (v.isSubscript()) { 916 as.addAttribute(TextAttribute.SUPERSCRIPT, TextAttribute.SUPERSCRIPT_SUB); 917 } 918 919 // draw 920 AttributedCharacterIterator aci = as.getIterator(null, p0 - start, p1 - start); 921 SwingUtilities2.drawString(getJComponent(v), 922 g2d,aci,x,y); 923 } 924 } 925 926 /* 927 * Convenience function for determining ComponentOrientation. Helps us 928 * avoid having Munge directives throughout the code. 929 */ 930 static boolean isLeftToRight( java.awt.Component c ) { 931 return c.getComponentOrientation().isLeftToRight(); 932 } 933 934 935 /** 936 * Provides a way to determine the next visually represented model 937 * location that one might place a caret. Some views may not be visible, 938 * they might not be in the same order found in the model, or they just 939 * might not allow access to some of the locations in the model. 940 * <p> 941 * This implementation assumes the views are layed out in a logical 942 * manner. That is, that the view at index x + 1 is visually after 943 * the View at index x, and that the View at index x - 1 is visually 944 * before the View at x. There is support for reversing this behavior 945 * only if the passed in <code>View</code> is an instance of 946 * <code>CompositeView</code>. The <code>CompositeView</code> 947 * must then override the <code>flipEastAndWestAtEnds</code> method. 948 * 949 * @param v View to query 950 * @param pos the position to convert >= 0 951 * @param a the allocated region to render into 952 * @param direction the direction from the current position that can 953 * be thought of as the arrow keys typically found on a keyboard; 954 * this may be one of the following: 955 * <ul> 956 * <li><code>SwingConstants.WEST</code> 957 * <li><code>SwingConstants.EAST</code> 958 * <li><code>SwingConstants.NORTH</code> 959 * <li><code>SwingConstants.SOUTH</code> 960 * </ul> 961 * @param biasRet an array contain the bias that was checked 962 * @return the location within the model that best represents the next 963 * location visual position 964 * @exception BadLocationException 965 * @exception IllegalArgumentException if <code>direction</code> is invalid 966 */ 967 static int getNextVisualPositionFrom(View v, int pos, Position.Bias b, 968 Shape alloc, int direction, 969 Position.Bias[] biasRet) 970 throws BadLocationException { 971 if (v.getViewCount() == 0) { 972 // Nothing to do. 973 return pos; 974 } 975 boolean top = (direction == SwingConstants.NORTH || 976 direction == SwingConstants.WEST); 977 int retValue; 978 if (pos == -1) { 979 // Start from the first View. 980 int childIndex = (top) ? v.getViewCount() - 1 : 0; 981 View child = v.getView(childIndex); 982 Shape childBounds = v.getChildAllocation(childIndex, alloc); 983 retValue = child.getNextVisualPositionFrom(pos, b, childBounds, 984 direction, biasRet); 985 if (retValue == -1 && !top && v.getViewCount() > 1) { 986 // Special case that should ONLY happen if first view 987 // isn't valid (can happen when end position is put at 988 // beginning of line. 989 child = v.getView(1); 990 childBounds = v.getChildAllocation(1, alloc); 991 retValue = child.getNextVisualPositionFrom(-1, biasRet[0], 992 childBounds, 993 direction, biasRet); 994 } 995 } 996 else { 997 int increment = (top) ? -1 : 1; 998 int childIndex; 999 if (b == Position.Bias.Backward && pos > 0) { 1000 childIndex = v.getViewIndex(pos - 1, Position.Bias.Forward); 1001 } 1002 else { 1003 childIndex = v.getViewIndex(pos, Position.Bias.Forward); 1004 } 1005 View child = v.getView(childIndex); 1006 Shape childBounds = v.getChildAllocation(childIndex, alloc); 1007 retValue = child.getNextVisualPositionFrom(pos, b, childBounds, 1008 direction, biasRet); 1009 if ((direction == SwingConstants.EAST || 1010 direction == SwingConstants.WEST) && 1011 (v instanceof CompositeView) && 1012 ((CompositeView)v).flipEastAndWestAtEnds(pos, b)) { 1013 increment *= -1; 1014 } 1015 childIndex += increment; 1016 if (retValue == -1 && childIndex >= 0 && 1017 childIndex < v.getViewCount()) { 1018 child = v.getView(childIndex); 1019 childBounds = v.getChildAllocation(childIndex, alloc); 1020 retValue = child.getNextVisualPositionFrom( 1021 -1, b, childBounds, direction, biasRet); 1022 // If there is a bias change, it is a fake position 1023 // and we should skip it. This is usually the result 1024 // of two elements side be side flowing the same way. 1025 if (retValue == pos && biasRet[0] != b) { 1026 return getNextVisualPositionFrom(v, pos, biasRet[0], 1027 alloc, direction, 1028 biasRet); 1029 } 1030 } 1031 else if (retValue != -1 && biasRet[0] != b && 1032 ((increment == 1 && child.getEndOffset() == retValue) || 1033 (increment == -1 && 1034 child.getStartOffset() == retValue)) && 1035 childIndex >= 0 && childIndex < v.getViewCount()) { 1036 // Reached the end of a view, make sure the next view 1037 // is a different direction. 1038 child = v.getView(childIndex); 1039 childBounds = v.getChildAllocation(childIndex, alloc); 1040 Position.Bias originalBias = biasRet[0]; 1041 int nextPos = child.getNextVisualPositionFrom( 1042 -1, b, childBounds, direction, biasRet); 1043 if (biasRet[0] == b) { 1044 retValue = nextPos; 1045 } 1046 else { 1047 biasRet[0] = originalBias; 1048 } 1049 } 1050 } 1051 return retValue; 1052 } 1053 }