001/* WrappedPlainView.java -- 
002   Copyright (C) 2005, 2006 Free Software Foundation, Inc.
003
004This file is part of GNU Classpath.
005
006GNU Classpath is free software; you can redistribute it and/or modify
007it under the terms of the GNU General Public License as published by
008the Free Software Foundation; either version 2, or (at your option)
009any later version.
010
011GNU Classpath is distributed in the hope that it will be useful, but
012WITHOUT ANY WARRANTY; without even the implied warranty of
013MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
014General Public License for more details.
015
016You should have received a copy of the GNU General Public License
017along with GNU Classpath; see the file COPYING.  If not, write to the
018Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
01902110-1301 USA.
020
021Linking this library statically or dynamically with other modules is
022making a combined work based on this library.  Thus, the terms and
023conditions of the GNU General Public License cover the whole
024combination.
025
026As a special exception, the copyright holders of this library give you
027permission to link this library with independent modules to produce an
028executable, regardless of the license terms of these independent
029modules, and to copy and distribute the resulting executable under
030terms of your choice, provided that you also meet, for each linked
031independent module, the terms and conditions of the license of that
032module.  An independent module is a module which is not derived from
033or based on this library.  If you modify this library, you may extend
034this exception to your version of the library, but you are not
035obligated to do so.  If you do not wish to do so, delete this
036exception statement from your version. */
037
038
039package javax.swing.text;
040
041import java.awt.Color;
042import java.awt.Container;
043import java.awt.FontMetrics;
044import java.awt.Graphics;
045import java.awt.Rectangle;
046import java.awt.Shape;
047
048import javax.swing.event.DocumentEvent;
049import javax.swing.text.Position.Bias;
050
051/**
052 * @author Anthony Balkissoon abalkiss at redhat dot com
053 *
054 */
055public class WrappedPlainView extends BoxView implements TabExpander
056{
057  /** The color for selected text **/
058  Color selectedColor;
059  
060  /** The color for unselected text **/
061  Color unselectedColor;
062  
063  /** The color for disabled components **/
064  Color disabledColor;
065  
066  /**
067   * Stores the font metrics. This is package private to avoid synthetic
068   * accessor method.
069   */
070  FontMetrics metrics;
071  
072  /** Whether or not to wrap on word boundaries **/
073  boolean wordWrap;
074  
075  /** A ViewFactory that creates WrappedLines **/
076  ViewFactory viewFactory = new WrappedLineCreator();
077  
078  /** The start of the selected text **/
079  int selectionStart;
080  
081  /** The end of the selected text **/
082  int selectionEnd;
083  
084  /** The height of the line (used while painting) **/
085  int lineHeight;
086
087  /**
088   * The base offset for tab calculations.
089   */
090  private int tabBase;
091
092  /**
093   * The tab size.
094   */
095  private int tabSize;
096
097  /**
098   * The instance returned by {@link #getLineBuffer()}.
099   */
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}