12ba2038b2
src/main/java. Prepared to move tests out into main project. git-svn-id: svn+ssh://src.earth.threerings.net/nenya/trunk@1040 ed5b42cb-e716-0410-a449-f6a68f950b19
459 lines
15 KiB
Java
459 lines
15 KiB
Java
//
|
|
// $Id$
|
|
//
|
|
// Nenya library - tools for developing networked games
|
|
// Copyright (C) 2002-2010 Three Rings Design, Inc., All Rights Reserved
|
|
// http://code.google.com/p/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.util.ArrayList;
|
|
|
|
import java.awt.Graphics;
|
|
import java.awt.Graphics2D;
|
|
import java.awt.Rectangle;
|
|
import java.awt.event.MouseEvent;
|
|
import java.awt.event.MouseWheelEvent;
|
|
|
|
import javax.swing.SwingUtilities;
|
|
|
|
import com.google.common.collect.Lists;
|
|
|
|
import com.samskivert.util.RunAnywhere;
|
|
|
|
import com.threerings.media.image.ImageUtil;
|
|
import com.threerings.media.image.Mirage;
|
|
import com.threerings.media.util.MathUtil;
|
|
import com.threerings.media.util.Pathable;
|
|
|
|
import static com.threerings.media.Log.log;
|
|
|
|
/**
|
|
* Extends the base media panel with the notion of a virtual coordinate system. All entities in
|
|
* the virtual media panel have virtual coordinates and the virtual media panel displays a window
|
|
* into that virtual view. The panel can be made to scroll by adjusting the view offset slightly
|
|
* at the start of each tick and it will efficiently copy the unmodified view data and generate
|
|
* repaint requests for the exposed regions.
|
|
*/
|
|
public class VirtualMediaPanel extends MediaPanel
|
|
{
|
|
/** The code for the pathable following mode wherein we keep the view centered on the
|
|
* pathable's location. */
|
|
public static final byte CENTER_ON_PATHABLE = 0;
|
|
|
|
/** The code for the pathable following mode wherein we ensure that the marked pathable is
|
|
* always kept within the visible bounds of the view. */
|
|
public static final byte ENCLOSE_PATHABLE = 1;
|
|
|
|
/** The code for the pathable following mode wherein we set the upper-left corner of the view
|
|
* to the coordinates of the pathable. */
|
|
public static final byte TRACK_PATHABLE = 2;
|
|
|
|
/**
|
|
* Constructs a virtual media panel.
|
|
*/
|
|
public VirtualMediaPanel (FrameManager framemgr)
|
|
{
|
|
super(framemgr);
|
|
}
|
|
|
|
/**
|
|
* Set a background image to tile the background of the media panel.
|
|
*/
|
|
public void setBackground (Mirage background)
|
|
{
|
|
_background = background;
|
|
}
|
|
|
|
/**
|
|
* Sets the upper-left coordinate of the view port in virtual coordinates. The view will be as
|
|
* efficient as possible about repainting itself to achieve this new virtual location (meaning
|
|
* that if we need only to move one pixel to the left, it will use {@link Graphics#copyArea}
|
|
* to move our rendered view over one pixel and generate a dirty region for the exposed area).
|
|
* The new location will not take effect until the view is {@link MediaPanel#tick}ed, so only
|
|
* the last call to this method during a tick will have any effect.
|
|
*/
|
|
public void setViewLocation (int x, int y)
|
|
{
|
|
// make a note of our new x and y offsets
|
|
_nx = x;
|
|
_ny = y;
|
|
}
|
|
|
|
/**
|
|
* Returns the bounds of the viewport in virtual coordinates. The
|
|
* returned rectangle must <em>not</em> be modified.
|
|
*/
|
|
@Override
|
|
public Rectangle getViewBounds ()
|
|
{
|
|
return _vbounds;
|
|
}
|
|
|
|
/**
|
|
* Adds an entity that will be informed when the view scrolls.
|
|
*/
|
|
public void addViewTracker (ViewTracker tracker)
|
|
{
|
|
_trackers.add(tracker);
|
|
}
|
|
|
|
/**
|
|
* Removes an entity from the view trackers list.
|
|
*/
|
|
public void removeViewTracker (ViewTracker tracker)
|
|
{
|
|
_trackers.remove(tracker);
|
|
}
|
|
|
|
/**
|
|
* Instructs the view to follow the supplied pathable; ensuring that the view's coordinates
|
|
* are adjusted according to the follow mode.
|
|
*
|
|
* @param pable the pathable to follow.
|
|
* @param followMode the strategy for keeping the pathable in view.
|
|
*/
|
|
public void setFollowsPathable (Pathable pable, byte followMode)
|
|
{
|
|
_fmode = followMode;
|
|
_fpath = pable;
|
|
trackPathable(); // immediately update our location
|
|
}
|
|
|
|
/**
|
|
* Clears out the pathable that was being enclosed or followed due to a previous call to
|
|
* {@link #setFollowsPathable}.
|
|
*/
|
|
public void clearPathable ()
|
|
{
|
|
_fpath = null;
|
|
_fmode = (byte) -1;
|
|
}
|
|
|
|
/**
|
|
* We overload this to translate mouse events into the proper coordinates before they are
|
|
* dispatched to any of the mouse listeners.
|
|
*/
|
|
@Override
|
|
protected void processMouseEvent (MouseEvent event)
|
|
{
|
|
event.translatePoint(_vbounds.x, _vbounds.y);
|
|
super.processMouseEvent(event);
|
|
}
|
|
|
|
/**
|
|
* We overload this to translate mouse events into the proper coordinates before they are
|
|
* dispatched to any of the mouse listeners.
|
|
*/
|
|
@Override
|
|
protected void processMouseMotionEvent (MouseEvent event)
|
|
{
|
|
event.translatePoint(_vbounds.x, _vbounds.y);
|
|
super.processMouseMotionEvent(event);
|
|
}
|
|
|
|
/**
|
|
* We overload this to translate mouse events into the proper coordinates before they are
|
|
* dispatched to any of the mouse listeners.
|
|
*/
|
|
@Override
|
|
protected void processMouseWheelEvent (MouseWheelEvent event)
|
|
{
|
|
event.translatePoint(_vbounds.x, _vbounds.y);
|
|
super.processMouseWheelEvent(event);
|
|
}
|
|
|
|
@Override
|
|
protected void dirtyScreenRect (Rectangle rect)
|
|
{
|
|
// translate the screen rect into happy coordinates
|
|
rect.translate(_vbounds.x, _vbounds.y);
|
|
_metamgr.getRegionManager().addDirtyRegion(rect);
|
|
}
|
|
|
|
@Override
|
|
public void doLayout ()
|
|
{
|
|
super.doLayout();
|
|
|
|
// we need to obtain our absolute screen coordinates to work
|
|
// around the Windows copyArea() bug
|
|
findRootBounds();
|
|
}
|
|
|
|
@Override
|
|
public void setBounds (int x, int y, int width, int height)
|
|
{
|
|
super.setBounds(x, y, width, height);
|
|
|
|
// keep track of the size of the viewport
|
|
_vbounds.width = getWidth();
|
|
_vbounds.height = getHeight();
|
|
|
|
// we need to obtain our absolute screen coordinates to work
|
|
// around the Windows copyArea() bug
|
|
findRootBounds();
|
|
}
|
|
|
|
@Override
|
|
protected void addObscurerDirtyRegion (Rectangle region)
|
|
{
|
|
// Adjust for any scrolling we're currently doing.
|
|
super.addObscurerDirtyRegion(
|
|
new Rectangle(region.x - _dx, region.y - _dy, region.width, region.height));
|
|
}
|
|
|
|
/**
|
|
* Determines the absolute screen coordinates at which this panel is located and stores them
|
|
* for reference later when rendering. This is necessary in order to work around the Windows
|
|
* <code>copyArea()</code> bug.
|
|
*/
|
|
protected void findRootBounds ()
|
|
{
|
|
_abounds.setLocation(0, 0);
|
|
FrameManager.getRoot(this, _abounds);
|
|
}
|
|
|
|
@Override
|
|
protected void didTick (long tickStamp)
|
|
{
|
|
super.didTick(tickStamp);
|
|
adjustBoundsCenter();
|
|
}
|
|
|
|
protected void adjustBoundsCenter ()
|
|
{
|
|
int width = getWidth(), height = getHeight();
|
|
|
|
// adjusts our view location to track any pathable we might be tracking
|
|
trackPathable();
|
|
|
|
// if we have a new target location, we'll need to generate dirty
|
|
// regions for the area exposed by the scrolling
|
|
if (_nx != _vbounds.x || _ny != _vbounds.y) {
|
|
// determine how far we'll be moving on this tick
|
|
int dx = _nx - _vbounds.x, dy = _ny - _vbounds.y;
|
|
|
|
// log.info("Scrolling into place [n=(" + _nx + ", " + _ny +
|
|
// "), t=(" + _vbounds.x + ", " + _vbounds.y +
|
|
// "), d=(" + dx + ", " + dy +
|
|
// "), width=" + width + ", height=" + height + "].");
|
|
|
|
_dx = dx;
|
|
_dy = dy;
|
|
|
|
// these are used to prevent the vertical strip from
|
|
// overlapping the horizontal strip
|
|
int sy = _ny, shei = height;
|
|
|
|
// and add invalid rectangles for the exposed areas
|
|
if (dy > 0) {
|
|
shei = Math.max(shei - dy, 0);
|
|
_metamgr.getRegionManager().invalidateRegion(_nx, _ny + height - dy, width, dy);
|
|
} else if (dy < 0) {
|
|
sy -= dy;
|
|
_metamgr.getRegionManager().invalidateRegion(_nx, _ny, width, -dy);
|
|
}
|
|
if (dx > 0) {
|
|
_metamgr.getRegionManager().invalidateRegion(_nx + width - dx, sy, dx, shei);
|
|
} else if (dx < 0) {
|
|
_metamgr.getRegionManager().invalidateRegion(_nx, sy, -dx, shei);
|
|
}
|
|
|
|
|
|
// now go ahead and update our location so that changes in between here and the call
|
|
// to paint() for this tick don't booch everything
|
|
_vbounds.x = _nx; _vbounds.y = _ny;
|
|
|
|
addObscurerDirtyRegions(false);
|
|
|
|
// let derived classes react if they so desire
|
|
viewLocationDidChange(dx, dy);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Called during our tick when we have adjusted the view location. The {@link #_vbounds} will
|
|
* already have been updated to reflect our new view coordinates.
|
|
*
|
|
* @param dx the delta scrolled in the x direction (in pixels).
|
|
* @param dy the delta scrolled in the y direction (in pixels).
|
|
*/
|
|
protected void viewLocationDidChange (int dx, int dy)
|
|
{
|
|
// inform our view trackers
|
|
for (int ii = 0, ll = _trackers.size(); ii < ll; ii++) {
|
|
_trackers.get(ii).viewLocationDidChange(dx, dy);
|
|
}
|
|
|
|
// pass the word on to our sprite/anim managers via the meta manager
|
|
_metamgr.viewLocationDidChange(dx, dy);
|
|
}
|
|
|
|
/**
|
|
* Implements the standard pathable tracking support. Derived classes may wish to override
|
|
* this if they desire custom tracking functionality.
|
|
*/
|
|
protected void trackPathable ()
|
|
{
|
|
// if we're tracking a pathable, adjust our view coordinates
|
|
if (_fpath == null) {
|
|
return;
|
|
}
|
|
|
|
int width = getWidth(), height = getHeight();
|
|
int nx = _vbounds.x, ny = _vbounds.y;
|
|
|
|
// figure out where to move
|
|
switch (_fmode) {
|
|
case TRACK_PATHABLE:
|
|
nx = _fpath.getX();
|
|
ny = _fpath.getY();
|
|
break;
|
|
|
|
case CENTER_ON_PATHABLE:
|
|
nx = _fpath.getX() - width/2;
|
|
ny = _fpath.getY() - height/2;
|
|
break;
|
|
|
|
case ENCLOSE_PATHABLE:
|
|
Rectangle bounds = _fpath.getBounds();
|
|
if (nx > bounds.x) {
|
|
nx = bounds.x;
|
|
} else if (nx + width < bounds.x + bounds.width) {
|
|
nx = bounds.x + bounds.width - width;
|
|
}
|
|
if (ny > bounds.y) {
|
|
ny = bounds.y;
|
|
} else if (ny + height < bounds.y + bounds.height) {
|
|
ny = bounds.y + bounds.height - height;
|
|
}
|
|
break;
|
|
|
|
default:
|
|
log.warning("Eh? Set to invalid pathable mode", "mode", _fmode);
|
|
break;
|
|
}
|
|
|
|
// Log.info("Tracking pathable [mode=" + _fmode +
|
|
// ", pable=" + _fpath + ", nx=" + nx + ", ny=" + ny + "].");
|
|
|
|
setViewLocation(nx, ny);
|
|
}
|
|
|
|
@Override
|
|
protected void paint (Graphics2D gfx, Rectangle[] dirty)
|
|
{
|
|
// if we're scrolling, go ahead and do the business
|
|
if (_dx != 0 || _dy != 0) {
|
|
int width = getWidth(), height = getHeight();
|
|
int cx = (_dx > 0) ? _dx : 0;
|
|
int cy = (_dy > 0) ? _dy : 0;
|
|
|
|
// set the clip to the bounds of the component (we can't assume the clip is anything
|
|
// sensible upon entry to paint() because the frame manager expects us to set our own
|
|
// clip)
|
|
gfx.setClip(0, 0, width, height);
|
|
|
|
// on windows, attempting to call copyArea() on a translated graphics context results
|
|
// in boochness; so we have to untranslate the graphics context, do our copyArea() and
|
|
// then translate it back
|
|
if (RunAnywhere.isWindows()) {
|
|
gfx.translate(-_abounds.x, -_abounds.y);
|
|
gfx.copyArea(_abounds.x + cx, _abounds.y + cy,
|
|
width - Math.abs(_dx),
|
|
height - Math.abs(_dy), -_dx, -_dy);
|
|
gfx.translate(_abounds.x, _abounds.y);
|
|
} else if (RunAnywhere.isMacOS()) {
|
|
try {
|
|
gfx.copyArea(cx, cy,
|
|
width - Math.abs(_dx),
|
|
height - Math.abs(_dy), -_dx, -_dy);
|
|
} catch (Exception e) {
|
|
// HACK when it throws an exception trying to do the copy area, just repaint
|
|
// everything
|
|
dirty = new Rectangle[] { new Rectangle(_vbounds) };
|
|
}
|
|
} else {
|
|
gfx.copyArea(cx, cy,
|
|
width - Math.abs(_dx),
|
|
height - Math.abs(_dy), -_dx, -_dy);
|
|
}
|
|
|
|
// and clear out our scroll deltas
|
|
_dx = 0; _dy = 0;
|
|
}
|
|
|
|
// translate into happy space
|
|
gfx.translate(-_vbounds.x, -_vbounds.y);
|
|
|
|
// now do the actual painting
|
|
super.paint(gfx, dirty);
|
|
|
|
// translate back out of happy space
|
|
gfx.translate(_vbounds.x, _vbounds.y);
|
|
}
|
|
|
|
@Override
|
|
protected void constrainToBounds (Rectangle dirty)
|
|
{
|
|
SwingUtilities.computeIntersection(
|
|
_vbounds.x, _vbounds.y, getWidth(), getHeight(), dirty);
|
|
}
|
|
|
|
@Override
|
|
protected void paintBehind (Graphics2D gfx, Rectangle dirtyRect)
|
|
{
|
|
// if we have a background image specified, tile it!
|
|
if (_background != null) {
|
|
// make sure it's aligned
|
|
int iw = _background.getWidth();
|
|
int ih = _background.getHeight();
|
|
int lowx = iw * MathUtil.floorDiv(dirtyRect.x, iw);
|
|
int lowy = ih * MathUtil.floorDiv(dirtyRect.y, ih);
|
|
ImageUtil.tileImage(gfx, _background, lowx, lowy,
|
|
dirtyRect.width + (dirtyRect.x - lowx),
|
|
dirtyRect.height + (dirtyRect.y - lowy));
|
|
}
|
|
}
|
|
|
|
/** Our viewport bounds in virtual coordinates. */
|
|
protected Rectangle _vbounds = new Rectangle();
|
|
|
|
/** Our target offsets to be effected on the next tick. */
|
|
protected int _nx, _ny;
|
|
|
|
/** Our scroll offsets. */
|
|
protected int _dx, _dy;
|
|
|
|
/** Our tiling background image. */
|
|
protected Mirage _background;
|
|
|
|
/** The mode we're using when following a pathable. */
|
|
protected byte _fmode = -1;
|
|
|
|
/** The pathable being followed. */
|
|
protected Pathable _fpath;
|
|
|
|
/** We need to know our absolute coordinates in order to work around
|
|
* the Windows copyArea() bug. */
|
|
protected Rectangle _abounds = new Rectangle();
|
|
|
|
/** A list of entities to be informed when the view scrolls. */
|
|
protected ArrayList<ViewTracker> _trackers = Lists.newArrayList();
|
|
}
|