1/* 2 * Copyright (c) 1998, 2015, Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. Oracle designates this 8 * particular file as subject to the "Classpath" exception as provided 9 * by Oracle in the LICENSE file that accompanied this code. 10 * 11 * This code is distributed in the hope that it will be useful, but WITHOUT 12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 14 * version 2 for more details (a copy is included in the LICENSE file that 15 * accompanied this code). 16 * 17 * You should have received a copy of the GNU General Public License version 18 * 2 along with this work; if not, write to the Free Software Foundation, 19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 20 * 21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 22 * or visit www.oracle.com if you need additional information or have any 23 * questions. 24 */ 25package javax.swing.text; 26 27import java.util.Vector; 28import java.awt.*; 29import javax.swing.event.*; 30 31/** 32 * ZoneView is a View implementation that creates zones for which 33 * the child views are not created or stored until they are needed 34 * for display or model/view translations. This enables a substantial 35 * reduction in memory consumption for situations where the model 36 * being represented is very large, by building view objects only for 37 * the region being actively viewed/edited. The size of the children 38 * can be estimated in some way, or calculated asynchronously with 39 * only the result being saved. 40 * <p> 41 * ZoneView extends BoxView to provide a box that implements 42 * zones for its children. The zones are special View implementations 43 * (the children of an instance of this class) that represent only a 44 * portion of the model that an instance of ZoneView is responsible 45 * for. The zones don't create child views until an attempt is made 46 * to display them. A box shaped view is well suited to this because: 47 * <ul> 48 * <li> 49 * Boxes are a heavily used view, and having a box that 50 * provides this behavior gives substantial opportunity 51 * to plug the behavior into a view hierarchy from the 52 * view factory. 53 * <li> 54 * Boxes are tiled in one direction, so it is easy to 55 * divide them into zones in a reliable way. 56 * <li> 57 * Boxes typically have a simple relationship to the model (i.e. they 58 * create child views that directly represent the child elements). 59 * <li> 60 * Boxes are easier to estimate the size of than some other shapes. 61 * </ul> 62 * <p> 63 * The default behavior is controlled by two properties, maxZoneSize 64 * and maxZonesLoaded. Setting maxZoneSize to Integer.MAX_VALUE would 65 * have the effect of causing only one zone to be created. This would 66 * effectively turn the view into an implementation of the decorator 67 * pattern. Setting maxZonesLoaded to a value of Integer.MAX_VALUE would 68 * cause zones to never be unloaded. For simplicity, zones are created on 69 * boundaries represented by the child elements of the element the view is 70 * responsible for. The zones can be any View implementation, but the 71 * default implementation is based upon AsyncBoxView which supports fairly 72 * large zones efficiently. 73 * 74 * @author Timothy Prinzing 75 * @see View 76 * @since 1.3 77 */ 78public class ZoneView extends BoxView { 79 80 int maxZoneSize = 8 * 1024; 81 int maxZonesLoaded = 3; 82 Vector<View> loadedZones; 83 84 /** 85 * Constructs a ZoneView. 86 * 87 * @param elem the element this view is responsible for 88 * @param axis either View.X_AXIS or View.Y_AXIS 89 */ 90 public ZoneView(Element elem, int axis) { 91 super(elem, axis); 92 loadedZones = new Vector<View>(); 93 } 94 95 /** 96 * Get the current maximum zone size. 97 * @return the current maximum zone size 98 */ 99 public int getMaximumZoneSize() { 100 return maxZoneSize; 101 } 102 103 /** 104 * Set the desired maximum zone size. A 105 * zone may get larger than this size if 106 * a single child view is larger than this 107 * size since zones are formed on child view 108 * boundaries. 109 * 110 * @param size the number of characters the zone 111 * may represent before attempting to break 112 * the zone into a smaller size. 113 */ 114 public void setMaximumZoneSize(int size) { 115 maxZoneSize = size; 116 } 117 118 /** 119 * Get the current setting of the number of zones 120 * allowed to be loaded at the same time. 121 * @return current setting of the number of zones 122 * allowed to be loaded at the same time 123 */ 124 public int getMaxZonesLoaded() { 125 return maxZonesLoaded; 126 } 127 128 /** 129 * Sets the current setting of the number of zones 130 * allowed to be loaded at the same time. This will throw an 131 * <code>IllegalArgumentException</code> if <code>mzl</code> is less 132 * than 1. 133 * 134 * @param mzl the desired maximum number of zones 135 * to be actively loaded, must be greater than 0 136 * @exception IllegalArgumentException if <code>mzl</code> is < 1 137 */ 138 public void setMaxZonesLoaded(int mzl) { 139 if (mzl < 1) { 140 throw new IllegalArgumentException("ZoneView.setMaxZonesLoaded must be greater than 0."); 141 } 142 maxZonesLoaded = mzl; 143 unloadOldZones(); 144 } 145 146 /** 147 * Called by a zone when it gets loaded. This happens when 148 * an attempt is made to display or perform a model/view 149 * translation on a zone that was in an unloaded state. 150 * This is implemented to check if the maximum number of 151 * zones was reached and to unload the oldest zone if so. 152 * 153 * @param zone the child view that was just loaded. 154 */ 155 protected void zoneWasLoaded(View zone) { 156 //System.out.println("loading: " + zone.getStartOffset() + "," + zone.getEndOffset()); 157 loadedZones.addElement(zone); 158 unloadOldZones(); 159 } 160 161 void unloadOldZones() { 162 while (loadedZones.size() > getMaxZonesLoaded()) { 163 View zone = loadedZones.elementAt(0); 164 loadedZones.removeElementAt(0); 165 unloadZone(zone); 166 } 167 } 168 169 /** 170 * Unload a zone (Convert the zone to its memory saving state). 171 * The zones are expected to represent a subset of the 172 * child elements of the element this view is responsible for. 173 * Therefore, the default implementation is to simple remove 174 * all the children. 175 * 176 * @param zone the child view desired to be set to an 177 * unloaded state. 178 */ 179 protected void unloadZone(View zone) { 180 //System.out.println("unloading: " + zone.getStartOffset() + "," + zone.getEndOffset()); 181 zone.removeAll(); 182 } 183 184 /** 185 * Determine if a zone is in the loaded state. 186 * The zones are expected to represent a subset of the 187 * child elements of the element this view is responsible for. 188 * Therefore, the default implementation is to return 189 * true if the view has children. 190 * param zone the child view 191 * @param zone the zone 192 * @return whether or not the zone is in the loaded state. 193 */ 194 protected boolean isZoneLoaded(View zone) { 195 return (zone.getViewCount() > 0); 196 } 197 198 /** 199 * Create a view to represent a zone for the given 200 * range within the model (which should be within 201 * the range of this objects responsibility). This 202 * is called by the zone management logic to create 203 * new zones. Subclasses can provide a different 204 * implementation for a zone by changing this method. 205 * 206 * @param p0 the start of the desired zone. This should 207 * be >= getStartOffset() and < getEndOffset(). This 208 * value should also be < p1. 209 * @param p1 the end of the desired zone. This should 210 * be > getStartOffset() and <= getEndOffset(). This 211 * value should also be > p0. 212 * @return a view to represent a zone for the given range within 213 * the model 214 */ 215 protected View createZone(int p0, int p1) { 216 Document doc = getDocument(); 217 View zone; 218 try { 219 zone = new Zone(getElement(), 220 doc.createPosition(p0), 221 doc.createPosition(p1)); 222 } catch (BadLocationException ble) { 223 // this should puke in some way. 224 throw new StateInvariantError(ble.getMessage()); 225 } 226 return zone; 227 } 228 229 /** 230 * Loads all of the children to initialize the view. 231 * This is called by the <code>setParent</code> method. 232 * This is reimplemented to not load any children directly 233 * (as they are created by the zones). This method creates 234 * the initial set of zones. Zones don't actually get 235 * populated however until an attempt is made to display 236 * them or to do model/view coordinate translation. 237 * 238 * @param f the view factory 239 */ 240 protected void loadChildren(ViewFactory f) { 241 // build the first zone. 242 Document doc = getDocument(); 243 int offs0 = getStartOffset(); 244 int offs1 = getEndOffset(); 245 append(createZone(offs0, offs1)); 246 handleInsert(offs0, offs1 - offs0); 247 } 248 249 /** 250 * Returns the child view index representing the given position in 251 * the model. 252 * 253 * @param pos the position >= 0 254 * @return index of the view representing the given position, or 255 * -1 if no view represents that position 256 */ 257 protected int getViewIndexAtPosition(int pos) { 258 // PENDING(prinz) this could be done as a binary 259 // search, and probably should be. 260 int n = getViewCount(); 261 if (pos == getEndOffset()) { 262 return n - 1; 263 } 264 for(int i = 0; i < n; i++) { 265 View v = getView(i); 266 if(pos >= v.getStartOffset() && 267 pos < v.getEndOffset()) { 268 return i; 269 } 270 } 271 return -1; 272 } 273 274 void handleInsert(int pos, int length) { 275 int index = getViewIndex(pos, Position.Bias.Forward); 276 View v = getView(index); 277 int offs0 = v.getStartOffset(); 278 int offs1 = v.getEndOffset(); 279 if ((offs1 - offs0) > maxZoneSize) { 280 splitZone(index, offs0, offs1); 281 } 282 } 283 284 void handleRemove(int pos, int length) { 285 // IMPLEMENT 286 } 287 288 /** 289 * Break up the zone at the given index into pieces 290 * of an acceptable size. 291 */ 292 void splitZone(int index, int offs0, int offs1) { 293 // divide the old zone into a new set of bins 294 Element elem = getElement(); 295 Document doc = elem.getDocument(); 296 Vector<View> zones = new Vector<View>(); 297 int offs = offs0; 298 do { 299 offs0 = offs; 300 offs = Math.min(getDesiredZoneEnd(offs0), offs1); 301 zones.addElement(createZone(offs0, offs)); 302 } while (offs < offs1); 303 View oldZone = getView(index); 304 View[] newZones = new View[zones.size()]; 305 zones.copyInto(newZones); 306 replace(index, 1, newZones); 307 } 308 309 /** 310 * Returns the zone position to use for the 311 * end of a zone that starts at the given 312 * position. By default this returns something 313 * close to half the max zone size. 314 */ 315 int getDesiredZoneEnd(int pos) { 316 Element elem = getElement(); 317 int index = elem.getElementIndex(pos + (maxZoneSize / 2)); 318 Element child = elem.getElement(index); 319 int offs0 = child.getStartOffset(); 320 int offs1 = child.getEndOffset(); 321 if ((offs1 - pos) > maxZoneSize) { 322 if (offs0 > pos) { 323 return offs0; 324 } 325 } 326 return offs1; 327 } 328 329 // ---- View methods ---------------------------------------------------- 330 331 /** 332 * The superclass behavior will try to update the child views 333 * which is not desired in this case, since the children are 334 * zones and not directly effected by the changes to the 335 * associated element. This is reimplemented to do nothing 336 * and return false. 337 */ 338 protected boolean updateChildren(DocumentEvent.ElementChange ec, 339 DocumentEvent e, ViewFactory f) { 340 return false; 341 } 342 343 /** 344 * Gives notification that something was inserted into the document 345 * in a location that this view is responsible for. This is largely 346 * delegated to the superclass, but is reimplemented to update the 347 * relevant zone (i.e. determine if a zone needs to be split into a 348 * set of 2 or more zones). 349 * 350 * @param changes the change information from the associated document 351 * @param a the current allocation of the view 352 * @param f the factory to use to rebuild if the view has children 353 * @see View#insertUpdate 354 */ 355 public void insertUpdate(DocumentEvent changes, Shape a, ViewFactory f) { 356 handleInsert(changes.getOffset(), changes.getLength()); 357 super.insertUpdate(changes, a, f); 358 } 359 360 /** 361 * Gives notification that something was removed from the document 362 * in a location that this view is responsible for. This is largely 363 * delegated to the superclass, but is reimplemented to update the 364 * relevant zones (i.e. determine if zones need to be removed or 365 * joined with another zone). 366 * 367 * @param changes the change information from the associated document 368 * @param a the current allocation of the view 369 * @param f the factory to use to rebuild if the view has children 370 * @see View#removeUpdate 371 */ 372 public void removeUpdate(DocumentEvent changes, Shape a, ViewFactory f) { 373 handleRemove(changes.getOffset(), changes.getLength()); 374 super.removeUpdate(changes, a, f); 375 } 376 377 /** 378 * Internally created view that has the purpose of holding 379 * the views that represent the children of the ZoneView 380 * that have been arranged in a zone. 381 */ 382 class Zone extends AsyncBoxView { 383 384 private Position start; 385 private Position end; 386 387 public Zone(Element elem, Position start, Position end) { 388 super(elem, ZoneView.this.getAxis()); 389 this.start = start; 390 this.end = end; 391 } 392 393 /** 394 * Creates the child views and populates the 395 * zone with them. This is done by translating 396 * the positions to child element index locations 397 * and building views to those elements. If the 398 * zone is already loaded, this does nothing. 399 */ 400 public void load() { 401 if (! isLoaded()) { 402 setEstimatedMajorSpan(true); 403 Element e = getElement(); 404 ViewFactory f = getViewFactory(); 405 int index0 = e.getElementIndex(getStartOffset()); 406 int index1 = e.getElementIndex(getEndOffset()); 407 View[] added = new View[index1 - index0 + 1]; 408 for (int i = index0; i <= index1; i++) { 409 added[i - index0] = f.create(e.getElement(i)); 410 } 411 replace(0, 0, added); 412 413 zoneWasLoaded(this); 414 } 415 } 416 417 /** 418 * Removes the child views and returns to a 419 * state of unloaded. 420 */ 421 public void unload() { 422 setEstimatedMajorSpan(true); 423 removeAll(); 424 } 425 426 /** 427 * Determines if the zone is in the loaded state 428 * or not. 429 */ 430 public boolean isLoaded() { 431 return (getViewCount() != 0); 432 } 433 434 /** 435 * This method is reimplemented to not build the children 436 * since the children are created when the zone is loaded 437 * rather then when it is placed in the view hierarchy. 438 * The major span is estimated at this point by building 439 * the first child (but not storing it), and calling 440 * setEstimatedMajorSpan(true) followed by setSpan for 441 * the major axis with the estimated span. 442 */ 443 protected void loadChildren(ViewFactory f) { 444 // mark the major span as estimated 445 setEstimatedMajorSpan(true); 446 447 // estimate the span 448 Element elem = getElement(); 449 int index0 = elem.getElementIndex(getStartOffset()); 450 int index1 = elem.getElementIndex(getEndOffset()); 451 int nChildren = index1 - index0; 452 453 // replace this with something real 454 //setSpan(getMajorAxis(), nChildren * 10); 455 456 View first = f.create(elem.getElement(index0)); 457 first.setParent(this); 458 float w = first.getPreferredSpan(X_AXIS); 459 float h = first.getPreferredSpan(Y_AXIS); 460 if (getMajorAxis() == X_AXIS) { 461 w *= nChildren; 462 } else { 463 h += nChildren; 464 } 465 466 setSize(w, h); 467 } 468 469 /** 470 * Publish the changes in preferences upward to the parent 471 * view. 472 * <p> 473 * This is reimplemented to stop the superclass behavior 474 * if the zone has not yet been loaded. If the zone is 475 * unloaded for example, the last seen major span is the 476 * best estimate and a calculated span for no children 477 * is undesirable. 478 */ 479 protected void flushRequirementChanges() { 480 if (isLoaded()) { 481 super.flushRequirementChanges(); 482 } 483 } 484 485 /** 486 * Returns the child view index representing the given position in 487 * the model. Since the zone contains a cluster of the overall 488 * set of child elements, we can determine the index fairly 489 * quickly from the model by subtracting the index of the 490 * start offset from the index of the position given. 491 * 492 * @param pos the position >= 0 493 * @return index of the view representing the given position, or 494 * -1 if no view represents that position 495 * @since 1.3 496 */ 497 public int getViewIndex(int pos, Position.Bias b) { 498 boolean isBackward = (b == Position.Bias.Backward); 499 pos = (isBackward) ? Math.max(0, pos - 1) : pos; 500 Element elem = getElement(); 501 int index1 = elem.getElementIndex(pos); 502 int index0 = elem.getElementIndex(getStartOffset()); 503 return index1 - index0; 504 } 505 506 protected boolean updateChildren(DocumentEvent.ElementChange ec, 507 DocumentEvent e, ViewFactory f) { 508 // the structure of this element changed. 509 Element[] removedElems = ec.getChildrenRemoved(); 510 Element[] addedElems = ec.getChildrenAdded(); 511 Element elem = getElement(); 512 int index0 = elem.getElementIndex(getStartOffset()); 513 int index1 = elem.getElementIndex(getEndOffset()-1); 514 int index = ec.getIndex(); 515 if ((index >= index0) && (index <= index1)) { 516 // The change is in this zone 517 int replaceIndex = index - index0; 518 int nadd = Math.min(index1 - index0 + 1, addedElems.length); 519 int nremove = Math.min(index1 - index0 + 1, removedElems.length); 520 View[] added = new View[nadd]; 521 for (int i = 0; i < nadd; i++) { 522 added[i] = f.create(addedElems[i]); 523 } 524 replace(replaceIndex, nremove, added); 525 } 526 return true; 527 } 528 529 // --- View methods ---------------------------------- 530 531 /** 532 * Fetches the attributes to use when rendering. This view 533 * isn't directly responsible for an element so it returns 534 * the outer classes attributes. 535 */ 536 public AttributeSet getAttributes() { 537 return ZoneView.this.getAttributes(); 538 } 539 540 /** 541 * Renders using the given rendering surface and area on that 542 * surface. This is implemented to load the zone if its not 543 * already loaded, and then perform the superclass behavior. 544 * 545 * @param g the rendering surface to use 546 * @param a the allocated region to render into 547 * @see View#paint 548 */ 549 public void paint(Graphics g, Shape a) { 550 load(); 551 super.paint(g, a); 552 } 553 554 /** 555 * Provides a mapping from the view coordinate space to the logical 556 * coordinate space of the model. This is implemented to first 557 * make sure the zone is loaded before providing the superclass 558 * behavior. 559 * 560 * @param x x coordinate of the view location to convert >= 0 561 * @param y y coordinate of the view location to convert >= 0 562 * @param a the allocated region to render into 563 * @return the location within the model that best represents the 564 * given point in the view >= 0 565 * @see View#viewToModel 566 */ 567 public int viewToModel(float x, float y, Shape a, Position.Bias[] bias) { 568 load(); 569 return super.viewToModel(x, y, a, bias); 570 } 571 572 /** 573 * Provides a mapping from the document model coordinate space 574 * to the coordinate space of the view mapped to it. This is 575 * implemented to provide the superclass behavior after first 576 * making sure the zone is loaded (The zone must be loaded to 577 * make this calculation). 578 * 579 * @param pos the position to convert 580 * @param a the allocated region to render into 581 * @return the bounding box of the given position 582 * @exception BadLocationException if the given position does not represent a 583 * valid location in the associated document 584 * @see View#modelToView 585 */ 586 public Shape modelToView(int pos, Shape a, Position.Bias b) throws BadLocationException { 587 load(); 588 return super.modelToView(pos, a, b); 589 } 590 591 /** 592 * Start of the zones range. 593 * 594 * @see View#getStartOffset 595 */ 596 public int getStartOffset() { 597 return start.getOffset(); 598 } 599 600 /** 601 * End of the zones range. 602 */ 603 public int getEndOffset() { 604 return end.getOffset(); 605 } 606 607 /** 608 * Gives notification that something was inserted into 609 * the document in a location that this view is responsible for. 610 * If the zone has been loaded, the superclass behavior is 611 * invoked, otherwise this does nothing. 612 * 613 * @param e the change information from the associated document 614 * @param a the current allocation of the view 615 * @param f the factory to use to rebuild if the view has children 616 * @see View#insertUpdate 617 */ 618 public void insertUpdate(DocumentEvent e, Shape a, ViewFactory f) { 619 if (isLoaded()) { 620 super.insertUpdate(e, a, f); 621 } 622 } 623 624 /** 625 * Gives notification that something was removed from the document 626 * in a location that this view is responsible for. 627 * If the zone has been loaded, the superclass behavior is 628 * invoked, otherwise this does nothing. 629 * 630 * @param e the change information from the associated document 631 * @param a the current allocation of the view 632 * @param f the factory to use to rebuild if the view has children 633 * @see View#removeUpdate 634 */ 635 public void removeUpdate(DocumentEvent e, Shape a, ViewFactory f) { 636 if (isLoaded()) { 637 super.removeUpdate(e, a, f); 638 } 639 } 640 641 /** 642 * Gives notification from the document that attributes were changed 643 * in a location that this view is responsible for. 644 * If the zone has been loaded, the superclass behavior is 645 * invoked, otherwise this does nothing. 646 * 647 * @param e the change information from the associated document 648 * @param a the current allocation of the view 649 * @param f the factory to use to rebuild if the view has children 650 * @see View#removeUpdate 651 */ 652 public void changedUpdate(DocumentEvent e, Shape a, ViewFactory f) { 653 if (isLoaded()) { 654 super.changedUpdate(e, a, f); 655 } 656 } 657 658 } 659} 660