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