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 */ 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 */ GlyphView(Element elem)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 */ clone()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 */ getGlyphPainter()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 */ setGlyphPainter(GlyphPainter p)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 */ getText(int p0, int p1)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 */ getBackground()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 */ getForeground()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 */ getFont()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 */ isUnderline()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 */ isStrikeThrough()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 */ isSubscript()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 */ isSuperscript()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 */ getTabExpander()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 */ checkPainter()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 */ getTabbedSpan(float x, TabExpander e)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 */ getPartialSpan(int p0, int p1)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 */ getStartOffset()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 */ getEndOffset()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 */ initSelections(int p0, int p1)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 */ paint(Graphics g, Shape a)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 */ paintTextUsingColor(Graphics g, Shape a, Color c, int p0, int p1)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 getMinimumSpan(int axis)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 */ getPreferredSpan(int axis)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 */ getAlignment(int axis)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 */ modelToView(int pos, Shape a, Position.Bias b)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 */ viewToModel(float x, float y, Shape a, Position.Bias[] biasReturn)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 */ getBreakWeight(int axis, float pos, float len)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 */ breakView(int axis, int p0, float pos, float len)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 */ getBreakSpot(int p0, int p1)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 */ getBreaker()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 */ createFragment(int p0, int p1)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 */ getNextVisualPositionFrom(int pos, Position.Bias b, Shape a, int direction, Position.Bias[] biasRet)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 */ insertUpdate(DocumentEvent e, Shape a, ViewFactory f)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 */ removeUpdate(DocumentEvent e, Shape a, ViewFactory f)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 */ changedUpdate(DocumentEvent e, Shape a, ViewFactory f)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 syncCR()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 updateAfterChange()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; JustificationInfo(int start, int end, int leadingSpaces, int contentSpaces, int trailingSpaces, boolean hasTab, 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 getJustificationInfo(int rowStartOffset)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 independent 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 */ getSpan(GlyphView v, int p0, int p1, TabExpander e, float x)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 */ getHeight(GlyphView v)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 */ getAscent(GlyphView v)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 */ getDescent(GlyphView v)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 */ paint(GlyphView v, Graphics g, Shape a, int p0, int p1)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 */ modelToView(GlyphView v, int pos, Position.Bias bias, Shape a)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 */ viewToModel(GlyphView v, float x, float y, Shape a, Position.Bias[] biasReturn)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 */ getBoundedPosition(GlyphView v, int p0, float x, float len)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 */ getPainter(GlyphView v, int p0, int p1)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 */ getNextVisualPositionFrom(GlyphView v, int pos, Position.Bias b, Shape a, int direction, Position.Bias[] biasRet)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