001    /* RepaintManager.java --
002       Copyright (C) 2002, 2004, 2005  Free Software Foundation, Inc.
003    
004    This file is part of GNU Classpath.
005    
006    GNU Classpath is free software; you can redistribute it and/or modify
007    it under the terms of the GNU General Public License as published by
008    the Free Software Foundation; either version 2, or (at your option)
009    any later version.
010    
011    GNU Classpath is distributed in the hope that it will be useful, but
012    WITHOUT ANY WARRANTY; without even the implied warranty of
013    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
014    General Public License for more details.
015    
016    You should have received a copy of the GNU General Public License
017    along with GNU Classpath; see the file COPYING.  If not, write to the
018    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
019    02110-1301 USA.
020    
021    Linking this library statically or dynamically with other modules is
022    making a combined work based on this library.  Thus, the terms and
023    conditions of the GNU General Public License cover the whole
024    combination.
025    
026    As a special exception, the copyright holders of this library give you
027    permission to link this library with independent modules to produce an
028    executable, regardless of the license terms of these independent
029    modules, and to copy and distribute the resulting executable under
030    terms of your choice, provided that you also meet, for each linked
031    independent module, the terms and conditions of the license of that
032    module.  An independent module is a module which is not derived from
033    or based on this library.  If you modify this library, you may extend
034    this exception to your version of the library, but you are not
035    obligated to do so.  If you do not wish to do so, delete this
036    exception statement from your version. */
037    
038    
039    package javax.swing;
040    
041    import gnu.classpath.SystemProperties;
042    import gnu.java.awt.LowPriorityEvent;
043    
044    import java.applet.Applet;
045    import java.awt.Component;
046    import java.awt.Dimension;
047    import java.awt.EventQueue;
048    import java.awt.Graphics;
049    import java.awt.Image;
050    import java.awt.Rectangle;
051    import java.awt.Toolkit;
052    import java.awt.Window;
053    import java.awt.event.InvocationEvent;
054    import java.awt.image.VolatileImage;
055    import java.util.ArrayList;
056    import java.util.HashMap;
057    import java.util.HashSet;
058    import java.util.Iterator;
059    import java.util.Set;
060    import java.util.WeakHashMap;
061    
062    import javax.swing.text.JTextComponent;
063    
064    /**
065     * <p>The repaint manager holds a set of dirty regions, invalid components,
066     * and a double buffer surface.  The dirty regions and invalid components
067     * are used to coalesce multiple revalidate() and repaint() calls in the
068     * component tree into larger groups to be refreshed "all at once"; the
069     * double buffer surface is used by root components to paint
070     * themselves.</p>
071     *
072     * <p>See <a
073     * href="http://java.sun.com/products/jfc/tsc/articles/painting/index.html">this
074     * document</a> for more details.</p>
075     * document</a> for more details.</p>
076     *
077     * @author Roman Kennke (kennke@aicas.com)
078     * @author Graydon Hoare (graydon@redhat.com)
079     * @author Audrius Meskauskas (audriusa@bioinformatics.org)
080     */
081    public class RepaintManager
082    {
083      /**
084       * An InvocationEvent subclass that implements LowPriorityEvent. This is used
085       * to defer the execution of RepaintManager requests as long as possible on
086       * the event queue. This way we make sure that all available input is
087       * processed before getting active with the RepaintManager. This allows
088       * for better optimization (more validate and repaint requests can be
089       * coalesced) and thus has a positive effect on performance for GUI
090       * applications under heavy load.
091       */
092      private static class RepaintWorkerEvent
093        extends InvocationEvent
094        implements LowPriorityEvent
095      {
096    
097        /**
098         * Creates a new RepaintManager event.
099         *
100         * @param source the source
101         * @param runnable the runnable to execute
102         */
103        public RepaintWorkerEvent(Object source, Runnable runnable,
104                                  Object notifier, boolean catchEx)
105        {
106          super(source, runnable, notifier, catchEx);
107        }
108    
109        /**
110         * An application that I met implements its own event dispatching and
111         * calls dispatch() via reflection, and only checks declared methods,
112         * that is, it expects this method to be in the event's class, not
113         * in a superclass. So I put this in here... sigh.
114         */
115        public void dispatch()
116        {
117          super.dispatch();
118        }
119      }
120      
121      /**
122       * The current repaint managers, indexed by their ThreadGroups.
123       */
124      static WeakHashMap currentRepaintManagers;
125    
126      /**
127       * A rectangle object to be reused in damaged regions calculation.
128       */
129      private static Rectangle rectCache = new Rectangle();
130    
131      /**
132       * <p>A helper class which is placed into the system event queue at
133       * various times in order to facilitate repainting and layout. There is
134       * typically only one of these objects active at any time. When the
135       * {@link RepaintManager} is told to queue a repaint, it checks to see if
136       * a {@link RepaintWorker} is "live" in the system event queue, and if
137       * not it inserts one using {@link SwingUtilities#invokeLater}.</p>
138       *
139       * <p>When the {@link RepaintWorker} comes to the head of the system
140       * event queue, its {@link RepaintWorker#run} method is executed by the
141       * swing paint thread, which revalidates all invalid components and
142       * repaints any damage in the swing scene.</p>
143       */
144      private class RepaintWorker
145        implements Runnable
146      {
147    
148        boolean live;
149    
150        public RepaintWorker()
151        {
152          live = false;
153        }
154    
155        public synchronized void setLive(boolean b) 
156        {
157          live = b;
158        }
159    
160        public synchronized boolean isLive()
161        {
162          return live;
163        }
164    
165        public void run()
166        {
167          try
168            {
169              ThreadGroup threadGroup = Thread.currentThread().getThreadGroup();
170              RepaintManager rm =
171                (RepaintManager) currentRepaintManagers.get(threadGroup);
172              rm.validateInvalidComponents();
173              rm.paintDirtyRegions();
174            }
175          finally
176            {
177              setLive(false);
178            }
179        }
180    
181      }
182    
183      /** 
184       * A table storing the dirty regions of components.  The keys of this
185       * table are components, the values are rectangles. Each component maps
186       * to exactly one rectangle.  When more regions are marked as dirty on a
187       * component, they are union'ed with the existing rectangle.
188       *
189       * This is package private to avoid a synthetic accessor method in inner
190       * class.
191       *
192       * @see #addDirtyRegion
193       * @see #getDirtyRegion
194       * @see #isCompletelyDirty
195       * @see #markCompletelyClean
196       * @see #markCompletelyDirty
197       */
198      private HashMap dirtyComponents;
199    
200      /**
201       * The dirtyComponents which is used in paintDiryRegions to avoid unnecessary
202       * locking.
203       */
204      private HashMap dirtyComponentsWork;
205    
206      /**
207       * A single, shared instance of the helper class. Any methods which mark
208       * components as invalid or dirty eventually activate this instance. It
209       * is added to the event queue if it is not already active, otherwise
210       * reused.
211       *
212       * @see #addDirtyRegion
213       * @see #addInvalidComponent
214       */
215      private RepaintWorker repaintWorker;
216    
217      /** 
218       * The set of components which need revalidation, in the "layout" sense.
219       * There is no additional information about "what kind of layout" they
220       * need (as there is with dirty regions), so it is just a vector rather
221       * than a table.
222       *
223       * @see #addInvalidComponent
224       * @see #removeInvalidComponent
225       * @see #validateInvalidComponents
226       */
227      private ArrayList invalidComponents;
228    
229      /** 
230       * Whether or not double buffering is enabled on this repaint
231       * manager. This is merely a hint to clients; the RepaintManager will
232       * always return an offscreen buffer when one is requested.
233       * 
234       * @see #isDoubleBufferingEnabled
235       * @see #setDoubleBufferingEnabled
236       */
237      private boolean doubleBufferingEnabled;
238    
239      /**
240       * The offscreen buffers. This map holds one offscreen buffer per
241       * Window/Applet and releases them as soon as the Window/Applet gets garbage
242       * collected.
243       */
244      private WeakHashMap offscreenBuffers;
245    
246      /**
247       * The maximum width and height to allocate as a double buffer. Requests
248       * beyond this size are ignored.
249       *
250       * @see #paintDirtyRegions
251       * @see #getDoubleBufferMaximumSize
252       * @see #setDoubleBufferMaximumSize
253       */
254      private Dimension doubleBufferMaximumSize;
255    
256    
257      /**
258       * Create a new RepaintManager object.
259       */
260      public RepaintManager()
261      {
262        dirtyComponents = new HashMap();
263        dirtyComponentsWork = new HashMap();
264        invalidComponents = new ArrayList();
265        repaintWorker = new RepaintWorker();
266        doubleBufferMaximumSize = new Dimension(2000,2000);
267        doubleBufferingEnabled =
268          SystemProperties.getProperty("gnu.swing.doublebuffering", "true")
269                          .equals("true");
270        offscreenBuffers = new WeakHashMap();
271      }
272    
273      /**
274       * Returns the <code>RepaintManager</code> for the current thread's
275       * thread group. The default implementation ignores the
276       * <code>component</code> parameter and returns the same repaint manager
277       * for all components.
278       *
279       * @param component a component to look up the manager of
280       *
281       * @return the current repaint manager for the calling thread's thread group
282       *         and the specified component
283       *
284       * @see #setCurrentManager
285       */
286      public static RepaintManager currentManager(Component component)
287      {
288        if (currentRepaintManagers == null)
289          currentRepaintManagers = new WeakHashMap();
290        ThreadGroup threadGroup = Thread.currentThread().getThreadGroup();
291        RepaintManager currentManager =
292          (RepaintManager) currentRepaintManagers.get(threadGroup);
293        if (currentManager == null)
294          {
295            currentManager = new RepaintManager();
296            currentRepaintManagers.put(threadGroup, currentManager);
297          }
298        return currentManager;
299      }
300    
301      /**
302       * Returns the <code>RepaintManager</code> for the current thread's
303       * thread group. The default implementation ignores the
304       * <code>component</code> parameter and returns the same repaint manager
305       * for all components.
306       *
307       * This method is only here for backwards compatibility with older versions
308       * of Swing and simply forwards to {@link #currentManager(Component)}.
309       *
310       * @param component a component to look up the manager of
311       *
312       * @return the current repaint manager for the calling thread's thread group
313       *         and the specified component
314       *
315       * @see #setCurrentManager
316       */
317      public static RepaintManager currentManager(JComponent component)
318      {
319        return currentManager((Component)component);
320      }
321    
322      /**
323       * Sets the repaint manager for the calling thread's thread group.
324       *
325       * @param manager the repaint manager to set for the current thread's thread
326       *        group
327       *
328       * @see #currentManager(Component)
329       */
330      public static void setCurrentManager(RepaintManager manager)
331      {
332        if (currentRepaintManagers == null)
333          currentRepaintManagers = new WeakHashMap();
334    
335        ThreadGroup threadGroup = Thread.currentThread().getThreadGroup();
336        currentRepaintManagers.put(threadGroup, manager);
337      }
338    
339      /**
340       * Add a component to the {@link #invalidComponents} vector. If the
341       * {@link #repaintWorker} class is not active, insert it in the system
342       * event queue.
343       *
344       * @param component The component to add
345       *
346       * @see #removeInvalidComponent
347       */
348      public void addInvalidComponent(JComponent component)
349      {
350        Component validateRoot = null;
351        Component c = component;
352        while (c != null)
353          {
354            // Special cases we don't bother validating are when the invalidated
355            // component (or any of it's ancestors) is inside a CellRendererPane
356            // or if it doesn't have a peer yet (== not displayable).
357            if (c instanceof CellRendererPane || ! c.isDisplayable())
358              return;
359            if (c instanceof JComponent && ((JComponent) c).isValidateRoot())
360              {
361                validateRoot = c;
362                break;
363              }
364    
365            c = c.getParent();
366          }
367    
368        // If we didn't find a validate root, then we don't validate.
369        if (validateRoot == null)
370          return;
371    
372        // Make sure the validate root and all of it's ancestors are visible.
373        c = validateRoot;
374        while (c != null)
375          {
376            if (! c.isVisible() || ! c.isDisplayable())
377              return;
378            c = c.getParent();
379          }
380    
381        if (invalidComponents.contains(validateRoot))
382          return;
383    
384        //synchronized (invalidComponents)
385        //  {
386            invalidComponents.add(validateRoot);
387        //  }
388    
389        if (! repaintWorker.isLive())
390          {
391            repaintWorker.setLive(true);
392            invokeLater(repaintWorker);
393          }
394      }
395    
396      /**
397       * Remove a component from the {@link #invalidComponents} vector.
398       *
399       * @param component The component to remove
400       *
401       * @see #addInvalidComponent
402       */
403      public void removeInvalidComponent(JComponent component)
404      {
405        synchronized (invalidComponents)
406          {
407            invalidComponents.remove(component);
408          }
409      }
410    
411      /**
412       * Add a region to the set of dirty regions for a specified component.
413       * This involves union'ing the new region with any existing dirty region
414       * associated with the component. If the {@link #repaintWorker} class
415       * is not active, insert it in the system event queue.
416       *
417       * @param component The component to add a dirty region for
418       * @param x The left x coordinate of the new dirty region
419       * @param y The top y coordinate of the new dirty region
420       * @param w The width of the new dirty region
421       * @param h The height of the new dirty region
422       *
423       * @see #addDirtyRegion
424       * @see #getDirtyRegion
425       * @see #isCompletelyDirty
426       * @see #markCompletelyClean
427       * @see #markCompletelyDirty
428       */
429      public void addDirtyRegion(JComponent component, int x, int y,
430                                 int w, int h)
431      {
432        if (w <= 0 || h <= 0 || !component.isShowing())
433          return;
434        component.computeVisibleRect(rectCache);
435        SwingUtilities.computeIntersection(x, y, w, h, rectCache);
436    
437        if (! rectCache.isEmpty())
438          {
439            synchronized (dirtyComponents)
440              {
441                Rectangle dirtyRect = (Rectangle)dirtyComponents.get(component);
442                if (dirtyRect != null)
443                  {
444                    SwingUtilities.computeUnion(rectCache.x, rectCache.y,
445                                                rectCache.width, rectCache.height,
446                                                dirtyRect);
447                  }
448                else
449                  {
450                    dirtyComponents.put(component, rectCache.getBounds());
451                  }
452              }
453    
454            if (! repaintWorker.isLive())
455              {
456                repaintWorker.setLive(true);
457                invokeLater(repaintWorker);
458              }
459          }
460      }
461    
462      /**
463       * Get the dirty region associated with a component, or <code>null</code>
464       * if the component has no dirty region.
465       *
466       * @param component The component to get the dirty region of
467       *
468       * @return The dirty region of the component
469       *
470       * @see #dirtyComponents
471       * @see #addDirtyRegion
472       * @see #isCompletelyDirty
473       * @see #markCompletelyClean
474       * @see #markCompletelyDirty
475       */
476      public Rectangle getDirtyRegion(JComponent component)
477      {
478        Rectangle dirty = (Rectangle) dirtyComponents.get(component);
479        if (dirty == null)
480          dirty = new Rectangle();
481        return dirty;
482      }
483      
484      /**
485       * Mark a component as dirty over its entire bounds.
486       *
487       * @param component The component to mark as dirty
488       *
489       * @see #dirtyComponents
490       * @see #addDirtyRegion
491       * @see #getDirtyRegion
492       * @see #isCompletelyDirty
493       * @see #markCompletelyClean
494       */
495      public void markCompletelyDirty(JComponent component)
496      {
497        addDirtyRegion(component, 0, 0, Integer.MAX_VALUE, Integer.MAX_VALUE);
498      }
499    
500      /**
501       * Remove all dirty regions for a specified component
502       *
503       * @param component The component to mark as clean
504       *
505       * @see #dirtyComponents
506       * @see #addDirtyRegion
507       * @see #getDirtyRegion
508       * @see #isCompletelyDirty
509       * @see #markCompletelyDirty
510       */
511      public void markCompletelyClean(JComponent component)
512      {
513        synchronized (dirtyComponents)
514          {
515            dirtyComponents.remove(component);
516          }
517      }
518    
519      /**
520       * Return <code>true</code> if the specified component is completely
521       * contained within its dirty region, otherwise <code>false</code>
522       *
523       * @param component The component to check for complete dirtyness
524       *
525       * @return Whether the component is completely dirty
526       *
527       * @see #dirtyComponents
528       * @see #addDirtyRegion
529       * @see #getDirtyRegion
530       * @see #isCompletelyDirty
531       * @see #markCompletelyClean
532       */
533      public boolean isCompletelyDirty(JComponent component)
534      {
535        boolean dirty = false;
536        Rectangle r = getDirtyRegion(component);
537        if(r.width == Integer.MAX_VALUE && r.height == Integer.MAX_VALUE)
538          dirty = true;
539        return dirty;
540      }
541    
542      /**
543       * Validate all components which have been marked invalid in the {@link
544       * #invalidComponents} vector.
545       */
546      public void validateInvalidComponents()
547      {
548        // We don't use an iterator here because that would fail when there are
549        // components invalidated during the validation of others, which happens
550        // quite frequently. Instead we synchronize the access a little more.
551        while (invalidComponents.size() > 0)
552          {
553            Component comp;
554            synchronized (invalidComponents)
555              {
556                comp = (Component) invalidComponents.remove(0);
557              }
558            // Validate the validate component.
559            if (! (comp.isVisible() && comp.isShowing()))
560              continue;
561            comp.validate();
562          }
563      }
564    
565      /**
566       * Repaint all regions of all components which have been marked dirty in the
567       * {@link #dirtyComponents} table.
568       */
569      public void paintDirtyRegions()
570      {
571        // Short circuit if there is nothing to paint.
572        if (dirtyComponents.size() == 0)
573          return;
574    
575        // Swap dirtyRegions with dirtyRegionsWork to avoid locking.
576        synchronized (dirtyComponents)
577          {
578            HashMap swap = dirtyComponents;
579            dirtyComponents = dirtyComponentsWork;
580            dirtyComponentsWork = swap;
581          }
582    
583        // Compile a set of repaint roots.
584        HashSet repaintRoots = new HashSet();
585        Set components = dirtyComponentsWork.keySet();
586        for (Iterator i = components.iterator(); i.hasNext();)
587          {
588            JComponent dirty = (JComponent) i.next();
589            compileRepaintRoots(dirtyComponentsWork, dirty, repaintRoots);
590          }
591    
592        for (Iterator i = repaintRoots.iterator(); i.hasNext();)
593          {
594            JComponent comp = (JComponent) i.next();
595            Rectangle damaged = (Rectangle) dirtyComponentsWork.remove(comp);
596            if (damaged == null || damaged.isEmpty())
597              continue;
598            comp.paintImmediately(damaged);
599          }
600        dirtyComponentsWork.clear();
601      }
602    
603      /**
604       * Compiles a list of components that really get repainted. This is called
605       * once for each component in the dirtyRegions HashMap, each time with
606       * another <code>dirty</code> parameter. This searches up the component
607       * hierarchy of <code>dirty</code> to find the highest parent that is also
608       * marked dirty and merges the dirty regions.
609       *
610       * @param dirtyRegions the dirty regions 
611       * @param dirty the component for which to find the repaint root
612       * @param roots the list to which new repaint roots get appended
613       */
614      private void compileRepaintRoots(HashMap dirtyRegions, JComponent dirty,
615                                       HashSet roots)
616      {
617        Component current = dirty;
618        Component root = dirty;
619    
620        // This will contain the dirty region in the root coordinate system,
621        // possibly clipped by ancestor's bounds.
622        Rectangle originalDirtyRect = (Rectangle) dirtyRegions.get(dirty); 
623        rectCache.setBounds(originalDirtyRect);
624    
625        // The bounds of the current component.
626        int x = dirty.getX();
627        int y = dirty.getY();
628        int w = dirty.getWidth();
629        int h = dirty.getHeight();
630    
631        // Do nothing if dirty region is clipped away by the component's bounds.
632        rectCache = SwingUtilities.computeIntersection(0, 0, w, h, rectCache);
633        if (rectCache.isEmpty())
634          return;
635    
636        // The cumulated offsets. 
637        int dx = 0;
638        int dy = 0;
639        // The actual offset for the found root.
640        int rootDx = 0;
641        int rootDy = 0;
642    
643        // Search the highest component that is also marked dirty.
644        Component parent;
645        while (true)
646          {
647            parent = current.getParent();
648            if (parent == null || !(parent instanceof JComponent))
649              break;
650    
651            current = parent;
652            // Update the offset.
653            dx += x;
654            dy += y;
655            rectCache.x += x;
656            rectCache.y += y;
657            
658            x = current.getX();
659            y = current.getY();
660            w = current.getWidth();
661            h = current.getHeight();
662            rectCache = SwingUtilities.computeIntersection(0, 0, w, h, rectCache);
663    
664            // Don't paint if the dirty regions is clipped away by any of
665            // its ancestors.
666            if (rectCache.isEmpty())
667              return;
668    
669            // We can skip to the next up when this parent is not dirty.
670            if (dirtyRegions.containsKey(parent))
671              {
672                root = current;
673                rootDx = dx;
674                rootDy = dy;
675              }
676          }
677    
678        // Merge the rectangles of the root and the requested component if
679        // the are different.
680        if (root != dirty)
681          {
682            rectCache.x += rootDx - dx;
683            rectCache.y += rootDy - dy;
684            Rectangle dirtyRect = (Rectangle) dirtyRegions.get(root);
685            SwingUtilities.computeUnion(rectCache.x, rectCache.y, rectCache.width,
686                                        rectCache.height, dirtyRect);
687          }
688    
689        // Adds the root to the roots set.
690        if (! roots.contains(root))
691          roots.add(root);
692      }
693    
694      /**
695       * Get an offscreen buffer for painting a component's image. This image
696       * may be smaller than the proposed dimensions, depending on the value of
697       * the {@link #doubleBufferMaximumSize} property.
698       *
699       * @param component The component to return an offscreen buffer for
700       * @param proposedWidth The proposed width of the offscreen buffer
701       * @param proposedHeight The proposed height of the offscreen buffer
702       *
703       * @return A shared offscreen buffer for painting
704       */
705      public Image getOffscreenBuffer(Component component, int proposedWidth,
706                                      int proposedHeight)
707      {
708        Component root = SwingUtilities.getWindowAncestor(component);
709        Image buffer = (Image) offscreenBuffers.get(root);
710        if (buffer == null 
711            || buffer.getWidth(null) < proposedWidth 
712            || buffer.getHeight(null) < proposedHeight)
713          {
714            int width = Math.max(proposedWidth, root.getWidth());
715            width = Math.min(doubleBufferMaximumSize.width, width);
716            int height = Math.max(proposedHeight, root.getHeight());
717            height = Math.min(doubleBufferMaximumSize.height, height);
718            buffer = component.createImage(width, height);
719            offscreenBuffers.put(root, buffer);
720          }
721        return buffer;
722      }
723    
724      /**
725       * Blits the back buffer of the specified root component to the screen.
726       * This is package private because it must get called by JComponent.
727       *
728       * @param comp the component to be painted
729       * @param x the area to paint on screen, in comp coordinates
730       * @param y the area to paint on screen, in comp coordinates
731       * @param w the area to paint on screen, in comp coordinates
732       * @param h the area to paint on screen, in comp coordinates
733       */
734      void commitBuffer(Component comp, int x, int y, int w, int h)
735      {
736        Component root = comp;
737        while (root != null
738               && ! (root instanceof Window || root instanceof Applet))
739          {
740            x += root.getX();
741            y += root.getY();
742            root = root.getParent();
743          }
744    
745        if (root != null)
746          {
747            Graphics g = root.getGraphics();
748            Image buffer = (Image) offscreenBuffers.get(root);
749            if (buffer != null)
750              {
751                // Make sure we have a sane clip at this point.
752                g.clipRect(x, y, w, h);
753                g.drawImage(buffer, 0, 0, root);
754                g.dispose();
755              }
756          }
757      }
758    
759      /**
760       * Creates and returns a volatile offscreen buffer for the specified
761       * component that can be used as a double buffer. The returned image
762       * is a {@link VolatileImage}. Its size will be <code>(proposedWidth,
763       * proposedHeight)</code> except when the maximum double buffer size
764       * has been set in this RepaintManager.
765       *
766       * @param comp the Component for which to create a volatile buffer
767       * @param proposedWidth the proposed width of the buffer
768       * @param proposedHeight the proposed height of the buffer
769       *
770       * @since 1.4
771       *
772       * @see VolatileImage
773       */
774      public Image getVolatileOffscreenBuffer(Component comp, int proposedWidth,
775                                              int proposedHeight)
776      {
777        Component root = SwingUtilities.getWindowAncestor(comp);
778        Image buffer = (Image) offscreenBuffers.get(root);
779        if (buffer == null 
780            || buffer.getWidth(null) < proposedWidth 
781            || buffer.getHeight(null) < proposedHeight
782            || !(buffer instanceof VolatileImage))
783          {
784            int width = Math.max(proposedWidth, root.getWidth());
785            width = Math.min(doubleBufferMaximumSize.width, width);
786            int height = Math.max(proposedHeight, root.getHeight());
787            height = Math.min(doubleBufferMaximumSize.height, height);
788            buffer = root.createVolatileImage(width, height);
789            if (buffer != null)
790              offscreenBuffers.put(root, buffer);
791          }
792        return buffer;
793      }
794      
795    
796      /**
797       * Get the value of the {@link #doubleBufferMaximumSize} property.
798       *
799       * @return The current value of the property
800       *
801       * @see #setDoubleBufferMaximumSize
802       */
803      public Dimension getDoubleBufferMaximumSize()
804      {
805        return doubleBufferMaximumSize;
806      }
807    
808      /**
809       * Set the value of the {@link #doubleBufferMaximumSize} property.
810       *
811       * @param size The new value of the property
812       *
813       * @see #getDoubleBufferMaximumSize
814       */
815      public void setDoubleBufferMaximumSize(Dimension size)
816      {
817        doubleBufferMaximumSize = size;
818      }
819    
820      /**
821       * Set the value of the {@link #doubleBufferingEnabled} property.
822       *
823       * @param buffer The new value of the property
824       *
825       * @see #isDoubleBufferingEnabled
826       */
827      public void setDoubleBufferingEnabled(boolean buffer)
828      {
829        doubleBufferingEnabled = buffer;
830      }
831    
832      /**
833       * Get the value of the {@link #doubleBufferingEnabled} property.
834       *
835       * @return The current value of the property
836       *
837       * @see #setDoubleBufferingEnabled
838       */
839      public boolean isDoubleBufferingEnabled()
840      {
841        return doubleBufferingEnabled;
842      }
843      
844      public String toString()
845      {
846        return "RepaintManager";
847      }
848    
849      /**
850       * Sends an RepaintManagerEvent to the event queue with the specified
851       * runnable. This is similar to SwingUtilities.invokeLater(), only that the
852       * event is a low priority event in order to defer the execution a little
853       * more.
854       */
855      private void invokeLater(Runnable runnable)
856      {
857        Toolkit tk = Toolkit.getDefaultToolkit();
858        EventQueue evQueue = tk.getSystemEventQueue();
859        InvocationEvent ev = new RepaintWorkerEvent(evQueue, runnable, null, false);
860        evQueue.postEvent(ev);
861      }
862    }