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 }