package nl.utwente.viskell.ui; import java.util.function.BiConsumer; import java.util.prefs.Preferences; import javafx.animation.KeyFrame; import javafx.animation.Timeline; import javafx.event.ActionEvent; import javafx.geometry.Point2D; 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.paint.Color; import javafx.scene.shape.Circle; import javafx.scene.shape.Line; import javafx.util.Duration; /** * Helper class used for event handling on the background pane of a container. */ public class TouchContext { private static Preferences preferences = Preferences.userNodeForPackage(Main.class); /** The container this context handling events for. */ private final BlockContainer container; /** the last mouse Position the pan action was handled for */ private Point2D lastPanPosition; /** Boolean to indicate that a drag (pan) action has started, yet not finished. */ private boolean panning; /** the action to be executed on a panning movement, may be null. */ private BiConsumer<Double, Double> panningAction; private boolean willPanTouchArea; /** The line shown for the wire cutting mouse action, might be null if not valid. */ private Line mouseCutLine; public TouchContext(BlockContainer container, boolean willPanTouchArea) { super(); this.container = container; this.willPanTouchArea = willPanTouchArea; this.lastPanPosition = Point2D.ZERO; this.panning = false; this.panningAction = null; container.asNode().addEventHandler(MouseEvent.MOUSE_PRESSED, this::handleMousePress); container.asNode().addEventHandler(MouseEvent.MOUSE_DRAGGED, this::handleMouseDrag); container.asNode().addEventHandler(MouseEvent.MOUSE_RELEASED, this::handleMouseRelease); container.asNode().addEventHandler(TouchEvent.TOUCH_PRESSED, this::handleTouchPress); container.asNode().addEventHandler(ScrollEvent.SCROLL, this::handleScrollGesture); } private void dropMouseCutLine() { if (this.mouseCutLine != null) { this.container.getToplevel().removeUpperTouchArea(this.mouseCutLine); this.mouseCutLine = null; } } private void handleMousePress(MouseEvent e) { if (!e.isSynthesized()) { this.lastPanPosition = new Point2D(e.getScreenX(), e.getScreenY()); if (e.getButton() == MouseButton.PRIMARY && this.mouseCutLine == null) { Point2D pos = this.container.getToplevel().sceneToLocal(e.getSceneX(), e.getSceneY()); this.mouseCutLine = new Line(pos.getX(), pos.getY(), pos.getX(), pos.getY()); this.mouseCutLine.setStroke(Color.YELLOW); this.mouseCutLine.setStrokeWidth(3); this.mouseCutLine.setVisible(false); this.container.getToplevel().addUpperTouchArea(this.mouseCutLine); } } e.consume(); } private void handleMouseDrag(MouseEvent e) { if (e.isSynthesized()) { e.consume(); return; } Point2D currentPos = new Point2D(e.getScreenX(), e.getScreenY()); if (!e.isPrimaryButtonDown()) { Point2D delta = currentPos.subtract(this.lastPanPosition); if (this.panning) { if (this.panningAction != null) { this.panningAction.accept(delta.getX(), delta.getY()); } } else { this.panning = (Math.abs(delta.getX()) + Math.abs(delta.getY())) > 2; } } else if (this.mouseCutLine != null) { Point2D newPos = this.container.getToplevel().screenToLocal(currentPos); double lineDiffX = this.mouseCutLine.getStartX() - this.mouseCutLine.getEndX(); double lineDiffY = this.mouseCutLine.getStartY() - this.mouseCutLine.getEndY(); double lengthSQ = lineDiffX*lineDiffX + lineDiffY*lineDiffY; double distance = new Point2D(this.mouseCutLine.getStartX(), this.mouseCutLine.getStartY()).distance(newPos); if (distance*distance > lengthSQ) { this.mouseCutLine.setEndX(newPos.getX()); this.mouseCutLine.setEndY(newPos.getY()); if (distance > 300) { this.dropMouseCutLine(); } else if (distance > 75) { this.mouseCutLine.setVisible(true); } } else if (distance < 10 && lengthSQ > 100*100) { double midX = (this.mouseCutLine.getStartX()+this.mouseCutLine.getEndX())/2; double midY = (this.mouseCutLine.getStartY()+this.mouseCutLine.getEndY())/2; Circle cutArea = new Circle(midX, midY, 40); this.container.getToplevel().addUpperTouchArea(cutArea); this.container.getToplevel().cutIntersectingConnections(cutArea); this.container.getToplevel().removeUpperTouchArea(cutArea); this.dropMouseCutLine(); } } this.lastPanPosition = currentPos; e.consume(); } private void handleMouseRelease(MouseEvent e) { if (e.isSynthesized()) { return; } if (e.getButton() != MouseButton.PRIMARY && !this.panning) { Point2D pos = this.container.getToplevel().sceneToLocal(this.container.asNode().localToScene(e.getX(), e.getY())); this.container.getToplevel().showFunctionMenuAt(pos.getX(), pos.getY(), true); } this.panning = false; this.dropMouseCutLine(); e.consume(); } private void handleTouchPress(TouchEvent e) { this.container.getToplevel().addLowerTouchArea(new TouchArea(e.getTouchPoint())); e.consume(); } private void handleScrollGesture(ScrollEvent e) { // only react to proper panning gestures that not on the touch screen itself if ((!e.isDirect()) && !e.isInertia()) { if (this.panningAction != null) { if (preferences.getBoolean("invertScroll", false)) { this.panningAction.accept(-e.getDeltaX(), -e.getDeltaY()); } else { this.panningAction.accept(e.getDeltaX(), e.getDeltaY()); } } } e.consume(); } public void setPanningAction(BiConsumer<Double, Double> action) { this.panningAction = action; } /** A circular local area for handling multi finger touch actions. */ private class TouchArea extends Circle { private final ToplevelPane toplevel; /** The ID of finger that spawned this touch area. */ private int touchID; /** Whether this touch area has been dragged further than the drag threshold. */ private boolean dragStarted; /** Whether this touch area has spawned a menu. */ private boolean menuCreated; /** Timed delay for the removal of this touch area. */ private Timeline removeDelay; /** Timed delay for the creation of the function menu. */ private Timeline menuDelay; /** The line shown for the wire cutting gesture, might be null if not valid. */ private Line wireCutter; /** * @param touchPoint that is the center of new active touch area. */ private TouchArea(TouchPoint touchPoint) { super(); this.toplevel = TouchContext.this.container.getToplevel(); this.touchID = touchPoint.getId(); this.dragStarted = false; this.menuCreated = false; Point2D pos = this.toplevel.sceneToLocal(touchPoint.getSceneX(), touchPoint.getSceneY()); this.setCenterX(pos.getX()); this.setCenterY(pos.getY()); this.setRadius(100); this.setFill(Color.TRANSPARENT); this.removeDelay = new Timeline(new KeyFrame(Duration.millis(250), this::remove)); this.menuDelay = new Timeline(new KeyFrame(Duration.millis(200), this::finishMenu)); this.wireCutter = new Line(pos.getX(), pos.getY(), pos.getX(), pos.getY()); this.wireCutter.setStroke(Color.YELLOW); this.wireCutter.setStrokeWidth(3); this.wireCutter.setVisible(false); this.toplevel.addUpperTouchArea(this.wireCutter); touchPoint.grab(this); this.addEventHandler(TouchEvent.TOUCH_RELEASED, this::handleRelease); this.addEventHandler(TouchEvent.TOUCH_PRESSED, this::handlePress); this.addEventHandler(TouchEvent.TOUCH_MOVED, this::handleDrag); } private void remove(ActionEvent event) { this.toplevel.removeLowerTouchArea(this); this.removeCutter(); } private void removeCutter(){ if (this.wireCutter != null) { this.toplevel.removeUpperTouchArea(this.wireCutter); this.wireCutter = null; } } private void finishMenu(ActionEvent event) { this.toplevel.showFunctionMenuAt(this.getCenterX(), this.getCenterY(), false); this.toplevel.removeLowerTouchArea(this); this.menuCreated = true; } private void handlePress(TouchEvent event) { // this might have been a drag glitch, so halt release actions this.removeDelay.stop(); if (event.getTouchPoints().stream().filter(tp -> tp.belongsTo(this)).count() == 2) { this.menuDelay.stop(); this.removeCutter(); } event.consume(); } private void handleRelease(TouchEvent event) { long fingerCount = event.getTouchPoints().stream().filter(tp -> tp.belongsTo(this)).count(); if (fingerCount == 1) { // trigger area removal timer this.removeDelay.play(); } else if (this.dragStarted || this.menuCreated) { // avoid accidental creation of (more) menus } else if (fingerCount == 2) { // trigger menu creation timer this.menuDelay.play(); } event.consume(); } private void handleDrag(TouchEvent event) { TouchPoint touchPoint = event.getTouchPoint(); if (event.getTouchPoint().getId() != this.touchID) { // we use only primary finger for drag movement } else if (event.getTouchPoints().stream().filter(tp -> tp.belongsTo(this)).count() < 2) { // not a multi finger drag this.updateCutter(this.toplevel.sceneToLocal(touchPoint.getSceneX(), touchPoint.getSceneY())); } else { double deltaX = touchPoint.getX() - this.getCenterX(); double deltaY = touchPoint.getY() - this.getCenterY(); if (Math.abs(deltaX) + Math.abs(deltaY) < 2) { // ignore very small movements } else if ((deltaX*deltaX + deltaY*deltaY) > 10000) { // FIXME: ignore too large movements } else if (this.dragStarted || (deltaX*deltaX + deltaY*deltaY) > 63) { this.dragStarted = true; if (TouchContext.this.panningAction != null) { TouchContext.this.panningAction.accept(deltaX, deltaY); if (!TouchContext.this.willPanTouchArea) { this.setLayoutX(this.getLayoutX() + deltaX); this.setLayoutY(this.getLayoutY() + deltaY); } } } } event.consume(); } private void updateCutter(Point2D newPos) { if (this.wireCutter == null) { return; } double lineDiffX = this.wireCutter.getStartX() - this.wireCutter.getEndX(); double lineDiffY = this.wireCutter.getStartY() - this.wireCutter.getEndY(); double lengthSQ = lineDiffX*lineDiffX + lineDiffY*lineDiffY; double distance = new Point2D(this.wireCutter.getStartX(), this.wireCutter.getStartY()).distance(newPos); if (distance*distance > lengthSQ) { this.wireCutter.setEndX(newPos.getX()); this.wireCutter.setEndY(newPos.getY()); if (distance > 300) { this.removeCutter(); } else if (distance > 75) { this.wireCutter.setVisible(true); } } else if (distance < 20 && lengthSQ > 100*100) { double midX = (this.wireCutter.getStartX()+this.wireCutter.getEndX())/2; double midY = (this.wireCutter.getStartY()+this.wireCutter.getEndY())/2; Circle cutArea = new Circle(midX, midY, 40); this.toplevel.addUpperTouchArea(cutArea); this.toplevel.cutIntersectingConnections(cutArea); this.toplevel.removeUpperTouchArea(cutArea); this.removeCutter(); } } } }