/******************************************************************************* * Copyright (c) 2015, 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.handlers; import org.eclipse.gef.fx.nodes.InfiniteCanvas; import org.eclipse.gef.mvc.fx.policies.ViewportPolicy; import javafx.animation.AnimationTimer; import javafx.geometry.Pos; import javafx.scene.input.KeyCode; import javafx.scene.input.KeyEvent; /** * The {@link PanOnStrokeHandler} is an {@link IOnTypeHandler} that performs * viewport panning via the keyboard. * * @author mwienand * */ public class PanOnStrokeHandler extends AbstractHandler implements IOnStrokeHandler { /** * The default scroll amount per second, i.e. how many pixels the viewport * is moved per second. */ public static final double DEFAULT_SCROLL_AMOUNT_PER_SECOND = 150d; private PanningSupport panningSupport = new PanningSupport(); private AnimationTimer timer; // store pressed state for direction keys private boolean isDown; private boolean isUp; private boolean isLeft; private boolean isRight; // time (milli seconds) when a key was pressed private long startMillisDown; private long startMillisUp; private long startMillisLeft; private long startMillisRight; // current press duration (milli seconds) private long currentMillisDown = 0; private long currentMillisUp = 0; private long currentMillisLeft = 0; private long currentMillisRight = 0; // total press duration (milli seconds) private long totalMillisDown = 0; private long totalMillisUp = 0; private long totalMillisLeft = 0; private long totalMillisRight = 0; // save if gesture is valid private boolean invalidGesture = false; // cached during interaction private ViewportPolicy viewportPolicy; @Override public void abortPress() { if (invalidGesture) { return; } rollback(viewportPolicy); this.viewportPolicy = null; } /** * Returns the {@link ViewportPolicy} that is to be used for changing the * viewport. This method is called within {@link #initialPress(KeyEvent)} * where the resulting policy is cached for the keyboard. * * @return The {@link ViewportPolicy} that is to be used for changing the * viewport. */ protected ViewportPolicy determineViewportPolicy() { return getHost().getRoot().getAdapter(ViewportPolicy.class); } @Override public void finalRelease(KeyEvent event) { if (invalidGesture) { return; } updateMillisOnKeyRelease(event); timer.stop(); updateScrollPosition(); commit(viewportPolicy); viewportPolicy = null; totalMillisDown = 0; totalMillisUp = 0; totalMillisLeft = 0; totalMillisRight = 0; } /** * Returns the amount of units scrolled per second when a direction key is * pressed. * * @return The amount of units scrolled per second when a direction key is * pressed. */ public double getScrollAmountPerSecond() { return DEFAULT_SCROLL_AMOUNT_PER_SECOND; } /** * Returns the cached {@link ViewportPolicy} that was returned by * {@link #determineViewportPolicy()} within * {@link #initialPress(KeyEvent)}. * * @return The cached {@link ViewportPolicy}. */ public final ViewportPolicy getViewportPolicy() { return viewportPolicy; } @Override public void initialPress(KeyEvent event) { invalidGesture = !isPan(event); if (invalidGesture) { return; } // determine viewport policy to cache viewportPolicy = determineViewportPolicy(); init(viewportPolicy); updateMillisOnKeyPress(event); if (timer == null) { // FIXME: Test if we can construct the timer during field // initialization. A previous comment indicated that this was not // possible. timer = new AnimationTimer() { @Override public void handle(long nanos) { long now = System.currentTimeMillis(); // compute millis pressed per direction if (isDown) { currentMillisDown = now - startMillisDown; } if (isUp) { currentMillisUp = now - startMillisUp; } if (isLeft) { currentMillisLeft = now - startMillisLeft; } if (isRight) { currentMillisRight = now - startMillisRight; } updateScrollPosition(); } }; } timer.start(); } /** * Returns <code>true</code> to signify that scrolling and zooming is * restricted to the content bounds, <code>false</code> otherwise. * <p> * When content-restricted, the policy behaves texteditor-like, i.e. the * pivot point for zooming is at the top of the viewport and at the left of * the contents, and free space is only allowed to the right and to the * bottom of the contents. Therefore, the policy does not allow panning or * zooming if it would result in free space within the viewport at the top * or left sides of the contents. * * @return <code>true</code> to signify that scrolling and zooming is * restricted to the content bounds, <code>false</code> otherwise. */ protected boolean isContentRestricted() { return false; } /** * Returns <code>true</code> if the given {@link KeyEvent} should trigger * panning. Otherwise returns <code>false</code>. Per default, will return * <code>true</code> if <code><Up></code>, <code><Down></code>, * <code><Left></code>, <code><Right></code> * * @param event * The {@link KeyEvent} in question. * @return <code>true</code> to indicate that the given {@link KeyEvent} * should trigger panning, otherwise <code>false</code>. */ protected boolean isPan(KeyEvent event) { return event.getCode().equals(KeyCode.DOWN) || event.getCode().equals(KeyCode.UP) || event.getCode().equals(KeyCode.LEFT) || event.getCode().equals(KeyCode.RIGHT); } @Override public void press(KeyEvent event) { updateMillisOnKeyPress(event); } @Override public void release(KeyEvent event) { updateMillisOnKeyRelease(event); } private void updateMillisOnKeyPress(KeyEvent event) { long now = System.currentTimeMillis(); if (!isDown && event.getCode().equals(KeyCode.DOWN)) { startMillisDown = now; currentMillisDown = 0; isDown = true; } else if (!isUp && event.getCode().equals(KeyCode.UP)) { startMillisUp = now; currentMillisUp = 0; isUp = true; } else if (!isLeft && event.getCode().equals(KeyCode.LEFT)) { startMillisLeft = now; currentMillisLeft = 0; isLeft = true; } else if (!isRight && event.getCode().equals(KeyCode.RIGHT)) { startMillisRight = now; currentMillisRight = 0; isRight = true; } } private void updateMillisOnKeyRelease(KeyEvent event) { long now = System.currentTimeMillis(); if (event.getCode().equals(KeyCode.DOWN)) { isDown = false; totalMillisDown += now - startMillisDown; currentMillisDown = 0; } else if (event.getCode().equals(KeyCode.UP)) { isUp = false; totalMillisUp += now - startMillisUp; currentMillisUp = 0; } else if (event.getCode().equals(KeyCode.LEFT)) { isLeft = false; totalMillisLeft += now - startMillisLeft; currentMillisLeft = 0; } else if (event.getCode().equals(KeyCode.RIGHT)) { isRight = false; totalMillisRight += now - startMillisRight; currentMillisRight = 0; } } /** * Computes the viewport translation and applies it to the * {@link InfiniteCanvas} of the host's viewer using the * {@link ViewportPolicy}. */ protected void updateScrollPosition() { double scrollAmount = getScrollAmountPerSecond(); double dx = ((totalMillisLeft + currentMillisLeft) / 1000d) * scrollAmount - ((totalMillisRight + currentMillisRight) / 1000d) * scrollAmount; double dy = ((totalMillisUp + currentMillisUp) / 1000d) * scrollAmount - ((totalMillisDown + currentMillisDown) / 1000d) * scrollAmount; viewportPolicy.scroll(false, dx, dy); // restrict panning to contents if (isContentRestricted()) { panningSupport.removeFreeSpace(viewportPolicy, Pos.TOP_LEFT, true); panningSupport.removeFreeSpace(viewportPolicy, Pos.BOTTOM_RIGHT, false); } } }