| /* |
| * Copyright (c) 1998, 2008, Oracle and/or its affiliates. All rights reserved. |
| * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. |
| * |
| * This code is free software; you can redistribute it and/or modify it |
| * under the terms of the GNU General Public License version 2 only, as |
| * published by the Free Software Foundation. Oracle designates this |
| * particular file as subject to the "Classpath" exception as provided |
| * by Oracle in the LICENSE file that accompanied this code. |
| * |
| * This code is distributed in the hope that it will be useful, but WITHOUT |
| * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or |
| * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License |
| * version 2 for more details (a copy is included in the LICENSE file that |
| * accompanied this code). |
| * |
| * You should have received a copy of the GNU General Public License version |
| * 2 along with this work; if not, write to the Free Software Foundation, |
| * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. |
| * |
| * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA |
| * or visit www.oracle.com if you need additional information or have any |
| * questions. |
| */ |
| package javax.swing.text; |
| |
| import java.util.Vector; |
| import java.awt.*; |
| import javax.swing.event.*; |
| |
| /** |
| * ZoneView is a View implementation that creates zones for which |
| * the child views are not created or stored until they are needed |
| * for display or model/view translations. This enables a substantial |
| * reduction in memory consumption for situations where the model |
| * being represented is very large, by building view objects only for |
| * the region being actively viewed/edited. The size of the children |
| * can be estimated in some way, or calculated asynchronously with |
| * only the result being saved. |
| * <p> |
| * ZoneView extends BoxView to provide a box that implements |
| * zones for its children. The zones are special View implementations |
| * (the children of an instance of this class) that represent only a |
| * portion of the model that an instance of ZoneView is responsible |
| * for. The zones don't create child views until an attempt is made |
| * to display them. A box shaped view is well suited to this because: |
| * <ul> |
| * <li> |
| * Boxes are a heavily used view, and having a box that |
| * provides this behavior gives substantial opportunity |
| * to plug the behavior into a view hierarchy from the |
| * view factory. |
| * <li> |
| * Boxes are tiled in one direction, so it is easy to |
| * divide them into zones in a reliable way. |
| * <li> |
| * Boxes typically have a simple relationship to the model (i.e. they |
| * create child views that directly represent the child elements). |
| * <li> |
| * Boxes are easier to estimate the size of than some other shapes. |
| * </ul> |
| * <p> |
| * The default behavior is controled by two properties, maxZoneSize |
| * and maxZonesLoaded. Setting maxZoneSize to Integer.MAX_VALUE would |
| * have the effect of causing only one zone to be created. This would |
| * effectively turn the view into an implementation of the decorator |
| * pattern. Setting maxZonesLoaded to a value of Integer.MAX_VALUE would |
| * cause zones to never be unloaded. For simplicity, zones are created on |
| * boundaries represented by the child elements of the element the view is |
| * responsible for. The zones can be any View implementation, but the |
| * default implementation is based upon AsyncBoxView which supports fairly |
| * large zones efficiently. |
| * |
| * @author Timothy Prinzing |
| * @see View |
| * @since 1.3 |
| */ |
| public class ZoneView extends BoxView { |
| |
| int maxZoneSize = 8 * 1024; |
| int maxZonesLoaded = 3; |
| Vector<View> loadedZones; |
| |
| /** |
| * Constructs a ZoneView. |
| * |
| * @param elem the element this view is responsible for |
| * @param axis either View.X_AXIS or View.Y_AXIS |
| */ |
| public ZoneView(Element elem, int axis) { |
| super(elem, axis); |
| loadedZones = new Vector<View>(); |
| } |
| |
| /** |
| * Get the current maximum zone size. |
| */ |
| public int getMaximumZoneSize() { |
| return maxZoneSize; |
| } |
| |
| /** |
| * Set the desired maximum zone size. A |
| * zone may get larger than this size if |
| * a single child view is larger than this |
| * size since zones are formed on child view |
| * boundaries. |
| * |
| * @param size the number of characters the zone |
| * may represent before attempting to break |
| * the zone into a smaller size. |
| */ |
| public void setMaximumZoneSize(int size) { |
| maxZoneSize = size; |
| } |
| |
| /** |
| * Get the current setting of the number of zones |
| * allowed to be loaded at the same time. |
| */ |
| public int getMaxZonesLoaded() { |
| return maxZonesLoaded; |
| } |
| |
| /** |
| * Sets the current setting of the number of zones |
| * allowed to be loaded at the same time. This will throw an |
| * <code>IllegalArgumentException</code> if <code>mzl</code> is less |
| * than 1. |
| * |
| * @param mzl the desired maximum number of zones |
| * to be actively loaded, must be greater than 0 |
| * @exception IllegalArgumentException if <code>mzl</code> is < 1 |
| */ |
| public void setMaxZonesLoaded(int mzl) { |
| if (mzl < 1) { |
| throw new IllegalArgumentException("ZoneView.setMaxZonesLoaded must be greater than 0."); |
| } |
| maxZonesLoaded = mzl; |
| unloadOldZones(); |
| } |
| |
| /** |
| * Called by a zone when it gets loaded. This happens when |
| * an attempt is made to display or perform a model/view |
| * translation on a zone that was in an unloaded state. |
| * This is imlemented to check if the maximum number of |
| * zones was reached and to unload the oldest zone if so. |
| * |
| * @param zone the child view that was just loaded. |
| */ |
| protected void zoneWasLoaded(View zone) { |
| //System.out.println("loading: " + zone.getStartOffset() + "," + zone.getEndOffset()); |
| loadedZones.addElement(zone); |
| unloadOldZones(); |
| } |
| |
| void unloadOldZones() { |
| while (loadedZones.size() > getMaxZonesLoaded()) { |
| View zone = loadedZones.elementAt(0); |
| loadedZones.removeElementAt(0); |
| unloadZone(zone); |
| } |
| } |
| |
| /** |
| * Unload a zone (Convert the zone to its memory saving state). |
| * The zones are expected to represent a subset of the |
| * child elements of the element this view is responsible for. |
| * Therefore, the default implementation is to simple remove |
| * all the children. |
| * |
| * @param zone the child view desired to be set to an |
| * unloaded state. |
| */ |
| protected void unloadZone(View zone) { |
| //System.out.println("unloading: " + zone.getStartOffset() + "," + zone.getEndOffset()); |
| zone.removeAll(); |
| } |
| |
| /** |
| * Determine if a zone is in the loaded state. |
| * The zones are expected to represent a subset of the |
| * child elements of the element this view is responsible for. |
| * Therefore, the default implementation is to return |
| * true if the view has children. |
| */ |
| protected boolean isZoneLoaded(View zone) { |
| return (zone.getViewCount() > 0); |
| } |
| |
| /** |
| * Create a view to represent a zone for the given |
| * range within the model (which should be within |
| * the range of this objects responsibility). This |
| * is called by the zone management logic to create |
| * new zones. Subclasses can provide a different |
| * implementation for a zone by changing this method. |
| * |
| * @param p0 the start of the desired zone. This should |
| * be >= getStartOffset() and < getEndOffset(). This |
| * value should also be < p1. |
| * @param p1 the end of the desired zone. This should |
| * be > getStartOffset() and <= getEndOffset(). This |
| * value should also be > p0. |
| */ |
| protected View createZone(int p0, int p1) { |
| Document doc = getDocument(); |
| View zone; |
| try { |
| zone = new Zone(getElement(), |
| doc.createPosition(p0), |
| doc.createPosition(p1)); |
| } catch (BadLocationException ble) { |
| // this should puke in some way. |
| throw new StateInvariantError(ble.getMessage()); |
| } |
| return zone; |
| } |
| |
| /** |
| * Loads all of the children to initialize the view. |
| * This is called by the <code>setParent</code> method. |
| * This is reimplemented to not load any children directly |
| * (as they are created by the zones). This method creates |
| * the initial set of zones. Zones don't actually get |
| * populated however until an attempt is made to display |
| * them or to do model/view coordinate translation. |
| * |
| * @param f the view factory |
| */ |
| protected void loadChildren(ViewFactory f) { |
| // build the first zone. |
| Document doc = getDocument(); |
| int offs0 = getStartOffset(); |
| int offs1 = getEndOffset(); |
| append(createZone(offs0, offs1)); |
| handleInsert(offs0, offs1 - offs0); |
| } |
| |
| /** |
| * Returns the child view index representing the given position in |
| * the model. |
| * |
| * @param pos the position >= 0 |
| * @return index of the view representing the given position, or |
| * -1 if no view represents that position |
| */ |
| protected int getViewIndexAtPosition(int pos) { |
| // PENDING(prinz) this could be done as a binary |
| // search, and probably should be. |
| int n = getViewCount(); |
| if (pos == getEndOffset()) { |
| return n - 1; |
| } |
| for(int i = 0; i < n; i++) { |
| View v = getView(i); |
| if(pos >= v.getStartOffset() && |
| pos < v.getEndOffset()) { |
| return i; |
| } |
| } |
| return -1; |
| } |
| |
| void handleInsert(int pos, int length) { |
| int index = getViewIndex(pos, Position.Bias.Forward); |
| View v = getView(index); |
| int offs0 = v.getStartOffset(); |
| int offs1 = v.getEndOffset(); |
| if ((offs1 - offs0) > maxZoneSize) { |
| splitZone(index, offs0, offs1); |
| } |
| } |
| |
| void handleRemove(int pos, int length) { |
| // IMPLEMENT |
| } |
| |
| /** |
| * Break up the zone at the given index into pieces |
| * of an acceptable size. |
| */ |
| void splitZone(int index, int offs0, int offs1) { |
| // divide the old zone into a new set of bins |
| Element elem = getElement(); |
| Document doc = elem.getDocument(); |
| Vector<View> zones = new Vector<View>(); |
| int offs = offs0; |
| do { |
| offs0 = offs; |
| offs = Math.min(getDesiredZoneEnd(offs0), offs1); |
| zones.addElement(createZone(offs0, offs)); |
| } while (offs < offs1); |
| View oldZone = getView(index); |
| View[] newZones = new View[zones.size()]; |
| zones.copyInto(newZones); |
| replace(index, 1, newZones); |
| } |
| |
| /** |
| * Returns the zone position to use for the |
| * end of a zone that starts at the given |
| * position. By default this returns something |
| * close to half the max zone size. |
| */ |
| int getDesiredZoneEnd(int pos) { |
| Element elem = getElement(); |
| int index = elem.getElementIndex(pos + (maxZoneSize / 2)); |
| Element child = elem.getElement(index); |
| int offs0 = child.getStartOffset(); |
| int offs1 = child.getEndOffset(); |
| if ((offs1 - pos) > maxZoneSize) { |
| if (offs0 > pos) { |
| return offs0; |
| } |
| } |
| return offs1; |
| } |
| |
| // ---- View methods ---------------------------------------------------- |
| |
| /** |
| * The superclass behavior will try to update the child views |
| * which is not desired in this case, since the children are |
| * zones and not directly effected by the changes to the |
| * associated element. This is reimplemented to do nothing |
| * and return false. |
| */ |
| protected boolean updateChildren(DocumentEvent.ElementChange ec, |
| DocumentEvent e, ViewFactory f) { |
| return false; |
| } |
| |
| /** |
| * Gives notification that something was inserted into the document |
| * in a location that this view is responsible for. This is largely |
| * delegated to the superclass, but is reimplemented to update the |
| * relevant zone (i.e. determine if a zone needs to be split into a |
| * set of 2 or more zones). |
| * |
| * @param changes the change information from the associated document |
| * @param a the current allocation of the view |
| * @param f the factory to use to rebuild if the view has children |
| * @see View#insertUpdate |
| */ |
| public void insertUpdate(DocumentEvent changes, Shape a, ViewFactory f) { |
| handleInsert(changes.getOffset(), changes.getLength()); |
| super.insertUpdate(changes, a, f); |
| } |
| |
| /** |
| * Gives notification that something was removed from the document |
| * in a location that this view is responsible for. This is largely |
| * delegated to the superclass, but is reimplemented to update the |
| * relevant zones (i.e. determine if zones need to be removed or |
| * joined with another zone). |
| * |
| * @param changes the change information from the associated document |
| * @param a the current allocation of the view |
| * @param f the factory to use to rebuild if the view has children |
| * @see View#removeUpdate |
| */ |
| public void removeUpdate(DocumentEvent changes, Shape a, ViewFactory f) { |
| handleRemove(changes.getOffset(), changes.getLength()); |
| super.removeUpdate(changes, a, f); |
| } |
| |
| /** |
| * Internally created view that has the purpose of holding |
| * the views that represent the children of the ZoneView |
| * that have been arranged in a zone. |
| */ |
| class Zone extends AsyncBoxView { |
| |
| private Position start; |
| private Position end; |
| |
| public Zone(Element elem, Position start, Position end) { |
| super(elem, ZoneView.this.getAxis()); |
| this.start = start; |
| this.end = end; |
| } |
| |
| /** |
| * Creates the child views and populates the |
| * zone with them. This is done by translating |
| * the positions to child element index locations |
| * and building views to those elements. If the |
| * zone is already loaded, this does nothing. |
| */ |
| public void load() { |
| if (! isLoaded()) { |
| setEstimatedMajorSpan(true); |
| Element e = getElement(); |
| ViewFactory f = getViewFactory(); |
| int index0 = e.getElementIndex(getStartOffset()); |
| int index1 = e.getElementIndex(getEndOffset()); |
| View[] added = new View[index1 - index0 + 1]; |
| for (int i = index0; i <= index1; i++) { |
| added[i - index0] = f.create(e.getElement(i)); |
| } |
| replace(0, 0, added); |
| |
| zoneWasLoaded(this); |
| } |
| } |
| |
| /** |
| * Removes the child views and returns to a |
| * state of unloaded. |
| */ |
| public void unload() { |
| setEstimatedMajorSpan(true); |
| removeAll(); |
| } |
| |
| /** |
| * Determines if the zone is in the loaded state |
| * or not. |
| */ |
| public boolean isLoaded() { |
| return (getViewCount() != 0); |
| } |
| |
| /** |
| * This method is reimplemented to not build the children |
| * since the children are created when the zone is loaded |
| * rather then when it is placed in the view hierarchy. |
| * The major span is estimated at this point by building |
| * the first child (but not storing it), and calling |
| * setEstimatedMajorSpan(true) followed by setSpan for |
| * the major axis with the estimated span. |
| */ |
| protected void loadChildren(ViewFactory f) { |
| // mark the major span as estimated |
| setEstimatedMajorSpan(true); |
| |
| // estimate the span |
| Element elem = getElement(); |
| int index0 = elem.getElementIndex(getStartOffset()); |
| int index1 = elem.getElementIndex(getEndOffset()); |
| int nChildren = index1 - index0; |
| |
| // replace this with something real |
| //setSpan(getMajorAxis(), nChildren * 10); |
| |
| View first = f.create(elem.getElement(index0)); |
| first.setParent(this); |
| float w = first.getPreferredSpan(X_AXIS); |
| float h = first.getPreferredSpan(Y_AXIS); |
| if (getMajorAxis() == X_AXIS) { |
| w *= nChildren; |
| } else { |
| h += nChildren; |
| } |
| |
| setSize(w, h); |
| } |
| |
| /** |
| * Publish the changes in preferences upward to the parent |
| * view. |
| * <p> |
| * This is reimplemented to stop the superclass behavior |
| * if the zone has not yet been loaded. If the zone is |
| * unloaded for example, the last seen major span is the |
| * best estimate and a calculated span for no children |
| * is undesirable. |
| */ |
| protected void flushRequirementChanges() { |
| if (isLoaded()) { |
| super.flushRequirementChanges(); |
| } |
| } |
| |
| /** |
| * Returns the child view index representing the given position in |
| * the model. Since the zone contains a cluster of the overall |
| * set of child elements, we can determine the index fairly |
| * quickly from the model by subtracting the index of the |
| * start offset from the index of the position given. |
| * |
| * @param pos the position >= 0 |
| * @return index of the view representing the given position, or |
| * -1 if no view represents that position |
| * @since 1.3 |
| */ |
| public int getViewIndex(int pos, Position.Bias b) { |
| boolean isBackward = (b == Position.Bias.Backward); |
| pos = (isBackward) ? Math.max(0, pos - 1) : pos; |
| Element elem = getElement(); |
| int index1 = elem.getElementIndex(pos); |
| int index0 = elem.getElementIndex(getStartOffset()); |
| return index1 - index0; |
| } |
| |
| protected boolean updateChildren(DocumentEvent.ElementChange ec, |
| DocumentEvent e, ViewFactory f) { |
| // the structure of this element changed. |
| Element[] removedElems = ec.getChildrenRemoved(); |
| Element[] addedElems = ec.getChildrenAdded(); |
| Element elem = getElement(); |
| int index0 = elem.getElementIndex(getStartOffset()); |
| int index1 = elem.getElementIndex(getEndOffset()-1); |
| int index = ec.getIndex(); |
| if ((index >= index0) && (index <= index1)) { |
| // The change is in this zone |
| int replaceIndex = index - index0; |
| int nadd = Math.min(index1 - index0 + 1, addedElems.length); |
| int nremove = Math.min(index1 - index0 + 1, removedElems.length); |
| View[] added = new View[nadd]; |
| for (int i = 0; i < nadd; i++) { |
| added[i] = f.create(addedElems[i]); |
| } |
| replace(replaceIndex, nremove, added); |
| } |
| return true; |
| } |
| |
| // --- View methods ---------------------------------- |
| |
| /** |
| * Fetches the attributes to use when rendering. This view |
| * isn't directly responsible for an element so it returns |
| * the outer classes attributes. |
| */ |
| public AttributeSet getAttributes() { |
| return ZoneView.this.getAttributes(); |
| } |
| |
| /** |
| * Renders using the given rendering surface and area on that |
| * surface. This is implemented to load the zone if its not |
| * already loaded, and then perform the superclass behavior. |
| * |
| * @param g the rendering surface to use |
| * @param a the allocated region to render into |
| * @see View#paint |
| */ |
| public void paint(Graphics g, Shape a) { |
| load(); |
| super.paint(g, a); |
| } |
| |
| /** |
| * Provides a mapping from the view coordinate space to the logical |
| * coordinate space of the model. This is implemented to first |
| * make sure the zone is loaded before providing the superclass |
| * behavior. |
| * |
| * @param x x coordinate of the view location to convert >= 0 |
| * @param y y coordinate of the view location to convert >= 0 |
| * @param a the allocated region to render into |
| * @return the location within the model that best represents the |
| * given point in the view >= 0 |
| * @see View#viewToModel |
| */ |
| public int viewToModel(float x, float y, Shape a, Position.Bias[] bias) { |
| load(); |
| return super.viewToModel(x, y, a, bias); |
| } |
| |
| /** |
| * Provides a mapping from the document model coordinate space |
| * to the coordinate space of the view mapped to it. This is |
| * implemented to provide the superclass behavior after first |
| * making sure the zone is loaded (The zone must be loaded to |
| * make this calculation). |
| * |
| * @param pos the position to convert |
| * @param a the allocated region to render into |
| * @return the bounding box of the given position |
| * @exception BadLocationException if the given position does not represent a |
| * valid location in the associated document |
| * @see View#modelToView |
| */ |
| public Shape modelToView(int pos, Shape a, Position.Bias b) throws BadLocationException { |
| load(); |
| return super.modelToView(pos, a, b); |
| } |
| |
| /** |
| * Start of the zones range. |
| * |
| * @see View#getStartOffset |
| */ |
| public int getStartOffset() { |
| return start.getOffset(); |
| } |
| |
| /** |
| * End of the zones range. |
| */ |
| public int getEndOffset() { |
| return end.getOffset(); |
| } |
| |
| /** |
| * Gives notification that something was inserted into |
| * the document in a location that this view is responsible for. |
| * If the zone has been loaded, the superclass behavior is |
| * invoked, otherwise this does nothing. |
| * |
| * @param e the change information from the associated document |
| * @param a the current allocation of the view |
| * @param f the factory to use to rebuild if the view has children |
| * @see View#insertUpdate |
| */ |
| public void insertUpdate(DocumentEvent e, Shape a, ViewFactory f) { |
| if (isLoaded()) { |
| super.insertUpdate(e, a, f); |
| } |
| } |
| |
| /** |
| * Gives notification that something was removed from the document |
| * in a location that this view is responsible for. |
| * If the zone has been loaded, the superclass behavior is |
| * invoked, otherwise this does nothing. |
| * |
| * @param e the change information from the associated document |
| * @param a the current allocation of the view |
| * @param f the factory to use to rebuild if the view has children |
| * @see View#removeUpdate |
| */ |
| public void removeUpdate(DocumentEvent e, Shape a, ViewFactory f) { |
| if (isLoaded()) { |
| super.removeUpdate(e, a, f); |
| } |
| } |
| |
| /** |
| * Gives notification from the document that attributes were changed |
| * in a location that this view is responsible for. |
| * If the zone has been loaded, the superclass behavior is |
| * invoked, otherwise this does nothing. |
| * |
| * @param e the change information from the associated document |
| * @param a the current allocation of the view |
| * @param f the factory to use to rebuild if the view has children |
| * @see View#removeUpdate |
| */ |
| public void changedUpdate(DocumentEvent e, Shape a, ViewFactory f) { |
| if (isLoaded()) { |
| super.changedUpdate(e, a, f); |
| } |
| } |
| |
| } |
| } |