// // $Id$ // // Nenya library - tools for developing networked games // Copyright (C) 2002-2007 Three Rings Design, Inc., All Rights Reserved // http://www.threerings.net/code/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.awt.Component; import java.awt.Graphics2D; import java.awt.Graphics; import java.awt.Point; import java.awt.Rectangle; import java.awt.Shape; import java.awt.event.ActionEvent; import java.awt.event.MouseEvent; import javax.swing.JComponent; import javax.swing.SwingUtilities; import javax.swing.event.AncestorEvent; import javax.swing.event.MouseInputAdapter; import java.util.ArrayList; import com.samskivert.swing.Controller; import com.samskivert.swing.event.AncestorAdapter; import com.samskivert.swing.event.CommandEvent; import com.threerings.media.timer.MediaTimer; import com.threerings.media.animation.Animation; import com.threerings.media.animation.AnimationManager; import com.threerings.media.sprite.Sprite; import com.threerings.media.sprite.SpriteManager; import com.threerings.media.sprite.action.ActionSprite; import com.threerings.media.sprite.action.ArmingSprite; import com.threerings.media.sprite.action.CommandSprite; import com.threerings.media.sprite.action.DisableableSprite; import com.threerings.media.sprite.action.HoverSprite; import static com.threerings.media.Log.log; /** * Provides a useful extensible framework for rendering animated displays that use sprites and * animations. Sprites and animations can be added to this panel and they will automatically be * ticked and rendered (see {@link #addSprite} and {@link #addAnimation}). * *
To facilitate optimized sprite and animation rendering, the panel provides a dirty region * manager which is used to only repaint dirtied regions on each frame. Derived classes can add * dirty regions to the scene and/or augment the dirty regions created by moving sprites and * changing animations. * *
Sprite and animation rendering is done in two layers: front and back. Callbacks are provided * to render behind the back layer ({@link #paintBehind}), in between front and back ({@link * #paintBetween}) and in front of the front layer ({@link #paintInFront}). * *
The animated panel automatically registers with the {@link FrameManager} to participate in
* the frame tick. It only does so while it is a visible part of the UI hierarchy, so animations
* and sprites are paused while the animated panel is hidden.
*/
public class MediaPanel extends JComponent
implements FrameParticipant, MediaConstants, MediaHost
{
public interface Obscurer
{
/**
* Returns the region obscured by the obscurer, in screen coords.
*/
public Rectangle getObscured ();
}
/**
* Constructs a media panel.
*/
public MediaPanel (FrameManager framemgr)
{
setOpaque(true); // our repaints shouldn't cause other jcomponents to
// create our meta manager
_metamgr = new MetaMediaManager(framemgr, this);
_animmgr = _metamgr.getAnimationManager();
_spritemgr = _metamgr.getSpriteManager();
_remgr = _metamgr.getRegionManager();
// participate in the frame when we're visible
addAncestorListener(new AncestorAdapter() {
@Override
public void ancestorAdded (AncestorEvent event) {
_metamgr.getFrameManager().registerFrameParticipant(MediaPanel.this);
}
@Override
public void ancestorRemoved (AncestorEvent event) {
_metamgr.getFrameManager().removeFrameParticipant(MediaPanel.this);
}
});
}
/**
* Get the bounds of the viewport, in media coordinates. For the base MediaPanel, this will
* always be (0, 0, width, height).
*/
public Rectangle getViewBounds ()
{
return new Rectangle(getWidth(), getHeight());
}
/**
* Returns a reference to the animation manager used by this media panel.
*/
public AnimationManager getAnimationManager ()
{
return _metamgr.getAnimationManager();
}
/**
* Returns a reference to the sprite manager used by this media panel.
*/
public SpriteManager getSpriteManager ()
{
return _metamgr.getSpriteManager();
}
/**
* Returns a reference to the region manager used by this media panel.
*/
public RegionManager getRegionManager ()
{
return _metamgr.getRegionManager();
}
/**
* Pauses the sprites and animations that are currently active on this media panel. Also stops
* listening to the frame tick while paused.
*/
public void setPaused (boolean paused)
{
_metamgr.setPaused(paused);
}
/**
* Returns a timestamp from the {@link MediaTimer} used to track time intervals for this media
* panel. Note: this should only be called from the AWT thread.
*/
public long getTimeStamp ()
{
return _metamgr.getTimeStamp();
}
/**
* Adds a sprite to this panel.
*/
public void addSprite (Sprite sprite)
{
_metamgr.addSprite(sprite);
if (((sprite instanceof ActionSprite) ||
(sprite instanceof HoverSprite)) && (_actionSpriteCount++ == 0)) {
if (_actionHandler == null) {
_actionHandler = createActionSpriteHandler();
}
addMouseListener(_actionHandler);
addMouseMotionListener(_actionHandler);
}
}
/**
* @return true if the sprite is already added to this panel.
*/
public boolean isManaged (Sprite sprite)
{
return _metamgr.isManaged(sprite);
}
/**
* Removes a sprite from this panel.
*/
public void removeSprite (Sprite sprite)
{
_metamgr.removeSprite(sprite);
if (((sprite instanceof ActionSprite) ||
(sprite instanceof HoverSprite)) && (--_actionSpriteCount == 0)) {
removeMouseListener(_actionHandler);
removeMouseMotionListener(_actionHandler);
}
}
/**
* Removes all sprites from this panel.
*/
public void clearSprites ()
{
_metamgr.clearSprites();
if (_actionHandler != null) {
removeMouseListener(_actionHandler);
removeMouseMotionListener(_actionHandler);
_actionSpriteCount = 0;
}
}
/**
* Adds an animation to this panel. Animations are automatically
* removed when they finish.
*/
public void addAnimation (Animation anim)
{
_metamgr.addAnimation(anim);
}
/**
* @return true if the animation is already added to this panel.
*/
public boolean isManaged (Animation anim)
{
return _metamgr.isManaged(anim);
}
/**
* Aborts a currently running animation and removes it from this
* panel. Animations are normally automatically removed when they
* finish.
*/
public void abortAnimation (Animation anim)
{
_metamgr.abortAnimation(anim);
}
/**
* Removes all animations from this panel.
*/
public void clearAnimations ()
{
_metamgr.clearAnimations();
}
// from interface MediaHost
public Graphics2D createGraphics ()
{
return (Graphics2D)getGraphics();
}
// from interface FrameParticipant
public void tick (long tickStamp)
{
if (_metamgr.isPaused()) {
return;
}
// let derived classes do their business
willTick(tickStamp);
// tick our meta manager which will tick our sprites and animations
_metamgr.tick(tickStamp);
// let derived classes do their business
didTick(tickStamp);
// make a note that the next paint will correspond to a call to tick()
_tickPaintPending = true;
}
// from interface FrameParticipant
public boolean needsPaint ()
{
boolean needsPaint = _metamgr.needsPaint();
// if we have no dirty regions, clear our pending tick indicator because we're not going to
// get painted
if (!needsPaint) {
_tickPaintPending = false;
}
return needsPaint;
}
// from interface FrameParticipant
public Component getComponent ()
{
return this;
}
@Override
public void setOpaque (boolean opaque)
{
if (!opaque) {
log.warning("Media panels shouldn't be setOpaque(false).");
Thread.dumpStack();
}
super.setOpaque(true);
}
@Override
public void repaint (long tm, int x, int y, int width, int height)
{
if (width > 0 && height > 0) {
dirtyScreenRect(new Rectangle(x, y, width, height));
}
}
@Override
public void paint (Graphics g)
{
Graphics2D gfx = (Graphics2D)g;
// no use in painting if we're not showing or if we've not yet been validated
if (!isValid() || !isShowing()) {
return;
}
// if this isn't a tick paint, then we need to mark the clipping rectangle as dirty
if (!_tickPaintPending) {
Shape clip = g.getClip();
if (clip == null) {
// mark the whole component as dirty
repaint();
} else {
dirtyScreenRect(clip.getBounds());
}
// we used to bail out here and not render until our next tick, but it turns out that
// we need to render here because Swing may have repainted our parent over us and
// expect that we're going to paint ourselves on top of whatever it just painted, so we
// go ahead and paint now to avoid flashing
} else {
_tickPaintPending = false;
}
addObscurerDirtyRegions();
// if we have no invalid rects, there's no need to repaint
if (!_metamgr.getRegionManager().haveDirtyRegions()) {
return;
}
// get our dirty rectangles and delegate the main painting to a method that can be more
// easily overridden
Rectangle[] dirty = _metamgr.getRegionManager().getDirtyRegions();
_metamgr.noteDirty(dirty.length);
try {
paint(gfx, dirty);
} catch (Throwable t) {
log.warning(this + " choked in paint(" + dirty + ").", t);
}
// render our performance debugging if it's enabled
_metamgr.paintPerf(gfx);
}
/**
* Adds an element that could be obscuring the panel and thus requires extra redrawing.
*/
public void addObscurer (Obscurer obscurer) {
if (_obscurerList == null) {
_obscurerList = new ArrayList If derived classes wish to convert from screen coordinates to some virtual coordinate
* system to be used by their repaint manager, this is the place to do it.
*/
protected void dirtyScreenRect (Rectangle rect)
{
_metamgr.getRegionManager().addDirtyRegion(rect);
}
/**
* Paints behind all sprites and animations. The supplied invalid rectangle should be redrawn
* in the supplied graphics context. Sub-classes should override this method to do the actual
* rendering for their display.
*/
protected void paintBehind (Graphics2D gfx, Rectangle dirtyRect)
{
}
/**
* Paints between the front and back layer of sprites and animations. The supplied invalid
* rectangle should be redrawn in the supplied graphics context. Sub-classes should override
* this method to do the actual rendering for their display.
*/
protected void paintBetween (Graphics2D gfx, Rectangle dirtyRect)
{
}
/**
* Paints in front of all sprites and animations. The supplied invalid rectangle should be
* redrawn in the supplied graphics context. Sub-classes should override this method to do the
* actual rendering for their display.
*/
protected void paintInFront (Graphics2D gfx, Rectangle dirtyRect)
{
}
/**
* Renders the sprites and animations that intersect the supplied dirty region in the specified
* layer. Derived classes can override this method if they need to do custom sprite or
* animation rendering (if they need to do special sprite z-order handling, for example). The
* clipping region will already be set appropriately.
*/
protected void paintBits (Graphics2D gfx, int layer, Rectangle dirty)
{
_metamgr.paintMedia(gfx, layer, dirty);
}
/**
* Creates the mouse listener that will handle action sprites and their variants.
*/
protected ActionSpriteHandler createActionSpriteHandler ()
{
return new ActionSpriteHandler();
}
/** Handles ActionSprite/HoverSprite/ArmingSprite manipulation. */
protected class ActionSpriteHandler extends MouseInputAdapter
{
@Override
public void mousePressed (MouseEvent me)
{
if (_activeSprite == null) {
// see if we can find one
Sprite s = getHit(me);
if (s instanceof ActionSprite) {
_activeSprite = s;
}
}
if (_activeSprite instanceof ArmingSprite) {
((ArmingSprite) _activeSprite).setArmed(true);
}
}
@Override
public void mouseReleased (MouseEvent me)
{
if (_activeSprite instanceof ArmingSprite) {
((ArmingSprite)_activeSprite).setArmed(false);
}
if ((_activeSprite instanceof ActionSprite) &&
_activeSprite.hitTest(me.getX(), me.getY())) {
ActionEvent event;
if (_activeSprite instanceof CommandSprite) {
CommandSprite cs = (CommandSprite) _activeSprite;
event = new CommandEvent(
MediaPanel.this, cs.getActionCommand(),
cs.getCommandArgument(), me.getWhen(),
me.getModifiers());
} else {
ActionSprite as = (ActionSprite) _activeSprite;
event = new ActionEvent(
MediaPanel.this, ActionEvent.ACTION_PERFORMED,
as.getActionCommand(), me.getWhen(), me.getModifiers());
}
Controller.postAction(event);
}
if (!(_activeSprite instanceof HoverSprite)) {
_activeSprite = null;
}
mouseMoved(me);
}
@Override
public void mouseDragged (MouseEvent me)
{
if (_activeSprite instanceof ArmingSprite) {
((ArmingSprite) _activeSprite).setArmed(
_activeSprite.hitTest(me.getX(), me.getY()));
}
}
@Override
public void mouseMoved (MouseEvent me)
{
Sprite s = getHit(me);
if (_activeSprite == s) {
return;
}
if (_activeSprite instanceof HoverSprite) {
((HoverSprite) _activeSprite).setHovered(false);
}
_activeSprite = s;
if (_activeSprite instanceof HoverSprite) {
((HoverSprite) _activeSprite).setHovered(true);
}
}
/**
* Utility method, get the highest non-disabled action/hover sprite.
*/
protected Sprite getHit (MouseEvent me)
{
ArrayList