001    /* WrappedPlainView.java --
002       Copyright (C) 2005, 2006 Free Software Foundation, Inc.
003    
004    This file is part of GNU Classpath.
005    
006    GNU Classpath is free software; you can redistribute it and/or modify
007    it under the terms of the GNU General Public License as published by
008    the Free Software Foundation; either version 2, or (at your option)
009    any later version.
010    
011    GNU Classpath is distributed in the hope that it will be useful, but
012    WITHOUT ANY WARRANTY; without even the implied warranty of
013    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
014    General Public License for more details.
015    
016    You should have received a copy of the GNU General Public License
017    along with GNU Classpath; see the file COPYING.  If not, write to the
018    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
019    02110-1301 USA.
020    
021    Linking this library statically or dynamically with other modules is
022    making a combined work based on this library.  Thus, the terms and
023    conditions of the GNU General Public License cover the whole
024    combination.
025    
026    As a special exception, the copyright holders of this library give you
027    permission to link this library with independent modules to produce an
028    executable, regardless of the license terms of these independent
029    modules, and to copy and distribute the resulting executable under
030    terms of your choice, provided that you also meet, for each linked
031    independent module, the terms and conditions of the license of that
032    module.  An independent module is a module which is not derived from
033    or based on this library.  If you modify this library, you may extend
034    this exception to your version of the library, but you are not
035    obligated to do so.  If you do not wish to do so, delete this
036    exception statement from your version. */
037    
038    
039    package javax.swing.text;
040    
041    import java.awt.Color;
042    import java.awt.Container;
043    import java.awt.FontMetrics;
044    import java.awt.Graphics;
045    import java.awt.Rectangle;
046    import java.awt.Shape;
047    
048    import javax.swing.event.DocumentEvent;
049    import javax.swing.text.Position.Bias;
050    
051    /**
052     * @author Anthony Balkissoon abalkiss at redhat dot com
053     *
054     */
055    public 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    }