
com.threerings.media.ActiveRepaintManager Maven / Gradle / Ivy
//
// 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 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> iter = _spare.entrySet().iterator();
PRUNE:
while (iter.hasNext()) {
Map.Entry 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 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 _dirty = Maps.newHashMap();
/** A spare hashmap that we swap in while repainting dirty components in the old hashmap. */
protected Map _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;
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy