Source for javax.swing.text.WrappedPlainView

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