package nl.utwente.viskell.ui; import javafx.animation.KeyFrame; import javafx.animation.ScaleTransition; import javafx.animation.Timeline; import javafx.collections.FXCollections; import javafx.collections.ObservableList; import javafx.event.Event; import javafx.fxml.FXML; import javafx.geometry.Bounds; import javafx.geometry.Point2D; import javafx.scene.Node; import javafx.scene.control.*; import javafx.scene.input.MouseButton; import javafx.scene.input.MouseEvent; import javafx.scene.input.ScrollEvent; import javafx.scene.input.TouchEvent; import javafx.scene.input.TouchPoint; import javafx.scene.layout.Pane; import javafx.scene.layout.Region; import javafx.scene.layout.StackPane; import javafx.util.Duration; import nl.utwente.viskell.ghcj.GhciSession; import nl.utwente.viskell.haskell.env.CatalogFunction; import nl.utwente.viskell.haskell.env.HaskellCatalog; import nl.utwente.viskell.haskell.type.*; import nl.utwente.viskell.ui.components.*; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Optional; import java.util.function.Consumer; import java.util.stream.Collectors; /** * FunctionMenu is a viskell specific menu implementation. A FunctionMenu is an * always present menu, once called it will remain open until the * {@linkplain #close() } method is called. An application can have multiple * instances of FunctionMenu where each menu maintains it's own state. * <p> * FunctionMenu is constructed out of three different spaces: * {@code searchSpace}, {@code categorySpace} and {@code utilSpace}. The * {@code searchSpace} should be used to display search components that can be * used to find stored functions more quickly, {@code categorySpace} contains an * {@linkplain Accordion} where each category of functions is displayed in their * own {@linkplain TitledPane}. {@code utilSpace} can then contain any form of * utility methods or components that might need quick accessing. * </p> */ public class FunctionMenu extends StackPane implements ComponentLoader { /** The context that deals with dragging for this Menu */ protected DragContext dragContext; /** Keep track of how many blocks are placed from this menu */ private int blockCounter = 0; private Accordion categoryContainer = new Accordion(); private ToplevelPane parent; @FXML private Pane searchSpace; @FXML private Pane categorySpace; @FXML private Pane utilSpace; public FunctionMenu(boolean byMouse, HaskellCatalog catalog, ToplevelPane pane) { this.parent = pane; this.loadFXML("FunctionMenu"); this.dragContext = new DragContext(this); /* Create content for searchSpace. */ // TODO create search tools and add them to searchSpace. /* Create content for categorySpace. */ ArrayList<String> categories = new ArrayList<>(catalog.getCategories()); categories.add("Deconstructors"); Collections.sort(categories); for (String category : categories) { ObservableList<CatalogFunction> items = FXCollections.observableArrayList(); ArrayList<CatalogFunction> entries; if ("Deconstructors".equals(category)) { entries = categories.stream().flatMap(c -> catalog.getCategory(c).stream()).filter(e -> e.isConstructor()) .collect(Collectors.toCollection(() -> new ArrayList<>())); } else { entries = new ArrayList<>(catalog.getCategory(category)); } Collections.sort(entries); items.addAll(entries); ListView<CatalogFunction> listView = new ListView<>(items); listView.setCellFactory((list) -> { return new ListCell<CatalogFunction>() { { this.setOnMouseReleased(e -> { if (this.isEmpty()) { return; } if ((e.isSynthesized() && e.getButton() != MouseButton.SECONDARY) || !this.contains(e.getX(), e.getY())) { return; } CatalogFunction entry = this.getItem(); if ("Deconstructors".equals(category) && entry.isConstructor()) { addBlock(new MatchBlock(parent, entry)); } else if (e.getButton() == MouseButton.SECONDARY && entry.isConstructor()) { addBlock(new MatchBlock(parent, entry)); } else if (!(entry.getFreshSignature() instanceof FunType)) { addBlock(new ConstantBlock(pane, entry.getFreshSignature(), entry.getName(), true)); } else { if (entry.getName().startsWith("(") && entry.getFreshSignature().countArguments() == 2) { addBlock(new BinOpApplyBlock(parent, entry)); } else { addBlock(new FunApplyBlock(parent, new LibraryFunUse(entry))); } } }); final double[] touchStartY = new double[]{0.0}; this.setOnTouchPressed(e -> { touchStartY[0] = this.localToParent(e.getTouchPoint().getX(), e.getTouchPoint().getY()).getY(); }); this.setOnTouchReleased(e -> { if (this.isEmpty()) { return; } double touchParentY = this.localToParent(e.getTouchPoint().getX(), e.getTouchPoint().getY()).getY(); if (Math.abs(touchStartY[0] - touchParentY) > 10) { // a release after scrolling is not intended as touch click return; } if (!this.contains(e.getTouchPoint().getX(), e.getTouchPoint().getY())) { return; } CatalogFunction entry = this.getItem(); if ("Deconstructors".equals(category) && entry.isConstructor()) { addBlock(new MatchBlock(parent, entry)); } else if (!(entry.getFreshSignature() instanceof FunType)) { addBlock(new ConstantBlock(pane, entry.getFreshSignature(), entry.getName(), true)); } else { if (entry.getName().startsWith("(") && entry.getFreshSignature().countArguments() == 2) { addBlock(new BinOpApplyBlock(parent, entry)); } else { addBlock(new FunApplyBlock(parent, new LibraryFunUse(entry))); } } }); this.setOnTouchMoved(e -> { if (this.isEmpty()) { return; } if (this.contains(e.getTouchPoint().getX(), e.getTouchPoint().getY())) { return; } double sceneX = e.getTouchPoint().getSceneX(); Bounds bounds = FunctionMenu.this.localToScene(FunctionMenu.this.getBoundsInLocal()); if (sceneX < bounds.getMinX()-75 || sceneX > bounds.getMaxX()+25) { CatalogFunction entry = this.getItem(); if ("Deconstructors".equals(category) && entry.isConstructor()) { addDraggedBlock(e.getTouchPoint(), new MatchBlock(parent, entry)); } else if (!(entry.getFreshSignature() instanceof FunType)) { addDraggedBlock(e.getTouchPoint(), new ConstantBlock(pane, entry.getFreshSignature(), entry.getName(), true)); } else { if (entry.getName().startsWith("(") && entry.getFreshSignature().countArguments() == 2) { addDraggedBlock(e.getTouchPoint(), new BinOpApplyBlock(parent, entry)); } else { addDraggedBlock(e.getTouchPoint(), new FunApplyBlock(parent, new LibraryFunUse(entry))); } } e.consume(); } }); } @Override protected void updateItem(CatalogFunction item, boolean empty) { super.updateItem(item, empty); this.setText(item == null ? null : item.getDisplayName()); } }; }); TitledPane submenu = new TitledPane(category, listView); submenu.setAnimated(false); // toggling of the submenu by touch submenu.setOnTouchReleased(event -> { if (event.getTouchPoint().getY() < 24) { submenu.setExpanded(! submenu.isExpanded()); } }); //Prevent dragging the whole menu when dragging inside a category list listView.addEventHandler(TouchEvent.TOUCH_MOVED, Event::consume); // Consume scroll events to prevent mixing of zooming and list scrolling. listView.addEventHandler(ScrollEvent.SCROLL, Event::consume); categoryContainer.getPanes().addAll(submenu); } // drop all synthesized mouse events categoryContainer.addEventFilter(MouseEvent.ANY, e -> {if (e.isSynthesized()) e.consume();}); // Hiding all other categories on expanding one of them. List<TitledPane> allCatPanes = new ArrayList<>(categoryContainer.getPanes()); categoryContainer.expandedPaneProperty().addListener(e -> { TitledPane expPane = categoryContainer.getExpandedPane(); if (expPane != null) { categoryContainer.getPanes().retainAll(expPane); } else { categoryContainer.getPanes().clear(); categoryContainer.getPanes().addAll(allCatPanes); } }); this.categorySpace.getChildren().add(categoryContainer); /* Create content for utilSpace. */ Button closeButton = new MenuButton("Close", bm -> close(bm)); closeButton.getStyleClass().add("escape"); Button valBlockButton = new MenuButton("Constant", bm -> addConstantBlock()); Button arbBlockButton = new MenuButton("Arbitrary", bm -> addBlock(new ArbitraryBlock(parent))); Button disBlockButton = new MenuButton("Observe", bm -> addBlock(new DisplayBlock(parent))); Button lambdaBlockButton = new MenuButton("Lambda", bm -> addLambdaBlock()); Button choiceBlockButton = new MenuButton("Choice", bm -> addChoiceBlock()); Button applyBlockButton = new MenuButton("Apply", bm -> addBlock(new FunApplyBlock(parent, new ApplyAnchor(1)))); utilSpace.getChildren().addAll(closeButton, disBlockButton, arbBlockButton, valBlockButton, lambdaBlockButton, applyBlockButton, choiceBlockButton); if (GhciSession.pickBackend() == GhciSession.Backend.GHCi) { // These blocks are specifically for GHCi Button rationalBlockButton = new MenuButton("Rational", bm -> addBlock(new SliderBlock(parent, false))); Button IntegerBlockButton = new MenuButton("Integer", bm -> addBlock(new SliderBlock(parent, true))); Button graphBlockButton = new MenuButton("Graph", bm -> addBlock(new GraphBlock(parent))); utilSpace.getChildren().addAll(graphBlockButton, IntegerBlockButton, rationalBlockButton); } if (GhciSession.pickBackend() == GhciSession.Backend.Clash) { // These blocks are specifically for Clash Button simulateBlockButton = new MenuButton("Simulate", bm -> addBlock(new SimulateBlock(parent))); utilSpace.getChildren().addAll(simulateBlockButton); } // with an odd number of block buttons fill the last spot with a close button if (utilSpace.getChildren().size() % 2 == 1) { Button extraCloseButton = new MenuButton("Close", bm -> close(bm)); utilSpace.getChildren().add(extraCloseButton); } for (Node button : utilSpace.getChildren()) { ((Region) button).setMaxWidth(Double.MAX_VALUE); } // opening animation of this menu, during which it can't be accidentally used this.setMouseTransparent(true); this.setScaleX(0.3); this.setScaleY(0.1); ScaleTransition opening = new ScaleTransition(byMouse ? Duration.ONE : Duration.millis(300), this); opening.setToX(1); opening.setToY(1); opening.setOnFinished(e -> this.setMouseTransparent(false)); opening.play(); } /** Specialized Button that behaves better in a many touch environment. */ private static class MenuButton extends Button { private int touchDragCounter; private MenuButton(String text, Consumer<Boolean> action) { super(text); this.touchDragCounter = 0; this.getStyleClass().add("menuButton"); this.setOnMouseClicked(event -> {if (!event.isSynthesized()) action.accept(true);}); Timeline dragReset = new Timeline(new KeyFrame(Duration.millis(500), e -> this.touchDragCounter = 0)); this.setOnTouchReleased(event -> {if (this.touchDragCounter < 7) action.accept(false);}); this.setOnTouchPressed(event -> dragReset.play()); this.setOnTouchMoved(event -> this.touchDragCounter++); } } private void addConstantBlock() { ConstantBlock val = new ConstantBlock(this.parent); addBlock(val); val.editValue(Optional.of("\"Hello, World!\"")); } private void addLambdaBlock() { addBlock(new LambdaBlock(this.parent, 1)); } private void addChoiceBlock() { ChoiceBlock def = new ChoiceBlock(this.parent); addBlock(def); } private void addBlock(Block block) { parent.addBlock(block); Bounds menuBounds = this.getBoundsInParent(); int offSetY = (this.blockCounter % 5) * 20 + (block.getAllOutputs().isEmpty() ? 250 : 125); if (this.localToScene(Point2D.ZERO).getX() < 200) { // too close to the left side of screen, put block on the right int offSetX = (this.blockCounter % 5) * 10 + (block.belongsOnBottom() ? 50 : 100); block.relocate(menuBounds.getMaxX() + offSetX, menuBounds.getMinY()+ offSetY); } else { int offSetX = (this.blockCounter % 5) * 10 - (block.belongsOnBottom() ? 400 : 200); block.relocate(menuBounds.getMinX() + offSetX, menuBounds.getMinY()+ offSetY); } if (! block.belongsOnBottom()) { block.refreshContainer(); } block.initiateConnectionChanges(); this.blockCounter++; } private void addDraggedBlock(TouchPoint touchPoint, Block block) { Point2D pos = parent.sceneToLocal(touchPoint.getSceneX(), touchPoint.getSceneY()); parent.addBlock(block); block.relocate(pos.getX(), pos.getY()); touchPoint.grab(block); block.initiateConnectionChanges(); } /** Closes this menu by removing it from it's parent. */ public void close(boolean byMouse) { // disable it first, before removal in a closing animation this.setMouseTransparent(true); ScaleTransition closing = new ScaleTransition(byMouse ? Duration.ONE :Duration.millis(300), this); closing.setToX(0.3); closing.setToY(0.1); closing.setOnFinished(e -> parent.removeMenu(this)); closing.play(); } }