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