package nl.utwente.viskell.ui.components;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import javafx.application.Platform;
import javafx.geometry.BoundingBox;
import javafx.geometry.Bounds;
import javafx.geometry.Point2D;
import javafx.scene.Node;
import javafx.scene.layout.StackPane;
import nl.utwente.viskell.haskell.expr.Expression;
import nl.utwente.viskell.haskell.expr.LetExpression;
import nl.utwente.viskell.ui.*;
import nl.utwente.viskell.ui.serialize.Bundleable;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.*;
import java.util.function.Predicate;
/**
* Base block shaped UI Component that other visual elements will extend from.
* Blocks can add input and output values by implementing the InputBlock and
* OutputBlock interfaces.
* <p>
* MouseEvents are used for setting the selection state of a block, single
* clicks can toggle the state to selected. When a block has already been
* selected and receives another single left click it will toggle a contextual
* menu for the block.
* </p>
* <p>
* Each block implementation should also feature it's own FXML implementation.
* </p>
*/
public abstract class Block extends StackPane implements Bundleable, ComponentLoader {
private static final String BLOCK_ID_PARAMETER = "id";
private static final String BLOCK_X_PARAMETER = "x";
private static final String BLOCK_Y_PARAMETER = "y";
private static final String BLOCK_PROPERTIES_PARAMETER = "properties";
/** The pane that is used to hold state and place all components on. */
private final ToplevelPane toplevel;
/** The context that deals with dragging and touch event for this Block */
protected DragContext dragContext;
/** Whether the anchor types are fresh*/
private boolean freshAnchorTypes;
/** Status of change updating process in this block. */
private boolean updateInProgress;
/** The container to which this Block currently belongs */
protected BlockContainer container;
/** Whether this block has a meaningful interpretation the current container context. */
protected boolean inValidContext;
/**
* In order to serialize using simple class names we need some way to map the simple class
* name to the full class names. This is one that should survive automatic refactoring of classes
* into different packages - but won't survive class renaming
*/
private static final Map<String, String> blockClassMap;
static {
Map<String, String> aMap = new HashMap<>();
aMap.put(JoinerBlock.class.getSimpleName(), JoinerBlock.class.getName());
aMap.put(LambdaBlock.class.getSimpleName(), LambdaBlock.class.getName());
aMap.put(ConstantMatchBlock.class.getSimpleName(), ConstantMatchBlock.class.getName());
aMap.put(ChoiceBlock.class.getSimpleName(), ChoiceBlock.class.getName());
aMap.put(LiftingBlock.class.getSimpleName(), LiftingBlock.class.getName());
aMap.put(MatchBlock.class.getSimpleName(), MatchBlock.class.getName());
aMap.put(GraphBlock.class.getSimpleName(), GraphBlock.class.getName());
aMap.put(FunApplyBlock.class.getSimpleName(), FunApplyBlock.class.getName());
aMap.put(BinOpApplyBlock.class.getSimpleName(), BinOpApplyBlock.class.getName());
aMap.put(SimulateBlock.class.getSimpleName(), SimulateBlock.class.getName());
aMap.put(DisplayBlock.class.getSimpleName(), DisplayBlock.class.getName());
aMap.put(SplitterBlock.class.getSimpleName(), SplitterBlock.class.getName());
aMap.put(ArbitraryBlock.class.getSimpleName(), ArbitraryBlock.class.getName());
aMap.put(SliderBlock.class.getSimpleName(), SliderBlock.class.getName());
aMap.put(ConstantBlock.class.getSimpleName(), ConstantBlock.class.getName());
blockClassMap = Collections.unmodifiableMap(aMap);
}
/**
* @param pane The pane this block belongs to.
*/
public Block(ToplevelPane pane) {
this.toplevel = pane;
this.freshAnchorTypes = false;
this.updateInProgress = false;
this.container = pane;
this.container.attachBlock(this);
this.inValidContext = true;
// only the actual shape should be selected for events, not the larger outside bounds
this.setPickOnBounds(false);
if (! this.belongsOnBottom()) {
// make all non container blocks resize themselves around horizontal midpoint to reduce visual movement
this.translateXProperty().bind(this.widthProperty().divide(2).negate());
}
this.dragContext = new DragContext(this);
this.dragContext.setSecondaryClickAction((p, byMouse) -> CircleMenu.showFor(this, this.localToParent(p), byMouse));
this.dragContext.setDragFinishAction(event -> refreshContainer());
this.dragContext.setContactAction(x -> this.getStyleClass().add("activated"));
this.dragContext.setReleaseAction(x -> this.getStyleClass().removeAll("activated"));
}
/** @return the parent CustomUIPane of this component. */
public final ToplevelPane getToplevel() {
return this.toplevel;
}
public boolean isActivated() {
return this.dragContext.isActivated();
}
/**
* @return All InputAnchors of the block.
*/
public abstract List<InputAnchor> getAllInputs();
/**
* @return All OutputAnchors of the Block.
*/
public abstract List<OutputAnchor> getAllOutputs();
public List<ConnectionAnchor> getAllAnchors() {
List<ConnectionAnchor> result = new ArrayList<>(this.getAllInputs());
result.addAll(this.getAllOutputs());
return result;
}
/** @return true if no connected output anchor exist */
public boolean isBottomMost() {
for (OutputAnchor anchor : getAllOutputs()) {
if (anchor.hasConnection()) {
return false;
}
}
return true;
}
/**
* Starts a new (2 phase) change propagation process from this block.
*/
public final void initiateConnectionChanges() {
this.handleConnectionChanges(false);
this.handleConnectionChanges(true);
}
/**
* Connection change preparation; set fresh types in all anchors.
*/
public final void prepareConnectionChanges() {
if (this.updateInProgress || this.freshAnchorTypes) {
return; // refresh anchor types in each block only once
}
this.freshAnchorTypes = true;
this.refreshAnchorTypes();
this.inValidContext = this.checkValidInCurrentContainer();
if (this.inValidContext) {
this.getStyleClass().removeAll("invalid");
} else {
this.getStyleClass().removeAll("invalid");
this.getStyleClass().add("invalid");
}
}
/**
* Set fresh types in all anchors of this block for the next typechecking cycle.
*/
protected abstract void refreshAnchorTypes();
/**
* Handle the expression and types changes caused by modified connections or values.
* Propagate the changes through connected blocks, and if final phase trigger a visual update.
* @param finalPhase whether the change propagation is in the second (final) phase.
*/
public void handleConnectionChanges(boolean finalPhase) {
if (this.updateInProgress != finalPhase) {
return; // avoid doing extra work and infinite recursion
}
if (! finalPhase) {
// in first phase ensure that anchor types are refreshed
this.prepareConnectionChanges();
}
this.updateInProgress = !finalPhase;
this.freshAnchorTypes = false;
// First make sure that all connected inputs will be updated too.
for (InputAnchor input : this.getAllInputs()) {
input.getConnection().ifPresent(c -> c.handleConnectionChangesUpwards(finalPhase));
}
// propagate changes down from the output anchor to connected inputs
this.getAllOutputs().stream().forEach(output -> output.getOppositeAnchors().forEach(input -> input.handleConnectionChanges(finalPhase)));
// propagate changes to the outside of a choiceblock
if (container instanceof Lane) {
((Lane)container).handleConnectionChanges(finalPhase);
}
if (finalPhase) {
// Now that the expressions and types are fully updated, initiate a visual refresh.
Platform.runLater(this::invalidateVisualState);
}
}
/**
* @param outsideAnchors the set being accumulated of out-of-reach OutputAnchors that are required for the expression.
* @return The expression this block represents.
*/
public abstract Expression getLocalExpr(Set<OutputAnchor> outsideAnchors);
/**
* This method is only used for the inspector window.
* @return A complete expression of this block and all its dependencies.
*/
public final Expression getFullExpr() {
Set<OutputAnchor> outerAnchors = new HashSet<>();
Expression localExpr = getLocalExpr(outerAnchors);
LetExpression fullExpr = new LetExpression(localExpr, false);
extendExprGraph(fullExpr, this.toplevel, outerAnchors);
outerAnchors.forEach(block -> block.extendExprGraph(fullExpr, this.toplevel, new HashSet<>()));
return fullExpr;
}
/**
* Extends the expression graph to include all subexpression required
* @param exprGraph the let expression representing the current expression graph
* @param container the container to which this expression graph is constrained
* @param outsideAnchors a mutable set of required OutputAnchors from a surrounding container
*/
protected void extendExprGraph(LetExpression exprGraph, BlockContainer container, Set<OutputAnchor> outsideAnchors) {
for (InputAnchor input : this.getAllInputs()) {
input.extendExprGraph(exprGraph, container, outsideAnchors);
}
}
/** Called when the VisualState changed. */
public abstract void invalidateVisualState();
/** @return whether this block is visually shown below common blocks (is constant per instance). */
public boolean belongsOnBottom() {
return false;
}
/** @return class-specific properties of this Block. */
protected Map<String, Object> toBundleFragment() {
return ImmutableMap.of();
}
/** @return the container to which this block belongs */
public BlockContainer getContainer() {
return container;
}
/** @return the list of internal containers wrapped inside this block */
public List<? extends WrappedContainer> getInternalContainers() {
return ImmutableList.of();
}
/** @return an independent copy of this Block, or Optional.empty if the internal state is too complex too copy. */
public abstract Optional<Block> getNewCopy();
/** Remove all associations of this block with others in preparation of removal, including all connections */
public void deleteAllLinks() {
this.getAllInputs().forEach(InputAnchor::removeConnections);
this.getAllOutputs().forEach(OutputAnchor::removeConnections);
this.container.detachBlock(this);
this.container = TrashContainer.instance;
}
public void moveIntoContainer(BlockContainer target) {
BlockContainer source = this.container;
if (source != target) {
this.container.detachBlock(this);
this.container = target;
target.attachBlock(this);
if (this.getInternalContainers().size() > 0) {
this.toplevel.moveInFrontOfParentContainers(this);
}
if (source instanceof WrappedContainer) {
((WrappedContainer)source).handleConnectionChanges(false);
((WrappedContainer)source).handleConnectionChanges(true);
}
if (target instanceof WrappedContainer) {
((WrappedContainer)target).handleConnectionChanges(false);
((WrappedContainer)target).handleConnectionChanges(true);
}
this.initiateConnectionChanges();
}
}
/** @return the bounds of this block in scene coordinates, excluding the parts sticking out such as anchors. */
public Bounds getBodyBounds() {
Node body = this.getChildren().get(0);
return body.localToScene(body.getLayoutBounds());
}
/** Scans for and attaches to a new container, if any */
public void refreshContainer() {
Bounds myBounds = this.getBodyBounds();
Point2D center = new Point2D((myBounds.getMinX()+myBounds.getMaxX())/2, (myBounds.getMinY()+myBounds.getMaxY())/2);
List<Point2D> corners = ImmutableList.of(
new Point2D(myBounds.getMinX(), myBounds.getMinY()),
new Point2D(myBounds.getMaxX(), myBounds.getMinY()),
new Point2D(myBounds.getMinX(), myBounds.getMaxY()),
new Point2D(myBounds.getMaxX(), myBounds.getMaxY()));
// use center point plus one corner to determine wherein this block is, to ease moving a block into a small container
Predicate<Bounds> within = bounds -> bounds.contains(center) && corners.stream().anyMatch(bounds::contains);
// a container may never end up in itself or its children
List<? extends WrappedContainer> internals = this.getInternalContainers();
Predicate<BlockContainer> notInSelf = con -> internals.stream().noneMatch(con::isContainedWithin);
BlockContainer newContainer = toplevel.getAllBlockContainers().
filter(container -> within.test(container.containmentBoundsInScene()) && notInSelf.test(container)).
reduce((a, b) -> !a.containmentBoundsInScene().contains(b.containmentBoundsInScene()) ? a : b).
orElse(this.toplevel);
Bounds fitBounds = this.localToParent(this.sceneToLocal(myBounds));
this.moveIntoContainer(newContainer);
newContainer.expandToFit(new BoundingBox(fitBounds.getMinX()-10, fitBounds.getMinY()-10, fitBounds.getWidth()+20, fitBounds.getHeight()+20));
}
/** @return whether this block has a meaningful interpretation the current container. */
public boolean checkValidInCurrentContainer() {
return ! (this.container instanceof TrashContainer);
}
public boolean canAlterAnchors() {
return false;
}
public void alterAnchorCount(boolean isRemove) {
// does not if not supported
}
@Override
public Map<String, Object> toBundle() {
return ImmutableMap.of(
Bundleable.KIND, getClass().getSimpleName(),
BLOCK_ID_PARAMETER, hashCode(),
BLOCK_X_PARAMETER, getLayoutX(),
BLOCK_Y_PARAMETER, getLayoutY(),
BLOCK_PROPERTIES_PARAMETER, toBundleFragment()
);
}
public static Block fromBundle(Map<String,Object> blockBundle,
ToplevelPane toplevelPane,
Map<Integer, Block> blockLookupTable)
throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {
String kind = (String)blockBundle.get(Bundleable.KIND);
String className = blockClassMap.get(kind);
Class<?> clazz = Class.forName(className);
// Find the static "fromBundleFragment" method for the named type and call it
Method fromBundleMethod = clazz.getDeclaredMethod("fromBundleFragment", ToplevelPane.class, Map.class);
Block block = (Block) fromBundleMethod.invoke(null, toplevelPane, blockBundle.get(BLOCK_PROPERTIES_PARAMETER));
block.setLayoutX((Double)blockBundle.get(BLOCK_X_PARAMETER));
block.setLayoutY((Double) blockBundle.get(BLOCK_Y_PARAMETER));
blockLookupTable.put(((Double)blockBundle.get(Block.BLOCK_ID_PARAMETER)).intValue(), block);
// Ensure initialization of types related to the block
block.initiateConnectionChanges();
return block;
}
}