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