Source for javax.swing.text.WrappedPlainView

   1: /* WrappedPlainView.java -- 
   2:    Copyright (C) 2005 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.SwingConstants;
  49: import javax.swing.event.DocumentEvent;
  50: import javax.swing.text.Position.Bias;
  51: 
  52: /**
  53:  * @author Anthony Balkissoon abalkiss at redhat dot com
  54:  *
  55:  */
  56: public class WrappedPlainView extends BoxView implements TabExpander
  57: {
  58:   /** The color for selected text **/
  59:   Color selectedColor;
  60:   
  61:   /** The color for unselected text **/
  62:   Color unselectedColor;
  63:   
  64:   /** The color for disabled components **/
  65:   Color disabledColor;
  66:   
  67:   /** Stores the font metrics **/
  68:   protected FontMetrics metrics;
  69:   
  70:   /** Whether or not to wrap on word boundaries **/
  71:   boolean wordWrap;
  72:   
  73:   /** A ViewFactory that creates WrappedLines **/
  74:   ViewFactory viewFactory = new WrappedLineCreator();
  75:   
  76:   /** The start of the selected text **/
  77:   int selectionStart;
  78:   
  79:   /** The end of the selected text **/
  80:   int selectionEnd;
  81:   
  82:   /**
  83:    * The instance returned by {@link #getLineBuffer()}.
  84:    */
  85:   private transient Segment lineBuffer;
  86:   
  87:   public WrappedPlainView (Element elem)
  88:   {
  89:     this (elem, false);
  90:   }
  91:   
  92:   public WrappedPlainView (Element elem, boolean wordWrap)
  93:   {
  94:     super (elem, Y_AXIS);
  95:     this.wordWrap = wordWrap;    
  96:   }  
  97:   
  98:   /**
  99:    * Provides access to the Segment used for retrievals from the Document.
 100:    * @return the Segment.
 101:    */
 102:   protected final Segment getLineBuffer()
 103:   {
 104:     if (lineBuffer == null)
 105:       lineBuffer = new Segment();
 106:     return lineBuffer;
 107:   }
 108:   
 109:   /**
 110:    * Returns the next tab stop position after a given reference position.
 111:    *
 112:    * This implementation ignores the <code>tabStop</code> argument.
 113:    * 
 114:    * @param x the current x position in pixels
 115:    * @param tabStop the position within the text stream that the tab occured at
 116:    */
 117:   public float nextTabStop(float x, int tabStop)
 118:   {
 119:     JTextComponent host = (JTextComponent)getContainer();
 120:     float tabSizePixels = getTabSize()
 121:                           * host.getFontMetrics(host.getFont()).charWidth('m');
 122:     return (float) (Math.floor(x / tabSizePixels) + 1) * tabSizePixels;
 123:   }
 124:   
 125:   /**
 126:    * Returns the tab size for the Document based on 
 127:    * PlainDocument.tabSizeAttribute, defaulting to 8 if this property is
 128:    * not defined
 129:    * 
 130:    * @return the tab size.
 131:    */
 132:   protected int getTabSize()
 133:   {
 134:     Object tabSize = getDocument().getProperty(PlainDocument.tabSizeAttribute);
 135:     if (tabSize == null)
 136:       return 8;
 137:     return ((Integer)tabSize).intValue();
 138:   }
 139:   
 140:   /**
 141:    * Draws a line of text, suppressing white space at the end and expanding
 142:    * tabs.  Calls drawSelectedText and drawUnselectedText.
 143:    * @param p0 starting document position to use
 144:    * @param p1 ending document position to use
 145:    * @param g graphics context
 146:    * @param x starting x position
 147:    * @param y starting y position
 148:    */
 149:   protected void drawLine(int p0, int p1, Graphics g, int x, int y)
 150:   {
 151:     try
 152:     {
 153:       // We have to draw both selected and unselected text.  There are
 154:       // several cases:
 155:       //  - entire range is unselected
 156:       //  - entire range is selected
 157:       //  - start of range is selected, end of range is unselected
 158:       //  - start of range is unselected, end of range is selected
 159:       //  - middle of range is selected, start and end of range is unselected
 160:       
 161:       // entire range unselected:      
 162:       if ((selectionStart == selectionEnd) || 
 163:           (p0 > selectionEnd || p1 < selectionStart))
 164:         drawUnselectedText(g, x, y, p0, p1);
 165:       
 166:       // entire range selected
 167:       else if (p0 >= selectionStart && p1 <= selectionEnd)
 168:         drawSelectedText(g, x, y, p0, p1);
 169:       
 170:       // start of range selected, end of range unselected
 171:       else if (p0 >= selectionStart)
 172:         {
 173:           x = drawSelectedText(g, x, y, p0, selectionEnd);
 174:           drawUnselectedText(g, x, y, selectionEnd, p1);
 175:         }
 176:       
 177:       // start of range unselected, end of range selected
 178:       else if (selectionStart > p0 && selectionEnd > p1)
 179:         {
 180:           x = drawUnselectedText(g, x, y, p0, selectionStart);
 181:           drawSelectedText(g, x, y, selectionStart, p1);
 182:         }
 183:       
 184:       // middle of range selected
 185:       else if (selectionStart > p0)
 186:         {
 187:           x = drawUnselectedText(g, x, y, p0, selectionStart);
 188:           x = drawSelectedText(g, x, y, selectionStart, selectionEnd);
 189:           drawUnselectedText(g, x, y, selectionEnd, p1);
 190:         }        
 191:     }
 192:     catch (BadLocationException ble)
 193:     {
 194:       // shouldn't happen
 195:     }
 196:   }
 197: 
 198:   /**
 199:    * Renders the range of text as selected text.  Just paints the text 
 200:    * in the color specified by the host component.  Assumes the highlighter
 201:    * will render the selected background.
 202:    * @param g the graphics context
 203:    * @param x the starting X coordinate
 204:    * @param y the starting Y coordinate
 205:    * @param p0 the starting model location
 206:    * @param p1 the ending model location 
 207:    * @return the X coordinate of the end of the text
 208:    * @throws BadLocationException if the given range is invalid
 209:    */
 210:   protected int drawSelectedText(Graphics g, int x, int y, int p0, int p1)
 211:       throws BadLocationException
 212:   {
 213:     g.setColor(selectedColor);
 214:     Segment segment = getLineBuffer();
 215:     getDocument().getText(p0, p1 - p0, segment);
 216:     return Utilities.drawTabbedText(segment, x, y, g, this, p0);
 217:   }
 218: 
 219:   /**
 220:    * Renders the range of text as normal unhighlighted text.
 221:    * @param g the graphics context
 222:    * @param x the starting X coordinate
 223:    * @param y the starting Y coordinate
 224:    * @param p0 the starting model location
 225:    * @param p1 the end model location
 226:    * @return the X location of the end off the range
 227:    * @throws BadLocationException if the range given is invalid
 228:    */
 229:   protected int drawUnselectedText(Graphics g, int x, int y, int p0, int p1)
 230:       throws BadLocationException
 231:   {    
 232:     JTextComponent textComponent = (JTextComponent) getContainer();
 233:     if (textComponent.isEnabled())
 234:       g.setColor(unselectedColor);
 235:     else
 236:       g.setColor(disabledColor);
 237: 
 238:     Segment segment = getLineBuffer();
 239:     getDocument().getText(p0, p1 - p0, segment);
 240:     return Utilities.drawTabbedText(segment, x, y, g, this, p0);
 241:   }  
 242:   
 243:   /**
 244:    * Loads the children to initiate the view.  Called by setParent.
 245:    * Creates a WrappedLine for each child Element.
 246:    */
 247:   protected void loadChildren (ViewFactory f)
 248:   {
 249:     Element root = getElement();
 250:     int numChildren = root.getElementCount();
 251:     if (numChildren == 0)
 252:       return;
 253:     
 254:     View[] children = new View[numChildren];
 255:     for (int i = 0; i < numChildren; i++)
 256:       children[i] = new WrappedLine(root.getElement(i));
 257:     replace(0, 0, children);
 258:   }
 259:   
 260:   /**
 261:    * Calculates the break position for the text between model positions
 262:    * p0 and p1.  Will break on word boundaries or character boundaries
 263:    * depending on the break argument given in construction of this 
 264:    * WrappedPlainView.  Used by the nested WrappedLine class to determine
 265:    * when to start the next logical line.
 266:    * @param p0 the start model position
 267:    * @param p1 the end model position
 268:    * @return the model position at which to break the text
 269:    */
 270:   protected int calculateBreakPosition(int p0, int p1)
 271:   {
 272:     Container c = getContainer();
 273:     Rectangle alloc = c.isValid() ? c.getBounds()
 274:                                  : new Rectangle(c.getPreferredSize());
 275:     updateMetrics();
 276:     try
 277:       {
 278:         getDocument().getText(p0, p1 - p0, getLineBuffer());
 279:       }
 280:     catch (BadLocationException ble)
 281:       {
 282:         // this shouldn't happen
 283:       }
 284:     // FIXME: Should we account for the insets of the container?
 285:     if (wordWrap)
 286:       return p0
 287:              + Utilities.getBreakLocation(lineBuffer, metrics, alloc.x,
 288:                                           alloc.x + alloc.width, this, 0);
 289:     else
 290:       {
 291:       return p0
 292:              + Utilities.getTabbedTextOffset(lineBuffer, metrics, alloc.x,
 293:                                              alloc.x + alloc.width, this, 0);
 294:       }
 295:   }
 296:   
 297:   void updateMetrics()
 298:   {
 299:     Container component = getContainer();
 300:     metrics = component.getFontMetrics(component.getFont());
 301:   }
 302:   
 303:   /**
 304:    * Determines the preferred span along the given axis.  Implemented to 
 305:    * cache the font metrics and then call the super classes method.
 306:    */
 307:   public float getPreferredSpan (int axis)
 308:   {
 309:     updateMetrics();
 310:     return super.getPreferredSpan(axis);
 311:   }
 312:   
 313:   /**
 314:    * Determines the minimum span along the given axis.  Implemented to 
 315:    * cache the font metrics and then call the super classes method.
 316:    */
 317:   public float getMinimumSpan (int axis)
 318:   {
 319:     updateMetrics();
 320:     return super.getMinimumSpan(axis);
 321:   }
 322:   
 323:   /**
 324:    * Determines the maximum span along the given axis.  Implemented to 
 325:    * cache the font metrics and then call the super classes method.
 326:    */
 327:   public float getMaximumSpan (int axis)
 328:   {
 329:     updateMetrics();
 330:     return super.getMaximumSpan(axis);
 331:   }
 332:   
 333:   /**
 334:    * Called when something was inserted.  Overridden so that
 335:    * the view factory creates WrappedLine views.
 336:    */
 337:   public void insertUpdate (DocumentEvent e, Shape a, ViewFactory f)
 338:   {
 339:     super.insertUpdate(e, a, viewFactory);
 340:     // FIXME: could improve performance by repainting only the necessary area
 341:     getContainer().repaint();
 342:   }
 343:   
 344:   /**
 345:    * Called when something is removed.  Overridden so that
 346:    * the view factory creates WrappedLine views.
 347:    */
 348:   public void removeUpdate (DocumentEvent e, Shape a, ViewFactory f)
 349:   {
 350:     super.removeUpdate(e, a, viewFactory);
 351:     // FIXME: could improve performance by repainting only the necessary area
 352:     getContainer().repaint();
 353:   }
 354:   
 355:   /**
 356:    * Called when the portion of the Document that this View is responsible
 357:    * for changes.  Overridden so that the view factory creates
 358:    * WrappedLine views.
 359:    */
 360:   public void changedUpdate (DocumentEvent e, Shape a, ViewFactory f)
 361:   {
 362:     super.changedUpdate(e, a, viewFactory);
 363:     // FIXME: could improve performance by repainting only the necessary area
 364:     getContainer().repaint();
 365:   }
 366:     
 367:   class WrappedLineCreator implements ViewFactory
 368:   {
 369:     // Creates a new WrappedLine
 370:     public View create(Element elem)
 371:     {
 372:       return new WrappedLine(elem);
 373:     }    
 374:   }
 375:   
 376:   /**
 377:    * Renders the <code>Element</code> that is associated with this
 378:    * <code>View</code>.  Caches the metrics and then calls
 379:    * super.paint to paint all the child views.
 380:    *
 381:    * @param g the <code>Graphics</code> context to render to
 382:    * @param a the allocated region for the <code>Element</code>
 383:    */
 384:   public void paint(Graphics g, Shape a)
 385:   {
 386:     JTextComponent comp = (JTextComponent)getContainer();
 387:     selectionStart = comp.getSelectionStart();
 388:     selectionEnd = comp.getSelectionEnd();
 389:     updateMetrics();
 390:     super.paint(g, a);
 391:   }
 392:   
 393:   /**
 394:    * Sets the size of the View.  Implemented to update the metrics
 395:    * and then call super method.
 396:    */
 397:   public void setSize (float width, float height)
 398:   {
 399:     updateMetrics();
 400:     if (width != getWidth())
 401:       preferenceChanged(null, true, true);
 402:     super.setSize(width, height);
 403:   }
 404:   
 405:   class WrappedLine extends View
 406:   { 
 407:     /** Used to cache the number of lines for this View **/
 408:     int numLines;
 409:     
 410:     public WrappedLine(Element elem)
 411:     {
 412:       super(elem);
 413:       determineNumLines();
 414:     }
 415: 
 416:     /**
 417:      * Renders this (possibly wrapped) line using the given Graphics object
 418:      * and on the given rendering surface.
 419:      */
 420:     public void paint(Graphics g, Shape s)
 421:     {
 422:       // Ensure metrics are up-to-date.
 423:       updateMetrics();
 424:       JTextComponent textComponent = (JTextComponent) getContainer();
 425: 
 426:       g.setFont(textComponent.getFont());
 427:       selectedColor = textComponent.getSelectedTextColor();
 428:       unselectedColor = textComponent.getForeground();
 429:       disabledColor = textComponent.getDisabledTextColor();
 430: 
 431:       // FIXME: this is a hack, for some reason textComponent.getSelectedColor
 432:       // was returning black, which is not visible against a black background
 433:       selectedColor = Color.WHITE;
 434:       
 435:       Rectangle rect = s.getBounds();
 436:       int lineHeight = metrics.getHeight();
 437: 
 438:       int end = getEndOffset();
 439:       int currStart = getStartOffset();
 440:       int currEnd;      
 441:       while (currStart < end)
 442:         {
 443:           currEnd = calculateBreakPosition(currStart, end);
 444:           drawLine(currStart, currEnd, g, rect.x, rect.y);
 445:           rect.y += lineHeight;          
 446:           if (currEnd == currStart)
 447:             currStart ++;
 448:           else
 449:             currStart = currEnd;          
 450:         }
 451:     }
 452:     
 453:     /**
 454:      * Determines the number of logical lines that the Element
 455:      * needs to be displayed
 456:      * @return the number of lines needed to display the Element
 457:      */
 458:     int determineNumLines()
 459:     {      
 460:       numLines = 0;
 461:       int end = getEndOffset();
 462:       if (end == 0)
 463:         return 0;
 464:             
 465:       int breakPoint;
 466:       for (int i = getStartOffset(); i < end;)
 467:         {
 468:           numLines ++;
 469:           // careful: check that there's no off-by-one problem here
 470:           // depending on which position calculateBreakPosition returns
 471:           breakPoint = calculateBreakPosition(i, end);
 472:           if (breakPoint == i)
 473:             i ++;
 474:           else
 475:             i = breakPoint;
 476:         }
 477:       return numLines;
 478:     }
 479:     
 480:     /**
 481:      * Determines the preferred span for this view along the given axis.
 482:      * 
 483:      * @param axis the axis (either X_AXIS or Y_AXIS)
 484:      * 
 485:      * @return the preferred span along the given axis.
 486:      * @throws IllegalArgumentException if axis is not X_AXIS or Y_AXIS
 487:      */
 488:     public float getPreferredSpan(int axis)
 489:     {
 490:       if (axis == X_AXIS)
 491:         return getWidth();
 492:       else if (axis == Y_AXIS)
 493:         return numLines * metrics.getHeight(); 
 494:       
 495:       throw new IllegalArgumentException("Invalid axis for getPreferredSpan: "
 496:                                          + axis);
 497:     }
 498:     
 499:     /**
 500:      * Provides a mapping from model space to view space.
 501:      * 
 502:      * @param pos the position in the model
 503:      * @param a the region into which the view is rendered
 504:      * @param b the position bias (forward or backward)
 505:      * 
 506:      * @return a box in view space that represents the given position 
 507:      * in model space
 508:      * @throws BadLocationException if the given model position is invalid
 509:      */
 510:     public Shape modelToView(int pos, Shape a, Bias b)
 511:         throws BadLocationException
 512:     {
 513:       Segment s = getLineBuffer();
 514:       int lineHeight = metrics.getHeight();
 515:       Rectangle rect = a.getBounds();
 516:       
 517:       // Return a rectangle with width 1 and height equal to the height 
 518:       // of the text
 519:       rect.height = lineHeight;
 520:       rect.width = 1;
 521: 
 522:       int currLineStart = getStartOffset();
 523:       int end = getEndOffset();
 524:       
 525:       if (pos < currLineStart || pos >= end)
 526:         throw new BadLocationException("invalid offset", pos);
 527:            
 528:       while (true)
 529:         {
 530:           int currLineEnd = calculateBreakPosition(currLineStart, end);
 531:           // If pos is between currLineStart and currLineEnd then just find
 532:           // the width of the text from currLineStart to pos and add that
 533:           // to rect.x
 534:           if (pos >= currLineStart && pos < currLineEnd || pos == end - 1)
 535:             {             
 536:               try
 537:                 {
 538:                   getDocument().getText(currLineStart, pos - currLineStart, s);
 539:                 }
 540:               catch (BadLocationException ble)
 541:                 {
 542:                   // Shouldn't happen
 543:                 }
 544:               rect.x += Utilities.getTabbedTextWidth(s, metrics, rect.x,
 545:                                                      WrappedPlainView.this,
 546:                                                      currLineStart);
 547:               return rect;
 548:             }
 549:           // Increment rect.y so we're checking the next logical line
 550:           rect.y += lineHeight;
 551:           
 552:           // Increment currLineStart to the model position of the start
 553:           // of the next logical line
 554:           if (currLineEnd == currLineStart)
 555:             currLineStart = end;
 556:           else
 557:             currLineStart = currLineEnd;
 558:         }
 559: 
 560:     }
 561: 
 562:     /**
 563:      * Provides a mapping from view space to model space.
 564:      * 
 565:      * @param x the x coordinate in view space
 566:      * @param y the y coordinate in view space
 567:      * @param a the region into which the view is rendered
 568:      * @param b the position bias (forward or backward)
 569:      * 
 570:      * @return the location in the model that best represents the
 571:      * given point in view space
 572:      */
 573:     public int viewToModel(float x, float y, Shape a, Bias[] b)
 574:     {
 575:       Segment s = getLineBuffer();
 576:       Rectangle rect = a.getBounds();
 577:       int currLineStart = getStartOffset();
 578:       int end = getEndOffset();
 579:       int lineHeight = metrics.getHeight();
 580:       if (y < rect.y)
 581:         return currLineStart;
 582:       if (y > rect.y + rect.height)
 583:         return end - 1;
 584: 
 585:       while (true)
 586:         {
 587:           int currLineEnd = calculateBreakPosition(currLineStart, end);
 588:           // If we're at the right y-position that means we're on the right
 589:           // logical line and we should look for the character
 590:           if (y >= rect.y && y < rect.y + lineHeight)
 591:             {
 592:               // Check if the x position is to the left or right of the text
 593:               if (x < rect.x)
 594:                 return currLineStart;
 595:               if (x > rect.x + rect.width)
 596:                 return currLineEnd - 1;
 597:               
 598:               try
 599:                 {
 600:                   getDocument().getText(currLineStart, end - currLineStart, s);
 601:                 }
 602:               catch (BadLocationException ble)
 603:                 {
 604:                   // Shouldn't happen
 605:                 }
 606:               int mark = Utilities.getTabbedTextOffset(s, metrics, rect.x,
 607:                                                        (int) x,
 608:                                                        WrappedPlainView.this,
 609:                                                        currLineStart);
 610:               return currLineStart + mark;
 611:             }
 612:           // Increment rect.y so we're checking the next logical line
 613:           rect.y += lineHeight;
 614:           
 615:           // Increment currLineStart to the model position of the start
 616:           // of the next logical line
 617:           if (currLineEnd == currLineStart)
 618:             currLineStart = end;
 619:           else
 620:             currLineStart = currLineEnd;
 621:         }
 622:     }    
 623:     
 624:     /**
 625:      * This method is called from insertUpdate and removeUpdate.
 626:      * If the number of lines in the document has changed, just repaint
 627:      * the whole thing (note, could improve performance by not repainting 
 628:      * anything above the changes).  If the number of lines hasn't changed, 
 629:      * just repaint the given Rectangle.
 630:      * @param a the Rectangle to repaint if the number of lines hasn't changed
 631:      */
 632:     void updateDamage (Rectangle a)
 633:     {
 634:       int newNumLines = determineNumLines();
 635:       if (numLines != newNumLines)
 636:         {
 637:           numLines = newNumLines;
 638:           getContainer().repaint();
 639:         }
 640:       else
 641:         getContainer().repaint(a.x, a.y, a.width, a.height);
 642:     }
 643:     
 644:     /**
 645:      * This method is called when something is inserted into the Document
 646:      * that this View is displaying.
 647:      * 
 648:      * @param changes the DocumentEvent for the changes.
 649:      * @param a the allocation of the View
 650:      * @param f the ViewFactory used to rebuild
 651:      */
 652:     public void insertUpdate (DocumentEvent changes, Shape a, ViewFactory f)
 653:     {
 654:       updateDamage((Rectangle)a); 
 655:     }
 656:     
 657:     /**
 658:      * This method is called when something is removed from the Document
 659:      * that this View is displaying.
 660:      * 
 661:      * @param changes the DocumentEvent for the changes.
 662:      * @param a the allocation of the View
 663:      * @param f the ViewFactory used to rebuild
 664:      */
 665:     public void removeUpdate (DocumentEvent changes, Shape a, ViewFactory f)
 666:     {
 667:       updateDamage((Rectangle)a); 
 668:     }
 669:   }
 670: }