001 /* CompositeView.java -- An abstract view that manages child views
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.Rectangle;
042 import java.awt.Shape;
043
044 import javax.swing.SwingConstants;
045
046 /**
047 * An abstract base implementation of {@link View} that manages child
048 * <code>View</code>s.
049 *
050 * @author Roman Kennke (roman@kennke.org)
051 */
052 public abstract class CompositeView
053 extends View
054 {
055
056 /**
057 * The child views of this <code>CompositeView</code>.
058 */
059 private View[] children;
060
061 /**
062 * The number of child views.
063 */
064 private int numChildren;
065
066 /**
067 * The allocation of this <code>View</code> minus its insets. This is
068 * initialized in {@link #getInsideAllocation} and reused and modified in
069 * {@link #childAllocation(int, Rectangle)}.
070 */
071 private final Rectangle insideAllocation = new Rectangle();
072
073 /**
074 * The insets of this <code>CompositeView</code>. This is initialized
075 * in {@link #setInsets}.
076 */
077 private short top;
078 private short bottom;
079 private short left;
080 private short right;
081
082 /**
083 * Creates a new <code>CompositeView</code> for the given
084 * <code>Element</code>.
085 *
086 * @param element the element that is rendered by this CompositeView
087 */
088 public CompositeView(Element element)
089 {
090 super(element);
091 children = new View[0];
092 top = 0;
093 bottom = 0;
094 left = 0;
095 right = 0;
096 }
097
098 /**
099 * Loads the child views of this <code>CompositeView</code>. This method
100 * is called from {@link #setParent} to initialize the child views of
101 * this composite view.
102 *
103 * @param f the view factory to use for creating new child views
104 *
105 * @see #setParent
106 */
107 protected void loadChildren(ViewFactory f)
108 {
109 if (f != null)
110 {
111 Element el = getElement();
112 int count = el.getElementCount();
113 View[] newChildren = new View[count];
114 for (int i = 0; i < count; ++i)
115 {
116 Element child = el.getElement(i);
117 View view = f.create(child);
118 newChildren[i] = view;
119 }
120 // I'd have called replace(0, getViewCount(), newChildren) here
121 // in order to replace all existing views. However according to
122 // Harmony's tests this is not what the RI does.
123 replace(0, 0, newChildren);
124 }
125 }
126
127 /**
128 * Sets the parent of this <code>View</code>.
129 * In addition to setting the parent, this calls {@link #loadChildren}, if
130 * this <code>View</code> does not already have its children initialized.
131 *
132 * @param parent the parent to set
133 */
134 public void setParent(View parent)
135 {
136 super.setParent(parent);
137 if (parent != null && numChildren == 0)
138 loadChildren(getViewFactory());
139 }
140
141 /**
142 * Returns the number of child views.
143 *
144 * @return the number of child views
145 */
146 public int getViewCount()
147 {
148 return numChildren;
149 }
150
151 /**
152 * Returns the child view at index <code>n</code>.
153 *
154 * @param n the index of the requested child view
155 *
156 * @return the child view at index <code>n</code>
157 */
158 public View getView(int n)
159 {
160 return children[n];
161 }
162
163 /**
164 * Replaces child views by some other child views. If there are no views to
165 * remove (<code>length == 0</code>), the result is a simple insert, if
166 * there are no children to add (<code>view == null</code>) the result
167 * is a simple removal.
168 *
169 * @param offset the start offset from where to remove children
170 * @param length the number of children to remove
171 * @param views the views that replace the removed children
172 */
173 public void replace(int offset, int length, View[] views)
174 {
175 // Make sure we have an array. The Harmony testsuite indicates that we
176 // have to do something like this.
177 if (views == null)
178 views = new View[0];
179
180 // First we set the parent of the removed children to null.
181 int endOffset = offset + length;
182 for (int i = offset; i < endOffset; ++i)
183 {
184 if (children[i].getParent() == this)
185 children[i].setParent(null);
186 children[i] = null;
187 }
188
189 // Update the children array.
190 int delta = views.length - length;
191 int src = offset + length;
192 int numMove = numChildren - src;
193 int dst = src + delta;
194 if (numChildren + delta > children.length)
195 {
196 // Grow array.
197 int newLength = Math.max(2 * children.length, numChildren + delta);
198 View[] newChildren = new View[newLength];
199 System.arraycopy(children, 0, newChildren, 0, offset);
200 System.arraycopy(views, 0, newChildren, offset, views.length);
201 System.arraycopy(children, src, newChildren, dst, numMove);
202 children = newChildren;
203 }
204 else
205 {
206 // Patch existing array.
207 System.arraycopy(children, src, children, dst, numMove);
208 System.arraycopy(views, 0, children, offset, views.length);
209 }
210 numChildren += delta;
211
212 // Finally we set the parent of the added children to this.
213 for (int i = 0; i < views.length; ++i)
214 views[i].setParent(this);
215 }
216
217 /**
218 * Returns the allocation for the specified child <code>View</code>.
219 *
220 * @param index the index of the child view
221 * @param a the allocation for this view
222 *
223 * @return the allocation for the specified child <code>View</code>
224 */
225 public Shape getChildAllocation(int index, Shape a)
226 {
227 Rectangle r = getInsideAllocation(a);
228 childAllocation(index, r);
229 return r;
230 }
231
232 /**
233 * Maps a position in the document into the coordinate space of the View.
234 * The output rectangle usually reflects the font height but has a width
235 * of zero.
236 *
237 * @param pos the position of the character in the model
238 * @param a the area that is occupied by the view
239 * @param bias either {@link Position.Bias#Forward} or
240 * {@link Position.Bias#Backward} depending on the preferred
241 * direction bias. If <code>null</code> this defaults to
242 * <code>Position.Bias.Forward</code>
243 *
244 * @return a rectangle that gives the location of the document position
245 * inside the view coordinate space
246 *
247 * @throws BadLocationException if <code>pos</code> is invalid
248 * @throws IllegalArgumentException if b is not one of the above listed
249 * valid values
250 */
251 public Shape modelToView(int pos, Shape a, Position.Bias bias)
252 throws BadLocationException
253 {
254 boolean backward = bias == Position.Bias.Backward;
255 int testpos = backward ? Math.max(0, pos - 1) : pos;
256
257 Shape ret = null;
258 if (! backward || testpos >= getStartOffset())
259 {
260 int childIndex = getViewIndexAtPosition(testpos);
261 if (childIndex != -1 && childIndex < getViewCount())
262 {
263 View child = getView(childIndex);
264 if (child != null && testpos >= child.getStartOffset()
265 && testpos < child.getEndOffset())
266 {
267 Shape childAlloc = getChildAllocation(childIndex, a);
268 if (childAlloc != null)
269 {
270 ret = child.modelToView(pos, childAlloc, bias);
271 // Handle corner case.
272 if (ret == null && child.getEndOffset() == pos)
273 {
274 childIndex++;
275 if (childIndex < getViewCount())
276 {
277 child = getView(childIndex);
278 childAlloc = getChildAllocation(childIndex, a);
279 ret = child.modelToView(pos, childAlloc, bias);
280 }
281 }
282 }
283 }
284 }
285 }
286
287 if (ret == null)
288 throw new BadLocationException("Position " + pos
289 + " is not represented by view.", pos);
290
291 return ret;
292 }
293
294 /**
295 * Maps a region in the document into the coordinate space of the View.
296 *
297 * @param p1 the beginning position inside the document
298 * @param b1 the direction bias for the beginning position
299 * @param p2 the end position inside the document
300 * @param b2 the direction bias for the end position
301 * @param a the area that is occupied by the view
302 *
303 * @return a rectangle that gives the span of the document region
304 * inside the view coordinate space
305 *
306 * @throws BadLocationException if <code>p1</code> or <code>p2</code> are
307 * invalid
308 * @throws IllegalArgumentException if b1 or b2 is not one of the above
309 * listed valid values
310 */
311 public Shape modelToView(int p1, Position.Bias b1,
312 int p2, Position.Bias b2, Shape a)
313 throws BadLocationException
314 {
315 // TODO: This is most likely not 100% ok, figure out what else is to
316 // do here.
317 return super.modelToView(p1, b1, p2, b2, a);
318 }
319
320 /**
321 * Maps coordinates from the <code>View</code>'s space into a position
322 * in the document model.
323 *
324 * @param x the x coordinate in the view space, x >= 0
325 * @param y the y coordinate in the view space, y >= 0
326 * @param a the allocation of this <code>View</code>
327 * @param b the bias to use
328 *
329 * @return the position in the document that corresponds to the screen
330 * coordinates <code>x, y</code> >= 0
331 */
332 public int viewToModel(float x, float y, Shape a, Position.Bias[] b)
333 {
334 if (x >= 0 && y >= 0)
335 {
336 Rectangle r = getInsideAllocation(a);
337 View view = getViewAtPoint((int) x, (int) y, r);
338 return view.viewToModel(x, y, r, b);
339 }
340 return 0;
341 }
342
343 /**
344 * Returns the next model location that is visible in eiter north / south
345 * direction or east / west direction. This is used to determine the placement
346 * of the caret when navigating around the document with the arrow keys. This
347 * is a convenience method for {@link #getNextNorthSouthVisualPositionFrom}
348 * and {@link #getNextEastWestVisualPositionFrom}.
349 *
350 * @param pos
351 * the model position to start search from
352 * @param b
353 * the bias for <code>pos</code>
354 * @param a
355 * the allocated region for this view
356 * @param direction
357 * the direction from the current position, can be one of the
358 * following:
359 * <ul>
360 * <li>{@link SwingConstants#WEST}</li>
361 * <li>{@link SwingConstants#EAST}</li>
362 * <li>{@link SwingConstants#NORTH}</li>
363 * <li>{@link SwingConstants#SOUTH}</li>
364 * </ul>
365 * @param biasRet
366 * the bias of the return value gets stored here
367 * @return the position inside the model that represents the next visual
368 * location
369 * @throws BadLocationException
370 * if <code>pos</code> is not a valid location inside the document
371 * model
372 * @throws IllegalArgumentException
373 * if <code>direction</code> is invalid
374 */
375 public int getNextVisualPositionFrom(int pos, Position.Bias b, Shape a,
376 int direction, Position.Bias[] biasRet)
377 throws BadLocationException
378 {
379 int retVal = -1;
380 switch (direction)
381 {
382 case SwingConstants.WEST:
383 case SwingConstants.EAST:
384 retVal = getNextEastWestVisualPositionFrom(pos, b, a, direction,
385 biasRet);
386 break;
387 case SwingConstants.NORTH:
388 case SwingConstants.SOUTH:
389 retVal = getNextNorthSouthVisualPositionFrom(pos, b, a, direction,
390 biasRet);
391 break;
392 default:
393 throw new IllegalArgumentException("Illegal value for direction.");
394 }
395 return retVal;
396 }
397
398 /**
399 * Returns the index of the child view that represents the specified
400 * model location.
401 *
402 * @param pos the model location for which to determine the child view index
403 * @param b the bias to be applied to <code>pos</code>
404 *
405 * @return the index of the child view that represents the specified
406 * model location
407 */
408 public int getViewIndex(int pos, Position.Bias b)
409 {
410 if (b == Position.Bias.Backward)
411 pos -= 1;
412 int i = -1;
413 if (pos >= getStartOffset() && pos < getEndOffset())
414 i = getViewIndexAtPosition(pos);
415 return i;
416 }
417
418 /**
419 * Returns <code>true</code> if the specified point lies before the
420 * given <code>Rectangle</code>, <code>false</code> otherwise.
421 *
422 * "Before" is typically defined as being to the left or above.
423 *
424 * @param x the X coordinate of the point
425 * @param y the Y coordinate of the point
426 * @param r the rectangle to test the point against
427 *
428 * @return <code>true</code> if the specified point lies before the
429 * given <code>Rectangle</code>, <code>false</code> otherwise
430 */
431 protected abstract boolean isBefore(int x, int y, Rectangle r);
432
433 /**
434 * Returns <code>true</code> if the specified point lies after the
435 * given <code>Rectangle</code>, <code>false</code> otherwise.
436 *
437 * "After" is typically defined as being to the right or below.
438 *
439 * @param x the X coordinate of the point
440 * @param y the Y coordinate of the point
441 * @param r the rectangle to test the point against
442 *
443 * @return <code>true</code> if the specified point lies after the
444 * given <code>Rectangle</code>, <code>false</code> otherwise
445 */
446 protected abstract boolean isAfter(int x, int y, Rectangle r);
447
448 /**
449 * Returns the child <code>View</code> at the specified location.
450 *
451 * @param x the X coordinate
452 * @param y the Y coordinate
453 * @param r the inner allocation of this <code>BoxView</code> on entry,
454 * the allocation of the found child on exit
455 *
456 * @return the child <code>View</code> at the specified location
457 */
458 protected abstract View getViewAtPoint(int x, int y, Rectangle r);
459
460 /**
461 * Computes the allocation for a child <code>View</code>. The parameter
462 * <code>a</code> stores the allocation of this <code>CompositeView</code>
463 * and is then adjusted to hold the allocation of the child view.
464 *
465 * @param index the index of the child <code>View</code>
466 * @param a the allocation of this <code>CompositeView</code> before the
467 * call, the allocation of the child on exit
468 */
469 protected abstract void childAllocation(int index, Rectangle a);
470
471 /**
472 * Returns the child <code>View</code> that contains the given model
473 * position. The given <code>Rectangle</code> gives the parent's allocation
474 * and is changed to the child's allocation on exit.
475 *
476 * @param pos the model position to query the child <code>View</code> for
477 * @param a the parent allocation on entry and the child allocation on exit
478 *
479 * @return the child view at the given model position
480 */
481 protected View getViewAtPosition(int pos, Rectangle a)
482 {
483 View view = null;
484 int i = getViewIndexAtPosition(pos);
485 if (i >= 0 && i < getViewCount() && a != null)
486 {
487 view = getView(i);
488 childAllocation(i, a);
489 }
490 return view;
491 }
492
493 /**
494 * Returns the index of the child <code>View</code> for the given model
495 * position.
496 *
497 * @param pos the model position for whicht the child <code>View</code> is
498 * queried
499 *
500 * @return the index of the child <code>View</code> for the given model
501 * position
502 */
503 protected int getViewIndexAtPosition(int pos)
504 {
505 // We have a 1:1 mapping of elements to views here, so we forward
506 // this to the element.
507 Element el = getElement();
508 return el.getElementIndex(pos);
509 }
510
511 /**
512 * Returns the allocation that is given to this <code>CompositeView</code>
513 * minus this <code>CompositeView</code>'s insets.
514 *
515 * Also this translates from an immutable allocation to a mutable allocation
516 * that is typically reused and further narrowed, like in
517 * {@link #childAllocation}.
518 *
519 * @param a the allocation given to this <code>CompositeView</code>
520 *
521 * @return the allocation that is given to this <code>CompositeView</code>
522 * minus this <code>CompositeView</code>'s insets or
523 * <code>null</code> if a was <code>null</code>
524 */
525 protected Rectangle getInsideAllocation(Shape a)
526 {
527 if (a == null)
528 return null;
529
530 // Try to avoid allocation of Rectangle here.
531 Rectangle alloc = a instanceof Rectangle ? (Rectangle) a : a.getBounds();
532
533 // Initialize the inside allocation rectangle. This is done inside
534 // a synchronized block in order to avoid multiple threads creating
535 // this instance simultanously.
536 Rectangle inside = insideAllocation;
537 inside.x = alloc.x + getLeftInset();
538 inside.y = alloc.y + getTopInset();
539 inside.width = alloc.width - getLeftInset() - getRightInset();
540 inside.height = alloc.height - getTopInset() - getBottomInset();
541 return inside;
542 }
543
544 /**
545 * Sets the insets defined by attributes in <code>attributes</code>. This
546 * queries the attribute keys {@link StyleConstants#SpaceAbove},
547 * {@link StyleConstants#SpaceBelow}, {@link StyleConstants#LeftIndent} and
548 * {@link StyleConstants#RightIndent} and calls {@link #setInsets} to
549 * actually set the insets on this <code>CompositeView</code>.
550 *
551 * @param attributes the attributes from which to query the insets
552 */
553 protected void setParagraphInsets(AttributeSet attributes)
554 {
555 top = (short) StyleConstants.getSpaceAbove(attributes);
556 bottom = (short) StyleConstants.getSpaceBelow(attributes);
557 left = (short) StyleConstants.getLeftIndent(attributes);
558 right = (short) StyleConstants.getRightIndent(attributes);
559 }
560
561 /**
562 * Sets the insets of this <code>CompositeView</code>.
563 *
564 * @param t the top inset
565 * @param l the left inset
566 * @param b the bottom inset
567 * @param r the right inset
568 */
569 protected void setInsets(short t, short l, short b, short r)
570 {
571 top = t;
572 left = l;
573 bottom = b;
574 right = r;
575 }
576
577 /**
578 * Returns the left inset of this <code>CompositeView</code>.
579 *
580 * @return the left inset of this <code>CompositeView</code>
581 */
582 protected short getLeftInset()
583 {
584 return left;
585 }
586
587 /**
588 * Returns the right inset of this <code>CompositeView</code>.
589 *
590 * @return the right inset of this <code>CompositeView</code>
591 */
592 protected short getRightInset()
593 {
594 return right;
595 }
596
597 /**
598 * Returns the top inset of this <code>CompositeView</code>.
599 *
600 * @return the top inset of this <code>CompositeView</code>
601 */
602 protected short getTopInset()
603 {
604 return top;
605 }
606
607 /**
608 * Returns the bottom inset of this <code>CompositeView</code>.
609 *
610 * @return the bottom inset of this <code>CompositeView</code>
611 */
612 protected short getBottomInset()
613 {
614 return bottom;
615 }
616
617 /**
618 * Returns the next model location that is visible in north or south
619 * direction.
620 * This is used to determine the
621 * placement of the caret when navigating around the document with
622 * the arrow keys.
623 *
624 * @param pos the model position to start search from
625 * @param b the bias for <code>pos</code>
626 * @param a the allocated region for this view
627 * @param direction the direction from the current position, can be one of
628 * the following:
629 * <ul>
630 * <li>{@link SwingConstants#NORTH}</li>
631 * <li>{@link SwingConstants#SOUTH}</li>
632 * </ul>
633 * @param biasRet the bias of the return value gets stored here
634 *
635 * @return the position inside the model that represents the next visual
636 * location
637 *
638 * @throws BadLocationException if <code>pos</code> is not a valid location
639 * inside the document model
640 * @throws IllegalArgumentException if <code>direction</code> is invalid
641 */
642 protected int getNextNorthSouthVisualPositionFrom(int pos, Position.Bias b,
643 Shape a, int direction,
644 Position.Bias[] biasRet)
645 throws BadLocationException
646 {
647 // TODO: It is unknown to me how this method has to be implemented and
648 // there is no specification telling me how to do it properly. Therefore
649 // the implementation was done for cases that are known.
650 //
651 // If this method ever happens to act silly for your particular case then
652 // it is likely that it is a cause of not knowing about your case when it
653 // was implemented first. You are free to fix the behavior.
654 //
655 // Here are the assumptions that lead to the implementation:
656 // If direction is NORTH chose the View preceding the one that contains the
657 // offset 'pos' (imagine the views are stacked on top of each other where
658 // the top is 0 and the bottom is getViewCount()-1.
659 // Consecutively when the direction is SOUTH the View following the one
660 // the offset 'pos' lies in is questioned.
661 //
662 // This limitation is described as PR 27345.
663 int index = getViewIndex(pos, b);
664 View v = null;
665
666 if (index == -1)
667 return pos;
668
669 switch (direction)
670 {
671 case NORTH:
672 // If we cannot calculate a proper offset return the one that was
673 // provided.
674 if (index <= 0)
675 return pos;
676
677 v = getView(index - 1);
678 break;
679 case SOUTH:
680 // If we cannot calculate a proper offset return the one that was
681 // provided.
682 if (index >= getViewCount() - 1)
683 return pos;
684
685 v = getView(index + 1);
686 break;
687 default:
688 throw new IllegalArgumentException();
689 }
690
691 return v.getNextVisualPositionFrom(pos, b, a, direction, biasRet);
692 }
693
694 /**
695 * Returns the next model location that is visible in east or west
696 * direction.
697 * This is used to determine the
698 * placement of the caret when navigating around the document with
699 * the arrow keys.
700 *
701 * @param pos the model position to start search from
702 * @param b the bias for <code>pos</code>
703 * @param a the allocated region for this view
704 * @param direction the direction from the current position, can be one of
705 * the following:
706 * <ul>
707 * <li>{@link SwingConstants#EAST}</li>
708 * <li>{@link SwingConstants#WEST}</li>
709 * </ul>
710 * @param biasRet the bias of the return value gets stored here
711 *
712 * @return the position inside the model that represents the next visual
713 * location
714 *
715 * @throws BadLocationException if <code>pos</code> is not a valid location
716 * inside the document model
717 * @throws IllegalArgumentException if <code>direction</code> is invalid
718 */
719 protected int getNextEastWestVisualPositionFrom(int pos, Position.Bias b,
720 Shape a, int direction,
721 Position.Bias[] biasRet)
722 throws BadLocationException
723 {
724 // TODO: It is unknown to me how this method has to be implemented and
725 // there is no specification telling me how to do it properly. Therefore
726 // the implementation was done for cases that are known.
727 //
728 // If this method ever happens to act silly for your particular case then
729 // it is likely that it is a cause of not knowing about your case when it
730 // was implemented first. You are free to fix the behavior.
731 //
732 // Here are the assumptions that lead to the implementation:
733 // If direction is EAST increase the offset by one and ask the View to
734 // which that index belong to calculate the 'next visual position'.
735 // If the direction is WEST do the same with offset 'pos' being decreased
736 // by one.
737 // This behavior will fail in a right-to-left or bidi environment!
738 //
739 // This limitation is described as PR 27346.
740 int index;
741
742 View v = null;
743
744 switch (direction)
745 {
746 case EAST:
747 index = getViewIndex(pos + 1, b);
748 // If we cannot calculate a proper offset return the one that was
749 // provided.
750 if (index == -1)
751 return pos;
752
753 v = getView(index);
754 break;
755 case WEST:
756 index = getViewIndex(pos - 1, b);
757 // If we cannot calculate a proper offset return the one that was
758 // provided.
759 if (index == -1)
760 return pos;
761
762 v = getView(index);
763 break;
764 default:
765 throw new IllegalArgumentException();
766 }
767
768 return v.getNextVisualPositionFrom(pos,
769 b,
770 a,
771 direction,
772 biasRet);
773 }
774
775 /**
776 * Determines if the next view in horinzontal direction is located to
777 * the east or west of the view at position <code>pos</code>. Usually
778 * the <code>View</code>s are laid out from the east to the west, so
779 * we unconditionally return <code>false</code> here. Subclasses that
780 * support bidirectional text may wish to override this method.
781 *
782 * @param pos the position in the document
783 * @param bias the bias to be applied to <code>pos</code>
784 *
785 * @return <code>true</code> if the next <code>View</code> is located
786 * to the EAST, <code>false</code> otherwise
787 */
788 protected boolean flipEastAndWestAtEnds(int pos, Position.Bias bias)
789 {
790 return false;
791 }
792 }