package nl.utwente.viskell.ui.components; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import javafx.beans.binding.Bindings; import javafx.fxml.FXML; import javafx.geometry.BoundingBox; import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.scene.Node; import javafx.scene.control.Label; import javafx.scene.layout.*; import nl.utwente.viskell.haskell.expr.*; import nl.utwente.viskell.haskell.type.FunType; import nl.utwente.viskell.haskell.type.Type; import nl.utwente.viskell.haskell.type.TypeScope; import nl.utwente.viskell.ui.DragContext; import nl.utwente.viskell.ui.ToplevelPane; import nl.utwente.viskell.ui.serialize.Bundleable; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.*; import java.util.stream.Collectors; public class FunApplyBlock extends Block { /** Function input anchor that can be dragged down for currying. */ private class FunInputAnchor extends Pane implements ConnectionAnchor.Target { /** The connection anchor for this input argument. */ private final InputAnchor anchor; /** The input type label of this anchor. */ private final Label inputType; /** Label with function arrow when in curried state. */ private final Label curryArrow; /** The draggable pane combining both type label and curry arrow */ private final Pane typePane; /** Whether this input argument is in curried state. */ private boolean curried; /** The drag handler for this. */ private final DragContext dragContext; public FunInputAnchor() { this.curried = false; this.inputType = new Label("....."); this.inputType.setMinWidth(USE_PREF_SIZE); this.inputType.getStyleClass().add("inputType"); this.curryArrow = new Label("->"); this.curryArrow.setMinWidth(USE_PREF_SIZE); this.curryArrow.getStyleClass().add("curryArrow"); this.curryArrow.setVisible(false); this.curryArrow.setMouseTransparent(true); this.typePane = new HBox(this.inputType, this.curryArrow) { @Override public void relocate(double x, double y) { super.relocate(x, y); FunInputAnchor.this.dragShift(y); } }; this.typePane.setMinWidth(USE_PREF_SIZE); this.typePane.setPickOnBounds(false); this.anchor = new InputAnchor(FunApplyBlock.this); this.anchor.layoutXProperty().bind(this.inputType.widthProperty().divide(2)); this.getChildren().addAll(this.anchor, this.typePane); this.setTranslateY(-9); this.setPickOnBounds(false); dragContext = new DragContext(this.typePane); dragContext.setDragInitAction(c -> {this.curried = false;}); dragContext.setDragFinishAction(c -> { double height = this.inputType.getHeight(); boolean mostlyDown = (this.typePane.getLayoutY() > height) && !this.anchor.hasConnection(); double newY = mostlyDown ? 1.5*height : 0; this.typePane.relocate(0, newY); this.curried = mostlyDown; FunApplyBlock.this.dragShiftOuput(newY - height); FunApplyBlock.this.initiateConnectionChanges(); if (curried && this.anchor.getWireInProgress() != null) { this.anchor.getWireInProgress().remove(); } }); } @Override public ConnectionAnchor getAssociatedAnchor() { return this.anchor; } @Override public double computePrefHeight(double width) { double height = this.inputType.prefHeight(width); this.dragContext.setDragLimits(new BoundingBox(0, 0, 0, height*1.5)); return height; } /** Shift related things down to some distance. */ private void dragShift(double y) { double height = this.inputType.getHeight(); this.anchor.setLayoutY(y); this.anchor.setOpacity(1 - (y/(height))); this.anchor.setVisible(y < height); this.curryArrow.setManaged(y > height || Iterables.getLast(FunApplyBlock.this.inputs) != this); this.curryArrow.setVisible(y > height); FunApplyBlock.this.dragShiftOuput(y - height); } /** Refresh visual information such as types */ public void invalidateVisualState() { this.anchor.invalidateVisualState(); boolean validConnection = this.anchor.hasValidConnection(); this.setTranslateY(validConnection ? 0 : -9); this.inputType.setText(validConnection ? "zyxwv" : this.anchor.getStringType()); this.typePane.setVisible(!validConnection); } } /** The information about the function. */ private FunctionReference funRef; private List<FunInputAnchor> inputs; /** Text label for the output type */ private final Label resTypeLabel; /** The uncurried output type */ private Type resType; /** The result anchor of this function. */ private final OutputAnchor output; /** The background for curriedType labels */ private final Pane curriedOutput; /** The space containing anchors and type labels. */ @FXML private Pane bodySpace; private final Pane inputSpace; private static final Map<String, String> functionReferenceClassMap; static { Map<String, String> aMap = new HashMap<>(); aMap.put(LocalDefUse.class.getSimpleName(), LocalDefUse.class.getName()); aMap.put(LibraryFunUse.class.getSimpleName(), LibraryFunUse.class.getName()); aMap.put(ApplyAnchor.class.getSimpleName(), ApplyAnchor.class.getName()); functionReferenceClassMap = Collections.unmodifiableMap(aMap); } public FunApplyBlock(ToplevelPane pane, FunctionReference funRef) { super(pane); this.loadFXML("FunApplyBlock"); this.funRef = funRef; this.funRef.initializeBlock(this); ((HBox)this.bodySpace.getParent()).getChildren().add(0, funRef.asRegion()); this.inputs = new ArrayList<>(); this.output = new OutputAnchor(this, new Binder("res")); this.resTypeLabel = new Label(""); this.resTypeLabel.setMinWidth(USE_PREF_SIZE); this.resTypeLabel.getStyleClass().add("resultType"); VBox outputSpace = new VBox(this.resTypeLabel, this.output); outputSpace.setAlignment(Pos.CENTER); outputSpace.setPickOnBounds(false); Type t = funRef.refreshedType(funRef.requiredArguments(), new TypeScope()); while (t instanceof FunType) { FunType ft = (FunType) t; FunInputAnchor ia = new FunInputAnchor(); this.inputs.add(ia); t = ft.getResult(); } if (! this.inputs.isEmpty()) { Iterables.getLast(FunApplyBlock.this.inputs).curryArrow.setManaged(false); } this.inputSpace = new HBox(5, this.inputs.toArray(new Node[this.inputs.size()])); this.inputSpace.setPickOnBounds(false); this.curriedOutput = new Pane() { @Override public double computePrefWidth(double height) { return Math.max(inputSpace.prefWidth(height), outputSpace.getLayoutX()+resTypeLabel.prefWidth(height)); } @Override public double computePrefHeight(double width) { return resTypeLabel.prefHeight(width)*2; } }; this.curriedOutput.setVisible(false); this.curriedOutput.getStyleClass().add("curriedOutput"); this.curriedOutput.setBorder(new Border(new BorderStroke(null, BorderStrokeStyle.SOLID, null, null, new Insets(-1)))); this.bodySpace.getChildren().addAll(this.curriedOutput, this.inputSpace, outputSpace); outputSpace.layoutYProperty().bind(this.inputSpace.heightProperty()); outputSpace.layoutXProperty().bind(Bindings.max(0, this.inputSpace.widthProperty().divide(2).subtract(resTypeLabel.widthProperty().divide(2)))); outputSpace.setTranslateY(9); this.curriedOutput.translateYProperty().bind(outputSpace.translateYProperty()); } /** @return a Map of class-specific properties of this Block. */ @Override protected Map<String, Object> toBundleFragment() { Map<String, Object> bundleFragment = ImmutableMap.of( "curriedArgs", this.inputs.stream().map(i -> i.curried).toArray(), "funRef", this.funRef.toBundleFragment() ); return bundleFragment; } /** return a new instance of this Block type deserializing class-specific properties used in constructor **/ public static FunApplyBlock fromBundleFragment(ToplevelPane pane, Map<String,Object> bundleFragment) throws ClassNotFoundException, InvocationTargetException, IllegalAccessException, NoSuchMethodException { Map<String, Object> funRefBundle = (Map<String, Object>)bundleFragment.get("funRef"); String funcReferenceClassName = functionReferenceClassMap.get(funRefBundle.get(Bundleable.KIND)); Class<?> clazz = Class.forName(funcReferenceClassName); // Find the static "fromBundleFragment" method for the named type and call it Method fromBundleMethod = clazz.getDeclaredMethod("fromBundleFragment", Map.class); FunctionReference functionReference = (FunctionReference)fromBundleMethod.invoke(null, funRefBundle); return new FunApplyBlock(pane, functionReference); } /** Shift the output type/anchor down to some distance. */ private void dragShiftOuput(double y) { double shift = Math.max(0, y); if (this.inputs.stream().map(i -> i.curried).filter(c -> c).count() == 0) { this.output.getParent().setTranslateY(9 + shift); this.curriedOutput.setVisible(false); this.curriedOutput.setManaged(false); } else { this.curriedOutput.setManaged(true); this.curriedOutput.setVisible(true); this.curriedOutput.requestLayout(); } } public FunctionReference getFunReference() { return this.funRef; } public void convertToOpenApply(ApplyAnchor apply) { this.funRef.deleteLinks(); this.funRef = apply; apply.initializeBlock(this); ((HBox)this.bodySpace.getParent()).getChildren().set(0, funRef.asRegion()); this.initiateConnectionChanges(); } @Override public List<InputAnchor> getAllInputs() { List<InputAnchor> res = new ArrayList<>(); this.inputs.stream().map(i -> i.anchor).collect(Collectors.toCollection(() -> res)); this.funRef.getInputAnchor().ifPresent(fia -> res.add(0, fia)); return res; } @Override public List<OutputAnchor> getAllOutputs() { return ImmutableList.of(this.output); } @Override public Optional<Block> getNewCopy() { if (this.inputs.stream().map(i -> i.curried).filter(c -> c).count() == 0) { return Optional.of(new FunApplyBlock(this.getToplevel(), this.funRef.getNewCopy())); } return Optional.empty(); } @Override protected void refreshAnchorTypes() { TypeScope scope = new TypeScope(); Type type = this.funRef.refreshedType(this.inputs.size(), scope); for (FunInputAnchor arg : this.inputs) { if (type instanceof FunType) { FunType ftype = (FunType)type; arg.anchor.setFreshRequiredType(ftype.getArgument(), scope); type = ftype.getResult(); } else { new RuntimeException("too many arguments in this functionblock " + funRef.getName()); } } this.resType = type.getFresh(scope); Type curriedType = this.resType; for (FunInputAnchor arg : Lists.reverse(this.inputs)) { if (arg.curried) { curriedType = new FunType (arg.anchor.getType(), curriedType); } } this.output.setExactRequiredType(curriedType); } @Override public Expression getLocalExpr(Set<OutputAnchor> outsideAnchors) { Expression expr = this.funRef.getLocalExpr(outsideAnchors); List<Binder> curriedArgs = new ArrayList<>(); for (FunInputAnchor arg : this.inputs) { if (arg.curried) { Binder ca = new Binder("ca"); curriedArgs.add(ca); expr = new Apply(expr, new LocalVar(ca)); } else { expr = new Apply(expr, arg.anchor.getLocalExpr(outsideAnchors)); } } if (curriedArgs.isEmpty()) { return expr; } else { return new Lambda(curriedArgs, expr); } } @Override public void invalidateVisualState() { this.funRef.invalidateVisualState(); this.resTypeLabel.setText(this.resType.prettyPrint()); this.output.invalidateVisualState(); for (FunInputAnchor arg : this.inputs) { arg.invalidateVisualState(); } } @Override public boolean checkValidInCurrentContainer() { return this.funRef.isScopeCorrectIn(this.container) && super.checkValidInCurrentContainer(); } @Override public void deleteAllLinks() { this.funRef.deleteLinks(); super.deleteAllLinks(); } @Override public boolean canAlterAnchors() { return this.funRef instanceof ApplyAnchor; } @Override public void alterAnchorCount(boolean isRemove) { if (this.funRef instanceof ApplyAnchor) { ApplyAnchor apply = (ApplyAnchor)this.funRef; if (apply.hasConnection()) { if (isRemove && apply.getType().countArguments() <= this.inputs.size()) { return; } if (!isRemove && apply.getType().countArguments() >= this.inputs.size()) { return; } } if (isRemove) { FunInputAnchor removed = this.inputs.remove(this.inputs.size()-1); this.inputSpace.getChildren().remove(removed); } else { FunInputAnchor extra = new FunInputAnchor(); this.inputs.add(extra); this.inputSpace.getChildren().add(extra); } this.initiateConnectionChanges(); } } @Override public String toString() { return funRef.getName(); } }