package nl.utwente.viskell.ui;
import com.google.common.collect.ImmutableMap;
import javafx.geometry.BoundingBox;
import javafx.geometry.Bounds;
import javafx.geometry.Point2D;
import javafx.scene.Node;
import javafx.scene.layout.Pane;
import javafx.scene.layout.Region;
import javafx.scene.shape.Path;
import javafx.scene.shape.Shape;
import nl.utwente.viskell.ghcj.GhciSession;
import nl.utwente.viskell.haskell.env.Environment;
import nl.utwente.viskell.ui.components.*;
import nl.utwente.viskell.ui.serialize.Bundleable;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* The core Pane that represent the programming workspace.
* It is a layered visualization of all blocks, wires, and menu elements.
* And represents the toplevel container of all blocks.
*/
public class ToplevelPane extends Region implements BlockContainer, Bundleable {
public static final String BLOCKS_SERIALIZED_NAME = "Blocks";
public static final String CONNECTIONS_SERIALIZED_NAME = "Connections";
/** bottom pane layer intended for block container such as lambda's */
private final Pane bottomLayer;
/** middle pane layer for ordinary blocks */
private final Pane blockLayer;
/** higher pane layer for connections wires */
private final Pane wireLayer;
private GhciSession ghci;
/** The set of blocks that logically belong to this top level */
private final Set<Block> attachedBlocks;
/**
* Constructs a new instance.
*/
public ToplevelPane(GhciSession ghci) {
super();
this.attachedBlocks = new HashSet<>();
this.bottomLayer = new Pane();
this.blockLayer = new Pane(this.bottomLayer);
this.wireLayer = new Pane(this.blockLayer);
this.getChildren().add(this.wireLayer);
this.ghci = ghci;
TouchContext context = new TouchContext(this, true);
context.setPanningAction((deltaX, deltaY) -> {
this.setTranslateX(this.getTranslateX() + deltaX);
this.setTranslateY(this.getTranslateY() + deltaY);
});
}
/** Shows a new function menu at the specified location in this pane. */
public void showFunctionMenuAt(double x, double y, boolean byMouse) {
FunctionMenu menu = new FunctionMenu(byMouse, ghci.getCatalog(), this);
double verticalCenter = 150; // just a guesstimate, because computing it here is annoying
Point2D scenePos = this.localToScene(x, y - verticalCenter);
// avoid opening menu above top edge of the window
Point2D newPos = this.sceneToLocal(scenePos.getX(), Math.max(0, scenePos.getY()));
menu.relocate(newPos.getX(), newPos.getY());
this.addMenu(menu);
}
/**
* @return The Env instance to be used within this CustomUIPane.
*/
public Environment getEnvInstance() {
return ghci.getCatalog().asEnvironment();
}
/** Remove the given block from this UI pane, including its connections. */
public void removeBlock(Block block) {
block.deleteAllLinks();
if (block.belongsOnBottom()) {
this.bottomLayer.getChildren().remove(block);
} else {
this.blockLayer.getChildren().remove(block);
}
}
/** Attempts to create a copy of a block and add it to this pane. */
public void copyBlock(Block block) {
block.getNewCopy().ifPresent(copy -> {
this.addBlock(copy);
copy.relocate(block.getLayoutX()+20, block.getLayoutY()+20);
copy.initiateConnectionChanges();
});
}
public GhciSession getGhciSession() {
return ghci;
}
/**
* Terminate the current GhciSession, if any, then start a new one.
* Waits for the old session to end, but not for the new session to start.
*/
public void restartBackend() {
ghci.stopAsync();
ghci.awaitTerminated();
ghci = new GhciSession();
ghci.startAsync();
}
public void addBlock(Block block) {
if (block.belongsOnBottom()) {
this.bottomLayer.getChildren().add(block);
} else {
this.blockLayer.getChildren().add(block);
}
}
public boolean addMenu(Pane menu) {
return this.getChildren().add(menu);
}
public boolean removeMenu(Pane menu) {
return this.getChildren().remove(menu);
}
public boolean addConnection(Connection connection) {
return this.wireLayer.getChildren().add(connection);
}
public boolean removeConnection(Connection connection) {
return this.wireLayer.getChildren().remove(connection);
}
public boolean addWire(DrawWire drawWire) {
return this.getChildren().add(drawWire);
}
public boolean removeWire(DrawWire drawWire) {
return this.getChildren().remove(drawWire);
}
public boolean addUpperTouchArea(Shape area) {
return this.getChildren().add(area);
}
public boolean removeUpperTouchArea(Shape area) {
return this.getChildren().remove(area);
}
public boolean addLowerTouchArea(Shape area) {
return this.bottomLayer.getChildren().add(area);
}
public boolean removeLowerTouchArea(Shape area) {
return this.bottomLayer.getChildren().remove(area);
}
public void clearChildren() {
this.bottomLayer.getChildren().clear();
this.blockLayer.getChildren().remove(1, this.blockLayer.getChildren().size());
this.wireLayer.getChildren().remove(1, this.wireLayer.getChildren().size());
this.attachedBlocks.clear();
}
public Stream<Node> streamChildren() {
Stream<Node> bottom = this.bottomLayer.getChildren().stream();
Stream<Node> blocks = this.blockLayer.getChildren().stream().skip(1);
Stream<Node> wires = this.wireLayer.getChildren().stream().skip(1);
return Stream.concat(bottom, Stream.concat(blocks, wires));
}
@Override
public Map<String, Object> toBundle() {
ImmutableMap.Builder<String, Object> bundle = ImmutableMap.builder();
Stream<Node> blocks = Stream.concat(this.bottomLayer.getChildren().stream(),
this.blockLayer.getChildren().stream());
bundle.put(BLOCKS_SERIALIZED_NAME, blocks
.filter(n -> n instanceof Bundleable)
.map(n -> ((Bundleable) n).toBundle())
.toArray());
bundle.put(CONNECTIONS_SERIALIZED_NAME, this.wireLayer.getChildren().stream()
.filter(n -> n instanceof Bundleable)
.map(n -> ((Bundleable) n).toBundle())
.toArray());
return bundle.build();
}
public void fromBundle(Map<String, Object> layers) {
if (layers != null) {
Map<Integer, Block> blockLookupTable = new HashMap<>();
List<Map<String, Object>> blocksBundle = (ArrayList<Map<String, Object>>) layers.get(BLOCKS_SERIALIZED_NAME);
if (blocksBundle != null) {
for (Map<String, Object> bundle : blocksBundle) {
Block block;
try {
block = Block.fromBundle(bundle, this, blockLookupTable);
addBlock(block);
} catch (Exception e) {
e.printStackTrace();
}
}
}
List<Map<String, Object>> connectionsBundle = (ArrayList<Map<String, Object>>) layers.get(CONNECTIONS_SERIALIZED_NAME);
if (connectionsBundle != null) {
for (Map<String, Object> bundle : connectionsBundle) {
try {
Connection.fromBundle(bundle, blockLookupTable);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
}
public Stream<BlockContainer> getAllBlockContainers() {
return bottomLayer.getChildrenUnmodifiable().stream().flatMap(node ->
(node instanceof Block) ? ((Block)node).getInternalContainers().stream() : Stream.empty());
}
/**
* Ensures that the ordering of container blocks on the bottom layer is consistent with parent ordering.
* @param block that might need corrections in the visual ordering.
*/
public void moveInFrontOfParentContainers(Block block) {
if (block.getContainer() instanceof WrappedContainer) {
Block parent = ((WrappedContainer)block.getContainer()).getWrapper();
int childIndex = this.bottomLayer.getChildren().indexOf(block);
int parentIndex = this.bottomLayer.getChildren().indexOf(parent);
if (childIndex < parentIndex && childIndex >= 0) {
this.bottomLayer.getChildren().remove(block);
this.bottomLayer.getChildren().add(parentIndex, block);
// moving the block after the parent might have caused ordering issues in the block inbetween, resolve them
new ArrayList<>(this.bottomLayer.getChildren().subList(childIndex, parentIndex - 1)).stream()
.filter(node -> node instanceof Block)
.forEach(node -> this.moveInFrontOfParentContainers((Block) node));
}
}
}
@Override
public Bounds containmentBoundsInScene() {
return this.localToScene(this.getBoundsInLocal());
}
/**
* @param pos the position to look around in coordinate system of this pane.
* @param distance the maximum 'nearby' distance.
*/
public List<ConnectionAnchor> allNearbyFreeAnchors(Point2D pos, double distance) {
ArrayList<ConnectionAnchor> anchors = new ArrayList<>();
Bounds testBounds = new BoundingBox(pos.getX()-distance, pos.getY()-distance, distance*2, distance*2);
for (Block nearBlock : this.streamChildren().filter(n -> n instanceof Block).map(n -> (Block)n).filter(b -> b.getBoundsInParent().intersects(testBounds)).collect(Collectors.toList())) {
for (ConnectionAnchor anchor : nearBlock.getAllAnchors()) {
Point2D anchorPos = anchor.getAttachmentPoint();
if (pos.distance(anchorPos) < distance && anchor.getWireInProgress() == null && !anchor.hasConnection()) {
anchors.add(anchor);
}
}
}
return anchors;
}
protected void cutIntersectingConnections(Shape cutter) {
new ArrayList<>(this.wireLayer.getChildren()).stream()
.filter(node -> node instanceof Connection).forEach(node -> {
Connection wire = (Connection) node;
if (((Path) Shape.intersect(wire, cutter)).getElements().size() > 0) {
wire.remove();
}
});
}
@Override
public void attachBlock(Block block) {
this.attachedBlocks.add(block);
}
@Override
public void detachBlock(Block block) {
this.attachedBlocks.remove(block);
}
@Override
public Stream<Block> getAttachedBlocks() {
return this.attachedBlocks.stream();
}
@Override
public BlockContainer getParentContainer() {
return this;
}
@Override
public Node asNode() {
return this;
}
@Override
public ToplevelPane getToplevel() {
return this;
}
@Override
public void expandToFit(Bounds bounds) {
// The toplevel is large enough to fit practical everything
}
/**
* Zooms the underlying main pane in/out with a ratio, up to reasonable limits.
* @param ratio the additional zoom factor to apply.
*/
protected void zoom(double ratio) {
double scale = getScaleX();
/* Limit zoom to reasonable range. */
if (scale <= 0.2 && ratio < 1) return;
if (scale >= 3 && ratio > 1) return;
setScaleX(scale * ratio);
setScaleY(scale * ratio);
setTranslateX(getTranslateX() * ratio);
setTranslateY(getTranslateY() * ratio);
}
}