Package org.jdesktop.wonderland.modules.appbase.client.view

Source Code of org.jdesktop.wonderland.modules.appbase.client.view.View2DEntity

/**
* Open Wonderland
*
* Copyright (c) 2010 - 2011, Open Wonderland Foundation, All Rights Reserved
*
* Redistributions in source code form must reproduce the above
* copyright and this condition.
*
* The contents of this file are subject to the GNU General Public
* License, Version 2 (the "License"); you may not use this file
* except in compliance with the License. A copy of the License is
* available at http://www.opensource.org/licenses/gpl-license.php.
*
* The Open Wonderland Foundation designates this particular file as
* subject to the "Classpath" exception as provided by the Open Wonderland
* Foundation in the License file that accompanied this code.
*/

/**
* Project Wonderland
*
* Copyright (c) 2004-2009, Sun Microsystems, Inc., All Rights Reserved
*
* Redistributions in source code form must reproduce the above
* copyright and this condition.
*
* The contents of this file are subject to the GNU General Public
* License, Version 2 (the "License"); you may not use this file
* except in compliance with the License. A copy of the License is
* available at http://www.opensource.org/licenses/gpl-license.php.
*
* Sun designates this particular file as subject to the "Classpath"
* exception as provided by Sun in the License file that accompanied
* this code.
*/
package org.jdesktop.wonderland.modules.appbase.client.view;

import org.jdesktop.mtgame.Entity;
import com.jme.image.Image;
import com.jme.image.Texture;
import com.jme.image.Texture2D;
import com.jme.math.Quaternion;
import com.jme.math.Vector2f;
import com.jme.math.Vector3f;
import com.jme.scene.Node;
import com.jme.scene.Spatial;
import java.awt.Point;
import com.jme.scene.state.TextureState;
import java.awt.Dimension;
import java.awt.event.MouseEvent;
import java.util.LinkedList;
import java.util.logging.Logger;
import org.jdesktop.mtgame.CollisionComponent;
import org.jdesktop.mtgame.EntityComponent;
import org.jdesktop.mtgame.JMECollisionSystem;
import org.jdesktop.mtgame.RenderComponent;
import org.jdesktop.mtgame.RenderUpdater;
import org.jdesktop.wonderland.client.input.EventListener;
import org.jdesktop.wonderland.client.jme.ClientContextJME;
import org.jdesktop.wonderland.client.jme.input.MouseDraggedEvent3D;
import org.jdesktop.wonderland.client.jme.input.MouseEvent3D;
import org.jdesktop.wonderland.client.jme.input.MouseWheelEvent3D;
import org.jdesktop.wonderland.common.ExperimentalAPI;
import org.jdesktop.wonderland.common.InternalAPI;
import org.jdesktop.wonderland.common.cell.CellTransform;
import org.jdesktop.wonderland.modules.appbase.client.App2D;
import org.jdesktop.wonderland.modules.appbase.client.ControlArb;
import org.jdesktop.wonderland.modules.appbase.client.Window2D;
import org.jdesktop.wonderland.modules.appbase.client.view.View2D.Type;
import java.awt.Button;
import org.jdesktop.wonderland.modules.appbase.client.DrawingSurface;
import org.jdesktop.wonderland.modules.appbase.client.swing.WindowSwing;

/**
* A view which is capable of displaying a window using an MTGame Entity and scene graph.
* At any one time the window contents can be displayed in the 3D world (<code>setOrtho(false)</code)
* or in the ortho plane (<code>setOrtho(true)</code). (The ortho plane usually corresponds to the
* Wonderland main HUD, or "on the glass").
* <br><br>
* By default, the view is a quad, but this can be changed via the method <code>setGeometryNode</code>.
* <br><br>
* If the user resizable attribute is set to true via <code>setUserResizable</code> this view's dimensions
* can be dynamically controlled from elsewhere (often via a GUI element) by calling the methods
* <code>userResizeStart</code>, <code>userResizeUpdate</code> and <code>userResizeFinish</code>.
* <br><br>
* A view also supports a number of window manipulation methods which relay actions to the view's window.
* Examples include windowCloseUser and windowRestackToTop.
* <br><br>
* Note: this class supports independent pixel scales for the 3D world and the ortho plane. The values
* set via <code>setPixelScale</code> are used for the 3D world and the values set via <code>setPixelScaleOrtho</code>
* are used for the ortho plane.
* <br><br>
* This view class also supports a set of positioning methods for when the view is in the ortho plane
* (e.g. <code>setLocationOrtho</code>) than when the view is in the 3D world (see the <code>View2D</code) class).
* <br><br>
* You can move this type of view in the local XY plane when it is in the 3D world by calling
* <code>applyDeltaTranslationUser</code>. This post-multiplies a translation matrix into the view's
* current world-to-local transform matrix. Certain utility routines exit (e.g. <code>userMovePlanarStart</code>,
* <code>userMovePlanarUpdate</code>, and <code>userMovePlanarUpdate</code>) can be called instead in order to make this easier.
* (Only view's of type SECONDARY can be moved planar).
* <br><br>
* TODO: SOMEDAY: Implement move planar for primary views. Must update the cell when doing this.
* <br><br>
* @author deronj
*/
@ExperimentalAPI
public abstract class View2DEntity implements View2D {

    // IMPLEMENTATION NOTE: The entity and scene graph of this type of view has the following structure
    // Entity -> viewNode -> geometryNode -> Geometry

    private static final Logger logger = Logger.getLogger(View2DEntity.class.getName());

    private static float PIXEL_SCALE_DEFAULT = 0.01f;

    private static enum AttachState { DETACHED, ATTACHED_TO_ENTITY, ATTACHED_TO_WORLD };

    private static enum FrameChange {
        ATTACH_FRAME, DETACH_FRAME, REATTACH_FRAME, UPDATE_TITLE, UPDATE_USER_RESIZABLE
    };
           
    // Attribute changed flags
    protected static final int CHANGED_TYPE             = 0x0001;
    protected static final int CHANGED_PARENT           = 0x0002;
    protected static final int CHANGED_VISIBLE          = 0x0004;
    protected static final int CHANGED_DECORATED        = 0x0008;
    protected static final int CHANGED_GEOMETRY         = 0x0010;
    protected static final int CHANGED_SIZE_APP         = 0x0020;
    protected static final int CHANGED_PIXEL_SCALE      = 0x0040;
    protected static final int CHANGED_OFFSET           = 0x0080;
    protected static final int CHANGED_USER_TRANSFORM   = 0x0100;
    protected static final int CHANGED_TITLE            = 0x0200;
    protected static final int CHANGED_STACK            = 0x0400;
    protected static final int CHANGED_ORTHO            = 0x0800;
    protected static final int CHANGED_LOCATION_ORTHO   = 0x1000;
    protected static final int CHANGED_TEX_COORDS       = 0x2000;
    protected static final int CHANGED_USER_RESIZABLE   = 0x4000;

    protected static final int CHANGED_ALL = -1;

    /** The app to which the view belongs. */
    private App2D app;

    /** The name of the view. */
    protected String name;

    /** The entity of this view. */
    protected Entity entity;

    /** The parent entity to which this entity is connected. */
    private Entity parentEntity;

    /** The base node of the view. */
    protected Node viewNode;

    /** The textured 3D object in which displays the view contents. */
    protected GeometryNode geometryNode;

    /** The new geometry object to be used. */
    protected GeometryNode newGeometryNode;

    /** Whether we have created the geometry node ourselves. */
    private boolean geometrySelfCreated;

    /** The control arbitrator of the window being displayed. */
    private ControlArb controlArb;

    /** The view's window. */
    protected Window2D window;

    /** The type of this view. */
    protected Type type = Type.UNKNOWN;

    /** The parent view of this view. */
    protected View2DEntity parent;

    /** Whether the app wants the view to be visible. */
    private boolean visibleApp;

    /** Whether the user wants the view to be visible. */
    private boolean visibleUser;

    /** Whether this view should be decorated by a frame. */
    private boolean decorated;

    /** Whether this view's frame resize corner is enabled. */
    protected boolean userResizable = false;

    /** The frame title. */
    private String title;

    /** The size of the view specified by the app. */
    private Dimension sizeApp = new Dimension(1, 1);

    /** The size of the displayed pixels (in local units) when view is in cell mode. */
    private Vector2f pixelScaleCell;

    /** The size of the displayed pixels (in local units) when view is in ortho mode. */
    private Vector2f pixelScaleOrtho;

    /** The location of the view when view is in ortho mode. */
    private Vector2f locationOrtho = new Vector2f(0, 0);

    /** The interactive GUI object for this view. */
    private Gui2DInterior gui;

    /** The local offset translation from the center of the parent to the center of this view. */
    private Vector2f offset = new Vector2f(0f, 0f);

    /** The pixel offset translation from the top left corner of the parent to the top left corner of this view. */
    private Point pixelOffset = new Point(0, 0);

    /** The previous drag vector during an interactive planar move. */
    private Vector2f userMovePlanarDragVectorPrev;
   
    /** The next delta translation to apply. */
    private Vector3f deltaTranslationToApply;

    /** A copy of the current view node's user transformation in world (aka non-ortho) mode. */
    protected CellTransform userTransformCell = new CellTransform(null, null);

    /** True if the user transform cell was changed by an entire matrix replacement */
    protected boolean userTransformCellReplaced = false;

    /** True if we shouldn't bother informing other clients of modifications to the user transform cell. */
    protected boolean userTransformCellChangedLocalOnly = false;

    /** A copy of the current view node's user transformation in ortho mode. */
    private CellTransform userTransformOrtho = new CellTransform(null, null);

    /** The event listeners which are attached to this view while the view is attached to its cell */
    private LinkedList<EventListener> eventListeners = new LinkedList<EventListener>();

    /** The view's which are children of this view. */
    private LinkedList<View2DEntity> children = new LinkedList<View2DEntity>();

    /* The set of changes which have occurred since the last update. */
    protected int changeMask;

    /** Whether the view entity is to be displayed in ortho mode ("on the glass"). */
    private boolean ortho;
   
    /** Whether the view entity is attached and to what. */
    private AttachState attachState = AttachState.DETACHED;

    /** A dummy AWT component (used by deliverEvent). */
    private static Button dummyButton = new Button();

    /*
     ** TODO: WORKAROUND FOR A WONDERLAND PICKER PROBLEM:
     ** TODO: >>>>>>>> Is this obsolete in 0.5?
     **
     ** We cannot rely on the x and y values in the intersection info coming from LG
     ** for mouse release events.
     ** The problem is both the same for the X11 and AWT pickers. The LG pickers are
     ** currently defined to set the node info of all events interior to and terminating
     ** a grab to the node info of the button press which started the grab. Not only is
     ** the destination node set (as is proper) but also the x and y intersection info
     ** (which is dubious and, I believe, improper). Note that there is a hack in both
     ** pickers to work around this problem for drag events, which was all LG cared about
     ** at the time. I don't want to perturb the semantics of the LG pickers at this time,
     ** but in the future the intersection info must be dealt with correctly for all
     ** events interior to and terminating a grab. Note that this problem doesn't happen
     ** with button presses, because these start grabs.
     **
     ** For now I'm simply going to treat the intersection info in button release events
     ** as garbage and supply the proper values by tracking the pointer position myself.
     */
    private boolean pointerMoveSeen = false;
    private int pointerLastX;
    private int pointerLastY;

    /** A list of frame changes to be performed outside the window lock. */
    private LinkedList<FrameChange> frameChanges = new LinkedList<FrameChange>();

    /** A flag which indicates that the cleanup method is being executed. */
    private boolean inCleanup = false;

    // TODO: HACK: Part 1 of 4: temporary workaround for 951
    // Parts 2 and 3 are later in this file. Part 4 is in HUDView3D.
    private float hackZEpsilon = 0f;

    /**
     * Create an instance of View2DEntity with default geometry node.
     * @param The entity in which the view is displayed.
     * @param window The window displayed in this view.
     */
    public View2DEntity (Window2D window) {
        this(window, null);
    }

    /**
     * Create an instance of View2DEntity with a specified geometry node.
     * @param window The window displayed in this view.
     * @param geometryNode The geometry node on which to display the view.
     *
     * NOTE: the subclass must force a complete update after calling this constructor, as follows:
     *
     * changeMask = CHANGED_ALL;
     * update();
     */
    public View2DEntity (Window2D window, GeometryNode geometryNode) {
        this.window = window;
        this.newGeometryNode = geometryNode;

        name = "View for " + window.getName();

        // Create entity and node
        entity = new Entity("Entity for " + name);
        viewNode = new Node("Node for " + name);
        RenderComponent rc =
            ClientContextJME.getWorldManager().getRenderManager().createRenderComponent(viewNode);
        entity.addComponent(RenderComponent.class, rc);
        entityMakePickable(entity);

        // Create input-related objects
        gui = new Gui2DInterior(this);
        gui.attachEventListeners(entity);
        controlArb = getWindow().getApp().getControlArb();

        logger.info("View2DEntity created: " + this);
    }

    /** {@inheritDoc} */
    public synchronized void cleanup () {
        inCleanup = true;

        changeMask = 0;
        disableGUI();

        setParent(null);
        setVisibleUser(false, false);
        setOrtho(false, false);
        setGeometryNode(null, false);
        update();
        // Note: don't do an updateFrame here because a deadlock can result!
        children.clear();

        if (gui != null) {
            gui.detachEventListeners(entity);
            gui.cleanup();
            gui = null;
        }


        if (geometryNode != null) {
            viewNode.detachChild(geometryNode);
            if (geometrySelfCreated) {
                geometryNode.cleanup();
                geometrySelfCreated = false;
            }
            geometryNode = null;
            newGeometryNode = null;
        }

        if (entity != null) {

            // Make sure that entity listeners and components are really gone
            // There is a case with movement into or out of the HUD where these
            // might get stuck on the entity
            for (EventListener listener : eventListeners) {
                listener.removeFromEntity(entity);
            }
            entity = null;
        }

        viewNode = null;
        controlArb = null;
        window = null;
        app = null;

        inCleanup = false;
    }

    /**
     * TODO: HACK: Part 2 of 4: temporary workaround for 951
     * @deprecated
     */
    @InternalAPI
    public void setHackZEpsilon (float epsilon) {
        hackZEpsilon = epsilon;
    }

    /** {@inheritDoc} */
    public abstract View2DDisplayer getDisplayer ();

    /** Return this view's entity. */
    public Entity getEntity () {
        return entity;
    }

    /** Return this view's root node. */
    public Node getNode () {
        return viewNode;
    }

    /** {@inheritDoc} */
    public String getName () {
        return name;
    }

    /**
     * {@inheritDoc}
     */
    public Window2D getWindow () {
        return window;
    }

    /**
     * INTERNAL API.
     * <br>
     * Returns the root scene graph node of this view.
     */
    public Node getViewNode () {
        return viewNode;
    }

    /**
     * INTERNAL API
     * <br>
     * Disables the GUI2D of this view. Used only by FrameHeaderSwing.
     */
    @InternalAPI
    public void disableGUI () {
        if (gui != null) {
            gui.detachEventListeners(entity);
            gui.cleanup();
            gui = null;
        }
    }

    /** {@inheritDoc} */
    public synchronized void setType (Type type) {
        setType(type, true);
    }

    /** {@inheritDoc} */
    public synchronized void setType (Type type, boolean update) {
        if (this.type == type) return;

        // Validate type argument
        if (this.type == Type.UNKNOWN) {
            // All new types are permitted
        } else if (this.type == Type.SECONDARY && type == Type.PRIMARY) {
            // A promotion of a secondary to a primary is permitted.
        } else if (this.type == Type.PRIMARY && type == Type.SECONDARY) {
            // A demotion of a primary to a secondary is permitted.
            // AppXrw.selectPrimaryWindow sometimes does this.
        } else if (this.type == Type.SECONDARY && type == Type.POPUP) {
            // A change of a secondary to a popup is permitted.
        } else {
            // No other type changes are permitted.
            logger.severe("Old view type = " + this.type);
            logger.severe("New view type = " + type);
            throw new RuntimeException("Invalid type change.");
        }
           
        logger.info("view = " + this);
        logger.info("change type = " + type);

        this.type = type;
        changeMask |= CHANGED_TYPE;
        if (update) {
            update();
            if (!inCleanup) {
                updateFrame();
            }
        }
    }

    /** {@inheritDoc} */
    public Type getType () {
        return type;
    }

    /**
     * {@inheritDoc}
     * <br><br>
     * This also has the side effect of setting the ortho attribute of this
     * view (and all of its children) to the ortho attribute of the parent.
     */
    public synchronized void setParent (View2D parent) {
        setParent(parent, true);
    }

    /**
     * {@inheritDoc}
     * <br><br>
     * This also has the side effect of setting the ortho attribute of this
     * view (and all of its children) to the ortho attribute of the parent.
     */
    public synchronized void setParent (View2D parent, boolean update) {
        if (this.parent == parent) return;

        // Detach this view from previous parent
        if (this.parent != null) {
            this.parent.children.remove(this);
        }

        logger.info("view = " + this);
        logger.info("change parent of view " + this + " to new parent view = " + parent);

        this.parent = (View2DEntity) parent;

        // Attach view to new parent
        if (this.parent != null) {
            this.parent.children.add(this);
        }

        changeMask |= CHANGED_PARENT;
        if (update) {
            update();
            if (!inCleanup) {
                updateFrame();
            }
        }

        // Inherit ortho state of parent (must do this after self update)
        if (this.parent != null) {
            // Update immediately
            setOrtho(this.parent.isOrtho());
        }
    }

    /** {@inheritDoc} */
    public View2D getParent () {
        return parent;
    }

    /** {@inheritDoc} */
    public synchronized void setVisibleApp (boolean visibleApp) {
        setVisibleApp(visibleApp, true);
    }

    /** {@inheritDoc} */
    public synchronized void setVisibleApp (boolean visibleApp, boolean update) {
        if (this.visibleApp == visibleApp) return;
        logger.info("view = " + this);
        logger.info("change visibleApp = " + visibleApp);
        this.visibleApp = visibleApp;
        changeMask |= CHANGED_VISIBLE;
        if (update) {
            update();
            if (!inCleanup) {
                updateFrame();
            }
        }
    }

    /** {@inheritDoc} */
    public boolean isVisibleApp () {
        return visibleApp;
    }

    /** {@inheritDoc} */
    public synchronized void setVisibleUser (boolean visibleUser) {
        setVisibleUser(visibleUser, true);
    }

    /** {@inheritDoc} */
    public synchronized void setVisibleUser (boolean visibleUser, boolean update) {
        if (this.visibleUser == visibleUser) return;
        logger.info("view = " + this);
        logger.info("change visibleUser = " + visibleUser);
        this.visibleUser = visibleUser;
        changeMask |= CHANGED_VISIBLE;
        if (update) {
            update();
            if (!inCleanup) {
                updateFrame();
            }
        }
    }

    /** {@inheritDoc} */
    public boolean isVisibleUser () {
        return visibleUser;
    }

    /** Recalculates the visibility of this view. */
    private synchronized void updateVisibility (boolean inCleanupParent) {
        changeMask |= CHANGED_VISIBLE;
        update();
        if (!inCleanup && !inCleanupParent) {
            updateFrame();
        }
    }

    /** {@inheritDoc} */
    public boolean isActuallyVisible () {
        logger.info("Check actually visible for view " + this);
        logger.info("visibleApp = " + visibleApp);
        logger.info("visibleUser = " + visibleUser);
        if (!visibleApp || !visibleUser) return false;
        return true;
    }

    /** {@inheritDoc} */
    public synchronized void setDecorated (boolean decorated) {
        setDecorated(decorated, true);
    }

    /** {@inheritDoc} */
    public synchronized void setDecorated (boolean decorated, boolean update) {
        if (this.decorated == decorated) return;
        logger.info("view = " + this);
        logger.info("change decorated = " + decorated);
        this.decorated = decorated;
        changeMask |= CHANGED_DECORATED;
        if (update) {
            update();
            if (!inCleanup) {
                updateFrame();
            }
        }
    }

    /** {@inheritDoc} */
    public boolean isDecorated () {
        return decorated;
    }

    /** {@inheritDoc} */
    public synchronized void setTitle (String title) {
        setTitle(title, true);
    }

    /** {@inheritDoc} */
    public synchronized void setTitle (String title, boolean update) {
        this.title = title;
        changeMask |= CHANGED_TITLE;
        if (update) {
            update();
            if (!inCleanup) {
                updateFrame();
            }
        }
    }

    /** {@inheritDoc} */
    public String getTitle () {
        return title;
    }

    /** {@inheritDoc} */
    public synchronized void setUserResizable (boolean userResizable) {
        setUserResizable(userResizable, true);
    }

    /** {@inheritDoc} */
    public synchronized void setUserResizable (boolean userResizable, boolean update) {
        if (this.userResizable == userResizable) return;
        logger.info("view = " + this);
        logger.info("change userResizable = " + userResizable);
        this.userResizable = userResizable;
        changeMask |= CHANGED_USER_RESIZABLE;
        if (update) {
            update();
            if (!inCleanup) {
                updateFrame();
            }
        }
    }

    /** {@inheritDoc} */
    public boolean isUserResizable () {
        return userResizable;
    }

    /**
     * {@inheritDoc}
     */
    public void stackChanged (boolean update) {
        changeMask |= CHANGED_STACK;
        if (update) {
            update();
            if (!inCleanup) {
                updateFrame();
            }
        }
    }

    /**
     * Specify the portion of the window which is displayed by this view when it is in cell mode.
     * Update afterward.
       TODO: notyet: public void setWindowAperture (Rectangle aperture);
     */

    /**
     * Specify the portion of the window which is displayed by this view when it is in cell mode.
     * Update if specified.
       TODO: notyet: public void setWindowAperture (Rectangle aperture, boolean update);
     */

    /**
     * Return the portion of the window which is displayed by this view when it is in cell mode.
       TODO: notyet: public Rectangle getWindowAperture ();
     */

    /**
     * Specify the portion of the window which is displayed by this view when it is in ortho mode.
     * Update afterward.
       TODO: notyet: public void setWindowApertureOrtho (Rectangle aperture);
     */

    /**
     * Specify the portion of the window which is displayed by this view when it is in ortho mode.
     * Update if specified.
       TODO: notyet: public void setWindowApertureOrtho (Rectangle aperture, boolean update);
     */

    /**
     * Return the portion of the window which is displayed by this view when it is in ortho mode.
       TODO: notyet: public Rectangle getWindowApertureOrtho ();
     */

    /** {@inheritDoc} */
    public synchronized void setGeometryNode (GeometryNode geometryNode) {
        setGeometryNode(geometryNode, true);
    }

    /** {@inheritDoc} */
    public synchronized void setGeometryNode (GeometryNode geometryNode, boolean update) {
        logger.info("view = " + this);
        logger.info("change geometryNode = " + geometryNode);
        newGeometryNode = geometryNode;
        changeMask |= CHANGED_GEOMETRY;
        if (update) {
            update();
            if (!inCleanup) {
                updateFrame();
            }
        }
    }
   
    /** {@inheritDoc} */
    public GeometryNode getGeometryNode () {
        return geometryNode;
    }

    /** {@inheritDoc} */
    public synchronized void setSizeApp (Dimension size) {
        setSizeApp(size, true);
    }

    /** {@inheritDoc} */
    public synchronized void setSizeApp (Dimension size, boolean update) {
        if (size.width == sizeApp.width && size.height == sizeApp.height) return;

        logger.info("view = " + this);
        logger.info("change sizeApp = " + sizeApp);

        sizeApp = (Dimension) size.clone();

        // Note: AWT doesn't like zero image sizes
        sizeApp.width = (sizeApp.width <= 0) ? 1 : sizeApp.width;
        sizeApp.height = (sizeApp.height <= 0) ? 1 : sizeApp.height;

        changeMask |= CHANGED_SIZE_APP;
        if (update) {
            update();
            if (!inCleanup) {
                updateFrame();
            }
        }
    }

    /** {@inheritDoc} */
    public Dimension getSizeApp () {
        return (Dimension) sizeApp.clone();
    }

    /** {@inheritDoc} */
    public float getDisplayerLocalWidth () {
        // TODO: ignore size mode and user size for now - always track window size as specified by app
        return getPixelScaleCurrent().x * sizeApp.width;
    }

    /** {@inheritDoc} */
    public float getDisplayerLocalHeight () {
        // TODO: ignore size mode and user size for now - always track window size as specified by app
        return getPixelScaleCurrent().y * sizeApp.height;
    }

    /**
     * A window close which comes from the user. Close the window of this view.
     */
    public void windowCloseUser () {
        window.closeUser();
    }

    /** {@inheritDoc} */
    public void windowRestackToTop () {
        window.restackToTop();
    }

    /** {@inheritDoc} */
    public void windowRestackToBottom () {
        window.restackToBottom();
    }

    /** {@inheritDoc} */
    public void windowRestackAbove (View2D sibling) {
        if (sibling == null) {
            window.restackToTop();
        } else {
            window.restackAbove(sibling.getWindow());
        }
    }

    /** {@inheritDoc} */
    public void windowRestackBelow (View2D sibling) {
        if (sibling == null) {
            window.restackToBottom();
        } else {
            window.restackBelow(sibling.getWindow());
        }
    }

    /**
     * Specify the size of the displayed pixels used when the view is in cell mode.
     * Update afterward. This defaults to the initial pixel scale of the window.
     */
    public synchronized void setPixelScale (Vector2f pixelScale) {
        setPixelScale(pixelScale, true);
    }

    /**
     * Specify the size of the displayed pixels used when the view is in cell mode.
     * Update if specified. Defaults to the pixel scale of the window.
     */
    public synchronized void setPixelScale (Vector2f pixelScale, boolean update) {
        logger.info("view = " + this);
        logger.info("change pixelScale cell = " + pixelScale);
        this.pixelScaleCell = pixelScale.clone();
        changeMask |= CHANGED_PIXEL_SCALE;
        if (update) {
            update();
            if (!inCleanup) {
                updateFrame();
            }
        }
    }

    /** Return the pixel scale used when the view is displayed in cell mode. */
    public Vector2f getPixelScale () {
        if (pixelScaleCell == null) {
            if (window != null) {
                return window.getPixelScale();
            } else {
                return new Vector2f(0.01f, 0.01f);
            }
        } else {
            return pixelScaleCell.clone();
        }
    }

    /**
     * Specify the size of the displayed pixels used when the view is in ortho mode.
     * Update afterward. This defaults to (1.0, 1.0).
     */
    public synchronized void setPixelScaleOrtho (Vector2f pixelScale) {
        setPixelScaleOrtho(pixelScale, true);
    }

    /**
     * Specify the size of the displayed pixels used when the view is in ortho mode.
     * Update if specified. This defaults to (1.0, 1.0).
     */
    public synchronized void setPixelScaleOrtho (Vector2f pixelScale, boolean update) {
        logger.info("view = " + this);
        logger.info("change pixelScale ortho = " + pixelScale);
        this.pixelScaleOrtho = pixelScale.clone();
        changeMask |= CHANGED_PIXEL_SCALE;
        if (update) {
            update();
            if (!inCleanup) {
                updateFrame();
            }
        }
    }

    /** Return the pixel scale used when the view is displayed in ortho mode. */
    public Vector2f getPixelScaleOrtho () {
        if (pixelScaleOrtho == null) {
            return new Vector2f(1f, 1f);
        } else {
            return pixelScaleOrtho.clone();
        }
    }

    /** Returns the pixel scale vector for the current mode. */
    public Vector2f getPixelScaleCurrent () {
        Vector2f pixelScale;
        if (ortho) {
            pixelScale = getPixelScaleOrtho();
        } else {
            pixelScale = getPixelScale();
        }
        return pixelScale;
    }

    /**
     * Specify the location of a primary view used when the view is in ortho mode.
     * The location is an offset relative to the origin of the displayer and is in
     * the coordinate system of the ortho plane. Update afterward.
     * This attribute is ignored for non-primary views.
     *
     * Note: setPixelOffset is the other part of the ortho offset translation. The two offsets are
     * added to produce the effective offset.
     *
     * Note: there is no corresponding attribute for cell mode because the cell itself automatically
     * controls the location of a primary view within the cell (usually centered) and the cell location
     * within the world is derived from WFS and other input.
     */
    public synchronized void setLocationOrtho (Vector2f location) {
        setLocationOrtho(location, true);
    }

    /**
     * Specify the location of a primary view used when the view is in ortho mode.
     * The location is an offset relative to the origin of the displayer and is in
     * the coordinate system of the ortho plane. Update if specified.
     * This attribute is ignored for non-primary views.
     *
     * Note: there is no corresponding attribute for cell mode because the cell itself automatically
     * controls the location of a primary view within the cell (usually centered) and the cell location
     * within the world is derived from WFS and other input.
     */
    public synchronized void setLocationOrtho (Vector2f location, boolean update) {
        if (locationOrtho.x == location.x && locationOrtho.y == location.y) return;
        logger.info("view = " + this);
        logger.info("change location ortho = " + location);
        locationOrtho = location.clone();
        changeMask |= CHANGED_LOCATION_ORTHO;
        if (update) {
            update();
            if (!inCleanup) {
                updateFrame();
            }
        }
    }

    /**
     * Returns the location used when this view is in ortho mode.
     */
    public Vector2f getLocationOrtho () {
        return locationOrtho.clone();
    }

    /**
     * Issue 151: Returns the corresponding component location for locationOrtho.
     */
    public Point getLocation() {
      return
        new Point(
          (int)(locationOrtho.x - getDisplayerLocalWidth() / 2),
          (int)(locationOrtho.y - getDisplayerLocalHeight() / 2)
        );
    }
   
    /** {@inheritDoc} */
    public synchronized void setOffset(Vector2f offset) {
        setOffset(offset, true);
    }

    /** {@inheritDoc} */
    public synchronized void setOffset(Vector2f offset, boolean update) {
        if (this.offset.x == offset.x && this.offset.y == offset.y) return;
        logger.info("view = " + this);
        logger.info("change offset = " + offset);
        this.offset = (Vector2f) offset.clone();
        changeMask |= CHANGED_OFFSET;
        if (update) {
            update();
            if (!inCleanup) {
                updateFrame();
            }
        }
    }

    /** {@inheritDoc} */
    public Vector2f getOffset () {
        return (Vector2f) offset.clone();
    }

    /** Called for children to mark them as changed when the parent view size changes. */
    private void changeOffsetSelfAndChildren () {
        changeMask |= CHANGED_OFFSET;
        for (View2DEntity childView : children) {
            childView.changeOffsetSelfAndChildren();
        }
    }

    /** {@inheritDoc} */
    public synchronized void setPixelOffset(Point pixelOffset) {
        setPixelOffset(pixelOffset, true);
    }

    /** {@inheritDoc} */
    public synchronized void setPixelOffset(Point pixelOffset, boolean update) {
        if (this.pixelOffset.x == pixelOffset.x && this.pixelOffset.y == pixelOffset.y) return;
        logger.info("view = " + this);
        logger.info("change pixelOffset = " + pixelOffset);
        this.pixelOffset = (Point) pixelOffset.clone();
        changeMask |= CHANGED_OFFSET;
        if (update) {
            update();
            if (!inCleanup) {
                updateFrame();
            }
        }
    }

    /** {@inheritDoc} */
    public Point getPixelOffset () {
        return (Point) pixelOffset.clone();
    }

    /** {@inheritDoc} */
    public void applyDeltaTranslationUser (Vector3f deltaTranslation) {
        applyDeltaTranslationUser(deltaTranslation, true);
    }

    /** {@inheritDoc} */
    public void applyDeltaTranslationUser (Vector3f deltaTranslation, boolean update) {
        deltaTranslationToApply = deltaTranslation.clone();
        changeMask |= CHANGED_USER_TRANSFORM;
        if (update) {
            update();
            if (!inCleanup) {
                updateFrame();
            }
        }
    }

    /**
     * Called by the UI to indicate the start of an interactive planar move.
     */
    public synchronized void userMovePlanarStart () {
        userMovePlanarDragVectorPrev = new Vector2f();
    }

    /**
     * Called by the UI to indicate a drag vector update during interactive planar move.
     */
    public synchronized void userMovePlanarUpdate (Vector2f dragVector) {
       
        // Calculate the delta of the movement since the last update
        Vector2f deltaDragVector = dragVector.clone();
        deltaDragVector.subtractLocal(userMovePlanarDragVectorPrev);
        userMovePlanarDragVectorPrev = dragVector;

        applyDeltaTranslationUser(new Vector3f(deltaDragVector.x, deltaDragVector.y, 0f), true);
    }

    public synchronized void userMovePlanarFinish () {
    }

    private Dimension userResizeCalcWindowNewSize (Vector2f dragVector) {
        if (dragVector.x == 0 && dragVector.y == 0) return null;

        // Convert local coords drag vector to a pixel vector
        Vector2f pixelScale = getPixelScaleCurrent();
        //System.err.println("pixelScale = " + pixelScale);
        int deltaWidth = (int)(dragVector.x / pixelScale.x);
        int deltaHeight = (int)(dragVector.y / pixelScale.y);

        //System.err.println("deltaWH = " + deltaWidth + ", " + deltaHeight);

        // TODO: for now, we only support the resize mode where resizing the view
        // directly updates the window size.
        int width = window.getWidth();
        int height = window.getHeight();
        //System.err.println("Old size, wh = " + width + ", " + height);
        width += deltaWidth;
        height -= deltaHeight;
        if (width < 1) width = 1;
        if (height < 1) height = 1;
        //System.err.println("New size, wh = " + width + ", " + height);

        return new Dimension(width, height);
    }

    /**
     * Called by the UI to indicate the start of an interactive resize.
     */
    public void userResizeStart () {
    }

    /**
     * Called by the UI to indicate a drag vector update during interactive resize.
     * This is called on the EDT.
     */
    public void userResizeUpdate (Vector2f dragVector) {
    }

    public void userResizeFinish () {
    }

    protected void userResizeFrameUpdate (float width3D, float height3D, Dimension newSize) {
    }

    public void updateViewSizeOnly (int x, int y, int width, int height,
                                    float newViewWidth3D, float newViewHeight3D,
                                    Dimension parentViewNewSize) {
        final Vector3f trans = new Vector3f();

        trans.x = offset.x;
        trans.y = offset.y;

        if (type != Type.PRIMARY && type != Type.UNKNOWN && parent != null) {

            // Convert pixel offset to local coords and add it in
            // TODO: does the width/height need to include the scroll bars?
            Vector2f pixelScale = getPixelScaleCurrent();
            Dimension parentSize = parent.getSizeApp()// TODO: is this now unused?
            trans.y += parentViewNewSize.height * pixelScale.y / 2f;
            trans.y -= y * pixelScale.y / 2f;
        }

        final float width3D = getPixelScaleCurrent().x * width;
        final float height3D = getPixelScaleCurrent().y * height;

        Image image = getWindow().getTexture().getImage();
        final float widthRatio = (float)width / image.getWidth();
        final float heightRatio = (float)height / image.getHeight();

        ClientContextJME.getWorldManager().addRenderUpdater(new RenderUpdater() {
            public void update(Object arg0) {
                geometryNode.setLocalTranslation(trans);
                geometryNode.setSize(width3D, height3D);
                geometryNode.setTexCoords(widthRatio, heightRatio);
                ClientContextJME.getWorldManager().addToUpdateList(viewNode);
            }
        }, null);
    }

    /** TODO: NOTYET
    public synchronized void userMoveZStart (float dy) {
    }

    public synchronized void userMoveZUpdate (float dy) {
    }

    public synchronized void userMoveZFinish () {
    }
    */

    /**
     * Specifies whether the view entity is to be displayed in ortho mode ("on the glass").
     * Update immediately. In addition, the ortho attribute of all descendents is set to the
     * same value. And, when ortho is true, the decorated attribute is ignored and is regarded
     * as false.
     */
    public synchronized void setOrtho (boolean ortho) {
        setOrtho(ortho, true);
    }

    /**
     * Specifies whether the view entity is to be displayed in ortho mode ("on the glass").
     * Update if specified. This also changes the ortho attribute of all descendent views.
     */
    public synchronized void setOrtho (boolean ortho, boolean update) {
        if (this.ortho == ortho) return;
        logger.info("view = " + this);
        logger.info("change ortho = " + ortho);
        this.ortho = ortho;
        changeMask |= CHANGED_ORTHO;

        logger.info("Make corresponding ortho changes to descendents");
        logger.info("Num children = " + children.size());
        for (View2DEntity child : children) {
            child.setOrtho(ortho, false);
        }

        if (update) {
            update();
            if (!inCleanup) {
                updateFrame();
            }
        }
    }

    /**
     * Returns whether the view entity is in ortho mode.
     */
    public boolean isOrtho () {
        return ortho;
    }

    private void logChangeMask (int mask) {
        logger.info("changeMask " + Integer.toHexString(mask));
        int bit = 0x1;
        for (int i = 0; i < 32; i++, bit <<= 1) {
            int thisBit = mask & bit;
            if (thisBit != 0) {
                String str;
                switch (thisBit) {
                case CHANGED_TYPE:
                    str = "CHANGED_TYPE";
                    break;
                case CHANGED_PARENT:
                    str = "CHANGED_PARENT";
                    break;
                case CHANGED_VISIBLE:
                    str = "CHANGED_VISIBLE";
                    break;
                case CHANGED_DECORATED:
                    str = "CHANGED_DECORATED";
                    break;
                case CHANGED_GEOMETRY:
                    str = "CHANGED_GEOMETRY";
                    break;
                case CHANGED_SIZE_APP:
                    str = "CHANGED_SIZE_APP";
                    break;
                case CHANGED_PIXEL_SCALE:
                    str = "CHANGED_PIXEL_SCALE";
                    break;
                case CHANGED_OFFSET:
                    str = "CHANGED_OFFSET";
                    break;
                case CHANGED_USER_TRANSFORM:
                    str = "CHANGED_USER_TRANSFORM";
                    break;
                case CHANGED_TITLE:
                    str = "CHANGED_TITLE";
                    break;
                case CHANGED_STACK:
                    str = "CHANGED_STACK";
                    break;
                case CHANGED_ORTHO:
                    str = "CHANGED_ORTHO";
                    break;
                case CHANGED_LOCATION_ORTHO:
                    str = "CHANGED_LOCATION_ORTHO";
                    break;
                case CHANGED_TEX_COORDS:
                    str = "CHANGED_TEX_COORDS";
                    break;
                case CHANGED_USER_RESIZABLE:
                    str = "CHANGED_USER_RESIZABLE";
                    break;
                default:
                    continue;
                }

                // Printed selected values
                String str2 = null;
                switch (thisBit) {
                case CHANGED_TYPE:
                    str2 = ": type = " + type;
                    break;
                case CHANGED_PARENT:
                    str2 = ": parent = " + parent;
                    break;
                case CHANGED_VISIBLE:
                    str2 = ": visibleApp = " + visibleApp + ", visibleUser = " + visibleUser;
                    break;
                case CHANGED_DECORATED:
                    str2 = ": decorated = " + decorated;
                    break;
                case CHANGED_GEOMETRY:
                    break;
                case CHANGED_SIZE_APP:
                    str2 = ": sizeApp = " + sizeApp;
                    break;
                case CHANGED_PIXEL_SCALE:
                    str2 = ": pixelScaleCell = " + pixelScaleCell + ", pixelScaleOrtho = " + pixelScaleOrtho;
                    break;
                case CHANGED_OFFSET:
                    str2 = ": offset = " + offset + ", pixelOffset = " + pixelOffset;
                    break;
                case CHANGED_USER_TRANSFORM:
                    str2 = ": deltaTranslationToApply = " + deltaTranslationToApply;
                    break;
                case CHANGED_TITLE:
                    str2 = ": title = " + title;
                    break;
                case CHANGED_STACK:
                    break;
                case CHANGED_ORTHO:
                    str2 = ": ortho = " + ortho;
                    break;
                case CHANGED_LOCATION_ORTHO:
                    str2 = ": locationOrtho = " + locationOrtho;
                    break;
                case CHANGED_TEX_COORDS:
                    break;
                case CHANGED_USER_RESIZABLE:
                    str2 = ": userResizable = " + userResizable;
                    break;
                }

                str += "(" + Integer.toHexString(thisBit) + ")";
                if (str2 != null) {
                    str += str2;
                }
                logger.info(str);
            }
        }
    }

    /** {@inheritDoc} */
    public synchronized void update () {
        // Only-Update-When-Visible Optimization
        // 1. Always perform any visibility or size changes immediately.
        if ((changeMask & (CHANGED_VISIBLE | CHANGED_SIZE_APP)) == 0) {
            // 2. Don't perform other changes unless the view if both the app and the user
            // have made it visible.
            if (!visibleApp || !visibleUser) {
                return;
            }
        }

        // Note: all of the scene graph changes are queued up and executed at the end
        boolean windowNeedsValidate = false;

        // For Debug - Part 1: Uncomment this to print info for HUD views only
        //if (!("View2DCell".equals(this.getClass().getName()))) {

        logger.info("------------------ Processing changes for view " + this);
        logger.info("type " + type);
        logChangeMask(changeMask);

        // For Debug - Part 2: Uncomment this to print info for HUD views only
        //        }

        // React to topology related changes
        if ((changeMask & (CHANGED_GEOMETRY | CHANGED_SIZE_APP | CHANGED_TYPE | CHANGED_PARENT |
                           CHANGED_VISIBLE | CHANGED_ORTHO)) != 0) {
            logger.fine("Update topology for view " + this);
           
            // First, detach entity (if necessary)
            switch (attachState) {
            case ATTACHED_TO_ENTITY:
                if (parentEntity != null) {
                    logger.fine("Remove entity " + entity + " from parent entity " + parentEntity);
                    RenderComponent rc = (RenderComponent) entity.getComponent(RenderComponent.class);
                    sgChangeAttachPointSetAddEntity(rc, null, null, null);
                    parentEntity.removeEntity(entity);
                    parentEntity = null;
                }
                break;
            case ATTACHED_TO_WORLD:
                logger.fine("Remove entity " + entity + " from world manager.");
                ClientContextJME.getWorldManager().removeEntity(entity);
                break;
            }
            attachState = AttachState.DETACHED;

            // Does the geometry node itself need to change?
            if ((changeMask & CHANGED_GEOMETRY) != 0) {
                if (geometryNode != null) {
                    // Note: don't need to do in RenderUpdater because we've detached our entity
                    sgChangeGeometryDetachFromView(viewNode, geometryNode);
                    if (geometrySelfCreated) {
                        sgChangeGeometryCleanup(geometryNode);
                        geometrySelfCreated = false;
                    }
                }
                if (newGeometryNode != null) {
                    geometryNode = newGeometryNode;
                    newGeometryNode = null;
                } else {
                    geometryNode = new GeometryNodeQuad(this);
                    geometrySelfCreated = true;
                }
                // Note: don't need to do in RenderUpdater because we've detached our entity
                sgChangeGeometryAttachToView(viewNode, geometryNode);
            }

            // Uses: window
            if ((changeMask & (CHANGED_GEOMETRY | CHANGED_SIZE_APP)) != 0) {
                logger.fine("Update texture for view " + this);
                if (geometryNode != null) {
                    DrawingSurface surface = getWindow().getSurface();
                    if (surface != null) {
                        sgChangeGeometryTextureSet(geometryNode, getWindow().getTexture(), surface);
                        windowNeedsValidate = true;
                    }
                }
            }

            // Now reattach geometry if view should be visible
            // Note: MTGame can currently only setOrtho on a visible rc
            // Uses: visible, ortho
            if (isActuallyVisible()) {
                if (ortho) {
                    logger.fine("View is ortho for view " + this);
                    entity.getComponent(RenderComponent.class).setOrtho(true);
                    entity.getComponent(CollisionComponent.class).setCollidable(false);

                    if (type == Type.PRIMARY  || type == Type.UNKNOWN) {
                        // Attach top level ortho views directly to world
                        ClientContextJME.getWorldManager().addEntity(entity);
                        attachState = AttachState.ATTACHED_TO_WORLD;
                        logger.fine("Attached entity " + entity + " to world manager.");
                    } else {
                        parentEntity = getParentEntity();
                        if (parentEntity == null) {
                            // Has no Parent; attach directly to world
                            ClientContextJME.getWorldManager().addEntity(entity);
                            attachState = AttachState.ATTACHED_TO_WORLD;
                            logger.fine("Attached parentless entity " + entity + " to world manager.");
                        } else {
                            RenderComponent rc = (RenderComponent) entity.getComponent(RenderComponent.class);
                            // TODO: these two statements appear to be obsolete.
                            RenderComponent rcParent =
                                (RenderComponent) parentEntity.getComponent(RenderComponent.class);
                            Node attachNode = rcParent.getSceneRoot();

                            // Note: we need to attach non-primaries to the parent geometry node in
                            // ortho mode, rather than the view node. This way it picks up the parent's
                            // offset translation, which contains locationOrtho
                            // TODO: do this cleaner. Convert attach node to a view and get the
                            // geometry node for this view.
                            attachNode = (Node) attachNode.getChild(0);

                            sgChangeAttachPointSetAddEntity(rc, attachNode, parentEntity, entity);
                            attachState = AttachState.ATTACHED_TO_ENTITY;
                            logger.fine("Attach ortho entity " + entity + " to geometry node of parent entity " + parentEntity);
                        }
                    }
                } else {
                    logger.fine("View is not ortho for view " + this);
                    parentEntity = getParentEntity();
                    if (parentEntity == null) {
                        logger.warning("getParentEntity() returns null; must be non-null");
                    } else {
                        logger.fine("Attach entity " + entity + " to parent entity " + parentEntity);

                        RenderComponent rc = (RenderComponent) entity.getComponent(RenderComponent.class);
                        RenderComponent rcParent =
                            (RenderComponent) parentEntity.getComponent(RenderComponent.class);
                        Node attachNode = rcParent.getSceneRoot();

                        // SPECIAL NOTE: Here is where special surgery is done on header windows so
                        // that they are parented to the *geometry node* of their parent view instead of
                        // the view node, as windows normally are. This way it picks up the offset
                        // translation in the geometry node and stays in sync with the rest of the frame.
                        // See also: SPECIAL NOTE in Frame2DCell.attachViewToEntity.
                        if (window instanceof WindowSwingHeader) {
                            WindowSwingHeader wsh = (WindowSwingHeader) window;
                            if (wsh.getView().getType() == View2D.Type.SECONDARY) {
                                // TODO: do this cleaner. Convert attach node to a view and get the
                                // geometry node for this view.
                                attachNode = (Node) attachNode.getChild(0);
                            }
                        }

                        sgChangeAttachPointSetAddEntity(rc, attachNode, parentEntity, entity);
                        attachState = AttachState.ATTACHED_TO_ENTITY;
                        entity.getComponent(RenderComponent.class).setOrtho(false);
                        entity.getComponent(CollisionComponent.class).setCollidable(true);
                    }
                }
            }

            if ((changeMask & CHANGED_VISIBLE) != 0) {
                // Update visibility of children
                logger.fine("Update children visibility for view " + this);
                for (View2DEntity child : children) {
                    child.updateVisibility(inCleanup);
                }
            }           
        } // End Topology Changes

        // Determine what frame changes need to be performed. But these aren't executed now;
        // they are executed later by view.updateFrame, which must be invoked outside the window lock.
        // issue 151: prepare for reattachment of frame on resize.
        if ((changeMask & (CHANGED_DECORATED | CHANGED_TITLE | CHANGED_TYPE | CHANGED_SIZE_APP |
                           CHANGED_PIXEL_SCALE | CHANGED_USER_RESIZABLE | CHANGED_VISIBLE)) != 0) {
            logger.fine("Update frame for view " + this);
            logger.fine("decorated " + decorated);

            if ((changeMask & (CHANGED_DECORATED | CHANGED_VISIBLE)) != 0) {
                // Some popups initiall are decorated and then are set to undecorated before
                // the popup becomes visible. So to avoid wasting time, wait until the window
                // becomes visible before attaching its frame.
                if (decorated && isActuallyVisible()) {
                    if (!hasFrame()) {
                        logger.fine("Attach frame");
                        frameChanges.add(FrameChange.ATTACH_FRAME);
                    }
                } else {
                    if (hasFrame()) {
                        logger.fine("Detach frame");
                        frameChanges.add(FrameChange.DETACH_FRAME);
                    }
                }
            }
           
            if ((changeMask & CHANGED_TITLE) != 0) {
                if (decorated && hasFrame()) {
                    frameChanges.add(FrameChange.UPDATE_TITLE);
                }
            }

            if ((changeMask & (CHANGED_TYPE | CHANGED_SIZE_APP)) != 0) {
                if (decorated) {
                    frameChanges.add(FrameChange.REATTACH_FRAME);
                }
            }
            if ((changeMask & (CHANGED_USER_RESIZABLE | CHANGED_VISIBLE)) != 0) {
                if (decorated) {
                    frameChanges.add(FrameChange.UPDATE_USER_RESIZABLE);
                }
            }
        }           

        if ((changeMask & (CHANGED_STACK | CHANGED_ORTHO)) != 0) {
            logger.fine("Update geometry ortho Z order for view " + this);
            if (ortho) {
                if (window != null) {
                    int zOrder = window.getZOrder();
                    logger.fine("Z order = " + zOrder);
                    if (zOrder >= 0) {
                        sgChangeGeometryOrthoZOrderSet(geometryNode, zOrder);
                    }
                }
            }
        }

        if ((changeMask & CHANGED_ORTHO) != 0) {
            // MTGame: can currently only setOrtho on a visible rc
            if (isActuallyVisible()) {
                entity.getComponent(RenderComponent.class).setOrtho(ortho);
                entity.getComponent(CollisionComponent.class).setCollidable(!ortho);
            }
            sgChangeViewNodeOrthoSet(viewNode, ortho);
        }

        // React to size related changes (must be done before handling transform changes)
        if ((changeMask & (CHANGED_DECORATED | CHANGED_SIZE_APP | CHANGED_PIXEL_SCALE |
                           CHANGED_ORTHO)) != 0) {

            float width = getDisplayerLocalWidth();
            float height = getDisplayerLocalHeight();
            sgChangeGeometrySizeSet(geometryNode, width, height);

            /**
             * Subtle: Changing the size of the quad will stomp the texture coordinates.
             * We must force them to be restored.
             */
            changeMask |= CHANGED_TEX_COORDS;
        }

        // React to texture coordinate changes
        // Uses: window, texture
        if ((changeMask & (CHANGED_TEX_COORDS|CHANGED_GEOMETRY|CHANGED_SIZE_APP)) != 0) {
            // TODO: for now, texcoords only depend on app size. Eventually this should
            // be the effective aperture rectangle width and height
            float width = (float) sizeApp.width;   
            float height = (float) sizeApp.height;
            if (getWindow() != null && getWindow().getTexture() != null) {
                Image image = getWindow().getTexture().getImage();
                float widthRatio = width / image.getWidth();
                float heightRatio = height / image.getHeight();
                sgChangeGeometryTexCoordsSet(geometryNode, widthRatio, heightRatio);
                windowNeedsValidate = true;
            }
        }

        // React to transform related changes
        // Uses: type, parent, pixelscale, size, offset, ortho, locationOrtho, stack
        if ((changeMask & (CHANGED_TYPE | CHANGED_PARENT | CHANGED_PIXEL_SCALE | CHANGED_SIZE_APP |
                           CHANGED_OFFSET | CHANGED_ORTHO | CHANGED_LOCATION_ORTHO | CHANGED_STACK)) != 0) {
            CellTransform transform = null;

            switch (type) {
            case UNKNOWN:
            case PRIMARY:
                transform = new CellTransform(null, null);
                if (ortho) {
                    Vector3f orthoLocTranslation = new Vector3f();
                    orthoLocTranslation.x = locationOrtho.x;
                    orthoLocTranslation.y = locationOrtho.y;
                    transform.setTranslation(orthoLocTranslation);
                } else {
                    // Note: primaries now also honor the offset.
                    // Uses: type, parent, pixelScale, size, offset, ortho
                    transform = calcOffsetStackTransform();
                }
                break;
            case SECONDARY:
            case POPUP:
                // Uses: type, parent, pixelScale, size, offset, ortho
                transform = calcOffsetStackTransform();
            }
            sgChangeGeometryTransformOffsetStackSet(geometryNode, transform);
        }

        // Update the view node's user transform, if necessary
        // Uses: type, deltaTranslationToApply
        if ((changeMask & (CHANGED_TYPE | CHANGED_USER_TRANSFORM | CHANGED_ORTHO)) != 0) {

            // Select the current user transform based on the ortho mode
            CellTransform currentUserTransform;
            if (ortho) {
                currentUserTransform = userTransformOrtho;
            } else {
                currentUserTransform = userTransformCell;
            }

            if (!userTransformCellReplaced) {           
                // Apply any pending user transform deltas (by post-multiplying them
                // into the current user transform
                logger.fine("currentUserTransform (before) = " + currentUserTransform);
                userTransformApplyDeltas(currentUserTransform);
            }

            logger.fine("currentUserTransform (latest) = " + currentUserTransform);

            // Now put the update user transformation into effect
            switch (type) {
            case UNKNOWN:
            case PRIMARY:
                updatePrimaryTransform(currentUserTransform);
                break;
            case SECONDARY:
                sgChangeTransformUserSet(viewNode, currentUserTransform);
                // Note: moving a secondary in the cell doesn't change the position
                // of the secondary in ortho, and vice versa.
                if (!ortho && !userTransformCellChangedLocalOnly) {
                    window.changedUserTransformCell(userTransformCell, this);
                }
                break;
            case POPUP:
                // Always set to identity
                sgChangeTransformUserSet(viewNode, new CellTransform(null, null));
            }

            userTransformCellReplaced = false;
            userTransformCellChangedLocalOnly = false;
        }

        // Changing the 3D size of the app can change the offset of children, such as headers.
        if ((changeMask & (CHANGED_SIZE_APP | CHANGED_PIXEL_SCALE)) != 0) {
            for (View2DEntity childView : children) {
                childView.changeOffsetSelfAndChildren();
            }
        }

        sgProcessChanges();

        /* For Debug
        System.err.println("************* After View2DEntity.processChanges, viewNode = ");
        GraphicsUtils.printNode(viewNode);
        */

        /* For debug of ortho entities which should be visible
        WorldManager wm = ClientContextJME.getWorldManager();
        for (int i=0; i < wm.numEntities(); i++) {
            Entity e = wm.getEntity(i);
            if (e.toString().equals("<Plug the name of the window in here")) {
                System.err.println("e = " + e);
                RenderComponent rc = (RenderComponent) e.getComponent(RenderComponent.class);
            }
        }
        */

        // In certain situations, especially after we change the texture, WindowSwings
        // need to be repainted into that texture.
        if (windowNeedsValidate) {
            if (window instanceof WindowSwing) {
                ((WindowSwing)window).validate();
            }
        }

        // Inform the window's surface of the view visibility.
        if (window != null) {
            DrawingSurface surface = window.getSurface();
            if (surface != null) {
                surface.setViewIsVisible(this, isActuallyVisible());
            }
        }

        // Make sure that all descendent views are up-to-date
        logger.fine("Update children for view " + this);
        for (View2DEntity child : children) {
            if (child.changeMask != 0) {
                child.update();
            }
        }

        changeMask = 0;
    }

    /**
     * This returns the entity in the world to which this view should be attached as a child.
     * It must return non-null.
     * Uses: type, parent.
     */
    protected abstract Entity getParentEntity ();

    // Uses: type, parent, pixelscale, size, offset
    // View2DCell subclass uses: type, ortho, parent, stack
    private CellTransform calcOffsetStackTransform () {
        CellTransform transform = new CellTransform(null, null);

        // Uses: parent, pixelScale, size, offset, ortho
        Vector3f offsetTranslation = calcOffsetTranslation();

        // Uses: type
        Vector3f stackTranslation = calcStackTranslation();

        offsetTranslation.addLocal(stackTranslation);

        // TODO: HACK: Part 3 of 4 temporary workaround for 951
        offsetTranslation.addLocal(new Vector3f(0f, 0f, hackZEpsilon));

        transform.setTranslation(offsetTranslation);

        return transform;
    }

    // Uses: parent, pixelscale, size, offset
    // Convert the pixel-offset-from-upper-left of parent to a distance vector from the center of parent
    private Vector3f calcOffsetTranslation () {
        Vector3f translation = new Vector3f();

        if (ortho) {
            if (type == Type.PRIMARY || type == Type.UNKNOWN) {
                translation.x = locationOrtho.x;
                translation.y = locationOrtho.y;
            } else {

                if (parent == null) return translation;

                // Initialize to the first part of the offset (the local coordinate translation)
                logger.fine("view = " + this);
                logger.fine("parent = " + parent);
                logger.fine("locationOrtho = " + locationOrtho);
                logger.fine("offset = " + offset);
                translation.x = locationOrtho.x + offset.x;
                translation.y = locationOrtho.y + offset.y;
                logger.fine("translation 1 = " + translation);
               
                // Convert pixel offset to local coords and add it in
                Dimension parentSize = parent.getSizeApp();
                Vector2f pixelScaleOrtho = parent.getPixelScaleOrtho();
                logger.fine("parentSize = " + parentSize);
                translation.x += -parentSize.width * pixelScaleOrtho.x / 2f;
                translation.y += parentSize.height * pixelScaleOrtho.y / 2f;
                logger.fine("translation 2 = " + translation);
                logger.fine("sizeApp = " + sizeApp);
                pixelScaleOrtho = getPixelScaleOrtho();
                translation.x += sizeApp.width * pixelScaleOrtho.x / 2f;
                translation.y -= sizeApp.height * pixelScaleOrtho.y / 2f;
                logger.fine("translation 3 = " + translation);
                logger.fine("pixelOffset = " + pixelOffset);
                translation.x += pixelOffset.x * pixelScaleOrtho.x;
                translation.y -= pixelOffset.y * pixelScaleOrtho.y;
                logger.fine("translation 4 = " + translation);
            }
        } else {   

            // Initialize to the first part of the offset (the local coordinate translation)
            logger.fine("view = " + this);
            logger.fine("parent = " + parent);
            logger.fine("offset = " + offset);
            translation.x = offset.x;
            translation.y = offset.y;
            logger.fine("translation 1 = " + translation);

            if (type != Type.PRIMARY && type != Type.UNKNOWN && parent != null) {

                // Convert pixel offset to local coords and add it in
                // TODO: does the width/height need to include the scroll bars?
                Vector2f pixelScale = parent.getPixelScaleCurrent();
                Dimension parentSize = parent.getSizeApp();
                logger.fine("parentSize = " + parentSize);
                translation.x += -parentSize.width * pixelScale.x / 2f;
                translation.y += parentSize.height * pixelScale.y / 2f;
                logger.fine("translation 2 = " + translation);
                logger.fine("sizeApp = " + sizeApp);
                pixelScale = getPixelScaleCurrent();
                translation.x += sizeApp.width * pixelScale.x / 2f;
                translation.y -= sizeApp.height * pixelScale.y / 2f;
                logger.fine("translation 3 = " + translation);
                logger.fine("pixelOffset = " + pixelOffset);
                translation.x += pixelOffset.x * pixelScale.x;
                translation.y -= pixelOffset.y * pixelScale.y;
                logger.fine("translation 4 = " + translation);
            }
        }

        logger.fine("view = " + this);
        logger.fine("offset translation = " + translation);

        return translation;
    }


    protected Vector3f calcStackTranslation () {
        return new Vector3f(0f, 0f, 0f);
    }

    // Apply the pending deltas to the given user transform
    protected void userTransformApplyDeltas (CellTransform userTransform) {
        userTransformApplyDeltaTranslation(userTransform);
    }

    // Apply any pending translation delta to the given user transform.
    protected void userTransformApplyDeltaTranslation (CellTransform userTransform) {
        if (deltaTranslationToApply != null) {
            CellTransform transform = new CellTransform(null, null);
            transform.setTranslation(deltaTranslationToApply);
            //System.err.println("******* delta translation transform = " + transform);
            userTransform.mul(transform);
            deltaTranslationToApply = null;
        }
    }

    protected void updatePrimaryTransform (CellTransform transform) {
    }

    private enum SGChangeOp {
        GEOMETRY_ATTACH_TO_VIEW,
        GEOMETRY_DETACH_FROM_VIEW,
        GEOMETRY_SIZE_SET,
        GEOMETRY_TEX_COORDS_SET,
        GEOMETRY_TEXTURE_SET,
        GEOMETRY_ORTHO_Z_ORDER_SET,
        GEOMETRY_TRANSFORM_OFFSET_STACK_SET,
        GEOMETRY_CLEANUP,
        VIEW_NODE_ORTHO_SET,
        TRANSFORM_USER_SET,
        ATTACH_POINT_SET_ADD_ENTITY
    };

    private static class SGChange {
        private SGChangeOp op;
        private SGChange (SGChangeOp op) {
            this.op = op;
        }
        private SGChangeOp getOp () {
            return op;
        }
    }

    private static class SGChangeGeometryAttachToView extends SGChange {
        private Node viewNode;
        private GeometryNode geometryNode;
        private SGChangeGeometryAttachToView (Node viewNode, GeometryNode geometryNode) {
            super(SGChangeOp.GEOMETRY_ATTACH_TO_VIEW);
            this.viewNode = viewNode;
            this.geometryNode = geometryNode;
        }
    }

    private static class SGChangeGeometryDetachFromView extends SGChange {
        private Node viewNode;
        private GeometryNode geometryNode;
        private SGChangeGeometryDetachFromView (Node viewNode, GeometryNode geometryNode) {
            super(SGChangeOp.GEOMETRY_DETACH_FROM_VIEW);
            this.viewNode = viewNode;
            this.geometryNode = geometryNode;
        }
    }

    private static class SGChangeGeometrySizeSet extends SGChange {
        private GeometryNode geometryNode;
        private float width;
        private float height;
        private SGChangeGeometrySizeSet (GeometryNode geometryNode, float width, float height) {
            super(SGChangeOp.GEOMETRY_SIZE_SET);
            this.geometryNode = geometryNode;
            this.width = width;
            this.height = height;
        }
    }

    private static class SGChangeGeometryTexCoordsSet extends SGChange {
        private GeometryNode geometryNode;
        private float widthRatio;
        private float heightRatio;
        private SGChangeGeometryTexCoordsSet (GeometryNode geometryNode,
                                              float widthRatio, float heightRatio) {
            super(SGChangeOp.GEOMETRY_TEX_COORDS_SET);
            this.geometryNode = geometryNode;
            this.widthRatio = widthRatio;
            this.heightRatio = heightRatio;
        }
    }

    private static class SGChangeGeometryTextureSet extends SGChange {
        private GeometryNode geometryNode;
        private Texture2D texture;
        private DrawingSurface surface;
        private SGChangeGeometryTextureSet (GeometryNode geometryNode, Texture2D texture,
                                            DrawingSurface surface) {
            super(SGChangeOp.GEOMETRY_TEXTURE_SET);
            this.geometryNode = geometryNode;
            this.texture = texture;
            this.surface = surface;
        }
    }

    private static class SGChangeGeometryOrthoZOrderSet extends SGChange {
        private GeometryNode geometryNode;
        private int zOrder;
        private SGChangeGeometryOrthoZOrderSet (GeometryNode geometryNode, int zOrder) {
            super(SGChangeOp.GEOMETRY_ORTHO_Z_ORDER_SET);
            this.geometryNode = geometryNode;
            this.zOrder = zOrder;
        }
    }

    private static class SGChangeGeometryCleanup extends SGChange {
        private GeometryNode geometryNode;
        private SGChangeGeometryCleanup (GeometryNode geometryNode) {
            super(SGChangeOp.GEOMETRY_CLEANUP);
            this.geometryNode = geometryNode;
        }
    }

    private static class SGChangeViewNodeOrthoSet extends SGChange {
        private Node viewNode;
        private boolean ortho;
        private SGChangeViewNodeOrthoSet (Node viewNode, boolean ortho) {
            super(SGChangeOp.VIEW_NODE_ORTHO_SET);
            this.viewNode = viewNode;
            this.ortho = ortho;
        }
    }

    private static class SGChangeTransform extends SGChange {
        protected CellTransform transform;
        private SGChangeTransform (SGChangeOp op, CellTransform transform) {
            super(op);
            this.transform = transform;
        }
    }
   
    private static class SGChangeGeometryTransformOffsetStackSet extends SGChangeTransform {
        private GeometryNode geometryNode;
        private SGChangeGeometryTransformOffsetStackSet (GeometryNode geometryNode, CellTransform transform) {
            super(SGChangeOp.GEOMETRY_TRANSFORM_OFFSET_STACK_SET, transform);
            this.geometryNode = geometryNode;
        }
    }

    private static class SGChangeTransformUserSet extends SGChangeTransform {
        private Node viewNode;
        private SGChangeTransformUserSet (Node viewNode, CellTransform transform) {
            super(SGChangeOp.TRANSFORM_USER_SET, transform);
            this.viewNode = viewNode;
       }
    }

    private static class SGChangeAttachPointSetAddEntity extends SGChange {
        private RenderComponent rc;
        private Node node;
        private Entity parentEntity;
        private Entity entity;
        private SGChangeAttachPointSetAddEntity (RenderComponent rc, Node node, Entity parentEntity,
                                                 Entity entity) {
            super(SGChangeOp.ATTACH_POINT_SET_ADD_ENTITY);
            this.rc = rc;
            this.node = node;
            this.parentEntity = parentEntity;
            this.entity = entity;
       }
    }

    // The list of scene graph changes (to be applied at the end of update).
    private LinkedList<SGChange> sgChanges = new LinkedList<SGChange>();

    private synchronized void sgChangeGeometryAttachToView(Node viewNode, GeometryNode geometryNode) {
        sgChanges.add(new SGChangeGeometryAttachToView(viewNode, geometryNode));
    }

    private synchronized void sgChangeGeometryDetachFromView(Node viewNode, GeometryNode geometryNode) {
        sgChanges.add(new SGChangeGeometryDetachFromView(viewNode, geometryNode));
    }

    private synchronized void sgChangeGeometrySizeSet(GeometryNode geometryNode, float width, float height) {
        sgChanges.add(new SGChangeGeometrySizeSet(geometryNode, width, height));
    }

    private synchronized void sgChangeGeometryTexCoordsSet(GeometryNode geometryNode, float widthRatio,
                                                           float heightRatio) {
        sgChanges.add(new SGChangeGeometryTexCoordsSet(geometryNode, widthRatio, heightRatio));
    }

    private synchronized void sgChangeGeometryTextureSet(GeometryNode geometryNode, Texture2D texture,
                                                         DrawingSurface surface) {
        sgChanges.add(new SGChangeGeometryTextureSet(geometryNode, texture, surface));
    }

    private synchronized void sgChangeGeometryOrthoZOrderSet(GeometryNode geometryNode, int zOrder) {
        sgChanges.add(new SGChangeGeometryOrthoZOrderSet(geometryNode, zOrder));
    }

    private synchronized void sgChangeGeometryCleanup(GeometryNode geometryNode) {
        sgChanges.add(new SGChangeGeometryCleanup(geometryNode));
    }

    private synchronized void sgChangeViewNodeOrthoSet(Node viewNode, boolean ortho) {
        sgChanges.add(new SGChangeViewNodeOrthoSet(viewNode, ortho));
    }

    private synchronized void sgChangeGeometryTransformOffsetStackSet (GeometryNode geometryNode,
                                                                       CellTransform transform) {
        sgChanges.add(new SGChangeGeometryTransformOffsetStackSet(geometryNode,transform));
    }

    protected synchronized void sgChangeTransformUserSet (Node viewNode, CellTransform transform) {
        sgChanges.add(new SGChangeTransformUserSet(viewNode, transform));
    }

    protected synchronized void sgChangeAttachPointSetAddEntity (RenderComponent rc, Node node,
                                                                 Entity parentEntity, Entity entity) {
        sgChanges.add(new SGChangeAttachPointSetAddEntity(rc, node, parentEntity, entity));
    }

    // Note: this method doesn't need to be synchronized because it does everything via a
    // synchronous render updater
    private void sgProcessChanges () {
        if (sgChanges.size() <= 0) return;

         ClientContextJME.getWorldManager().addRenderUpdater(new RenderUpdater() {
             public void update(Object arg0) {

                 for (SGChange sgChange : sgChanges) {
                     switch (sgChange.getOp()) {

                     case GEOMETRY_ATTACH_TO_VIEW: {
                         SGChangeGeometryAttachToView chg = (SGChangeGeometryAttachToView) sgChange;
                         chg.viewNode.attachChild(chg.geometryNode);
                         logger.fine("Attach geometryNode " + chg.geometryNode + " to viewNode " +
                                     chg.viewNode);
                         break;
                     }

                     case GEOMETRY_DETACH_FROM_VIEW: {
                         SGChangeGeometryDetachFromView chg = (SGChangeGeometryDetachFromView) sgChange;
                         chg.viewNode.detachChild(chg.geometryNode);
                         logger.fine("Detach geometryNode " + chg.geometryNode + " from viewNode " +
                                     chg.viewNode);
                         break;
                     }

                     case GEOMETRY_SIZE_SET: {
                         SGChangeGeometrySizeSet chg = (SGChangeGeometrySizeSet) sgChange;
                         chg.geometryNode.setSize(chg.width, chg.height);
                         forceTextureIdAssignment(true);
                         logger.fine("******** Geometry node = " + chg.geometryNode);
                         logger.fine("******** Geometry node setSize, wh = " + chg.width + ", " + chg.height);
                         break;
                     }

                     case GEOMETRY_TEX_COORDS_SET: {
                         SGChangeGeometryTexCoordsSet chg = (SGChangeGeometryTexCoordsSet) sgChange;
                         chg.geometryNode.setTexCoords(chg.widthRatio, chg.heightRatio);
                         logger.fine("******** viewNode = " + viewNode);
                         logger.fine("******** Geometry node setTexCoords, whRatio = " + chg.widthRatio + ", " +
                                     chg.heightRatio);
                         break;
                     }

                     case GEOMETRY_TEXTURE_SET: {
                         SGChangeGeometryTextureSet chg = (SGChangeGeometryTextureSet) sgChange;

                         DrawingSurface surface = chg.surface;
                         boolean restoreUpdating = false;
                         if (surface.getUpdateEnable()) {
                             surface.setUpdateEnable(false);
                             restoreUpdating = true;
                         }
                         chg.geometryNode.setTexture(chg.texture);
                        
                         if (restoreUpdating) {
                             surface.setUpdateEnable(true);
                         }

                         logger.fine("Geometry node setTexture, texture = " + chg.texture);
                         break;
                     }

                     case GEOMETRY_ORTHO_Z_ORDER_SET: {
                         SGChangeGeometryOrthoZOrderSet chg = (SGChangeGeometryOrthoZOrderSet) sgChange;
                         if (chg.geometryNode != null) {
                             chg.geometryNode.setOrthoZOrder(chg.zOrder);
                         }
                         logger.fine("Geometry set ortho z order = " + chg.zOrder);
                         break;
                     }

                     case GEOMETRY_CLEANUP: {
                         SGChangeGeometryCleanup chg = (SGChangeGeometryCleanup) sgChange;
                         chg.geometryNode.cleanup();
                         logger.fine("Geometry node cleanup");
                         break;
                     }

                     case VIEW_NODE_ORTHO_SET: {
                         SGChangeViewNodeOrthoSet chg = (SGChangeViewNodeOrthoSet) sgChange;
                         if (chg.ortho) {
                             chg.viewNode.setCullHint(Spatial.CullHint.Never);
                         } else {
                             chg.viewNode.setCullHint(Spatial.CullHint.Inherit);
                         }
                         logger.fine("View node ortho cull hint set = " + chg.ortho);
                         break;
                     }

                     case GEOMETRY_TRANSFORM_OFFSET_STACK_SET: {
                         // The offset/stack transform resides in the geometry
                         SGChangeGeometryTransformOffsetStackSet chg =
                             (SGChangeGeometryTransformOffsetStackSet) sgChange;
                         chg.geometryNode.setTransform(chg.transform);
                         logger.fine("Geometry node set transform, transform = " + chg.transform);
                         break;
                     }

                     case TRANSFORM_USER_SET: {
                         SGChangeTransformUserSet chg = (SGChangeTransformUserSet) sgChange;
                         CellTransform userTransform = chg.transform.clone(null);
                         Quaternion r = userTransform.getRotation(null);
                         chg.viewNode.setLocalRotation(r);
                         logger.fine("View node set rotation = " + r);
                         Vector3f t = userTransform.getTranslation(null);
                         chg.viewNode.setLocalTranslation(t);
                         logger.fine("View node set translation = " + t);
                         break;
                     }

                     case ATTACH_POINT_SET_ADD_ENTITY: {
                         SGChangeAttachPointSetAddEntity chg = (SGChangeAttachPointSetAddEntity) sgChange;
                         chg.rc.setAttachPoint(chg.node);
                        
                         // Note: in the latest MTGame, addEntity makes the entity immediately visible.
                         // So, to avoid having the scene graph come up at the wrong place, we need
                         // to perform the addEntity after setting the attach point.
                         if (chg.parentEntity != null && chg.entity != null) {
                             chg.parentEntity.addEntity(chg.entity);
                         }
                         break;
                     }

                     }
                 }


                 // Propagate changes to JME
                 if (viewNode != null) {
                     ClientContextJME.getWorldManager().addToUpdateList(viewNode);
                 }

                 sgChanges.clear();
             }
         }, null, true);
         // NOTE: it is critical that this render updater runs to completion before anything else happens
    }

    /** {@inheritDoc} */
    public void deliverEvent(Window2D window, MouseEvent3D me3d) {

        // NOTE: This is called on the EDT.

        /*
        System.err.println("********** me3d = " + me3d);
        System.err.println("********** awt event = " + me3d.getAwtEvent());
        PickDetails pickDetails = me3d.getPickDetails();
        System.err.println("********** pt = " + pickDetails.getPosition());
        */

        // No special processing is needed for wheel events. Just
        // send the 2D wheel event which is contained in the 3D event.
        if (me3d instanceof MouseWheelEvent3D) {
            controlArb.deliverEvent(window, (MouseEvent) me3d.getAwtEvent());
            return;
        }

        // Can't convert if there is no geometry
        if (geometryNode == null) {
            return;
        }

        // Convert mouse event intersection point to 2D. For most events this is the intersection
        // point based on the destination pick details calculated by the input system, but for drag
        // events this needs to be derived from the actual hit pick details (because for drag events
        // the destination pick details might be overridden by a grab).
        Point point;
        if (me3d.getID() == MouseEvent.MOUSE_DRAGGED) {
            MouseDraggedEvent3D de3d = (MouseDraggedEvent3D) me3d;
            point = geometryNode.calcPositionInPixelCoordinates(de3d.getHitIntersectionPointWorld(), true);
        } else {
            point = geometryNode.calcPositionInPixelCoordinates(me3d.getIntersectionPointWorld(), false);
        }
        if (point == null) {
            // Event was outside our panel so do nothing
            // This can happen for drag events
            return;
        }

        // Construct a corresponding 2D event
        MouseEvent me = (MouseEvent) me3d.getAwtEvent();
        int id = me.getID();
        long when = me.getWhen();
        int modifiers = me.getModifiers();
        int button = me.getButton();

        // TODO: WORKAROUND FOR A WONDERLAND PICKER PROBLEM:
        // See comment for pointerMoveSeen above
        if (id == MouseEvent.MOUSE_RELEASED && pointerMoveSeen) {
            point.x = pointerLastX;
            point.y = pointerLastY;
        }

        me = new MouseEvent(dummyButton, id, when, modifiers, point.x, point.y,
                0, false, button);

        // Send event to the window's control arbiter
        controlArb.deliverEvent(window, me);

        // TODO: WORKAROUND FOR A WONDERLAND PICKER PROBLEM:
        // See comment for pointerMoveSeen above
        if (id == MouseEvent.MOUSE_MOVED || id == MouseEvent.MOUSE_DRAGGED) {
            pointerMoveSeen = true;
            pointerLastX = point.x;
            pointerLastY = point.y;
        }
    }

    /** {@inheritDoc} */
    public Point calcPositionInPixelCoordinates(Vector3f point, boolean clamp) {
        if (geometryNode == null) {
            return null;
        }
        return geometryNode.calcPositionInPixelCoordinates(point, clamp);
    }

    /** {@inheritDoc} */
    public Point calcIntersectionPixelOfEyeRay(int x, int y) {
        if (geometryNode == null) {
            return null;
        }
        if (isOrtho()) {
            return geometryNode.calcIntersectionPixelOfEyeRayOrtho(x, y);
        } else {
            return geometryNode.calcIntersectionPixelOfEyeRay(x, y);
        }
    }

    /**
     * Force the texture ID of the texture to be allocated.
     * @param inRenderLoop true if the call is already being made from inside the render loop.
     */
    private void forceTextureIdAssignment(boolean inRenderLoop) {
        if (geometryNode == null) {
            setGeometryNode(null);
            if (geometryNode == null) {
                logger.severe("Cannot allocate geometry node for view!!");
                return;
            }
        }
        final TextureState ts = geometryNode.getTextureState();
        if (ts == null) {
            logger.warning("Trying to force texture id assignment while view texture state is null");
            return;
        }

        logger.info("texid alloc: ts.getTexture() = " + ts.getTexture());

        Texture tex = ts.getTexture();
        if (tex != null) {
            if (tex.getTextureId() != 0) {
                // Don't allocate a texture ID if one is already allocated. This can happen,
                // for example, when a new view is created which shows the same texture as
                // an existing view. One such case is Apps-in-HUD.
                return;
            }
        }

        if (inRenderLoop) {
            // We're already in the render loop
            ts.load();
        } else {
            // Not in render loop. Must do this inside the render loop.
            ClientContextJME.getWorldManager().addRenderUpdater(new RenderUpdater() {
                public void update(Object arg0) {
                    // The JME magic - must be called from within the render loop
                    ts.load();
                }
           }, null, true)// Note: a rare case in which we must wait for the render updater to complete
        }

        /* For debug: Verify that ID was actually allocated
        Texture tex = geometryNode.getTexture();
        if (tex != null) {
            int texid = tex.getTextureId();
            logger.severe("))))))))))) V2E: texid alloc: texid = " + texid);
            if (texid == 0) {
                logger.severe("Failed to allocated texture ID");
            }
        }
        */
    }

    /** {@inheritDoc} */
    public synchronized void addEventListener(EventListener listener) {
        listener.addToEntity(entity);
    }

    /** {@inheritDoc} */
    public synchronized void removeEventListener(EventListener listener) {
        if (listener != null) {
            listener.removeFromEntity(entity);
        }
    }

    /** {@inheritDoc} */
    public void addEntityComponent(Class clazz, EntityComponent comp) {
        entity.addComponent(clazz, comp);
    }

    /** {@inheritDoc} */
    public void removeEntityComponent(Class clazz) {
        if (entity != null) {
            entity.removeComponent(clazz);
        }
    }

    /**
     * Return whether this view has an attached frame.
     */
    protected boolean hasFrame () {
        return false;
    }

    /**
     * Attach a frame to this view. This can be overridden by the subclass so a
     * subclass-specific frame can be attached.
     */
    protected void attachFrame () {
    }

    /**
     * Detach this view's frame from the view.
     */
    protected void detachFrame () {
    }

    /**
     * Detach frame from the view and reattach it. This takes into account any changes
     * in the type of the view. This can be overridden by the subclass so a
     * subclass-specific frame can be reattached.
     */
    protected void reattachFrame () {
    }

    /**
     * Update the frame's title.
     */
    protected void frameUpdateTitle () {
    }

    /**
     * Update this frame's userResizable attribute
     */
    protected void frameUpdateUserResizable () {
    }


    /**
     * Update the frame.
     */
    protected void frameUpdate () {
    }

    /**
     * Make an entity pickable by attaching a collision component. Entity must already have
     * a render component and a scene root node.
     */
    public static void entityMakePickable (Entity entity) {
        JMECollisionSystem collisionSystem = (JMECollisionSystem) ClientContextJME.getWorldManager().
            getCollisionManager().loadCollisionSystem(JMECollisionSystem.class);
        RenderComponent rc = (RenderComponent) entity.getComponent(RenderComponent.class);
        CollisionComponent cc = collisionSystem.createCollisionComponent(rc.getSceneRoot());
        entity.addComponent(CollisionComponent.class, cc);
        cc.setCollidable(true);
        cc.setInheritCollidable(true);
    }


    /** {@inheritDoc} */
    public void updateFrame () {
     
      // issue 151
      if (inCleanup) {
        return;
      }
       
      for (FrameChange frameChg : frameChanges) {
            switch (frameChg) {
            case ATTACH_FRAME:
                attachFrame();
                break;
            case DETACH_FRAME:
                detachFrame();
                break;
            case REATTACH_FRAME:
                reattachFrame();
                break;
            case UPDATE_TITLE:
                frameUpdateTitle();
                break;
            case UPDATE_USER_RESIZABLE:
                frameUpdateUserResizable();
                break;
            }
        }
        frameChanges.clear();

        frameUpdate();
    }

    @Override
    public String toString () {
        return name;
    }
}
TOP

Related Classes of org.jdesktop.wonderland.modules.appbase.client.view.View2DEntity

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.