package nl.utwente.viskell.ui.components; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Optional; import java.util.Set; import java.util.stream.Stream; import javafx.fxml.FXML; import javafx.geometry.BoundingBox; import javafx.geometry.Bounds; import javafx.scene.Node; import javafx.scene.layout.BorderPane; import javafx.scene.layout.Pane; import javafx.scene.paint.Color; import javafx.scene.shape.Polygon; import nl.utwente.viskell.haskell.expr.Binder; import nl.utwente.viskell.haskell.expr.Case; import nl.utwente.viskell.haskell.expr.ConstantBinder; import nl.utwente.viskell.haskell.expr.ConstructorBinder; import nl.utwente.viskell.haskell.expr.LetExpression; import nl.utwente.viskell.haskell.type.TypeScope; import nl.utwente.viskell.ui.BlockContainer; import nl.utwente.viskell.ui.ComponentLoader; import nl.utwente.viskell.ui.DragContext; import nl.utwente.viskell.ui.TouchContext; /** * A single alternative within a ChoiceBlock * */ public class Lane extends BorderPane implements WrappedContainer, ComponentLoader { /** The argument anchors of this alternative */ protected List<BinderAnchor> arguments; /** The result anchor of this alternative */ protected ResultAnchor result; /** Whether the anchor types are fresh*/ protected boolean freshAnchorTypes; /** Status of change updating process in this block. */ protected boolean firstPhaseInProgress; /** Status of change updating process in this block. */ protected boolean finalPhaseInProgress; /** The wrapper to which this alternative belongs */ protected ChoiceBlock parent; /** The draggable resizer in the bottom right corner */ private Pane resizer; /** The container Node for binder anchors */ @FXML protected Pane argSpace; /** The resizable area on which blocks can be placed */ @FXML protected Pane guardSpace; /** The container Node for result anchors */ @FXML protected Pane resultSpace; /** A set of blocks that belong to this container */ protected Set<Block> attachedBlocks; public Lane(ChoiceBlock wrapper) { super(); loadFXML("Lane"); parent = wrapper; arguments = new ArrayList<>(); result = new ResultAnchor(this, wrapper, Optional.empty()); attachedBlocks = new HashSet<>(); resultSpace.getChildren().add(result); setupResizer(); TouchContext context = new TouchContext(this, false); context.setPanningAction((deltaX, deltaY) -> { if (! this.parent.isActivated()) { this.parent.relocate(this.parent.getLayoutX() + deltaX, this.parent.getLayoutY() + deltaY); } }); } /** * Returns the pattern and guard expression for this lane as well as a set of required blocks. * * Firstly, the expression is generated from the result anchor. * Secondly, the expression is extended with bottom-most blocks within this lane. * Bottom-most blocks are *assumed* to be either deconstructors or expressions resulting in a Bool. * * @return this lane's Alternative */ public Case.Alternative getAlternative(Set<OutputAnchor> outsideAnchors) { LetExpression guards = new LetExpression(result.getLocalExpr(outsideAnchors), true); result.extendExprGraph(guards, this, outsideAnchors); attachedBlocks.stream().filter(Block::isBottomMost).forEach(block -> { if (block instanceof MatchBlock) { guards.addLetBinding(((MatchBlock)block).getPrimaryBinder(), block.getAllInputs().get(0).getFullExpr()); } else if (block instanceof ConstantMatchBlock) { guards.addLetBinding(new ConstantBinder(((ConstantMatchBlock)block).getValue()), block.getAllInputs().get(0).getFullExpr()); } else if (block instanceof SplitterBlock) { guards.addLetBinding(((SplitterBlock)block).getPrimaryBinder(), block.getAllInputs().get(0).getFullExpr()); } else { block.getAllOutputs().forEach(anchor -> { if (anchor.getStringType().equals("Bool")) { guards.addLetBinding(new ConstructorBinder("True"), anchor.getVariable()); anchor.extendExprGraph(guards, this, outsideAnchors); } }); } block.extendExprGraph(guards, this, outsideAnchors); }); /** * Iterate over the container until it doesn't find new nodes. */ boolean cont = true; for (int numAnchors = -1; numAnchors != outsideAnchors.size() || cont; numAnchors = outsideAnchors.size()) { for (OutputAnchor anchor : new ArrayList<>(outsideAnchors)) { if (anchor.getContainer() == this) { anchor.extendExprGraph(guards, this, outsideAnchors); } else { outsideAnchors.add(anchor); } } cont = (numAnchors != outsideAnchors.size()); } return (new Case.Alternative(new ConstructorBinder("()"), guards)); } /** Returns the result anchor of this Lane */ public ResultAnchor getOutput() { return result; } public List<ConnectionAnchor> getAllAnchors() { List<ConnectionAnchor> anchors = new ArrayList<>(this.arguments); anchors.add(this.result); return anchors; } @Override public void refreshAnchorTypes() { // refresh anchor types only once at the start of the typechecking process if (!firstPhaseInProgress && !freshAnchorTypes) { freshAnchorTypes = true; TypeScope scope = new TypeScope(); arguments.forEach(argument -> argument.refreshType(scope)); result.refreshAnchorType(scope); } } @Override public final void handleConnectionChanges(boolean finalPhase) { // avoid doing extra work and infinite recursion if ((!finalPhase && !firstPhaseInProgress) || (finalPhase && !finalPhaseInProgress)) { if (!finalPhase) { // in first phase ensure that anchor types are refreshed if propagating from the outside refreshAnchorTypes(); firstPhaseInProgress = true; finalPhaseInProgress = false; } else { firstPhaseInProgress = false; finalPhaseInProgress = true; } freshAnchorTypes = false; // first propagate up from the result anchor result.getConnection().ifPresent(c -> c.handleConnectionChangesUpwards(finalPhase)); // also propagate in from above in case the lane is partially connected arguments.forEach(argument -> { argument.getOppositeAnchors().forEach(anchor -> { anchor.handleConnectionChanges(finalPhase); // take the type of argument connections into account even if the connected block is being processed anchor.getConnection().ifPresent(connection -> connection.handleConnectionChangesUpwards(finalPhase)); }); }); // propagate internal type changes outwards parent.handleConnectionChanges(finalPhase); } } /** Called when the VisualState changed. */ public void invalidateVisualState() { arguments.forEach(BinderAnchor::invalidateVisualState); result.invalidateVisualState(); } /** Add and initializes a resizer element to this block */ private void setupResizer() { Polygon triangle = new Polygon(); triangle.getPoints().addAll(new Double[]{20.0, 20.0, 20.0, 0.0, 0.0, 20.0}); triangle.setFill(Color.BLUE); this.resizer = new Pane(triangle); triangle.setLayoutX(10); triangle.setLayoutY(10); this.resizer.setManaged(false); this.getChildren().add(this.resizer); this.resizer.relocate(240-20, 320-20); DragContext sizeDrag = new DragContext(this.resizer); sizeDrag.setDragLimits(new BoundingBox(200, 200, Integer.MAX_VALUE, Integer.MAX_VALUE)); } /** Adds extra input binder anchor to this lane */ public void addExtraInput() { BinderAnchor arg = new BinderAnchor(this, getWrapper(), new Binder("a_" + arguments.size())); arguments.add(arg); argSpace.getChildren().add(arg); } /** Removes the last input binder anchor of this lane */ public void removeLastInput() { BinderAnchor arg = arguments.remove(arguments.size()-1); arg.removeConnections(); argSpace.getChildren().remove(arg); } @Override public ChoiceBlock getWrapper() { return this.parent; } @Override public void attachBlock(Block block) { attachedBlocks.add(block); } @Override public void detachBlock(Block block) { attachedBlocks.remove(block); } @Override public Stream<Block> getAttachedBlocks() { return this.attachedBlocks.stream(); } @Override public BlockContainer getParentContainer() { return parent.getContainer(); } @Override public Node asNode() { return this; } @Override protected double computePrefWidth(double height) { guardSpace.setPrefWidth(resizer.getBoundsInParent().getMaxX()-5); return super.computePrefWidth(height); } @Override protected double computePrefHeight(double width) { double resizerY = resizer.getLayoutY(); parent.getLanes().stream().filter(lane -> lane.resizer.getLayoutY() != resizerY).forEach(lane -> lane.resizer.setLayoutY(resizerY)); guardSpace.setPrefHeight(resizer.getBoundsInParent().getMaxY()); return super.computePrefHeight(width); } @Override public Bounds containmentBoundsInScene() { Bounds local = this.getBoundsInLocal(); // include border area around this lane BoundingBox withBorders = new BoundingBox(local.getMinX()-10, local.getMinY()-25, local.getWidth()+20, local.getHeight()+50); return this.localToScene(withBorders); } @Override public void deleteAllLinks() { this.arguments.forEach(OutputAnchor::removeConnections); this.result.removeConnections(); new ArrayList<>(this.attachedBlocks).forEach(block -> block.moveIntoContainer(this.getParentContainer())); } @Override public void expandToFit(Bounds blockBounds) { Bounds containerBounds = this.parent.getToplevel().sceneToLocal(this.getCenter().localToScene(this.getCenter().getBoundsInLocal())); double shiftX = Math.min(0, blockBounds.getMinX() - containerBounds.getMinX()); double shiftY = Math.min(0, blockBounds.getMinY() - containerBounds.getMinY()); double extraX = Math.max(0, blockBounds.getMaxX() - containerBounds.getMaxX()) + Math.abs(shiftX); double extraY = Math.max(0, blockBounds.getMaxY() - containerBounds.getMaxY()) + Math.abs(shiftY); this.resizer.relocate(this.resizer.getLayoutX() + extraX, this.resizer.getLayoutY() + extraY); double shiftXForRights = extraX + shiftX; this.parent.shiftAllBut(shiftX, shiftY, this, shiftXForRights); // also resize its parent in case of nested containers Bounds fitBounds = this.parent.getBoundsInParent(); this.getParentContainer().expandToFit(new BoundingBox(fitBounds.getMinX()-10, fitBounds.getMinY()-10, fitBounds.getWidth()+20, fitBounds.getHeight()+20)); } }