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:     // Return early and do no updates if the allocation area is null
 441:     // (like the RI).
 442:     if (a == null)
 443:       return;
 444:     
 445:     float oldMaxLineLength = maxLineLength; 
 446:     Rectangle alloc = a.getBounds();
 447:     Element el = getElement();
 448:     ElementChange ec = changes.getChange(el);
 449:     
 450:     // If ec is null then no lines were added or removed, just 
 451:     // repaint the changed line
 452:     if (ec == null)
 453:       {
 454:         int line = el.getElementIndex(changes.getOffset());
 455:         
 456:         // If characters have been removed from the current longest line
 457:         // we have to find out which one is the longest now otherwise
 458:         // the preferred x-axis span will not shrink.
 459:         if (changes.getType() == DocumentEvent.EventType.REMOVE
 460:             && el.getElement(line) == longestLine)
 461:           {
 462:             maxLineLength = -1;
 463:             if (determineMaxLineLength() != alloc.width)
 464:               preferenceChanged(this, true, false);
 465:           }
 466:         
 467:         damageLineRange(line, line, a, getContainer());
 468:         return;
 469:       }
 470:     
 471:     Element[] removed = ec.getChildrenRemoved();
 472:     Element[] newElements = ec.getChildrenAdded();
 473:     
 474:     // If no Elements were added or removed, we just want to repaint
 475:     // the area containing the line that was modified
 476:     if (removed == null && newElements == null)
 477:       {
 478:         int line = getElement().getElementIndex(changes.getOffset());
 479:         
 480:         damageLineRange(line, line, a, getContainer());
 481:         return;
 482:       }
 483: 
 484:     // Check to see if we removed the longest line, if so we have to
 485:     // search through all lines and find the longest one again.
 486:     if (removed != null)
 487:       {
 488:         for (int i = 0; i < removed.length; i++)
 489:           if (removed[i].equals(longestLine))
 490:             {
 491:               // reset maxLineLength and search through all lines for longest one
 492:               maxLineLength = -1;
 493:               if (determineMaxLineLength() != alloc.width)
 494:                 preferenceChanged(this, true, removed.length != newElements.length);
 495:               
 496:               ((JTextComponent)getContainer()).repaint();
 497:               
 498:               return;
 499:             }
 500:       }
 501:     
 502:     // If we've reached here, that means we haven't removed the longest line
 503:     if (newElements == null)
 504:       {
 505:         // No lines were added, just repaint the container and exit
 506:         ((JTextComponent)getContainer()).repaint();
 507:         
 508:         return;
 509:       }
 510: 
 511:     //  Make sure we have the metrics
 512:     updateMetrics();
 513:        
 514:     // If we've reached here, that means we haven't removed the longest line
 515:     // and we have added at least one line, so we have to check if added lines
 516:     // are longer than the previous longest line        
 517:     Segment seg = getLineBuffer();
 518:     float longestNewLength = 0;
 519:     Element longestNewLine = null;    
 520: 
 521:     // Loop through the added lines to check their length
 522:     for (int i = 0; i < newElements.length; i++)
 523:       {
 524:         Element child = newElements[i];
 525:         int start = child.getStartOffset();
 526:         int end = child.getEndOffset() - 1;
 527:         try
 528:           {
 529:             el.getDocument().getText(start, end - start, seg);
 530:           }
 531:         catch (BadLocationException ex)
 532:           {
 533:             AssertionError ae = new AssertionError("Unexpected bad location");
 534:         ae.initCause(ex);
 535:         throw ae;
 536:           }
 537:                 
 538:         if (seg == null || seg.array == null || seg.count == 0)
 539:           continue;
 540:         
 541:         int width = metrics.charsWidth(seg.array, seg.offset, seg.count);
 542:         if (width > longestNewLength)
 543:           {
 544:             longestNewLine = child;
 545:             longestNewLength = width;
 546:           }
 547:       }
 548:     
 549:     // Check if the longest of the new lines is longer than our previous
 550:     // longest line, and if so update our values
 551:     if (longestNewLength > maxLineLength)
 552:       {
 553:         maxLineLength = longestNewLength;
 554:         longestLine = longestNewLine;
 555:       }
 556:     
 557:     // Report any changes to the preferred sizes of the view
 558:     // which may cause the underlying component to be revalidated.
 559:     boolean widthChanged = oldMaxLineLength != maxLineLength;
 560:     boolean heightChanged = removed.length != newElements.length; 
 561:     if (widthChanged || heightChanged)
 562:       preferenceChanged(this, widthChanged, heightChanged);
 563:     
 564:     // Repaint the container
 565:     ((JTextComponent)getContainer()).repaint();
 566:   }
 567: 
 568:   /**
 569:    * This method is called when something is inserted into the Document
 570:    * that this View is displaying.
 571:    * 
 572:    * @param changes the DocumentEvent for the changes.
 573:    * @param a the allocation of the View
 574:    * @param f the ViewFactory used to rebuild
 575:    */
 576:   public void insertUpdate(DocumentEvent changes, Shape a, ViewFactory f)
 577:   {
 578:     updateDamage(changes, a, f);
 579:   }
 580: 
 581:   /**
 582:    * This method is called when something is removed from the Document
 583:    * that this View is displaying.
 584:    * 
 585:    * @param changes the DocumentEvent for the changes.
 586:    * @param a the allocation of the View
 587:    * @param f the ViewFactory used to rebuild
 588:    */
 589:   public void removeUpdate(DocumentEvent changes, Shape a, ViewFactory f)
 590:   {
 591:     updateDamage(changes, a, f);
 592:   }
 593:   
 594:   /**
 595:    * This method is called when attributes were changed in the 
 596:    * Document in a location that this view is responsible for.
 597:    */
 598:   public void changedUpdate (DocumentEvent changes, Shape a, ViewFactory f)
 599:   {
 600:     updateDamage(changes, a, f);
 601:   }
 602:   
 603:   /**
 604:    * Repaint the given line range.  This is called from insertUpdate,
 605:    * changedUpdate, and removeUpdate when no new lines were added 
 606:    * and no lines were removed, to repaint the line that was 
 607:    * modified.
 608:    * 
 609:    * @param line0 the start of the range
 610:    * @param line1 the end of the range
 611:    * @param a the rendering region of the host
 612:    * @param host the Component that uses this View (used to call repaint
 613:    * on that Component)
 614:    * 
 615:    * @since 1.4
 616:    */
 617:   protected void damageLineRange (int line0, int line1, Shape a, Component host)
 618:   {
 619:     if (a == null)
 620:       return;
 621: 
 622:     Rectangle rec0 = lineToRect(a, line0);
 623:     Rectangle rec1 = lineToRect(a, line1);
 624: 
 625:     if (rec0 == null || rec1 == null)
 626:       // something went wrong, repaint the entire host to be safe
 627:       host.repaint();
 628:     else
 629:       {
 630:         Rectangle repaintRec = SwingUtilities.computeUnion(rec0.x, rec0.y,
 631:                                                            rec0.width,
 632:                                                            rec0.height, rec1);
 633:         host.repaint(repaintRec.x, repaintRec.y, repaintRec.width,
 634:                      repaintRec.height);
 635:       }    
 636:   }
 637: 
 638:   /**
 639:    * Provides a {@link Segment} object, that can be used to fetch text from
 640:    * the document.
 641:    *
 642:    * @returna {@link Segment} object, that can be used to fetch text from
 643:    *          the document
 644:    */
 645:   protected final Segment getLineBuffer()
 646:   {
 647:     if (lineBuffer == null)
 648:       lineBuffer = new Segment();
 649:     return lineBuffer;
 650:   }
 651: }