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 }