001/* PlainView.java -- 002 Copyright (C) 2004, 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.Component; 043import java.awt.Font; 044import java.awt.FontMetrics; 045import java.awt.Graphics; 046import java.awt.Rectangle; 047import java.awt.Shape; 048 049import javax.swing.SwingUtilities; 050import javax.swing.event.DocumentEvent; 051import javax.swing.event.DocumentEvent.ElementChange; 052 053public class PlainView extends View implements TabExpander 054{ 055 Color selectedColor; 056 Color unselectedColor; 057 058 /** 059 * The color that is used to draw disabled text fields. 060 */ 061 Color disabledColor; 062 063 /** 064 * While painting this is the textcomponent's current start index 065 * of the selection. 066 */ 067 int selectionStart; 068 069 /** 070 * While painting this is the textcomponent's current end index 071 * of the selection. 072 */ 073 int selectionEnd; 074 075 Font font; 076 077 /** The length of the longest line in the Document **/ 078 float maxLineLength = -1; 079 080 /** The longest line in the Document **/ 081 Element longestLine = null; 082 083 protected FontMetrics metrics; 084 085 /** 086 * The instance returned by {@link #getLineBuffer()}. 087 */ 088 private transient Segment lineBuffer; 089 090 /** 091 * The base offset for tab calculations. 092 */ 093 private int tabBase; 094 095 /** 096 * The tab size. 097 */ 098 private int tabSize; 099 100 public PlainView(Element elem) 101 { 102 super(elem); 103 } 104 105 /** 106 * @since 1.4 107 */ 108 protected void updateMetrics() 109 { 110 Component component = getContainer(); 111 Font font = component.getFont(); 112 113 if (this.font != font) 114 { 115 this.font = font; 116 metrics = component.getFontMetrics(font); 117 tabSize = getTabSize() * metrics.charWidth('m'); 118 } 119 } 120 121 /** 122 * @since 1.4 123 */ 124 protected Rectangle lineToRect(Shape a, int line) 125 { 126 // Ensure metrics are up-to-date. 127 updateMetrics(); 128 129 Rectangle rect = a instanceof Rectangle ? (Rectangle) a : a.getBounds(); 130 int fontHeight = metrics.getHeight(); 131 return new Rectangle(rect.x, rect.y + (line * fontHeight), 132 rect.width, fontHeight); 133 } 134 135 public Shape modelToView(int position, Shape a, Position.Bias b) 136 throws BadLocationException 137 { 138 // Ensure metrics are up-to-date. 139 updateMetrics(); 140 141 Document document = getDocument(); 142 143 // Get rectangle of the line containing position. 144 int lineIndex = getElement().getElementIndex(position); 145 Rectangle rect = lineToRect(a, lineIndex); 146 tabBase = rect.x; 147 148 // Get the rectangle for position. 149 Element line = getElement().getElement(lineIndex); 150 int lineStart = line.getStartOffset(); 151 Segment segment = getLineBuffer(); 152 document.getText(lineStart, position - lineStart, segment); 153 int xoffset = Utilities.getTabbedTextWidth(segment, metrics, tabBase, 154 this, lineStart); 155 156 // Calc the real rectangle. 157 rect.x += xoffset; 158 rect.width = 1; 159 rect.height = metrics.getHeight(); 160 161 return rect; 162 } 163 164 /** 165 * Draws a line of text. The X and Y coordinates specify the start of 166 * the <em>baseline</em> of the line. 167 * 168 * @param lineIndex the index of the line 169 * @param g the graphics to use for drawing the text 170 * @param x the X coordinate of the baseline 171 * @param y the Y coordinate of the baseline 172 */ 173 protected void drawLine(int lineIndex, Graphics g, int x, int y) 174 { 175 try 176 { 177 Element line = getElement().getElement(lineIndex); 178 int startOffset = line.getStartOffset(); 179 int endOffset = line.getEndOffset() - 1; 180 181 if (selectionStart <= startOffset) 182 // Selection starts before the line ... 183 if (selectionEnd <= startOffset) 184 { 185 // end ends before the line: Draw completely unselected text. 186 drawUnselectedText(g, x, y, startOffset, endOffset); 187 } 188 else if (selectionEnd <= endOffset) 189 { 190 // and ends within the line: First part is selected, 191 // second is not. 192 x = drawSelectedText(g, x, y, startOffset, selectionEnd); 193 drawUnselectedText(g, x, y, selectionEnd, endOffset); 194 } 195 else 196 // and ends behind the line: Draw completely selected text. 197 drawSelectedText(g, x, y, startOffset, endOffset); 198 else if (selectionStart < endOffset) 199 // Selection starts within the line .. 200 if (selectionEnd < endOffset) 201 { 202 // and ends within it: First part unselected, second part 203 // selected, third part unselected. 204 x = drawUnselectedText(g, x, y, startOffset, selectionStart); 205 x = drawSelectedText(g, x, y, selectionStart, selectionEnd); 206 drawUnselectedText(g, x, y, selectionEnd, endOffset); 207 } 208 else 209 { 210 // and ends behind the line: First part unselected, second 211 // part selected. 212 x = drawUnselectedText(g, x, y, startOffset, selectionStart); 213 drawSelectedText(g, x, y, selectionStart, endOffset); 214 } 215 else 216 // Selection is behind this line: Draw completely unselected text. 217 drawUnselectedText(g, x, y, startOffset, endOffset); 218 } 219 catch (BadLocationException e) 220 { 221 AssertionError ae = new AssertionError("Unexpected bad location"); 222 ae.initCause(e); 223 throw ae; 224 } 225 } 226 227 protected int drawSelectedText(Graphics g, int x, int y, int p0, int p1) 228 throws BadLocationException 229 { 230 g.setColor(selectedColor); 231 Segment segment = getLineBuffer(); 232 getDocument().getText(p0, p1 - p0, segment); 233 return Utilities.drawTabbedText(segment, x, y, g, this, segment.offset); 234 } 235 236 /** 237 * Draws a chunk of unselected text. 238 * 239 * @param g the graphics to use for drawing the text 240 * @param x the X coordinate of the baseline 241 * @param y the Y coordinate of the baseline 242 * @param p0 the start position in the text model 243 * @param p1 the end position in the text model 244 * 245 * @return the X location of the end of the range 246 * 247 * @throws BadLocationException if <code>p0</code> or <code>p1</code> are 248 * invalid 249 */ 250 protected int drawUnselectedText(Graphics g, int x, int y, int p0, int p1) 251 throws BadLocationException 252 { 253 JTextComponent textComponent = (JTextComponent) getContainer(); 254 if (textComponent.isEnabled()) 255 g.setColor(unselectedColor); 256 else 257 g.setColor(disabledColor); 258 259 Segment segment = getLineBuffer(); 260 getDocument().getText(p0, p1 - p0, segment); 261 return Utilities.drawTabbedText(segment, x, y, g, this, segment.offset); 262 } 263 264 public void paint(Graphics g, Shape s) 265 { 266 // Ensure metrics are up-to-date. 267 updateMetrics(); 268 269 JTextComponent textComponent = (JTextComponent) getContainer(); 270 271 selectedColor = textComponent.getSelectedTextColor(); 272 unselectedColor = textComponent.getForeground(); 273 disabledColor = textComponent.getDisabledTextColor(); 274 selectionStart = textComponent.getSelectionStart(); 275 selectionEnd = textComponent.getSelectionEnd(); 276 277 Rectangle rect = s instanceof Rectangle ? (Rectangle) s : s.getBounds(); 278 tabBase = rect.x; 279 280 // FIXME: Text may be scrolled. 281 Document document = textComponent.getDocument(); 282 Element root = getElement(); 283 int height = metrics.getHeight(); 284 285 // For layered highlighters we need to paint the layered highlights 286 // before painting any text. 287 LayeredHighlighter hl = null; 288 Highlighter h = textComponent.getHighlighter(); 289 if (h instanceof LayeredHighlighter) 290 hl = (LayeredHighlighter) h; 291 292 int count = root.getElementCount(); 293 294 // Determine first and last line inside the clip. 295 Rectangle clip = g.getClipBounds(); 296 SwingUtilities.computeIntersection(rect.x, rect.y, rect.width, rect.height, 297 clip); 298 int line0 = (clip.y - rect.y) / height; 299 line0 = Math.max(0, Math.min(line0, count - 1)); 300 int line1 = (clip.y + clip.height - rect.y) / height; 301 line1 = Math.max(0, Math.min(line1, count - 1)); 302 int y = rect.y + metrics.getAscent() + height * line0; 303 for (int i = line0; i <= line1; i++) 304 { 305 if (hl != null) 306 { 307 Element lineEl = root.getElement(i); 308 // Exclude the trailing newline from beeing highlighted. 309 if (i == count) 310 hl.paintLayeredHighlights(g, lineEl.getStartOffset(), 311 lineEl.getEndOffset(), s, textComponent, 312 this); 313 else 314 hl.paintLayeredHighlights(g, lineEl.getStartOffset(), 315 lineEl.getEndOffset() - 1, s, 316 textComponent, this); 317 } 318 drawLine(i, g, rect.x, y); 319 y += height; 320 } 321 } 322 323 /** 324 * Returns the tab size of a tab. Checks the Document's 325 * properties for PlainDocument.tabSizeAttribute and returns it if it is 326 * defined, otherwise returns 8. 327 * 328 * @return the tab size. 329 */ 330 protected int getTabSize() 331 { 332 Object tabSize = getDocument().getProperty(PlainDocument.tabSizeAttribute); 333 if (tabSize == null) 334 return 8; 335 return ((Integer)tabSize).intValue(); 336 } 337 338 /** 339 * Returns the next tab stop position after a given reference position. 340 * 341 * This implementation ignores the <code>tabStop</code> argument. 342 * 343 * @param x the current x position in pixels 344 * @param tabStop the position within the text stream that the tab occured at 345 */ 346 public float nextTabStop(float x, int tabStop) 347 { 348 float next = x; 349 if (tabSize != 0) 350 { 351 int numTabs = (((int) x) - tabBase) / tabSize; 352 next = tabBase + (numTabs + 1) * tabSize; 353 } 354 return next; 355 } 356 357 /** 358 * Returns the length of the longest line, used for getting the span 359 * @return the length of the longest line 360 */ 361 float determineMaxLineLength() 362 { 363 // if the longest line is cached, return the cached value 364 if (maxLineLength != -1) 365 return maxLineLength; 366 367 // otherwise we have to go through all the lines and find it 368 Element el = getElement(); 369 Segment seg = getLineBuffer(); 370 float span = 0; 371 for (int i = 0; i < el.getElementCount(); i++) 372 { 373 Element child = el.getElement(i); 374 int start = child.getStartOffset(); 375 int end = child.getEndOffset() - 1; 376 try 377 { 378 el.getDocument().getText(start, end - start, seg); 379 } 380 catch (BadLocationException ex) 381 { 382 AssertionError ae = new AssertionError("Unexpected bad location"); 383 ae.initCause(ex); 384 throw ae; 385 } 386 387 if (seg == null || seg.array == null || seg.count == 0) 388 continue; 389 390 int width = metrics.charsWidth(seg.array, seg.offset, seg.count); 391 if (width > span) 392 { 393 longestLine = child; 394 span = width; 395 } 396 } 397 maxLineLength = span; 398 return maxLineLength; 399 } 400 401 public float getPreferredSpan(int axis) 402 { 403 if (axis != X_AXIS && axis != Y_AXIS) 404 throw new IllegalArgumentException(); 405 406 // make sure we have the metrics 407 updateMetrics(); 408 409 Element el = getElement(); 410 float span; 411 412 switch (axis) 413 { 414 case X_AXIS: 415 span = determineMaxLineLength(); 416 break; 417 case Y_AXIS: 418 default: 419 span = metrics.getHeight() * el.getElementCount(); 420 break; 421 } 422 423 return span; 424 } 425 426 /** 427 * Maps coordinates from the <code>View</code>'s space into a position 428 * in the document model. 429 * 430 * @param x the x coordinate in the view space 431 * @param y the y coordinate in the view space 432 * @param a the allocation of this <code>View</code> 433 * @param b the bias to use 434 * 435 * @return the position in the document that corresponds to the screen 436 * coordinates <code>x, y</code> 437 */ 438 public int viewToModel(float x, float y, Shape a, Position.Bias[] b) 439 { 440 Rectangle rec = a instanceof Rectangle ? (Rectangle) a : a.getBounds(); 441 tabBase = rec.x; 442 443 int pos; 444 if ((int) y < rec.y) 445 // Above our area vertically. Return start offset. 446 pos = getStartOffset(); 447 else if ((int) y > rec.y + rec.height) 448 // Below our area vertically. Return end offset. 449 pos = getEndOffset() - 1; 450 else 451 { 452 // Inside the allocation vertically. Determine line and X offset. 453 Document doc = getDocument(); 454 Element root = doc.getDefaultRootElement(); 455 int line = Math.abs(((int) y - rec.y) / metrics.getHeight()); 456 if (line >= root.getElementCount()) 457 pos = getEndOffset() - 1; 458 else 459 { 460 Element lineEl = root.getElement(line); 461 if (x < rec.x) 462 // To the left of the allocation area. 463 pos = lineEl.getStartOffset(); 464 else if (x > rec.x + rec.width) 465 // To the right of the allocation area. 466 pos = lineEl.getEndOffset() - 1; 467 else 468 { 469 try 470 { 471 int p0 = lineEl.getStartOffset(); 472 int p1 = lineEl.getEndOffset(); 473 Segment s = new Segment(); 474 doc.getText(p0, p1 - p0, s); 475 tabBase = rec.x; 476 pos = p0 + Utilities.getTabbedTextOffset(s, metrics, 477 tabBase, (int) x, 478 this, p0); 479 } 480 catch (BadLocationException ex) 481 { 482 // Should not happen. 483 pos = -1; 484 } 485 } 486 487 } 488 } 489 // Bias is always forward. 490 b[0] = Position.Bias.Forward; 491 return pos; 492 } 493 494 /** 495 * Since insertUpdate and removeUpdate each deal with children 496 * Elements being both added and removed, they both have to perform 497 * the same checks. So they both simply call this method. 498 * @param changes the DocumentEvent for the changes to the Document. 499 * @param a the allocation of the View. 500 * @param f the ViewFactory to use for rebuilding. 501 */ 502 protected void updateDamage(DocumentEvent changes, Shape a, ViewFactory f) 503 { 504 // This happens during initialization. 505 if (metrics == null) 506 { 507 updateMetrics(); 508 preferenceChanged(null, true, true); 509 return; 510 } 511 512 Element element = getElement(); 513 514 // Find longest line if it hasn't been initialized yet. 515 if (longestLine == null) 516 findLongestLine(0, element.getElementCount() - 1); 517 518 ElementChange change = changes.getChange(element); 519 if (changes.getType() == DocumentEvent.EventType.INSERT) 520 { 521 // Handles character/line insertion. 522 523 // Determine if lines have been added. In this case we repaint 524 // differently. 525 boolean linesAdded = true; 526 if (change == null) 527 linesAdded = false; 528 529 // Determine the start line. 530 int start; 531 if (linesAdded) 532 start = change.getIndex(); 533 else 534 start = element.getElementIndex(changes.getOffset()); 535 536 // Determine the length of the updated region. 537 int length = 0; 538 if (linesAdded) 539 length = change.getChildrenAdded().length - 1; 540 541 // Update the longest line and length. 542 int oldMaxLength = (int) maxLineLength; 543 if (longestLine.getEndOffset() < changes.getOffset() 544 || longestLine.getStartOffset() > changes.getOffset() 545 + changes.getLength()) 546 { 547 findLongestLine(start, start + length); 548 } 549 else 550 { 551 findLongestLine(0, element.getElementCount() - 1); 552 } 553 554 // Trigger a preference change so that the layout gets updated 555 // correctly. 556 preferenceChanged(null, maxLineLength != oldMaxLength, linesAdded); 557 558 // Damage the updated line range. 559 int endLine = start; 560 if (linesAdded) 561 endLine = element.getElementCount() - 1; 562 damageLineRange(start, endLine, a, getContainer()); 563 564 } 565 else 566 { 567 // Handles character/lines removals. 568 569 // Update the longest line and length and trigger preference changed. 570 int oldMaxLength = (int) maxLineLength; 571 if (change != null) 572 { 573 // Line(s) have been removed. 574 findLongestLine(0, element.getElementCount() - 1); 575 preferenceChanged(null, maxLineLength != oldMaxLength, true); 576 } 577 else 578 { 579 // No line has been removed. 580 int lineNo = getElement().getElementIndex(changes.getOffset()); 581 Element line = getElement().getElement(lineNo); 582 if (longestLine == line) 583 { 584 findLongestLine(0, element.getElementCount() - 1); 585 preferenceChanged(null, maxLineLength != oldMaxLength, false); 586 } 587 damageLineRange(lineNo, lineNo, a, getContainer()); 588 } 589 } 590 } 591 592 /** 593 * This method is called when something is inserted into the Document 594 * that this View is displaying. 595 * 596 * @param changes the DocumentEvent for the changes. 597 * @param a the allocation of the View 598 * @param f the ViewFactory used to rebuild 599 */ 600 public void insertUpdate(DocumentEvent changes, Shape a, ViewFactory f) 601 { 602 updateDamage(changes, a, f); 603 } 604 605 /** 606 * This method is called when something is removed from the Document 607 * that this View is displaying. 608 * 609 * @param changes the DocumentEvent for the changes. 610 * @param a the allocation of the View 611 * @param f the ViewFactory used to rebuild 612 */ 613 public void removeUpdate(DocumentEvent changes, Shape a, ViewFactory f) 614 { 615 updateDamage(changes, a, f); 616 } 617 618 /** 619 * This method is called when attributes were changed in the 620 * Document in a location that this view is responsible for. 621 */ 622 public void changedUpdate (DocumentEvent changes, Shape a, ViewFactory f) 623 { 624 updateDamage(changes, a, f); 625 } 626 627 /** 628 * Repaint the given line range. This is called from insertUpdate, 629 * changedUpdate, and removeUpdate when no new lines were added 630 * and no lines were removed, to repaint the line that was 631 * modified. 632 * 633 * @param line0 the start of the range 634 * @param line1 the end of the range 635 * @param a the rendering region of the host 636 * @param host the Component that uses this View (used to call repaint 637 * on that Component) 638 * 639 * @since 1.4 640 */ 641 protected void damageLineRange (int line0, int line1, Shape a, Component host) 642 { 643 if (a == null) 644 return; 645 646 Rectangle rec0 = lineToRect(a, line0); 647 Rectangle rec1 = lineToRect(a, line1); 648 649 if (rec0 == null || rec1 == null) 650 // something went wrong, repaint the entire host to be safe 651 host.repaint(); 652 else 653 { 654 Rectangle repaintRec = SwingUtilities.computeUnion(rec0.x, rec0.y, 655 rec0.width, 656 rec0.height, rec1); 657 host.repaint(repaintRec.x, repaintRec.y, repaintRec.width, 658 repaintRec.height); 659 } 660 } 661 662 /** 663 * Provides a {@link Segment} object, that can be used to fetch text from 664 * the document. 665 * 666 * @returna {@link Segment} object, that can be used to fetch text from 667 * the document 668 */ 669 protected final Segment getLineBuffer() 670 { 671 if (lineBuffer == null) 672 lineBuffer = new Segment(); 673 return lineBuffer; 674 } 675 676 /** 677 * Finds and updates the longest line in the view inside an interval of 678 * lines. 679 * 680 * @param start the start of the search interval 681 * @param end the end of the search interval 682 */ 683 private void findLongestLine(int start, int end) 684 { 685 for (int i = start; i <= end; i++) 686 { 687 int w = getLineLength(i); 688 if (w > maxLineLength) 689 { 690 maxLineLength = w; 691 longestLine = getElement().getElement(i); 692 } 693 } 694 } 695 696 /** 697 * Determines the length of the specified line. 698 * 699 * @param line the number of the line 700 * 701 * @return the length of the line in pixels 702 */ 703 private int getLineLength(int line) 704 { 705 Element lineEl = getElement().getElement(line); 706 Segment buffer = getLineBuffer(); 707 try 708 { 709 Document doc = getDocument(); 710 doc.getText(lineEl.getStartOffset(), 711 lineEl.getEndOffset() - lineEl.getStartOffset() - 1, 712 buffer); 713 } 714 catch (BadLocationException ex) 715 { 716 AssertionError err = new AssertionError("Unexpected bad location"); 717 err.initCause(ex); 718 throw err; 719 } 720 721 return Utilities.getTabbedTextWidth(buffer, metrics, tabBase, this, 722 lineEl.getStartOffset()); 723 } 724}