package nl.utwente.viskell.ui.components; import com.google.common.base.Splitter; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import javafx.application.Platform; import javafx.beans.property.ReadOnlyDoubleProperty; import javafx.event.Event; import javafx.fxml.FXML; import javafx.geometry.BoundingBox; import javafx.geometry.Bounds; import javafx.scene.control.Label; import javafx.scene.control.TextInputDialog; import javafx.scene.input.MouseEvent; import javafx.scene.input.TouchEvent; 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.Expression; import nl.utwente.viskell.haskell.type.Type; import nl.utwente.viskell.ui.BlockContainer; import nl.utwente.viskell.ui.DragContext; import nl.utwente.viskell.ui.ToplevelPane; import java.util.*; /** Lambda block represent the externals of a lambda abstraction and can be named to become a local definition. */ public class LambdaBlock extends Block { /** the area which the lambda container body is in */ @FXML private Pane bodySpace; /** the area which the function anchor is in */ @FXML private Pane funSpace; /** The label with the explicit name this definition, if it has one. */ @FXML private Label definitionName; /** The label with the result type of this lambda. */ @FXML private Label signature; /** The optional explicit type signature of this block */ private Optional<Type> explicitSignature; /** The internal lambda within this definition block */ private LambdaContainer body; /** the arity of this block for serialization **/ private int arity; /** The function anchor (second bottom anchor) */ private PolyOutputAnchor fun; /** The draggable resizer in the bottom right corner */ private Pane resizer; private List<LocalDefUse> allDefinitionUsers; /** * Constructs a DefinitionBlock that is an untyped lambda of n arguments. * @param pane the parent ui pane. * @param arity the number of arguments of this lambda. */ public LambdaBlock(ToplevelPane pane, int arity) { super(pane); this.loadFXML("LambdaBlock"); this.arity = arity; this.signature.setText(""); this.explicitSignature = Optional.empty(); this.definitionName.setText(""); this.definitionName.setVisible(false); this.allDefinitionUsers = new ArrayList<>(); this.body = new LambdaContainer(this, arity); this.bodySpace.getChildren().add(this.body); this.fun = new PolyOutputAnchor(this, new Binder("lam")); this.funSpace.getChildren().add(this.fun); this.dragContext.setGoToForegroundOnContact(false); 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(resizer); this.resizer.relocate(300, 300); DragContext sizeDrag = new DragContext(this.resizer); sizeDrag.setDragLimits(new BoundingBox(200, 200, Integer.MAX_VALUE, Integer.MAX_VALUE)); // make the width of the name/signature to lower limit for the resizer ((Pane)this.signature.getParent()).widthProperty().addListener(e -> { double width = ((ReadOnlyDoubleProperty)e).doubleValue(); if (width > 200) { sizeDrag.setDragLimits(new BoundingBox(width, 200, Integer.MAX_VALUE, Integer.MAX_VALUE)); } if ((width-20) > this.resizer.getLayoutX()) { Platform.runLater(() -> this.resizer.relocate(width-20, this.resizer.getLayoutY())); } }); } /** @return a Map of class-specific properties of this Block. */ @Override protected Map<String, Object> toBundleFragment() { return ImmutableMap.of("arity", this.arity); } /** return a new instance of this Block type deserializing class-specific properties used in constructor **/ public static LambdaBlock fromBundleFragment(ToplevelPane pane, Map<String,Object> bundleFragment) { int arity = ((Double)bundleFragment.get("arity")).intValue(); return new LambdaBlock(pane, arity); } /** * Construct a function block performing this block's actions * @param event the event triggering this creation */ protected void createFunctionUseBlock(Event event) { ToplevelPane toplevel = this.getToplevel(); LocalDefUse funRef = new LocalDefUse(this); this.allDefinitionUsers.add(funRef); Block block = new FunApplyBlock(toplevel, funRef); toplevel.addBlock(block); Bounds bounds = this.getBoundsInParent(); block.relocate(bounds.getMinX()-20, bounds.getMaxY()+10); block.refreshContainer(); block.initiateConnectionChanges(); event.consume(); } public String getName() { return this.definitionName.getText(); } /** @return whether this is an unnamed lambda */ public boolean isTypedLambda() { return this.explicitSignature.isPresent(); } /** @return The output binder of this block */ public Binder getBinder() { return fun.binder; } @Override protected double computePrefWidth(double height) { this.body.setPrefWidth(this.resizer.getBoundsInParent().getMaxX()); return super.computePrefWidth(height); } @Override protected double computePrefHeight(double width) { this.body.setPrefHeight(this.resizer.getBoundsInParent().getMaxY() - 25); return super.computePrefHeight(width); } @Override public List<InputAnchor> getAllInputs() { return ImmutableList.of(); } @Override public List<OutputAnchor> getAllOutputs() { return ImmutableList.of(this.fun); } @Override public List<ConnectionAnchor> getAllAnchors() { List<ConnectionAnchor> result = new ArrayList<>(this.body.getAllAnchors()); result.add(this.fun); return result; } @Override public Optional<Block> getNewCopy() { // copying the internals is too complex for now return Optional.empty(); } public void removeUser(LocalDefUse user) { this.allDefinitionUsers.remove(user); } @Override public void refreshAnchorTypes() { // do typechecking internal connections first so that the lambda type is inferred body.handleConnectionChanges(false); fun.setExactRequiredType(explicitSignature.orElse(body.getLambdaType()).getFresh()); } public void handleConnectionChanges(boolean finalPhase) { // first propagate into the internals this.body.handleConnectionChanges(finalPhase); // also users of this function block need to be updated for (LocalDefUse user : this.allDefinitionUsers) { user.handleConnectionChanges(finalPhase); } // continue as normal with propagating changes on the outside super.handleConnectionChanges(finalPhase); } @Override public Expression getLocalExpr(Set<OutputAnchor> outsideAnchors) { return this.body.getLocalExpr(outsideAnchors); } @Override public void invalidateVisualState() { this.body.invalidateVisualState(); if (!this.explicitSignature.isPresent()) { this.signature.setText(this.fun.getStringType()); } } @Override public boolean belongsOnBottom() { return true; } public LambdaContainer getBody() { return body; } @Override public List<LambdaContainer> getInternalContainers() { return ImmutableList.of(this.body); } @Override public void relocate(double x, double y) { double dx = x-getLayoutX(), dy = y-getLayoutY(); super.relocate(x, y); body.moveNodes(dx, dy); } @Override public void deleteAllLinks() { for (LocalDefUse user : new ArrayList<>(this.allDefinitionUsers)) { user.onDefinitionRemoved(); } this.body.deleteAllLinks(); super.deleteAllLinks(); } protected void shiftAndGrow(double shiftX, double shiftY, double extraX, double extraY) { super.relocate(this.getLayoutX() + shiftX , this.getLayoutY() + shiftY); this.resizer.relocate(this.resizer.getLayoutX() + extraX, this.resizer.getLayoutY() + extraY); } public void resizeToFitAll() { if (this.body.getAttachedBlocks().count() == 0) { // resize to default double diffX = this.resizer.getLayoutX() - 300; double diffY = this.resizer.getLayoutY() - 300; this.shiftAndGrow(diffX/2, diffY/2, -diffX, -diffY); } else { // resize to the union of all contained blocks, taking in account minimum size and some margins Bounds current = this.getToplevel().sceneToLocal(this.body.localToScene(this.body.getBoundsInLocal())); Bounds union = this.body.getAttachedBlocks().map(b -> b.getBoundsInParent()).reduce(BlockContainer::union).get(); double shiftX = union.getMinX() - current.getMinX(); double shiftY = union.getMinY() - current.getMinY(); double extraX = union.getWidth() - current.getWidth(); double extraY = union.getHeight() - current.getHeight(); double marginX = Math.max(20, 200 - union.getWidth()); double marginY = Math.max(20, 200 - union.getHeight()); this.shiftAndGrow(shiftX - marginX/2 , shiftY - marginY/2 , extraX + marginX, extraY + marginY); } } @Override public boolean canAlterAnchors() { return !this.isTypedLambda(); } @Override public void alterAnchorCount(boolean isRemove) { if (isRemove) { this.body.removeLastInput(); } else { this.body.addExtraInput(); } } public void editSignature() { String input = this.definitionName.getText().isEmpty() ? "example" : this.definitionName.getText(); if (this.explicitSignature.isPresent()) { input += " :: " + this.explicitSignature.get().prettyPrint(); } TextInputDialog dialog = new TextInputDialog(input); dialog.setTitle("Edit lambda signature"); dialog.setHeaderText("Set the name and optionally the type"); Optional<String> result = dialog.showAndWait(); result.ifPresent(signature -> { if (! this.definitionName.isVisible()) { this.definitionName.addEventHandler(MouseEvent.MOUSE_RELEASED, this::createFunctionUseBlock); this.definitionName.addEventHandler(TouchEvent.TOUCH_RELEASED, this::createFunctionUseBlock); } List<String> parts = Splitter.on(" :: ").splitToList(signature); if (parts.size() < 2) { this.definitionName.setText(signature); this.definitionName.setVisible(true); } else { this.definitionName.setText(parts.get(0)); this.definitionName.setVisible(true); // FIXME: what to do in case type parsing fail? Type type = this.getToplevel().getEnvInstance().buildType(parts.get(1)); if (type.countArguments() >= this.body.argCount()) { this.explicitSignature = Optional.of(type); this.signature.setText(type.prettyPrint()); this.body.enforceExplicitType(type); this.initiateConnectionChanges(); } } }); } }