package edu.mit.blocks.workspace;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.GraphicsEnvironment;
import java.awt.Point;
import java.awt.PopupMenu;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import javax.swing.BoundedRangeModel;
import javax.swing.JComponent;
import javax.swing.JLayeredPane;
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.codeblockutil.CGraphite;
import edu.mit.blocks.codeblockutil.CHoverScrollPane;
import edu.mit.blocks.codeblockutil.CScrollPane;
import edu.mit.blocks.codeblockutil.CScrollPane.ScrollPolicy;
import edu.mit.blocks.renderable.RenderableBlock;
import org.w3c.dom.NodeList;
/**
* A BlockCanvas is a container of Pages and is a scrollable
* panel. When a page is added to a BlockCanvas, that
* particular new page must be added to both the data
* structure holding the set of pages and the scrollable
* panel that renders the page.
*
* A BlockCanvas is also a PageChangeListener. When any
* pages are changed, the Blockcanvas must update itself
* appropriately to reflect this change.
*
* As of the current implementation, the BlockCanvas must
* have at least one Page when it becomes visible (that is,
* when its viewable JComponent becomes visible).
*/
public class BlockCanvas implements PageChangeListener, ISupportMemento {
/** serial version ID */
private static final long serialVersionUID = 7458721329L;
/** the collection of pages that this BlockCanvas stores */
private List<Page> pages = new ArrayList<Page>();
/** the collection of PageDivideres that this BlockCanvas stores */
private List<PageDivider> dividers = new ArrayList<PageDivider>();
/** The Swing representation of the page container */
private JComponent canvas;
/** The scrollable JComponent representing the graphical part of this BlockCanvas */
private CScrollPane scrollPane;
/** The workspace in use */
private final Workspace workspace;
private boolean collapsible = false;
//////////////////////////////
//Constructor/Destructor //
//////////////////////////////
/**
* Constructs BlockCanvas and subscribes
* this BlockCanvas to PageChange events
*/
public BlockCanvas(Workspace workspace) {
this.workspace = workspace;
this.canvas = new Canvas();
this.scrollPane = new CHoverScrollPane(canvas,
ScrollPolicy.VERTICAL_BAR_ALWAYS,
ScrollPolicy.HORIZONTAL_BAR_ALWAYS,
18, CGraphite.blue, null);
scrollPane.setScrollingUnit(5);
canvas.setLayout(null);
canvas.setBackground(Color.gray);
canvas.setOpaque(true);
PageChangeEventManager.addPageChangeListener(this);
}
/**
* @effects resets BlockCanvas by removing all pages, dividers, and blocks.
*/
public void reset() {
pages.clear();
canvas.removeAll();
dividers.clear();
scrollPane.revalidate();
}
//////////////////////////////
//Rendering View Accessor //
//////////////////////////////
/** @return X Coordinate of BlockCanvas graphical representation */
public int getX() {
return scrollPane.getX();
}
/** @return Y coordinate of BlockCanvas graphical representation */
public int getY() {
return scrollPane.getY();
}
/** @return width of BlockCanvas graphical representation */
public int getWidth() {
return scrollPane.getWidth();
}
/** @return height of BlockCanvas graphical representation */
public int getHeight() {
return scrollPane.getHeight();
}
/** @return vertical scroll bar bounding range model. MAY BE NULL */
public BoundedRangeModel getVerticalModel() {
return scrollPane.getVerticalModel();
}
/** @return horizontal scroll bar bounding range model. MAY BE NULL */
public BoundedRangeModel getHorizontalModel() {
return scrollPane.getHorizontalModel();
}
/**
* @return the Swing Container that holds all the graphical panels of
* all the pages in this Blockcanvas
*/
public JComponent getCanvas() {
return this.canvas;
}
/**
* Warning: Please take special care in useing this method, as it exposes
* implementation details.
* @return JComponent representation of this
*/
public JComponent getJComponent() {
return scrollPane;
}
/** @return string representation of this */
@Override
public String toString() {
return "BlockCanvas " + pages.size() + " pages.";
}
public List<Page> getLeftmostPages(int left) {
List<Page> leftmostPages = new ArrayList<Page>();
int scrollPosition = this.scrollPane.getHorizontalModel().getValue();
int pagePosition = 0;
for (Page p : this.pages) {
pagePosition += p.getJComponent().getWidth();
if (pagePosition >= scrollPosition) {
if (pagePosition - p.getJComponent().getWidth() - scrollPosition <= left - 10) {
leftmostPages.add(p);
}
}
}
return leftmostPages;
}
//////////////////////////////
//Block Mutators/Accessors //
//////////////////////////////
/**
* @return the RendearbleBlocks that are contained within this widget
* or an empty Iterable if no blocks exists
*/
public Iterable<RenderableBlock> getBlocks() {
ArrayList<RenderableBlock> allPageBlocks = new ArrayList<RenderableBlock>();
for (Page p : pages) {
allPageBlocks.addAll(p.getBlocks());
}
return allPageBlocks;
}
/**
* @effects Automatically arranges all the blocks within this.
*/
public void arrangeAllBlocks() {
for (Page page : pages) {
page.reformBlockOrdering();
}
}
/**
* @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 Iterable<RenderableBlock> getTopLevelBlocks() {
return null;
}
/**
* @param block - the RenderableBlock to make sure is shown in the viewport
* @requires block ! null
* @modifies the vertical and horizontal scrollbal boundedRangeModel
* @effects This method causes the workspace to scroll if needed
* to complete show the given RenderableBlock.
*/
public void scrollToShowBlock(RenderableBlock block) {
//not yet implemented
}
public void scrollToComponent(JComponent c) {
//not yet implemented
}
//////////////////////////////
//Page Mutators/Accessors //
//////////////////////////////
/**
* @return the number of Pages.
*/
public int numOfPages() {
return pages.size();
}
/**
* @param position - 0 is the left most position
*
* @requires none
* @return true if there exists a page at the specified position
*/
public boolean hasPageAt(int position) {
return (position >= 0 && position < pages.size());
}
/**
* @param position - 0 is the left most position
*
* @requires none
* @return page at position or null if non exists at position
*/
protected Page getPageAt(int position) {
if (hasPageAt(position)) {
return pages.get(position);
} else {
return null;
}
}
/**
* @param name - name of page
*
* @requires none
* @return FIRST page with matchng name (if more than
* one page has matching name, it returns the first) or null
* if no matching name exists.
*/
public Page getPageNamed(String name) {
for (Page p : pages) {
if (p.getPageName().equals(name)) {
return p;
}
}
return null;
}
/**
* @return List of pages or an empty list if no pages exists
*/
public List<Page> getPages() {
return new ArrayList<Page>(pages);
}
/**
* @param page the page to add to the BlockCanvas
*
* @requires page != null
* @modifies this.pages
* @effects Adds the given page to the rightmost side of the BlockCanvas
*/
public void addPage(Page page) {
this.addPage(page, pages.size());
}
/**
* @param page - page to be added
* @param position - the index at which to add the page where 0 is rightmost
*
* @requires none
* @modifies this.pages
* @effects Inserts the specified page at the specified position.
* Shifts the element currently at that position (if any)
* and any subsequent elements to the right (adds one to
* their current position).
* @throws RuntimeException if (position < 0 || position > pages.size() || page == null)
*/
public void addPage(Page page, int position) {
if (page == null) {
throw new RuntimeException("Invariant Violated: May not add null Pages");
} else if (position < 0 || position > pages.size()) {
System.out.println(position + ", " + pages.size());
throw new RuntimeException("Invariant Violated: Specified position out of bounds");
}
pages.add(position, page);
canvas.add(page.getJComponent(), 0);
PageDivider pd = new PageDivider(workspace, page);
dividers.add(pd);
canvas.add(pd, 0);
PageChangeEventManager.notifyListeners();
}
/**
* @param page - the page to be removed
*
* @requires page != null
* @modifies this.pages
* @effects Removes the given Page from the BlockCanvas.
* If specified page not found in BlockCanvas, do nothing.
* if more than one page equals the specified page, then remove
* the first equal() instance.
*/
public Page removePage(Page page) {
if (page != null) {
// clear the blocks from the page and remove it internally
page.clearPage();
pages.remove(page);
// remove the pageDivider for this page too
for (PageDivider div : dividers) {
if (div.getLeftPage() == page) {
dividers.remove(div);
canvas.remove(div);
break;
}
}
// remove the page from the canvas and revalidate so it looks okay
canvas.remove(page.getJComponent());
canvas.revalidate();
canvas.repaint();
PageChangeEventManager.notifyListeners();
}
return page;
}
/**
* @param position - 0 is the left most page
*
* @requires none
* @return the page that was removed or null if non was removed.
* @modifies this.pages
* @effects If the position is within bounds, then remove
* the page located at the specified position.
* Do nothing if the position is out of bounds.
*/
public Page removePage(int position) {
if (this.hasPageAt(position)) {
return removePage(pages.get(position));
} else {
return null;
}
}
/**
* @param page the desired page to switch view to
*
* @requires page != null
* @modifies the ghorizontal boundedrangemodel of this blockcanvas
* @effects Switches the canvas view to the specified page.
*/
public void switchViewToPage(Page page) {
scrollPane.getHorizontalModel().setValue(page.getJComponent().getX());
}
/**
* @param oldName - the original name of the page
* @param newName - the String name to rename the page to
*
* @requires oldName != null && newName != null
* @return If a matching page was found, return the renamed Page.
* Otherwise, return null.
* @modifies the page with the matching oldName
* @effects Renames the page with the specified oldName to the newName.
*/
public Page renamePage(String oldName, String newName) {
for (Page page : pages) {
if (page.getPageName().equals(oldName)) {
page.setPageName(newName);
update();
return page;
}
}
return null;
}
////////////////////////////////
//PageChangeListener Interface//
////////////////////////////////
/** @overrides PageChangeListener.update() */
@Override
public void update() {
this.reformBlockCanvas(); // just repaint and it'll all look right again
}
/**
* @modifies every page in this blockcanvas as well as the canvas
* @effects resynchronize model and view, resize, reposition, and set color
* of EVERY page in the BlockCanvas. Then move very divider to
* the far right side of it's corresponding page. Note that
* reforming must perform ALL FIVE ACTIONS when invoked.
*/
public void reformBlockCanvas() {
int widthCounter = 0;
int maxHeight = 0;
for (int i = 0; i < pages.size(); i++) {
Page p = pages.get(i);
// compute maximum overall page height.
if (p.getMinimumPixelHeight()>maxHeight) {
maxHeight = p.getMinimumPixelHeight();
}
if (p.getDefaultPageColor() == null) {
if (i % 2 == 1) {
p.setPageColor(new Color(30, 30, 30));
} else {
p.setPageColor(new Color(40, 40, 40));
}
} else {
p.setPageColor(p.getDefaultPageColor());
}
widthCounter = widthCounter + p.reformBounds(widthCounter);
}
for (PageDivider d : dividers) {
d.setBounds(
d.getLeftPage().getJComponent().getX() + d.getLeftPage().getJComponent().getWidth() - 3,
0,
5,
d.getLeftPage().getJComponent().getHeight());
}
canvas.setPreferredSize(new Dimension(widthCounter, (int) (maxHeight * Page.getZoomLevel())));
scrollPane.revalidate();
scrollPane.repaint();
}
//////////////////////////////
//Saving and Loading //
//////////////////////////////
/**
* Returns an XML node describing all the blocks and pages within
* the BlockCanvas
* @return Node or {@code null} if there are no pages
*/
public Node getSaveNode(Document document) {
if (pages.size() > 0) {
// TODO ria just do BLOCKS, CHECK OUT HOW SAVING WILL BE LIKE WITH REFACTORING
Element pageElement = document.createElement("Pages");
if (Workspace.everyPageHasDrawer) {
pageElement.setAttribute("drawer-with-page", "yes");
}
pageElement.setAttribute("collapsible-pages", collapsible ? "yes" : "no");
for (Page page : pages) {
Node pageNode = page.getSaveNode(document);
pageElement.appendChild(pageNode);
}
return pageElement;
}
return null;
}
/**
* Loads all the RenderableBlocks and their associated Blocks that
* reside within the block canvas. All blocks will have their nessary
* data populated including connection information, stubs, etc.
* Note: This method should only be called if this language only uses the
* BlockCanvas to work with blocks and no pages. Otherwise, workspace live blocks
* are loaded from Pages.
* @param root the Document Element containing the desired information
*/
protected void loadSaveString(Element root) {
//Extract canvas blocks and load
//load pages, page drawers, and their blocks from save file
//PageDrawerManager.loadPagesAndDrawers(root);
PageDrawerLoadingUtils.loadPagesAndDrawers(workspace, root, workspace.getFactoryManager());
final NodeList pagesRoot = root.getElementsByTagName("Pages");
if (pagesRoot != null && pagesRoot.getLength() > 0) {
final Node pagesNode = pagesRoot.item(0);
if (pagesNode != null) {
collapsible = PageDrawerLoadingUtils.getBooleanValue(pagesNode, "collapsible-pages");
}
}
// FIXME: this UI code should not be here, fails unit tests that run in headless mode
// As a workaround, only execute if we have a UI
if (!GraphicsEnvironment.isHeadless()) {
int screenWidth = java.awt.Toolkit.getDefaultToolkit().getScreenSize().width;
int canvasWidth = canvas.getPreferredSize().width;
if (canvasWidth < screenWidth) {
Page p = pages.get(pages.size() - 1);
p.addPixelWidth(screenWidth - canvasWidth);
PageChangeEventManager.notifyListeners();
}
}
}
//////////////////////////////
//REDO/UNOD //
//////////////////////////////
/** @overrides ISupportMomento.getState */
public Object getState() {
Map<String, Object> pageStates = new HashMap<String, Object>();
for (Page page : pages) {
pageStates.put(page.getPageName(), page.getState());
}
return pageStates;
}
/** @overrides ISupportMomento.loadState() */
@SuppressWarnings("unchecked")
public void loadState(Object memento) {
assert (memento instanceof HashMap) : "ISupportMemento contract violated in BlockCanvas";
if (memento instanceof HashMap) {
Map<String, Object> pageStates = (HashMap<String, Object>) memento;
List<String> unloadedPages = new LinkedList<String>();
List<String> loadedPages = new LinkedList<String>();
for (String name : pageStates.keySet()) {
unloadedPages.add(name);
}
//First, load all the pages that are in the state to be loaded
//against all the pages that already exist.
for (Page existingPage : this.pages) {
String existingPageName = existingPage.getPageName();
if (pageStates.containsKey(existingPageName)) {
existingPage.loadState(pageStates.get(existingPageName));
unloadedPages.remove(existingPageName);
loadedPages.add(existingPageName);
}
}
//Now, remove all the pages that don't exist in the save state
for (Page existingPage : this.pages) {
String existingPageName = existingPage.getPageName();
if (!loadedPages.contains(existingPageName)) {
this.pages.remove(existingPage);
}
}
//Finally, add all the remaining pages that weren't there before
for (String newPageName : unloadedPages) {
Page newPage = new Page(workspace, newPageName);
newPage.loadState(pageStates.get(newPageName));
pages.add(newPage);
}
}
}
/**
* The graphical representation of the block canvas's Swng Container of pages.
* Note that this is not the graphical scrollable JComponent that represents
* the BlockCanvas.
*/
public class Canvas extends JLayeredPane implements MouseListener, MouseMotionListener {
private static final long serialVersionUID = 438974092314L;
private Point p;
public Canvas() {
super();
this.p = null;
this.addMouseListener(this);
this.addMouseMotionListener(this);
}
public void mousePressed(MouseEvent e) {
p = e.getPoint();
}
public void mouseClicked(MouseEvent e) {
if (SwingUtilities.isRightMouseButton(e) || e.isControlDown()) {
//pop up context menu
PopupMenu popup = ContextMenu.getContextMenuFor(BlockCanvas.this);
this.add(popup);
popup.show(this, e.getX(), e.getY());
}
}
public void mouseDragged(MouseEvent e) {
if (p == null) {
//do nothing
} else {
BoundedRangeModel hModel = scrollPane.getHorizontalModel();
BoundedRangeModel vModel = scrollPane.getVerticalModel();
hModel.setValue(hModel.getValue() + (p.x - e.getX()));
vModel.setValue(vModel.getValue() + (p.y - e.getY()));
}
}
public void mouseReleased(MouseEvent e) {
this.p = null;
}
public void mouseMoved(MouseEvent e) {
}
public void mouseEntered(MouseEvent e) {
}
public void mouseExited(MouseEvent e) {
}
}
}