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 &lt; 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 &gt;= getStartOffset() and &lt; getEndOffset().  This
208     *  value should also be &lt; p1.
209     * @param p1 the end of the desired zone.  This should
210     *  be &gt; getStartOffset() and &lt;= getEndOffset().  This
211     *  value should also be &gt; 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 &gt;= 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