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