/******************************************************************************* * 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 * *******************************************************************************/ package org.eclipse.gef.mvc.fx.gestures; import java.util.Collection; import java.util.IdentityHashMap; import java.util.Map; import org.eclipse.gef.geometry.planar.Point; import org.eclipse.gef.mvc.fx.handlers.IOnHoverHandler; import org.eclipse.gef.mvc.fx.parts.PartUtils; import org.eclipse.gef.mvc.fx.viewer.IViewer; import javafx.animation.Animation.Status; import javafx.animation.PauseTransition; import javafx.event.EventHandler; import javafx.event.EventTarget; import javafx.scene.Node; import javafx.scene.Scene; import javafx.scene.input.MouseEvent; import javafx.util.Duration; /** * The {@link HoverGesture} is an {@link AbstractGesture} that handles mouse hover * changes. * * @author mwienand * */ public class HoverGesture extends AbstractGesture { /** * Time in milliseconds until the hover handles are created when the host is * hovered. */ public static final long HOVER_INTENT_MILLIS = 250; /** * Distance in pixels which the mouse is allowed to move so that it is * regarded to be stationary. */ public static final double HOVER_INTENT_MOUSE_MOVE_THRESHOLD = 4; /** * The type of the policy that has to be supported by target parts. */ public static final Class<IOnHoverHandler> ON_HOVER_POLICY_KEY = IOnHoverHandler.class; private final Map<Scene, EventHandler<MouseEvent>> hoverFilters = new IdentityHashMap<>(); // TODO: Investigate if hover intent works with multiple scenes, or if // multiple scenes require special treatment. private Point hoverIntentScreenPosition; private PauseTransition hoverIntentDelay = new PauseTransition( Duration.millis(getHoverIntentMillis())); private Node hoverIntent; private Node potentialHoverIntent; { hoverIntentDelay.setOnFinished((ae) -> onHoverIntentDelayFinished()); } /** * Creates an {@link EventHandler} for hover {@link MouseEvent}s. The * handler will search for a target part within the given {@link IViewer} * and notify all hover policies of that target part about hover changes. * <p> * If no target part can be identified, then the root part of the given * {@link IViewer} is used as the target part. * * @return The {@link EventHandler} that handles hover changes for the given * {@link IViewer}. */ protected EventHandler<MouseEvent> createHoverFilter() { return new EventHandler<MouseEvent>() { @Override public void handle(MouseEvent event) { updateHoverIntentPosition(event); if (!isHoverEvent(event)) { return; } EventTarget eventTarget = event.getTarget(); if (eventTarget instanceof Node) { IViewer viewer = PartUtils.retrieveViewer(getDomain(), (Node) eventTarget); if (viewer != null) { notifyHover(viewer, event, (Node) eventTarget); } updateHoverIntent(event, (Node) eventTarget); } } }; } @Override protected void doActivate() { super.doActivate(); for (IViewer viewer : getDomain().getViewers().values()) { // XXX: Filter is only registered once per scene. The IViewer is // determined for each input event individually. Scene scene = viewer.getCanvas().getScene(); if (!hoverFilters.containsKey(scene)) { EventHandler<MouseEvent> hoverFilter = createHoverFilter(); scene.addEventFilter(MouseEvent.ANY, hoverFilter); hoverFilters.put(scene, hoverFilter); } } } @Override protected void doDeactivate() { hoverIntentDelay.stop(); for (Scene scene : hoverFilters.keySet()) { scene.removeEventFilter(MouseEvent.ANY, hoverFilters.remove(scene)); } super.doDeactivate(); } /** * Returns the duration (in millis) for which the mouse should be * stationarry to trigger hover intent. * * @return the duration (in millis) for which the mouse should be stationary * to trigger hover intent. */ protected long getHoverIntentMillis() { return HOVER_INTENT_MILLIS; } /** * Returns the number of pixels the mouse is allowed to move to be still * regarded as stationary. * * @return the number of pixels the mouse is allowed to move to be still * regarded as stationary. */ protected double getHoverIntentMouseMoveThreshold() { return HOVER_INTENT_MOUSE_MOVE_THRESHOLD; } /** * Returns <code>true</code> if the given {@link MouseEvent} should be * tested for changing the hover target. * * @param event * The {@link MouseEvent}. * @return <code>true</code> if the given {@link MouseEvent} should be * tested for changing the hover target */ protected boolean isHoverEvent(MouseEvent event) { return event.getEventType().equals(MouseEvent.MOUSE_MOVED) || event.getEventType().equals(MouseEvent.MOUSE_ENTERED_TARGET) || event.getEventType().equals(MouseEvent.MOUSE_EXITED_TARGET); } /** * * @param viewer * The {@link IViewer}. * @param event * The corresponding {@link MouseEvent}. * @param eventTarget * The target {@link Node}. */ protected void notifyHover(IViewer viewer, MouseEvent event, Node eventTarget) { // determine hover policies Collection<? extends IOnHoverHandler> policies = getHandlerResolver() .resolve(HoverGesture.this, eventTarget, viewer, ON_HOVER_POLICY_KEY); getDomain().openExecutionTransaction(HoverGesture.this); // active policies are unnecessary because hover is not a // gesture, just one event at one point in time for (IOnHoverHandler policy : policies) { policy.hover(event); } getDomain().closeExecutionTransaction(HoverGesture.this); } /** * * @param viewer * The {@link IViewer}. * @param hoverIntent * The hover intent {@link Node}. */ protected void notifyHoverIntent(IViewer viewer, Node hoverIntent) { // determine hover policies Collection<? extends IOnHoverHandler> policies = getHandlerResolver() .resolve(HoverGesture.this, hoverIntent, viewer, ON_HOVER_POLICY_KEY); getDomain().openExecutionTransaction(HoverGesture.this); // active policies are unnecessary because hover is not a // gesture, just one event at one point in time for (IOnHoverHandler policy : policies) { policy.hoverIntent(hoverIntent); } getDomain().closeExecutionTransaction(HoverGesture.this); } /** * Callback method that is invoked when the mouse was stationary over a * visual for some amount of time. */ private void onHoverIntentDelayFinished() { hoverIntent = potentialHoverIntent; potentialHoverIntent = null; IViewer viewer = PartUtils.retrieveViewer(getDomain(), hoverIntent); if (viewer != null) { notifyHoverIntent(viewer, hoverIntent); } } /** * Updates hover intent delays depending on the given event and hovered * node. * * @param event * The {@link MouseEvent}. * @param eventTarget * The hovered {@link Node}. */ private void updateHoverIntent(MouseEvent event, Node eventTarget) { if (eventTarget != hoverIntent) { potentialHoverIntent = eventTarget; hoverIntentScreenPosition = new Point(event.getScreenX(), event.getScreenY()); hoverIntentDelay.playFromStart(); } else { hoverIntentDelay.stop(); } } /** * Updates the hover intent position (and restarts the hover intent delay) * if the mouse was moved too much. * * @param event * The {@link MouseEvent}. */ private void updateHoverIntentPosition(MouseEvent event) { if (hoverIntentDelay.getStatus().equals(Status.RUNNING)) { double dx = hoverIntentScreenPosition.x - event.getScreenX(); double dy = hoverIntentScreenPosition.y - event.getScreenY(); double threshold = getHoverIntentMouseMoveThreshold(); if (Math.abs(dx) > threshold || Math.abs(dy) > threshold) { hoverIntentDelay.playFromStart(); hoverIntentScreenPosition.x = event.getScreenX(); hoverIntentScreenPosition.y = event.getScreenY(); } } } }