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.awt.Component; 28 import java.awt.Rectangle; 29 import java.awt.Graphics; 30 import java.awt.FontMetrics; 31 import java.awt.Shape; 32 import java.awt.Graphics2D; 33 import java.awt.font.TextAttribute; 34 import java.awt.geom.Rectangle2D; 35 36 import java.text.*; 37 import javax.swing.JComponent; 38 import javax.swing.SwingConstants; 39 import javax.swing.text.ParagraphView.Row; 40 import sun.swing.SwingUtilities2; 41 import static sun.swing.SwingUtilities2.drawChars; 42 import static sun.swing.SwingUtilities2.getFontCharWidth; 43 import static sun.swing.SwingUtilities2.getFontCharsWidth; 44 45 /** 46 * A collection of methods to deal with various text 47 * related activities. 48 * 49 * @author Timothy Prinzing 50 */ 51 public class Utilities { 52 /** 53 * If <code>view</code>'s container is a <code>JComponent</code> it 54 * is returned, after casting. 55 */ 56 static JComponent getJComponent(View view) { 57 if (view != null) { 58 Component component = view.getContainer(); 59 if (component instanceof JComponent) { 60 return (JComponent)component; 61 } 62 } 63 return null; 64 } 65 66 /** 67 * Draws the given text, expanding any tabs that are contained 68 * using the given tab expansion technique. This particular 69 * implementation renders in a 1.1 style coordinate system 70 * where ints are used and 72dpi is assumed. 71 * 72 * @param s the source of the text 73 * @param x the X origin >= 0 74 * @param y the Y origin >= 0 75 * @param g the graphics context 76 * @param e how to expand the tabs. If this value is null, 77 * tabs will be expanded as a space character. 78 * @param startOffset starting offset of the text in the document >= 0 79 * @return the X location at the end of the rendered text 80 * 81 * @deprecated replaced by 82 * {@link #drawTabbedText(Segment, float, float, Graphics2D, TabExpander, int)} 83 */ 84 @Deprecated(since = "9") 85 public static final int drawTabbedText(Segment s, int x, int y, Graphics g, 86 TabExpander e, int startOffset) { 87 return drawTabbedText(null, s, x, y, g, e, startOffset); 88 } 89 90 /** 91 * Draws the given text, expanding any tabs that are contained 92 * using the given tab expansion technique. 93 * 94 * @param s the source of the text 95 * @param x the X origin {@code >= 0} 96 * @param y the Y origin {@code >= 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 {@code >= 0} 101 * @return the X location at the end of the rendered text 102 * 103 * @since 9 104 */ 105 public static final float drawTabbedText(Segment s, float x, float y, 106 Graphics2D g, 107 TabExpander e, 108 int startOffset) 109 { 110 return drawTabbedText(null, s, x, y, g, e, startOffset, null, true); 111 } 112 113 /** 114 * Draws the given text, expanding any tabs that are contained 115 * using the given tab expansion technique. This particular 116 * implementation renders in a 1.1 style coordinate system 117 * where ints are used and 72dpi is assumed. 118 * 119 * @param view View requesting rendering, may be null. 120 * @param s the source of the text 121 * @param x the X origin >= 0 122 * @param y the Y origin >= 0 123 * @param g the graphics context 124 * @param e how to expand the tabs. If this value is null, 125 * tabs will be expanded as a space character. 126 * @param startOffset starting offset of the text in the document >= 0 127 * @return the X location at the end of the rendered text 128 */ 129 static final int drawTabbedText(View view, 130 Segment s, int x, int y, Graphics g, 131 TabExpander e, int startOffset) { 132 return drawTabbedText(view, s, x, y, g, e, startOffset, null); 133 } 134 135 // In addition to the previous method it can extend spaces for 136 // justification. 137 // 138 // all params are the same as in the preious method except the last 139 // one: 140 // @param justificationData justificationData for the row. 141 // if null not justification is needed 142 static final int drawTabbedText(View view, 143 Segment s, int x, int y, Graphics g, 144 TabExpander e, int startOffset, 145 int [] justificationData) { 146 return (int) drawTabbedText(view, s, x, y, g, e, startOffset, 147 justificationData, false); 148 } 149 150 static final float drawTabbedText(View view, 151 Segment s, float x, float y, Graphics g, 152 TabExpander e, int startOffset, 153 int [] justificationData, 154 boolean useFPAPI) 155 { 156 JComponent component = getJComponent(view); 157 FontMetrics metrics = SwingUtilities2.getFontMetrics(component, g); 158 float nextX = x; 159 char[] txt = s.array; 160 int txtOffset = s.offset; 161 int flushLen = 0; 162 int flushIndex = s.offset; 163 int spaceAddon = 0; 164 int spaceAddonLeftoverEnd = -1; 165 int startJustifiableContent = 0; 166 int endJustifiableContent = 0; 167 if (justificationData != null) { 168 int offset = - startOffset + txtOffset; 169 View parent = null; 170 if (view != null 171 && (parent = view.getParent()) != null) { 172 offset += parent.getStartOffset(); 173 } 174 spaceAddon = 175 justificationData[Row.SPACE_ADDON]; 176 spaceAddonLeftoverEnd = 177 justificationData[Row.SPACE_ADDON_LEFTOVER_END] + offset; 178 startJustifiableContent = 179 justificationData[Row.START_JUSTIFIABLE] + offset; 180 endJustifiableContent = 181 justificationData[Row.END_JUSTIFIABLE] + offset; 182 } 183 int n = s.offset + s.count; 184 for (int i = txtOffset; i < n; i++) { 185 if (txt[i] == '\t' 186 || ((spaceAddon != 0 || i <= spaceAddonLeftoverEnd) 187 && (txt[i] == ' ') 188 && startJustifiableContent <= i 189 && i <= endJustifiableContent 190 )) { 191 if (flushLen > 0) { 192 nextX = drawChars(component, g, txt, flushIndex, flushLen, x, y); 193 flushLen = 0; 194 } 195 flushIndex = i + 1; 196 if (txt[i] == '\t') { 197 if (e != null) { 198 nextX = e.nextTabStop(nextX, startOffset + i - txtOffset); 199 } else { 200 nextX += getFontCharWidth(' ', metrics, useFPAPI); 201 } 202 } else if (txt[i] == ' ') { 203 float spaceWidth = getFontCharWidth(' ', metrics, useFPAPI); 204 nextX += spaceWidth + spaceAddon; 205 if (i <= spaceAddonLeftoverEnd) { 206 nextX++; 207 } 208 } 209 x = nextX; 210 } else if ((txt[i] == '\n') || (txt[i] == '\r')) { 211 if (flushLen > 0) { 212 nextX = drawChars(component, g, txt, flushIndex, flushLen, 213 x, y, useFPAPI); 214 flushLen = 0; 215 } 216 flushIndex = i + 1; 217 x = nextX; 218 } else { 219 flushLen += 1; 220 } 221 } 222 if (flushLen > 0) { 223 nextX = drawChars(component, g,txt, flushIndex, flushLen, x, y, useFPAPI); 224 } 225 return nextX; 226 } 227 228 /** 229 * Determines the width of the given segment of text taking tabs 230 * into consideration. This is implemented in a 1.1 style coordinate 231 * system where ints are used and 72dpi is assumed. 232 * 233 * @param s the source of the text 234 * @param metrics the font metrics to use for the calculation 235 * @param x the X origin >= 0 236 * @param e how to expand the tabs. If this value is null, 237 * tabs will be expanded as a space character. 238 * @param startOffset starting offset of the text in the document >= 0 239 * @return the width of the text 240 * 241 * @deprecated replaced by 242 * {@link #getTabbedTextWidth(Segment, FontMetrics, float, TabExpander, int)} 243 */ 244 @Deprecated(since = "9") 245 public static final int getTabbedTextWidth(Segment s, FontMetrics metrics, int x, 246 TabExpander e, int startOffset) { 247 return getTabbedTextWidth(null, s, metrics, x, e, startOffset, null); 248 } 249 250 /** 251 * Determines the width of the given segment of text taking tabs 252 * into consideration. 253 * 254 * @param s the source of the text 255 * @param metrics the font metrics to use for the calculation 256 * @param x the X origin {@code >= 0} 257 * @param e how to expand the tabs. If this value is null, 258 * tabs will be expanded as a space character. 259 * @param startOffset starting offset of the text in the document {@code >= 0} 260 * @return the width of the text 261 * 262 * @since 9 263 */ 264 public static final float getTabbedTextWidth(Segment s, FontMetrics metrics, 265 float x, TabExpander e, 266 int startOffset) { 267 return getTabbedTextWidth(null, s, metrics, x, e, startOffset, null); 268 } 269 270 // In addition to the previous method it can extend spaces for 271 // justification. 272 // 273 // all params are the same as in the preious method except the last 274 // one: 275 // @param justificationData justificationData for the row. 276 // if null not justification is needed 277 static final int getTabbedTextWidth(View view, Segment s, 278 FontMetrics metrics, int x, 279 TabExpander e, int startOffset, 280 int[] justificationData) 281 { 282 return (int) getTabbedTextWidth(view, s, metrics, x, e, startOffset, 283 justificationData, false); 284 285 } 286 287 static final float getTabbedTextWidth(View view, Segment s, 288 FontMetrics metrics, float x, 289 TabExpander e, int startOffset, 290 int[] justificationData) 291 { 292 return getTabbedTextWidth(view, s, metrics, x, e, startOffset, 293 justificationData, true); 294 295 } 296 297 static final float getTabbedTextWidth(View view, Segment s, 298 FontMetrics metrics, float x, 299 TabExpander e, int startOffset, 300 int[] justificationData, 301 boolean useFPAPI) { 302 float nextX = x; 303 char[] txt = s.array; 304 int txtOffset = s.offset; 305 int n = s.offset + s.count; 306 int charCount = 0; 307 int spaceAddon = 0; 308 int spaceAddonLeftoverEnd = -1; 309 int startJustifiableContent = 0; 310 int endJustifiableContent = 0; 311 if (justificationData != null) { 312 int offset = - startOffset + txtOffset; 313 View parent = null; 314 if (view != null 315 && (parent = view.getParent()) != null) { 316 offset += parent.getStartOffset(); 317 } 318 spaceAddon = 319 justificationData[Row.SPACE_ADDON]; 320 spaceAddonLeftoverEnd = 321 justificationData[Row.SPACE_ADDON_LEFTOVER_END] + offset; 322 startJustifiableContent = 323 justificationData[Row.START_JUSTIFIABLE] + offset; 324 endJustifiableContent = 325 justificationData[Row.END_JUSTIFIABLE] + offset; 326 } 327 328 for (int i = txtOffset; i < n; i++) { 329 if (txt[i] == '\t' 330 || ((spaceAddon != 0 || i <= spaceAddonLeftoverEnd) 331 && (txt[i] == ' ') 332 && startJustifiableContent <= i 333 && i <= endJustifiableContent 334 )) { 335 nextX += metrics.charsWidth(txt, i-charCount, charCount); 336 charCount = 0; 337 if (txt[i] == '\t') { 338 if (e != null) { 339 nextX = e.nextTabStop(nextX, startOffset + i - txtOffset); 340 } else { 341 nextX += getFontCharWidth(' ', metrics, useFPAPI); 342 } 343 } else if (txt[i] == ' ') { 344 float spaceWidth = getFontCharWidth(' ', metrics, useFPAPI); 345 nextX += spaceWidth + spaceAddon; 346 if (i <= spaceAddonLeftoverEnd) { 347 nextX++; 348 } 349 } 350 } else if(txt[i] == '\n') { 351 // Ignore newlines, they take up space and we shouldn't be 352 // counting them. 353 nextX += getFontCharsWidth(txt, i - charCount, charCount, 354 metrics, useFPAPI); 355 charCount = 0; 356 } else { 357 charCount++; 358 } 359 } 360 nextX += getFontCharsWidth(txt, n - charCount, charCount, 361 metrics, useFPAPI); 362 return nextX - x; 363 } 364 365 /** 366 * Determines the relative offset into the given text that 367 * best represents the given span in the view coordinate 368 * system. This is implemented in a 1.1 style coordinate 369 * system where ints are used and 72dpi is assumed. 370 * 371 * @param s the source of the text 372 * @param metrics the font metrics to use for the calculation 373 * @param x0 the starting view location representing the start 374 * of the given text >= 0. 375 * @param x the target view location to translate to an 376 * offset into the text >= 0. 377 * @param e how to expand the tabs. If this value is null, 378 * tabs will be expanded as a space character. 379 * @param startOffset starting offset of the text in the document >= 0 380 * @return the offset into the text >= 0 381 * 382 * @deprecated replaced by 383 * {@link #getTabbedTextOffset(Segment, FontMetrics, float, float, 384 * TabExpander, int, boolean)} 385 */ 386 @Deprecated(since = "9") 387 public static final int getTabbedTextOffset(Segment s, FontMetrics metrics, 388 int x0, int x, TabExpander e, 389 int startOffset) { 390 return getTabbedTextOffset(s, metrics, x0, x, e, startOffset, true); 391 } 392 393 static final int getTabbedTextOffset(View view, Segment s, FontMetrics metrics, 394 int x0, int x, TabExpander e, 395 int startOffset, 396 int[] justificationData) { 397 return getTabbedTextOffset(view, s, metrics, x0, x, e, startOffset, true, 398 justificationData, false); 399 } 400 401 /** 402 * Determines the relative offset into the given text that 403 * best represents the given span in the view coordinate 404 * system. 405 * 406 * @param s the source of the text 407 * @param metrics the font metrics to use for the calculation 408 * @param x0 the starting view location representing the start 409 * of the given text >= 0. 410 * @param x the target view location to translate to an 411 * offset into the text >= 0. 412 * @param e how to expand the tabs. If this value is null, 413 * tabs will be expanded as a space character. 414 * @param startOffset starting offset of the text in the document >= 0 415 * @param round whether or not to round 416 * @return the offset into the text >= 0 417 * 418 * @deprecated replaced by 419 * {@link #getTabbedTextOffset(Segment, FontMetrics, float, float, 420 * TabExpander, int, boolean)} 421 */ 422 @Deprecated(since = "9") 423 public static final int getTabbedTextOffset(Segment s, 424 FontMetrics metrics, 425 int x0, int x, TabExpander e, 426 int startOffset, 427 boolean round) { 428 return getTabbedTextOffset(null, s, metrics, x0, x, e, startOffset, 429 round, null, false); 430 } 431 432 /** 433 * Determines the relative offset into the given text that 434 * best represents the given span in the view coordinate 435 * system. 436 * 437 * @param s the source of the text 438 * @param metrics the font metrics to use for the calculation 439 * @param x0 the starting view location representing the start 440 * of the given text {@code >= 0}. 441 * @param x the target view location to translate to an 442 * offset into the text {@code >= 0}. 443 * @param e how to expand the tabs. If this value is null, 444 * tabs will be expanded as a space character. 445 * @param startOffset starting offset of the text in the document {@code >= 0} 446 * @param round whether or not to round 447 * @return the offset into the text {@code >= 0} 448 * 449 * @since 9 450 */ 451 public static final int getTabbedTextOffset(Segment s, 452 FontMetrics metrics, 453 float x0, float x, 454 TabExpander e, 455 int startOffset, 456 boolean round) 457 { 458 return getTabbedTextOffset(null, s, metrics, x0, x, e, 459 startOffset, round, null, true); 460 } 461 462 // In addition to the previous method it can extend spaces for 463 // justification. 464 // 465 // all params are the same as in the preious method except the last 466 // one: 467 // @param justificationData justificationData for the row. 468 // if null not justification is needed 469 static final int getTabbedTextOffset(View view, 470 Segment s, 471 FontMetrics metrics, 472 float x0, float x, TabExpander e, 473 int startOffset, 474 boolean round, 475 int[] justificationData, 476 boolean useFPAPI) { 477 if (x0 >= x) { 478 // x before x0, return. 479 return 0; 480 } 481 float nextX = x0; 482 // s may be a shared segment, so it is copied prior to calling 483 // the tab expander 484 char[] txt = s.array; 485 int txtOffset = s.offset; 486 int txtCount = s.count; 487 int spaceAddon = 0 ; 488 int spaceAddonLeftoverEnd = -1; 489 int startJustifiableContent = 0 ; 490 int endJustifiableContent = 0; 491 if (justificationData != null) { 492 int offset = - startOffset + txtOffset; 493 View parent = null; 494 if (view != null 495 && (parent = view.getParent()) != null) { 496 offset += parent.getStartOffset(); 497 } 498 spaceAddon = 499 justificationData[Row.SPACE_ADDON]; 500 spaceAddonLeftoverEnd = 501 justificationData[Row.SPACE_ADDON_LEFTOVER_END] + offset; 502 startJustifiableContent = 503 justificationData[Row.START_JUSTIFIABLE] + offset; 504 endJustifiableContent = 505 justificationData[Row.END_JUSTIFIABLE] + offset; 506 } 507 int n = s.offset + s.count; 508 for (int i = s.offset; i < n; i++) { 509 if (txt[i] == '\t' 510 || ((spaceAddon != 0 || i <= spaceAddonLeftoverEnd) 511 && (txt[i] == ' ') 512 && startJustifiableContent <= i 513 && i <= endJustifiableContent 514 )){ 515 if (txt[i] == '\t') { 516 if (e != null) { 517 nextX = e.nextTabStop(nextX, startOffset + i - txtOffset); 518 } else { 519 nextX += getFontCharWidth(' ', metrics, useFPAPI); 520 } 521 } else if (txt[i] == ' ') { 522 nextX += getFontCharWidth(' ', metrics, useFPAPI); 523 nextX += spaceAddon; 524 if (i <= spaceAddonLeftoverEnd) { 525 nextX++; 526 } 527 } 528 } else { 529 nextX += getFontCharWidth(txt[i], metrics, useFPAPI); 530 } 531 if (x < nextX) { 532 // found the hit position... return the appropriate side 533 int offset; 534 535 // the length of the string measured as a whole may differ from 536 // the sum of individual character lengths, for example if 537 // fractional metrics are enabled; and we must guard from this. 538 if (round) { 539 offset = i + 1 - txtOffset; 540 541 float width = getFontCharsWidth(txt, txtOffset, offset, 542 metrics, useFPAPI); 543 float span = x - x0; 544 545 if (span < width) { 546 while (offset > 0) { 547 float charsWidth = getFontCharsWidth(txt, txtOffset, 548 offset - 1, metrics, useFPAPI); 549 float nextWidth = offset > 1 ? charsWidth : 0; 550 551 if (span >= nextWidth) { 552 if (span - nextWidth < width - span) { 553 offset--; 554 } 555 556 break; 557 } 558 559 width = nextWidth; 560 offset--; 561 } 562 } 563 } else { 564 offset = i - txtOffset; 565 566 while (offset > 0 && getFontCharsWidth(txt, txtOffset, offset, 567 metrics, useFPAPI) 568 > (x - x0)) { 569 offset--; 570 } 571 } 572 573 return offset; 574 } 575 } 576 577 // didn't find, return end offset 578 return txtCount; 579 } 580 581 /** 582 * Determine where to break the given text to fit 583 * within the given span. This tries to find a word boundary. 584 * @param s the source of the text 585 * @param metrics the font metrics to use for the calculation 586 * @param x0 the starting view location representing the start 587 * of the given text. 588 * @param x the target view location to translate to an 589 * offset into the text. 590 * @param e how to expand the tabs. If this value is null, 591 * tabs will be expanded as a space character. 592 * @param startOffset starting offset in the document of the text 593 * @return the offset into the given text 594 * 595 * @deprecated replaced by 596 * {@link #getBreakLocation(Segment, FontMetrics, float, float, 597 * TabExpander, int)} 598 */ 599 @Deprecated(since = "9") 600 public static final int getBreakLocation(Segment s, FontMetrics metrics, 601 int x0, int x, TabExpander e, 602 int startOffset) { 603 return getBreakLocation(s, metrics, x0, x, e, startOffset, false); 604 } 605 606 static final int getBreakLocation(Segment s, FontMetrics metrics, 607 float x0, float x, TabExpander e, 608 int startOffset, boolean useFPIAPI) { 609 char[] txt = s.array; 610 int txtOffset = s.offset; 611 int txtCount = s.count; 612 int index = getTabbedTextOffset(null, s, metrics, x0, x, e, startOffset, 613 false, null, useFPIAPI); 614 615 if (index >= txtCount - 1) { 616 return txtCount; 617 } 618 619 for (int i = txtOffset + index; i >= txtOffset; i--) { 620 char ch = txt[i]; 621 if (ch < 256) { 622 // break on whitespace 623 if (Character.isWhitespace(ch)) { 624 index = i - txtOffset + 1; 625 break; 626 } 627 } else { 628 // a multibyte char found; use BreakIterator to find line break 629 BreakIterator bit = BreakIterator.getLineInstance(); 630 bit.setText(s); 631 int breakPos = bit.preceding(i + 1); 632 if (breakPos > txtOffset) { 633 index = breakPos - txtOffset; 634 } 635 break; 636 } 637 } 638 return index; 639 } 640 641 /** 642 * Determine where to break the given text to fit 643 * within the given span. This tries to find a word boundary. 644 * @param s the source of the text 645 * @param metrics the font metrics to use for the calculation 646 * @param x0 the starting view location representing the start 647 * of the given text. 648 * @param x the target view location to translate to an 649 * offset into the text. 650 * @param e how to expand the tabs. If this value is null, 651 * tabs will be expanded as a space character. 652 * @param startOffset starting offset in the document of the text 653 * @return the offset into the given text 654 * 655 * @since 9 656 */ 657 public static final int getBreakLocation(Segment s, FontMetrics metrics, 658 float x0, float x, TabExpander e, 659 int startOffset) { 660 return getBreakLocation(s, metrics, x0, x, e, startOffset, false); 661 } 662 663 /** 664 * Determines the starting row model position of the row that contains 665 * the specified model position. The component given must have a 666 * size to compute the result. If the component doesn't have a size 667 * a value of -1 will be returned. 668 * 669 * @param c the editor 670 * @param offs the offset in the document >= 0 671 * @return the position >= 0 if the request can be computed, otherwise 672 * a value of -1 will be returned. 673 * @exception BadLocationException if the offset is out of range 674 */ 675 @SuppressWarnings("deprecation") 676 public static final int getRowStart(JTextComponent c, int offs) throws BadLocationException { 677 Rectangle r = c.modelToView(offs); 678 if (r == null) { 679 return -1; 680 } 681 int lastOffs = offs; 682 int y = r.y; 683 while ((r != null) && (y == r.y)) { 684 // Skip invisible elements 685 if(r.height !=0) { 686 offs = lastOffs; 687 } 688 lastOffs -= 1; 689 r = (lastOffs >= 0) ? c.modelToView(lastOffs) : null; 690 } 691 return offs; 692 } 693 694 /** 695 * Determines the ending row model position of the row that contains 696 * the specified model position. The component given must have a 697 * size to compute the result. If the component doesn't have a size 698 * a value of -1 will be returned. 699 * 700 * @param c the editor 701 * @param offs the offset in the document >= 0 702 * @return the position >= 0 if the request can be computed, otherwise 703 * a value of -1 will be returned. 704 * @exception BadLocationException if the offset is out of range 705 */ 706 @SuppressWarnings("deprecation") 707 public static final int getRowEnd(JTextComponent c, int offs) throws BadLocationException { 708 Rectangle2D r = c.modelToView2D(offs); 709 if (r == null) { 710 return -1; 711 } 712 int n = c.getDocument().getLength(); 713 int lastOffs = offs; 714 double y = r.getY(); 715 while ((r != null) && (y == r.getY())) { 716 // Skip invisible elements 717 if (r.getHeight() !=0) { 718 offs = lastOffs; 719 } 720 lastOffs += 1; 721 r = (lastOffs <= n) ? c.modelToView(lastOffs) : null; 722 } 723 return offs; 724 } 725 726 /** 727 * Determines the position in the model that is closest to the given 728 * view location in the row above. The component given must have a 729 * size to compute the result. If the component doesn't have a size 730 * a value of -1 will be returned. 731 * 732 * @param c the editor 733 * @param offs the offset in the document >= 0 734 * @param x the X coordinate >= 0 735 * @return the position >= 0 if the request can be computed, otherwise 736 * a value of -1 will be returned. 737 * @exception BadLocationException if the offset is out of range 738 * 739 * @deprecated replaced by 740 * {@link #getPositionAbove(JTextComponent, int, float)} 741 */ 742 @Deprecated(since = "9") 743 public static final int getPositionAbove(JTextComponent c, int offs, int x) 744 throws BadLocationException 745 { 746 return getPositionAbove(c, offs, x, false); 747 } 748 749 @SuppressWarnings("deprecation") 750 static final int getPositionAbove(JTextComponent c, int offs, float x, 751 boolean useFPAPI) throws BadLocationException 752 { 753 int lastOffs = getRowStart(c, offs) - 1; 754 if (lastOffs < 0) { 755 return -1; 756 } 757 double bestSpan = Integer.MAX_VALUE; 758 double y = 0; 759 Rectangle2D r = null; 760 if (lastOffs >= 0) { 761 r = useFPAPI ? c.modelToView2D(lastOffs) : c.modelToView(lastOffs); 762 y = r.getY(); 763 } 764 while ((r != null) && (y == r.getY())) { 765 double span = Math.abs(r.getX() - x); 766 if (span < bestSpan) { 767 offs = lastOffs; 768 bestSpan = span; 769 } 770 lastOffs -= 1; 771 772 if ((lastOffs >= 0)) { 773 r = useFPAPI ? c.modelToView2D(lastOffs) : c.modelToView(lastOffs); 774 } else { 775 r = null; 776 } 777 } 778 return offs; 779 } 780 781 /** 782 * Determines the position in the model that is closest to the given 783 * view location in the row above. The component given must have a 784 * size to compute the result. If the component doesn't have a size 785 * a value of -1 will be returned. 786 * 787 * @param c the editor 788 * @param offs the offset in the document {@code >= 0} 789 * @param x the X coordinate {@code >= 0} 790 * @return the position {@code >= 0} if the request can be computed, otherwise 791 * a value of -1 will be returned. 792 * @exception BadLocationException if the offset is out of range 793 * 794 * @since 9 795 */ 796 public static final int getPositionAbove(JTextComponent c, int offs, float x) 797 throws BadLocationException { 798 return getPositionAbove(c, offs, x, true); 799 } 800 801 /** 802 * Determines the position in the model that is closest to the given 803 * view location in the row below. The component given must have a 804 * size to compute the result. If the component doesn't have a size 805 * a value of -1 will be returned. 806 * 807 * @param c the editor 808 * @param offs the offset in the document >= 0 809 * @param x the X coordinate >= 0 810 * @return the position >= 0 if the request can be computed, otherwise 811 * a value of -1 will be returned. 812 * @exception BadLocationException if the offset is out of range 813 * 814 * @deprecated replaced by 815 * {@link #getPositionBelow(JTextComponent, int, float)} 816 */ 817 @Deprecated(since = "9") 818 public static final int getPositionBelow(JTextComponent c, int offs, int x) 819 throws BadLocationException 820 { 821 return getPositionBelow(c, offs, x, false); 822 } 823 824 @SuppressWarnings("deprecation") 825 static final int getPositionBelow(JTextComponent c, int offs, float x, 826 boolean useFPAPI) throws BadLocationException 827 { 828 int lastOffs = getRowEnd(c, offs) + 1; 829 if (lastOffs <= 0) { 830 return -1; 831 } 832 double bestSpan = Integer.MAX_VALUE; 833 int n = c.getDocument().getLength(); 834 double y = 0; 835 Rectangle2D r = null; 836 if (lastOffs <= n) { 837 r = useFPAPI ? c.modelToView2D(lastOffs) : c.modelToView(lastOffs); 838 y = r.getY(); 839 } 840 while ((r != null) && (y == r.getY())) { 841 double span = Math.abs(x - r.getX()); 842 if (span < bestSpan) { 843 offs = lastOffs; 844 bestSpan = span; 845 } 846 lastOffs += 1; 847 848 if (lastOffs <= n) { 849 r = useFPAPI ? c.modelToView2D(lastOffs) : c.modelToView(lastOffs); 850 } else { 851 r = null; 852 } 853 } 854 return offs; 855 } 856 857 /** 858 * Determines the position in the model that is closest to the given 859 * view location in the row below. The component given must have a 860 * size to compute the result. If the component doesn't have a size 861 * a value of -1 will be returned. 862 * 863 * @param c the editor 864 * @param offs the offset in the document {@code >= 0} 865 * @param x the X coordinate {@code >= 0} 866 * @return the position {@code >= 0} if the request can be computed, otherwise 867 * a value of -1 will be returned. 868 * @exception BadLocationException if the offset is out of range 869 * 870 * @since 9 871 */ 872 public static final int getPositionBelow(JTextComponent c, int offs, float x) 873 throws BadLocationException { 874 return getPositionBelow(c, offs, x, true); 875 } 876 877 /** 878 * Determines the start of a word for the given model location. 879 * Uses BreakIterator.getWordInstance() to actually get the words. 880 * 881 * @param c the editor 882 * @param offs the offset in the document >= 0 883 * @return the location in the model of the word start >= 0 884 * @exception BadLocationException if the offset is out of range 885 */ 886 public static final int getWordStart(JTextComponent c, int offs) throws BadLocationException { 887 Document doc = c.getDocument(); 888 Element line = getParagraphElement(c, offs); 889 if (line == null) { 890 throw new BadLocationException("No word at " + offs, offs); 891 } 892 int lineStart = line.getStartOffset(); 893 int lineEnd = Math.min(line.getEndOffset(), doc.getLength()); 894 895 Segment seg = SegmentCache.getSharedSegment(); 896 doc.getText(lineStart, lineEnd - lineStart, seg); 897 if(seg.count > 0) { 898 BreakIterator words = BreakIterator.getWordInstance(c.getLocale()); 899 words.setText(seg); 900 int wordPosition = seg.offset + offs - lineStart; 901 if(wordPosition >= words.last()) { 902 wordPosition = words.last() - 1; 903 } 904 words.following(wordPosition); 905 offs = lineStart + words.previous() - seg.offset; 906 } 907 SegmentCache.releaseSharedSegment(seg); 908 return offs; 909 } 910 911 /** 912 * Determines the end of a word for the given location. 913 * Uses BreakIterator.getWordInstance() to actually get the words. 914 * 915 * @param c the editor 916 * @param offs the offset in the document >= 0 917 * @return the location in the model of the word end >= 0 918 * @exception BadLocationException if the offset is out of range 919 */ 920 public static final int getWordEnd(JTextComponent c, int offs) throws BadLocationException { 921 Document doc = c.getDocument(); 922 Element line = getParagraphElement(c, offs); 923 if (line == null) { 924 throw new BadLocationException("No word at " + offs, offs); 925 } 926 int lineStart = line.getStartOffset(); 927 int lineEnd = Math.min(line.getEndOffset(), doc.getLength()); 928 929 Segment seg = SegmentCache.getSharedSegment(); 930 doc.getText(lineStart, lineEnd - lineStart, seg); 931 if(seg.count > 0) { 932 BreakIterator words = BreakIterator.getWordInstance(c.getLocale()); 933 words.setText(seg); 934 int wordPosition = offs - lineStart + seg.offset; 935 if(wordPosition >= words.last()) { 936 wordPosition = words.last() - 1; 937 } 938 offs = lineStart + words.following(wordPosition) - seg.offset; 939 } 940 SegmentCache.releaseSharedSegment(seg); 941 return offs; 942 } 943 944 /** 945 * Determines the start of the next word for the given location. 946 * Uses BreakIterator.getWordInstance() to actually get the words. 947 * 948 * @param c the editor 949 * @param offs the offset in the document >= 0 950 * @return the location in the model of the word start >= 0 951 * @exception BadLocationException if the offset is out of range 952 */ 953 public static final int getNextWord(JTextComponent c, int offs) throws BadLocationException { 954 int nextWord; 955 Element line = getParagraphElement(c, offs); 956 for (nextWord = getNextWordInParagraph(c, line, offs, false); 957 nextWord == BreakIterator.DONE; 958 nextWord = getNextWordInParagraph(c, line, offs, true)) { 959 960 // didn't find in this line, try the next line 961 offs = line.getEndOffset(); 962 line = getParagraphElement(c, offs); 963 } 964 return nextWord; 965 } 966 967 /** 968 * Finds the next word in the given elements text. The first 969 * parameter allows searching multiple paragraphs where even 970 * the first offset is desired. 971 * Returns the offset of the next word, or BreakIterator.DONE 972 * if there are no more words in the element. 973 */ 974 static int getNextWordInParagraph(JTextComponent c, Element line, int offs, boolean first) throws BadLocationException { 975 if (line == null) { 976 throw new BadLocationException("No more words", offs); 977 } 978 Document doc = line.getDocument(); 979 int lineStart = line.getStartOffset(); 980 int lineEnd = Math.min(line.getEndOffset(), doc.getLength()); 981 if ((offs >= lineEnd) || (offs < lineStart)) { 982 throw new BadLocationException("No more words", offs); 983 } 984 Segment seg = SegmentCache.getSharedSegment(); 985 doc.getText(lineStart, lineEnd - lineStart, seg); 986 BreakIterator words = BreakIterator.getWordInstance(c.getLocale()); 987 words.setText(seg); 988 if ((first && (words.first() == (seg.offset + offs - lineStart))) && 989 (! Character.isWhitespace(seg.array[words.first()]))) { 990 991 return offs; 992 } 993 int wordPosition = words.following(seg.offset + offs - lineStart); 994 if ((wordPosition == BreakIterator.DONE) || 995 (wordPosition >= seg.offset + seg.count)) { 996 // there are no more words on this line. 997 return BreakIterator.DONE; 998 } 999 // if we haven't shot past the end... check to 1000 // see if the current boundary represents whitespace. 1001 // if so, we need to try again 1002 char ch = seg.array[wordPosition]; 1003 if (! Character.isWhitespace(ch)) { 1004 return lineStart + wordPosition - seg.offset; 1005 } 1006 1007 // it was whitespace, try again. The assumption 1008 // is that it must be a word start if the last 1009 // one had whitespace following it. 1010 wordPosition = words.next(); 1011 if (wordPosition != BreakIterator.DONE) { 1012 offs = lineStart + wordPosition - seg.offset; 1013 if (offs != lineEnd) { 1014 return offs; 1015 } 1016 } 1017 SegmentCache.releaseSharedSegment(seg); 1018 return BreakIterator.DONE; 1019 } 1020 1021 1022 /** 1023 * Determine the start of the prev word for the given location. 1024 * Uses BreakIterator.getWordInstance() to actually get the words. 1025 * 1026 * @param c the editor 1027 * @param offs the offset in the document >= 0 1028 * @return the location in the model of the word start >= 0 1029 * @exception BadLocationException if the offset is out of range 1030 */ 1031 public static final int getPreviousWord(JTextComponent c, int offs) throws BadLocationException { 1032 int prevWord; 1033 Element line = getParagraphElement(c, offs); 1034 for (prevWord = getPrevWordInParagraph(c, line, offs); 1035 prevWord == BreakIterator.DONE; 1036 prevWord = getPrevWordInParagraph(c, line, offs)) { 1037 1038 // didn't find in this line, try the prev line 1039 offs = line.getStartOffset() - 1; 1040 line = getParagraphElement(c, offs); 1041 } 1042 return prevWord; 1043 } 1044 1045 /** 1046 * Finds the previous word in the given elements text. The first 1047 * parameter allows searching multiple paragraphs where even 1048 * the first offset is desired. 1049 * Returns the offset of the next word, or BreakIterator.DONE 1050 * if there are no more words in the element. 1051 */ 1052 static int getPrevWordInParagraph(JTextComponent c, Element line, int offs) throws BadLocationException { 1053 if (line == null) { 1054 throw new BadLocationException("No more words", offs); 1055 } 1056 Document doc = line.getDocument(); 1057 int lineStart = line.getStartOffset(); 1058 int lineEnd = line.getEndOffset(); 1059 if ((offs > lineEnd) || (offs < lineStart)) { 1060 throw new BadLocationException("No more words", offs); 1061 } 1062 Segment seg = SegmentCache.getSharedSegment(); 1063 doc.getText(lineStart, lineEnd - lineStart, seg); 1064 BreakIterator words = BreakIterator.getWordInstance(c.getLocale()); 1065 words.setText(seg); 1066 if (words.following(seg.offset + offs - lineStart) == BreakIterator.DONE) { 1067 words.last(); 1068 } 1069 int wordPosition = words.previous(); 1070 if (wordPosition == (seg.offset + offs - lineStart)) { 1071 wordPosition = words.previous(); 1072 } 1073 1074 if (wordPosition == BreakIterator.DONE) { 1075 // there are no more words on this line. 1076 return BreakIterator.DONE; 1077 } 1078 // if we haven't shot past the end... check to 1079 // see if the current boundary represents whitespace. 1080 // if so, we need to try again 1081 char ch = seg.array[wordPosition]; 1082 if (! Character.isWhitespace(ch)) { 1083 return lineStart + wordPosition - seg.offset; 1084 } 1085 1086 // it was whitespace, try again. The assumption 1087 // is that it must be a word start if the last 1088 // one had whitespace following it. 1089 wordPosition = words.previous(); 1090 if (wordPosition != BreakIterator.DONE) { 1091 return lineStart + wordPosition - seg.offset; 1092 } 1093 SegmentCache.releaseSharedSegment(seg); 1094 return BreakIterator.DONE; 1095 } 1096 1097 /** 1098 * Determines the element to use for a paragraph/line. 1099 * 1100 * @param c the editor 1101 * @param offs the starting offset in the document >= 0 1102 * @return the element 1103 */ 1104 public static final Element getParagraphElement(JTextComponent c, int offs) { 1105 Document doc = c.getDocument(); 1106 if (doc instanceof StyledDocument) { 1107 return ((StyledDocument)doc).getParagraphElement(offs); 1108 } 1109 Element map = doc.getDefaultRootElement(); 1110 int index = map.getElementIndex(offs); 1111 Element paragraph = map.getElement(index); 1112 if ((offs >= paragraph.getStartOffset()) && (offs < paragraph.getEndOffset())) { 1113 return paragraph; 1114 } 1115 return null; 1116 } 1117 1118 static boolean isComposedTextElement(Document doc, int offset) { 1119 Element elem = doc.getDefaultRootElement(); 1120 while (!elem.isLeaf()) { 1121 elem = elem.getElement(elem.getElementIndex(offset)); 1122 } 1123 return isComposedTextElement(elem); 1124 } 1125 1126 static boolean isComposedTextElement(Element elem) { 1127 AttributeSet as = elem.getAttributes(); 1128 return isComposedTextAttributeDefined(as); 1129 } 1130 1131 static boolean isComposedTextAttributeDefined(AttributeSet as) { 1132 return ((as != null) && 1133 (as.isDefined(StyleConstants.ComposedTextAttribute))); 1134 } 1135 1136 /** 1137 * Draws the given composed text passed from an input method. 1138 * 1139 * @param view View hosting text 1140 * @param attr the attributes containing the composed text 1141 * @param g the graphics context 1142 * @param x the X origin 1143 * @param y the Y origin 1144 * @param p0 starting offset in the composed text to be rendered 1145 * @param p1 ending offset in the composed text to be rendered 1146 * @return the new insertion position 1147 */ 1148 static int drawComposedText(View view, AttributeSet attr, Graphics g, 1149 int x, int y, int p0, int p1) 1150 throws BadLocationException 1151 { 1152 return (int) drawComposedText(view, attr, g, x, y, p0, p1, false); 1153 } 1154 1155 static float drawComposedText(View view, AttributeSet attr, Graphics g, 1156 float x, float y, int p0, int p1) 1157 throws BadLocationException 1158 { 1159 return drawComposedText(view, attr, g, x, y, p0, p1, true); 1160 } 1161 1162 static float drawComposedText(View view, AttributeSet attr, Graphics g, 1163 float x, float y, int p0, int p1, 1164 boolean useFPAPI) 1165 throws BadLocationException 1166 { 1167 Graphics2D g2d = (Graphics2D)g; 1168 AttributedString as = (AttributedString)attr.getAttribute( 1169 StyleConstants.ComposedTextAttribute); 1170 as.addAttribute(TextAttribute.FONT, g.getFont()); 1171 1172 if (p0 >= p1) 1173 return x; 1174 1175 AttributedCharacterIterator aci = as.getIterator(null, p0, p1); 1176 return x + SwingUtilities2.drawString(getJComponent(view), g2d, aci, x, y); 1177 } 1178 1179 /** 1180 * Paints the composed text in a GlyphView 1181 */ 1182 static void paintComposedText(Graphics g, Rectangle alloc, GlyphView v) { 1183 if (g instanceof Graphics2D) { 1184 Graphics2D g2d = (Graphics2D) g; 1185 int p0 = v.getStartOffset(); 1186 int p1 = v.getEndOffset(); 1187 AttributeSet attrSet = v.getElement().getAttributes(); 1188 AttributedString as = 1189 (AttributedString)attrSet.getAttribute(StyleConstants.ComposedTextAttribute); 1190 int start = v.getElement().getStartOffset(); 1191 int y = alloc.y + alloc.height - (int)v.getGlyphPainter().getDescent(v); 1192 int x = alloc.x; 1193 1194 //Add text attributes 1195 as.addAttribute(TextAttribute.FONT, v.getFont()); 1196 as.addAttribute(TextAttribute.FOREGROUND, v.getForeground()); 1197 if (StyleConstants.isBold(v.getAttributes())) { 1198 as.addAttribute(TextAttribute.WEIGHT, TextAttribute.WEIGHT_BOLD); 1199 } 1200 if (StyleConstants.isItalic(v.getAttributes())) { 1201 as.addAttribute(TextAttribute.POSTURE, TextAttribute.POSTURE_OBLIQUE); 1202 } 1203 if (v.isUnderline()) { 1204 as.addAttribute(TextAttribute.UNDERLINE, TextAttribute.UNDERLINE_ON); 1205 } 1206 if (v.isStrikeThrough()) { 1207 as.addAttribute(TextAttribute.STRIKETHROUGH, TextAttribute.STRIKETHROUGH_ON); 1208 } 1209 if (v.isSuperscript()) { 1210 as.addAttribute(TextAttribute.SUPERSCRIPT, TextAttribute.SUPERSCRIPT_SUPER); 1211 } 1212 if (v.isSubscript()) { 1213 as.addAttribute(TextAttribute.SUPERSCRIPT, TextAttribute.SUPERSCRIPT_SUB); 1214 } 1215 1216 // draw 1217 AttributedCharacterIterator aci = as.getIterator(null, p0 - start, p1 - start); 1218 SwingUtilities2.drawString(getJComponent(v), 1219 g2d,aci,x,y); 1220 } 1221 } 1222 1223 /* 1224 * Convenience function for determining ComponentOrientation. Helps us 1225 * avoid having Munge directives throughout the code. 1226 */ 1227 static boolean isLeftToRight( java.awt.Component c ) { 1228 return c.getComponentOrientation().isLeftToRight(); 1229 } 1230 1231 1232 /** 1233 * Provides a way to determine the next visually represented model 1234 * location that one might place a caret. Some views may not be visible, 1235 * they might not be in the same order found in the model, or they just 1236 * might not allow access to some of the locations in the model. 1237 * <p> 1238 * This implementation assumes the views are layed out in a logical 1239 * manner. That is, that the view at index x + 1 is visually after 1240 * the View at index x, and that the View at index x - 1 is visually 1241 * before the View at x. There is support for reversing this behavior 1242 * only if the passed in <code>View</code> is an instance of 1243 * <code>CompositeView</code>. The <code>CompositeView</code> 1244 * must then override the <code>flipEastAndWestAtEnds</code> method. 1245 * 1246 * @param v View to query 1247 * @param pos the position to convert >= 0 1248 * @param a the allocated region to render into 1249 * @param direction the direction from the current position that can 1250 * be thought of as the arrow keys typically found on a keyboard; 1251 * this may be one of the following: 1252 * <ul> 1253 * <li><code>SwingConstants.WEST</code> 1254 * <li><code>SwingConstants.EAST</code> 1255 * <li><code>SwingConstants.NORTH</code> 1256 * <li><code>SwingConstants.SOUTH</code> 1257 * </ul> 1258 * @param biasRet an array contain the bias that was checked 1259 * @return the location within the model that best represents the next 1260 * location visual position 1261 * @exception BadLocationException 1262 * @exception IllegalArgumentException if <code>direction</code> is invalid 1263 */ 1264 static int getNextVisualPositionFrom(View v, int pos, Position.Bias b, 1265 Shape alloc, int direction, 1266 Position.Bias[] biasRet) 1267 throws BadLocationException { 1268 if (v.getViewCount() == 0) { 1269 // Nothing to do. 1270 return pos; 1271 } 1272 boolean top = (direction == SwingConstants.NORTH || 1273 direction == SwingConstants.WEST); 1274 int retValue; 1275 if (pos == -1) { 1276 // Start from the first View. 1277 int childIndex = (top) ? v.getViewCount() - 1 : 0; 1278 View child = v.getView(childIndex); 1279 Shape childBounds = v.getChildAllocation(childIndex, alloc); 1280 retValue = child.getNextVisualPositionFrom(pos, b, childBounds, 1281 direction, biasRet); 1282 if (retValue == -1 && !top && v.getViewCount() > 1) { 1283 // Special case that should ONLY happen if first view 1284 // isn't valid (can happen when end position is put at 1285 // beginning of line. 1286 child = v.getView(1); 1287 childBounds = v.getChildAllocation(1, alloc); 1288 retValue = child.getNextVisualPositionFrom(-1, biasRet[0], 1289 childBounds, 1290 direction, biasRet); 1291 } 1292 } 1293 else { 1294 int increment = (top) ? -1 : 1; 1295 int childIndex; 1296 if (b == Position.Bias.Backward && pos > 0) { 1297 childIndex = v.getViewIndex(pos - 1, Position.Bias.Forward); 1298 } 1299 else { 1300 childIndex = v.getViewIndex(pos, Position.Bias.Forward); 1301 } 1302 View child = v.getView(childIndex); 1303 Shape childBounds = v.getChildAllocation(childIndex, alloc); 1304 retValue = child.getNextVisualPositionFrom(pos, b, childBounds, 1305 direction, biasRet); 1306 if ((direction == SwingConstants.EAST || 1307 direction == SwingConstants.WEST) && 1308 (v instanceof CompositeView) && 1309 ((CompositeView)v).flipEastAndWestAtEnds(pos, b)) { 1310 increment *= -1; 1311 } 1312 childIndex += increment; 1313 if (retValue == -1 && childIndex >= 0 && 1314 childIndex < v.getViewCount()) { 1315 child = v.getView(childIndex); 1316 childBounds = v.getChildAllocation(childIndex, alloc); 1317 retValue = child.getNextVisualPositionFrom( 1318 -1, b, childBounds, direction, biasRet); 1319 // If there is a bias change, it is a fake position 1320 // and we should skip it. This is usually the result 1321 // of two elements side be side flowing the same way. 1322 if (retValue == pos && biasRet[0] != b) { 1323 return getNextVisualPositionFrom(v, pos, biasRet[0], 1324 alloc, direction, 1325 biasRet); 1326 } 1327 } 1328 else if (retValue != -1 && biasRet[0] != b && 1329 ((increment == 1 && child.getEndOffset() == retValue) || 1330 (increment == -1 && 1331 child.getStartOffset() == retValue)) && 1332 childIndex >= 0 && childIndex < v.getViewCount()) { 1333 // Reached the end of a view, make sure the next view 1334 // is a different direction. 1335 child = v.getView(childIndex); 1336 childBounds = v.getChildAllocation(childIndex, alloc); 1337 Position.Bias originalBias = biasRet[0]; 1338 int nextPos = child.getNextVisualPositionFrom( 1339 -1, b, childBounds, direction, biasRet); 1340 if (biasRet[0] == b) { 1341 retValue = nextPos; 1342 } 1343 else { 1344 biasRet[0] = originalBias; 1345 } 1346 } 1347 } 1348 return retValue; 1349 } 1350 }