Package com.threerings.stage.tools.editor

Source Code of com.threerings.stage.tools.editor.EditorScenePanel$SceneModelListener

//
// $Id$
//
// Vilya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/vilya/
//
// 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.stage.tools.editor;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;

import java.awt.AlphaComposite;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Composite;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Polygon;
import java.awt.Rectangle;
import java.awt.Shape;
import java.awt.Stroke;
import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent;
import java.awt.event.MouseEvent;
import java.awt.geom.Ellipse2D;

import javax.swing.BoundedRangeModel;
import javax.swing.DefaultBoundedRangeModel;
import javax.swing.JFrame;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;

import com.google.common.collect.Lists;

import com.samskivert.util.RandomUtil;

import com.samskivert.swing.Controller;
import com.samskivert.swing.TGraphics2D;
import com.samskivert.swing.util.SwingUtil;

import com.threerings.util.DirectionCodes;

import com.threerings.media.tile.ObjectTile;
import com.threerings.media.tile.Tile;
import com.threerings.media.tile.TileUtil;

import com.threerings.miso.client.MisoScenePanel;
import com.threerings.miso.client.ObjectActionHandler;
import com.threerings.miso.client.SceneBlock;
import com.threerings.miso.client.SceneObject;
import com.threerings.miso.data.MisoSceneModel;
import com.threerings.miso.data.ObjectInfo;
import com.threerings.miso.util.MisoUtil;

import com.threerings.whirled.spot.data.Portal;
import com.threerings.whirled.spot.tools.EditablePortal;

import com.threerings.stage.client.StageScenePanel;
import com.threerings.stage.data.StageLocation;
import com.threerings.stage.data.StageMisoSceneModel;
import com.threerings.stage.tools.editor.util.EditorContext;
import com.threerings.stage.tools.editor.util.EditorDialogUtil;
import com.threerings.stage.tools.editor.util.ExtrasPainter;

import static com.threerings.stage.Log.log;

/**
* Displays the scene view and handles UI events on the scene. Various actions may be performed on
* the scene depending on the selected action mode, including placing and deleting tiles or
* locations and creating portals.
*/
public class EditorScenePanel extends StageScenePanel
    implements EditorModelListener, ChangeListener
{
    public interface SceneModelListener
    {
        public void setMisoSceneModel (StageMisoSceneModel sceneModel);
    }

    /**
     * Constructs the editor scene view panel.
     */
    public EditorScenePanel (EditorContext ctx, JFrame frame, EditorModel model,
        SceneModelListener sceneListener)
    {
        super(ctx, new Controller() { });

        _sceneListener = sceneListener;

        // keep these around for later
        _ctx = ctx;
        _frame = frame;

        // save off references to our objects
        _emodel = model;
        _emodel.addListener(this);

        // listen for alt keys
        ctx.getKeyDispatcher().addGlobalKeyListener(this);

        // listen to our range models and scroll our badself
        _horizRange.addChangeListener(this);
        _vertRange.addChangeListener(this);
    }

    /**
     * Returns a range model that controls the scrollability of the scene
     * in the horizontal direction.
     */
    public BoundedRangeModel getHorizModel ()
    {
        return _horizRange;
    }

    /**
     * Returns a range model that controls the scrollability of the scene
     * in the vertical direction.
     */
    public BoundedRangeModel getVertModel ()
    {
        return _vertRange;
    }

    // documentation inherited from interface
    public void stateChanged (ChangeEvent e)
    {
        setViewLocation(_horizRange.getValue(), _vertRange.getValue());
        _refreshBox = true;
    }

    @Override
    public void setSceneModel (MisoSceneModel model)
    {
        super.setSceneModel(model);

        // compute the "area" in which we'll allow the view to scroll
        computeScrollArea();
    }

    /**
     * Updates the default tileset in the currently edited scene.
     */
    public void updateDefaultTileSet (int tileSetId)
    {
        _model.setDefaultBaseTileSet(tileSetId);
        _blocks.clear();
        rethink();
    }

    @Override
    public void setBounds (int x, int y, int width, int height)
    {
        super.setBounds(x, y, width, height);

        updateScrollArea(_horizRange.getValue(), _vertRange.getValue());

        rethink();
    }

    /**
     * Computes the area in which the view is allowed to scroll.
     */
    protected void computeScrollArea ()
    {
        StageMisoSceneModel ysmodel = (StageMisoSceneModel)_model;
        _area = null;
        for (Iterator<StageMisoSceneModel.Section> iter = ysmodel.getSections(); iter.hasNext(); ) {
            StageMisoSceneModel.Section sect = iter.next();
            Rectangle sbounds = MisoUtil.getFootprintPolygon(
                _metrics, sect.x, sect.y, ysmodel.swidth, ysmodel.sheight).getBounds();
            if (_area == null) {
                _area = sbounds;
            } else {
                _area.add(sbounds);
            }
        }

        // if we have no blocks, fake something up to start
        if (_area == null) {
            _area = new Rectangle(-250, -250, 500, 500);
        }

        updateScrollArea(_horizRange.getValue(), _vertRange.getValue());
    }

    /**
     * Updates our bounded range models to reflect potential changes in
     * the viewable area and the scrollable area.
     */
    protected void updateScrollArea (int hval, int vval)
    {
        int hmax = _area.x+_area.width;
        hval = Math.min(hval, hmax-_vbounds.width);
        _horizRange.setRangeProperties(
            hval, _vbounds.width, _area.x, hmax, false);

        int vmax = _area.y+_area.height;
        vval = Math.min(vval, vmax-_vbounds.height);
        _vertRange.setRangeProperties(
            vval, _vbounds.height, _area.y, vmax, false);

//         log.info("Updated extents area:" + StringUtil.toString(_area) +
//                  " vb:" + StringUtil.toString(_vbounds) + ".");
        // update the dimensions of the scrollbox

        // possibly refresh the dimensions of the box
        // and queue a repaint for it
        SwingUtil.refresh(_box);
        _refreshBox = true;
    }

    /**
     * Handle placing the currently selected tile at the given screen coordinates in the scene.
     */
    protected void placeTile (int x, int y)
    {
        markCheckpoint();

        Rectangle drag = clearTileSelectRegion(x, y);

        // sanity check
        if (!_emodel.isTileValid()) {
            return;
        }

        switch (_emodel.getLayerIndex()) {
        case EditorModel.BASE_LAYER:
            updateBaseTiles(x, y, drag, _emodel.getFQTileId(),
                            _emodel.getTileSetId(),
                            _emodel.getTileSet().getTileCount());
            break;

        case EditorModel.OBJECT_LAYER:
            addObject((ObjectTile)_emodel.getTile(),
                      _emodel.getFQTileId(), x, y);
            break;
        }

        // potentially update our scrollable area
        computeScrollArea();
    }

    /**
     * Handle deleting the tile at the given screen coordinates from
     * the scene.
     */
    protected void deleteTile (int x, int y)
    {
        markCheckpoint();

        Rectangle drag = clearTileSelectRegion(x, y);
        log.info("Deleting " + drag);

        switch (_emodel.getLayerIndex()) {
        case EditorModel.BASE_LAYER:
            updateBaseTiles(x, y, drag, 0, 0, 1);
            break;

        case EditorModel.OBJECT_LAYER:
            if (drag != null) {
                // locate any object that intersects this rectangle
                List<SceneObject> hits = Lists.newArrayList();
                for (SceneObject scobj: _vizobjs) {
                    if (scobj.objectFootprintOverlaps(drag)) {
                        hits.add(scobj);
                    }
                }
                // and delete 'em
                for (int ii = 0; ii < hits.size(); ii++) {
                    deleteObject(hits.get(ii));
                }

            } else {
                // delete the object tile over which the mouse is hovering
                if (_hobject instanceof SceneObject) {
                    deleteObject((SceneObject)_hobject);
                }
            }
            break;
        }
    }

    /**
     * Used to place or delete base tiles.
     */
    protected void updateBaseTiles (int x, int y, Rectangle drag,
                                    int fqTileId, int tileSetId, int tileCount)
    {
        if (drag == null) {
            setBaseTile(fqTileId, x, y);
        } else {
            setBaseTiles(drag, tileSetId, tileCount);
        }
    }

    /**
     * Handle editing the tile at the given screen coordinates from the
     * scene. If the tile is not an object tile, we don't do anything.
     */
    protected void editTile (int x, int y)
    {
        markCheckpoint();

        // bail if we're not hovering over a scene object
        if (_hobject == null || !(_hobject instanceof SceneObject)) {
            return;
        }

        // create our object editor dialog if we haven't yet
        if (_objEditor == null) {
            _objEditor = new ObjectEditorDialog(_ctx, this);
        }

        // prepare and display our object editor dialog
        _eobject = (SceneObject)_hobject;
        _objEditor.prepare(_eobject);
        EditorDialogUtil.display(_frame, _objEditor);
    }

    protected void selectTile (int x, int y)
    {
        // bail if we're not hovering over a scene object
        if (_hobject == null || !(_hobject instanceof SceneObject)) {
            return;
        }

        SceneObject scObj = (SceneObject)_hobject;
        int tileSetId = TileUtil.getTileSetId(scObj.info.tileId);
        int tileIndex = TileUtil.getTileIndex(scObj.info.tileId);
        _emodel.setTile(scObj.tile.key.tileSet, tileSetId, tileIndex);
        _emodel.setLayerIndex(EditorModel.OBJECT_LAYER);
    }

    /**
     * Called by the {@link ObjectEditorDialog} when it is dismissed.
     */
    protected void objectEditorDismissed ()
    {
        recomputeVisible();
        _model.updateObject(_eobject.info);
        _eobject = null;
    }

    /**
     * Pop up the portal dialog for the specified location.
     */
    protected void editPortal (EditablePortal portal)
    {
        markCheckpoint();

        // create our portal dialog if we haven't yet
        if (_dialogPortal == null) {
            _dialogPortal = new PortalDialog(_ctx, this);
        }

        // pass location information on to the dialog
        _dialogPortal.prepare(_scene, portal);

        // allow the user to edit the info
        EditorDialogUtil.display(_frame, _dialogPortal);

        // this gets called when a portal is added, so go ahead and
        // recompute them little buggers
        recomputePortals();
        recomputeVisible();
    }

    // documentation inherited
    public void modelChanged (int event)
    {
        switch (event) {
        case ACTION_MODE_CHANGED:
            switch (_emodel.getActionMode()) {
            case EditorModel.ACTION_PLACE_TILE:
                enableCoordHighlighting(false);
                if (_emodel.isTileValid()) {
                    setPlacingTile(_emodel.getTile());
                }
                break;

            case EditorModel.ACTION_EDIT_TILE:
                setPlacingTile(null);
                enableCoordHighlighting(false);
                break;

            case EditorModel.ACTION_PLACE_PORTAL:
                setPlacingTile(null);
                enableCoordHighlighting(true);
                break;
            }
            break;

        case TILE_CHANGED:
            if (_emodel.isTileValid() &&
                _emodel.getActionMode() == EditorModel.ACTION_PLACE_TILE) {
                setPlacingTile(_emodel.getTile());
            }
            break;
        }

        repaint();
    }

    @Override
    public void mousePressed (MouseEvent event)
    {
        int mx = event.getX(), my = event.getY();
        switch (_emodel.getActionMode()) {
        case EditorModel.ACTION_PLACE_TILE:
//             if (_emodel.getLayerIndex() == EditorModel.BASE_LAYER) {
                setTileSelectRegion(getTileCoords(mx, my));
//             }
            break;

        case EditorModel.ACTION_PLACE_PORTAL:
            markCheckpoint();

            Point fcoords = getFullCoords(mx, my);
            // mouse button three is delete
            if (event.getButton() == MouseEvent.BUTTON3) {
                deletePortal(fcoords.x, fcoords.y);
            } else {
                // if they clicked on an existing portal...
                EditablePortal portal = (EditablePortal)
                    getPortal(fcoords.x, fcoords.y);
                if (portal != null) {
                    // ...edit it...
                    editPortal(portal);
                } else {
                    // ...otherwise create a new one
                    new PortalTool().init(this, mx, my);
                }
            }
            break;

        default:
            super.mousePressed(event);
            break;
        }
    }

    @Override
    protected boolean handleMousePressed (Object hobject, MouseEvent event)
    {
        // don't do the standard cluster and location stuff here
        return false;
    }

    @Override
    public void mouseReleased (MouseEvent e)
    {
        super.mouseReleased(e);

        Point tc = getTileCoords(e.getX(), e.getY());
        switch (_emodel.getActionMode()) {
        case EditorModel.ACTION_PLACE_TILE:
            switch (e.getButton()) {
            case MouseEvent.BUTTON1:
                if ((e.getModifiers() & MouseEvent.CTRL_MASK) != 0) {
                    // They've got ctrl down, let's select that tile instead.
                    clearTileSelectRegion(tc.x, tc.y);
                    selectTile(tc.x, tc.y);
                } else {
                    placeTile(tc.x, tc.y);
                    _refreshBox = true;
                }
                break;

            case MouseEvent.BUTTON2:
                editTile(tc.x, tc.y);
                break;

            case MouseEvent.BUTTON3:
                deleteTile(tc.x, tc.y);
                _refreshBox = true;
                break;
            }
            break;

        case EditorModel.ACTION_EDIT_TILE:
            editTile(tc.x, tc.y);
            break;

        case EditorModel.ACTION_PLACE_PORTAL:
            // nothing to do here; the portal tool handles all
            break;
        }

        repaint();
    }

    @Override
    public void mouseMoved (MouseEvent e)
    {
        super.mouseMoved(e);
        int x = e.getX(), y = e.getY();
        boolean repaint = false;

        // update the potential tile placement
        if (_ptile != null) {
            boolean changed;

            if (_ptile instanceof ObjectTile) {
                changed = updateObjectTileCoords(x, y, _ppos, (ObjectTile)_ptile);
                if (changed) {
                    _pscobj.relocateObject(_metrics, _ppos.x, _ppos.y);
                }
            } else {
                changed = updateTileCoords(x, y, _ppos);
            }

            if (changed) {
                _validPlacement =
                    isTilePlacementValid(_ppos.x, _ppos.y, _ptile);
                repaint = true;
            }
        }

        // update the highlighted portal's fine coordinates
        if (_coordHighlighting) {
            repaint = (updateCoordPos(x, y, _hfull) || repaint);
        }

        // TODO: dirty things with a finer grain
        if (repaint) {
            repaint();
        }
    }

    @Override
    public void mouseDragged (MouseEvent e)
    {
        super.mouseDragged(e);
        // do the same thing as when we move
        mouseMoved(e);
    }

    @Override
    public void mouseExited (MouseEvent e)
    {
        super.mouseExited(e);

        // remove any highlighted tiles and placing tile
        _ppos.setLocation(Integer.MIN_VALUE, 0);
        _hfull.setLocation(Integer.MIN_VALUE, 0);
    }

    @Override
    public void keyPressed (KeyEvent e)
    {
        if (e.getKeyCode() == KeyEvent.VK_ALT) {
            // enable scene view tooltips
            setShowFlags(SHOW_TIPS, true);
        }
    }

    @Override
    public void keyReleased (KeyEvent e)
    {
        if (e.getKeyCode() == KeyEvent.VK_ALT) {
            // disable scene view tooltips
            setShowFlags(SHOW_TIPS, false);
        }
    }

    @Override
    protected void fireObjectAction (
        ObjectActionHandler handler, SceneObject scobj, ActionEvent event)
    {
        // do nothing in the editor thanksverymuch
    }

    /**
     * A place for subclasses to react to the hover object changing.
     * One of the supplied arguments may be null.
     */
    @Override
    protected void hoverObjectChanged (Object oldHover, Object newHover)
    {
        super.hoverObjectChanged(oldHover, newHover);

        // we always repaint our objects when the hover changes
        if (oldHover instanceof SceneObject) {
            SceneObject oldhov = (SceneObject)oldHover;
            _remgr.invalidateRegion(oldhov.getObjectFootprint().getBounds());
        }
        if (newHover instanceof SceneObject) {
            SceneObject newhov = (SceneObject)newHover;
            _remgr.invalidateRegion(newhov.getObjectFootprint().getBounds());
        }
    }

    /**
     * Sets a base tile at the specified position in the scene (in tile
     * coordinates).
     *
     * @return - if the tile was successfully set
     */
    public boolean setBaseTile (int fqTileId, int x, int y)
    {
        if (!_model.setBaseTile(fqTileId, x, y)) {
            return false;
        }
        SceneBlock block = getBlock(x, y);
        if (block != null && block.isResolved()) {
            block.updateBaseTile(fqTileId, x, y);
        }

        // and recompute any surrounding fringe
        for (int fx = x - 1, xn = x + 1; fx <= xn; fx++) {
            for (int fy = y - 1, yn = y + 1; fy <= yn; fy++) {
                block = getBlock(fx, fy);
                if (block != null && block.isResolved()) {
                    block.updateFringe(fx, fy);
                }
            }
        }
        return true;
    }

    /**
     * Set a region of tiles to a random selection from the supplied tileset.
     */
    public void setBaseTiles (Rectangle r, int setId, int tileCount)
    {
        for (int x = r.x; x < r.x + r.width; x++) {
            for (int y = r.y; y < r.y + r.height; y++) {
                int index = RandomUtil.getInt(tileCount);
                int fqTileId = TileUtil.getFQTileId(setId, index);
                setBaseTile(fqTileId, x, y);
            }
        }
    }

    /**
     * Sets an object tile at the specified position in the scene (in tile coordinates).
     *
     * @return - the created object or null if an identical object was already in that spot.
     */
    public ObjectInfo addObject (ObjectTile tile, int fqTileId, int x, int y)
    {
        Point p = new Point(x, y);
        adjustObjectCoordsAccordingToGrip(p, tile);
        return addObject(new ObjectInfo(fqTileId, x, y));
    }

    /**
     * Adds the given object to the scene.
     *
     * @return the added object or null if an identical object was already in that spot.
     */
    public ObjectInfo addObject (ObjectInfo oinfo)
    {
        // first attempt to add it to the appropriate scene block; this
        // will fail if there's already a copy of the same object at this
        // coordinate
        SceneBlock block = getBlock(oinfo.x, oinfo.y);
        if (block== null || !block.isResolved() || block.addObject(oinfo)) {
            // create an object info and add it to the scene model
            if (_model.addObject(oinfo)) {
                // recompute our visible object set
                recomputeVisible();
                return oinfo;
            }
        }
        return null;
    }

    /**
     * Deletes the object tile at the specified tile coordinates.
     *
     * @return true - if a matching object was found and deleted.
     */
    public boolean deleteObject (SceneObject scobj)
    {
        if (deleteObject(scobj.info)) {
            // make sure we clear the hover if that's what we're deleting
            if (_hobject == scobj) {
                _hobject = null;
            }
            return true;
        }
        log.warning("Requested to remove unknown object " + scobj + ".");
        return false;
    }

    /**
     * Delete the given object from the scene.
     *
     * @return true - if a matching object was found and deleted.
     */
    public boolean deleteObject (ObjectInfo info)
    {
        // remove it from the scene model
        if (_model.removeObject(info)) {
            // clear the object out of its block
            SceneBlock block = getBlock(info.x, info.y);
            if (block != null && block.isResolved()) {
                block.deleteObject(info);
            }

            // recompute our visible object set
            recomputeVisible();
            return true;
        }
        return false;
    }

    /**
     * Sets the tile that is currently being placed. It will not be
     * rendered until after a call to {@link MisoScenePanel#updateTileCoords} on the
     * placing tile (which happens automatically when the mouse moves).
     */
    public void setPlacingTile (Tile tile)
    {
        _ptile = tile;

        // if this is an object tile, create a temporary scene object we
        // can use to perform calculations with the object while placing
        if (_ptile instanceof ObjectTile) {
            _pscobj = new SceneObject(this, new ObjectInfo(0, _ppos.x, _ppos.y), (ObjectTile)tile);
        } else {
            _pscobj = null;
        }
    }

    /**
     * Sets the start (in tile coords) of a mouse drag when placing
     * a rectangular area of base tiles.
     */
    public void setTileSelectRegion (Point drag)
    {
        _drag = drag;
    }

    /**
     * Clear and return the drag rectangle for selecting a rectangular
     * region.
     *
     * @return null if the drag is the same as the supplied tile
     * coordinates, a rectangle containing the selected region if it was
     * different.
     */
    public Rectangle clearTileSelectRegion (int x, int y)
    {
        Rectangle drect = null;
        if (_drag != null && (x != _drag.x || y != _drag.y)) {
            int w = 1 + ((x > _drag.x) ? (x - _drag.x) : (_drag.x - x));
            int h = 1 + ((y > _drag.y) ? (y - _drag.y) : (_drag.y - y));
            drect = new Rectangle(
                Math.min(x, _drag.x), Math.min(y, _drag.y), w, h);
        }
        _drag = null;
        return drect;
    }

    /**
     * Enables or disables highlighting of the tile over which the mouse
     * is currently positioned.
     */
    public void enableCoordHighlighting (boolean enabled)
    {
        _coordHighlighting = enabled;
    }

    /**
     * Deletes the portal at the specified full coordinates.
     */
    public void deletePortal (int x, int y)
    {
        Portal port = getPortal(x, y);
        if (port != null) {
            _scene.removePortal(port);
            recomputePortals();
            recomputeVisible();
        }
    }

    /**
     * Returns the portal that serves as the default entrance to this
     * scene or null if no default is set.
     */
    public Portal getEntrance ()
    {
        return _scene.getDefaultEntrance();
    }

    /**
     * Makes the specified portal the default entrance to this scene.
     */
    public void setEntrance (Portal port)
    {
        _scene.setDefaultEntrance(port);
    }

    @Override
    protected void recomputeVisible ()
    {
        super.recomputeVisible();

        // see if any of our visible objects overlap and mark them as bad
        // monkeys; we love N^2 algorithms
        for (SceneObject scobj : _vizobjs) {
            scobj.setWarning(overlaps(scobj));
        }
    }

    @Override
    protected void warnVisible (SceneBlock block, Rectangle sbounds)
    {
        // nothing doing
    }

    /** Helper function for {@link #recomputeVisible}. */
    protected boolean overlaps (SceneObject tobj)
    {
        for (SceneObject scobj : _vizobjs) {
            if (scobj != tobj && tobj.objectFootprintOverlaps(scobj) &&
                tobj.getPriority() == scobj.getPriority()) {
                return true;
            }
        }
        return false;
    }

    protected void paintHighlights (Graphics2D gfx, Rectangle dirty)
    {
        Polygon hpoly = null;

        if (_hobject != null && _hobject instanceof SceneObject) {
            SceneObject scobj = (SceneObject)_hobject;
            hpoly = scobj.getObjectFootprint();

        }

        if (_emodel.getActionMode() == EditorModel.ACTION_PLACE_TILE &&
            (hpoly == null ||
             _emodel.getLayerIndex() == EditorModel.BASE_LAYER)) {
            hpoly = MisoUtil.getTilePolygon(_metrics, _hcoords.x, _hcoords.y);
        }

        if (hpoly != null) {
            gfx.setColor(Color.green);
            gfx.draw(hpoly);
        }

        // paint the highlighted full coordinate
        if (_coordHighlighting && _hfull.x != Integer.MIN_VALUE) {
            Point spos = new Point();
            MisoUtil.fullToScreen(_metrics, _hfull.x, _hfull.y, spos);

            // set the desired stroke and color
            Stroke ostroke = gfx.getStroke();
            gfx.setStroke(HIGHLIGHT_STROKE);

            // draw a red circle at the coordinate
            gfx.setColor(Color.red);
            gfx.draw(new Ellipse2D.Float(spos.x - 1, spos.y - 1, 3, 3));

            // restore the original stroke
            gfx.setStroke(ostroke);
        }
    }

    @Override
    protected void paintExtras (Graphics2D gfx, Rectangle dirty)
    {
        super.paintExtras(gfx, dirty);

        // we don't want to paint the 'extras' stuff to the copy..
        if (gfx instanceof TGraphics2D) {
            gfx = ((TGraphics2D) gfx).getPrimary();
        }

        paintPortals(gfx);
        paintHighlights(gfx, dirty);
        paintPlacingTile(gfx);

        // and call into any extras painters
        for (int ii = 0; ii < _extras.size(); ii++) {
            _extras.get(ii).paintExtras(gfx);
        }
    }

    /**
     * Add an extras painter.
     */
    protected void addExtrasPainter (ExtrasPainter painter)
    {
        _extras.add(painter);
    }

    /**
     * Remove the specified extras painter.
     */
    protected void removeExtrasPainter (ExtrasPainter painter)
    {
        _extras.remove(painter);
    }

    /**
     * Paints a transparent image of the tile being placed and draws a
     * highlight around the bounds of the tile's current prospective
     * position. The highlight is drawn in green if the tile placement is
     * valid, or red if not.
     *
     * @param gfx the graphics context.
     */
    protected void paintPlacingTile (Graphics2D gfx)
    {
        // bail if we've no placing tile
        if (_ptile == null || _ppos.x == Integer.MIN_VALUE) {
            return;
        }

        // draw a transparent rendition of the placing tile image
        Composite ocomp = gfx.getComposite();
        gfx.setComposite(ALPHA_PLACING);
        Shape bpoly;
        if (_pscobj != null) {
            _pscobj.paint(gfx);
            bpoly = _pscobj.getObjectFootprint();

        } else {
            bpoly = MisoUtil.getTilePolygon(_metrics, _ppos.x, _ppos.y);
            Rectangle bounds = bpoly.getBounds();
            _ptile.paint(gfx, bounds.x, bounds.y);
        }
        gfx.setComposite(ocomp);

        // if we're dragging, grab that footprint
        if (_drag != null) {
            bpoly = MisoUtil.getMultiTilePolygon(_metrics, _ppos, _drag);
        }

        // draw an outline around the tile footprint
        gfx.setColor(_validPlacement ? Color.blue : Color.red);
        gfx.draw(bpoly);
    }

    /**
     * Paint demarcations at all portals in the scene.
     *
     * @param gfx the graphics context.
     */
    protected void paintPortals (Graphics2D gfx)
    {
        Iterator<Portal> iter = _scene.getPortals();
        while (iter.hasNext()) {
            paintPortal(gfx, (EditablePortal)iter.next());
        }
    }

    /**
     * Paint the specified portal.
     */
    protected void paintPortal (Graphics2D gfx, EditablePortal port)
    {
        paintLocation(gfx, (StageLocation)port.loc, Color.BLUE,
            port.equals(_scene.getDefaultEntrance()));
    }

    /**
     * Paint the specified StageLocation
     */
    protected void paintLocation (Graphics2D gfx, StageLocation loc, Color color, boolean highlight)
    {
        // get the portal's center coordinate
        Point spos = new Point();
        MisoUtil.fullToScreen(_metrics, loc.x, loc.y, spos);
        int cx = spos.x, cy = spos.y;

        // translate the origin to center on the portal
        gfx.translate(cx, cy);

        // rotate to reflect the portal orientation
        double rot = (Math.PI / 4.0f) * loc.orient;
        gfx.rotate(rot);

        // draw the triangle
        gfx.setColor(color);
        gfx.fill(_locTri);

        // outline the triangle in black
        gfx.setColor(Color.black);
        gfx.draw(_locTri);

        // draw the rectangle
        gfx.setColor(Color.red);
        gfx.fillRect(-1, 2, 3, 3);

        // restore the original transform
        gfx.rotate(-rot);
        gfx.translate(-cx, -cy);

        // highlight the portal if it's the default entrance
        if (highlight) {
            gfx.setColor(Color.cyan);
            gfx.drawRect(spos.x - 5, spos.y - 5, 10, 10);
        }
    }

    /**
     * Updates the coordinate position and returns true if it has changed.
     */
    public boolean updateCoordPos (int x, int y, Point cpos)
    {
        Point npos = MisoUtil.screenToFull(_metrics, x, y, new Point());
        if (!cpos.equals(npos)) {
            cpos.setLocation(npos.x, npos.y);
            return true;
        } else {
            return false;
        }
    }

    /**
     * Returns whether placing a tile at the given coordinates in the
     * scene is valid.  Makes sure placing an object fits within the scene
     * and doesn't overlap any other objects.
     */
    protected boolean isTilePlacementValid (int x, int y, Tile tile)
    {
        if (tile instanceof ObjectTile) {
            // create a temporary scene object for this tile
            SceneObject nobj = new SceneObject(this, new ObjectInfo(0, x, y), (ObjectTile)tile);
            // report invalidity if overlaps any existing objects
            int ocount = _vizobjs.size();
            for (int ii = 0; ii < ocount; ii++) {
                SceneObject scobj = _vizobjs.get(ii);
                if (scobj.objectFootprintOverlaps(nobj)) {
                    return false;
                }
            }
        }

        return true;
    }

    @Override
    protected boolean skipHitObject (SceneObject scobj)
    {
        return false; // skip nothing
    }

    /**
     * Converts the supplied screen coordinates into tile coordinates for
     * an object tile. (See {@link MisoScenePanel#updateTileCoords}.)
     *
     * @return true if the tile coordinates have changed.
     */
    protected boolean updateObjectTileCoords (int sx, int sy, Point tpos,
                                              ObjectTile otile)
    {
        Point npos = new Point();
        MisoUtil.screenToTile(_metrics, sx, sy, npos);
        adjustObjectCoordsAccordingToGrip(npos, otile);
        if (!tpos.equals(npos)) {
            tpos.setLocation(npos.x, npos.y);
            return true;
        } else {
            return false;
        }
    }

    /**
     * Alter the position of the object according to which corner
     * we are holding it by.
     */
    protected void adjustObjectCoordsAccordingToGrip (Point p, ObjectTile tile)
    {
        int dy = Math.max(3, (tile.getHeight() / _metrics.tilehei));
        int dx = Math.max(3, (tile.getWidth() / _metrics.tilewid));

        switch (_emodel.getObjectGripDirection()) {
        case DirectionCodes.NORTH:
            p.x += dy;
            p.y += dy;
            break;

        case DirectionCodes.WEST:
            p.x += dx;
            break;

        case DirectionCodes.EAST:
            p.y += dx;
            break;

        case DirectionCodes.NORTHWEST:
            p.x += dy + (dx / 2);
            p.y += dy - (dx / 2);
            break;

        case DirectionCodes.NORTHEAST:
            p.x += dy - (dx / 2);
            p.y += dy + (dx / 2);
            break;

        case DirectionCodes.SOUTHWEST:
            p.x += (dx / 2);
            p.y -= (dx / 2);
            break;

        case DirectionCodes.SOUTHEAST:
            p.x -= (dx / 2);
            p.y += (dx / 2);
            break;
        }
    }

    /**
     * Sets the editor model.
     */
    protected void setEditorModel (EditorModel model)
    {
        _emodel = model;
    }

    /**
     * Set the scroll box that tracks our view.
     */
    public void setEditorScrollBox (EditorScrollBox box)
    {
        _box = box;
    }

    @Override
    protected void paint (Graphics2D gfx, Rectangle[] dirty)
    {
        // if we need to refresh the box and we have all the scene data, do it
        if (_refreshBox && _visiBlocks.isEmpty()) {
            _refreshBox = false;
            Graphics2D mini = _box.getMiniGraphics();
            Graphics2D t = new TGraphics2D(gfx, mini);
            super.paint(t, dirty);
            mini.dispose();
            _box.repaint();

        } else {
            // otherwise, just do a normal fast paint
            super.paint(gfx, dirty);
        }
    }

    protected void markCheckpoint ()
    {
        _undo.preserve();
        _redo.clear();
    }

    public void undo ()
    {
        if (_undo.size() > 0) {
            _redo.preserve();
            _undo.rollback();
        }
    }

    public void redo ()
    {
        if (_redo.size() > 0) {
            _undo.preserve();
            _redo.rollback();
        }
    }

    protected class UndoStack
    {
        public void preserve () {
            _list.addFirst(_model.clone());

            if (_list.size() > MAX_UNDO_SIZE) {
                _list.removeLast();
            }
        }

        public void rollback () {
            if (_list.size() > 0) {
                _model = _list.removeFirst();
                _sceneListener.setMisoSceneModel((StageMisoSceneModel)_model);
                refreshScene();
            }
        }

        public int size () {
            return _list.size();
        }

        public void clear () {
            _list.clear();
        }

        LinkedList<MisoSceneModel> _list = Lists.newLinkedList();
    }

    protected static final int MAX_UNDO_SIZE = 20;

    /** Provides access to stuff. */
    protected EditorContext _ctx;

    /** Our editor model. */
    protected EditorModel _emodel;

    /** The scrollbox that tracks our view. */
    protected EditorScrollBox _box;

    /** Do we need to refresh the image being displayed in our scrollbox? */
    protected boolean _refreshBox;

    /** We need this to create our dialogs when they are needed. */
    protected JFrame _frame;

    /** Allows scrolling horizontally. */
    protected BoundedRangeModel _horizRange = new DefaultBoundedRangeModel();

    /** Allows scrolling vertically. */
    protected BoundedRangeModel _vertRange = new DefaultBoundedRangeModel();

    /** The virtual screen rectangle around which we scroll. */
    protected Rectangle _area;

    /** Whether or not coordinate highlighting is enabled. */
    protected boolean _coordHighlighting;

    /** The currently highlighted full coordinate. */
    protected Point _hfull = new Point(Integer.MIN_VALUE, 0);

    /** The location of the start of a tile drag in tile coords. */
    protected Point _drag = null;

    /** The position of the tile currently being placed. */
    protected Point _ppos = new Point(Integer.MIN_VALUE, 0);

    /** Used to track whether or not the current "placing" tile is in a
     * valid position. */
    protected boolean _validPlacement = false;

    /** The tile currently being placed. */
    protected Tile _ptile;

    /** Metrics for the tile currently being placed if it is an object
     * tile. */
    protected SceneObject _pscobj;

    /** A list of things that will do some extra painting for us. */
    protected ArrayList<ExtrasPainter> _extras = Lists.newArrayList();

    /** The dialog providing portal edit functionality. */
    protected PortalDialog _dialogPortal;

    /** The dialog providing object edit functionality. */
    protected ObjectEditorDialog _objEditor;

    /** The object currently being edited by the object editor dialog. */
    protected SceneObject _eobject;

    protected UndoStack _undo = new UndoStack();
    protected UndoStack _redo = new UndoStack();

    protected SceneModelListener _sceneListener;

    /** The triangle used to render a portal on-screen. */
    protected static Polygon _locTri;

    static {
        _locTri = new Polygon();
        _locTri.addPoint(-3, -3);
        _locTri.addPoint(3, -3);
        _locTri.addPoint(0, 3);
    }

    /** The stroke object used to draw highlighted tiles and coordinates. */
    protected static final Stroke HIGHLIGHT_STROKE = new BasicStroke(2);

    /** Alpha level used to render transparent placing tile image. */
    protected static final Composite ALPHA_PLACING =
        AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.5f);
}
TOP

Related Classes of com.threerings.stage.tools.editor.EditorScenePanel$SceneModelListener

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.