Package edu.mit.blocks.renderable

Source Code of edu.mit.blocks.renderable.RenderableBlock$RenderableBlockState

package edu.mit.blocks.renderable;

import java.awt.AlphaComposite;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.GraphicsEnvironment;
import java.awt.Image;
import java.awt.Point;
import java.awt.PopupMenu;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.geom.AffineTransform;
import java.awt.geom.Area;
import java.awt.geom.Point2D;
import java.awt.image.BufferedImage;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;

import javax.swing.ImageIcon;
import javax.swing.JComponent;
import javax.swing.JToolTip;
import javax.swing.KeyStroke;
import javax.swing.SwingUtilities;

import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import edu.mit.blocks.codeblocks.Block;
import edu.mit.blocks.codeblocks.BlockConnector;
import edu.mit.blocks.codeblocks.BlockConnectorShape;
import edu.mit.blocks.codeblocks.BlockLink;
import edu.mit.blocks.codeblocks.BlockLinkChecker;
import edu.mit.blocks.codeblocks.BlockShape;
import edu.mit.blocks.codeblocks.InfixBlockShape;
import edu.mit.blocks.codeblocks.JComponentDragHandler;
import edu.mit.blocks.codeblocks.rendering.BlockShapeUtil;
import edu.mit.blocks.codeblockutil.CToolTip;
import edu.mit.blocks.codeblockutil.GraphicsManager;
import edu.mit.blocks.renderable.BlockImageIcon.ImageLocation;
import edu.mit.blocks.workspace.ContextMenu;
import edu.mit.blocks.workspace.FactoryManager;
import edu.mit.blocks.workspace.ISupportMemento;
import edu.mit.blocks.workspace.MiniMap;
import edu.mit.blocks.workspace.RBParent;
import edu.mit.blocks.workspace.SearchableElement;
import edu.mit.blocks.workspace.Workspace;
import edu.mit.blocks.workspace.WorkspaceEvent;
import edu.mit.blocks.workspace.WorkspaceWidget;

/**
* RenderableBlock is responsible for all graphical rendering of a code Block.  This class is also
* responsible for consuming all mouse and key events on itself.  Each RenderableBlock object is
* coupled with its associated Block object, and uses information maintained in Block to
* render the graphical block accordingly.
*/
public class RenderableBlock extends JComponent implements SearchableElement, MouseListener, MouseMotionListener, ISupportMemento, CommentSource {

    private static final long serialVersionUID = 1L;
    // The following may be null: parent, lastdragwidget, comment

    /** True if in debug mode */
    private static final boolean DEBUG = false;

    /** The maximum distance between blocks that are still considered nearby enough to link */
    private static final double NEARBY_RADIUS = 20.0;

    /** The alpha level while dragging - lower means more transparent */
    private static final float DRAGGING_ALPHA = 0.66F;

    ///////////////////////
    //COMPONENT FIELDS

    /** The workspace in use */
    protected final Workspace workspace;

    /** BlockID of this.  MAY BE Block.NULL */
    private final Long blockID;
    /** Parent workspace widget.  May be null */
    private WorkspaceWidget parent;
    /** The previous known workspacewidget this block was dragged over.  May be null */
    private WorkspaceWidget lastDragWidget = null;
    /** The comment of this.  May be null */
    private Comment comment = null;
    /**  set true when comment is added or removed from this block */
    private boolean commentLabelChanged = false;
    /**
     * An internal JComponent whose functionality is independant of any other
     * functionality. If the block widget is the largest component in the
     * block, then the renderableblock's Shape is determined form the dimensions
     * of this widget.  They should not be related to starlogo or codeblocks.  MAY BE NULL*/
    private JComponent blockWidget = null;
    /////////////////////////////////
    //RENDERING RELATED FIELDS
    /** Shape components used to draw this block's geometrical shape.  Includes:
     * (1) the block shape which is an abstract outline of the shape,
     * (2) the abstractBlockArea which is a filled in abstract shape,
     * (3) blockArea which is a filled in pixel shape,
     * (4) and the popupIconShape which is the popupicon of this block*/
    private BlockShape blockShape;
    private Area abstractBlockArea;
    private Area blockArea;
    /** static drawing area for unstable blocks.  MAY BE NULL */
    private BufferedImage buffImg = null;
    //////////////////////////////////////
    //Internal Managers
    /** HighlightManager that manages drawing of highlights around this block */
    private RBHighlightHandler highlighter;
    /** dragHandler keeps the block within the workspace area. It manages relocating the block. */
    private JComponentDragHandler dragHandler;
    ////////////////////////
    //ATTRIBUTE FIELDS
    /** Binary atttributes of this RenderableBlocks:
     * (1) popupIconVisible is true if the popup icon is visible,
     * (2) isSearchResult is true if this block is being queried by search
     * (3) isPickedUp is true if mousePressed was performed on this block,
     * (4) dragging is true if mouseDragged was performed on this block at least once,
     * (5) linkedDefArgsBefore is any default arguments were never attached
     * (6) isLoading is true if RenderableBlock is still loading- Though its data may
     *         have loaded completely, it still may need other connected
     *         RenderableBlocks to finish loading as well.  In this
     *         case, isLoading would still be false */
    private boolean isSearchResult = false;
    private boolean pickedUp = false;
    private boolean dragging = false;
    private boolean linkedDefArgsBefore = false;
    private boolean isLoading = false;
    ///////////////////////////
    //Sockets and Labels
    /** TODO: Documentation does not exist for these components.  Consult author*/
    private final NameLabel blockLabel;
    private final PageLabel pageLabel;
    private final ConnectorTag plugTag;
    private final ConnectorTag afterTag;
    private final ConnectorTag beforeTag;
    private List<ConnectorTag> socketTags = new ArrayList<ConnectorTag>();
    ////////////////////////////////
    // Collapse Label
    private CollapseLabel collapseLabel;
    //////////////////////////////////
    //TO BE DEPRECATED
    private HashMap<ImageLocation, BlockImageIcon> imageMap = new HashMap<ImageLocation, BlockImageIcon>();
    // the values of the x and y coordinates of block when zoom = 1.0
    private double unzoomedX;
    private double unzoomedY;

    /**
     * Constructs a new RenderableBlock instance with the specified parent WorkspaceWidget and
     * Long blockID of its associated Block
     * @param workspace The workspace in use
     * @param parent the WorkspaceWidget containing this
     * @param blockID Long Block id of associated with this
     */
    public RenderableBlock(Workspace workspace, WorkspaceWidget parent, Long blockID) {
        this(workspace, parent, blockID, false);
    }

    /**
     * Constructs a new RenderableBlock instance with the specified parent WorkspaceWidget and
     * Long blockID of its associated Block
     * @param workspace The workspace in use
     * @param parent the WorkspaceWidget containing this
     * @param blockID Long Block id of associated with this
     * @param isLoading indicates if this block is still waiting for all information
     * needed to properly construct it
     */
    private RenderableBlock(Workspace workspace, WorkspaceWidget parent, Long blockID, boolean isLoading) {
        super();
        this.workspace = workspace;
        /*
         * Sets whether focus traversal keys are enabled
         * for this Component. Components for which focus
         * traversal keys are disabled receive key events
         * for focus traversal keys.
         */
        this.setFocusTraversalKeysEnabled(false);

        this.parent = parent;
        this.blockID = blockID;
        workspace.getEnv().addRenderableBlock(this);

        //initialize block image map
        //note: must do this before updateBuffImg();
        for (BlockImageIcon img : getBlock().getInitBlockImageMap().values()) {
            imageMap.put(img.getImageLocation(), new BlockImageIcon(img.getImageIcon(),
                    img.getImageLocation(), img.isEditable(), img.wrapText()));
            add(imageMap.get(img.getImageLocation()));
        }
        //set null layout so as to add blockLabels where ever we want
        setLayout(null);

        dragHandler = new JComponentDragHandler(workspace, this); // set up drag handler delegate
        addMouseListener(this);
        addMouseMotionListener(this);


        //initialize tags, labels, and sockets:
        this.plugTag = new ConnectorTag(getBlock().getPlug());
        this.afterTag = new ConnectorTag(getBlock().getAfterConnector());
        this.beforeTag = new ConnectorTag(getBlock().getBeforeConnector());
        this.blockLabel = new NameLabel(workspace, getBlock().getBlockLabel(), BlockLabel.Type.NAME_LABEL, getBlock().isLabelEditable(), blockID);
        this.pageLabel = new PageLabel(workspace, getBlock().getPageLabel(), BlockLabel.Type.PAGE_LABEL, false, blockID);
        this.add(pageLabel.getJComponent());
        this.add(blockLabel.getJComponent(), 0);
        synchronizeSockets();

        // initialize collapse label
        if (getBlock().isProcedureDeclBlock() && (parent == null || !(parent instanceof FactoryManager))) {
            this.collapseLabel = new CollapseLabel(workspace, blockID);
            this.add(collapseLabel);
        }

        //form basic shape
        if (getBlock().isInfix()) {
            blockShape = new InfixBlockShape(this);
        } else {
            blockShape = new BlockShape(this);
        }

        if (!isLoading) {
            //reformBlockShape so as to update socket points to position labels and setBounds of this rb
            reformBlockShape();

            //to cache image upon instantiation, update buffered image here:
            updateBuffImg();
        } else {
            blockArea = new Area();
        }

        highlighter = new RBHighlightHandler(this);

        String blockDescription = getBlock().getBlockDescription();
        if (blockDescription != null) {
            setBlockToolTip(getBlock().getBlockDescription().trim());
        }
        setCursor(dragHandler.getDragHintCursor());
    }

    /**
     * Returns the workspace in which this block is living in
     * @return Thw eorkspace in use
     */
    public Workspace getWorkspace() {
        return workspace;
    }

    /**
     * Returns the Long id of this
     * @return the Long id of this
     */
    @Override
    public Long getBlockID() {
        return blockID;
    }

    /**
     * Returns the height of the block shape of this
     * @return the height of the block shape of this
     */
    public int getBlockHeight() {
        return blockArea.getBounds().height;
    }

    /**
     * Returns the dimensions of the block shape of this
     * @return the dimensions of the block shape of this
     */
    public Dimension getBlockSize() {
        return blockArea.getBounds().getSize();
    }

    /**
     * Returns the width of the block shape of this
     * @return the width of the block shape of this
     */
    public int getBlockWidth() {
        return blockArea.getBounds().width;
    }

    /**
     * Returns the BlockShape instance representing this
     * @return the BlockShape instance representing this
     */
    public BlockShape getBlockShape() {
        return blockShape;
    }

    /**
     * @return the abstractBlockArea
     */
    Area getAbstractBlockArea() {
        return abstractBlockArea;
    }

    /**
     * @param abstractBlockArea the abstractBlockArea to set
     */
    void setAbstractBlockArea(Area abstractBlockArea) {
        this.abstractBlockArea = abstractBlockArea;
    }

    /**
     * Moves this component to a new location. The top-left corner of
     * the new location is specified by the <code>x</code> and <code>y</code>
     * parameters in the coordinate space of this component's parent.
     * @param x the <i>x</i>-coordinate of the new location's
     *          top-left corner in the parent's coordinate space
     * @param y the <i>y</i>-coordinate of the new location's
     *          top-left corner in the parent's coordinate space
     */
    @Override
    public void setLocation(int x, int y) {
        int dx, dy;
        dx = x - getX();
        dy = y - getY();
        super.setLocation(x, y);

        if (hasComment() && !(dx == x && dy == y)) {
            if (getComment().getParent() != getParent()) {
                getComment().setParent(getParent(), Workspace.DRAGGED_BLOCK_LAYER);
            }
            getComment().translatePosition(dx, dy);
        }
    }

    /**
     * Moves this component to a new location. The top-left corner of
     * the new location is specified by point <code>p</code>. Point
     * <code>p</code> is given in the parent's coordinate space.
     * @param p the point defining the top-left corner
     *          of the new location, given in the coordinate space of this
     *          component's parent
     */
    @Override
    public void setLocation(Point p) {
        setLocation(p.x, p.y);
    }

    /**
     * Returns the width of the stroke used to draw the highlight.
     * Note that the highlight will only appear half this width,
     * so the overall width of the block + highlight will be
     * blockWidth + highlightStrokeWidth.
     * @return the width of the stroke used to draw the highlight.
     */
    public int getHighlightStrokeWidth() {
        return RBHighlightHandler.HIGHLIGHT_STROKE_WIDTH;
    }

    /**
     * Returns the bounds of the block stack of this, where this block
     * is at the top of its stack (in other words, it does not take the
     * bounds of the blocks above it into account).
     * @return the bounds of the block stack of this, where this block
     * is at the top of its stack.
     */
    public Rectangle getStackBounds() {
        return new Rectangle(this.getLocation(), calcStackDimensions(this));
    }

    /**
     * Helper method to calculate the bounds of a stack.  For now this method naively traverses
     * through the entire stack of the specified RenderableBlock rb and calculates the bounds.
     * @param rb the RenderableBlock to calculate the stack bounds of
     * @return Dimensions of the stack of the specified rb
     */
    private Dimension calcStackDimensions(RenderableBlock rb) {
        if (rb.getBlock().getAfterBlockID() != Block.NULL) {
            Dimension dim = calcStackDimensions(workspace.getEnv().getRenderableBlock(rb.getBlock().getAfterBlockID()));
            return new Dimension(Math.max(rb.getBlockWidth() + rb.getMaxWidthOfSockets(rb.getBlockID()),
                    dim.width),
                    rb.getBlockHeight() + dim.height);
        } else {
            return new Dimension(rb.getBlockWidth() + rb.getMaxWidthOfSockets(rb.blockID),
                    rb.getBlockHeight());
        }
    }

    /**
     * sets the label to belonging to this renderable block to
     * editing state == true (editing mode)
     */
    public void switchToLabelEditingMode(boolean highlighted) {
        if (getBlock().isLabelEditable()) {
            if (highlighted) {
                this.blockLabel.setEditingState(true);
                this.blockLabel.highlightText();
            } else {
                this.blockLabel.setEditingState(true);
            }
        }
    }

    /**
     * returns the blockWidget for this RenderableBlock
     * @return
     */
    JComponent getBlockWidget() {
        return blockWidget;
    }

    /**
     * @return the dimension of the sole block widget in this block.
     *        May NOT return null.
     */
    public Dimension getBlockWidgetDimension() {
        if (this.blockWidget == null) {
            return new Dimension(0, 0);
        } else {
            return this.blockWidget.getSize();
        }
    }

    /**
     * @param blockWidget
     *
     * @requires none
     * @modifies this.blockWidget
     * @effects sets block widget to the input argument "blockWidget"
     *       and revalidates the JComponent representation of renderableblock
     */
    public void setBlockWidget(JComponent blockWidget) {
        if (this.blockWidget != null) {
            this.remove(this.blockWidget);
        }
        this.blockWidget = blockWidget;
        if (blockWidget != null) {
            this.add(blockWidget);
        }
        this.revalidate();

    }

    public JComponentDragHandler getDragHandler() {
        return dragHandler;
    }

    /**
     * Returns the BlockImageIcon instance at the specified location; null if
     * no BlockImageIcon exists at that location
     * @param location the ImageLocation of the desired BlockImageIcon
     * @return the BlockImageIcon instance at the specified location; null if
     * no BlockImageIcon exists at that location
     */
    public BlockImageIcon getImageIconAt(ImageLocation location) {
        return imageMap.get(location);
    }

    ///////////////////
    // LABEL METHODS //
    ///////////////////
    /**
     * Synchronizes this RenderableBlock's socket components (including tags, labels)
     * with the associated Block's list of sockets.
     * Complexity:   Running time for n Block sockets
     *         and m Renderable tags: O(m+nm)=O(nm)
     * @effects for every socket in Block:
     *         (1) check/add corresponding tag structure,
     *         (2) check/add block label
     *       for every tag in Renderable:
     *         (1) delete any sockets not in Block
     */
    private boolean synchronizeSockets() {
        boolean changed = false;
        List<ConnectorTag> newSocketTags = new ArrayList<ConnectorTag>();
        for (ConnectorTag tag : socketTags) {
            if (tag.getLabel() != null) {
                this.remove(tag.getLabel().getJComponent());
            }
        }
        for (int i = 0; i < getBlock().getNumSockets(); i++) {
            BlockConnector socket = getBlock().getSocketAt(i);
            ConnectorTag tag = this.getConnectorTag(socket);
            if (tag == null) {
                tag = new ConnectorTag(socket);
                if (SocketLabel.ignoreSocket(socket)) {
                    tag.setLabel(null); //ignored sockets have no labels
                } else {
                    SocketLabel label = new SocketLabel(workspace, socket, socket.getLabel(), BlockLabel.Type.PORT_LABEL, socket.isLabelEditable(), blockID);
                    String argumentToolTip = getBlock().getArgumentDescription(i);
                    if (argumentToolTip != null) {
                        label.setToolTipText(getBlock().getArgumentDescription(i).trim());
                    }
                    tag.setLabel(label);
                    label.setZoomLevel(this.getZoom());
                    label.setText(socket.getLabel());
                    this.add(label.getJComponent());
                    changed = true;
                }
            } else {
                SocketLabel label = tag.getLabel();
                if (!SocketLabel.ignoreSocket(socket)) {
                    //ignored bottom sockets or sockets with label == ""
                    if (label == null) {
                        label = new SocketLabel(workspace, socket, socket.getLabel(), BlockLabel.Type.PORT_LABEL, socket.isLabelEditable(), blockID);
                        String argumentToolTip = getBlock().getArgumentDescription(i);
                        if (argumentToolTip != null) {
                            label.setToolTipText(getBlock().getArgumentDescription(i).trim());
                        }
                        tag.setLabel(label);
                        label.setText(socket.getLabel());
                        this.add(label.getJComponent());
                        changed = true;
                    } else {
                        label.setText(socket.getLabel());
                        this.add(label.getJComponent());
                        changed = true;
                    }
                    label.setZoomLevel(this.getZoom());
                }
            }
            newSocketTags.add(tag);
        }
        this.socketTags.clear();
        this.socketTags = newSocketTags;
        return changed;
    }

    /**
     * Updates all the labels within this block.  Returns true if this update found any changed labels; false otherwise
     * @return true if this update found any changed labels; false otherwise.
     */
    private boolean synchronizeLabelsAndSockets() {
        boolean blockLabelChanged = getBlock().getBlockLabel() != null && !blockLabel.getText().equals(getBlock().getBlockLabel());
        boolean pageLabelChanged = getBlock().getPageLabel() != null && !pageLabel.getText().equals(getBlock().getPageLabel());
        boolean socketLabelsChanged = false;

        // If tag label isn't the same as socket label, synchronize.
        // If the block doesn't have an editable socket label, synchronize.
        //
        // Needed to not synchronize the socket if it is label editable so it doesn't synchronize when
        // it gains focus.
        //
        // May possibly be done better if synchronizeSockets is rewritten. It has to be written such that
        // it doesn't remove the sockets' JComponents/remake them. Currently relies on the synchronizeSockets()
        // call in getSocketPixelPoint(BlockConnector) to make sure the dimensions and number of sockets
        // are consistent.
        for (int i = 0; i < getBlock().getNumSockets(); i++) {
            BlockConnector socket = getBlock().getSocketAt(i);
            ConnectorTag tag = this.getConnectorTag(socket);
            if (tag != null) {
                if (tag.getLabel() != null) {
                    if (!tag.getLabel().getText().equals(socket.getLabel())) {
                        socketLabelsChanged = synchronizeSockets();
                        break;
                    }
                }
            }
            if (!socket.isLabelEditable()) {
                socketLabelsChanged = synchronizeSockets();
                break;
            }
        }
        if (blockLabelChanged) {
            blockLabel.setText(getBlock().getBlockLabel());
        }
        if (pageLabelChanged) {
            pageLabel.setText(getBlock().getPageLabel());
        }
        if (blockLabelChanged || pageLabelChanged || socketLabelsChanged || commentLabelChanged) {
            reformBlockShape();
            commentLabelChanged = false;
        }
        if (BlockLinkChecker.hasPlugEquivalent(getBlock())) {
            BlockConnector plug = BlockLinkChecker.getPlugEquivalent(getBlock());
            Block plugBlock = workspace.getEnv().getBlock(plug.getBlockID());
            if (plugBlock != null) {
                if (plugBlock.getConnectorTo(blockID) == null) {
                    throw new RuntimeException("one-sided connection from " + getBlock().getBlockLabel() + " to " + workspace.getEnv().getBlock(blockID).getBlockLabel());
                }
                workspace.getEnv().getRenderableBlock(plug.getBlockID()).updateSocketSpace(plugBlock.getConnectorTo(blockID), blockID, true);
            }
        }
        return false;
    }

    /**
     * Determine the width necessary to accommodate for placed labels.  Used to
     * determine the minimum width of a block.
     * @return int pixel width needed for the labels
     */
    public int accomodateLabelsWidth() {
        int maxSocketWidth = 0;
        int width = 0;

        for (ConnectorTag tag : socketTags) {
            SocketLabel label = tag.getLabel();
            if (label != null) {
                maxSocketWidth = Math.max(maxSocketWidth, label.getAbstractWidth());
            }
        }
        if (getBlock().hasPageLabel()) {
            width += Math.max(blockLabel.getAbstractWidth(), pageLabel.getAbstractWidth()) + maxSocketWidth;
            width += getControlLabelsWidth();
        } else {
            width += blockLabel.getAbstractWidth() + maxSocketWidth;
            width += getControlLabelsWidth() + 4;
        }
        return width;
    }

    /**
     * Returns the width of the page label on this block; if page label
     * is not enabled and does not exist, returns 0.
     * @return the width of the page label on this block iff page label
     * is enabled and exists; returns 0 otherwise.
     */
    public int accomodatePageLabelHeight() {
        if (getBlock().hasPageLabel()) {
            return pageLabel.getAbstractHeight();
        } else {
            return 0;
        }
    }

    /**
     * Sets all the labels of this block as uneditable block labels.
     * Useful for Factory blocks.
     */
    public void setBlockLabelUneditable() {
        blockLabel.setEditable(false);
    }

    ////////////////////////////////////////
    // BLOCK IMAGE MANAGEMENT AND METHODS //
    ////////////////////////////////////////
    /**
     * Returns the total height of all the images to draw on this block
     * @return the total height of all the images to draw on this block
     */
    public int accomodateImagesHeight() {
        int maxImgHt = 0;
        for (BlockImageIcon img : getBlock().getInitBlockImageMap().values()) {
            maxImgHt += img.getImageIcon().getIconHeight();
        }
        return maxImgHt;
    }

    /**
     * Returns the total width of all the images to draw on this block
     * @return the total width of all the images to draw on this block
     */
    public int accomodateImagesWidth() {
        int maxImgWt = 0;
        for (BlockImageIcon img : getBlock().getInitBlockImageMap().values()) {
            maxImgWt += img.getImageIcon().getIconWidth();
        }
        return maxImgWt;
    }

    //////////////////////////////////////////////
    //BLOCK LINKING CHECKS ON OTHER RENDERABLES //
    //////////////////////////////////////////////
    /**
     * Looks for links between this RenderableBlock and others.
     * @return a BlockLink object with information on the closest possible linking between this RenderableBlock and another.
     */
    public BlockLink getNearbyLink() {
        return BlockLinkChecker.getLink(workspace, this, workspace.getBlockCanvas().getBlocks());
    }

    ///////////////////////
    /// SOCKET METHODS ////
    ///////////////////////
    /**
     * Returns the maximum width between all the socket connectors of this or 0 if this does not
     * have any sockets
     * @return the maximum width between all the socket connectors of this or 0 if this does not
     * have any sockets
     */
    public int getMaxSocketShapeWidth() {
        int maxSocketWidth = 0;
        for (BlockConnector socket : getBlock().getSockets()) {
            int socketWidth = BlockConnectorShape.getConnectorDimensions(socket).width;
            if (socketWidth > maxSocketWidth) {
                maxSocketWidth = socketWidth;
            }
        }

        return maxSocketWidth;
    }

    /**
     * Returns a new Point object that represents the pixel location of this socket's center.
     * Mutating the new Point will not affect future calls to getSocketPoint; that is, this
     * method clones a new Point object.  The new Point object MAY NOT BE NULL.
     *
     * @param socket - the socket whose point we want.  socket MAY NOT BE NULL.
     * @return a Point representing the socket's center
     * @requires socket != null and socket is one of this block's socket
     */
    public Point getSocketPixelPoint(BlockConnector socket) {
        ConnectorTag tag = this.getConnectorTag(socket);
        if (tag != null) {
            return tag.getPixelLocation();
        }

        System.out.println("Error, Socket has no connector tag: " + socket);
        return new Point(0, -100); //JBT hopefully this doesn't hurt anything,  this is masking a bug that needs to be tracked down, why is the connector tag missing?
    }

    /**
     * Returns a new Point object that represents the abstract location of this socket's center.
     * Mutating the new Point will not affect future calls to getSocketPoint; that is, this
     * method clones a new Point object.  The new Point object MAY NOT BE NULL.
     *
     * @param socket - the socket whose point we want.  socket MAY NOT BE NULL.
     * @return a Point representing the socket's center
     * @requires socket != null and socket is one of this block's socket
     */
    public Point getSocketAbstractPoint(BlockConnector socket) {
        ConnectorTag tag = this.getConnectorTag(socket);
        return tag.getAbstractLocation();
    }

    /**
     * Updates the center point location of this socket
     *
     * @param socket - the socket whose point we will update.  Socket MAY NOT BE NULL
     * @param point - the ABSTRACT location of socket's center.  ABSTRACT LOCATION!!!
     *
     * @requires socket != null and there exist a matching tag for the socket
     */
    public void updateSocketPoint(BlockConnector socket, Point2D point) {
        ConnectorTag tag = this.getConnectorTag(socket);
        //TODO: what if tag does not exist?  should we throw exception or add new tag?
        tag.setAbstractLocation(point);

    }

    /**
     * Updates the renderable block with the underlying block's before,
     * after, and plug connectors.
     */
    public void updateConnectors() {
        Block b = workspace.getEnv().getBlock(blockID);
        afterTag.setSocket(b.getAfterConnector());
        beforeTag.setSocket(b.getBeforeConnector());
        plugTag.setSocket(b.getPlug());
    }

    /////////////////////////////////////
    // PARENT WORKSPACE WIDGET METHODS //
    /////////////////////////////////////
    /**
     * Returns the parent WorkspaceWidget containing this
     * @return the parent WorkspaceWidget containing this
     */
    @Override
    public WorkspaceWidget getParentWidget() {
        return parent;
    }

    /**
     * Sets the parent WorkspaceWidget containing this
     * @param widget the desired WorkspaceWidget
     */
    public void setParentWidget(WorkspaceWidget widget) {
        parent = widget;
    }

    /**
     * Overriding JComponent.contains(int x, int y) so that this component's
     * boundaries are defined by the actual area occupied by the Renderable
     * Block shape.  Returns true iff the specified coordinates are contained
     * within the area of the BlockShape.
     * @return true iff the specified coordinates are contained within the Area
     * of the BlockShape
     */
    @Override
    public boolean contains(int x, int y) {
        return blockArea.contains(x, y);
    }

    //////////////////////
    // BLOCK MANAGEMENT //
    //////////////////////
    /**
     * Shortcut to get block with current BlockID of this renderable block.
     */
    public Block getBlock() {
        return workspace.getEnv().getBlock(this.blockID);
    }

    public Color getBLockColor() {
        return getBlock().getColor();
    }

    /**
     * Links the default arguments of this block if it has any and if this block has not already linked its
     * default args in this session.  Re-linking this block's default args everytime it gets dropped/moved
     * within the block canvas can get annoying.
     */
    public void linkDefArgs() {
        if (!linkedDefArgsBefore && getBlock().hasDefaultArgs()) {
            Iterator<Long> ids = getBlock().linkAllDefaultArgs().iterator();
            Iterator<BlockConnector> sockets = getBlock().getSockets().iterator();
            Long id;
            BlockConnector socket;

            // Store the ids, sockets, and blocks we need to update.
            List<Long> idList = new ArrayList<Long>();
            List<BlockConnector> socketList = new ArrayList<BlockConnector>();
            List<RenderableBlock> argList = new ArrayList<RenderableBlock>();
            while (ids.hasNext() && sockets.hasNext()) {
                id = ids.next();
                socket = sockets.next();
                if (id != Block.NULL) {
                    //for each block id, create a new RenderableBlock
                    RenderableBlock arg = new RenderableBlock(workspace, this.getParentWidget(), id);
                    arg.setZoomLevel(this.zoom);
                    //getParentWidget().addBlock(arg);
                    //arg.repaint();
                    //this.getParent().add(arg);
                    //set the location of the def arg at
                    Point myLocation = getLocation();
                    Point2D socketPt = getSocketPixelPoint(socket);
                    Point2D plugPt = arg.getSocketPixelPoint(arg.getBlock().getPlug());
                    arg.setLocation((int) (socketPt.getX() + myLocation.x - plugPt.getX()), (int) (socketPt.getY() + myLocation.y - plugPt.getY()));
                    //update the socket space of at this socket
                    this.getConnectorTag(socket).setDimension(new Dimension(
                            arg.getBlockWidth() - (int) BlockConnectorShape.NORMAL_DATA_PLUG_WIDTH,
                            arg.getBlockHeight()));
                    //drop each block to this parent's widget/component
                    //getParentWidget().blockDropped(arg);
                    getParentWidget().addBlock(arg);

                    idList.add(id);
                    socketList.add(socket);
                    argList.add(arg);
                }
            }

            int size = idList.size();
            for (int i = 0; i < size; i++) {
                workspace.notifyListeners(
                        new WorkspaceEvent(workspace, this.getParentWidget(),
                        argList.get(i).getBlockID(),
                        WorkspaceEvent.BLOCK_ADDED, true));

                //must call this method to update the dimensions of this
                //TODO ria in the future would be good to just link the default args
                //but first creating a block link object and then connecting
                //something like notifying the renderableblock to update its dimensions will be
                //take care of
                this.blockConnected(socketList.get(i), idList.get(i));
                argList.get(i).repaint();
            }
            this.redrawFromTop();
            linkedDefArgsBefore = true;
        }
    }

    /**
     * Modifies this RenderableBlock such that default
     * arguments are ignored.  In the future, invoking
     * this.linkDefArgs() will trigger no action.
     *
     * @requires none
     * @modifies this.linkedDefArgsBefore;
     * @effects sets linkedDefArgsBefore to false;
     */
    public void ignoreDefaultArguments() {
        linkedDefArgsBefore = true;
    }

    ////////////////////////
    //// BLOCK RESIZING ////
    ////////////////////////
    /**
     * Returns the dimension associated with a socket.  If a socket dimension has not yet
     * been set, this will return null.
     */
    public Dimension getSocketSpaceDimension(BlockConnector socket) {
        if (this.getConnectorTag(socket) == null) {
            return null;
        } else {
            return this.getConnectorTag(socket).getDimension();
        }
    }

    /**
     * Updates the socket socket space of the specified connectedSocket of this after a block
     * connection/disconnection.  The socket space specifies the dimensions of the block
     * with id connectedToBlockID. RenderableBlock will use these dimensions to
     * determine the appropriate bounds to stretch the connectedSocket by.
     * @param connectedSocket BlockConnector which block connection/disconnection occurred
     * @param connectedToBlockID the Long block ID of the block connected/disconnected to the specified connectedSocket
     * @param isConnected boolean flag to determine if a block connected or disconnected to the connectedSocket
     */
    private void updateSocketSpace(BlockConnector connectedSocket, long connectedToBlockID, boolean isConnected) {
        //System.out.println("updating socket space of :" + connectedSocket.getLabel() +" of rb: "+this);
        if (!isConnected) {
            //remove the mapping
            this.getConnectorTag(connectedSocket).setDimension(null);

        } else {
            //if no before block, then no recursion
            //if command connector with position type bottom (just a control connector socket)
            // and we have a before, then skip and recurse up
            if (getBlock().getBeforeBlockID() != Block.NULL
                    && BlockConnectorShape.isCommandConnector(connectedSocket)
                    && connectedSocket.getPositionType() == BlockConnector.PositionType.BOTTOM) {

                //get before connector
                Long beforeID = getBlock().getBeforeBlockID();
                BlockConnector beforeSocket = workspace.getEnv().getBlock(beforeID).getConnectorTo(getBlockID());
                workspace.getEnv().getRenderableBlock(beforeID).updateSocketSpace(beforeSocket, getBlockID(), true);
                return;
            }

            //add dimension to the mapping
            this.getConnectorTag(connectedSocket).setDimension(calcDimensionOfSocket(connectedSocket));
        }

        //reform shape with new socket dimension
        reformBlockShape();
        //next time, redraw with new positions and moving children blocks
        clearBufferedImage();

        //after everything on this block has been updated, recurse upward if possible
        BlockConnector plugEquiv = BlockLinkChecker.getPlugEquivalent(getBlock());
        if (plugEquiv != null && plugEquiv.hasBlock()) {
            Long plugID = plugEquiv.getBlockID();
            BlockConnector socketEquiv = workspace.getEnv().getBlock(plugID).getConnectorTo(getBlockID());
            //update the socket space of a connected before/parent block
            workspace.getEnv().getRenderableBlock(plugID).updateSocketSpace(socketEquiv, getBlockID(), true);
        }
    }

    /**
     * Calculates the dimensions at the specified socket
     * @param socket BlockConnector to calculate the dimension of
     * @return Dimension of the specified socket
     */
    private Dimension calcDimensionOfSocket(BlockConnector socket) {
        Dimension finalDimension = new Dimension(0, 0);
        long curBlockID = socket.getBlockID();
        while (curBlockID != Block.NULL) {
            Block curBlock = workspace.getEnv().getBlock(curBlockID);
            //System.out.println("evaluating block :" + curBlock.getBlockLabel());
            RenderableBlock curRenderableBlock = workspace.getEnv().getRenderableBlock(curBlockID);
            Dimension curRBSize = curRenderableBlock.getBlockSize();

            //add height
            finalDimension.height += curRBSize.height;
            //subtract after plug
            if (curBlock.hasAfterConnector()) {
                finalDimension.height -= BlockConnectorShape.CONTROL_PLUG_HEIGHT;
            }
            //set largest width by iterating through to sockets and getting
            //the max width ONLY if curBlockID == connectedToBlockID
            int width = curRBSize.width;

            if (curBlock.getNumSockets() > 0 && !curBlock.isInfix()) {
                int maxSocWidth = getMaxWidthOfSockets(curBlockID);
                //need to add the placeholder width within bottom sockets if maxSocWidth is zero
                if (maxSocWidth == 0) {
                    // Adjust for zoom
                    width += 2 * BlockShape.BOTTOM_SOCKET_SIDE_SPACER * curRenderableBlock.getZoom();
                }

                if (maxSocWidth > 0) {
                    //need to minus the data plug width, otherwise it is counted twice
                    maxSocWidth -= BlockConnectorShape.NORMAL_DATA_PLUG_WIDTH;

                    // Adjust for zoom
                    width += maxSocWidth * curRenderableBlock.getZoom();
                }
            }

            if (width > finalDimension.width) {
                finalDimension.width = width;
            }

            //move down the afters
            curBlockID = workspace.getEnv().getBlock(curBlockID).getAfterBlockID();
        }
        return finalDimension;
    }

    /**
     * Redraws this RenderableBlock along with the RenderableBlocks
     * after it, which include after and socket blocks.  In other words,
     * this method redraws the stack of blocks that begin with this.
     * NOTE: this is inefficient, should only use this if needed
     * NOTE: Must call this after loading of blocks to update the socket
     * dimensions of this and set the isLoading flag to false
     */
    public void redrawFromTop() {
        if (GraphicsEnvironment.isHeadless()) {
            return;
        }

        isLoading = false;
        for (BlockConnector socket : BlockLinkChecker.getSocketEquivalents(getBlock())) {

            if (socket.hasBlock()) {
                //loop through all the afters of the connected block
                long curBlockID = socket.getBlockID();
                // TODO: this is a patch, but we need to fix the root of the problem!
                if (workspace.getEnv().getRenderableBlock(curBlockID) == null) {
                    System.out.println("does not exist yet, block: " + curBlockID);
                    continue;
                }

                workspace.getEnv().getRenderableBlock(curBlockID).redrawFromTop();

                //add dimension to the mapping
                this.getConnectorTag(socket).setDimension(calcDimensionOfSocket(socket));
            } else {
                this.getConnectorTag(socket).setDimension(null);
            }
        }

        //reform shape with new socket dimension
        reformBlockShape();
        //next time, redraw with new positions and moving children blocks
        clearBufferedImage();
    }

    /**
     * Helper method for updateSocketSpace and calcStackDim.
     * Returns the maximum width of the specified blockID's socket blocks
     * @param blockID the Long blockID of the desired block
     */
    public int getMaxWidthOfSockets(Long blockID) {
        int width = 0;
        Block block = workspace.getEnv().getBlock(blockID);
        RenderableBlock rb = workspace.getEnv().getRenderableBlock(blockID);

        for (BlockConnector socket : block.getSockets()) {
            Dimension socketDim = rb.getSocketSpaceDimension(socket);
            if (socketDim != null) {
                if (socketDim.width > width) {
                    width = socketDim.width;
                }
            }
        }

        return width;
    }

    /////////////////////
    //BLOCK CONNECTION //
    /////////////////////
    /**
     * Notifies this renderable block that ITS socket connectedSocket was connected to
     * ANOTHER block with ID connectedBlockID.
     */
    public void blockConnected(BlockConnector connectedSocket, long connectedBlockID) {
        //notify block first so that we will only need to repaint this block once
        getBlock().blockConnected(connectedSocket, connectedBlockID);

        //synchronize sockets
        synchronizeSockets();

        // make sure the connected block is positioned correctly
        moveConnectedBlocks();

        updateSocketSpace(connectedSocket, connectedBlockID, true);
    }

    /**
     * Notifies this renderable block that its socket connectedSocket had a block
     * disconnected from it.
     */
    public void blockDisconnected(BlockConnector disconnectedSocket) {
        //notify block first so that we will only need to repaint this block once
        getBlock().blockDisconnected(disconnectedSocket);

        updateSocketSpace(disconnectedSocket, Block.NULL, false);

        //synchronize sockets
        synchronizeSockets();
    }

    ///////////////////
    //BLOCK RENDERING//
    ///////////////////
    /**
     * Clears the BufferedImage of this
     */
    public void clearBufferedImage() {
        GraphicsManager.recycleGCCompatibleImage(buffImg);
        buffImg = null;
    }

    /**
     * Clears the BufferedImage of this and repaint this entirely
     */
    public void repaintBlock() {
        if (GraphicsEnvironment.isHeadless()) {
            return;
        }

        clearBufferedImage();

        if (this.isVisible()) {
            //NOTE: If it's not visible, this will throw an exception.
            //as during the redraw, it will try to access location information
            //of this
            repaint();
            highlighter.repaint();
        }
    }

    /**
     * Swing paint method for J-Component
     * Checks to see if the buffer has been cleared (or yet to be created),
     * if so then it redraws the buffer and then draws the image on the graphics2d or
     * else it uses the previous buffer.
     */
    @Override
    public void paintComponent(Graphics g) {
        Graphics2D g2 = (Graphics2D) g;
        if (!isLoading) {
            // if buffImg is null, redraw block shape
            if (buffImg == null) {
                updateBuffImg();//this method also moves connected blocks
            }
            if (dragging) {
                g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, DRAGGING_ALPHA));
                g2.drawImage(buffImg, 0, 0, null);
                g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 1));
            } else {
                g2.drawImage(buffImg, 0, 0, null);
            }
        }
    }

    /**
     * Reforms the blockShape of this renderableBlock and saves it into the blockArea while
     * updating the bounds of this RenderableBlock.  Used to update the shape and socket
     * positions while avoiding a full updateBuffImg.
     */
    private void reformBlockShape() {
        abstractBlockArea = blockShape.reformArea();
        //TODO for zooming, create an AffineTransform to scale the block shape
        AffineTransform at = new AffineTransform();
        at.setToScale(zoom, zoom);
        blockArea = abstractBlockArea.createTransformedArea(at);

        //note: need to add twice the highlight stroke width so that the highlight does not get cut off
        Rectangle updatedDimensionRect = new Rectangle(
                this.getX(),
                this.getY(),
                blockArea.getBounds().width,
                blockArea.getBounds().height);
        if (!this.getBounds().equals(updatedDimensionRect)) {
            moveConnectedBlocks(); // bounds have changed, so move connected blocks
        }
        this.setBounds(updatedDimensionRect);

        //////////////////////////////////////////
        //set position of block labels.
        //////////////////////////////////////////
        if (pageLabel != null && getBlock().hasPageLabel()) {
            pageLabel.update();
        }
        if (blockLabel != null) {
            blockLabel.update();
        }
        if (collapseLabel != null) {
            collapseLabel.update();
        }
        if (comment != null) {
            comment.update();
        }
        for (ConnectorTag tag : socketTags) {
            BlockConnector socket = tag.getSocket();
            SocketLabel label = tag.getLabel();
            if (label == null || SocketLabel.ignoreSocket(socket)) {
                continue;
            }
            label.update(getSocketAbstractPoint(socket));
        }
    }

    /**
     * returns the Area of the block
     * @return
     */
    public Area getBlockArea() {
        return blockArea;
    }

    /**
     * Redraws the entire buffer on a Graphics2D, called by paintCompnent() only
     * if the buffer has been cleared.
     */
    private void updateBuffImg() {
        if (GraphicsEnvironment.isHeadless()) {
            return;
        }

        //if label text has changed, then resync labels/sockets and reform shape
        if (!synchronizeLabelsAndSockets()) {
            reformBlockShape();//if updateLabels is true, we don't need to reform AGAIN
        }

        //create image
        //note: need to add twice the highlight stroke width so that the highlight does not get cut off
        GraphicsManager.recycleGCCompatibleImage(buffImg);
        buffImg = GraphicsManager.getGCCompatibleImage(
                blockArea.getBounds().width,
                blockArea.getBounds().height);
        Graphics2D buffImgG2 = (Graphics2D) buffImg.getGraphics();

        //update bounds of this renderableBlock as bounds of the shape
        Dimension updatedDimensionRect = new Dimension(blockArea.getBounds().getSize());

        //get size of block to determine size needed for bevel image
        Image bevelImage = BlockShapeUtil.getBevelImage(
                updatedDimensionRect.width, updatedDimensionRect.height, blockArea);

        //need antialiasing to remove color fill artifacts outside the bevel
        buffImgG2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);

        //ADD BLOCK COLOR
        Color blockColor = this.getBLockColor();
        buffImgG2.setColor(blockColor);
        buffImgG2.fill(blockArea);

        //draw the bevel on the shape -- comment this line to not apply beveling
        buffImgG2.drawImage(bevelImage, 0, 0, null);

        //DRAW BLOCK IMAGES
        repositionBlockImages(blockArea.getBounds().width, blockArea.getBounds().height);
    }

    /**
     * Draws the BlockImageIcon instances of this onto itself
     * @param width the current width of the buffered image
     * @param height the current height of the buffered image
     */
    private void repositionBlockImages(int width, int height) {
        int margin = 5;

        //TODO need to take other images into acct if we enable multiple block images
        for (BlockImageIcon img : imageMap.values()) {
            ImageIcon icon = img.getImageIcon();
            Point imgLoc = new Point(0, 0);
            if (img.getImageLocation() == BlockImageIcon.ImageLocation.CENTER) {
                imgLoc.setLocation((width - icon.getIconWidth()) / 2, (height - icon.getIconHeight()) / 2);
            } else if (img.getImageLocation() == ImageLocation.NORTH) {
                imgLoc.setLocation((width - icon.getIconWidth()) / 2, margin);
            } else if (img.getImageLocation() == ImageLocation.SOUTH) {
                imgLoc.setLocation((width - icon.getIconWidth()) / 2, height - margin - icon.getIconHeight());
            } else if (img.getImageLocation() == ImageLocation.EAST) {
                imgLoc.setLocation(width - margin - icon.getIconWidth(), (height - icon.getIconHeight()) / 2);
            } else if (img.getImageLocation() == ImageLocation.WEST) {
                imgLoc.setLocation(margin, (height - icon.getIconHeight()) / 2);
            } else if (img.getImageLocation() == ImageLocation.NORTHEAST) {
                imgLoc.setLocation(width - margin - icon.getIconWidth(), margin);
            } else if (img.getImageLocation() == ImageLocation.NORTHWEST) {
                imgLoc.setLocation(margin, margin);
            } else if (img.getImageLocation() == ImageLocation.SOUTHEAST) {
                imgLoc.setLocation(width - margin - icon.getIconWidth(), height - margin - icon.getIconHeight());
            } else if (img.getImageLocation() == BlockImageIcon.ImageLocation.SOUTHWEST) {
                //put in southwest corner
                imgLoc.setLocation(margin, height - (icon.getIconHeight() + margin));
            }


            if (getBlock().hasPlug() && (img.getImageLocation() != ImageLocation.EAST
                    || img.getImageLocation() != ImageLocation.NORTHEAST
                    || img.getImageLocation() != ImageLocation.SOUTHEAST));
            imgLoc.x += 4; // need to nudge it a little more because of plug
            img.setLocation(imgLoc.x, imgLoc.y);
        }
    }

    /**
     * Sets the highlight color of this block.
     * The specified highlight may be overrided if this block has focus,
     * is a search result, or is "bad".  However when those states are no
     * longer active, the color is set back to the specified hlColor, if
     * resetHightlight() was not called in the meantime.
     * @param color the desired highlight Color
     */
    public void setBlockHighlightColor(Color color) {
        highlighter.setHighlightColor(color);
    }

    /**
     * Hides highlighting for this block.
     */
    public void resetHighlight() {
        highlighter.resetHighlight();
    }

    /**
     * Tells this RenderableBlock to move its highlight handler to a new parent
     * (should be called after this RB is moved to a new parent)
     * @param parent the RBParent that is the RB's new parent
     */
    public void setHighlightParent(RBParent parent) {
        highlighter.setParent(parent);
    }

    /**
     * Overridden from JComponent.
     * Returns true iff it has a parent, its parent is visible, and itself is visible; false otherwise.
     * @return true iff it has a parent, its parent is visible, and itself is visible; false otherwise.
     */
    public boolean isVisible() {
        return super.isVisible() && getParent() != null && getParent().isVisible();
    }

    ////////////////////////
    // COMMENT MANAGEMENT //
    ////////////////////////
    /**
     * @return true iff this.comment !=null
     */
    public boolean hasComment() {
        if (comment != null) {
            return true;
        }
        return false;
    }

    /**
     * @return this.comment
     */
    public Comment getComment() {
        return comment;
    }

    /**
     * Sets this RenderableBlock's comment
     * @param comment
     */
    public void setComment(Comment comment) {
        this.comment = comment;
    }

    /**
     * If this does NOT have comment, then add a new comment to
     * this parent Container at a point (10,-20) away from upper-right
     * hand corner.  Modify such that this.hasComment will now return true.
     */
    public void addComment() {
        if (hasComment()) {
            //a renderable block may only have ONE comment
        } else {
            int x = this.getX() + this.getWidth() + 30;
            int y = this.getY() - 40;
            comment = new Comment(workspace, "", this, this.getBlock().getColor(), zoom);
            if (this.getParentWidget() != null) {
                comment.setParent(this.getParentWidget().getJComponent());
            } else {
                comment.setParent(this.getParent());
            }
            comment.setLocation(x, y);
            commentLabelChanged = true;
        }

        revalidate();
        getHighlightHandler().revalidate();
        updateBuffImg();
        comment.getArrow().updateArrow();
        getParent().repaint();
    }

    /**
     * remove this comment from this parent Container and modify such that
     * this.hasComment returns false.
     */
    public void removeComment() {
        if (hasComment()) {
            comment.delete();
            comment = null;
            commentLabelChanged = true;
            reformBlockShape();
            revalidate();
            getHighlightHandler().revalidate();
            updateBuffImg();
            getParent().repaint();
        }
    }

    /**
     * returns where the CommentArrow should draw from
     * @return
     */
    public Point getCommentLocation() {
        Point location = this.getLocation();

        if (comment != null) {
            CommentLabel commentLabel = comment.getCommentLabel();
            if (commentLabel != null) {
                location.translate(commentLabel.getX() - 2, commentLabel.getY() - 2);
                location.translate(commentLabel.getWidth() / 2, commentLabel.getHeight() / 2);
            }
        }

        return location;
    }

    //////////////////////////////////
    // MOVEMENT OF CONNECTED BLOCKS //
    //////////////////////////////////
    /**
     * Aligns all RenderableBlocks plugged into this one with the current location of this RenderableBlock.
     * These RenderableBlocks to move include blocks connected at sockets and the after connector.
     */
    public void moveConnectedBlocks() {
        if (DEBUG) {
            System.out.println("move connected blocks of this: " + this);
        }

        // if this hasn't been added anywhere, asking its location will break stuff
        if (getParent() == null) {
            return;
        }

        Block b = workspace.getEnv().getBlock(blockID);
        Point socketLocation;
        Point plugLocation;
        RenderableBlock rb;
        Point myScreenOffset = getLocation();
        Point otherScreenOffset;
        for (BlockConnector socket : BlockLinkChecker.getSocketEquivalents(b)) {
            socketLocation = getSocketPixelPoint(socket);
            if (socket.hasBlock()) {
                rb = workspace.getEnv().getRenderableBlock(socket.getBlockID());

                // TODO: djwendel - this is a patch, but the root of the problem
                // needs to be found and fixed!!
                if (rb == null) {
                    System.out.println("Block doesn't exist yet: " + socket.getBlockID());
                    continue;
                }

                plugLocation = rb.getSocketPixelPoint(BlockLinkChecker.getPlugEquivalent(workspace.getEnv().getBlock(socket.getBlockID())));
                otherScreenOffset = SwingUtilities.convertPoint(rb.getParent(), rb.getLocation(), getParent());
                otherScreenOffset.translate(-rb.getX(), -rb.getY());
                rb.setLocation((int) Math.round((float) myScreenOffset.getX() + socketLocation.getX() - (float) otherScreenOffset.getX() - plugLocation.getX()),
                        (int) Math.round((float) myScreenOffset.getY() + socketLocation.getY() - (float) otherScreenOffset.getY() - plugLocation.getY()));

                rb.moveConnectedBlocks();
            }
        }
    }

    private void startDragging(RenderableBlock renderable, WorkspaceWidget widget) {
        renderable.pickedUp = true;
        renderable.lastDragWidget = widget;
        if (renderable.hasComment()) {
            renderable.comment.setConstrainComment(false);
        }
        Component oldParent = renderable.getParent();
        Workspace workspace = renderable.getWorkspace();
        workspace.addToBlockLayer(renderable);
        renderable.setLocation(SwingUtilities.convertPoint(oldParent, renderable.getLocation(), workspace));
        renderable.setHighlightParent(workspace);
        for (BlockConnector socket : BlockLinkChecker.getSocketEquivalents(workspace.getEnv().getBlock(renderable.blockID))) {
            if (socket.hasBlock()) {
                startDragging(workspace.getEnv().getRenderableBlock(socket.getBlockID()), widget);
            }
        }
    }

    /**
     * This method is called when this RenderableBlock is plugged into another RenderableBlock that has finished dragging.
     * @param widget the WorkspaceWidget where this RenderableBlock is being dropped.
     */
    public static void stopDragging(RenderableBlock renderable, WorkspaceWidget widget) {
        if (!renderable.dragging) {
            throw new RuntimeException("dropping without prior dragging?");
        }
        //notify children
        for (BlockConnector socket : BlockLinkChecker.getSocketEquivalents(renderable.getBlock())) {
            if (socket.hasBlock()) {
                stopDragging(renderable.getWorkspace().getEnv().getRenderableBlock(socket.getBlockID()), widget);
            }
        }
        // drop this block on its widget (if w is null it'll throw an exception)
        widget.blockDropped(renderable);
        // stop rendering as transparent
        renderable.dragging = false;
        //move comment
        if (renderable.hasComment()) {
            if (renderable.getParentWidget() != null) {
                renderable.comment.setParent(renderable.getParentWidget().getJComponent(), 0);
            } else {
                renderable.comment.setParent(null, renderable.getBounds());
            }

            renderable.comment.setConstrainComment(true);
            renderable.comment.setLocation(renderable.comment.getLocation());
            renderable.comment.getArrow().updateArrow();
        }
    }

    private void drag(RenderableBlock renderable, int dx, int dy, WorkspaceWidget widget, boolean isTopLevelBlock) {
        if (!renderable.pickedUp) {
            throw new RuntimeException("dragging without prior pickup");
        }
        //mark this as being dragged
        renderable.dragging = true;
        // move the block by drag amount
        if (!isTopLevelBlock) {
            renderable.setLocation(renderable.getX() + dx, renderable.getY() + dy);
        }
        // send blockEntered/blockExited/blogDragged as appropriate
        if (widget != null) {
            if (!widget.equals(renderable.lastDragWidget)) {
                widget.blockEntered(renderable);
                if (renderable.lastDragWidget != null) {
                    renderable.lastDragWidget.blockExited(renderable);
                }
            }
            widget.blockDragged(renderable);
            renderable.lastDragWidget = widget;
        }

        // translate highlight along with the block - this would happen automatically,
        // but putting the call here takes out any lag.
        renderable.highlighter.repaint();
        // Propagate the drag event to anything plugged into this block
        for (BlockConnector socket : BlockLinkChecker.getSocketEquivalents(renderable.getBlock())) {
            if (socket.hasBlock()) {
                drag(workspace.getEnv().getRenderableBlock(socket.getBlockID()), dx, dy, widget, false);
            }
        }
    }

    ///////////////////
    //MOUSE EVENTS   //
    ///////////////////
    /**
     * Makes public the protected processMouseEvent() method from Component so that the children within this block
     * may pass mouse events to this
     */
    public void processMouseEvent(MouseEvent e) {
        super.processMouseEvent(e);
    }

    public void mouseReleased(MouseEvent e) {
        if (SwingUtilities.isLeftMouseButton(e)) {
            if (pickedUp) {
                dragHandler.mouseReleased(e);

                //if the block was dragged before...then
                if (dragging) {
                    BlockLink link = getNearbyLink(); //look for nearby link opportunities
                    WorkspaceWidget widget = null;

                    // if a suitable link wasn't found, just drop the block
                    if (link == null) {
                        widget = lastDragWidget;
                        stopDragging(this, widget);
                    } // otherwise, if a link WAS found...
                    else {

                        /* Make sure that no matter who's connecting to whom, the block
                        * that's being dragged gets dropped on the parent widget of the
                        * block that's already on the canvas.
                        */
                        if (blockID.equals(link.getSocketBlockID())) {
                            // dragged block is the socket block, so take plug's parent.
                            widget = workspace.getEnv().getRenderableBlock(link.getPlugBlockID()).getParentWidget();
                        } else {
                            // dragged block is the plug block, so take the socket block's parent.
                            widget = workspace.getEnv().getRenderableBlock(link.getSocketBlockID()).getParentWidget();
                        }

                        // drop the block and connect its link
                        stopDragging(this, widget);
                        link.connect();
                        workspace.notifyListeners(new WorkspaceEvent(workspace, widget, link, WorkspaceEvent.BLOCKS_CONNECTED));
                        workspace.getEnv().getRenderableBlock(link.getSocketBlockID()).moveConnectedBlocks();
                    }

                    //set the locations for X and Y based on zoom at 1.0
                    this.unzoomedX = this.calculateUnzoomedX(this.getX());
                    this.unzoomedY = this.calculateUnzoomedY(this.getY());

                    workspace.notifyListeners(new WorkspaceEvent(workspace, widget, link, WorkspaceEvent.BLOCK_MOVED, true));
                    if (widget instanceof MiniMap) {
                        workspace.getMiniMap().animateAutoCenter(this);
                    }
                }
            }           
        }
        pickedUp = false;
        if (e.isPopupTrigger() || SwingUtilities.isRightMouseButton(e) || e.isControlDown()) {
            //add context menu at right click location to provide functionality
            //for adding new comments and removing comments
            PopupMenu popup = ContextMenu.getContextMenuFor(this);
            add(popup);
            popup.show(this, e.getX(), e.getY());
        }
        workspace.getMiniMap().repaint();
    }

    public void mouseDragged(MouseEvent e) {
        if (SwingUtilities.isLeftMouseButton(e)) {
            if (pickedUp) {
                Point pp = SwingUtilities.convertPoint(this, e.getPoint(), workspace.getMiniMap());
                if (workspace.getMiniMap().contains(pp)) {
                    workspace.getMiniMap().blockDragged(this, e.getPoint());
                    lastDragWidget = workspace.getMiniMap();
                    return;
                }

                // drag this block if appropriate (checks bounds first)
                dragHandler.mouseDragged(e);

                // Find the widget under the mouse
                dragHandler.myLoc.move(getX() + dragHandler.mPressedX, getY() + dragHandler.mPressedY);
                Point p = SwingUtilities.convertPoint(this.getParent(), dragHandler.myLoc, workspace);
                WorkspaceWidget widget = workspace.getWidgetAt(p);
                if (widget == null) {
                    // not on a workspace widget, cancel dragging
                    return;
                }

                //if this is the first call to mouseDragged
                if (!dragging) {
                    Block block = getBlock();
                    BlockConnector plug = BlockLinkChecker.getPlugEquivalent(block);
                    if (plug != null && plug.hasBlock()) {
                        Block parent = workspace.getEnv().getBlock(plug.getBlockID());
                        BlockConnector socket = parent.getConnectorTo(blockID);
                        BlockLink link = BlockLink.getBlockLink(workspace, block, parent, plug, socket);
                        link.disconnect();
                        //socket is removed internally from block's socket list if socket is expandable
                        workspace.getEnv().getRenderableBlock(parent.getBlockID()).blockDisconnected(socket);

                        //NOTIFY WORKSPACE LISTENERS OF DISCONNECTION
                        workspace.notifyListeners(new WorkspaceEvent(workspace, widget, link, WorkspaceEvent.BLOCKS_DISCONNECTED));
                    }
                    startDragging(this, widget);
                }

                // drag this block and all attached to it
                drag(this, dragHandler.dragDX, dragHandler.dragDY, widget, true);

                workspace.getMiniMap().repaint();
            }           
        }
    }
    //show the pulldown icon if hasComboPopup = true

    public void mouseEntered(MouseEvent e) {
        dragHandler.mouseEntered(e);
        //!dragging: don't redraw while dragging
        //!SwingUtilities.isLeftMouseButton: dragging mouse moves into another block because of delay
        //!popupIconVisible: only update if there is a change
        //getBlock().hasSiblings(): only deal with blocks with siblings
        if (!SwingUtilities.isLeftMouseButton(e) && !dragging && getBlock().hasSiblings()) {
            blockLabel.showMenuIcon(true);
        }
    }

    public void mouseExited(MouseEvent e) {
        dragHandler.mouseExited(e);
        //!dragging: don't redraw while dragging
        //!SwingUtilities.isLeftMouseButton: dragging mouse moves into another block because of delay
        //popupIconVisible: only update if there is a change
        //getBlock().hasSiblings(): only deal with blocks with siblings
        if (!SwingUtilities.isLeftMouseButton(e) && !dragging && !blockArea.contains(e.getPoint())) {
            blockLabel.showMenuIcon(false);
        }
    }

    public void mouseMoved(MouseEvent e) {
    }

    public void mouseClicked(MouseEvent e) {
        if (SwingUtilities.isLeftMouseButton(e)) {
            dragHandler.mouseClicked(e);
            if (e.getClickCount() == 2 && !dragging) {
                workspace.notifyListeners(new WorkspaceEvent(workspace, this.getParentWidget(), this.getBlockID(), WorkspaceEvent.BLOCK_STACK_COMPILED));
            }
        }
    }

    public void mousePressed(MouseEvent e) {
        if (SwingUtilities.isLeftMouseButton(e)) {
            dragHandler.mousePressed(e);
            pickedUp = true; //mark this block as currently being picked up
        }
    }

    ////////////////
    //SEARCHABLE ELEMENT
    ////////////////
    public String getKeyword() {
        return getBlock().getBlockLabel();
    }

    public String getGenus() {
        return getBlock().getGenusName();
    }

    public void updateInSearchResults(boolean inSearchResults) {
        isSearchResult = inSearchResults;
        highlighter.setIsSearchResult(isSearchResult);
        //repaintBlock();
    }

    public boolean isSearchResult() {
        return isSearchResult;
    }

    ////////////////
    //SAVING AND LOADING
    ////////////////
    /**
     * Returns the node of this
     * @return the node of this
     */
    public Node getSaveNode(Document document) {
      // XXX seems strange that comment is kept here but saved in the block
      return getBlock().getSaveNode(document, descale(this.getX()), descale(this.getY()),
                    comment != null ? comment.getSaveNode(document) : null, isCollapsed());
    }

    /**
     * Returns whether or not this is still loading data.
     * @return whether or not this is still loading data.
     */
    public boolean isLoading() {
        return isLoading;
    }

    /**
     * Loads a RenderableBlock and its related Block instance from the specified blockNode;
     * returns null if no RenderableBlock was loaded.
     * @param workspace The workspace to use
     * @param blockNode Node containing information to load into a RenderableBlock instance
     * @param parent WorkspaceWidget to contain the block to load
     * @return RenderableBlock instance holding the information in blockNode; null if no RenderableBlock loaded
     */
    public static RenderableBlock loadBlockNode(Workspace workspace, Node blockNode, WorkspaceWidget parent, HashMap<Long, Long> idMapping) {
        boolean isBlock = blockNode.getNodeName().equals("Block");
        boolean isBlockStub = blockNode.getNodeName().equals("BlockStub");

        if (isBlock || isBlockStub) {
            RenderableBlock rb = new RenderableBlock(workspace, parent, Block.loadBlockFrom(workspace, blockNode, idMapping).getBlockID(), true);

            if (isBlockStub) {
                //need to get actual block node
                NodeList stubchildren = blockNode.getChildNodes();
                for (int j = 0; j < stubchildren.getLength(); j++) {
                    Node node = stubchildren.item(j);
                    if (node.getNodeName().equals("Block")) {
                        blockNode = node;
                        break;
                    }
                }
            }


            if (rb.getBlock().labelMustBeUnique()) {
                //TODO check the instance number of this block
                //and update instance checker
            }

            Point blockLoc = new Point(0, 0);
            NodeList children = blockNode.getChildNodes();
            Node child;

            for (int i = 0; i < children.getLength(); i++) {
                child = children.item(i);
                if (child.getNodeName().equals("Location")) {
                    //extract location information
                    extractLocationInfo(child, blockLoc);
                } else if (child.getNodeName().equals("Comment")) {
                    rb.comment = Comment.loadComment(workspace, child.getChildNodes(), rb);
                    if (rb.comment != null) {
                        rb.comment.setParent(rb.getParentWidget().getJComponent());
                    }
                } else if (child.getNodeName().equals("Collapsed")) {
                    rb.setCollapsed(true);
                }
            }
            //set location from info
            rb.setLocation(blockLoc.x, blockLoc.y);

            if (rb.comment != null) {
                rb.comment.getArrow().updateArrow();
            }


            return rb;
        }
        return null;
    }

    /**
     * Read Location Node change loc to location in Node
     * @param location
     * @param loc
     */
    public static void extractLocationInfo(Node location, Point loc) {
        NodeList coordinates = location.getChildNodes();
        Node coor;
        for (int j = 0; j < coordinates.getLength(); j++) {
            coor = coordinates.item(j);
            if (coor.getNodeName().equals("X")) {
                loc.x = Integer.parseInt(coor.getTextContent());
            } else if (coor.getNodeName().equals("Y")) {
                loc.y = Integer.parseInt(coor.getTextContent());
            }
        }
    }

    /**
     * Changes Point boxSize (x,y) to the (width,height) of boxSizeNode
     * That is x = width and y = height
     * @param boxSizeNode
     * @param boxSize
     */
    public static void extractBoxSizeInfo(Node boxSizeNode, Dimension boxSize) {
        NodeList coordinates = boxSizeNode.getChildNodes();
        Node coor;
        for (int j = 0; j < coordinates.getLength(); j++) {
            coor = coordinates.item(j);
            if (coor.getNodeName().equals("Width")) {
                boxSize.width = Integer.parseInt(coor.getTextContent());
            } else if (coor.getNodeName().equals("Height")) {
                boxSize.height = Integer.parseInt(coor.getTextContent());
            }
        }
    }

    public String toString() {
        StringBuffer buf = new StringBuffer();
        buf.append("RenderableBlock " + getBlockID() + ": " + getBlock().getBlockLabel());
        return buf.toString();
    }

    /***********************************
     * State Saving Stuff for Undo/Redo *
     ***********************************/
    private class RenderableBlockState {

        public int x;
        public int y;
    }

    public Object getState() {
        RenderableBlockState blockState = new RenderableBlockState();
        blockState.x = getX();
        blockState.y = getY();
        return blockState;
    }

    public void loadState(Object memento) {
        assert (memento instanceof RenderableBlockState) : "ISupportMemento contract violated in RenderableBlock";
        if (memento instanceof RenderableBlockState) {
            RenderableBlockState state = (RenderableBlockState) memento;
            this.setLocation(state.x, state.y);
        }
    }
    /***************************************
     * Zoom support methods
     ***************************************/
    private double zoom = 1.0;

    public void setZoomLevel(double newZoom) {
        //create zoom transformers
        this.zoom = newZoom;

        //rescale internal components
        if (pageLabel != null && getBlock().hasPageLabel()) {
            this.pageLabel.setZoomLevel(newZoom);
        }
        if (blockLabel != null) {
            this.blockLabel.setZoomLevel(newZoom);
        }
        if (collapseLabel != null) {
            collapseLabel.setZoomLevel(newZoom);
        }
        this.plugTag.setZoomLevel(newZoom);
        this.afterTag.setZoomLevel(newZoom);
        this.beforeTag.setZoomLevel(newZoom);
        for (ConnectorTag tag : socketTags) {
            tag.setZoomLevel(newZoom);
        }
        if (this.hasComment()) {
            this.comment.setZoomLevel(newZoom);
        }
    }

    /**
     * the current zoom for this RenderableBlock
     * @return the zoom
     */
    public double getZoom() {
        return zoom;
    }

    /**
     * returns a new int  x based on the current zoom
     * @param x
     * @return
     */
    int rescale(int x) {
        return (int) (x * zoom);
    }

    /**
     * returns a new double x position based on the current zoom
     * @param x
     * @return
     */
    int rescale(double x) {
        return (int) (x * zoom);
    }

    /**
     * returns the descaled x based on the current zoom
     * that is given a scaled x it returns what that position would be when zoom == 1
     * @param x
     * @return
     */
    private int descale(int x) {
        return (int) (x / zoom);
    }

    /**
     * returns the descaled x based on the current zoom
     * that is given a scaled x it returns what that position would be when zoom == 1
     * @param x
     * @return
     */
    private int descale(double x) {
        return (int) (x / zoom);
    }

    /**
     * calculates the x when the zoom is 1.0
     * @param x of the current position
     * @return the x when the zoom is 1.0
     */
    public int calculateUnzoomedX(int x) {
        return (int) (x / zoom);
    }

    /**
     * calculates the y when the zoom is 1.0
     * @param y of the current position
     * @return the y when the zoom is 1.0
     */
    public int calculateUnzoomedY(int y) {
        return (int) (y / zoom);
    }

    /**
     * mutator for the initial value of x
     * @param unzoomedX
     */
    public void setUnzoomedX(double unzoomedX) {
        this.unzoomedX = unzoomedX;
    }

    /**
     * mutator for the initial value of y
     * @param unzoomedY
     */
    public void setUnzoomedY(double unzoomedY) {
        this.unzoomedY = unzoomedY;
    }

    /**
     * observer for the initial value of x
     * @return initial value of x coordinate
     */
    public double getUnzoomedX() {
        return this.unzoomedX;
    }

    /**
     * observer for the initial value of y
     * @return initial value of x coordinate
     */
    public double getUnzoomedY() {
        return this.unzoomedY;
    }

    public void processKeyPressed(KeyEvent e) {
        for (KeyListener l : this.getKeyListeners()) {
            l.keyPressed(e);
        }
    }

    /////////////////
    //Tool Tips
    /////////////////
    public JToolTip createToolTip() {
        return new CToolTip(new Color(255, 255, 225));
    }

    public void setBlockToolTip(String text) {
        this.setToolTipText(text);
        this.blockLabel.setToolTipText(text);
    }

    protected boolean processKeyBinding(KeyStroke ks, KeyEvent e,
            int condition, boolean pressed) {
        switch (e.getKeyCode()) {
            case KeyEvent.VK_UP:
                return false;
            case KeyEvent.VK_DOWN:
                return false;
            case KeyEvent.VK_LEFT:
                return false;
            case KeyEvent.VK_RIGHT:
                return false;
            case KeyEvent.VK_ENTER:
                return false;
            default:
                return super.processKeyBinding(ks, e, condition, pressed);
        }
    }

    private ConnectorTag getConnectorTag(BlockConnector socket) {
        if (socket == null) {
            throw new RuntimeException("Socket may not be null");
        }
        if (socket.equals(plugTag.getSocket())) {
            return plugTag;
        }
        if (socket.equals(afterTag.getSocket())) {
            return afterTag;
        }
        if (socket.equals(beforeTag.getSocket())) {
            return beforeTag;
        }
        for (ConnectorTag tag : this.socketTags) {
            if (socket.equals(tag.getSocket())) {
                return tag;
            }
        }
        return null;
    }

    /**
     * Returns the collapsed state if the block has a collapseLabel otherwise false.
     */
    public boolean isCollapsed() {
        if (collapseLabel != null) {
            return collapseLabel.isActive();
        }

        return false;
    }

    /**
     * If this block can be collapsed its collapse state will be set
     * @param collapse
     */
    public void setCollapsed(boolean collapse) {
        if (collapseLabel != null) {
            collapseLabel.setActive(collapse);
        }
    }

    /**
     * Sets the visibility of blocks connected to this block
     * according to the current collapse state.
     */
    public void updateCollapse() {
        if (collapseLabel != null) {
            collapseLabel.updateCollapse();
        }
    }

    /**
     * Returns the width of the collapseLabel for this block if there is one, 0 otherwise
     * @return
     */
    public int getCollapseLabelWidth() {
        if (collapseLabel != null) {
            return collapseLabel.getWidth();
        }

        return 0;
    }

    /**
     * returns the RBHighlightHandler for this RenderableBlock
     * @return the highlighter
     */
    RBHighlightHandler getHighlightHandler() {
        return highlighter;
    }

    /**
     * returns the larger width of the CollapseLabel or CommentLabel if they exist for this RenderableBlock
     * @return
     */
    int getControlLabelsWidth() {
        int x = 0;
        if (getComment() != null) {
            x += Math.max(getComment().getCommentLabelWidth(), getCollapseLabelWidth());
        } else {
            x += getCollapseLabelWidth();
        }
        return x;
    }
}
TOP

Related Classes of edu.mit.blocks.renderable.RenderableBlock$RenderableBlockState

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.