001/* PlainView.java -- 
002   Copyright (C) 2004, 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.Component;
043import java.awt.Font;
044import java.awt.FontMetrics;
045import java.awt.Graphics;
046import java.awt.Rectangle;
047import java.awt.Shape;
048
049import javax.swing.SwingUtilities;
050import javax.swing.event.DocumentEvent;
051import javax.swing.event.DocumentEvent.ElementChange;
052
053public 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