1 /* 2 * Copyright (c) 1999, 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.awt.*; 28 import java.text.BreakIterator; 29 import javax.swing.event.*; 30 import java.util.BitSet; 31 import java.util.Locale; 32 33 import javax.swing.UIManager; 34 import sun.swing.SwingUtilities2; 35 import static sun.swing.SwingUtilities2.IMPLIED_CR; 36 37 /** 38 * A GlyphView is a styled chunk of text that represents a view 39 * mapped over an element in the text model. This view is generally 40 * responsible for displaying text glyphs using character level 41 * attributes in some way. 42 * An implementation of the GlyphPainter class is used to do the 43 * actual rendering and model/view translations. This separates 44 * rendering from layout and management of the association with 45 * the model. 46 * <p> 47 * The view supports breaking for the purpose of formatting. 48 * The fragments produced by breaking share the view that has 49 * primary responsibility for the element (i.e. they are nested 50 * classes and carry only a small amount of state of their own) 51 * so they can share its resources. 52 * <p> 53 * Since this view 54 * represents text that may have tabs embedded in it, it implements the 55 * <code>TabableView</code> interface. Tabs will only be 56 * expanded if this view is embedded in a container that does 57 * tab expansion. ParagraphView is an example of a container 58 * that does tab expansion. 59 * 60 * @since 1.3 61 * 62 * @author Timothy Prinzing 63 */ 64 public class GlyphView extends View implements TabableView, Cloneable { 65 66 /** 67 * Constructs a new view wrapped on an element. 68 * 69 * @param elem the element 70 */ 71 public GlyphView(Element elem) { 72 super(elem); 73 offset = 0; 74 length = 0; 75 Element parent = elem.getParentElement(); 76 AttributeSet attr = elem.getAttributes(); 77 78 // if there was an implied CR 79 impliedCR = (attr != null && attr.getAttribute(IMPLIED_CR) != null && 80 // if this is non-empty paragraph 81 parent != null && parent.getElementCount() > 1); 82 skipWidth = elem.getName().equals("br"); 83 } 84 85 /** 86 * Creates a shallow copy. This is used by the 87 * createFragment and breakView methods. 88 * 89 * @return the copy 90 */ 91 protected final Object clone() { 92 Object o; 93 try { 94 o = super.clone(); 95 } catch (CloneNotSupportedException cnse) { 96 o = null; 97 } 98 return o; 99 } 100 101 /** 102 * Fetch the currently installed glyph painter. 103 * If a painter has not yet been installed, and 104 * a default was not yet needed, null is returned. 105 */ 106 public GlyphPainter getGlyphPainter() { 107 return painter; 108 } 109 110 /** 111 * Sets the painter to use for rendering glyphs. 112 */ 113 public void setGlyphPainter(GlyphPainter p) { 114 painter = p; 115 } 116 117 /** 118 * Fetch a reference to the text that occupies 119 * the given range. This is normally used by 120 * the GlyphPainter to determine what characters 121 * it should render glyphs for. 122 * 123 * @param p0 the starting document offset >= 0 124 * @param p1 the ending document offset >= p0 125 * @return the <code>Segment</code> containing the text 126 */ 127 public Segment getText(int p0, int p1) { 128 // When done with the returned Segment it should be released by 129 // invoking: 130 // SegmentCache.releaseSharedSegment(segment); 131 Segment text = SegmentCache.getSharedSegment(); 132 try { 133 Document doc = getDocument(); 134 doc.getText(p0, p1 - p0, text); 135 } catch (BadLocationException bl) { 136 throw new StateInvariantError("GlyphView: Stale view: " + bl); 137 } 138 return text; 139 } 140 141 /** 142 * Fetch the background color to use to render the 143 * glyphs. If there is no background color, null should 144 * be returned. This is implemented to call 145 * <code>StyledDocument.getBackground</code> if the associated 146 * document is a styled document, otherwise it returns null. 147 */ 148 public Color getBackground() { 149 Document doc = getDocument(); 150 if (doc instanceof StyledDocument) { 151 AttributeSet attr = getAttributes(); 152 if (attr.isDefined(StyleConstants.Background)) { 153 return ((StyledDocument)doc).getBackground(attr); 154 } 155 } 156 return null; 157 } 158 159 /** 160 * Fetch the foreground color to use to render the 161 * glyphs. If there is no foreground color, null should 162 * be returned. This is implemented to call 163 * <code>StyledDocument.getBackground</code> if the associated 164 * document is a StyledDocument. If the associated document 165 * is not a StyledDocument, the associated components foreground 166 * color is used. If there is no associated component, null 167 * is returned. 168 */ 169 public Color getForeground() { 170 Document doc = getDocument(); 171 if (doc instanceof StyledDocument) { 172 AttributeSet attr = getAttributes(); 173 return ((StyledDocument)doc).getForeground(attr); 174 } 175 Component c = getContainer(); 176 if (c != null) { 177 return c.getForeground(); 178 } 179 return null; 180 } 181 182 /** 183 * Fetch the font that the glyphs should be based 184 * upon. This is implemented to call 185 * <code>StyledDocument.getFont</code> if the associated 186 * document is a StyledDocument. If the associated document 187 * is not a StyledDocument, the associated components font 188 * is used. If there is no associated component, null 189 * is returned. 190 */ 191 public Font getFont() { 192 Document doc = getDocument(); 193 if (doc instanceof StyledDocument) { 194 AttributeSet attr = getAttributes(); 195 return ((StyledDocument)doc).getFont(attr); 196 } 197 Component c = getContainer(); 198 if (c != null) { 199 return c.getFont(); 200 } 201 return null; 202 } 203 204 /** 205 * Determine if the glyphs should be underlined. If true, 206 * an underline should be drawn through the baseline. 207 */ 208 public boolean isUnderline() { 209 AttributeSet attr = getAttributes(); 210 return StyleConstants.isUnderline(attr); 211 } 212 213 /** 214 * Determine if the glyphs should have a strikethrough 215 * line. If true, a line should be drawn through the center 216 * of the glyphs. 217 */ 218 public boolean isStrikeThrough() { 219 AttributeSet attr = getAttributes(); 220 return StyleConstants.isStrikeThrough(attr); 221 } 222 223 /** 224 * Determine if the glyphs should be rendered as superscript. 225 */ 226 public boolean isSubscript() { 227 AttributeSet attr = getAttributes(); 228 return StyleConstants.isSubscript(attr); 229 } 230 231 /** 232 * Determine if the glyphs should be rendered as subscript. 233 */ 234 public boolean isSuperscript() { 235 AttributeSet attr = getAttributes(); 236 return StyleConstants.isSuperscript(attr); 237 } 238 239 /** 240 * Fetch the TabExpander to use if tabs are present in this view. 241 */ 242 public TabExpander getTabExpander() { 243 return expander; 244 } 245 246 /** 247 * Check to see that a glyph painter exists. If a painter 248 * doesn't exist, a default glyph painter will be installed. 249 */ 250 protected void checkPainter() { 251 if (painter == null) { 252 if (defaultPainter == null) { 253 // the classname should probably come from a property file. 254 String classname = "javax.swing.text.GlyphPainter1"; 255 try { 256 Class<?> c; 257 ClassLoader loader = getClass().getClassLoader(); 258 if (loader != null) { 259 c = loader.loadClass(classname); 260 } else { 261 c = Class.forName(classname); 262 } 263 Object o = c.newInstance(); 264 if (o instanceof GlyphPainter) { 265 defaultPainter = (GlyphPainter) o; 266 } 267 } catch (Throwable e) { 268 throw new StateInvariantError("GlyphView: Can't load glyph painter: " 269 + classname); 270 } 271 } 272 setGlyphPainter(defaultPainter.getPainter(this, getStartOffset(), 273 getEndOffset())); 274 } 275 } 276 277 // --- TabableView methods -------------------------------------- 278 279 /** 280 * Determines the desired span when using the given 281 * tab expansion implementation. 282 * 283 * @param x the position the view would be located 284 * at for the purpose of tab expansion >= 0. 285 * @param e how to expand the tabs when encountered. 286 * @return the desired span >= 0 287 * @see TabableView#getTabbedSpan 288 */ 289 public float getTabbedSpan(float x, TabExpander e) { 290 checkPainter(); 291 292 TabExpander old = expander; 293 expander = e; 294 295 if (expander != old) { 296 // setting expander can change horizontal span of the view, 297 // so we have to call preferenceChanged() 298 preferenceChanged(null, true, false); 299 } 300 301 this.x = (int) x; 302 int p0 = getStartOffset(); 303 int p1 = getEndOffset(); 304 float width = painter.getSpan(this, p0, p1, expander, x); 305 return width; 306 } 307 308 /** 309 * Determines the span along the same axis as tab 310 * expansion for a portion of the view. This is 311 * intended for use by the TabExpander for cases 312 * where the tab expansion involves aligning the 313 * portion of text that doesn't have whitespace 314 * relative to the tab stop. There is therefore 315 * an assumption that the range given does not 316 * contain tabs. 317 * <p> 318 * This method can be called while servicing the 319 * getTabbedSpan or getPreferredSize. It has to 320 * arrange for its own text buffer to make the 321 * measurements. 322 * 323 * @param p0 the starting document offset >= 0 324 * @param p1 the ending document offset >= p0 325 * @return the span >= 0 326 */ 327 public float getPartialSpan(int p0, int p1) { 328 checkPainter(); 329 float width = painter.getSpan(this, p0, p1, expander, x); 330 return width; 331 } 332 333 // --- View methods --------------------------------------------- 334 335 /** 336 * Fetches the portion of the model that this view is responsible for. 337 * 338 * @return the starting offset into the model 339 * @see View#getStartOffset 340 */ 341 public int getStartOffset() { 342 Element e = getElement(); 343 return (length > 0) ? e.getStartOffset() + offset : e.getStartOffset(); 344 } 345 346 /** 347 * Fetches the portion of the model that this view is responsible for. 348 * 349 * @return the ending offset into the model 350 * @see View#getEndOffset 351 */ 352 public int getEndOffset() { 353 Element e = getElement(); 354 return (length > 0) ? e.getStartOffset() + offset + length : e.getEndOffset(); 355 } 356 357 /** 358 * Lazily initializes the selections field 359 */ 360 private void initSelections(int p0, int p1) { 361 int viewPosCount = p1 - p0 + 1; 362 if (selections == null || viewPosCount > selections.length) { 363 selections = new byte[viewPosCount]; 364 return; 365 } 366 for (int i = 0; i < viewPosCount; selections[i++] = 0); 367 } 368 369 /** 370 * Renders a portion of a text style run. 371 * 372 * @param g the rendering surface to use 373 * @param a the allocated region to render into 374 */ 375 public void paint(Graphics g, Shape a) { 376 checkPainter(); 377 378 boolean paintedText = false; 379 Component c = getContainer(); 380 int p0 = getStartOffset(); 381 int p1 = getEndOffset(); 382 Rectangle alloc = (a instanceof Rectangle) ? (Rectangle)a : a.getBounds(); 383 Color bg = getBackground(); 384 Color fg = getForeground(); 385 386 if (c != null && ! c.isEnabled()) { 387 fg = (c instanceof JTextComponent ? 388 ((JTextComponent)c).getDisabledTextColor() : 389 UIManager.getColor("textInactiveText")); 390 } 391 if (bg != null) { 392 g.setColor(bg); 393 g.fillRect(alloc.x, alloc.y, alloc.width, alloc.height); 394 } 395 if (c instanceof JTextComponent) { 396 JTextComponent tc = (JTextComponent) c; 397 Highlighter h = tc.getHighlighter(); 398 if (h instanceof LayeredHighlighter) { 399 ((LayeredHighlighter)h).paintLayeredHighlights 400 (g, p0, p1, a, tc, this); 401 } 402 } 403 404 if (Utilities.isComposedTextElement(getElement())) { 405 Utilities.paintComposedText(g, a.getBounds(), this); 406 paintedText = true; 407 } else if(c instanceof JTextComponent) { 408 JTextComponent tc = (JTextComponent) c; 409 Color selFG = tc.getSelectedTextColor(); 410 411 if (// there's a highlighter (bug 4532590), and 412 (tc.getHighlighter() != null) && 413 // selected text color is different from regular foreground 414 (selFG != null) && !selFG.equals(fg)) { 415 416 Highlighter.Highlight[] h = tc.getHighlighter().getHighlights(); 417 if(h.length != 0) { 418 boolean initialized = false; 419 int viewSelectionCount = 0; 420 for (int i = 0; i < h.length; i++) { 421 Highlighter.Highlight highlight = h[i]; 422 int hStart = highlight.getStartOffset(); 423 int hEnd = highlight.getEndOffset(); 424 if (hStart > p1 || hEnd < p0) { 425 // the selection is out of this view 426 continue; 427 } 428 if (!SwingUtilities2.useSelectedTextColor(highlight, tc)) { 429 continue; 430 } 431 if (hStart <= p0 && hEnd >= p1){ 432 // the whole view is selected 433 paintTextUsingColor(g, a, selFG, p0, p1); 434 paintedText = true; 435 break; 436 } 437 // the array is lazily created only when the view 438 // is partially selected 439 if (!initialized) { 440 initSelections(p0, p1); 441 initialized = true; 442 } 443 hStart = Math.max(p0, hStart); 444 hEnd = Math.min(p1, hEnd); 445 paintTextUsingColor(g, a, selFG, hStart, hEnd); 446 // the array represents view positions [0, p1-p0+1] 447 // later will iterate this array and sum its 448 // elements. Positions with sum == 0 are not selected. 449 selections[hStart-p0]++; 450 selections[hEnd-p0]--; 451 452 viewSelectionCount++; 453 } 454 455 if (!paintedText && viewSelectionCount > 0) { 456 // the view is partially selected 457 int curPos = -1; 458 int startPos = 0; 459 int viewLen = p1 - p0; 460 while (curPos++ < viewLen) { 461 // searching for the next selection start 462 while(curPos < viewLen && 463 selections[curPos] == 0) curPos++; 464 if (startPos != curPos) { 465 // paint unselected text 466 paintTextUsingColor(g, a, fg, 467 p0 + startPos, p0 + curPos); 468 } 469 int checkSum = 0; 470 // searching for next start position of unselected text 471 while (curPos < viewLen && 472 (checkSum += selections[curPos]) != 0) curPos++; 473 startPos = curPos; 474 } 475 paintedText = true; 476 } 477 } 478 } 479 } 480 if(!paintedText) 481 paintTextUsingColor(g, a, fg, p0, p1); 482 } 483 484 /** 485 * Paints the specified region of text in the specified color. 486 */ 487 final void paintTextUsingColor(Graphics g, Shape a, Color c, int p0, int p1) { 488 // render the glyphs 489 g.setColor(c); 490 painter.paint(this, g, a, p0, p1); 491 492 // render underline or strikethrough if set. 493 boolean underline = isUnderline(); 494 boolean strike = isStrikeThrough(); 495 if (underline || strike) { 496 // calculate x coordinates 497 Rectangle alloc = (a instanceof Rectangle) ? (Rectangle)a : a.getBounds(); 498 View parent = getParent(); 499 if ((parent != null) && (parent.getEndOffset() == p1)) { 500 // strip whitespace on end 501 Segment s = getText(p0, p1); 502 while (Character.isWhitespace(s.last())) { 503 p1 -= 1; 504 s.count -= 1; 505 } 506 SegmentCache.releaseSharedSegment(s); 507 } 508 int x0 = alloc.x; 509 int p = getStartOffset(); 510 if (p != p0) { 511 x0 += (int) painter.getSpan(this, p, p0, getTabExpander(), x0); 512 } 513 int x1 = x0 + (int) painter.getSpan(this, p0, p1, getTabExpander(), x0); 514 515 // calculate y coordinate 516 int y = alloc.y + (int)(painter.getHeight(this) - painter.getDescent(this)); 517 if (underline) { 518 int yTmp = y + 1; 519 g.drawLine(x0, yTmp, x1, yTmp); 520 } 521 if (strike) { 522 // move y coordinate above baseline 523 int yTmp = y - (int) (painter.getAscent(this) * 0.3f); 524 g.drawLine(x0, yTmp, x1, yTmp); 525 } 526 527 } 528 } 529 530 /** 531 * Determines the minimum span for this view along an axis. 532 * 533 * <p>This implementation returns the longest non-breakable area within 534 * the view as a minimum span for {@code View.X_AXIS}.</p> 535 * 536 * @param axis may be either {@code View.X_AXIS} or {@code View.Y_AXIS} 537 * @return the minimum span the view can be rendered into 538 * @throws IllegalArgumentException if the {@code axis} parameter is invalid 539 * @see javax.swing.text.View#getMinimumSpan 540 */ 541 @Override 542 public float getMinimumSpan(int axis) { 543 switch (axis) { 544 case View.X_AXIS: 545 if (minimumSpan < 0) { 546 minimumSpan = 0; 547 int p0 = getStartOffset(); 548 int p1 = getEndOffset(); 549 while (p1 > p0) { 550 int breakSpot = getBreakSpot(p0, p1); 551 if (breakSpot == BreakIterator.DONE) { 552 // the rest of the view is non-breakable 553 breakSpot = p0; 554 } 555 minimumSpan = Math.max(minimumSpan, 556 getPartialSpan(breakSpot, p1)); 557 // Note: getBreakSpot returns the *last* breakspot 558 p1 = breakSpot - 1; 559 } 560 } 561 return minimumSpan; 562 case View.Y_AXIS: 563 return super.getMinimumSpan(axis); 564 default: 565 throw new IllegalArgumentException("Invalid axis: " + axis); 566 } 567 } 568 569 /** 570 * Determines the preferred span for this view along an 571 * axis. 572 * 573 * @param axis may be either View.X_AXIS or View.Y_AXIS 574 * @return the span the view would like to be rendered into >= 0. 575 * Typically the view is told to render into the span 576 * that is returned, although there is no guarantee. 577 * The parent may choose to resize or break the view. 578 */ 579 public float getPreferredSpan(int axis) { 580 if (impliedCR) { 581 return 0; 582 } 583 checkPainter(); 584 int p0 = getStartOffset(); 585 int p1 = getEndOffset(); 586 switch (axis) { 587 case View.X_AXIS: 588 if (skipWidth) { 589 return 0; 590 } 591 return painter.getSpan(this, p0, p1, expander, this.x); 592 case View.Y_AXIS: 593 float h = painter.getHeight(this); 594 if (isSuperscript()) { 595 h += h/3; 596 } 597 return h; 598 default: 599 throw new IllegalArgumentException("Invalid axis: " + axis); 600 } 601 } 602 603 /** 604 * Determines the desired alignment for this view along an 605 * axis. For the label, the alignment is along the font 606 * baseline for the y axis, and the superclasses alignment 607 * along the x axis. 608 * 609 * @param axis may be either View.X_AXIS or View.Y_AXIS 610 * @return the desired alignment. This should be a value 611 * between 0.0 and 1.0 inclusive, where 0 indicates alignment at the 612 * origin and 1.0 indicates alignment to the full span 613 * away from the origin. An alignment of 0.5 would be the 614 * center of the view. 615 */ 616 public float getAlignment(int axis) { 617 checkPainter(); 618 if (axis == View.Y_AXIS) { 619 boolean sup = isSuperscript(); 620 boolean sub = isSubscript(); 621 float h = painter.getHeight(this); 622 float d = painter.getDescent(this); 623 float a = painter.getAscent(this); 624 float align; 625 if (sup) { 626 align = 1.0f; 627 } else if (sub) { 628 align = (h > 0) ? (h - (d + (a / 2))) / h : 0; 629 } else { 630 align = (h > 0) ? (h - d) / h : 0; 631 } 632 return align; 633 } 634 return super.getAlignment(axis); 635 } 636 637 /** 638 * Provides a mapping from the document model coordinate space 639 * to the coordinate space of the view mapped to it. 640 * 641 * @param pos the position to convert >= 0 642 * @param a the allocated region to render into 643 * @param b either <code>Position.Bias.Forward</code> 644 * or <code>Position.Bias.Backward</code> 645 * @return the bounding box of the given position 646 * @exception BadLocationException if the given position does not represent a 647 * valid location in the associated document 648 * @see View#modelToView 649 */ 650 public Shape modelToView(int pos, Shape a, Position.Bias b) throws BadLocationException { 651 checkPainter(); 652 return painter.modelToView(this, pos, b, a); 653 } 654 655 /** 656 * Provides a mapping from the view coordinate space to the logical 657 * coordinate space of the model. 658 * 659 * @param x the X coordinate >= 0 660 * @param y the Y coordinate >= 0 661 * @param a the allocated region to render into 662 * @param biasReturn either <code>Position.Bias.Forward</code> 663 * or <code>Position.Bias.Backward</code> is returned as the 664 * zero-th element of this array 665 * @return the location within the model that best represents the 666 * given point of view >= 0 667 * @see View#viewToModel 668 */ 669 public int viewToModel(float x, float y, Shape a, Position.Bias[] biasReturn) { 670 checkPainter(); 671 return painter.viewToModel(this, x, y, a, biasReturn); 672 } 673 674 /** 675 * Determines how attractive a break opportunity in 676 * this view is. This can be used for determining which 677 * view is the most attractive to call <code>breakView</code> 678 * on in the process of formatting. The 679 * higher the weight, the more attractive the break. A 680 * value equal to or lower than <code>View.BadBreakWeight</code> 681 * should not be considered for a break. A value greater 682 * than or equal to <code>View.ForcedBreakWeight</code> should 683 * be broken. 684 * <p> 685 * This is implemented to forward to the superclass for 686 * the Y_AXIS. Along the X_AXIS the following values 687 * may be returned. 688 * <dl> 689 * <dt><b>View.ExcellentBreakWeight</b> 690 * <dd>if there is whitespace proceeding the desired break 691 * location. 692 * <dt><b>View.BadBreakWeight</b> 693 * <dd>if the desired break location results in a break 694 * location of the starting offset. 695 * <dt><b>View.GoodBreakWeight</b> 696 * <dd>if the other conditions don't occur. 697 * </dl> 698 * This will normally result in the behavior of breaking 699 * on a whitespace location if one can be found, otherwise 700 * breaking between characters. 701 * 702 * @param axis may be either View.X_AXIS or View.Y_AXIS 703 * @param pos the potential location of the start of the 704 * broken view >= 0. This may be useful for calculating tab 705 * positions. 706 * @param len specifies the relative length from <em>pos</em> 707 * where a potential break is desired >= 0. 708 * @return the weight, which should be a value between 709 * View.ForcedBreakWeight and View.BadBreakWeight. 710 * @see LabelView 711 * @see ParagraphView 712 * @see View#BadBreakWeight 713 * @see View#GoodBreakWeight 714 * @see View#ExcellentBreakWeight 715 * @see View#ForcedBreakWeight 716 */ 717 public int getBreakWeight(int axis, float pos, float len) { 718 if (axis == View.X_AXIS) { 719 checkPainter(); 720 int p0 = getStartOffset(); 721 int p1 = painter.getBoundedPosition(this, p0, pos, len); 722 return p1 == p0 ? View.BadBreakWeight : 723 getBreakSpot(p0, p1) != BreakIterator.DONE ? 724 View.ExcellentBreakWeight : View.GoodBreakWeight; 725 } 726 return super.getBreakWeight(axis, pos, len); 727 } 728 729 /** 730 * Breaks this view on the given axis at the given length. 731 * This is implemented to attempt to break on a whitespace 732 * location, and returns a fragment with the whitespace at 733 * the end. If a whitespace location can't be found, the 734 * nearest character is used. 735 * 736 * @param axis may be either View.X_AXIS or View.Y_AXIS 737 * @param p0 the location in the model where the 738 * fragment should start it's representation >= 0. 739 * @param pos the position along the axis that the 740 * broken view would occupy >= 0. This may be useful for 741 * things like tab calculations. 742 * @param len specifies the distance along the axis 743 * where a potential break is desired >= 0. 744 * @return the fragment of the view that represents the 745 * given span, if the view can be broken. If the view 746 * doesn't support breaking behavior, the view itself is 747 * returned. 748 * @see View#breakView 749 */ 750 public View breakView(int axis, int p0, float pos, float len) { 751 if (axis == View.X_AXIS) { 752 checkPainter(); 753 int p1 = painter.getBoundedPosition(this, p0, pos, len); 754 int breakSpot = getBreakSpot(p0, p1); 755 756 if (breakSpot != -1) { 757 p1 = breakSpot; 758 } 759 // else, no break in the region, return a fragment of the 760 // bounded region. 761 if (p0 == getStartOffset() && p1 == getEndOffset()) { 762 return this; 763 } 764 GlyphView v = (GlyphView) createFragment(p0, p1); 765 v.x = (int) pos; 766 return v; 767 } 768 return this; 769 } 770 771 /** 772 * Returns a location to break at in the passed in region, or 773 * BreakIterator.DONE if there isn't a good location to break at 774 * in the specified region. 775 */ 776 private int getBreakSpot(int p0, int p1) { 777 if (breakSpots == null) { 778 // Re-calculate breakpoints for the whole view 779 int start = getStartOffset(); 780 int end = getEndOffset(); 781 int[] bs = new int[end + 1 - start]; 782 int ix = 0; 783 784 // Breaker should work on the parent element because there may be 785 // a valid breakpoint at the end edge of the view (space, etc.) 786 Element parent = getElement().getParentElement(); 787 int pstart = (parent == null ? start : parent.getStartOffset()); 788 int pend = (parent == null ? end : parent.getEndOffset()); 789 790 Segment s = getText(pstart, pend); 791 s.first(); 792 BreakIterator breaker = getBreaker(); 793 breaker.setText(s); 794 795 // Backward search should start from end+1 unless there's NO end+1 796 int startFrom = end + (pend > end ? 1 : 0); 797 for (;;) { 798 startFrom = breaker.preceding(s.offset + (startFrom - pstart)) 799 + (pstart - s.offset); 800 if (startFrom > start) { 801 // The break spot is within the view 802 bs[ix++] = startFrom; 803 } else { 804 break; 805 } 806 } 807 808 SegmentCache.releaseSharedSegment(s); 809 breakSpots = new int[ix]; 810 System.arraycopy(bs, 0, breakSpots, 0, ix); 811 } 812 813 int breakSpot = BreakIterator.DONE; 814 for (int i = 0; i < breakSpots.length; i++) { 815 int bsp = breakSpots[i]; 816 if (bsp <= p1) { 817 if (bsp > p0) { 818 breakSpot = bsp; 819 } 820 break; 821 } 822 } 823 return breakSpot; 824 } 825 826 /** 827 * Return break iterator appropriate for the current document. 828 * 829 * For non-i18n documents a fast whitespace-based break iterator is used. 830 */ 831 private BreakIterator getBreaker() { 832 Document doc = getDocument(); 833 if ((doc != null) && Boolean.TRUE.equals( 834 doc.getProperty(AbstractDocument.MultiByteProperty))) { 835 Container c = getContainer(); 836 Locale locale = (c == null ? Locale.getDefault() : c.getLocale()); 837 return BreakIterator.getLineInstance(locale); 838 } else { 839 return new WhitespaceBasedBreakIterator(); 840 } 841 } 842 843 /** 844 * Creates a view that represents a portion of the element. 845 * This is potentially useful during formatting operations 846 * for taking measurements of fragments of the view. If 847 * the view doesn't support fragmenting (the default), it 848 * should return itself. 849 * <p> 850 * This view does support fragmenting. It is implemented 851 * to return a nested class that shares state in this view 852 * representing only a portion of the view. 853 * 854 * @param p0 the starting offset >= 0. This should be a value 855 * greater or equal to the element starting offset and 856 * less than the element ending offset. 857 * @param p1 the ending offset > p0. This should be a value 858 * less than or equal to the elements end offset and 859 * greater than the elements starting offset. 860 * @return the view fragment, or itself if the view doesn't 861 * support breaking into fragments 862 * @see LabelView 863 */ 864 public View createFragment(int p0, int p1) { 865 checkPainter(); 866 Element elem = getElement(); 867 GlyphView v = (GlyphView) clone(); 868 v.offset = p0 - elem.getStartOffset(); 869 v.length = p1 - p0; 870 v.painter = painter.getPainter(v, p0, p1); 871 v.justificationInfo = null; 872 return v; 873 } 874 875 /** 876 * Provides a way to determine the next visually represented model 877 * location that one might place a caret. Some views may not be 878 * visible, they might not be in the same order found in the model, or 879 * they just might not allow access to some of the locations in the 880 * model. 881 * This method enables specifying a position to convert 882 * within the range of >=0. If the value is -1, a position 883 * will be calculated automatically. If the value < -1, 884 * the {@code BadLocationException} will be thrown. 885 * 886 * @param pos the position to convert 887 * @param a the allocated region to render into 888 * @param direction the direction from the current position that can 889 * be thought of as the arrow keys typically found on a keyboard. 890 * This may be SwingConstants.WEST, SwingConstants.EAST, 891 * SwingConstants.NORTH, or SwingConstants.SOUTH. 892 * @return the location within the model that best represents the next 893 * location visual position. 894 * @exception BadLocationException the given position is not a valid 895 * position within the document 896 * @exception IllegalArgumentException for an invalid direction 897 */ 898 public int getNextVisualPositionFrom(int pos, Position.Bias b, Shape a, 899 int direction, 900 Position.Bias[] biasRet) 901 throws BadLocationException { 902 903 if (pos < -1 || pos > getDocument().getLength()) { 904 throw new BadLocationException("invalid position", pos); 905 } 906 return painter.getNextVisualPositionFrom(this, pos, b, a, direction, biasRet); 907 } 908 909 /** 910 * Gives notification that something was inserted into 911 * the document in a location that this view is responsible for. 912 * This is implemented to call preferenceChanged along the 913 * axis the glyphs are rendered. 914 * 915 * @param e the change information from the associated document 916 * @param a the current allocation of the view 917 * @param f the factory to use to rebuild if the view has children 918 * @see View#insertUpdate 919 */ 920 public void insertUpdate(DocumentEvent e, Shape a, ViewFactory f) { 921 justificationInfo = null; 922 breakSpots = null; 923 minimumSpan = -1; 924 syncCR(); 925 preferenceChanged(null, true, false); 926 } 927 928 /** 929 * Gives notification that something was removed from the document 930 * in a location that this view is responsible for. 931 * This is implemented to call preferenceChanged along the 932 * axis the glyphs are rendered. 933 * 934 * @param e the change information from the associated document 935 * @param a the current allocation of the view 936 * @param f the factory to use to rebuild if the view has children 937 * @see View#removeUpdate 938 */ 939 public void removeUpdate(DocumentEvent e, Shape a, ViewFactory f) { 940 justificationInfo = null; 941 breakSpots = null; 942 minimumSpan = -1; 943 syncCR(); 944 preferenceChanged(null, true, false); 945 } 946 947 /** 948 * Gives notification from the document that attributes were changed 949 * in a location that this view is responsible for. 950 * This is implemented to call preferenceChanged along both the 951 * horizontal and vertical axis. 952 * 953 * @param e the change information from the associated document 954 * @param a the current allocation of the view 955 * @param f the factory to use to rebuild if the view has children 956 * @see View#changedUpdate 957 */ 958 public void changedUpdate(DocumentEvent e, Shape a, ViewFactory f) { 959 minimumSpan = -1; 960 syncCR(); 961 preferenceChanged(null, true, true); 962 } 963 964 // checks if the paragraph is empty and updates impliedCR flag 965 // accordingly 966 private void syncCR() { 967 if (impliedCR) { 968 Element parent = getElement().getParentElement(); 969 impliedCR = (parent != null && parent.getElementCount() > 1); 970 } 971 } 972 973 /** {@inheritDoc} */ 974 @Override 975 void updateAfterChange() { 976 // Drop the break spots. They will be re-calculated during 977 // layout. It is necessary for proper line break calculation. 978 breakSpots = null; 979 } 980 981 /** 982 * Class to hold data needed to justify this GlyphView in a PargraphView.Row 983 */ 984 static class JustificationInfo { 985 //justifiable content start 986 final int start; 987 //justifiable content end 988 final int end; 989 final int leadingSpaces; 990 final int contentSpaces; 991 final int trailingSpaces; 992 final boolean hasTab; 993 final BitSet spaceMap; 994 JustificationInfo(int start, int end, 995 int leadingSpaces, 996 int contentSpaces, 997 int trailingSpaces, 998 boolean hasTab, 999 BitSet spaceMap) { 1000 this.start = start; 1001 this.end = end; 1002 this.leadingSpaces = leadingSpaces; 1003 this.contentSpaces = contentSpaces; 1004 this.trailingSpaces = trailingSpaces; 1005 this.hasTab = hasTab; 1006 this.spaceMap = spaceMap; 1007 } 1008 } 1009 1010 1011 1012 JustificationInfo getJustificationInfo(int rowStartOffset) { 1013 if (justificationInfo != null) { 1014 return justificationInfo; 1015 } 1016 //states for the parsing 1017 final int TRAILING = 0; 1018 final int CONTENT = 1; 1019 final int SPACES = 2; 1020 int startOffset = getStartOffset(); 1021 int endOffset = getEndOffset(); 1022 Segment segment = getText(startOffset, endOffset); 1023 int txtOffset = segment.offset; 1024 int txtEnd = segment.offset + segment.count - 1; 1025 int startContentPosition = txtEnd + 1; 1026 int endContentPosition = txtOffset - 1; 1027 int lastTabPosition = txtOffset - 1; 1028 int trailingSpaces = 0; 1029 int contentSpaces = 0; 1030 int leadingSpaces = 0; 1031 boolean hasTab = false; 1032 BitSet spaceMap = new BitSet(endOffset - startOffset + 1); 1033 1034 //we parse conent to the right of the rightmost TAB only. 1035 //we are looking for the trailing and leading spaces. 1036 //position after the leading spaces (startContentPosition) 1037 //position before the trailing spaces (endContentPosition) 1038 for (int i = txtEnd, state = TRAILING; i >= txtOffset; i--) { 1039 if (' ' == segment.array[i]) { 1040 spaceMap.set(i - txtOffset); 1041 if (state == TRAILING) { 1042 trailingSpaces++; 1043 } else if (state == CONTENT) { 1044 state = SPACES; 1045 leadingSpaces = 1; 1046 } else if (state == SPACES) { 1047 leadingSpaces++; 1048 } 1049 } else if ('\t' == segment.array[i]) { 1050 hasTab = true; 1051 break; 1052 } else { 1053 if (state == TRAILING) { 1054 if ('\n' != segment.array[i] 1055 && '\r' != segment.array[i]) { 1056 state = CONTENT; 1057 endContentPosition = i; 1058 } 1059 } else if (state == CONTENT) { 1060 //do nothing 1061 } else if (state == SPACES) { 1062 contentSpaces += leadingSpaces; 1063 leadingSpaces = 0; 1064 } 1065 startContentPosition = i; 1066 } 1067 } 1068 1069 SegmentCache.releaseSharedSegment(segment); 1070 1071 int startJustifiableContent = -1; 1072 if (startContentPosition < txtEnd) { 1073 startJustifiableContent = 1074 startContentPosition - txtOffset; 1075 } 1076 int endJustifiableContent = -1; 1077 if (endContentPosition > txtOffset) { 1078 endJustifiableContent = 1079 endContentPosition - txtOffset; 1080 } 1081 justificationInfo = 1082 new JustificationInfo(startJustifiableContent, 1083 endJustifiableContent, 1084 leadingSpaces, 1085 contentSpaces, 1086 trailingSpaces, 1087 hasTab, 1088 spaceMap); 1089 return justificationInfo; 1090 } 1091 1092 // --- variables ------------------------------------------------ 1093 1094 /** 1095 * Used by paint() to store highlighted view positions 1096 */ 1097 private byte[] selections = null; 1098 1099 int offset; 1100 int length; 1101 // if it is an implied newline character 1102 boolean impliedCR; 1103 boolean skipWidth; 1104 1105 /** 1106 * how to expand tabs 1107 */ 1108 TabExpander expander; 1109 1110 /** Cached minimum x-span value */ 1111 private float minimumSpan = -1; 1112 1113 /** Cached breakpoints within the view */ 1114 private int[] breakSpots = null; 1115 1116 /** 1117 * location for determining tab expansion against. 1118 */ 1119 int x; 1120 1121 /** 1122 * Glyph rendering functionality. 1123 */ 1124 GlyphPainter painter; 1125 1126 /** 1127 * The prototype painter used by default. 1128 */ 1129 static GlyphPainter defaultPainter; 1130 1131 private JustificationInfo justificationInfo = null; 1132 1133 /** 1134 * A class to perform rendering of the glyphs. 1135 * This can be implemented to be stateless, or 1136 * to hold some information as a cache to 1137 * facilitate faster rendering and model/view 1138 * translation. At a minimum, the GlyphPainter 1139 * allows a View implementation to perform its 1140 * duties independant of a particular version 1141 * of JVM and selection of capabilities (i.e. 1142 * shaping for i18n, etc). 1143 * 1144 * @since 1.3 1145 */ 1146 public static abstract class GlyphPainter { 1147 1148 /** 1149 * Determine the span the glyphs given a start location 1150 * (for tab expansion). 1151 */ 1152 public abstract float getSpan(GlyphView v, int p0, int p1, TabExpander e, float x); 1153 1154 public abstract float getHeight(GlyphView v); 1155 1156 public abstract float getAscent(GlyphView v); 1157 1158 public abstract float getDescent(GlyphView v); 1159 1160 /** 1161 * Paint the glyphs representing the given range. 1162 */ 1163 public abstract void paint(GlyphView v, Graphics g, Shape a, int p0, int p1); 1164 1165 /** 1166 * Provides a mapping from the document model coordinate space 1167 * to the coordinate space of the view mapped to it. 1168 * This is shared by the broken views. 1169 * 1170 * @param v the <code>GlyphView</code> containing the 1171 * destination coordinate space 1172 * @param pos the position to convert 1173 * @param bias either <code>Position.Bias.Forward</code> 1174 * or <code>Position.Bias.Backward</code> 1175 * @param a Bounds of the View 1176 * @return the bounding box of the given position 1177 * @exception BadLocationException if the given position does not represent a 1178 * valid location in the associated document 1179 * @see View#modelToView 1180 */ 1181 public abstract Shape modelToView(GlyphView v, 1182 int pos, Position.Bias bias, 1183 Shape a) throws BadLocationException; 1184 1185 /** 1186 * Provides a mapping from the view coordinate space to the logical 1187 * coordinate space of the model. 1188 * 1189 * @param v the <code>GlyphView</code> to provide a mapping for 1190 * @param x the X coordinate 1191 * @param y the Y coordinate 1192 * @param a the allocated region to render into 1193 * @param biasReturn either <code>Position.Bias.Forward</code> 1194 * or <code>Position.Bias.Backward</code> 1195 * is returned as the zero-th element of this array 1196 * @return the location within the model that best represents the 1197 * given point of view 1198 * @see View#viewToModel 1199 */ 1200 public abstract int viewToModel(GlyphView v, 1201 float x, float y, Shape a, 1202 Position.Bias[] biasReturn); 1203 1204 /** 1205 * Determines the model location that represents the 1206 * maximum advance that fits within the given span. 1207 * This could be used to break the given view. The result 1208 * should be a location just shy of the given advance. This 1209 * differs from viewToModel which returns the closest 1210 * position which might be proud of the maximum advance. 1211 * 1212 * @param v the view to find the model location to break at. 1213 * @param p0 the location in the model where the 1214 * fragment should start it's representation >= 0. 1215 * @param x the graphic location along the axis that the 1216 * broken view would occupy >= 0. This may be useful for 1217 * things like tab calculations. 1218 * @param len specifies the distance into the view 1219 * where a potential break is desired >= 0. 1220 * @return the maximum model location possible for a break. 1221 * @see View#breakView 1222 */ 1223 public abstract int getBoundedPosition(GlyphView v, int p0, float x, float len); 1224 1225 /** 1226 * Create a painter to use for the given GlyphView. If 1227 * the painter carries state it can create another painter 1228 * to represent a new GlyphView that is being created. If 1229 * the painter doesn't hold any significant state, it can 1230 * return itself. The default behavior is to return itself. 1231 * @param v the <code>GlyphView</code> to provide a painter for 1232 * @param p0 the starting document offset >= 0 1233 * @param p1 the ending document offset >= p0 1234 */ 1235 public GlyphPainter getPainter(GlyphView v, int p0, int p1) { 1236 return this; 1237 } 1238 1239 /** 1240 * Provides a way to determine the next visually represented model 1241 * location that one might place a caret. Some views may not be 1242 * visible, they might not be in the same order found in the model, or 1243 * they just might not allow access to some of the locations in the 1244 * model. 1245 * 1246 * @param v the view to use 1247 * @param pos the position to convert >= 0 1248 * @param b either <code>Position.Bias.Forward</code> 1249 * or <code>Position.Bias.Backward</code> 1250 * @param a the allocated region to render into 1251 * @param direction the direction from the current position that can 1252 * be thought of as the arrow keys typically found on a keyboard. 1253 * This may be SwingConstants.WEST, SwingConstants.EAST, 1254 * SwingConstants.NORTH, or SwingConstants.SOUTH. 1255 * @param biasRet either <code>Position.Bias.Forward</code> 1256 * or <code>Position.Bias.Backward</code> 1257 * is returned as the zero-th element of this array 1258 * @return the location within the model that best represents the next 1259 * location visual position. 1260 * @exception BadLocationException for a bad location within a document model 1261 * @exception IllegalArgumentException for an invalid direction 1262 */ 1263 public int getNextVisualPositionFrom(GlyphView v, int pos, Position.Bias b, Shape a, 1264 int direction, 1265 Position.Bias[] biasRet) 1266 throws BadLocationException { 1267 1268 int startOffset = v.getStartOffset(); 1269 int endOffset = v.getEndOffset(); 1270 Segment text; 1271 1272 switch (direction) { 1273 case View.NORTH: 1274 case View.SOUTH: 1275 if (pos != -1) { 1276 // Presumably pos is between startOffset and endOffset, 1277 // since GlyphView is only one line, we won't contain 1278 // the position to the nort/south, therefore return -1. 1279 return -1; 1280 } 1281 Container container = v.getContainer(); 1282 1283 if (container instanceof JTextComponent) { 1284 Caret c = ((JTextComponent)container).getCaret(); 1285 Point magicPoint; 1286 magicPoint = (c != null) ? c.getMagicCaretPosition() :null; 1287 1288 if (magicPoint == null) { 1289 biasRet[0] = Position.Bias.Forward; 1290 return startOffset; 1291 } 1292 int value = v.viewToModel(magicPoint.x, 0f, a, biasRet); 1293 return value; 1294 } 1295 break; 1296 case View.EAST: 1297 if(startOffset == v.getDocument().getLength()) { 1298 if(pos == -1) { 1299 biasRet[0] = Position.Bias.Forward; 1300 return startOffset; 1301 } 1302 // End case for bidi text where newline is at beginning 1303 // of line. 1304 return -1; 1305 } 1306 if(pos == -1) { 1307 biasRet[0] = Position.Bias.Forward; 1308 return startOffset; 1309 } 1310 if(pos == endOffset) { 1311 return -1; 1312 } 1313 if(++pos == endOffset) { 1314 // Assumed not used in bidi text, GlyphPainter2 will 1315 // override as necessary, therefore return -1. 1316 return -1; 1317 } 1318 else { 1319 biasRet[0] = Position.Bias.Forward; 1320 } 1321 return pos; 1322 case View.WEST: 1323 if(startOffset == v.getDocument().getLength()) { 1324 if(pos == -1) { 1325 biasRet[0] = Position.Bias.Forward; 1326 return startOffset; 1327 } 1328 // End case for bidi text where newline is at beginning 1329 // of line. 1330 return -1; 1331 } 1332 if(pos == -1) { 1333 // Assumed not used in bidi text, GlyphPainter2 will 1334 // override as necessary, therefore return -1. 1335 biasRet[0] = Position.Bias.Forward; 1336 return endOffset - 1; 1337 } 1338 if(pos == startOffset) { 1339 return -1; 1340 } 1341 biasRet[0] = Position.Bias.Forward; 1342 return (pos - 1); 1343 default: 1344 throw new IllegalArgumentException("Bad direction: " + direction); 1345 } 1346 return pos; 1347 1348 } 1349 } 1350 }