Source for javax.swing.text.PlainView

   1: /* PlainView.java -- 
   2:    Copyright (C) 2004, 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.Component;
  43: import java.awt.Font;
  44: import java.awt.FontMetrics;
  45: import java.awt.Graphics;
  46: import java.awt.Rectangle;
  47: import java.awt.Shape;
  48: 
  49: import javax.swing.SwingUtilities;
  50: import javax.swing.event.DocumentEvent;
  51: import javax.swing.event.DocumentEvent.ElementChange;
  52: 
  53: public class PlainView extends View implements TabExpander
  54: {
  55:   Color selectedColor;
  56:   Color unselectedColor;
  57: 
  58:   /**
  59:    * The color that is used to draw disabled text fields.
  60:    */
  61:   Color disabledColor;
  62:   
  63:   /**
  64:    * While painting this is the textcomponent's current start index
  65:    * of the selection.
  66:    */
  67:   int selectionStart;
  68: 
  69:   /**
  70:    * While painting this is the textcomponent's current end index
  71:    * of the selection.
  72:    */
  73:   int selectionEnd;
  74: 
  75:   Font font;
  76:   
  77:   /** The length of the longest line in the Document **/
  78:   float maxLineLength = -1;
  79:   
  80:   /** The longest line in the Document **/
  81:   Element longestLine = null;
  82:   
  83:   protected FontMetrics metrics;
  84: 
  85:   /**
  86:    * The instance returned by {@link #getLineBuffer()}.
  87:    */
  88:   private transient Segment lineBuffer;
  89: 
  90:   public PlainView(Element elem)
  91:   {
  92:     super(elem);
  93:   }
  94: 
  95:   /**
  96:    * @since 1.4
  97:    */
  98:   protected void updateMetrics()
  99:   {
 100:     Component component = getContainer();
 101:     Font font = component.getFont();
 102: 
 103:     if (this.font != font)
 104:       {
 105:     this.font = font;
 106:     metrics = component.getFontMetrics(font);
 107:       }
 108:   }
 109:   
 110:   /**
 111:    * @since 1.4
 112:    */
 113:   protected Rectangle lineToRect(Shape a, int line)
 114:   {
 115:     // Ensure metrics are up-to-date.
 116:     updateMetrics();
 117:     
 118:     Rectangle rect = a.getBounds();
 119:     int fontHeight = metrics.getHeight();
 120:     return new Rectangle(rect.x, rect.y + (line * fontHeight),
 121:              rect.width, fontHeight);
 122:   }
 123: 
 124:   public Shape modelToView(int position, Shape a, Position.Bias b)
 125:     throws BadLocationException
 126:   {
 127:     // Ensure metrics are up-to-date.
 128:     updateMetrics();
 129:     
 130:     Document document = getDocument();
 131: 
 132:     // Get rectangle of the line containing position.
 133:     int lineIndex = getElement().getElementIndex(position);
 134:     Rectangle rect = lineToRect(a, lineIndex);
 135: 
 136:     // Get the rectangle for position.
 137:     Element line = getElement().getElement(lineIndex);
 138:     int lineStart = line.getStartOffset();
 139:     Segment segment = getLineBuffer();
 140:     document.getText(lineStart, position - lineStart, segment);
 141:     int xoffset = Utilities.getTabbedTextWidth(segment, metrics, rect.x,
 142:                            this, lineStart);
 143: 
 144:     // Calc the real rectangle.
 145:     rect.x += xoffset;
 146:     rect.width = 1;
 147:     rect.height = metrics.getHeight();
 148: 
 149:     return rect;
 150:   }
 151:   
 152:   /**
 153:    * Draws a line of text. The X and Y coordinates specify the start of
 154:    * the <em>baseline</em> of the line.
 155:    *
 156:    * @param lineIndex the index of the line
 157:    * @param g the graphics to use for drawing the text
 158:    * @param x the X coordinate of the baseline
 159:    * @param y the Y coordinate of the baseline
 160:    */
 161:   protected void drawLine(int lineIndex, Graphics g, int x, int y)
 162:   {
 163:     try
 164:       {
 165:         Element line = getElement().getElement(lineIndex);
 166:         int startOffset = line.getStartOffset();
 167:         int endOffset = line.getEndOffset() - 1;
 168:         
 169:         if (selectionStart <= startOffset)
 170:           // Selection starts before the line ...
 171:           if (selectionEnd <= startOffset)
 172:             {
 173:               // end ends before the line: Draw completely unselected text.
 174:               drawUnselectedText(g, x, y, startOffset, endOffset);
 175:             }
 176:           else if (selectionEnd <= endOffset)
 177:             {
 178:               // and ends within the line: First part is selected,
 179:               // second is not.
 180:               x = drawSelectedText(g, x, y, startOffset, selectionEnd);
 181:               drawUnselectedText(g, x, y, selectionEnd, endOffset);
 182:             }
 183:           else
 184:             // and ends behind the line: Draw completely selected text.
 185:             drawSelectedText(g, x, y, startOffset, endOffset);
 186:         else if (selectionStart < endOffset)
 187:           // Selection starts within the line ..
 188:           if (selectionEnd < endOffset)
 189:             {
 190:               // and ends within it: First part unselected, second part
 191:               // selected, third part unselected.
 192:               x = drawUnselectedText(g, x, y, startOffset, selectionStart);
 193:               x = drawSelectedText(g, x, y, selectionStart, selectionEnd);
 194:               drawUnselectedText(g, x, y, selectionEnd, endOffset);
 195:             }
 196:           else
 197:             {
 198:               // and ends behind the line: First part unselected, second
 199:               // part selected.
 200:               x = drawUnselectedText(g, x, y, startOffset, selectionStart);
 201:               drawSelectedText(g, x, y, selectionStart, endOffset);
 202:             }
 203:         else
 204:           // Selection is behind this line: Draw completely unselected text.
 205:           drawUnselectedText(g, x, y, startOffset, endOffset);
 206:       }
 207:     catch (BadLocationException e)
 208:     {
 209:       AssertionError ae = new AssertionError("Unexpected bad location");
 210:       ae.initCause(e);
 211:       throw ae;
 212:     }
 213:   }
 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, segment.offset);
 222:   }
 223: 
 224:   /**
 225:    * Draws a chunk of unselected text.
 226:    *
 227:    * @param g the graphics to use for drawing the text
 228:    * @param x the X coordinate of the baseline
 229:    * @param y the Y coordinate of the baseline
 230:    * @param p0 the start position in the text model
 231:    * @param p1 the end position in the text model
 232:    *
 233:    * @return the X location of the end of the range
 234:    *
 235:    * @throws BadLocationException if <code>p0</code> or <code>p1</code> are
 236:    *         invalid
 237:    */
 238:   protected int drawUnselectedText(Graphics g, int x, int y, int p0, int p1)
 239:     throws BadLocationException
 240:   {
 241:     JTextComponent textComponent = (JTextComponent) getContainer();
 242:     if (textComponent.isEnabled())
 243:       g.setColor(unselectedColor);
 244:     else
 245:       g.setColor(disabledColor);
 246: 
 247:     Segment segment = getLineBuffer();
 248:     getDocument().getText(p0, p1 - p0, segment);
 249:     return Utilities.drawTabbedText(segment, x, y, g, this, segment.offset);
 250:   }
 251: 
 252:   public void paint(Graphics g, Shape s)
 253:   {
 254:     // Ensure metrics are up-to-date.
 255:     updateMetrics();
 256:     
 257:     JTextComponent textComponent = (JTextComponent) getContainer();
 258: 
 259:     selectedColor = textComponent.getSelectedTextColor();
 260:     unselectedColor = textComponent.getForeground();
 261:     disabledColor = textComponent.getDisabledTextColor();
 262:     selectionStart = textComponent.getSelectionStart();
 263:     selectionEnd = textComponent.getSelectionEnd();
 264: 
 265:     Rectangle rect = s.getBounds();
 266: 
 267:     // FIXME: Text may be scrolled.
 268:     Document document = textComponent.getDocument();
 269:     Element root = document.getDefaultRootElement();
 270:     int y = rect.y + metrics.getAscent();
 271:     int height = metrics.getHeight();
 272:     
 273:     int count = root.getElementCount();
 274:     for (int i = 0; i < count; i++)
 275:       {
 276:         drawLine(i, g, rect.x, y);
 277:         y += height;
 278:       }
 279:   }
 280: 
 281:   /**
 282:    * Returns the tab size of a tab.  Checks the Document's
 283:    * properties for PlainDocument.tabSizeAttribute and returns it if it is
 284:    * defined, otherwise returns 8.
 285:    * 
 286:    * @return the tab size.
 287:    */
 288:   protected int getTabSize()
 289:   {
 290:     Object tabSize = getDocument().getProperty(PlainDocument.tabSizeAttribute);
 291:     if (tabSize == null)
 292:       return 8;
 293:     return ((Integer)tabSize).intValue();
 294:   }
 295: 
 296:   /**
 297:    * Returns the next tab stop position after a given reference position.
 298:    *
 299:    * This implementation ignores the <code>tabStop</code> argument.
 300:    * 
 301:    * @param x the current x position in pixels
 302:    * @param tabStop the position within the text stream that the tab occured at
 303:    */
 304:   public float nextTabStop(float x, int tabStop)
 305:   {
 306:     float tabSizePixels = getTabSize() * metrics.charWidth('m');
 307:     return (float) (Math.floor(x / tabSizePixels) + 1) * tabSizePixels;
 308:   }
 309: 
 310:   /**
 311:    * Returns the length of the longest line, used for getting the span
 312:    * @return the length of the longest line
 313:    */
 314:   float determineMaxLineLength()
 315:   {
 316:     // if the longest line is cached, return the cached value
 317:     if (maxLineLength != -1)
 318:       return maxLineLength;
 319:     
 320:     // otherwise we have to go through all the lines and find it
 321:     Element el = getElement();
 322:     Segment seg = getLineBuffer();
 323:     float span = 0;
 324:     for (int i = 0; i < el.getElementCount(); i++)
 325:       {
 326:         Element child = el.getElement(i);
 327:         int start = child.getStartOffset();
 328:         int end = child.getEndOffset() - 1;
 329:         try
 330:           {
 331:             el.getDocument().getText(start, end - start, seg);
 332:           }
 333:         catch (BadLocationException ex)
 334:           {
 335:             AssertionError ae = new AssertionError("Unexpected bad location");
 336:         ae.initCause(ex);
 337:         throw ae;
 338:           }
 339:         
 340:         if (seg == null || seg.array == null || seg.count == 0)
 341:           continue;
 342:         
 343:         int width = metrics.charsWidth(seg.array, seg.offset, seg.count);
 344:         if (width > span)
 345:           {
 346:             longestLine = child;
 347:             span = width;
 348:           }
 349:       }
 350:     maxLineLength = span;
 351:     return maxLineLength;
 352:   }
 353:   
 354:   public float getPreferredSpan(int axis)
 355:   {
 356:     if (axis != X_AXIS && axis != Y_AXIS)
 357:       throw new IllegalArgumentException();
 358: 
 359:     // make sure we have the metrics
 360:     updateMetrics();
 361: 
 362:     Element el = getElement();
 363:     float span;
 364: 
 365:     switch (axis)
 366:       {
 367:       case X_AXIS:
 368:         span = determineMaxLineLength();
 369:         break;
 370:       case Y_AXIS:
 371:       default:
 372:         span = metrics.getHeight() * el.getElementCount();
 373:         break;
 374:       }
 375:     
 376:     return span;
 377:   }
 378: 
 379:   /**
 380:    * Maps coordinates from the <code>View</code>'s space into a position
 381:    * in the document model.
 382:    *
 383:    * @param x the x coordinate in the view space
 384:    * @param y the y coordinate in the view space
 385:    * @param a the allocation of this <code>View</code>
 386:    * @param b the bias to use
 387:    *
 388:    * @return the position in the document that corresponds to the screen
 389:    *         coordinates <code>x, y</code>
 390:    */
 391:   public int viewToModel(float x, float y, Shape a, Position.Bias[] b)
 392:   {
 393:     Rectangle rec = a.getBounds();
 394:     Document doc = getDocument();
 395:     Element root = doc.getDefaultRootElement();
 396:     
 397:     // PlainView doesn't support line-wrapping so we can find out which
 398:     // Element was clicked on just by the y-position.    
 399:     // Since the coordinates may be outside of the coordinate space
 400:     // of the allocation area (e.g. user dragged mouse outside
 401:     // the component) we have to limit the values.
 402:     // This has the nice effect that the user can drag the
 403:     // mouse above or below the component and it will still
 404:     // react to the x values (e.g. when selecting).
 405:     int lineClicked
 406:       = Math.min(Math.max((int) (y - rec.y) / metrics.getHeight(), 0),
 407:                           root.getElementCount() - 1);
 408:     
 409:     Element line = root.getElement(lineClicked);
 410:     
 411:     Segment s = getLineBuffer();
 412:     int start = line.getStartOffset();
 413:     // We don't want the \n at the end of the line.
 414:     int end = line.getEndOffset() - 1;
 415:     try
 416:       {
 417:         doc.getText(start, end - start, s);
 418:       }
 419:     catch (BadLocationException ble)
 420:       {
 421:         AssertionError ae = new AssertionError("Unexpected bad location");
 422:         ae.initCause(ble);
 423:         throw ae;
 424:       }
 425:     
 426:     int pos = Utilities.getTabbedTextOffset(s, metrics, rec.x, (int)x, this, start);
 427:     return Math.max (0, pos);
 428:   }     
 429:   
 430:   /**
 431:    * Since insertUpdate and removeUpdate each deal with children
 432:    * Elements being both added and removed, they both have to perform
 433:    * the same checks.  So they both simply call this method.
 434:    * @param changes the DocumentEvent for the changes to the Document.
 435:    * @param a the allocation of the View.
 436:    * @param f the ViewFactory to use for rebuilding.
 437:    */
 438:   protected void updateDamage(DocumentEvent changes, Shape a, ViewFactory f)
 439:   {
 440:     // This happens during initialization.
 441:     if (metrics == null)
 442:       {
 443:         updateMetrics();
 444:         preferenceChanged(null, true, true);
 445:         return;
 446:       }
 447: 
 448:     Element element = getElement();
 449: 
 450:     // Find longest line if it hasn't been initialized yet.
 451:     if (longestLine == null)
 452:       findLongestLine(0, element.getElementCount() - 1);
 453: 
 454:     ElementChange change = changes.getChange(element);
 455:     if (changes.getType() == DocumentEvent.EventType.INSERT)
 456:       {
 457:         // Handles character/line insertion.
 458: 
 459:         // Determine if lines have been added. In this case we repaint
 460:         // differently.
 461:         boolean linesAdded = true;
 462:         if (change == null)
 463:           linesAdded = false;
 464: 
 465:         // Determine the start line.
 466:         int start;
 467:         if (linesAdded)
 468:           start = change.getIndex();
 469:         else
 470:           start = element.getElementIndex(changes.getOffset());
 471: 
 472:         // Determine the length of the updated region.
 473:         int length = 0;
 474:         if (linesAdded)
 475:           length = change.getChildrenAdded().length - 1;
 476: 
 477:         // Update the longest line and length.
 478:         int oldMaxLength = (int) maxLineLength;
 479:         if (longestLine.getEndOffset() < changes.getOffset()
 480:             || longestLine.getStartOffset() > changes.getOffset()
 481:                                              + changes.getLength())
 482:           {
 483:             findLongestLine(start, start + length);
 484:           }
 485:         else
 486:           {
 487:             findLongestLine(0, element.getElementCount() - 1);
 488:           }
 489: 
 490:         // Trigger a preference change so that the layout gets updated
 491:         // correctly.
 492:         preferenceChanged(null, maxLineLength != oldMaxLength, linesAdded);
 493: 
 494:         // Damage the updated line range.
 495:         int endLine = start;
 496:         if (linesAdded)
 497:           endLine = element.getElementCount() - 1;
 498:         damageLineRange(start, endLine, a, getContainer());
 499: 
 500:       }
 501:     else
 502:       {
 503:         // Handles character/lines removals.
 504: 
 505:         // Update the longest line and length and trigger preference changed.
 506:         int oldMaxLength = (int) maxLineLength;
 507:         if (change != null)
 508:           {
 509:             // Line(s) have been removed.
 510:             findLongestLine(0, element.getElementCount() - 1);
 511:             preferenceChanged(null, maxLineLength != oldMaxLength, true);
 512:           }
 513:         else
 514:           {
 515:             // No line has been removed.
 516:             int lineNo = getElement().getElementIndex(changes.getOffset());
 517:             Element line = getElement().getElement(lineNo);
 518:             if (longestLine == line)
 519:               {
 520:                 findLongestLine(0, element.getElementCount() - 1);
 521:                 preferenceChanged(null, maxLineLength != oldMaxLength, false);
 522:             }
 523:             damageLineRange(lineNo, lineNo, a, getContainer());
 524:         }
 525:       }
 526:   }
 527: 
 528:   /**
 529:    * This method is called when something is inserted into the Document
 530:    * that this View is displaying.
 531:    * 
 532:    * @param changes the DocumentEvent for the changes.
 533:    * @param a the allocation of the View
 534:    * @param f the ViewFactory used to rebuild
 535:    */
 536:   public void insertUpdate(DocumentEvent changes, Shape a, ViewFactory f)
 537:   {
 538:     updateDamage(changes, a, f);
 539:   }
 540: 
 541:   /**
 542:    * This method is called when something is removed from the Document
 543:    * that this View is displaying.
 544:    * 
 545:    * @param changes the DocumentEvent for the changes.
 546:    * @param a the allocation of the View
 547:    * @param f the ViewFactory used to rebuild
 548:    */
 549:   public void removeUpdate(DocumentEvent changes, Shape a, ViewFactory f)
 550:   {
 551:     updateDamage(changes, a, f);
 552:   }
 553:   
 554:   /**
 555:    * This method is called when attributes were changed in the 
 556:    * Document in a location that this view is responsible for.
 557:    */
 558:   public void changedUpdate (DocumentEvent changes, Shape a, ViewFactory f)
 559:   {
 560:     updateDamage(changes, a, f);
 561:   }
 562:   
 563:   /**
 564:    * Repaint the given line range.  This is called from insertUpdate,
 565:    * changedUpdate, and removeUpdate when no new lines were added 
 566:    * and no lines were removed, to repaint the line that was 
 567:    * modified.
 568:    * 
 569:    * @param line0 the start of the range
 570:    * @param line1 the end of the range
 571:    * @param a the rendering region of the host
 572:    * @param host the Component that uses this View (used to call repaint
 573:    * on that Component)
 574:    * 
 575:    * @since 1.4
 576:    */
 577:   protected void damageLineRange (int line0, int line1, Shape a, Component host)
 578:   {
 579:     if (a == null)
 580:       return;
 581: 
 582:     Rectangle rec0 = lineToRect(a, line0);
 583:     Rectangle rec1 = lineToRect(a, line1);
 584: 
 585:     if (rec0 == null || rec1 == null)
 586:       // something went wrong, repaint the entire host to be safe
 587:       host.repaint();
 588:     else
 589:       {
 590:         Rectangle repaintRec = SwingUtilities.computeUnion(rec0.x, rec0.y,
 591:                                                            rec0.width,
 592:                                                            rec0.height, rec1);
 593:         host.repaint(repaintRec.x, repaintRec.y, repaintRec.width,
 594:                      repaintRec.height);
 595:       }    
 596:   }
 597: 
 598:   /**
 599:    * Provides a {@link Segment} object, that can be used to fetch text from
 600:    * the document.
 601:    *
 602:    * @returna {@link Segment} object, that can be used to fetch text from
 603:    *          the document
 604:    */
 605:   protected final Segment getLineBuffer()
 606:   {
 607:     if (lineBuffer == null)
 608:       lineBuffer = new Segment();
 609:     return lineBuffer;
 610:   }
 611: 
 612:   /**
 613:    * Finds and updates the longest line in the view inside an interval of
 614:    * lines.
 615:    *
 616:    * @param start the start of the search interval
 617:    * @param end the end of the search interval
 618:    */
 619:   private void findLongestLine(int start, int end)
 620:   {
 621:     for (int i = start; i <= end; i++)
 622:       {
 623:         int w = getLineLength(i);
 624:         if (w > maxLineLength)
 625:           {
 626:             maxLineLength = w;
 627:             longestLine = getElement().getElement(i);
 628:           }
 629:       }
 630:   }
 631: 
 632:   /**
 633:    * Determines the length of the specified line.
 634:    *
 635:    * @param line the number of the line
 636:    *
 637:    * @return the length of the line in pixels
 638:    */
 639:   private int getLineLength(int line)
 640:   {
 641:     Element lineEl = getElement().getElement(line);
 642:     Segment buffer = getLineBuffer();
 643:     try
 644:       {
 645:         Document doc = getDocument();
 646:         doc.getText(lineEl.getStartOffset(),
 647:                     lineEl.getEndOffset() - lineEl.getStartOffset() - 1,
 648:                     buffer);
 649:       }
 650:     catch (BadLocationException ex)
 651:       {
 652:         AssertionError err = new AssertionError("Unexpected bad location");
 653:         err.initCause(ex);
 654:         throw err;
 655:       }
 656: 
 657:     return Utilities.getTabbedTextWidth(buffer, metrics, 0, this,
 658:                                         lineEl.getStartOffset());
 659:   }
 660: }