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