001    /* PlainView.java -- 
002       Copyright (C) 2004, 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.Component;
043    import java.awt.Font;
044    import java.awt.FontMetrics;
045    import java.awt.Graphics;
046    import java.awt.Rectangle;
047    import java.awt.Shape;
048    
049    import javax.swing.SwingUtilities;
050    import javax.swing.event.DocumentEvent;
051    import javax.swing.event.DocumentEvent.ElementChange;
052    
053    public class PlainView extends View implements TabExpander
054    {
055      Color selectedColor;
056      Color unselectedColor;
057    
058      /**
059       * The color that is used to draw disabled text fields.
060       */
061      Color disabledColor;
062      
063      /**
064       * While painting this is the textcomponent's current start index
065       * of the selection.
066       */
067      int selectionStart;
068    
069      /**
070       * While painting this is the textcomponent's current end index
071       * of the selection.
072       */
073      int selectionEnd;
074    
075      Font font;
076      
077      /** The length of the longest line in the Document **/
078      float maxLineLength = -1;
079      
080      /** The longest line in the Document **/
081      Element longestLine = null;
082      
083      protected FontMetrics metrics;
084    
085      /**
086       * The instance returned by {@link #getLineBuffer()}.
087       */
088      private transient Segment lineBuffer;
089    
090      /**
091       * The base offset for tab calculations.
092       */
093      private int tabBase;
094    
095      /**
096       * The tab size.
097       */
098      private int tabSize;
099    
100      public PlainView(Element elem)
101      {
102        super(elem);
103      }
104    
105      /**
106       * @since 1.4
107       */
108      protected void updateMetrics()
109      {
110        Component component = getContainer();
111        Font font = component.getFont();
112    
113        if (this.font != font)
114          {
115            this.font = font;
116            metrics = component.getFontMetrics(font);
117            tabSize = getTabSize() * metrics.charWidth('m');
118          }
119      }
120      
121      /**
122       * @since 1.4
123       */
124      protected Rectangle lineToRect(Shape a, int line)
125      {
126        // Ensure metrics are up-to-date.
127        updateMetrics();
128        
129        Rectangle rect = a instanceof Rectangle ? (Rectangle) a : a.getBounds();
130        int fontHeight = metrics.getHeight();
131        return new Rectangle(rect.x, rect.y + (line * fontHeight),
132                             rect.width, fontHeight);
133      }
134    
135      public Shape modelToView(int position, Shape a, Position.Bias b)
136        throws BadLocationException
137      {
138        // Ensure metrics are up-to-date.
139        updateMetrics();
140        
141        Document document = getDocument();
142    
143        // Get rectangle of the line containing position.
144        int lineIndex = getElement().getElementIndex(position);
145        Rectangle rect = lineToRect(a, lineIndex);
146        tabBase = rect.x;
147    
148        // Get the rectangle for position.
149        Element line = getElement().getElement(lineIndex);
150        int lineStart = line.getStartOffset();
151        Segment segment = getLineBuffer();
152        document.getText(lineStart, position - lineStart, segment);
153        int xoffset = Utilities.getTabbedTextWidth(segment, metrics, tabBase,
154                                                   this, lineStart);
155    
156        // Calc the real rectangle.
157        rect.x += xoffset;
158        rect.width = 1;
159        rect.height = metrics.getHeight();
160    
161        return rect;
162      }
163      
164      /**
165       * Draws a line of text. The X and Y coordinates specify the start of
166       * the <em>baseline</em> of the line.
167       *
168       * @param lineIndex the index of the line
169       * @param g the graphics to use for drawing the text
170       * @param x the X coordinate of the baseline
171       * @param y the Y coordinate of the baseline
172       */
173      protected void drawLine(int lineIndex, Graphics g, int x, int y)
174      {
175        try
176          {
177            Element line = getElement().getElement(lineIndex);
178            int startOffset = line.getStartOffset();
179            int endOffset = line.getEndOffset() - 1;
180            
181            if (selectionStart <= startOffset)
182              // Selection starts before the line ...
183              if (selectionEnd <= startOffset)
184                {
185                  // end ends before the line: Draw completely unselected text.
186                  drawUnselectedText(g, x, y, startOffset, endOffset);
187                }
188              else if (selectionEnd <= endOffset)
189                {
190                  // and ends within the line: First part is selected,
191                  // second is not.
192                  x = drawSelectedText(g, x, y, startOffset, selectionEnd);
193                  drawUnselectedText(g, x, y, selectionEnd, endOffset);
194                }
195              else
196                // and ends behind the line: Draw completely selected text.
197                drawSelectedText(g, x, y, startOffset, endOffset);
198            else if (selectionStart < endOffset)
199              // Selection starts within the line ..
200              if (selectionEnd < endOffset)
201                {
202                  // and ends within it: First part unselected, second part
203                  // selected, third part unselected.
204                  x = drawUnselectedText(g, x, y, startOffset, selectionStart);
205                  x = drawSelectedText(g, x, y, selectionStart, selectionEnd);
206                  drawUnselectedText(g, x, y, selectionEnd, endOffset);
207                }
208              else
209                {
210                  // and ends behind the line: First part unselected, second
211                  // part selected.
212                  x = drawUnselectedText(g, x, y, startOffset, selectionStart);
213                  drawSelectedText(g, x, y, selectionStart, endOffset);
214                }
215            else
216              // Selection is behind this line: Draw completely unselected text.
217              drawUnselectedText(g, x, y, startOffset, endOffset);
218          }
219        catch (BadLocationException e)
220        {
221          AssertionError ae = new AssertionError("Unexpected bad location");
222          ae.initCause(e);
223          throw ae;
224        }
225      }
226    
227      protected int drawSelectedText(Graphics g, int x, int y, int p0, int p1)
228        throws BadLocationException
229      {
230        g.setColor(selectedColor);
231        Segment segment = getLineBuffer();
232        getDocument().getText(p0, p1 - p0, segment);
233        return Utilities.drawTabbedText(segment, x, y, g, this, segment.offset);
234      }
235    
236      /**
237       * Draws a chunk of unselected text.
238       *
239       * @param g the graphics to use for drawing the text
240       * @param x the X coordinate of the baseline
241       * @param y the Y coordinate of the baseline
242       * @param p0 the start position in the text model
243       * @param p1 the end position in the text model
244       *
245       * @return the X location of the end of the range
246       *
247       * @throws BadLocationException if <code>p0</code> or <code>p1</code> are
248       *         invalid
249       */
250      protected int drawUnselectedText(Graphics g, int x, int y, int p0, int p1)
251        throws BadLocationException
252      {
253        JTextComponent textComponent = (JTextComponent) getContainer();
254        if (textComponent.isEnabled())
255          g.setColor(unselectedColor);
256        else
257          g.setColor(disabledColor);
258    
259        Segment segment = getLineBuffer();
260        getDocument().getText(p0, p1 - p0, segment);
261        return Utilities.drawTabbedText(segment, x, y, g, this, segment.offset);
262      }
263    
264      public void paint(Graphics g, Shape s)
265      {
266        // Ensure metrics are up-to-date.
267        updateMetrics();
268        
269        JTextComponent textComponent = (JTextComponent) getContainer();
270    
271        selectedColor = textComponent.getSelectedTextColor();
272        unselectedColor = textComponent.getForeground();
273        disabledColor = textComponent.getDisabledTextColor();
274        selectionStart = textComponent.getSelectionStart();
275        selectionEnd = textComponent.getSelectionEnd();
276    
277        Rectangle rect = s instanceof Rectangle ? (Rectangle) s : s.getBounds();
278        tabBase = rect.x;
279    
280        // FIXME: Text may be scrolled.
281        Document document = textComponent.getDocument();
282        Element root = getElement();
283        int height = metrics.getHeight();
284    
285        // For layered highlighters we need to paint the layered highlights
286        // before painting any text.
287        LayeredHighlighter hl = null;
288        Highlighter h = textComponent.getHighlighter();
289        if (h instanceof LayeredHighlighter)
290          hl = (LayeredHighlighter) h;
291    
292        int count = root.getElementCount();
293    
294        // Determine first and last line inside the clip.
295        Rectangle clip = g.getClipBounds();
296        SwingUtilities.computeIntersection(rect.x, rect.y, rect.width, rect.height,
297                                           clip);
298        int line0 = (clip.y - rect.y) / height;
299        line0 = Math.max(0, Math.min(line0, count - 1));
300        int line1 = (clip.y + clip.height - rect.y) / height;
301        line1 = Math.max(0, Math.min(line1, count - 1));
302        int y = rect.y + metrics.getAscent() + height * line0;
303        for (int i = line0; i <= line1; i++)
304          {
305            if (hl != null)
306              {
307                Element lineEl = root.getElement(i);
308                // Exclude the trailing newline from beeing highlighted.
309                if (i == count)
310                  hl.paintLayeredHighlights(g, lineEl.getStartOffset(),
311                                            lineEl.getEndOffset(), s, textComponent,
312                                            this);
313                else
314                  hl.paintLayeredHighlights(g, lineEl.getStartOffset(),
315                                            lineEl.getEndOffset() - 1, s,
316                                            textComponent, this);
317              }
318            drawLine(i, g, rect.x, y);
319            y += height;
320          }
321      }
322    
323      /**
324       * Returns the tab size of a tab.  Checks the Document's
325       * properties for PlainDocument.tabSizeAttribute and returns it if it is
326       * defined, otherwise returns 8.
327       * 
328       * @return the tab size.
329       */
330      protected int getTabSize()
331      {
332        Object tabSize = getDocument().getProperty(PlainDocument.tabSizeAttribute);
333        if (tabSize == null)
334          return 8;
335        return ((Integer)tabSize).intValue();
336      }
337    
338      /**
339       * Returns the next tab stop position after a given reference position.
340       *
341       * This implementation ignores the <code>tabStop</code> argument.
342       * 
343       * @param x the current x position in pixels
344       * @param tabStop the position within the text stream that the tab occured at
345       */
346      public float nextTabStop(float x, int tabStop)
347      {
348        float next = x;
349        if (tabSize != 0)
350          {
351            int numTabs = (((int) x) - tabBase) / tabSize;
352            next = tabBase + (numTabs + 1) * tabSize;
353          }
354        return next; 
355      }
356    
357      /**
358       * Returns the length of the longest line, used for getting the span
359       * @return the length of the longest line
360       */
361      float determineMaxLineLength()
362      {
363        // if the longest line is cached, return the cached value
364        if (maxLineLength != -1)
365          return maxLineLength;
366        
367        // otherwise we have to go through all the lines and find it
368        Element el = getElement();
369        Segment seg = getLineBuffer();
370        float span = 0;
371        for (int i = 0; i < el.getElementCount(); i++)
372          {
373            Element child = el.getElement(i);
374            int start = child.getStartOffset();
375            int end = child.getEndOffset() - 1;
376            try
377              {
378                el.getDocument().getText(start, end - start, seg);
379              }
380            catch (BadLocationException ex)
381              {
382                AssertionError ae = new AssertionError("Unexpected bad location");
383                ae.initCause(ex);
384                throw ae;
385              }
386            
387            if (seg == null || seg.array == null || seg.count == 0)
388              continue;
389            
390            int width = metrics.charsWidth(seg.array, seg.offset, seg.count);
391            if (width > span)
392              {
393                longestLine = child;
394                span = width;
395              }
396          }
397        maxLineLength = span;
398        return maxLineLength;
399      }
400      
401      public float getPreferredSpan(int axis)
402      {
403        if (axis != X_AXIS && axis != Y_AXIS)
404          throw new IllegalArgumentException();
405    
406        // make sure we have the metrics
407        updateMetrics();
408    
409        Element el = getElement();
410        float span;
411    
412        switch (axis)
413          {
414          case X_AXIS:
415            span = determineMaxLineLength();
416            break;
417          case Y_AXIS:
418          default:
419            span = metrics.getHeight() * el.getElementCount();
420            break;
421          }
422        
423        return span;
424      }
425    
426      /**
427       * Maps coordinates from the <code>View</code>'s space into a position
428       * in the document model.
429       *
430       * @param x the x coordinate in the view space
431       * @param y the y coordinate in the view space
432       * @param a the allocation of this <code>View</code>
433       * @param b the bias to use
434       *
435       * @return the position in the document that corresponds to the screen
436       *         coordinates <code>x, y</code>
437       */
438      public int viewToModel(float x, float y, Shape a, Position.Bias[] b)
439      {
440        Rectangle rec = a instanceof Rectangle ? (Rectangle) a : a.getBounds();
441        tabBase = rec.x;
442    
443        int pos;
444        if ((int) y < rec.y)
445          // Above our area vertically. Return start offset.
446          pos = getStartOffset();
447        else if ((int) y > rec.y + rec.height)
448          // Below our area vertically. Return end offset.
449          pos = getEndOffset() - 1;
450        else
451          {
452            // Inside the allocation vertically. Determine line and X offset.
453            Document doc = getDocument();
454            Element root = doc.getDefaultRootElement();
455            int line = Math.abs(((int) y - rec.y) / metrics.getHeight());
456            if (line >= root.getElementCount())
457              pos = getEndOffset() - 1;
458            else
459              {
460                Element lineEl = root.getElement(line);
461                if (x < rec.x)
462                  // To the left of the allocation area.
463                  pos = lineEl.getStartOffset();
464                else if (x > rec.x + rec.width)
465                  // To the right of the allocation area.
466                  pos = lineEl.getEndOffset() - 1;
467                else
468                  {
469                    try
470                      {
471                        int p0 = lineEl.getStartOffset();
472                        int p1 = lineEl.getEndOffset();
473                        Segment s = new Segment();
474                        doc.getText(p0, p1 - p0, s);
475                        tabBase = rec.x;
476                        pos = p0 + Utilities.getTabbedTextOffset(s, metrics,
477                                                                 tabBase, (int) x,
478                                                                 this, p0);
479                      }
480                    catch (BadLocationException ex)
481                      {
482                        // Should not happen.
483                        pos = -1;
484                      }
485                  }
486                
487              }
488          }
489        // Bias is always forward.
490        b[0] = Position.Bias.Forward;
491        return pos;
492      }     
493      
494      /**
495       * Since insertUpdate and removeUpdate each deal with children
496       * Elements being both added and removed, they both have to perform
497       * the same checks.  So they both simply call this method.
498       * @param changes the DocumentEvent for the changes to the Document.
499       * @param a the allocation of the View.
500       * @param f the ViewFactory to use for rebuilding.
501       */
502      protected void updateDamage(DocumentEvent changes, Shape a, ViewFactory f)
503      {
504        // This happens during initialization.
505        if (metrics == null)
506          {
507            updateMetrics();
508            preferenceChanged(null, true, true);
509            return;
510          }
511    
512        Element element = getElement();
513    
514        // Find longest line if it hasn't been initialized yet.
515        if (longestLine == null)
516          findLongestLine(0, element.getElementCount() - 1);
517    
518        ElementChange change = changes.getChange(element);
519        if (changes.getType() == DocumentEvent.EventType.INSERT)
520          {
521            // Handles character/line insertion.
522    
523            // Determine if lines have been added. In this case we repaint
524            // differently.
525            boolean linesAdded = true;
526            if (change == null)
527              linesAdded = false;
528    
529            // Determine the start line.
530            int start;
531            if (linesAdded)
532              start = change.getIndex();
533            else
534              start = element.getElementIndex(changes.getOffset());
535    
536            // Determine the length of the updated region.
537            int length = 0;
538            if (linesAdded)
539              length = change.getChildrenAdded().length - 1;
540    
541            // Update the longest line and length.
542            int oldMaxLength = (int) maxLineLength;
543            if (longestLine.getEndOffset() < changes.getOffset()
544                || longestLine.getStartOffset() > changes.getOffset()
545                                                 + changes.getLength())
546              {
547                findLongestLine(start, start + length);
548              }
549            else
550              {
551                findLongestLine(0, element.getElementCount() - 1);
552              }
553    
554            // Trigger a preference change so that the layout gets updated
555            // correctly.
556            preferenceChanged(null, maxLineLength != oldMaxLength, linesAdded);
557    
558            // Damage the updated line range.
559            int endLine = start;
560            if (linesAdded)
561              endLine = element.getElementCount() - 1;
562            damageLineRange(start, endLine, a, getContainer());
563    
564          }
565        else
566          {
567            // Handles character/lines removals.
568    
569            // Update the longest line and length and trigger preference changed.
570            int oldMaxLength = (int) maxLineLength;
571            if (change != null)
572              {
573                // Line(s) have been removed.
574                findLongestLine(0, element.getElementCount() - 1);
575                preferenceChanged(null, maxLineLength != oldMaxLength, true);
576              }
577            else
578              {
579                // No line has been removed.
580                int lineNo = getElement().getElementIndex(changes.getOffset());
581                Element line = getElement().getElement(lineNo);
582                if (longestLine == line)
583                  {
584                    findLongestLine(0, element.getElementCount() - 1);
585                    preferenceChanged(null, maxLineLength != oldMaxLength, false);
586                }
587                damageLineRange(lineNo, lineNo, a, getContainer());
588            }
589          }
590      }
591    
592      /**
593       * This method is called when something is inserted into the Document
594       * that this View is displaying.
595       * 
596       * @param changes the DocumentEvent for the changes.
597       * @param a the allocation of the View
598       * @param f the ViewFactory used to rebuild
599       */
600      public void insertUpdate(DocumentEvent changes, Shape a, ViewFactory f)
601      {
602        updateDamage(changes, a, f);
603      }
604    
605      /**
606       * This method is called when something is removed from the Document
607       * that this View is displaying.
608       * 
609       * @param changes the DocumentEvent for the changes.
610       * @param a the allocation of the View
611       * @param f the ViewFactory used to rebuild
612       */
613      public void removeUpdate(DocumentEvent changes, Shape a, ViewFactory f)
614      {
615        updateDamage(changes, a, f);
616      }
617      
618      /**
619       * This method is called when attributes were changed in the 
620       * Document in a location that this view is responsible for.
621       */
622      public void changedUpdate (DocumentEvent changes, Shape a, ViewFactory f)
623      {
624        updateDamage(changes, a, f);
625      }
626      
627      /**
628       * Repaint the given line range.  This is called from insertUpdate,
629       * changedUpdate, and removeUpdate when no new lines were added 
630       * and no lines were removed, to repaint the line that was 
631       * modified.
632       * 
633       * @param line0 the start of the range
634       * @param line1 the end of the range
635       * @param a the rendering region of the host
636       * @param host the Component that uses this View (used to call repaint
637       * on that Component)
638       * 
639       * @since 1.4
640       */
641      protected void damageLineRange (int line0, int line1, Shape a, Component host)
642      {
643        if (a == null)
644          return;
645    
646        Rectangle rec0 = lineToRect(a, line0);
647        Rectangle rec1 = lineToRect(a, line1);
648    
649        if (rec0 == null || rec1 == null)
650          // something went wrong, repaint the entire host to be safe
651          host.repaint();
652        else
653          {
654            Rectangle repaintRec = SwingUtilities.computeUnion(rec0.x, rec0.y,
655                                                               rec0.width,
656                                                               rec0.height, rec1);
657            host.repaint(repaintRec.x, repaintRec.y, repaintRec.width,
658                         repaintRec.height);
659          }    
660      }
661    
662      /**
663       * Provides a {@link Segment} object, that can be used to fetch text from
664       * the document.
665       *
666       * @returna {@link Segment} object, that can be used to fetch text from
667       *          the document
668       */
669      protected final Segment getLineBuffer()
670      {
671        if (lineBuffer == null)
672          lineBuffer = new Segment();
673        return lineBuffer;
674      }
675    
676      /**
677       * Finds and updates the longest line in the view inside an interval of
678       * lines.
679       *
680       * @param start the start of the search interval
681       * @param end the end of the search interval
682       */
683      private void findLongestLine(int start, int end)
684      {
685        for (int i = start; i <= end; i++)
686          {
687            int w = getLineLength(i);
688            if (w > maxLineLength)
689              {
690                maxLineLength = w;
691                longestLine = getElement().getElement(i);
692              }
693          }
694      }
695    
696      /**
697       * Determines the length of the specified line.
698       *
699       * @param line the number of the line
700       *
701       * @return the length of the line in pixels
702       */
703      private int getLineLength(int line)
704      {
705        Element lineEl = getElement().getElement(line);
706        Segment buffer = getLineBuffer();
707        try
708          {
709            Document doc = getDocument();
710            doc.getText(lineEl.getStartOffset(),
711                        lineEl.getEndOffset() - lineEl.getStartOffset() - 1,
712                        buffer);
713          }
714        catch (BadLocationException ex)
715          {
716            AssertionError err = new AssertionError("Unexpected bad location");
717            err.initCause(ex);
718            throw err;
719          }
720    
721        return Utilities.getTabbedTextWidth(buffer, metrics, tabBase, this,
722                                            lineEl.getStartOffset());
723      }
724    }
725