/******************************************************************************* * Copyright (c) 2014, 2016 itemis AG and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Matthias Wienand (itemis AG) - initial API and implementation * Alexander Nyßen (itemis AG) - refactorings * *******************************************************************************/ package org.eclipse.gef.mvc.fx.gestures; import java.util.ArrayList; import java.util.Collections; import java.util.IdentityHashMap; import java.util.List; import java.util.ListIterator; import java.util.Set; import org.eclipse.gef.fx.nodes.InfiniteCanvas; import org.eclipse.gef.geometry.planar.Dimension; import org.eclipse.gef.mvc.fx.domain.IDomain; import org.eclipse.gef.mvc.fx.handlers.IHandler; import org.eclipse.gef.mvc.fx.handlers.IOnClickHandler; import org.eclipse.gef.mvc.fx.handlers.IOnDragHandler; import org.eclipse.gef.mvc.fx.parts.IVisualPart; import org.eclipse.gef.mvc.fx.parts.PartUtils; import org.eclipse.gef.mvc.fx.viewer.IViewer; import org.eclipse.gef.mvc.fx.viewer.InfiniteCanvasViewer; import javafx.beans.value.ChangeListener; import javafx.beans.value.ObservableValue; import javafx.event.Event; import javafx.event.EventHandler; import javafx.event.EventTarget; import javafx.event.EventType; import javafx.geometry.Point2D; import javafx.scene.Node; import javafx.scene.Scene; import javafx.scene.input.KeyEvent; import javafx.scene.input.MouseEvent; /** * An {@link IGesture} to handle click/drag interaction gestures. * <p> * As click and drag are 'overlapping' gestures (a click is part of each drag, * which is composed out of click, drag, and release), these are handled * together here, even while distinct interaction policies will be queried to * handle the respective gesture parts. * <p> * During each click/drag interaction, the tool identifies respective * {@link IVisualPart}s that serve as interaction targets for click and drag * respectively. They are identified via hit-testing on the visuals and the * availability of a corresponding {@link IOnClickHandler} or * {@link IOnDragHandler}. * <p> * The {@link ClickDragGesture} handles the opening and closing of an * transaction operation via the {@link IDomain}, to which it is adapted. It * controls that a single transaction operation is used for the complete * interaction (including the click and potential drag part), so all interaction * results can be undone in a single undo step. * * @author mwienand * @author anyssen * */ public class ClickDragGesture extends AbstractGesture { /** * The typeKey used to retrieve those policies that are able to handle the * click part of the click/drag interaction gesture. */ public static final Class<IOnClickHandler> ON_CLICK_POLICY_KEY = IOnClickHandler.class; /** * The typeKey used to retrieve those policies that are able to handle the * drag part of the click/drag interaction gesture. */ public static final Class<IOnDragHandler> ON_DRAG_POLICY_KEY = IOnDragHandler.class; private final Set<Scene> scenes = Collections .newSetFromMap(new IdentityHashMap<>()); // TODO: Provide activeViewer in AbstractTool. private IViewer activeViewer; private Node pressed; private Point2D startMousePosition; /** * This {@link EventHandler} is registered as an event filter on the * {@link Scene} to handle drag and release events. */ private EventHandler<? super MouseEvent> mouseFilter = new EventHandler<MouseEvent>() { @Override public void handle(MouseEvent event) { // determine pressed/dragged/released state EventType<? extends Event> type = event.getEventType(); if (pressed == null && type.equals(MouseEvent.MOUSE_PRESSED)) { EventTarget target = event.getTarget(); if (target instanceof Node) { // initialize the gesture pressed = (Node) target; startMousePosition = new Point2D(event.getSceneX(), event.getSceneY()); press(pressed, event); } return; } else if (pressed == null) { // not initialized yet return; } if (type.equals(MouseEvent.MOUSE_EXITED_TARGET) || type.equals(MouseEvent.MOUSE_ENTERED_TARGET)) { // ignore mouse exited target events here (they may result from // visual changes that are caused by a preceding press) return; } boolean dragged = type.equals(MouseEvent.MOUSE_DRAGGED); boolean released = false; if (!dragged) { released = type.equals(MouseEvent.MOUSE_RELEASED); if (!released) { // account for missing RELEASE events if (!event.isPrimaryButtonDown() && !event.isSecondaryButtonDown() && !event.isMiddleButtonDown()) { // no button down released = true; } } } if (dragged || released) { double x = event.getSceneX(); double dx = x - startMousePosition.getX(); double y = event.getSceneY(); double dy = y - startMousePosition.getY(); if (dragged) { drag(pressed, event, dx, dy); } else { release(pressed, event, dx, dy); pressed = null; } } } }; private ChangeListener<Boolean> viewerFocusChangeListener = new ChangeListener<Boolean>() { @Override public void changed(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) { // cannot abort if no activeViewer if (activeViewer == null) { return; } // check if any viewer is focused for (IViewer v : getDomain().getViewers().values()) { if (v.isViewerFocused()) { return; } } // no viewer is focused => abort // cancel target policies for (IHandler handler : getActiveHandlers(activeViewer)) { if (handler instanceof IOnDragHandler) { ((IOnDragHandler) handler).abortDrag(); } } // clear active policies clearActiveHandlers(activeViewer); activeViewer = null; // close execution transaction getDomain().closeExecutionTransaction(ClickDragGesture.this); } }; private final IOnDragHandler indicationCursorPolicy[] = new IOnDragHandler[] { null }; @SuppressWarnings("unchecked") private final List<IOnDragHandler> possibleDragPolicies[] = new ArrayList[] { null }; private EventHandler<MouseEvent> indicationCursorMouseMoveFilter = new EventHandler<MouseEvent>() { @Override public void handle(MouseEvent event) { if (indicationCursorPolicy[0] != null) { indicationCursorPolicy[0].hideIndicationCursor(); indicationCursorPolicy[0] = null; } EventTarget eventTarget = event.getTarget(); if (eventTarget instanceof Node) { // determine all drag policies that can be // notified about events Node target = (Node) eventTarget; IViewer viewer = PartUtils.retrieveViewer(getDomain(), target); if (viewer != null) { possibleDragPolicies[0] = new ArrayList<>( getHandlerResolver().resolve( ClickDragGesture.this, target, viewer, ON_DRAG_POLICY_KEY)); } else { possibleDragPolicies[0] = new ArrayList<>(); } // search drag policies in reverse order first, // so that the policy closest to the target part // is the first policy to provide an indication // cursor ListIterator<? extends IOnDragHandler> dragIterator = possibleDragPolicies[0] .listIterator(possibleDragPolicies[0].size()); while (dragIterator.hasPrevious()) { IOnDragHandler policy = dragIterator.previous(); if (policy.showIndicationCursor(event)) { indicationCursorPolicy[0] = policy; break; } } } } }; private EventHandler<KeyEvent> indicationCursorKeyFilter = new EventHandler<KeyEvent>() { @Override public void handle(KeyEvent event) { if (indicationCursorPolicy[0] != null) { indicationCursorPolicy[0].hideIndicationCursor(); indicationCursorPolicy[0] = null; } if (possibleDragPolicies[0] == null || possibleDragPolicies[0].isEmpty()) { return; } // search drag policies in reverse order first, // so that the policy closest to the target part // is the first policy to provide an indication // cursor ListIterator<? extends IOnDragHandler> dragIterator = possibleDragPolicies[0] .listIterator(possibleDragPolicies[0].size()); while (dragIterator.hasPrevious()) { IOnDragHandler policy = dragIterator.previous(); if (policy.showIndicationCursor(event)) { indicationCursorPolicy[0] = policy; break; } } } }; @Override protected void doActivate() { super.doActivate(); for (final IViewer viewer : getDomain().getViewers().values()) { // register a viewer focus change listener viewer.viewerFocusedProperty() .addListener(viewerFocusChangeListener); Scene scene = viewer.getCanvas().getScene(); if (scenes.contains(scene)) { // already registered for this scene continue; } // register mouse move filter for forwarding events to drag policies // that can show a mouse cursor to indicate their action scene.addEventFilter(MouseEvent.MOUSE_MOVED, indicationCursorMouseMoveFilter); // register key event filter for forwarding events to drag policies // that can show a mouse cursor to indicate their action scene.addEventFilter(KeyEvent.ANY, indicationCursorKeyFilter); // register mouse filter for forwarding press, drag, and release // events scene.addEventFilter(MouseEvent.ANY, mouseFilter); scenes.add(scene); } } @Override protected void doDeactivate() { for (Scene scene : new ArrayList<>(scenes)) { scene.removeEventFilter(MouseEvent.ANY, mouseFilter); scene.removeEventFilter(MouseEvent.MOUSE_MOVED, indicationCursorMouseMoveFilter); scene.removeEventFilter(KeyEvent.ANY, indicationCursorKeyFilter); } for (final IViewer viewer : getDomain().getViewers().values()) { viewer.viewerFocusedProperty() .removeListener(viewerFocusChangeListener); } super.doDeactivate(); } /** * This method is called upon {@link MouseEvent#MOUSE_DRAGGED} events. * * @param target * The event target. * @param event * The corresponding {@link MouseEvent}. * @param dx * The horizontal displacement from the mouse press location. * @param dy * The vertical displacement from the mouse press location. */ protected void drag(Node target, MouseEvent event, double dx, double dy) { // abort processing of this gesture if no policies could be // found that can process it if (activeViewer == null || getActiveHandlers(activeViewer).isEmpty()) { return; } for (IOnDragHandler policy : getActiveHandlers(activeViewer)) { policy.drag(event, new Dimension(dx, dy)); } } @SuppressWarnings("unchecked") @Override public List<IOnDragHandler> getActiveHandlers(IViewer viewer) { return (List<IOnDragHandler>) super.getActiveHandlers(viewer); } /** * This method is called upon {@link MouseEvent#MOUSE_PRESSED} events. * * @param target * The event target. * @param event * The corresponding {@link MouseEvent}. */ protected void press(Node target, MouseEvent event) { IViewer viewer = PartUtils.retrieveViewer(getDomain(), target); if (viewer == null) { return; } if (viewer instanceof InfiniteCanvasViewer) { InfiniteCanvas canvas = ((InfiniteCanvasViewer) viewer).getCanvas(); // if any node in the target hierarchy is a scrollbar, // do not process the event if (event.getTarget() instanceof Node) { Node targetNode = (Node) event.getTarget(); while (targetNode != null) { if (targetNode == canvas.getHorizontalScrollBar() || targetNode == canvas.getVerticalScrollBar()) { return; } targetNode = targetNode.getParent(); } } } // show indication cursor on press so that the indication // cursor is shown even when no mouse move event was // previously fired indicationCursorMouseMoveFilter.handle(event); // disable indication cursor event filters within // press-drag-release gesture Scene scene = target.getScene(); if (scene == null) { // FIXME: Should not happen. System.err.println("Target part is not in Scene."); return; } scene.removeEventFilter(MouseEvent.MOUSE_MOVED, indicationCursorMouseMoveFilter); scene.removeEventFilter(KeyEvent.ANY, indicationCursorKeyFilter); // determine viewer that contains the given target part viewer = PartUtils.retrieveViewer(getDomain(), target); // determine click policies boolean opened = false; List<? extends IOnClickHandler> clickPolicies = getHandlerResolver() .resolve(ClickDragGesture.this, target, viewer, ON_CLICK_POLICY_KEY); // process click first if (clickPolicies != null && !clickPolicies.isEmpty()) { opened = true; getDomain().openExecutionTransaction(ClickDragGesture.this); for (IOnClickHandler clickPolicy : clickPolicies) { clickPolicy.click(event); } } // determine viewer that contains the given target part // again, now that the click policies have been executed activeViewer = PartUtils.retrieveViewer(getDomain(), target); // determine drag policies List<? extends IOnDragHandler> policies = null; if (activeViewer != null) { // XXX: A click policy could have changed the visual // hierarchy so that the viewer cannot be determined for // the target node anymore. If that is the case, no drag // policies should be notified about the event. policies = getHandlerResolver().resolve( ClickDragGesture.this, target, activeViewer, ON_DRAG_POLICY_KEY); } // abort processing of this gesture if no drag policies // could be found if (policies == null || policies.isEmpty()) { // remove this tool from the domain's execution // transaction if previously opened if (opened) { getDomain().closeExecutionTransaction(ClickDragGesture.this); } policies = null; return; } // add this tool to the execution transaction of the domain // if not yet opened if (!opened) { getDomain().openExecutionTransaction(ClickDragGesture.this); } // mark the drag policies as active setActiveHandlers(activeViewer, policies); // send press() to all drag policies for (IOnDragHandler policy : policies) { policy.startDrag(event); } } /** * This method is called upon {@link MouseEvent#MOUSE_RELEASED} events. This * method is also called for other mouse events, when a mouse release event * was not fired, but was detected otherwise (probably only possible when * using the JavaFX/SWT integration). * * @param target * The event target. * @param event * The corresponding {@link MouseEvent}. * @param dx * The horizontal displacement from the mouse press location. * @param dy * The vertical displacement from the mouse press location. */ protected void release(Node target, MouseEvent event, double dx, double dy) { if (activeViewer == null) { return; } // enable indication cursor event filters outside of // press-drag-release gesture Scene scene = activeViewer.getRootPart().getVisual().getScene(); if (scene == null) { throw new IllegalStateException( "Active viewer's root part visual is not in Scene."); } scene.addEventFilter(MouseEvent.MOUSE_MOVED, indicationCursorMouseMoveFilter); scene.addEventFilter(KeyEvent.ANY, indicationCursorKeyFilter); // abort processing of this gesture if no policies could be // found that can process it if (getActiveHandlers(activeViewer).isEmpty()) { activeViewer = null; return; } // send release() to all drag policies for (IOnDragHandler policy : getActiveHandlers(activeViewer)) { policy.endDrag(event, new Dimension(dx, dy)); } // clear active policies before processing release clearActiveHandlers(activeViewer); activeViewer = null; // remove this tool from the domain's execution transaction getDomain().closeExecutionTransaction(ClickDragGesture.this); // hide indication cursor if (indicationCursorPolicy[0] != null) { indicationCursorPolicy[0].hideIndicationCursor(); indicationCursorPolicy[0] = null; } } }