Package edu.mit.blocks.workspace

Source Code of edu.mit.blocks.workspace.Page

package edu.mit.blocks.workspace;

import java.awt.AlphaComposite;
import java.awt.Color;
import java.awt.Component;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.TreeSet;

import javax.swing.JComponent;
import javax.swing.JLayeredPane;
import javax.swing.JPanel;
import javax.swing.JToolTip;
import javax.swing.SwingUtilities;

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

import edu.mit.blocks.codeblocks.Block;
import edu.mit.blocks.codeblockutil.CToolTip;
import edu.mit.blocks.renderable.RenderableBlock;

/**
* A Page serves as both an abstract container of blocks
* and also a graphical panel that renders its collection
* of blocks.  Abstractly, a page has seven abstract fields:
* a color, a name, a font, a drawer, width, a height,
* and a set of blocks.  How it renders these abstract fields
* depends on the state of the page, including: zoom level,
* and minimumPixelWidth.
*
* A Page exists as a WorkspaceWidget, a SearchableContainer,
* ISupportMemento, an RBParent, a Zoomable object, and a JPanel.
* As a WorkspaceWidget, it can add, remove, blocks and manage
* block manipulations within itself.  As a searchableContainer,
* it can notify users that certain blocks have been queried.
* As an ISupportMomento, it can undo the current values of
* abstract fields.  As an RBParent, it can highlight blocks.
*
* Since a Page is both a Zoomable object and JPanel, Pages
* separate its abstract model and view by allowing clients
* to mutate its abstract fields directly.  But clients must
* remember to reform the pages in order to synchronize the
* data between the model and view.
*
* A page's abstract color is rendered the same no matter
* what state the page is in.  A page's abstract name is
* rendered thrice centered at every fourth of the page.
* The name is rendered with a size depending on the zoom
* level of that page (it tries to maintain a constant aspect
* ratio).  The drawer name is not rendered.  The width and
* height of the page is rendered differently depending on
* the zoom level and minimumPixelWidth.  Using the zoom level,
* it tries to maintain a constant aspect ratio but the
* absolute sizes varies with a bigger/smaller zoom level.
* the minimumPixelWidth limits the width from going below
* a certain size, no matter what the system tries to set
* the abstract width to be.  Finally the set of blocks are
* rendered directly onto the page with the same transformation
* as the ones imposed on the width and height of the page.
*
* As an implementation detail, a page tries to maintain a
* separation between its abstract states and its view.
* Clients of Pages should use reform*() methods to validate
* information between the abstract states and view.  Clients
* of Pages are warned against accessing Page.getJComponent(),
* as the method provides clients a way to unintentionally mutate
* an implementation specific detail of Pages.
*
* A Page implements ExplorerListener i.e. it listens for possible changes in
* an explorer that affects the display of the page. When an explorer event
* happens the page changes its display accordingly
*/
public class Page implements WorkspaceWidget, SearchableContainer, ISupportMemento {

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

    /** Width while in collapsed mode */
    private static final int COLLAPSED_WIDTH = 20;
    /** The smallest value that this.minimumPixelWidth/zoom can be */
    private static final int DEFAULT_MINUMUM_WIDTH = 100;
    /** The smallest value that this.minimumPixelHeight/zoom can be */
    private static final int DEFAULT_MINIMUM_HEIGHT = 100;
    /** The default abstract width */
    private static final int DEFAULT_ABSTRACT_WIDTH = 700;
    /** The default abstract height */
    public static final int DEFAULT_ABSTRACT_HEIGHT = 1600;
    /** An empty string */
    private static final String emptyString = "";
    /** this.zoomLevel: zoom level state */
    static double zoom = 1.0;
    /** The JComponent of this page */
    private final PageJComponent pageJComponent = new PageJComponent();
    /** The abstract width of this page */
    private double abstractWidth;
    /** The abstract height of this page */
    private double abstractHeight;
    /** The name of the drawer that this page refers to */
    private String pageDrawer;
    /** The default page color.  OVERRIDED BY BLOCK CANVAS */
    private final Color defaultColor;
    /** MouseIn Flag: true if and only if the mouse is in this page */
    private boolean mouseIsInPage = false;
    /** The minimum width of the page in pixels */
    private int minimumPixelWidth = 0;
    /** The minimum height of the page in pixels */
    private int minimumPixelHeight = 0;
    /** Fullview */
    private boolean fullview;
    /** The GUI component for interfacing with the user
     * to help the user collapse or restore the page */
    private CollapseButton collapse;
    /** The user-time unique id of this page. Once set, cannot be changed. */
    private String pageId = null;
    /** Toggles to show/hide minimize page button. */
    private boolean hideMinimize = false;
    //////////////////////////////
    //Constructor/ Destructor  //
    //////////////////////////////

    /**
     * Constructs a new Page
     *
     * @param name - name of this page (this.name)
     * @param pageWidth - the abstract width of this page (this.width)
     * @param pageHeight - the abstract height of this page (this.height)
     * @param pageDrawer - the name of the page drawer that this page refers to
     *
     * @requires name != null && pageDrawer != null
     * @effects constructs a new Page such that:
     *       1) The name of this page equals the argument "name".
     *       2) The abstract width of this page equals "pageWidth".
     *          If "pageWidth is <= to zero, then set the
     *          width to the DEFAULT_ABSTRACT_WIDTH.
     *       3) The abstract height of this page equals DEFAULT_ABSTRACT_HEIGHT.
     *       4) The drawer name equals pageDrawer if and only if Workspace.everyPageHasDrawer==true.
     *       5) The color of this page is null.
     *       6) The font of this page is "Default", PLAIN, and 12.
     *       7) The set of blocks is empty.
     */
    public Page(Workspace workspace, String name, int pageWidth, int pageHeight, String pageDrawer) {
        this(workspace, name, pageWidth, pageHeight, pageDrawer, true, null, true);
    }

    public Page(Workspace workspace, String name, int pageWidth, int pageHeight, String pageDrawer, boolean inFullview, Color defaultColor, boolean isCollapsible) {
        super();
        this.workspace = workspace;
        this.defaultColor = defaultColor;
        this.pageJComponent.setLayout(null);
        this.pageJComponent.setName(name);
        this.abstractWidth = pageWidth > 0 ? pageWidth : Page.DEFAULT_ABSTRACT_WIDTH;
        this.abstractHeight = Page.DEFAULT_ABSTRACT_HEIGHT;
        if (pageDrawer != null) {
            this.pageDrawer = pageDrawer;
        } else if (Workspace.everyPageHasDrawer) {
            this.pageDrawer = name;
        }
        this.pageJComponent.setOpaque(true);

        this.fullview = inFullview;
        this.collapse = new CollapseButton(inFullview, name);
        if (isCollapsible) {
            this.pageJComponent.add(collapse);
        }
        this.pageJComponent.setFullView(inFullview);
    }

    public void disableMinimize() {
        this.hideMinimize = true;
        this.collapse.repaint();
    }

    public void enableMinimize() {
        this.hideMinimize = false;
        this.collapse.repaint();
    }

    public void setHide(boolean hide) {
        this.hideMinimize = hide;
        this.collapse.repaint();
    }

    /**
     * Constructs a new Page
     *
     * @param workspace The workspace in use
     * @param name - name of this page (this.name)
     *
     * @requires name != null
     * @effects constructs a new Page such that:
     *       1) The name of this page equals the argument "name".
     *       2) The abstract width of this page equals DEFAULT_ABSTRACT_WIDTH.
     *       3) The abstract height of this page equals DEFAULT_ABSTRACT_HEIGHT.
     *       4) The drawer name equals "name"
     *       5) The color of this page is null.
     *       6) The font of this page is "Default", PLAIN, and 12.
     *       7) The set of blocks is empty.
     */
    public Page(Workspace workspace, String name) {
        this(workspace, name, -1, -1, name);
    }

    /**
     * Constructs a new Page
     *
     * @param workspace The workspace in use
     * @requires none
     * @effects constructs a new Page such that:
     *       1) The name of this page equals the argument "".
     *       2) The abstract width of this page equals DEFAULT_ABSTRACT_WIDTH.
     *       3) The abstract height of this page equals DEFAULT_ABSTRACT_HEIGHT.
     *       4) The drawer name equals ""
     *       5) The color of this page is null.
     *       6) The font of this page is "Default", PLAIN, and 12.
     *       7) The set of blocks is empty.
     */
    public static Page getBlankPage(Workspace workspace) {
        return new Page(workspace, emptyString);
    }

    /**
     * TODO: THIS METHOD NOT YET DOCUMENTED OR IMPLEMENTED
     * Removes all the RenderableBlock content of this.
     * Called when the Workspace is being reset.  Does not fire block
     * removed events.
     */
    public void reset() {
        this.pageJComponent.removeAll();
        Page.zoom = 1.0;
    }

    /**
     * Destructs this Page by setting its set of blocks to empty.
     * Does NOT fire block removed events.
     */
    public void clearPage() {
        for (RenderableBlock block : this.getBlocks()) {
            this.pageJComponent.remove(block);
        }
    }

    /**
     * Sets the page id. Consider the page id "final" but settable - once
     * set, it cannot be modified or unset.
     */
    public void setPageId(String id) {
        if (pageId == null) {
            pageId = id;
        } else {
            throw new RuntimeException("Tried to set pageId again: " + this);
        }
    }

    //////////////////////////////
    //Public Accessor      //
    //////////////////////////////
    /**
     * @return all the RenderableBlocks that reside within this page
     */
    @Override
    public Collection<RenderableBlock> getBlocks() {
        List<RenderableBlock> blocks = new ArrayList<RenderableBlock>();
        for (Component block : this.pageJComponent.getComponents()) {
            if (block instanceof RenderableBlock) {
                blocks.add((RenderableBlock) block);
            }
        }
        return blocks;
    }

    /**
     * @return a collection of top level blocks within this page (blocks with no
     *       parents that and are the first block of each stack) or an empty
     *       collection if no blocks are found on this page.
     */
    public Collection<RenderableBlock> getTopLevelBlocks() {
        List<RenderableBlock> topBlocks = new ArrayList<RenderableBlock>();
        for (RenderableBlock renderable : this.getBlocks()) {
            Block block = workspace.getEnv().getBlock(renderable.getBlockID());
            if (block.getPlug() == null || block.getPlugBlockID() == null || block.getPlugBlockID().equals(Block.NULL)) {
                if (block.getBeforeConnector() == null || block.getBeforeBlockID() == null || block.getBeforeBlockID().equals(Block.NULL)) {
                    topBlocks.add(renderable);
                    continue;
                }
            }
        }
        return topBlocks;
    }

    /**
     * Returns this page's id. Can be null, if id is not yet set.
     */
    public String getPageId() {
        return pageId;
    }

    /**
     * @return this page's name
     */
    public String getPageName() {
        return this.pageJComponent.getName();
    }

    /**
     * @return this page's color.  MAY RETURN NULL.
     */
    public Color getPageColor() {
        return this.pageJComponent.getBackground();
    }

    /**
     * @return this page's default color.  MAY RETURN NULL.
     */
    public Color getDefaultPageColor() {
        return this.defaultColor;
    }

    /**
     * @return this page's abstract width
     */
    public double getAbstractWidth() {
        return abstractWidth;
    }

    /**
     * @return this page's abstract height
     */
    public double getAbstractHeight() {
        return abstractHeight;
    }

    /**
     * @return this page drawer that this page refers to or null if non exists.
     *       MAY RETURN NULL.
     */
    public String getPageDrawer() {
        return pageDrawer;
    }

    /**
     * @return icon of this.  MAY BE NULL
     */
    public Image getIcon() {
        return this.pageJComponent.getImage();
    }

    public boolean isInFullview() {
        return fullview;
    }

    //////////////////////////////
    //Rendering Mutators    //
    //////////////////////////////
    /**
     * @param newName - the new name of this page.
     *
     * @requires newName != null
     * @modifies this.name
     * @effects sets the name of this page to be newName.
     */
    public void setPageName(String newName) {
        if (pageDrawer.equals(this.pageJComponent.getName())) {
            pageDrawer = newName;
        }

        this.pageJComponent.setName(newName);
        this.collapse.setText(newName);

        //iterate through blocks and update the ones that are page label enabled
        for (RenderableBlock block : this.getBlocks()) {
            if (workspace.getEnv().getBlock(block.getBlockID()).isPageLabelSetByPage()) {
                workspace.getEnv().getBlock(block.getBlockID()).setPageLabel(this.getPageName());
                block.repaintBlock();
            }
        }

        PageChangeEventManager.notifyListeners();
    }

    /**
     * @param image - the new icon of this.  May be null
     *
     * @requires NONE
     * @modifies this.icon
     * @effects change this.icon to specified icon.  The new icon may be null
     */
    public void setIcon(Image image) {
        this.pageJComponent.setImage(image);
    }

    /**
     * @param newColor - the new color of this page
     *
     * @requires none
     * @modifies this.color
     * @effects Set the color of this page tobe newColor.
     *       If newColor is null, sets the color to the deafult gray.
     */
    public void setPageColor(Color newColor) {
        this.pageJComponent.setBackground(newColor);
    }

    /**
     * @param deltaPixelWidth
     *
     * @requires Integer.MIN_VAL <= deltaPixelWidth <= Integer.MAX_VAL
     * @modifies this.width
     * @effects Adds deltaPixelWidth to the abstract width taking into
     *       account the zoom level.  May need to convert form pixel to abstract model.
     */
    public void addPixelWidth(int deltaPixelWidth) {
        if (fullview) {
            this.setPixelWidth((int) (this.getAbstractWidth() * zoom + deltaPixelWidth));
        }
    }

    /**
     * @requires Integer.MIN_VAL <= pixelWidth <= Integer.MAX_VAL
     * @modifies this.width
     * @effects sets abstract width to pixelWidth taking into account the zoom level.
     *       May need to convert form pixel to abstract model.

     */
    public void setPixelWidth(int pixelWidth) {
        if (pixelWidth < this.minimumPixelWidth) {
            this.abstractWidth = this.minimumPixelWidth / zoom;
        } else {
            this.abstractWidth = pixelWidth / zoom;
        }
    }

    //////////////////////////////
    //Reforming Mutators    //
    //////////////////////////////
    /**
     * @param pixelXCor - the new X location of page's JComponent in terms of pixels
     * @requires none
     * @return the current width of this page in terms of pixels
     * @modifies this.JComponent.size
     * @effects Reforms this page's JComponent in order to synchronize the
     *       abstract width and height with the graphical view.
     *       This process includes moving this page's JComponent to (pixelXCor,0)
     *       and setting this page's JComponent size to (this.abstractwidth*zoom, this.abstractheight*zoom)
     */
    public int reformBounds(double pixelXCor) {
        if (fullview) {
            this.getJComponent().setBounds(
                    (int) (pixelXCor),
                    0,
                    (int) (this.abstractWidth * zoom),
                    (int) (this.abstractHeight * zoom));
            this.getJComponent().setFont(new Font("Ariel", Font.PLAIN, (int) (12 * zoom)));
            return (int) (this.abstractWidth * zoom);
        } else {
            this.getJComponent().setBounds(
                    (int) (pixelXCor),
                    0,
                    COLLAPSED_WIDTH + 2,
                    (int) (this.abstractHeight * zoom));
            this.getJComponent().setFont(new Font("Ariel", Font.PLAIN, (int) (12 * zoom)));
            return COLLAPSED_WIDTH + 2;
        }

    }

    /**
     * @param block - the new block being added whose position must be revalidated
     *
     * @requires block != null
     * @modifies block.location or this page's abstract width
     * @effects shifts this block into the page or increases the
     *       width of this page to fit the new block.  It must then
     *       notify listeners that the page's size may have changed
     */
    public void reformBlockPosition(RenderableBlock block) {
        //move blocks in
        Point p = SwingUtilities.convertPoint(block.getParent(), block.getLocation(), this.pageJComponent);
        if (p.x < block.getHighlightStrokeWidth() / 2 + 1) {
            block.setLocation(block.getHighlightStrokeWidth() / 2 + 1, p.y);
            block.moveConnectedBlocks();
            // the block has moved, so update p
            p = SwingUtilities.convertPoint(block.getParent(), block.getLocation(), this.pageJComponent);
        } else if (p.x + block.getWidth() + block.getHighlightStrokeWidth() / 2 + 1 > this.pageJComponent.getWidth()) {
            this.setPixelWidth(p.x + block.getWidth() + block.getHighlightStrokeWidth() / 2 + 1);
        }

        if (p.y < block.getHighlightStrokeWidth() / 2 + 1) {
            block.setLocation(p.x, block.getHighlightStrokeWidth() / 2 + 1);
            block.moveConnectedBlocks();
        } else if (p.y + block.getStackBounds().height + block.getHighlightStrokeWidth() / 2 + 1 > this.pageJComponent.getHeight()) {
            block.setLocation(p.x, this.pageJComponent.getHeight() - block.getStackBounds().height - block.getHighlightStrokeWidth() / 2 + 1);
            block.moveConnectedBlocks();
        }

        if (block.hasComment()) {
            //p = SwingUtilities.convertPoint(block.getComment().getParent(), block.getComment().getLocation(), this.pageJComponent);
            p = block.getComment().getLocation();
            if (p.x + block.getComment().getWidth() + 1 > this.pageJComponent.getWidth()) {
                this.setPixelWidth(p.x + block.getComment().getWidth() + 1);
            }
        }
       
        // Recompute page height.
        reformMinimumPixelHeight();

        //repaint all pages
        PageChangeEventManager.notifyListeners();
    }

    /**
     * @modifies this.miniPixelWidth
     * @effects sets the minimumPixelWidth such that the following condition holds:
     *       DEFAULT_MINIMUMWIDTH < new minimumPixelWidth &&
     *       for each block, b, in this page's set of blocks {
     *         b.x+b.width < new minimumPixelWidth}
     */
    public void reformMinimumPixelWidth() {
        minimumPixelWidth = 0; // reset min to 0

        // loop through blocks, growing min to fit each block
        for (RenderableBlock b : this.getBlocks()) {
            if (b.getX() + b.getWidth() + b.getHighlightStrokeWidth() / 2 > minimumPixelWidth) {
                // increase min width to fit this block
                minimumPixelWidth = b.getX() + b.getWidth() + b.getHighlightStrokeWidth() / 2 + 1;
            }

            if (b.hasComment()) {
                if (b.getComment().getX() + b.getComment().getWidth() > minimumPixelWidth) {
                    // increase min width to fit this block
                    minimumPixelWidth = b.getComment().getX() + b.getComment().getWidth() + 1;
                }
            }
        }
        if (this.minimumPixelWidth < Page.DEFAULT_MINUMUM_WIDTH * zoom) {
            this.minimumPixelWidth = (int) (Page.DEFAULT_MINUMUM_WIDTH * zoom);
        }
    }
   
    public void reformMinimumPixelHeight() {
        minimumPixelHeight = 0;
        for (RenderableBlock b : this.getBlocks()) {
            if (b.getY()+b.getHeight()+b.getHighlightStrokeWidth() /2 > minimumPixelHeight) {
                minimumPixelHeight = b.getY() + b.getHeight() + b.getHighlightStrokeWidth()/2+1;
            }
            if (b.hasComment()) {
                minimumPixelHeight = b.getComment().getY() + b.getComment().getHeight() + 1;
            }
        }
        if (this.minimumPixelHeight < Page.DEFAULT_MINIMUM_HEIGHT * zoom) {
            this.minimumPixelHeight = (int) (Page.DEFAULT_MINIMUM_HEIGHT * zoom);
        }
    }
   
    public int getMinimumPixelHeight() {
        return this.minimumPixelHeight;
    }

    /**
     * @requires the current set of blocks of this page != null (though it may be empty)
     * @modifies all the block in this page's set of blocks
     * @effects Automatically arranges all the blocks within this page naively.
     */
    public void reformBlockOrdering() {
        BlockStackSorterUtil.sortBlockStacks(this, this.getTopLevelBlocks());
    }

    //////////////////////////////
    //Zoomable Interface    //
    //////////////////////////////
    /**
     * @param newZoom - the new zoom level
     *
     * @requires zoom != 0
     * @modifies zoom level
     * @effects Sets all the Zoomable Pages in contained in this BlockCanvas and
     * sets the zoom level to newZoom.
     */
    public static void setZoomLevel(double newZoom) {
        Page.zoom = newZoom;
    }

    /** @overrides Zoomable.getZoomLevel() */
    public static double getZoomLevel() {
        return Page.zoom;
    }

    //////////////////////////////
    //WORKSPACEWIDGET METHODS   //
    //////////////////////////////
    /** @overrides WorkspaceWidget.blockDropped() */
    @Override
    public void blockDropped(RenderableBlock block) {
        //add to view at the correct location
        Component oldParent = block.getParent();
        block.setLocation(SwingUtilities.convertPoint(oldParent,
                block.getLocation(), this.pageJComponent));
        addBlock(block);
        this.pageJComponent.setComponentZOrder(block, 0);
        this.pageJComponent.revalidate();
    }

    /** @overrides WorkspaceWidget.blockDragged() */
    @Override
    public void blockDragged(RenderableBlock block) {
        if (mouseIsInPage == false) {
            mouseIsInPage = true;
            this.pageJComponent.repaint();
        }
    }

    /** @overrides WorkspaceWidget.blockEntered() */
    @Override
    public void blockEntered(RenderableBlock block) {
        if (mouseIsInPage == false) {
            mouseIsInPage = true;
            this.pageJComponent.repaint();
        }
    }

    /** @overrides WorkspaceWidget.blockExited() */
    @Override
    public void blockExited(RenderableBlock block) {
        mouseIsInPage = false;
        this.pageJComponent.repaint();
    }

    /** @overrides WorkspaceWidget.addBlock() */
    @Override
    public void addBlock(RenderableBlock block) {
        //update parent widget if dropped block
        WorkspaceWidget oldParent = block.getParentWidget();
        if (oldParent != this) {
            if (oldParent != null) {
                oldParent.removeBlock(block);
                if (block.hasComment()) {
                    block.getComment().getParent().remove(block.getComment());
                }
            }
            block.setParentWidget(this);
            if (block.hasComment()) {
                block.getComment().setParent(block.getParentWidget().getJComponent());
            }
        }

        this.getRBParent().addToBlockLayer(block);
        block.setHighlightParent(this.getRBParent());

        //if block has page labels enabled, in other words, if it can, then set page label to this
        if (workspace.getEnv().getBlock(block.getBlockID()).isPageLabelSetByPage()) {
            workspace.getEnv().getBlock(block.getBlockID()).setPageLabel(this.getPageName());
        }

        //notify block to link default args if it has any
        block.linkDefArgs();

        //fire to workspace that block was added to canvas if oldParent != this
        if (oldParent != this) {
            workspace.notifyListeners(new WorkspaceEvent(workspace, oldParent, block.getBlockID(), WorkspaceEvent.BLOCK_MOVED));
            workspace.notifyListeners(new WorkspaceEvent(workspace, this, block.getBlockID(), WorkspaceEvent.BLOCK_ADDED, true));
        }

        // if the block is off the edge, shift everything or grow as needed to fully show it
        this.reformBlockPosition(block);

        this.pageJComponent.setComponentZOrder(block, 0);
    }

    /**
     * @param blocks the Collection of RenderableBlocks to add
     *
     * @requires blocks != null
     * @modifies this page's set of blocks
     * @effects Add the collection of blocks internally and graphically,
     *       delaying graphicalupdates until all of the blocks have been added.
     * @overrides WorkspaceWidget.blockEntered()
     */
    @Override
    public void addBlocks(Collection<RenderableBlock> blocks) {
        for (RenderableBlock block : blocks) {
            this.addBlock(block);
        }
        //since new components added, need to validate
        this.pageJComponent.revalidate();
    }

    /** @overrides WorkspaceWidget.removeBlock() */
    @Override
    public void removeBlock(RenderableBlock block) {
        this.pageJComponent.remove(block);
    }

    /** @overrides WorkspaceWidget.getJComponent() */
    @Override
    public JComponent getJComponent() {
        return this.pageJComponent;
    }

    /**
     * @return the RBParent representation of this Page
     */
    public RBParent getRBParent() {
        return (RBParent) this.pageJComponent;
    }

    /** @overrides WorkspaceWidget.contains() */
    @Override
    public boolean contains(int x, int y) {
        return this.pageJComponent.contains(x, y);
    }

    /** @overrides WorkspaceWidget.contains() */
    public boolean contains(Point p) {
        return this.contains(p.x, p.y);
    }

    /** Returns string representation of this */
    @Override
    public String toString() {
        return "Page name: " + getPageName() + " page color " + getPageColor() + " page width " + getAbstractWidth() + " page drawer " + pageDrawer;
    }

    //////////////////////////////////
    // SearchableContainer Methods  //
    //////////////////////////////////
    /** @overrides SearchableContainer.getSearchableElements */
    @Override
    public Iterable<RenderableBlock> getSearchableElements() {
        return getBlocks();
    }

    /** @overrides SearchableContainer.updateContainerSearchResults */
    @Override
    public void updateContainsSearchResults(boolean containsSearchResults) {
        // Do nothing, at least for now
    }

    //////////////////////////
    //SAVING AND LOADING  //
    //////////////////////////
    public ArrayList<RenderableBlock> loadPageFrom(Node pageNode, boolean importingPage) {
        //note: this code is duplicated in BlockCanvas.loadSaveString().
        NodeList pageChildren = pageNode.getChildNodes();
        Node pageChild;
        ArrayList<RenderableBlock> loadedBlocks = new ArrayList<RenderableBlock>();
        HashMap<Long, Long> idMapping = importingPage ? new HashMap<Long, Long>() : null;
        if (importingPage) {
            reset();
        }
        for (int i = 0; i < pageChildren.getLength(); i++) {
            pageChild = pageChildren.item(i);
            if (pageChild.getNodeName().equals("PageBlocks")) {
                NodeList blocks = pageChild.getChildNodes();
                Node blockNode;
                for (int j = 0; j < blocks.getLength(); j++) {
                    blockNode = blocks.item(j);
                    RenderableBlock rb = RenderableBlock.loadBlockNode(workspace, blockNode, this, idMapping);
                    // save the loaded blocks to add later
                    loadedBlocks.add(rb);
                }
                break//should only have one set of page blocks
            }
        }
        return loadedBlocks;
    }

    public void addLoadedBlocks(Collection<RenderableBlock> loadedBlocks, boolean importingPage) {
        for (RenderableBlock rb : loadedBlocks) {
            if (rb != null) {
                //add graphically
                getRBParent().addToBlockLayer(rb);
                rb.setHighlightParent(this.getRBParent());
                //System.out.println("loading rb to canvas: "+rb+" at: "+rb.getBounds());
                //add internallly
                workspace.notifyListeners(new WorkspaceEvent(workspace, this, rb.getBlockID(), WorkspaceEvent.BLOCK_ADDED));
                if (importingPage) {
                  workspace.getEnv().getBlock(rb.getBlockID()).setFocus(false);
                    rb.resetHighlight();
                    rb.clearBufferedImage();
                }
            }
        }


        //now we need to redraw all the blocks now that all renderable blocks
        //within this page have been loaded, to update the socket dimensions of
        //blocks, etc.
        for (RenderableBlock rb : this.getTopLevelBlocks()) {
            rb.redrawFromTop();
            if (rb.isCollapsed()) {
                //This insures that blocks connected to a collapsed top level block
                //are located properly and have the proper visibility set.
                //This doesn't work until all blocks are loaded and dimensions are set.
                rb.updateCollapse();
            }
        }
        this.pageJComponent.revalidate();
        this.pageJComponent.repaint();
    }

    public Node getSaveNode(Document document) {
      Element pageElement = document.createElement("Page");

      pageElement.setAttribute("page-name", getPageName());
      pageElement.setAttribute("page-color", getPageColor().getRed() + " " + getPageColor().getGreen() + " " + getPageColor().getBlue());
      pageElement.setAttribute("page-width", String.valueOf((int)getAbstractWidth()));
        if (fullview) {
            pageElement.setAttribute("page-infullview", "yes");
        } else {
            pageElement.setAttribute("page-infullview", "no");
        }
        if (pageDrawer != null) {
            pageElement.setAttribute("page-drawer", pageDrawer);
        }
        if (pageId != null) {
            pageElement.setAttribute("page-id", pageId);
        }

        //retrieve save strings of blocks within this Page
        Collection<RenderableBlock> blocks = this.getBlocks();
        if (blocks.size() > 0) {
            Element pageBlocksElement = document.createElement("PageBlocks");
            for (RenderableBlock rb : blocks) {
                pageBlocksElement.appendChild(rb.getSaveNode(document));
            }
            pageElement.appendChild(pageBlocksElement);
        }
      return pageElement;
    }

    ////////////////////////////////////
    //State Saving Stuff for Undo/Redo//
    ////////////////////////////////////
    /**
     * a data structure that holds the name, width, color, set of blocks,
     * and set of renderable blocks in this page.
     */
    private class PageState {

        public String name;
        public String id;
        public int width;
        public Color color;
        public boolean fullview;
        public Map<Long, Object> blocks = new HashMap<Long, Object>();
        public Map<Long, Object> renderableBlocks = new HashMap<Long, Object>();
    }

    /** @overrides ISupportMomento.getState */
    @Override
    public Object getState() {
        PageState state = new PageState();
        //Populate basic page information
        state.name = getPageName();
        state.id = getPageId();
        state.color = getPageColor();
        state.width = this.pageJComponent.getWidth();
        //Fill in block information
        for (RenderableBlock rb : this.getBlocks()) {
            state.renderableBlocks.put(rb.getBlockID(), rb.getState());
        }
        return state;
    }

    /** @overrides ISupportMomento.loadState() */
    @Override
    public void loadState(Object memento) {
        assert (memento instanceof PageState) : "ISupportMemento contract violated in Page";
        if (memento instanceof PageState) {
            PageState state = (PageState) memento;
            //load basic page information
            this.setPageName(state.name);
            this.setPageId(state.id);
            this.setPageColor(state.color);
            this.setPixelWidth(state.width);
            //Load block information
            Map<Long, Object> renderableBlockStates = state.renderableBlocks;
            List<Long> unloadedRenderableBlockStates = new LinkedList<Long>();
            List<Long> loadedBlocks = new LinkedList<Long>();
            for (Long id : renderableBlockStates.keySet()) {
                unloadedRenderableBlockStates.add(id);
            }
            //First, load all the blocks that are in the state to be loaded
            //against all the blocks that already exist.
            for (RenderableBlock existingBlock : getBlocks()) {
                Long existingBlockID = existingBlock.getBlockID();
                if (renderableBlockStates.containsKey(existingBlockID)) {
                    existingBlock.loadState(renderableBlockStates.get(existingBlockID));
                    unloadedRenderableBlockStates.remove(existingBlockID);
                    loadedBlocks.add(existingBlockID);
                }
            }
            ArrayList<RenderableBlock> blocksToRemove = new ArrayList<RenderableBlock>();
            //Now, find all the blocks that don't exist in the save state and flag them to be removed.
            for (RenderableBlock existingBlock : this.getBlocks()) {
                Long existingBlockID = existingBlock.getBlockID();
                if (!loadedBlocks.contains(existingBlockID)) {
                    blocksToRemove.add(existingBlock);
                }
            }
            //This loop is necessary to avoid a concurrent modification error that occurs
            //if the loop above removes the block while iterating over an unmodifiable
            //iterator.
            for (RenderableBlock toBeRemovedBlock : blocksToRemove) {
                this.removeBlock(toBeRemovedBlock);
            }
            //Finally, add all the remaining blocks that weren't there before
            ArrayList<RenderableBlock> blocksToAdd = new ArrayList<RenderableBlock>();
            for (Long newBlockID : unloadedRenderableBlockStates) {
                RenderableBlock newBlock = new RenderableBlock(workspace, this, newBlockID);
                newBlock.loadState(renderableBlockStates.get(newBlockID));
                blocksToAdd.add(newBlock);
            }
            this.addBlocks(blocksToAdd);
            this.pageJComponent.repaint();
        }
    }

    private class CollapseButton extends JPanel implements MouseListener {

        private static final long serialVersionUID = 328149080273L;
        //To get the shadow effect the text must be displayed multiple times at
        //multiple locations.  x represents the center, white label.
        // o is color values (0,0,0,0.5f) and b is black.
        //        o o
        //      o x b o
        //      o b o
        //        o
        //offsetArrays representing the translation movement needed to get from
        // the center location to a specific offset location given in {{x,y},{x,y}....}
        //..........................................grey points.............................................black points
        private final int[][] shadowPositionArray = {{0, -1}, {1, -1}, {-1, 0}, {2, 0}, {-1, 1}, {1, 1}, {0, 2}, {1, 0}, {0, 1}};
        private final float[] shadowColorArray = {0.5f, 0.5f, 0.5f, 0.5f, 0.5f, 0.5f, 0.5f, 0, 0};
        private double offsetSize = 1;
        private String[] charSet;
        private int FONT_SIZE = 12;
        private boolean pressed = false;
        private boolean focus = false;
        private String button_text = "";

        public CollapseButton(boolean inFullview, String text) {
            super();
            //this.setBounds(0,0,COLLAPSED_WIDTH,COLLAPSED_WIDTH);
            this.addMouseListener(this);
            this.setOpaque(false);
            this.charSet = new String[0];
            this.setFont(new Font("Ariel", Font.BOLD, FONT_SIZE));
            this.setText(text);
            loadBounds(inFullview);
        }

        @Override
        public JToolTip createToolTip() {
            return new CToolTip(new Color(0xFFFFDD));
        }

        public void setText(String text) {
            if (text != null) {
                text = text.toUpperCase();
                List<String> characters = new ArrayList<String>();
                for (int i = 0; i < text.length(); i++) {
                    characters.add(text.substring(i, i + 1));
                }
                charSet = characters.toArray(charSet);
                this.button_text = text;
            }
        }

        private void paintFull(Graphics g) {
            paintFull(g, Color.white);
        }

        private void paintCollapsed(Graphics g) {
            paintCollapsed(g, Color.white);
        }

        private void paintFull(Graphics g, Color col) {
            int w = this.getWidth();
            g.setColor(col);
            g.fillRect(5, 5, w - 10, w - 17);
            g.setColor(col);
            g.drawRoundRect(3, 3, w - 6, w - 6, 3, 3);
        }

        private void paintCollapsed(Graphics g, Color col) {
            int w = this.getWidth();
            g.setColor(col);
            g.fillRect(5, 5, w - 10, w - 15);
            Graphics2D g2 = (Graphics2D) g;
            for (int j = 0; j < charSet.length; j++) {
                String c = charSet[j];
                int x = 5;
                int y = (j + 2) * (FONT_SIZE + 3);
                g.setColor(Color.black);
                for (int i = 0; i < shadowPositionArray.length; i++) {
                    int dx = shadowPositionArray[i][0];
                    int dy = shadowPositionArray[i][1];
                    g2.setColor(new Color(0, 0, 0, shadowColorArray[i]));
                    g2.drawString(c, x + (int) ((dx) * offsetSize), y + (int) ((dy) * offsetSize));
                }
                g2.setColor(col);
                g2.drawString(c, x, y);
            }
            g2.drawRoundRect(3, 3, w - 6, w - 6 + charSet.length * (FONT_SIZE + 3), 3, 3);
        }

        @Override
        public void paintComponent(Graphics g) {
            int w = this.getWidth();

            if ((!Page.this.hideMinimize)) {
                if (fullview) {
                    this.setToolTipText("Collapse " + this.button_text);
                    paintFull(g);
                    if (pressed) {
                        g.setColor(Color.blue.darker());
                        g.fillRoundRect(3, 3, w - 6, w - 6, 3, 3);
                        paintFull(g);
                    } else {
                        if (focus) {
                            g.setColor(new Color(51, 153, 255)); //light blue
                            g.fillRoundRect(3, 3, w - 6, w - 6, 3, 3);
                            paintFull(g);
                        }
                    }
                } else {
                    this.setToolTipText("Restore " + this.button_text);
                    paintCollapsed(g);
                    if (pressed) {
                        g.setColor(Color.blue.darker());
                        g.fillRoundRect(3, 3, w - 6, w - 6 + charSet.length
                                * (FONT_SIZE + 3), 3, 3);
                        paintCollapsed(g);
                    } else {
                        if (focus) {
                            g.setColor(new Color(51, 153, 255)); //light blue
                            g.fillRoundRect(3, 3, w - 6, w - 6 + charSet.length
                                    * (FONT_SIZE + 3), 3, 3);
                            paintCollapsed(g);
                        }
                    }
                }
            } else {
                if (fullview) {
                    paintFull(g, Color.gray);
                } else {
                    paintCollapsed(g, Color.gray);
                }
            }

        }

        private void loadBounds(boolean fullview) {
            if (!fullview) {
                this.setBounds(0, 0, COLLAPSED_WIDTH, charSet.length
                        * (FONT_SIZE + 3) + COLLAPSED_WIDTH);
            } else {
                this.setBounds(0, 0, COLLAPSED_WIDTH, COLLAPSED_WIDTH);
            }
        }

        @Override
        public void mouseClicked(MouseEvent e) {

            if ((!Page.this.hideMinimize)) {
                if (fullview) {
                    this.setBounds(0, 0, COLLAPSED_WIDTH, charSet.length
                            * (FONT_SIZE + 3) + COLLAPSED_WIDTH);
                } else {
                    this.setBounds(0, 0, COLLAPSED_WIDTH, COLLAPSED_WIDTH);
                }
                fullview = !fullview;
                pageJComponent.setFullView(fullview);
                PageChangeEventManager.notifyListeners();
            }
        }

        @Override
        public void mousePressed(MouseEvent e) {
            if ((!Page.this.hideMinimize)) {
                pressed = true;
                this.repaint();
            }
        }

        public void mouseDragged(MouseEvent e) {
        }

        @Override
        public void mouseReleased(MouseEvent e) {
            if ((!Page.this.hideMinimize)) {
                pressed = false;
                this.repaint();
            }
        }

        public void mouseMoved(MouseEvent e) {
        }

        @Override
        public void mouseEntered(MouseEvent e) {
            if ((!Page.this.hideMinimize)) {
                focus = true;
                this.repaint();
            }
        }

        @Override
        public void mouseExited(MouseEvent e) {
            if ((!Page.this.hideMinimize)) {
                focus = false;
                this.repaint();
            }
        }
    }
}

/**
* This class serves as the zoomable JComponent and RBParent of the page
* that wraps it.
*/
class PageJComponent extends JLayeredPane implements RBParent {

    private static final long serialVersionUID = 83982193213L;
    private static final Integer BLOCK_LAYER = new Integer(1);
    private static final Integer HIGHLIGHT_LAYER = new Integer(0);
    private static final int IMAGE_WIDTH = 60;
    private Image image = null;
    private boolean fullview = true;

    public void setFullView(boolean isFullView) {
        this.fullview = isFullView;
    }

    public void setImage(Image image) {
        this.image = image;
    }

    public Image getImage() {
        return image;
    }

    /**
     * renders this JComponent
     */
    @Override
    public void paintComponent(Graphics g) {
        Graphics2D g2 = (Graphics2D) g;
        //paint page
        super.paintComponent(g);
        //set label color
        if (this.getBackground().getBlue() + this.getBackground().getGreen() + this.getBackground().getRed() > 400) {
            g.setColor(Color.DARK_GRAY);
        } else {
            g.setColor(Color.LIGHT_GRAY);
        }

        //paint label at correct position
        if (fullview) {
            int xpos = (int) (this.getWidth() * 0.5 - g.getFontMetrics().getStringBounds(this.getName(), g).getCenterX());
            g.drawString(this.getName(), xpos, getHeight() / 2);
            g.drawString(this.getName(), xpos, getHeight() / 4);
            g.drawString(this.getName(), xpos, getHeight() * 3 / 4);


            g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.33F));
            int imageX = (int) (this.getWidth() / 2 - IMAGE_WIDTH / 2 * Page.zoom);
            int imageWidth = (int) (IMAGE_WIDTH * Page.zoom);
            g.drawImage(this.getImage(), imageX, getHeight() / 2 + 5, imageWidth, imageWidth, null);
            g.drawImage(this.getImage(), imageX, getHeight() / 4 + 5, imageWidth, imageWidth, null);
            g.drawImage(this.getImage(), imageX, getHeight() * 3 / 4 + 5, imageWidth, imageWidth, null);
            g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 1));
        }

    }

    //////////////////////////////////
    //RBParent implemented methods  //
    //////////////////////////////////
    /** @overrides RBParent.addToBlockLayer() */
    @Override
    public void addToBlockLayer(Component c) {
        this.add(c, BLOCK_LAYER);

    }

    /** @overrides RBParent.addToHighlightLayer() */
    @Override
    public void addToHighlightLayer(Component c) {
        this.add(c, HIGHLIGHT_LAYER);
    }
}

/**
* A BlockStatckSortUtil is a utilities class that serves to order
* blocks from closest to furthest blocks (relative to the x=0 axis).
*/
class BlockStackSorterUtil {

    /** The minimum bounds between blocks */
    private static final int BUFFER_BETWEEN_BLOCKS = 20;
    /** A helper rectangle that maintains the bounds between blocks */
    private static final Rectangle positioningBounds = new Rectangle(BUFFER_BETWEEN_BLOCKS, BUFFER_BETWEEN_BLOCKS, 0, 0);
    /** An ordered set of blocks.  Blocks are ordered from closest to furthest (relative to x=0 axis) */
    private static final TreeSet<RenderableBlock> blocksToArrange = new TreeSet<RenderableBlock>(
            //TODO ria for now they are ordered in y-coor order
            //this naive ordering will also fail if two blocks have the same coordinates
            new Comparator<RenderableBlock>() {

        @Override
        public int compare(RenderableBlock rb1, RenderableBlock rb2) {
            if (rb1 == rb2) {
                return 0;
            } else {
                //translate points to a common reference: the parent of rb1
                Point pt1 = rb1.getLocation();
                Point pt2 = SwingUtilities.convertPoint(rb2.getParentWidget().getJComponent(),
                        rb2.getLocation(), rb1.getParentWidget().getJComponent());
                if (pt1.getY() < pt2.getY()) {
                    return -1;
                } else {
                    return 1;
                }
            }
        }
    });

    /**
     * This method serves to help clients sort blocks within a page
     * in some manner.
     *
     * @param page
     * @param topLevelBlocks
     *
     * @requires page != null && topLevelBlocks != null
     * @modifies the location of all topLevelBlocks
     * @effects sort the topLevelBlocks and move them to an order location on the page
     */
    protected static void sortBlockStacks(Page page, Collection<RenderableBlock> topLevelBlocks) {
        blocksToArrange.clear();
        positioningBounds.setBounds(BUFFER_BETWEEN_BLOCKS, BUFFER_BETWEEN_BLOCKS, 0, BUFFER_BETWEEN_BLOCKS);
        //created an ordered list of blocks based on x-coordinate position
        blocksToArrange.addAll(topLevelBlocks);

        //Naively places blocks from top to bottom, left to right.
        for (RenderableBlock block : blocksToArrange) {
            Rectangle bounds = block.getStackBounds();
            if (positioningBounds.height + bounds.height > page.getJComponent().getHeight()) {
                //need to go to next column
                positioningBounds.x = positioningBounds.x + positioningBounds.width + BUFFER_BETWEEN_BLOCKS;
                positioningBounds.width = 0;
                positioningBounds.height = BUFFER_BETWEEN_BLOCKS;
            }
            block.setLocation(positioningBounds.x, positioningBounds.height);

            //sets the x and y position for when workspace is unzoomed
            block.setUnzoomedX(block.calculateUnzoomedX(positioningBounds.x));
            block.setUnzoomedY(block.calculateUnzoomedY(positioningBounds.height));
            block.moveConnectedBlocks();

            //update positioning bounds
            positioningBounds.width = Math.max(positioningBounds.width, bounds.width);
            positioningBounds.height = positioningBounds.height + bounds.height + BUFFER_BETWEEN_BLOCKS;

            if (positioningBounds.x + positioningBounds.width > page.getJComponent().getWidth()) {
                //resize page to the difference
                page.addPixelWidth(positioningBounds.x + positioningBounds.width - page.getJComponent().getWidth());
            }
        }
    }
}
TOP

Related Classes of edu.mit.blocks.workspace.Page

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.