Frames | No Frames |
1: /* WrappedPlainView.java -- 2: Copyright (C) 2005, 2006 Free Software Foundation, Inc. 3: 4: This file is part of GNU Classpath. 5: 6: GNU Classpath is free software; you can redistribute it and/or modify 7: it under the terms of the GNU General Public License as published by 8: the Free Software Foundation; either version 2, or (at your option) 9: any later version. 10: 11: GNU Classpath is distributed in the hope that it will be useful, but 12: WITHOUT ANY WARRANTY; without even the implied warranty of 13: MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14: General Public License for more details. 15: 16: You should have received a copy of the GNU General Public License 17: along with GNU Classpath; see the file COPYING. If not, write to the 18: Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 19: 02110-1301 USA. 20: 21: Linking this library statically or dynamically with other modules is 22: making a combined work based on this library. Thus, the terms and 23: conditions of the GNU General Public License cover the whole 24: combination. 25: 26: As a special exception, the copyright holders of this library give you 27: permission to link this library with independent modules to produce an 28: executable, regardless of the license terms of these independent 29: modules, and to copy and distribute the resulting executable under 30: terms of your choice, provided that you also meet, for each linked 31: independent module, the terms and conditions of the license of that 32: module. An independent module is a module which is not derived from 33: or based on this library. If you modify this library, you may extend 34: this exception to your version of the library, but you are not 35: obligated to do so. If you do not wish to do so, delete this 36: exception statement from your version. */ 37: 38: 39: package javax.swing.text; 40: 41: import java.awt.Color; 42: import java.awt.Container; 43: import java.awt.FontMetrics; 44: import java.awt.Graphics; 45: import java.awt.Rectangle; 46: import java.awt.Shape; 47: 48: import javax.swing.event.DocumentEvent; 49: import javax.swing.text.Position.Bias; 50: 51: /** 52: * @author Anthony Balkissoon abalkiss at redhat dot com 53: * 54: */ 55: public class WrappedPlainView extends BoxView implements TabExpander 56: { 57: /** The color for selected text **/ 58: Color selectedColor; 59: 60: /** The color for unselected text **/ 61: Color unselectedColor; 62: 63: /** The color for disabled components **/ 64: Color disabledColor; 65: 66: /** 67: * Stores the font metrics. This is package private to avoid synthetic 68: * accessor method. 69: */ 70: FontMetrics metrics; 71: 72: /** Whether or not to wrap on word boundaries **/ 73: boolean wordWrap; 74: 75: /** A ViewFactory that creates WrappedLines **/ 76: ViewFactory viewFactory = new WrappedLineCreator(); 77: 78: /** The start of the selected text **/ 79: int selectionStart; 80: 81: /** The end of the selected text **/ 82: int selectionEnd; 83: 84: /** The height of the line (used while painting) **/ 85: int lineHeight; 86: 87: /** 88: * The instance returned by {@link #getLineBuffer()}. 89: */ 90: private transient Segment lineBuffer; 91: 92: public WrappedPlainView (Element elem) 93: { 94: this (elem, false); 95: } 96: 97: public WrappedPlainView (Element elem, boolean wordWrap) 98: { 99: super (elem, Y_AXIS); 100: this.wordWrap = wordWrap; 101: } 102: 103: /** 104: * Provides access to the Segment used for retrievals from the Document. 105: * @return the Segment. 106: */ 107: protected final Segment getLineBuffer() 108: { 109: if (lineBuffer == null) 110: lineBuffer = new Segment(); 111: return lineBuffer; 112: } 113: 114: /** 115: * Returns the next tab stop position after a given reference position. 116: * 117: * This implementation ignores the <code>tabStop</code> argument. 118: * 119: * @param x the current x position in pixels 120: * @param tabStop the position within the text stream that the tab occured at 121: */ 122: public float nextTabStop(float x, int tabStop) 123: { 124: JTextComponent host = (JTextComponent)getContainer(); 125: float tabSizePixels = getTabSize() 126: * host.getFontMetrics(host.getFont()).charWidth('m'); 127: return (float) (Math.floor(x / tabSizePixels) + 1) * tabSizePixels; 128: } 129: 130: /** 131: * Returns the tab size for the Document based on 132: * PlainDocument.tabSizeAttribute, defaulting to 8 if this property is 133: * not defined 134: * 135: * @return the tab size. 136: */ 137: protected int getTabSize() 138: { 139: Object tabSize = getDocument().getProperty(PlainDocument.tabSizeAttribute); 140: if (tabSize == null) 141: return 8; 142: return ((Integer)tabSize).intValue(); 143: } 144: 145: /** 146: * Draws a line of text, suppressing white space at the end and expanding 147: * tabs. Calls drawSelectedText and drawUnselectedText. 148: * @param p0 starting document position to use 149: * @param p1 ending document position to use 150: * @param g graphics context 151: * @param x starting x position 152: * @param y starting y position 153: */ 154: protected void drawLine(int p0, int p1, Graphics g, int x, int y) 155: { 156: try 157: { 158: // We have to draw both selected and unselected text. There are 159: // several cases: 160: // - entire range is unselected 161: // - entire range is selected 162: // - start of range is selected, end of range is unselected 163: // - start of range is unselected, end of range is selected 164: // - middle of range is selected, start and end of range is unselected 165: 166: // entire range unselected: 167: if ((selectionStart == selectionEnd) || 168: (p0 > selectionEnd || p1 < selectionStart)) 169: drawUnselectedText(g, x, y, p0, p1); 170: 171: // entire range selected 172: else if (p0 >= selectionStart && p1 <= selectionEnd) 173: drawSelectedText(g, x, y, p0, p1); 174: 175: // start of range selected, end of range unselected 176: else if (p0 >= selectionStart) 177: { 178: x = drawSelectedText(g, x, y, p0, selectionEnd); 179: drawUnselectedText(g, x, y, selectionEnd, p1); 180: } 181: 182: // start of range unselected, end of range selected 183: else if (selectionStart > p0 && selectionEnd > p1) 184: { 185: x = drawUnselectedText(g, x, y, p0, selectionStart); 186: drawSelectedText(g, x, y, selectionStart, p1); 187: } 188: 189: // middle of range selected 190: else if (selectionStart > p0) 191: { 192: x = drawUnselectedText(g, x, y, p0, selectionStart); 193: x = drawSelectedText(g, x, y, selectionStart, selectionEnd); 194: drawUnselectedText(g, x, y, selectionEnd, p1); 195: } 196: } 197: catch (BadLocationException ble) 198: { 199: // shouldn't happen 200: } 201: } 202: 203: /** 204: * Renders the range of text as selected text. Just paints the text 205: * in the color specified by the host component. Assumes the highlighter 206: * will render the selected background. 207: * @param g the graphics context 208: * @param x the starting X coordinate 209: * @param y the starting Y coordinate 210: * @param p0 the starting model location 211: * @param p1 the ending model location 212: * @return the X coordinate of the end of the text 213: * @throws BadLocationException if the given range is invalid 214: */ 215: protected int drawSelectedText(Graphics g, int x, int y, int p0, int p1) 216: throws BadLocationException 217: { 218: g.setColor(selectedColor); 219: Segment segment = getLineBuffer(); 220: getDocument().getText(p0, p1 - p0, segment); 221: return Utilities.drawTabbedText(segment, x, y, g, this, p0); 222: } 223: 224: /** 225: * Renders the range of text as normal unhighlighted text. 226: * @param g the graphics context 227: * @param x the starting X coordinate 228: * @param y the starting Y coordinate 229: * @param p0 the starting model location 230: * @param p1 the end model location 231: * @return the X location of the end off the range 232: * @throws BadLocationException if the range given is invalid 233: */ 234: protected int drawUnselectedText(Graphics g, int x, int y, int p0, int p1) 235: throws BadLocationException 236: { 237: JTextComponent textComponent = (JTextComponent) getContainer(); 238: if (textComponent.isEnabled()) 239: g.setColor(unselectedColor); 240: else 241: g.setColor(disabledColor); 242: 243: Segment segment = getLineBuffer(); 244: getDocument().getText(p0, p1 - p0, segment); 245: return Utilities.drawTabbedText(segment, x, y, g, this, p0); 246: } 247: 248: /** 249: * Loads the children to initiate the view. Called by setParent. 250: * Creates a WrappedLine for each child Element. 251: */ 252: protected void loadChildren (ViewFactory f) 253: { 254: Element root = getElement(); 255: int numChildren = root.getElementCount(); 256: if (numChildren == 0) 257: return; 258: 259: View[] children = new View[numChildren]; 260: for (int i = 0; i < numChildren; i++) 261: children[i] = new WrappedLine(root.getElement(i)); 262: replace(0, 0, children); 263: } 264: 265: /** 266: * Calculates the break position for the text between model positions 267: * p0 and p1. Will break on word boundaries or character boundaries 268: * depending on the break argument given in construction of this 269: * WrappedPlainView. Used by the nested WrappedLine class to determine 270: * when to start the next logical line. 271: * @param p0 the start model position 272: * @param p1 the end model position 273: * @return the model position at which to break the text 274: */ 275: protected int calculateBreakPosition(int p0, int p1) 276: { 277: Container c = getContainer(); 278: 279: int li = getLeftInset(); 280: int ti = getTopInset(); 281: 282: Rectangle alloc = new Rectangle(li, ti, 283: getWidth()-getRightInset()-li, 284: getHeight()-getBottomInset()-ti); 285: 286: // Mimic a behavior observed in the RI. 287: if (alloc.isEmpty()) 288: return 0; 289: 290: updateMetrics(); 291: 292: try 293: { 294: getDocument().getText(p0, p1 - p0, getLineBuffer()); 295: } 296: catch (BadLocationException ble) 297: { 298: // this shouldn't happen 299: throw new InternalError("Invalid offsets p0: " + p0 + " - p1: " + p1); 300: } 301: 302: if (wordWrap) 303: return Utilities.getBreakLocation(lineBuffer, metrics, alloc.x, 304: alloc.x + alloc.width, this, p0); 305: else 306: return p0 + Utilities.getTabbedTextOffset(lineBuffer, metrics, alloc.x, 307: alloc.x + alloc.width, this, 0, 308: true); 309: } 310: 311: void updateMetrics() 312: { 313: Container component = getContainer(); 314: metrics = component.getFontMetrics(component.getFont()); 315: } 316: 317: /** 318: * Determines the preferred span along the given axis. Implemented to 319: * cache the font metrics and then call the super classes method. 320: */ 321: public float getPreferredSpan (int axis) 322: { 323: updateMetrics(); 324: return super.getPreferredSpan(axis); 325: } 326: 327: /** 328: * Determines the minimum span along the given axis. Implemented to 329: * cache the font metrics and then call the super classes method. 330: */ 331: public float getMinimumSpan (int axis) 332: { 333: updateMetrics(); 334: return super.getMinimumSpan(axis); 335: } 336: 337: /** 338: * Determines the maximum span along the given axis. Implemented to 339: * cache the font metrics and then call the super classes method. 340: */ 341: public float getMaximumSpan (int axis) 342: { 343: updateMetrics(); 344: return super.getMaximumSpan(axis); 345: } 346: 347: /** 348: * Called when something was inserted. Overridden so that 349: * the view factory creates WrappedLine views. 350: */ 351: public void insertUpdate (DocumentEvent e, Shape a, ViewFactory f) 352: { 353: super.insertUpdate(e, a, viewFactory); 354: 355: // No repaint needed, as this is done by the WrappedLine instances. 356: } 357: 358: /** 359: * Called when something is removed. Overridden so that 360: * the view factory creates WrappedLine views. 361: */ 362: public void removeUpdate (DocumentEvent e, Shape a, ViewFactory f) 363: { 364: super.removeUpdate(e, a, viewFactory); 365: 366: // No repaint needed, as this is done by the WrappedLine instances. 367: } 368: 369: /** 370: * Called when the portion of the Document that this View is responsible 371: * for changes. Overridden so that the view factory creates 372: * WrappedLine views. 373: */ 374: public void changedUpdate (DocumentEvent e, Shape a, ViewFactory f) 375: { 376: super.changedUpdate(e, a, viewFactory); 377: 378: // No repaint needed, as this is done by the WrappedLine instances. 379: } 380: 381: class WrappedLineCreator implements ViewFactory 382: { 383: // Creates a new WrappedLine 384: public View create(Element elem) 385: { 386: return new WrappedLine(elem); 387: } 388: } 389: 390: /** 391: * Renders the <code>Element</code> that is associated with this 392: * <code>View</code>. Caches the metrics and then calls 393: * super.paint to paint all the child views. 394: * 395: * @param g the <code>Graphics</code> context to render to 396: * @param a the allocated region for the <code>Element</code> 397: */ 398: public void paint(Graphics g, Shape a) 399: { 400: JTextComponent comp = (JTextComponent)getContainer(); 401: // Ensure metrics are up-to-date. 402: updateMetrics(); 403: 404: selectionStart = comp.getSelectionStart(); 405: selectionEnd = comp.getSelectionEnd(); 406: 407: selectedColor = comp.getSelectedTextColor(); 408: unselectedColor = comp.getForeground(); 409: disabledColor = comp.getDisabledTextColor(); 410: selectedColor = comp.getSelectedTextColor(); 411: lineHeight = metrics.getHeight(); 412: g.setFont(comp.getFont()); 413: 414: super.paint(g, a); 415: } 416: 417: /** 418: * Sets the size of the View. Implemented to update the metrics 419: * and then call super method. 420: */ 421: public void setSize (float width, float height) 422: { 423: updateMetrics(); 424: if (width != getWidth()) 425: preferenceChanged(null, true, true); 426: super.setSize(width, height); 427: } 428: 429: class WrappedLine extends View 430: { 431: /** Used to cache the number of lines for this View **/ 432: int numLines = 1; 433: 434: public WrappedLine(Element elem) 435: { 436: super(elem); 437: determineNumLines(); 438: } 439: 440: /** 441: * Renders this (possibly wrapped) line using the given Graphics object 442: * and on the given rendering surface. 443: */ 444: public void paint(Graphics g, Shape s) 445: { 446: Rectangle rect = s.getBounds(); 447: 448: int end = getEndOffset(); 449: int currStart = getStartOffset(); 450: int currEnd; 451: int count = 0; 452: while (currStart < end) 453: { 454: currEnd = calculateBreakPosition(currStart, end); 455: 456: drawLine(currStart, currEnd, g, rect.x, rect.y + metrics.getAscent()); 457: 458: rect.y += lineHeight; 459: if (currEnd == currStart) 460: currStart ++; 461: else 462: currStart = currEnd; 463: 464: count++; 465: 466: } 467: 468: if (count != numLines) 469: { 470: numLines = count; 471: preferenceChanged(this, false, true); 472: } 473: 474: } 475: 476: /** 477: * Calculates the number of logical lines that the Element 478: * needs to be displayed and updates the variable numLines 479: * accordingly. 480: */ 481: void determineNumLines() 482: { 483: numLines = 0; 484: int end = getEndOffset(); 485: if (end == 0) 486: return; 487: 488: int breakPoint; 489: for (int i = getStartOffset(); i < end;) 490: { 491: numLines ++; 492: // careful: check that there's no off-by-one problem here 493: // depending on which position calculateBreakPosition returns 494: breakPoint = calculateBreakPosition(i, end); 495: 496: if (breakPoint == 0) 497: return; 498: 499: // If breakPoint is equal to the current index no further 500: // line is needed and we can end the loop. 501: if (breakPoint == i) 502: break; 503: else 504: i = breakPoint; 505: } 506: } 507: 508: /** 509: * Determines the preferred span for this view along the given axis. 510: * 511: * @param axis the axis (either X_AXIS or Y_AXIS) 512: * 513: * @return the preferred span along the given axis. 514: * @throws IllegalArgumentException if axis is not X_AXIS or Y_AXIS 515: */ 516: public float getPreferredSpan(int axis) 517: { 518: if (axis == X_AXIS) 519: return getWidth(); 520: else if (axis == Y_AXIS) 521: { 522: if (metrics == null) 523: updateMetrics(); 524: return numLines * metrics.getHeight(); 525: } 526: 527: throw new IllegalArgumentException("Invalid axis for getPreferredSpan: " 528: + axis); 529: } 530: 531: /** 532: * Provides a mapping from model space to view space. 533: * 534: * @param pos the position in the model 535: * @param a the region into which the view is rendered 536: * @param b the position bias (forward or backward) 537: * 538: * @return a box in view space that represents the given position 539: * in model space 540: * @throws BadLocationException if the given model position is invalid 541: */ 542: public Shape modelToView(int pos, Shape a, Bias b) 543: throws BadLocationException 544: { 545: Rectangle rect = a.getBounds(); 546: 547: // Throwing a BadLocationException is an observed behavior of the RI. 548: if (rect.isEmpty()) 549: throw new BadLocationException("Unable to calculate view coordinates " 550: + "when allocation area is empty.", 5); 551: 552: Segment s = getLineBuffer(); 553: int lineHeight = metrics.getHeight(); 554: 555: // Return a rectangle with width 1 and height equal to the height 556: // of the text 557: rect.height = lineHeight; 558: rect.width = 1; 559: 560: int currLineStart = getStartOffset(); 561: int end = getEndOffset(); 562: 563: if (pos < currLineStart || pos >= end) 564: throw new BadLocationException("invalid offset", pos); 565: 566: while (true) 567: { 568: int currLineEnd = calculateBreakPosition(currLineStart, end); 569: // If pos is between currLineStart and currLineEnd then just find 570: // the width of the text from currLineStart to pos and add that 571: // to rect.x 572: if (pos >= currLineStart && pos < currLineEnd) 573: { 574: try 575: { 576: getDocument().getText(currLineStart, pos - currLineStart, s); 577: } 578: catch (BadLocationException ble) 579: { 580: // Shouldn't happen 581: } 582: rect.x += Utilities.getTabbedTextWidth(s, metrics, rect.x, 583: WrappedPlainView.this, 584: currLineStart); 585: return rect; 586: } 587: // Increment rect.y so we're checking the next logical line 588: rect.y += lineHeight; 589: 590: // Increment currLineStart to the model position of the start 591: // of the next logical line 592: if (currLineEnd == currLineStart) 593: currLineStart = end; 594: else 595: currLineStart = currLineEnd; 596: } 597: 598: } 599: 600: /** 601: * Provides a mapping from view space to model space. 602: * 603: * @param x the x coordinate in view space 604: * @param y the y coordinate in view space 605: * @param a the region into which the view is rendered 606: * @param b the position bias (forward or backward) 607: * 608: * @return the location in the model that best represents the 609: * given point in view space 610: */ 611: public int viewToModel(float x, float y, Shape a, Bias[] b) 612: { 613: Segment s = getLineBuffer(); 614: Rectangle rect = a.getBounds(); 615: int currLineStart = getStartOffset(); 616: 617: // Although calling modelToView with the last possible offset will 618: // cause a BadLocationException in CompositeView it is allowed 619: // to return that offset in viewToModel. 620: int end = getEndOffset(); 621: 622: int lineHeight = metrics.getHeight(); 623: if (y < rect.y) 624: return currLineStart; 625: 626: if (y > rect.y + rect.height) 627: return end; 628: 629: // Note: rect.x and rect.width do not represent the width of painted 630: // text but the area where text *may* be painted. This means the width 631: // is most of the time identical to the component's width. 632: 633: while (currLineStart != end) 634: { 635: int currLineEnd = calculateBreakPosition(currLineStart, end); 636: 637: // If we're at the right y-position that means we're on the right 638: // logical line and we should look for the character 639: if (y >= rect.y && y < rect.y + lineHeight) 640: { 641: try 642: { 643: getDocument().getText(currLineStart, currLineEnd - currLineStart, s); 644: } 645: catch (BadLocationException ble) 646: { 647: // Shouldn't happen 648: } 649: 650: int offset = Utilities.getTabbedTextOffset(s, metrics, rect.x, 651: (int) x, 652: WrappedPlainView.this, 653: currLineStart); 654: // If the calculated offset is the end of the line (in the 655: // document (= start of the next line) return the preceding 656: // offset instead. This makes sure that clicking right besides 657: // the last character in a line positions the cursor after the 658: // last character and not in the beginning of the next line. 659: return (offset == currLineEnd) ? offset - 1 : offset; 660: } 661: // Increment rect.y so we're checking the next logical line 662: rect.y += lineHeight; 663: 664: // Increment currLineStart to the model position of the start 665: // of the next logical line. 666: currLineStart = currLineEnd; 667: 668: } 669: 670: return end; 671: } 672: 673: /** 674: * <p>This method is called from insertUpdate and removeUpdate.</p> 675: * 676: * <p>If the number of lines in the document has changed, just repaint 677: * the whole thing (note, could improve performance by not repainting 678: * anything above the changes). If the number of lines hasn't changed, 679: * just repaint the given Rectangle.</p> 680: * 681: * <p>Note that the <code>Rectangle</code> argument may be <code>null</code> 682: * when the allocation area is empty.</code> 683: * 684: * @param a the Rectangle to repaint if the number of lines hasn't changed 685: */ 686: void updateDamage (Rectangle a) 687: { 688: // If the allocation area is empty we can't do anything useful. 689: // As determining the number of lines is impossible in that state we 690: // reset it to an invalid value which can then be recalculated at a 691: // later point. 692: if (a == null || a.isEmpty()) 693: { 694: numLines = 1; 695: return; 696: } 697: 698: int oldNumLines = numLines; 699: determineNumLines(); 700: 701: if (numLines != oldNumLines) 702: preferenceChanged(this, false, true); 703: else 704: getContainer().repaint(a.x, a.y, a.width, a.height); 705: } 706: 707: /** 708: * This method is called when something is inserted into the Document 709: * that this View is displaying. 710: * 711: * @param changes the DocumentEvent for the changes. 712: * @param a the allocation of the View 713: * @param f the ViewFactory used to rebuild 714: */ 715: public void insertUpdate (DocumentEvent changes, Shape a, ViewFactory f) 716: { 717: updateDamage((Rectangle)a); 718: } 719: 720: /** 721: * This method is called when something is removed from the Document 722: * that this View is displaying. 723: * 724: * @param changes the DocumentEvent for the changes. 725: * @param a the allocation of the View 726: * @param f the ViewFactory used to rebuild 727: */ 728: public void removeUpdate (DocumentEvent changes, Shape a, ViewFactory f) 729: { 730: // Note: This method is not called when characters from the 731: // end of the document are removed. The reason for this 732: // can be found in the implementation of View.forwardUpdate: 733: // The document event will denote offsets which do not exist 734: // any more, getViewIndex() will therefore return -1 and this 735: // makes View.forwardUpdate() skip this method call. 736: // However this seems to cause no trouble and as it reduces the 737: // number of method calls it can stay this way. 738: 739: updateDamage((Rectangle)a); 740: } 741: } 742: }