001/* WrappedPlainView.java -- 002 Copyright (C) 2005, 2006 Free Software Foundation, Inc. 003 004This file is part of GNU Classpath. 005 006GNU Classpath is free software; you can redistribute it and/or modify 007it under the terms of the GNU General Public License as published by 008the Free Software Foundation; either version 2, or (at your option) 009any later version. 010 011GNU Classpath is distributed in the hope that it will be useful, but 012WITHOUT ANY WARRANTY; without even the implied warranty of 013MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 014General Public License for more details. 015 016You should have received a copy of the GNU General Public License 017along with GNU Classpath; see the file COPYING. If not, write to the 018Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 01902110-1301 USA. 020 021Linking this library statically or dynamically with other modules is 022making a combined work based on this library. Thus, the terms and 023conditions of the GNU General Public License cover the whole 024combination. 025 026As a special exception, the copyright holders of this library give you 027permission to link this library with independent modules to produce an 028executable, regardless of the license terms of these independent 029modules, and to copy and distribute the resulting executable under 030terms of your choice, provided that you also meet, for each linked 031independent module, the terms and conditions of the license of that 032module. An independent module is a module which is not derived from 033or based on this library. If you modify this library, you may extend 034this exception to your version of the library, but you are not 035obligated to do so. If you do not wish to do so, delete this 036exception statement from your version. */ 037 038 039package javax.swing.text; 040 041import java.awt.Color; 042import java.awt.Container; 043import java.awt.FontMetrics; 044import java.awt.Graphics; 045import java.awt.Rectangle; 046import java.awt.Shape; 047 048import javax.swing.event.DocumentEvent; 049import javax.swing.text.Position.Bias; 050 051/** 052 * @author Anthony Balkissoon abalkiss at redhat dot com 053 * 054 */ 055public class WrappedPlainView extends BoxView implements TabExpander 056{ 057 /** The color for selected text **/ 058 Color selectedColor; 059 060 /** The color for unselected text **/ 061 Color unselectedColor; 062 063 /** The color for disabled components **/ 064 Color disabledColor; 065 066 /** 067 * Stores the font metrics. This is package private to avoid synthetic 068 * accessor method. 069 */ 070 FontMetrics metrics; 071 072 /** Whether or not to wrap on word boundaries **/ 073 boolean wordWrap; 074 075 /** A ViewFactory that creates WrappedLines **/ 076 ViewFactory viewFactory = new WrappedLineCreator(); 077 078 /** The start of the selected text **/ 079 int selectionStart; 080 081 /** The end of the selected text **/ 082 int selectionEnd; 083 084 /** The height of the line (used while painting) **/ 085 int lineHeight; 086 087 /** 088 * The base offset for tab calculations. 089 */ 090 private int tabBase; 091 092 /** 093 * The tab size. 094 */ 095 private int tabSize; 096 097 /** 098 * The instance returned by {@link #getLineBuffer()}. 099 */ 100 private transient Segment lineBuffer; 101 102 public WrappedPlainView (Element elem) 103 { 104 this (elem, false); 105 } 106 107 public WrappedPlainView (Element elem, boolean wordWrap) 108 { 109 super (elem, Y_AXIS); 110 this.wordWrap = wordWrap; 111 } 112 113 /** 114 * Provides access to the Segment used for retrievals from the Document. 115 * @return the Segment. 116 */ 117 protected final Segment getLineBuffer() 118 { 119 if (lineBuffer == null) 120 lineBuffer = new Segment(); 121 return lineBuffer; 122 } 123 124 /** 125 * Returns the next tab stop position after a given reference position. 126 * 127 * This implementation ignores the <code>tabStop</code> argument. 128 * 129 * @param x the current x position in pixels 130 * @param tabStop the position within the text stream that the tab occured at 131 */ 132 public float nextTabStop(float x, int tabStop) 133 { 134 int next = (int) x; 135 if (tabSize != 0) 136 { 137 int numTabs = ((int) x - tabBase) / tabSize; 138 next = tabBase + (numTabs + 1) * tabSize; 139 } 140 return next; 141 } 142 143 /** 144 * Returns the tab size for the Document based on 145 * PlainDocument.tabSizeAttribute, defaulting to 8 if this property is 146 * not defined 147 * 148 * @return the tab size. 149 */ 150 protected int getTabSize() 151 { 152 Object tabSize = getDocument().getProperty(PlainDocument.tabSizeAttribute); 153 if (tabSize == null) 154 return 8; 155 return ((Integer)tabSize).intValue(); 156 } 157 158 /** 159 * Draws a line of text, suppressing white space at the end and expanding 160 * tabs. Calls drawSelectedText and drawUnselectedText. 161 * @param p0 starting document position to use 162 * @param p1 ending document position to use 163 * @param g graphics context 164 * @param x starting x position 165 * @param y starting y position 166 */ 167 protected void drawLine(int p0, int p1, Graphics g, int x, int y) 168 { 169 try 170 { 171 // We have to draw both selected and unselected text. There are 172 // several cases: 173 // - entire range is unselected 174 // - entire range is selected 175 // - start of range is selected, end of range is unselected 176 // - start of range is unselected, end of range is selected 177 // - middle of range is selected, start and end of range is unselected 178 179 // entire range unselected: 180 if ((selectionStart == selectionEnd) || 181 (p0 > selectionEnd || p1 < selectionStart)) 182 drawUnselectedText(g, x, y, p0, p1); 183 184 // entire range selected 185 else if (p0 >= selectionStart && p1 <= selectionEnd) 186 drawSelectedText(g, x, y, p0, p1); 187 188 // start of range selected, end of range unselected 189 else if (p0 >= selectionStart) 190 { 191 x = drawSelectedText(g, x, y, p0, selectionEnd); 192 drawUnselectedText(g, x, y, selectionEnd, p1); 193 } 194 195 // start of range unselected, end of range selected 196 else if (selectionStart > p0 && selectionEnd > p1) 197 { 198 x = drawUnselectedText(g, x, y, p0, selectionStart); 199 drawSelectedText(g, x, y, selectionStart, p1); 200 } 201 202 // middle of range selected 203 else if (selectionStart > p0) 204 { 205 x = drawUnselectedText(g, x, y, p0, selectionStart); 206 x = drawSelectedText(g, x, y, selectionStart, selectionEnd); 207 drawUnselectedText(g, x, y, selectionEnd, p1); 208 } 209 } 210 catch (BadLocationException ble) 211 { 212 // shouldn't happen 213 } 214 } 215 216 /** 217 * Renders the range of text as selected text. Just paints the text 218 * in the color specified by the host component. Assumes the highlighter 219 * will render the selected background. 220 * @param g the graphics context 221 * @param x the starting X coordinate 222 * @param y the starting Y coordinate 223 * @param p0 the starting model location 224 * @param p1 the ending model location 225 * @return the X coordinate of the end of the text 226 * @throws BadLocationException if the given range is invalid 227 */ 228 protected int drawSelectedText(Graphics g, int x, int y, int p0, int p1) 229 throws BadLocationException 230 { 231 g.setColor(selectedColor); 232 Segment segment = getLineBuffer(); 233 getDocument().getText(p0, p1 - p0, segment); 234 return Utilities.drawTabbedText(segment, x, y, g, this, p0); 235 } 236 237 /** 238 * Renders the range of text as normal unhighlighted text. 239 * @param g the graphics context 240 * @param x the starting X coordinate 241 * @param y the starting Y coordinate 242 * @param p0 the starting model location 243 * @param p1 the end model location 244 * @return the X location of the end off the range 245 * @throws BadLocationException if the range given is invalid 246 */ 247 protected int drawUnselectedText(Graphics g, int x, int y, int p0, int p1) 248 throws BadLocationException 249 { 250 JTextComponent textComponent = (JTextComponent) getContainer(); 251 if (textComponent.isEnabled()) 252 g.setColor(unselectedColor); 253 else 254 g.setColor(disabledColor); 255 256 Segment segment = getLineBuffer(); 257 getDocument().getText(p0, p1 - p0, segment); 258 return Utilities.drawTabbedText(segment, x, y, g, this, p0); 259 } 260 261 /** 262 * Loads the children to initiate the view. Called by setParent. 263 * Creates a WrappedLine for each child Element. 264 */ 265 protected void loadChildren (ViewFactory f) 266 { 267 Element root = getElement(); 268 int numChildren = root.getElementCount(); 269 if (numChildren == 0) 270 return; 271 272 View[] children = new View[numChildren]; 273 for (int i = 0; i < numChildren; i++) 274 children[i] = new WrappedLine(root.getElement(i)); 275 replace(0, 0, children); 276 } 277 278 /** 279 * Calculates the break position for the text between model positions 280 * p0 and p1. Will break on word boundaries or character boundaries 281 * depending on the break argument given in construction of this 282 * WrappedPlainView. Used by the nested WrappedLine class to determine 283 * when to start the next logical line. 284 * @param p0 the start model position 285 * @param p1 the end model position 286 * @return the model position at which to break the text 287 */ 288 protected int calculateBreakPosition(int p0, int p1) 289 { 290 Segment s = new Segment(); 291 try 292 { 293 getDocument().getText(p0, p1 - p0, s); 294 } 295 catch (BadLocationException ex) 296 { 297 assert false : "Couldn't load text"; 298 } 299 int width = getWidth(); 300 int pos; 301 if (wordWrap) 302 pos = p0 + Utilities.getBreakLocation(s, metrics, tabBase, 303 tabBase + width, this, p0); 304 else 305 pos = p0 + Utilities.getTabbedTextOffset(s, metrics, tabBase, 306 tabBase + width, this, p0, 307 false); 308 return pos; 309 } 310 311 void updateMetrics() 312 { 313 Container component = getContainer(); 314 metrics = component.getFontMetrics(component.getFont()); 315 tabSize = getTabSize()* metrics.charWidth('m'); 316 } 317 318 /** 319 * Determines the preferred span along the given axis. Implemented to 320 * cache the font metrics and then call the super classes method. 321 */ 322 public float getPreferredSpan (int axis) 323 { 324 updateMetrics(); 325 return super.getPreferredSpan(axis); 326 } 327 328 /** 329 * Determines the minimum span along the given axis. Implemented to 330 * cache the font metrics and then call the super classes method. 331 */ 332 public float getMinimumSpan (int axis) 333 { 334 updateMetrics(); 335 return super.getMinimumSpan(axis); 336 } 337 338 /** 339 * Determines the maximum span along the given axis. Implemented to 340 * cache the font metrics and then call the super classes method. 341 */ 342 public float getMaximumSpan (int axis) 343 { 344 updateMetrics(); 345 return super.getMaximumSpan(axis); 346 } 347 348 /** 349 * Called when something was inserted. Overridden so that 350 * the view factory creates WrappedLine views. 351 */ 352 public void insertUpdate (DocumentEvent e, Shape a, ViewFactory f) 353 { 354 // Update children efficiently. 355 updateChildren(e, a); 356 357 // Notify children. 358 Rectangle r = a != null && isAllocationValid() ? getInsideAllocation(a) 359 : null; 360 View v = getViewAtPosition(e.getOffset(), r); 361 if (v != null) 362 v.insertUpdate(e, r, f); 363 } 364 365 /** 366 * Called when something is removed. Overridden so that 367 * the view factory creates WrappedLine views. 368 */ 369 public void removeUpdate (DocumentEvent e, Shape a, ViewFactory f) 370 { 371 // Update children efficiently. 372 updateChildren(e, a); 373 374 // Notify children. 375 Rectangle r = a != null && isAllocationValid() ? getInsideAllocation(a) 376 : null; 377 View v = getViewAtPosition(e.getOffset(), r); 378 if (v != null) 379 v.removeUpdate(e, r, f); 380 } 381 382 /** 383 * Called when the portion of the Document that this View is responsible 384 * for changes. Overridden so that the view factory creates 385 * WrappedLine views. 386 */ 387 public void changedUpdate (DocumentEvent e, Shape a, ViewFactory f) 388 { 389 // Update children efficiently. 390 updateChildren(e, a); 391 } 392 393 /** 394 * Helper method. Updates the child views in response to 395 * insert/remove/change updates. This is here to be a little more efficient 396 * than the BoxView implementation. 397 * 398 * @param ev the document event 399 * @param a the shape 400 */ 401 private void updateChildren(DocumentEvent ev, Shape a) 402 { 403 Element el = getElement(); 404 DocumentEvent.ElementChange ec = ev.getChange(el); 405 if (ec != null) 406 { 407 Element[] removed = ec.getChildrenRemoved(); 408 Element[] added = ec.getChildrenAdded(); 409 View[] addedViews = new View[added.length]; 410 for (int i = 0; i < added.length; i++) 411 addedViews[i] = new WrappedLine(added[i]); 412 replace(ec.getIndex(), removed.length, addedViews); 413 if (a != null) 414 { 415 preferenceChanged(null, true, true); 416 getContainer().repaint(); 417 } 418 } 419 updateMetrics(); 420 } 421 422 class WrappedLineCreator implements ViewFactory 423 { 424 // Creates a new WrappedLine 425 public View create(Element elem) 426 { 427 return new WrappedLine(elem); 428 } 429 } 430 431 /** 432 * Renders the <code>Element</code> that is associated with this 433 * <code>View</code>. Caches the metrics and then calls 434 * super.paint to paint all the child views. 435 * 436 * @param g the <code>Graphics</code> context to render to 437 * @param a the allocated region for the <code>Element</code> 438 */ 439 public void paint(Graphics g, Shape a) 440 { 441 Rectangle r = a instanceof Rectangle ? (Rectangle) a : a.getBounds(); 442 tabBase = r.x; 443 444 JTextComponent comp = (JTextComponent)getContainer(); 445 // Ensure metrics are up-to-date. 446 updateMetrics(); 447 448 selectionStart = comp.getSelectionStart(); 449 selectionEnd = comp.getSelectionEnd(); 450 451 selectedColor = comp.getSelectedTextColor(); 452 unselectedColor = comp.getForeground(); 453 disabledColor = comp.getDisabledTextColor(); 454 selectedColor = comp.getSelectedTextColor(); 455 lineHeight = metrics.getHeight(); 456 g.setFont(comp.getFont()); 457 458 super.paint(g, a); 459 } 460 461 /** 462 * Sets the size of the View. Implemented to update the metrics 463 * and then call super method. 464 */ 465 public void setSize (float width, float height) 466 { 467 updateMetrics(); 468 if (width != getWidth()) 469 preferenceChanged(null, true, true); 470 super.setSize(width, height); 471 } 472 473 class WrappedLine extends View 474 { 475 /** Used to cache the number of lines for this View **/ 476 int numLines = 1; 477 478 public WrappedLine(Element elem) 479 { 480 super(elem); 481 } 482 483 /** 484 * Renders this (possibly wrapped) line using the given Graphics object 485 * and on the given rendering surface. 486 */ 487 public void paint(Graphics g, Shape s) 488 { 489 Rectangle rect = s.getBounds(); 490 491 int end = getEndOffset(); 492 int currStart = getStartOffset(); 493 int currEnd; 494 int count = 0; 495 496 // Determine layered highlights. 497 Container c = getContainer(); 498 LayeredHighlighter lh = null; 499 JTextComponent tc = null; 500 if (c instanceof JTextComponent) 501 { 502 tc = (JTextComponent) c; 503 Highlighter h = tc.getHighlighter(); 504 if (h instanceof LayeredHighlighter) 505 lh = (LayeredHighlighter) h; 506 } 507 508 while (currStart < end) 509 { 510 currEnd = calculateBreakPosition(currStart, end); 511 512 // Paint layered highlights, if any. 513 if (lh != null) 514 { 515 // Exclude trailing newline in last line. 516 if (currEnd == end) 517 lh.paintLayeredHighlights(g, currStart, currEnd - 1, s, tc, 518 this); 519 else 520 lh.paintLayeredHighlights(g, currStart, currEnd, s, tc, this); 521 522 } 523 drawLine(currStart, currEnd, g, rect.x, rect.y + metrics.getAscent()); 524 525 rect.y += lineHeight; 526 if (currEnd == currStart) 527 currStart ++; 528 else 529 currStart = currEnd; 530 531 count++; 532 533 } 534 535 if (count != numLines) 536 { 537 numLines = count; 538 preferenceChanged(this, false, true); 539 } 540 541 } 542 543 /** 544 * Calculates the number of logical lines that the Element 545 * needs to be displayed and updates the variable numLines 546 * accordingly. 547 */ 548 private int determineNumLines() 549 { 550 int nLines = 0; 551 int end = getEndOffset(); 552 for (int i = getStartOffset(); i < end;) 553 { 554 nLines++; 555 // careful: check that there's no off-by-one problem here 556 // depending on which position calculateBreakPosition returns 557 int breakPoint = calculateBreakPosition(i, end); 558 559 if (breakPoint == i) 560 i = breakPoint + 1; 561 else 562 i = breakPoint; 563 } 564 return nLines; 565 } 566 567 /** 568 * Determines the preferred span for this view along the given axis. 569 * 570 * @param axis the axis (either X_AXIS or Y_AXIS) 571 * 572 * @return the preferred span along the given axis. 573 * @throws IllegalArgumentException if axis is not X_AXIS or Y_AXIS 574 */ 575 public float getPreferredSpan(int axis) 576 { 577 if (axis == X_AXIS) 578 return getWidth(); 579 else if (axis == Y_AXIS) 580 { 581 if (metrics == null) 582 updateMetrics(); 583 return numLines * metrics.getHeight(); 584 } 585 586 throw new IllegalArgumentException("Invalid axis for getPreferredSpan: " 587 + axis); 588 } 589 590 /** 591 * Provides a mapping from model space to view space. 592 * 593 * @param pos the position in the model 594 * @param a the region into which the view is rendered 595 * @param b the position bias (forward or backward) 596 * 597 * @return a box in view space that represents the given position 598 * in model space 599 * @throws BadLocationException if the given model position is invalid 600 */ 601 public Shape modelToView(int pos, Shape a, Bias b) 602 throws BadLocationException 603 { 604 Rectangle rect = a.getBounds(); 605 606 // Throwing a BadLocationException is an observed behavior of the RI. 607 if (rect.isEmpty()) 608 throw new BadLocationException("Unable to calculate view coordinates " 609 + "when allocation area is empty.", pos); 610 611 Segment s = getLineBuffer(); 612 int lineHeight = metrics.getHeight(); 613 614 // Return a rectangle with width 1 and height equal to the height 615 // of the text 616 rect.height = lineHeight; 617 rect.width = 1; 618 619 int currLineStart = getStartOffset(); 620 int end = getEndOffset(); 621 622 if (pos < currLineStart || pos >= end) 623 throw new BadLocationException("invalid offset", pos); 624 625 while (true) 626 { 627 int currLineEnd = calculateBreakPosition(currLineStart, end); 628 // If pos is between currLineStart and currLineEnd then just find 629 // the width of the text from currLineStart to pos and add that 630 // to rect.x 631 if (pos >= currLineStart && pos < currLineEnd) 632 { 633 try 634 { 635 getDocument().getText(currLineStart, pos - currLineStart, s); 636 } 637 catch (BadLocationException ble) 638 { 639 // Shouldn't happen 640 } 641 rect.x += Utilities.getTabbedTextWidth(s, metrics, rect.x, 642 WrappedPlainView.this, 643 currLineStart); 644 return rect; 645 } 646 // Increment rect.y so we're checking the next logical line 647 rect.y += lineHeight; 648 649 // Increment currLineStart to the model position of the start 650 // of the next logical line 651 if (currLineEnd == currLineStart) 652 currLineStart = end; 653 else 654 currLineStart = currLineEnd; 655 } 656 657 } 658 659 /** 660 * Provides a mapping from view space to model space. 661 * 662 * @param x the x coordinate in view space 663 * @param y the y coordinate in view space 664 * @param a the region into which the view is rendered 665 * @param b the position bias (forward or backward) 666 * 667 * @return the location in the model that best represents the 668 * given point in view space 669 */ 670 public int viewToModel(float x, float y, Shape a, Bias[] b) 671 { 672 Segment s = getLineBuffer(); 673 Rectangle rect = a.getBounds(); 674 int currLineStart = getStartOffset(); 675 676 // Although calling modelToView with the last possible offset will 677 // cause a BadLocationException in CompositeView it is allowed 678 // to return that offset in viewToModel. 679 int end = getEndOffset(); 680 681 int lineHeight = metrics.getHeight(); 682 if (y < rect.y) 683 return currLineStart; 684 685 if (y > rect.y + rect.height) 686 return end - 1; 687 688 // Note: rect.x and rect.width do not represent the width of painted 689 // text but the area where text *may* be painted. This means the width 690 // is most of the time identical to the component's width. 691 692 while (currLineStart != end) 693 { 694 int currLineEnd = calculateBreakPosition(currLineStart, end); 695 696 // If we're at the right y-position that means we're on the right 697 // logical line and we should look for the character 698 if (y >= rect.y && y < rect.y + lineHeight) 699 { 700 try 701 { 702 getDocument().getText(currLineStart, currLineEnd - currLineStart, s); 703 } 704 catch (BadLocationException ble) 705 { 706 // Shouldn't happen 707 } 708 709 int offset = Utilities.getTabbedTextOffset(s, metrics, rect.x, 710 (int) x, 711 WrappedPlainView.this, 712 currLineStart); 713 // If the calculated offset is the end of the line (in the 714 // document (= start of the next line) return the preceding 715 // offset instead. This makes sure that clicking right besides 716 // the last character in a line positions the cursor after the 717 // last character and not in the beginning of the next line. 718 return (offset == currLineEnd) ? offset - 1 : offset; 719 } 720 // Increment rect.y so we're checking the next logical line 721 rect.y += lineHeight; 722 723 // Increment currLineStart to the model position of the start 724 // of the next logical line. 725 currLineStart = currLineEnd; 726 727 } 728 729 return end; 730 } 731 732 /** 733 * <p>This method is called from insertUpdate and removeUpdate.</p> 734 * 735 * <p>If the number of lines in the document has changed, just repaint 736 * the whole thing (note, could improve performance by not repainting 737 * anything above the changes). If the number of lines hasn't changed, 738 * just repaint the given Rectangle.</p> 739 * 740 * <p>Note that the <code>Rectangle</code> argument may be <code>null</code> 741 * when the allocation area is empty.</code> 742 * 743 * @param a the Rectangle to repaint if the number of lines hasn't changed 744 */ 745 void updateDamage (Rectangle a) 746 { 747 int nLines = determineNumLines(); 748 if (numLines != nLines) 749 { 750 numLines = nLines; 751 preferenceChanged(this, false, true); 752 getContainer().repaint(); 753 } 754 else if (a != null) 755 getContainer().repaint(a.x, a.y, a.width, a.height); 756 } 757 758 /** 759 * This method is called when something is inserted into the Document 760 * that this View is displaying. 761 * 762 * @param changes the DocumentEvent for the changes. 763 * @param a the allocation of the View 764 * @param f the ViewFactory used to rebuild 765 */ 766 public void insertUpdate (DocumentEvent changes, Shape a, ViewFactory f) 767 { 768 Rectangle r = a instanceof Rectangle ? (Rectangle) a : a.getBounds(); 769 updateDamage(r); 770 } 771 772 /** 773 * This method is called when something is removed from the Document 774 * that this View is displaying. 775 * 776 * @param changes the DocumentEvent for the changes. 777 * @param a the allocation of the View 778 * @param f the ViewFactory used to rebuild 779 */ 780 public void removeUpdate (DocumentEvent changes, Shape a, ViewFactory f) 781 { 782 // Note: This method is not called when characters from the 783 // end of the document are removed. The reason for this 784 // can be found in the implementation of View.forwardUpdate: 785 // The document event will denote offsets which do not exist 786 // any more, getViewIndex() will therefore return -1 and this 787 // makes View.forwardUpdate() skip this method call. 788 // However this seems to cause no trouble and as it reduces the 789 // number of method calls it can stay this way. 790 791 Rectangle r = a instanceof Rectangle ? (Rectangle) a : a.getBounds(); 792 updateDamage(r); 793 } 794 } 795}