package nl.utwente.viskell.ui; import java.util.Optional; import javafx.animation.KeyFrame; import javafx.animation.ScaleTransition; import javafx.animation.Timeline; import javafx.event.Event; import javafx.geometry.Point2D; import javafx.scene.Node; import javafx.scene.effect.DropShadow; import javafx.scene.image.Image; import javafx.scene.image.ImageView; import javafx.scene.input.MouseEvent; import javafx.scene.input.TouchEvent; import javafx.scene.layout.StackPane; import javafx.scene.paint.Color; import javafx.scene.shape.Circle; import javafx.util.Duration; import jfxtras.scene.layout.CircularPane; import nl.utwente.viskell.haskell.env.FunctionInfo; import nl.utwente.viskell.ui.components.*; /** * Circle menu is a context based menu implementation for {@link Block} classes. * Preferably each block class has it's own instance of circle menu. When a * block based class has significant differences to other block classes that * result in different context based actions it should use a specialized * extension of circle menu instead of this one. * <p> * Current context based features include delete. Copy, Paste and Save * functionality is under development. * </p> */ public class CircleMenu extends CircularPane { /** The context of the menu. */ private Block block; /** Timed delay for hide of this menu after inactivity. */ private Timeline hideDelay; /** Show the Circle menu for a specific block. */ public static void showFor(Block block, Point2D pos, boolean byMouse) { CircleMenu menu = new CircleMenu(block, byMouse); double centerX = pos.getX() - (menu.prefWidth(-1) / 2); double centerY = pos.getY() - (menu.prefHeight(-1) / 2); menu.setLayoutX(centerX); menu.setLayoutY(centerY); block.getToplevel().addMenu(menu); } private CircleMenu(Block block, boolean byMouse) { super(); this.block = block; this.hideDelay = new Timeline(new KeyFrame(Duration.millis(4000), e-> this.hide())); this.hideDelay.play(); this.setMouseTransparent(true); // Define menu items // Delete Option ImageView image = makeImageView("/ui/icons/appbar.delete.png"); MenuButton delete = new MenuButton("cut", image); delete.setOnActivate(() -> delete()); this.add(delete); if (block instanceof LambdaBlock) { image = makeImageView("/ui/icons/appbar.arrow.collapsed.png"); MenuButton resize = new MenuButton("resize", image); resize.setOnActivate(() -> ((LambdaBlock)block).resizeToFitAll()); this.add(resize); } else if (! (block instanceof ChoiceBlock)) { // Copy Option image = makeImageView("/ui/icons/appbar.page.copy.png"); MenuButton copy = new MenuButton("copy", image); copy.setOnActivate(() -> copy()); this.add(copy); } if (block instanceof SliderBlock) { image = makeImageView("/ui/icons/appbar.reset.png"); MenuButton reset = new MenuButton("resetSlider", image); reset.setOnActivate(() -> ((SliderBlock)block).resetSlider()); this.add(reset); } else if (block instanceof LambdaBlock) { image = makeImageView("/ui/icons/appbar.input.pen.png"); MenuButton editSig = new MenuButton("editSignature", image); editSig.setOnActivate(() -> { this.hide(); ((LambdaBlock)block).editSignature(); }); this.add(editSig); } else if (block instanceof ChoiceBlock) { image = makeImageView("/ui/icons/appbar.layout.collapse.right.png"); MenuButton addLane = new MenuButton("add lane", image); addLane.setOnActivate(() -> ((ChoiceBlock)block).addLane()); this.add(addLane); image = makeImageView("/ui/icons/appbar.layout.expand.left.variant.png"); MenuButton removeLane = new MenuButton("remove lane", image); removeLane.setOnActivate(() -> ((ChoiceBlock)block).removeLastLane()); this.add(removeLane); } else if (block instanceof ConstantBlock) { image = makeImageView("/ui/icons/appbar.page.edit.png"); MenuButton edit = new MenuButton("edit", image); edit.setOnActivate(() -> { this.hide(); ((ConstantBlock)block).editValue(Optional.empty()); }); this.add(edit); } if (block.canAlterAnchors()) { image = makeImageView("/ui/icons/appbar.edit.add.png"); MenuButton addInput = new MenuButton("add input", image); addInput.setOnActivate(() -> block.alterAnchorCount(false)); this.add(addInput); image = makeImageView("/ui/icons/appbar.edit.minus.png"); MenuButton removeInput = new MenuButton("remove input", image); removeInput.setOnActivate(() -> block.alterAnchorCount(true)); this.add(removeInput); } if (block instanceof ValueBlock) { image = makeImageView("/ui/icons/appbar.layer.up.png"); MenuButton lift = new MenuButton("lift", image); lift.setOnActivate(() -> { ToplevelPane toplevel = this.block.getToplevel(); toplevel.removeBlock(block); Block lifter = new LiftingBlock(toplevel, new NestedValue((ValueBlock)block)); lifter.setLayoutX(block.getLayoutX()-10); lifter.setLayoutY(block.getLayoutY()-10); toplevel.addBlock(lifter); lifter.initiateConnectionChanges(); }); this.add(lift); } else if (block instanceof FunApplyBlock) { image = makeImageView("/ui/icons/appbar.layer.up.png"); MenuButton lift = new MenuButton("lift", image); lift.setOnActivate(() -> { ToplevelPane toplevel = this.block.getToplevel(); FunctionReference funRef = ((FunApplyBlock)block).getFunReference(); if (funRef instanceof LibraryFunUse) { toplevel.removeBlock(block); Block lifter = new LiftingBlock(toplevel, new NestedFunction(((LibraryFunUse)funRef).getFunInfo(), block)); lifter.setLayoutX(block.getLayoutX()-10); lifter.setLayoutY(block.getLayoutY()-10); toplevel.addBlock(lifter); lifter.initiateConnectionChanges(); } }); this.add(lift); } else if (block instanceof BinOpApplyBlock) { image = makeImageView("/ui/icons/appbar.layer.up.png"); MenuButton lift = new MenuButton("lift", image); lift.setOnActivate(() -> { ToplevelPane toplevel = this.block.getToplevel(); FunctionInfo funInfo = ((BinOpApplyBlock)block).getFunInfo(); toplevel.removeBlock(block); Block lifter = new LiftingBlock(toplevel, new NestedFunction(funInfo, block)); lifter.setLayoutX(block.getLayoutX()-10); lifter.setLayoutY(block.getLayoutY()-10); toplevel.addBlock(lifter); lifter.initiateConnectionChanges(); }); this.add(lift); } else if (block instanceof LiftingBlock) { image = makeImageView("/ui/icons/appbar.layer.down.png"); MenuButton unlift = new MenuButton("unlift", image); unlift.setOnActivate(() -> { ToplevelPane toplevel = this.block.getToplevel(); Block original = ((LiftingBlock)block).getNested().getOriginal(); toplevel.removeBlock(block); original.setLayoutX(block.getLayoutX()); original.setLayoutY(block.getLayoutY()); toplevel.addBlock(original); original.initiateConnectionChanges(); //FIXME do not thrash the original block, making this call obsolete original.refreshContainer(); }); this.add(unlift); } if (block instanceof ConstantBlock || (block instanceof SliderBlock && ((SliderBlock)block).isIntegral)) { image = makeImageView("/ui/icons/appbar.refresh.counterclockwise.up.png"); MenuButton convertToMatch = new MenuButton("as match", image); convertToMatch.setOnActivate(() -> { ToplevelPane toplevel = this.block.getToplevel(); ValueBlock valBlock = (ValueBlock)block; Block matcher = new ConstantMatchBlock(toplevel, valBlock); matcher.setLayoutX(valBlock.getLayoutX()); matcher.setLayoutY(valBlock.getLayoutY()); toplevel.removeBlock(valBlock); toplevel.addBlock(matcher); matcher.initiateConnectionChanges(); matcher.refreshContainer(); }); this.add(convertToMatch); } else if (this.block instanceof ConstantMatchBlock) { image = makeImageView("/ui/icons/appbar.refresh.counterclockwise.down.png"); MenuButton convertToValue = new MenuButton("as value", image); convertToValue.setOnActivate(() -> { ToplevelPane toplevel = this.block.getToplevel(); ConstantMatchBlock cmBlock = (ConstantMatchBlock)block; Block valBlock = cmBlock.getOriginal(); valBlock.setLayoutX(cmBlock.getLayoutX()); valBlock.setLayoutY(cmBlock.getLayoutY()); toplevel.removeBlock(cmBlock); toplevel.addBlock(valBlock); valBlock.initiateConnectionChanges(); valBlock.refreshContainer(); }); this.add(convertToValue); } // pressing the menu area outside a button closes it this.addEventHandler(MouseEvent.MOUSE_PRESSED, e -> {this.hide(); e.consume();}); this.addEventHandler(TouchEvent.TOUCH_PRESSED, e -> {this.hide(); e.consume();}); // opening animation this.setScaleX(0.1); this.setScaleY(0.1); ScaleTransition opening = new ScaleTransition(byMouse ? Duration.ONE : Duration.millis(250), this); opening.setToX(1); opening.setToY(1); opening.setOnFinished(e -> this.setMouseTransparent(false)); opening.play(); } /** hide this menu by removing it */ private void hide() { this.block.getToplevel().removeMenu(this); } private ImageView makeImageView(String path) { ImageView image = new ImageView(new Image(this.getClass().getResourceAsStream(path))); return image; } /** Copy the {@link Block} in this context. */ private void copy() { block.getToplevel().copyBlock(block); } /** Delete the {@link Block} in this context. */ private void delete() { block.getToplevel().removeBlock(block); } /** A touch enabled button within this circle menu */ private class MenuButton extends StackPane { /** Whether this button has been pressed */ private boolean wasPressed; /** The action to execute on a 'click' */ private Runnable clickAction; /** * @param name of the button * @param image node shown on the button */ private MenuButton(String name, Node image) { super(); Circle backing = new Circle(0, 0, 32, Color.GOLD); backing.setEffect(new DropShadow(20, 5, 5, Color.BLACK)); backing.setStroke(Color.BLACK); backing.setStrokeWidth(1); this.getChildren().addAll(backing, image); this.setPrefSize(64, 64); this.setPickOnBounds(false); this.wasPressed = false; this.addEventHandler(MouseEvent.MOUSE_PRESSED, this::onPress); this.addEventHandler(TouchEvent.TOUCH_PRESSED, this::onPress); this.addEventHandler(MouseEvent.MOUSE_RELEASED, this::onRelease); this.addEventHandler(TouchEvent.TOUCH_RELEASED, this::onRelease); } /** Sets the action to execute on a 'click' */ private void setOnActivate(Runnable action) { this.clickAction = action; } /** Handles a press event for this button. */ private void onPress(Event event) { this.wasPressed = true; CircleMenu.this.hideDelay.playFromStart(); event.consume(); } /** Handles a release event for this button. */ private void onRelease(Event event) { if (this.wasPressed) { if (this.clickAction != null) { this.clickAction.run(); // avoid double actions this.clickAction = null; } CircleMenu.this.hide(); } event.consume(); } } }