1 /* 2 * Copyright (c) 1998, 2013, 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 */ 25 package javax.swing.text; 26 27 import java.util.Vector; 28 import java.awt.*; 29 import 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 */ 78 public 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 */ 98 public int getMaximumZoneSize() { 99 return maxZoneSize; 100 } 101 102 /** 103 * Set the desired maximum zone size. A 104 * zone may get larger than this size if 105 * a single child view is larger than this 106 * size since zones are formed on child view 107 * boundaries. 108 * 109 * @param size the number of characters the zone 110 * may represent before attempting to break 111 * the zone into a smaller size. 112 */ 113 public void setMaximumZoneSize(int size) { 114 maxZoneSize = size; 115 } 116 117 /** 118 * Get the current setting of the number of zones 119 * allowed to be loaded at the same time. 120 */ 121 public int getMaxZonesLoaded() { 122 return maxZonesLoaded; 123 } 124 125 /** 126 * Sets the current setting of the number of zones 127 * allowed to be loaded at the same time. This will throw an 128 * <code>IllegalArgumentException</code> if <code>mzl</code> is less 129 * than 1. 130 * 131 * @param mzl the desired maximum number of zones 132 * to be actively loaded, must be greater than 0 133 * @exception IllegalArgumentException if <code>mzl</code> is < 1 134 */ 135 public void setMaxZonesLoaded(int mzl) { 136 if (mzl < 1) { 137 throw new IllegalArgumentException("ZoneView.setMaxZonesLoaded must be greater than 0."); 138 } 139 maxZonesLoaded = mzl; 140 unloadOldZones(); 141 } 142 143 /** 144 * Called by a zone when it gets loaded. This happens when 145 * an attempt is made to display or perform a model/view 146 * translation on a zone that was in an unloaded state. 147 * This is implemented to check if the maximum number of 148 * zones was reached and to unload the oldest zone if so. 149 * 150 * @param zone the child view that was just loaded. 151 */ 152 protected void zoneWasLoaded(View zone) { 153 //System.out.println("loading: " + zone.getStartOffset() + "," + zone.getEndOffset()); 154 loadedZones.addElement(zone); 155 unloadOldZones(); 156 } 157 158 void unloadOldZones() { 159 while (loadedZones.size() > getMaxZonesLoaded()) { 160 View zone = loadedZones.elementAt(0); 161 loadedZones.removeElementAt(0); 162 unloadZone(zone); 163 } 164 } 165 166 /** 167 * Unload a zone (Convert the zone to its memory saving state). 168 * The zones are expected to represent a subset of the 169 * child elements of the element this view is responsible for. 170 * Therefore, the default implementation is to simple remove 171 * all the children. 172 * 173 * @param zone the child view desired to be set to an 174 * unloaded state. 175 */ 176 protected void unloadZone(View zone) { 177 //System.out.println("unloading: " + zone.getStartOffset() + "," + zone.getEndOffset()); 178 zone.removeAll(); 179 } 180 181 /** 182 * Determine if a zone is in the loaded state. 183 * The zones are expected to represent a subset of the 184 * child elements of the element this view is responsible for. 185 * Therefore, the default implementation is to return 186 * true if the view has children. 187 */ 188 protected boolean isZoneLoaded(View zone) { 189 return (zone.getViewCount() > 0); 190 } 191 192 /** 193 * Create a view to represent a zone for the given 194 * range within the model (which should be within 195 * the range of this objects responsibility). This 196 * is called by the zone management logic to create 197 * new zones. Subclasses can provide a different 198 * implementation for a zone by changing this method. 199 * 200 * @param p0 the start of the desired zone. This should 201 * be >= getStartOffset() and < getEndOffset(). This 202 * value should also be < p1. 203 * @param p1 the end of the desired zone. This should 204 * be > getStartOffset() and <= getEndOffset(). This 205 * value should also be > p0. 206 */ 207 protected View createZone(int p0, int p1) { 208 Document doc = getDocument(); 209 View zone; 210 try { 211 zone = new Zone(getElement(), 212 doc.createPosition(p0), 213 doc.createPosition(p1)); 214 } catch (BadLocationException ble) { 215 // this should puke in some way. 216 throw new StateInvariantError(ble.getMessage()); 217 } 218 return zone; 219 } 220 221 /** 222 * Loads all of the children to initialize the view. 223 * This is called by the <code>setParent</code> method. 224 * This is reimplemented to not load any children directly 225 * (as they are created by the zones). This method creates 226 * the initial set of zones. Zones don't actually get 227 * populated however until an attempt is made to display 228 * them or to do model/view coordinate translation. 229 * 230 * @param f the view factory 231 */ 232 protected void loadChildren(ViewFactory f) { 233 // build the first zone. 234 Document doc = getDocument(); 235 int offs0 = getStartOffset(); 236 int offs1 = getEndOffset(); 237 append(createZone(offs0, offs1)); 238 handleInsert(offs0, offs1 - offs0); 239 } 240 241 /** 242 * Returns the child view index representing the given position in 243 * the model. 244 * 245 * @param pos the position >= 0 246 * @return index of the view representing the given position, or 247 * -1 if no view represents that position 248 */ 249 protected int getViewIndexAtPosition(int pos) { 250 // PENDING(prinz) this could be done as a binary 251 // search, and probably should be. 252 int n = getViewCount(); 253 if (pos == getEndOffset()) { 254 return n - 1; 255 } 256 for(int i = 0; i < n; i++) { 257 View v = getView(i); 258 if(pos >= v.getStartOffset() && 259 pos < v.getEndOffset()) { 260 return i; 261 } 262 } 263 return -1; 264 } 265 266 void handleInsert(int pos, int length) { 267 int index = getViewIndex(pos, Position.Bias.Forward); 268 View v = getView(index); 269 int offs0 = v.getStartOffset(); 270 int offs1 = v.getEndOffset(); 271 if ((offs1 - offs0) > maxZoneSize) { 272 splitZone(index, offs0, offs1); 273 } 274 } 275 276 void handleRemove(int pos, int length) { 277 // IMPLEMENT 278 } 279 280 /** 281 * Break up the zone at the given index into pieces 282 * of an acceptable size. 283 */ 284 void splitZone(int index, int offs0, int offs1) { 285 // divide the old zone into a new set of bins 286 Element elem = getElement(); 287 Document doc = elem.getDocument(); 288 Vector<View> zones = new Vector<View>(); 289 int offs = offs0; 290 do { 291 offs0 = offs; 292 offs = Math.min(getDesiredZoneEnd(offs0), offs1); 293 zones.addElement(createZone(offs0, offs)); 294 } while (offs < offs1); 295 View oldZone = getView(index); 296 View[] newZones = new View[zones.size()]; 297 zones.copyInto(newZones); 298 replace(index, 1, newZones); 299 } 300 301 /** 302 * Returns the zone position to use for the 303 * end of a zone that starts at the given 304 * position. By default this returns something 305 * close to half the max zone size. 306 */ 307 int getDesiredZoneEnd(int pos) { 308 Element elem = getElement(); 309 int index = elem.getElementIndex(pos + (maxZoneSize / 2)); 310 Element child = elem.getElement(index); 311 int offs0 = child.getStartOffset(); 312 int offs1 = child.getEndOffset(); 313 if ((offs1 - pos) > maxZoneSize) { 314 if (offs0 > pos) { 315 return offs0; 316 } 317 } 318 return offs1; 319 } 320 321 // ---- View methods ---------------------------------------------------- 322 323 /** 324 * The superclass behavior will try to update the child views 325 * which is not desired in this case, since the children are 326 * zones and not directly effected by the changes to the 327 * associated element. This is reimplemented to do nothing 328 * and return false. 329 */ 330 protected boolean updateChildren(DocumentEvent.ElementChange ec, 331 DocumentEvent e, ViewFactory f) { 332 return false; 333 } 334 335 /** 336 * Gives notification that something was inserted into the document 337 * in a location that this view is responsible for. This is largely 338 * delegated to the superclass, but is reimplemented to update the 339 * relevant zone (i.e. determine if a zone needs to be split into a 340 * set of 2 or more zones). 341 * 342 * @param changes the change information from the associated document 343 * @param a the current allocation of the view 344 * @param f the factory to use to rebuild if the view has children 345 * @see View#insertUpdate 346 */ 347 public void insertUpdate(DocumentEvent changes, Shape a, ViewFactory f) { 348 handleInsert(changes.getOffset(), changes.getLength()); 349 super.insertUpdate(changes, a, f); 350 } 351 352 /** 353 * Gives notification that something was removed from the document 354 * in a location that this view is responsible for. This is largely 355 * delegated to the superclass, but is reimplemented to update the 356 * relevant zones (i.e. determine if zones need to be removed or 357 * joined with another zone). 358 * 359 * @param changes the change information from the associated document 360 * @param a the current allocation of the view 361 * @param f the factory to use to rebuild if the view has children 362 * @see View#removeUpdate 363 */ 364 public void removeUpdate(DocumentEvent changes, Shape a, ViewFactory f) { 365 handleRemove(changes.getOffset(), changes.getLength()); 366 super.removeUpdate(changes, a, f); 367 } 368 369 /** 370 * Internally created view that has the purpose of holding 371 * the views that represent the children of the ZoneView 372 * that have been arranged in a zone. 373 */ 374 class Zone extends AsyncBoxView { 375 376 private Position start; 377 private Position end; 378 379 public Zone(Element elem, Position start, Position end) { 380 super(elem, ZoneView.this.getAxis()); 381 this.start = start; 382 this.end = end; 383 } 384 385 /** 386 * Creates the child views and populates the 387 * zone with them. This is done by translating 388 * the positions to child element index locations 389 * and building views to those elements. If the 390 * zone is already loaded, this does nothing. 391 */ 392 public void load() { 393 if (! isLoaded()) { 394 setEstimatedMajorSpan(true); 395 Element e = getElement(); 396 ViewFactory f = getViewFactory(); 397 int index0 = e.getElementIndex(getStartOffset()); 398 int index1 = e.getElementIndex(getEndOffset()); 399 View[] added = new View[index1 - index0 + 1]; 400 for (int i = index0; i <= index1; i++) { 401 added[i - index0] = f.create(e.getElement(i)); 402 } 403 replace(0, 0, added); 404 405 zoneWasLoaded(this); 406 } 407 } 408 409 /** 410 * Removes the child views and returns to a 411 * state of unloaded. 412 */ 413 public void unload() { 414 setEstimatedMajorSpan(true); 415 removeAll(); 416 } 417 418 /** 419 * Determines if the zone is in the loaded state 420 * or not. 421 */ 422 public boolean isLoaded() { 423 return (getViewCount() != 0); 424 } 425 426 /** 427 * This method is reimplemented to not build the children 428 * since the children are created when the zone is loaded 429 * rather then when it is placed in the view hierarchy. 430 * The major span is estimated at this point by building 431 * the first child (but not storing it), and calling 432 * setEstimatedMajorSpan(true) followed by setSpan for 433 * the major axis with the estimated span. 434 */ 435 protected void loadChildren(ViewFactory f) { 436 // mark the major span as estimated 437 setEstimatedMajorSpan(true); 438 439 // estimate the span 440 Element elem = getElement(); 441 int index0 = elem.getElementIndex(getStartOffset()); 442 int index1 = elem.getElementIndex(getEndOffset()); 443 int nChildren = index1 - index0; 444 445 // replace this with something real 446 //setSpan(getMajorAxis(), nChildren * 10); 447 448 View first = f.create(elem.getElement(index0)); 449 first.setParent(this); 450 float w = first.getPreferredSpan(X_AXIS); 451 float h = first.getPreferredSpan(Y_AXIS); 452 if (getMajorAxis() == X_AXIS) { 453 w *= nChildren; 454 } else { 455 h += nChildren; 456 } 457 458 setSize(w, h); 459 } 460 461 /** 462 * Publish the changes in preferences upward to the parent 463 * view. 464 * <p> 465 * This is reimplemented to stop the superclass behavior 466 * if the zone has not yet been loaded. If the zone is 467 * unloaded for example, the last seen major span is the 468 * best estimate and a calculated span for no children 469 * is undesirable. 470 */ 471 protected void flushRequirementChanges() { 472 if (isLoaded()) { 473 super.flushRequirementChanges(); 474 } 475 } 476 477 /** 478 * Returns the child view index representing the given position in 479 * the model. Since the zone contains a cluster of the overall 480 * set of child elements, we can determine the index fairly 481 * quickly from the model by subtracting the index of the 482 * start offset from the index of the position given. 483 * 484 * @param pos the position >= 0 485 * @return index of the view representing the given position, or 486 * -1 if no view represents that position 487 * @since 1.3 488 */ 489 public int getViewIndex(int pos, Position.Bias b) { 490 boolean isBackward = (b == Position.Bias.Backward); 491 pos = (isBackward) ? Math.max(0, pos - 1) : pos; 492 Element elem = getElement(); 493 int index1 = elem.getElementIndex(pos); 494 int index0 = elem.getElementIndex(getStartOffset()); 495 return index1 - index0; 496 } 497 498 protected boolean updateChildren(DocumentEvent.ElementChange ec, 499 DocumentEvent e, ViewFactory f) { 500 // the structure of this element changed. 501 Element[] removedElems = ec.getChildrenRemoved(); 502 Element[] addedElems = ec.getChildrenAdded(); 503 Element elem = getElement(); 504 int index0 = elem.getElementIndex(getStartOffset()); 505 int index1 = elem.getElementIndex(getEndOffset()-1); 506 int index = ec.getIndex(); 507 if ((index >= index0) && (index <= index1)) { 508 // The change is in this zone 509 int replaceIndex = index - index0; 510 int nadd = Math.min(index1 - index0 + 1, addedElems.length); 511 int nremove = Math.min(index1 - index0 + 1, removedElems.length); 512 View[] added = new View[nadd]; 513 for (int i = 0; i < nadd; i++) { 514 added[i] = f.create(addedElems[i]); 515 } 516 replace(replaceIndex, nremove, added); 517 } 518 return true; 519 } 520 521 // --- View methods ---------------------------------- 522 523 /** 524 * Fetches the attributes to use when rendering. This view 525 * isn't directly responsible for an element so it returns 526 * the outer classes attributes. 527 */ 528 public AttributeSet getAttributes() { 529 return ZoneView.this.getAttributes(); 530 } 531 532 /** 533 * Renders using the given rendering surface and area on that 534 * surface. This is implemented to load the zone if its not 535 * already loaded, and then perform the superclass behavior. 536 * 537 * @param g the rendering surface to use 538 * @param a the allocated region to render into 539 * @see View#paint 540 */ 541 public void paint(Graphics g, Shape a) { 542 load(); 543 super.paint(g, a); 544 } 545 546 /** 547 * Provides a mapping from the view coordinate space to the logical 548 * coordinate space of the model. This is implemented to first 549 * make sure the zone is loaded before providing the superclass 550 * behavior. 551 * 552 * @param x x coordinate of the view location to convert >= 0 553 * @param y y coordinate of the view location to convert >= 0 554 * @param a the allocated region to render into 555 * @return the location within the model that best represents the 556 * given point in the view >= 0 557 * @see View#viewToModel 558 */ 559 public int viewToModel(float x, float y, Shape a, Position.Bias[] bias) { 560 load(); 561 return super.viewToModel(x, y, a, bias); 562 } 563 564 /** 565 * Provides a mapping from the document model coordinate space 566 * to the coordinate space of the view mapped to it. This is 567 * implemented to provide the superclass behavior after first 568 * making sure the zone is loaded (The zone must be loaded to 569 * make this calculation). 570 * 571 * @param pos the position to convert 572 * @param a the allocated region to render into 573 * @return the bounding box of the given position 574 * @exception BadLocationException if the given position does not represent a 575 * valid location in the associated document 576 * @see View#modelToView 577 */ 578 public Shape modelToView(int pos, Shape a, Position.Bias b) throws BadLocationException { 579 load(); 580 return super.modelToView(pos, a, b); 581 } 582 583 /** 584 * Start of the zones range. 585 * 586 * @see View#getStartOffset 587 */ 588 public int getStartOffset() { 589 return start.getOffset(); 590 } 591 592 /** 593 * End of the zones range. 594 */ 595 public int getEndOffset() { 596 return end.getOffset(); 597 } 598 599 /** 600 * Gives notification that something was inserted into 601 * the document in a location that this view is responsible for. 602 * If the zone has been loaded, the superclass behavior is 603 * invoked, otherwise this does nothing. 604 * 605 * @param e the change information from the associated document 606 * @param a the current allocation of the view 607 * @param f the factory to use to rebuild if the view has children 608 * @see View#insertUpdate 609 */ 610 public void insertUpdate(DocumentEvent e, Shape a, ViewFactory f) { 611 if (isLoaded()) { 612 super.insertUpdate(e, a, f); 613 } 614 } 615 616 /** 617 * Gives notification that something was removed from the document 618 * in a location that this view is responsible for. 619 * If the zone has been loaded, the superclass behavior is 620 * invoked, otherwise this does nothing. 621 * 622 * @param e the change information from the associated document 623 * @param a the current allocation of the view 624 * @param f the factory to use to rebuild if the view has children 625 * @see View#removeUpdate 626 */ 627 public void removeUpdate(DocumentEvent e, Shape a, ViewFactory f) { 628 if (isLoaded()) { 629 super.removeUpdate(e, a, f); 630 } 631 } 632 633 /** 634 * Gives notification from the document that attributes were changed 635 * in a location that this view is responsible for. 636 * If the zone has been loaded, the superclass behavior is 637 * invoked, otherwise this does nothing. 638 * 639 * @param e the change information from the associated document 640 * @param a the current allocation of the view 641 * @param f the factory to use to rebuild if the view has children 642 * @see View#removeUpdate 643 */ 644 public void changedUpdate(DocumentEvent e, Shape a, ViewFactory f) { 645 if (isLoaded()) { 646 super.changedUpdate(e, a, f); 647 } 648 } 649 650 } 651 }