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