// // Nenya library - tools for developing networked games // Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved // https://github.com/threerings/nenya // // This library is free software; you can redistribute it and/or modify it // under the terms of the GNU Lesser General Public License as published // by the Free Software Foundation; either version 2.1 of the License, or // (at your option) any later version. // // This library 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 // Lesser General Public License for more details. // // You should have received a copy of the GNU Lesser General Public // License along with this library; if not, write to the Free Software // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA package com.threerings.media; import java.applet.Applet; import java.util.Iterator; import java.util.Map; import java.awt.Component; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.Image; import java.awt.Rectangle; import java.awt.Window; import javax.swing.CellRendererPane; import javax.swing.JComponent; import javax.swing.JEditorPane; import javax.swing.JScrollPane; import javax.swing.JTextField; import javax.swing.RepaintManager; import javax.swing.SwingUtilities; import com.google.common.collect.Maps; import com.samskivert.util.ListUtil; import com.samskivert.util.RunAnywhere; import com.samskivert.util.StringUtil; import static com.threerings.media.Log.log; /** * Used to get Swing's repainting to jive with our active rendering strategy. * * @see FrameManager */ public class ActiveRepaintManager extends RepaintManager { /** * Components that are rooted in this component (which must be a {@link Window} or an {@link * Applet}) will be rendered into the offscreen buffer managed by the frame manager. Other * components will be rendered into separate offscreen buffers and repainted in the normal * Swing manner. */ public ActiveRepaintManager (Component root) { _root = root; } @Override public synchronized void addInvalidComponent (JComponent comp) { Component vroot = null; if (DEBUG) { log.info("Maybe invalidating " + toString(comp) + "."); } // locate the validation root for this component for (Component c = comp; c != null; c = c.getParent()) { // if the component is not part of an active widget hierarcy, we can stop now; if the // component is a cell render pane, we're apparently supposed to ignore it as wel if (!c.isDisplayable() || c instanceof CellRendererPane) { return; } // skip non-Swing components if (!(c instanceof JComponent)) { continue; } // if we find our validate root, we can stop looking; NOTE: JTextField incorrectly // claims to be a validate root thereby fucking up the program something serious; we // jovially ignore its claims here and restore order to the universe; see bug #403550 // for more fallout from Sun's fuckup if (!(c instanceof JTextField) && !(c instanceof JScrollPane) && ((JComponent)c).isValidateRoot()) { vroot = c; break; } } // if we found no validation root we can abort as this component is not part of any valid // widget hierarchy if (vroot == null) { if (DEBUG) { log.info("Skipping vrootless component: " + toString(comp)); } return; } // make sure that the component is actually in a window or applet that is showing if (getRoot(vroot) == null) { if (DEBUG) { log.info("Skipping rootless component", "comp", toString(comp), "vroot", toString(vroot)); } return; } // add the invalid component to our list and we'll validate it on the next frame if (!ListUtil.containsRef(_invalid, vroot)) { if (DEBUG) { log.info("Invalidating " + toString(vroot) + "."); } _invalid = ListUtil.add(_invalid, vroot); } } @Override public synchronized void addDirtyRegion (JComponent comp, int x, int y, int width, int height) { // ignore invalid requests if ((width <= 0) || (height <= 0) || (comp == null) || (comp.getWidth() <= 0) || (comp.getHeight() <= 0)) { // Log.info("Skipping bogus region " + comp.getClass().getName() + // ", x=" + x + ", y=" + y + ", width=" + width + ", height=" + height + "."); return; } // if this component is already dirty, simply expand their existing dirty rectangle Rectangle drect = _dirty.get(comp); if (drect != null) { drect.add(x, y); drect.add(x+width, y+height); return; } // make sure this component has a valid root if (getRoot(comp) == null) { // Log.info("Skipping rootless repaint " + comp + "."); return; } drect = new Rectangle(x, y, width, height); if (DEBUG) { log.info("Dirtying component", "comp", toString(comp), "drect", drect); } // if we made it this far, we can queue up a dirty region for this component to be // repainted on the next tick _dirty.put(comp, drect); } /** * Returns the root component for the supplied component or null if it is not part of a rooted * hierarchy or if any parent along the way is found to be hidden or without a peer. */ protected Component getRoot (Component comp) { for (Component c = comp; c != null; c = c.getParent()) { boolean hidden = !c.isDisplayable(); // on the mac, the JRootPane is invalidated before it is visible and is never again // invalidated or repainted, so we punt and allow all invisible components to be // invalidated and revalidated if (!RunAnywhere.isMacOS()) { hidden |= !c.isVisible(); } if (hidden) { return null; } if (c instanceof Window || c instanceof Applet) { return c; } } return null; } @Override public synchronized Rectangle getDirtyRegion (JComponent comp) { Rectangle drect = _dirty.get(comp); // copy the rectangle if we found one, otherwise create an empty rectangle because we don't // want them leaving empty handed return (drect == null) ? new Rectangle(0, 0, 0, 0) : new Rectangle(drect); } @Override public synchronized void markCompletelyClean (JComponent comp) { _dirty.remove(comp); } /** * Validates the invalid components that have been queued up since the last frame tick. */ public void validateComponents () { // swap out our invalid array Object[] invalid = null; synchronized (this) { invalid = _invalid; _invalid = null; } // if there's nothing to validate, we're home free if (invalid == null) { return; } // validate everything therein int icount = invalid.length; for (int ii = 0; ii < icount; ii++) { if (invalid[ii] != null) { if (DEBUG) { log.info("Validating " + invalid[ii]); } ((Component)invalid[ii]).validate(); } } } /** * Paints the components that have become dirty since the last tick. * * @return true if any components were painted. */ public boolean paintComponents (Graphics g, FrameManager fmgr) { synchronized (this) { // exit now if there are no dirty rectangles to paint if (_dirty.isEmpty()) { return false; } // otherwise, swap our hashmaps Map<JComponent,Rectangle> tmap = _spare; _spare = _dirty; _dirty = tmap; } // scan through the list, looking for components for whom a parent component is also dirty. // in such a case, the dirty rectangle for the parent component is expanded to contain the // dirty rectangle of the child and the child is removed from the repaint list (painting // the parent will repaint the child) Iterator<Map.Entry<JComponent,Rectangle>> iter = _spare.entrySet().iterator(); PRUNE: while (iter.hasNext()) { Map.Entry<JComponent,Rectangle> entry = iter.next(); JComponent comp = entry.getKey(); Rectangle drect = entry.getValue(); int x = comp.getX() + drect.x, y = comp.getY() + drect.y; // climb up the parent hierarchy, looking for the first opaque parent as well as the // root component for (Component c = comp.getParent(); c != null; c = c.getParent()) { // stop looking for combinable parents for non-visible or non-JComponents if (!c.isVisible() || !c.isDisplayable() || !(c instanceof JComponent)) { break; } // check to see if this parent is dirty Rectangle prect = _spare.get(c); if (prect != null) { // that we were going to merge it with its parent and blow it away drect.x = x; drect.y = y; if (DEBUG) { log.info("Found dirty parent", "comp", toString(comp), "drect", StringUtil.toString(drect), "pcomp", toString(c), "prect", StringUtil.toString(prect)); } prect.add(drect); if (DEBUG) { log.info("New prect " + StringUtil.toString(prect)); } // remove the child component and be on our way iter.remove(); continue PRUNE; } // translate the coordinates into this component's coordinate system x += c.getX(); y += c.getY(); } } // now paint each of the dirty components, by setting the clipping rectangle appropriately // and calling paint() on the associated root component iter = _spare.entrySet().iterator(); while (iter.hasNext()) { Map.Entry<JComponent,Rectangle> entry = iter.next(); JComponent comp = entry.getKey(); Rectangle drect = entry.getValue(); // get the root component, adjust the clipping (dirty) rectangle and obtain the bounds // of the client in absolute coordinates Component root = null, ocomp = null; // start with the components bounds which we'll switch to the opaque parent component's // bounds if and when we find one _cbounds.setBounds(0, 0, comp.getWidth(), comp.getHeight()); // climb up the parent hierarchy, looking for the first opaque parent as well as the // root component for (Component c = comp; c != null; c = c.getParent()) { if (!c.isVisible() || !c.isDisplayable()) { break; } if (c instanceof JComponent) { // make a note of the first opaque parent we find if (ocomp == null && ((JComponent)c).isOpaque()) { ocomp = c; // we need to obtain the opaque parent's coordinates in the root coordinate // system for when we repaint _cbounds.setBounds(0, 0, ocomp.getWidth(), ocomp.getHeight()); } } else { // oh god the hackery. apparently the fscking JEditorPane wraps a heavy weight // component around every swing component it uses when doing forms Component tp = c.getParent(); if (!(tp instanceof JEditorPane)) { root = c; break; } } // translate the coordinates into this component's coordinate system drect.x += c.getX(); drect.y += c.getY(); _cbounds.x += c.getX(); _cbounds.y += c.getY(); // clip the dirty region to the bounds of this component SwingUtilities.computeIntersection( c.getX(), c.getY(), c.getWidth(), c.getHeight(), drect); } // if we found no opaque parent, just paint the component itself (this seems to happen // with the top-level layered pane) if (ocomp == null) { ocomp = comp; } // if this component is rooted in our frame, repaint it into the supplied graphics // instance if (root == _root) { if (DEBUG) { log.info("Repainting", "comp", toString(comp) + StringUtil.toString(_cbounds), "ocomp", toString(ocomp), "drect", StringUtil.toString(drect)); } g.setClip(drect); g.translate(_cbounds.x, _cbounds.y); try { // some components are ill-behaved and may throw an exception while painting // themselves, and so we needs must deal with these fellows gracefully ocomp.paint(g); } catch (Exception e) { log.warning("Exception while painting component", "comp", ocomp, e); } g.translate(-_cbounds.x, -_cbounds.y); // we also need to repaint any components in this layer that are above our freshly // repainted component fmgr.renderLayers((Graphics2D)g, ocomp, _cbounds, _clipped, drect); } else if (root != null) { if (DEBUG) { log.info("Repainting old-school", "comp", toString(comp), "ocomp", toString(ocomp), "root", toString(root), "bounds", StringUtil.toString(_cbounds)); dumpHierarchy(comp); } // otherwise, repaint with standard swing double buffers Image obuf = getOffscreenBuffer(ocomp, _cbounds.width, _cbounds.height); Graphics og = null, cg = null; try { og = obuf.getGraphics(); ocomp.paint(og); cg = ocomp.getGraphics(); cg.drawImage(obuf, 0, 0, null); } finally { if (og != null) { og.dispose(); } if (cg != null) { cg.dispose(); } } } } // clear out the mapping of dirty components _spare.clear(); return true; } /** * Used to dump a component when debugging. */ protected static String toString (Component comp) { return comp.getClass().getName() + StringUtil.toString(comp.getBounds()); } /** * Dumps the containment hierarchy for the supplied component. */ protected static void dumpHierarchy (Component comp) { for (String indent = ""; comp != null; indent += " ") { log.info(indent + toString(comp)); comp = comp.getParent(); } } /** The root of our interface. */ protected Component _root; /** A list of invalid components. */ protected Object[] _invalid; /** A mapping of invalid rectangles for each widget that is dirty. */ protected Map<JComponent, Rectangle> _dirty = Maps.newHashMap(); /** A spare hashmap that we swap in while repainting dirty components in the old hashmap. */ protected Map<JComponent, Rectangle> _spare = Maps.newHashMap(); /** Used to compute dirty components' bounds. */ protected Rectangle _cbounds = new Rectangle(); /** Used when rendering "layered" components. */ protected boolean[] _clipped = new boolean[] { true }; /** We debug so much that we have to make it easy to enable and disable debug logging. Yay! */ protected static final boolean DEBUG = false; }