OSDN Git Service

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