/******************************************************************************* * 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) - Support for focus listener notification * *******************************************************************************/ package org.eclipse.gef.fx.swt.canvas; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.security.AccessControlContext; import java.security.AccessController; import java.security.PrivilegedAction; import java.util.ArrayList; import java.util.LinkedList; import java.util.List; import java.util.Queue; import java.util.Stack; import org.eclipse.gef.common.reflect.ReflectionUtils; import org.eclipse.swt.SWT; import org.eclipse.swt.events.DisposeEvent; import org.eclipse.swt.events.DisposeListener; import org.eclipse.swt.events.GestureEvent; import org.eclipse.swt.events.GestureListener; import org.eclipse.swt.events.KeyEvent; import org.eclipse.swt.events.KeyListener; import org.eclipse.swt.events.MouseWheelListener; import org.eclipse.swt.events.TraverseEvent; import org.eclipse.swt.events.TraverseListener; import org.eclipse.swt.graphics.ImageData; import org.eclipse.swt.graphics.Point; import org.eclipse.swt.widgets.Canvas; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Listener; import javafx.application.Platform; import javafx.beans.value.ChangeListener; import javafx.beans.value.ObservableValue; import javafx.embed.swt.FXCanvas; import javafx.embed.swt.SWTFXUtils; import javafx.event.Event; import javafx.event.EventDispatchChain; import javafx.event.EventDispatcher; import javafx.event.EventType; import javafx.scene.Cursor; import javafx.scene.ImageCursor; import javafx.scene.Scene; import javafx.scene.input.RotateEvent; import javafx.scene.input.ScrollEvent; import javafx.scene.input.SwipeEvent; import javafx.scene.input.ZoomEvent; import javafx.stage.Window; /** * A replacement of {@link FXCanvas} that fixes the following issues: * <ul> * <li>JDK-8088147 - [SWT] FXCanvas: implement custom cursors [workaround for * JavaSE-1.8 only, as fixed by SWTCursors in JavaSE-1.9]</li> * <li>JDK-8161282 - FXCanvas does not forward horizontal mouse scroll events to * the embedded scene. [workaround for JavaSE-1.8 only, as fixed by FXCanvas in * JavaSE-1.9]</li> * <li>JDK-8143596 - FXCanvas does not forward touch gestures to embedded scene. * [workaround for JavaSE-1.8 only, as fixed by FXCanvas in JavaSE-1.9]</li> * <li>JDK-8159227 - FXCanvas should properly forward consumption state of key * events from SWT to embedded scene.</li> * <li>JDK-8161587 - FXCanvas does not consistently render the scene graph when * long running event handlers are used.</li> * <li>JDK-8088862 - Provide possibility to traverse focus out of FX scene.</li> * </ul> * * @author anyssen * */ public class FXCanvasEx extends FXCanvas { // FIXME: Use different EventDispatcher for different scenarios (Java 8 vs. // Java 9, and Windows vs. other platforms) for maximum performance. private final class EventDispatcherEx implements EventDispatcher { private static final int REDRAW_INTERVAL_MILLIS = 40; // i.e. 25 fps private EventDispatcher delegate; private long lastRedrawMillis = System.currentTimeMillis(); private org.eclipse.swt.widgets.Event downEvent; protected EventDispatcherEx(EventDispatcher delegate) { this.delegate = delegate; } @Override public Event dispatchEvent(final Event event, final EventDispatchChain tail) { if (JAVA_8) { // XXX: Ensure key events that result from to be ignored SWT key // events (doit == false) are forwarded as consumed // (https://bugs.openjdk.java.net/browse/JDK-8159227) // TODO: Remove when dropping support for JavaSE-1.8. if (event instanceof javafx.scene.input.KeyEvent) { org.eclipse.swt.widgets.Event lastDownEvent = unprocessedKeyDownEvents .peek(); if (event.getEventType() .equals(javafx.scene.input.KeyEvent.KEY_PRESSED)) { if (!lastDownEvent.doit) { event.consume(); } // remove key down event and save it so that its doit // flag can be checked in case a KEY_TYPED event is // generated for it downEvent = unprocessedKeyDownEvents.poll(); // System.out.println("pressed " // + ((javafx.scene.input.KeyEvent) event) // .getCode() // + " :: " + "down=" // + unprocessedKeyDownEvents.size() + ", up=" // + unprocessedKeyUpEvents.size()); } else if (event.getEventType() .equals(javafx.scene.input.KeyEvent.KEY_TYPED)) { // consume event if last key down event was consumed if (!downEvent.doit) { event.consume(); } // System.out.println("typed " // + ((javafx.scene.input.KeyEvent) event) // .getCharacter() // + " :: " + "down=" // + unprocessedKeyDownEvents.size() + ", up=" // + unprocessedKeyUpEvents.size()); } else if (event.getEventType() .equals(javafx.scene.input.KeyEvent.KEY_RELEASED)) { // remove key up event org.eclipse.swt.widgets.Event lastUpEvent = unprocessedKeyUpEvents .poll(); if (!lastUpEvent.doit) { event.consume(); } // System.out.println("released " // + ((javafx.scene.input.KeyEvent) event) // .getCode() // + " :: " + "down=" // + unprocessedKeyDownEvents.size() + ", up=" // + unprocessedKeyUpEvents.size()); } } } // dispatch the most recent event Event returnedEvent = delegate.dispatchEvent(event, tail); // update UI (added to fix // https://bugs.openjdk.java.net/browse/JDK-8161587) long millisNow = System.currentTimeMillis(); if (millisNow - lastRedrawMillis > REDRAW_INTERVAL_MILLIS) { redraw(); if (WIN32) { // XXX: Only call update() on some platforms to prevent a // loss of performance while keeping the UI up-to-date. update(); } lastRedrawMillis = millisNow; } // return dispatched event return returnedEvent; } protected EventDispatcher dispose() { EventDispatcher d = delegate; delegate = null; return d; } } /** * The {@link ISceneRunnable} interface provides a callback method that is * invoked in a privileged runnable on the JavaFX application thread. The * callback is provided with a {@link TKSceneListenerWrapper} that can be * used to send events to JavaFX. * * @author mwienand * */ private interface ISceneRunnable { /** * Callback method that is called in a privileged runnable on the JavaFX * application thread. * * @param sceneListener * The TKSceneListenerWrapper that can be used to send events * to JavaFX. */ public void run(TKSceneListenerWrapper sceneListener); } // XXX: This class is used to wrap a com.sun.javafx.tk.TKSceneListener // object, so respective methods can be called on it via reflection without // introducing compile-time dependencies. // TODO: Remove when dropping support for JavaSE-1.8 private class TKSceneListenerWrapper { private Object tkSceneListener; private TKSceneListenerWrapper(Object tkSceneListener) { this.tkSceneListener = tkSceneListener; } public void rotateEvent(EventType<RotateEvent> eventType, double angle, double totalAngle, double x, double y, double screenX, double screenY, boolean _shiftDown, boolean _controlDown, boolean _altDown, boolean _metaDown, boolean _direct, boolean _inertia) { try { Method m = tkSceneListener.getClass().getDeclaredMethod( "rotateEvent", EventType.class, double.class, double.class, double.class, double.class, double.class, double.class, boolean.class, boolean.class, boolean.class, boolean.class, boolean.class, boolean.class); m.setAccessible(true); m.invoke(tkSceneListener, eventType, angle, totalAngle, x, y, screenX, screenY, _shiftDown, _controlDown, _altDown, _metaDown, _direct, _inertia); } catch (InvocationTargetException e) { Throwable targetException = e.getCause(); if (targetException instanceof RuntimeException) { throw ((RuntimeException) targetException); } else { targetException.printStackTrace(); } } catch (NoSuchMethodException | SecurityException | IllegalAccessException | IllegalArgumentException e) { e.printStackTrace(); } } public void scrollEvent(EventType<ScrollEvent> eventType, double scrollX, double scrollY, double totalScrollX, double totalScrollY, double xMultiplier, double yMultiplier, int touchCount, int scrollTextX, int scrollTextY, int defaultTextX, int defaultTextY, double x, double y, double screenX, double screenY, boolean _shiftDown, boolean _controlDown, boolean _altDown, boolean _metaDown, boolean _direct, boolean _inertia) { try { Method m = tkSceneListener.getClass().getDeclaredMethod( "scrollEvent", EventType.class, double.class, double.class, double.class, double.class, double.class, double.class, int.class, int.class, int.class, int.class, int.class, double.class, double.class, double.class, double.class, boolean.class, boolean.class, boolean.class, boolean.class, boolean.class, boolean.class); m.setAccessible(true); m.invoke(tkSceneListener, eventType, scrollX, scrollY, totalScrollX, totalScrollY, xMultiplier, yMultiplier, touchCount, scrollTextX, scrollTextY, defaultTextX, defaultTextY, x, y, screenX, screenY, _shiftDown, _controlDown, _altDown, _metaDown, _direct, _inertia); } catch (InvocationTargetException e) { Throwable targetException = e.getCause(); if (targetException instanceof RuntimeException) { throw ((RuntimeException) targetException); } else { targetException.printStackTrace(); } } catch (NoSuchMethodException | SecurityException | IllegalAccessException | IllegalArgumentException e) { e.printStackTrace(); } } public void swipeEvent(EventType<SwipeEvent> eventType, int touchCount, double x, double y, double screenX, double screenY, boolean _shiftDown, boolean _controlDown, boolean _altDown, boolean _metaDown, boolean _direct) { try { Method m = tkSceneListener.getClass().getDeclaredMethod( "swipeEvent", EventType.class, int.class, double.class, double.class, double.class, double.class, boolean.class, boolean.class, boolean.class, boolean.class, boolean.class); m.setAccessible(true); m.invoke(tkSceneListener, eventType, touchCount, x, y, screenX, screenY, _shiftDown, _controlDown, _altDown, _metaDown, _direct); } catch (InvocationTargetException e) { Throwable targetException = e.getCause(); if (targetException instanceof RuntimeException) { throw ((RuntimeException) targetException); } else { targetException.printStackTrace(); } } catch (NoSuchMethodException | SecurityException | IllegalAccessException | IllegalArgumentException e) { e.printStackTrace(); } } public void zoomEvent(EventType<ZoomEvent> eventType, double zoomFactor, double totalZoomFactor, double x, double y, double screenX, double screenY, boolean _shiftDown, boolean _controlDown, boolean _altDown, boolean _metaDown, boolean _direct, boolean _inertia) { try { Method m = tkSceneListener.getClass().getDeclaredMethod( "zoomEvent", EventType.class, double.class, double.class, double.class, double.class, double.class, double.class, boolean.class, boolean.class, boolean.class, boolean.class, boolean.class, boolean.class); m.setAccessible(true); m.invoke(tkSceneListener, eventType, zoomFactor, totalZoomFactor, x, y, screenX, screenY, _shiftDown, _controlDown, _altDown, _metaDown, _direct, _inertia); } catch (InvocationTargetException e) { Throwable targetException = e.getCause(); if (targetException instanceof RuntimeException) { throw ((RuntimeException) targetException); } else { targetException.printStackTrace(); } } catch (NoSuchMethodException | SecurityException | IllegalAccessException | IllegalArgumentException e) { e.printStackTrace(); } } } private static final boolean JAVA_8 = System.getProperty("java.version") .startsWith("1.8.0"); private static final boolean WIN32 = SWT.getPlatform().equals("win32"); /** * Returns the {@link FXCanvas} which contains the given {@link Scene}. * Therefore, it is only valid to call this method for a {@link Scene} which * is embedded into an SWT application via {@link FXCanvas}. * * @param scene * The {@link Scene} for which to determine the surrounding * {@link FXCanvas}. * @return The {@link FXCanvas} which contains the given {@link Scene}. */ public static FXCanvas getFXCanvas(Scene scene) { if (scene == null) { return null; } if (JAVA_8) { // Obtain FXCanvas by accessing outer class // of FXCanvas$HostContainer // TODO: Remove this when dropping support for J2SE-1.8 Window window = scene.getWindow(); if (window != null) { return ReflectionUtils.getPrivateFieldValue(ReflectionUtils .<Object> getPrivateFieldValue(window, "host"), "this$0"); } return null; } else { // On J2SE-1.9, retrieve FXCanvas through // FXCanvas.getFXCanvas(Scene), which was added for this // purpose. // TODO: Turn into an explicit call when dropping support // for J2SE-1.8. try { Method m = FXCanvas.class.getDeclaredMethod("getFXCanvas", Scene.class); return (FXCanvas) m.invoke(null, scene); } catch (Exception e) { throw new IllegalStateException( "Failed to call FXCanvas.getFXCanvas(Scene)", e); } } } // XXX: SWTCursors does not support image cursors up to JavaSE-1.9 // (https://bugs.openjdk.java.net/browse/JDK-8088147); this listener // provides a workaround for J2SE-1.8. It relies on JDK internals and may // access these only via pure reflection to not introduce any compile-time // dependencies (that would not work in a JIGSAW context). // TODO: Remove when dropping support for JavaSE-1.8. private ChangeListener<Cursor> cursorChangeListener = new ChangeListener<Cursor>() { @Override public void changed(ObservableValue<? extends Cursor> observable, Cursor oldCursor, Cursor newCursor) { if (newCursor instanceof ImageCursor) { // custom cursor, convert image ImageData imageData = SWTFXUtils.fromFXImage( ((ImageCursor) newCursor).getImage(), null); double hotspotX = ((ImageCursor) newCursor).getHotspotX(); double hotspotY = ((ImageCursor) newCursor).getHotspotY(); org.eclipse.swt.graphics.Cursor swtCursor = new org.eclipse.swt.graphics.Cursor( getDisplay(), imageData, (int) hotspotX, (int) hotspotY); try { Method currentCursorFrameAccessor = Cursor.class .getDeclaredMethod("getCurrentFrame", new Class[] {}); currentCursorFrameAccessor.setAccessible(true); Object currentCursorFrame = currentCursorFrameAccessor .invoke(newCursor, new Object[] {}); // there is a spelling-mistake in the internal API // (setPlatformCursor -> setPlatforCursor) Method platformCursorProvider = currentCursorFrame .getClass().getMethod("setPlatforCursor", new Class[] { Class.class, Object.class }); platformCursorProvider.setAccessible(true); platformCursorProvider.invoke(currentCursorFrame, org.eclipse.swt.graphics.Cursor.class, swtCursor); } catch (Exception e) { System.err.println( "Failed to set platform cursor on the current cursor frame."); e.printStackTrace(); } } } }; private Listener mouseWheelListener = new Listener() { @Override public void handleEvent(org.eclipse.swt.widgets.Event e) { if (!gestureActive && (!panGestureInertiaActive || lastGestureEvent == null || e.time != lastGestureEvent.time)) { if (e.type == SWT.MouseVerticalWheel) { sendScrollEventToFX(ScrollEvent.SCROLL, 0, e.count > 0 ? 1 : -1, e.x, e.y, e.stateMask); } else { sendScrollEventToFX(ScrollEvent.SCROLL, e.count > 0 ? 1 : -1, 0, e.x, e.y, e.stateMask); } } } private void sendScrollEventToFX(final EventType<ScrollEvent> eventType, double scrollX, double scrollY, int x, int y, int stateMask) { // granularity for mouse wheel scroll events is more // coarse-grained than for pan gesture events final double multiplier = 40.0; final Point los = toDisplay(x, y); scheduleSceneRunnable(new ISceneRunnable() { @Override public void run(TKSceneListenerWrapper sceneListener) { sceneListener.scrollEvent(eventType, scrollX, scrollY, scrollX, scrollY, multiplier, multiplier, 0, 0, 0, 0, 0, x, y, los.x, los.y, (stateMask & SWT.SHIFT) != 0, (stateMask & SWT.CONTROL) != 0, (stateMask & SWT.ALT) != 0, (stateMask & SWT.COMMAND) != 0, false, false); } }); } }; // including inertia events) private boolean gestureActive = false; // true while inertia events of a pan gesture might be processed private boolean panGestureInertiaActive = false; // the last gesture event that was received (may also be an inertia event) private GestureEvent lastGestureEvent; private GestureListener gestureListener = new GestureListener() { // used to keep track of which (atomic) gestures are enclosed private Stack<Integer> nestedGestures = new Stack<>(); // data used to compute inertia values for pan gesture events (as SWT // does not provide these) private long inertiaTime = 0; private double inertiaXScroll = 0.0; private double inertiaYScroll = 0.0; // used to compute zoom deltas, which are not provided by SWT private double lastTotalZoom = 0.0; private double lastTotalAngle = 0.0; double totalScrollX = 0; double totalScrollY = 0; @Override public void gesture(GestureEvent gestureEvent) { // An SWT gesture may be compound, comprising several MAGNIFY, PAN, // and ROTATE events, which are enclosed by a // generic BEGIN and END event (while SWIPE events occur without // being enclosed). // In JavaFX, such a compound gesture is represented through // (possibly nested) atomic gestures, which all // (again excluding swipe) have their specific START and FINISH // events. // While a complex SWT gesture is active, we therefore have to // generate START events for atomic gestures as // needed, finishing them all when the compound SWT gesture ends (in // the reverse order they were started), // after which we still process inertia events (that only seem to // occur for PAN). SWIPE events may simply be // forwarded. switch (gestureEvent.detail) { case SWT.GESTURE_BEGIN: // a (complex) gesture has started gestureActive = true; // we are within an active gesture, so no inertia processing now panGestureInertiaActive = false; break; case SWT.GESTURE_MAGNIFY: // emulate the start of an atomic gesture if (gestureActive && !nestedGestures.contains(SWT.GESTURE_MAGNIFY)) { sendZoomEventToFX(ZoomEvent.ZOOM_STARTED, gestureEvent); nestedGestures.push(SWT.GESTURE_MAGNIFY); } sendZoomEventToFX(ZoomEvent.ZOOM, gestureEvent); break; case SWT.GESTURE_PAN: // emulate the start of an atomic gesture if (gestureActive && !nestedGestures.contains(SWT.GESTURE_PAN)) { sendScrollEventToFX(ScrollEvent.SCROLL_STARTED, gestureEvent.xDirection, gestureEvent.yDirection, gestureEvent.x, gestureEvent.y, gestureEvent.stateMask, false); nestedGestures.push(SWT.GESTURE_PAN); } // SWT does not flag inertia events and does not allow to // distinguish emulated PAN gesture events // (resulting from mouse wheel interaction) from native ones // (resulting from touch device interaction); // as it will always send both, mouse wheel as well as PAN // gesture events when using the touch device or // the mouse wheel, we can identify native PAN gesture inertia // events only based on their temporal relationship // to the preceding gesture event. if (panGestureInertiaActive && gestureEvent.time > lastGestureEvent.time + 250) { panGestureInertiaActive = false; } if (gestureActive || panGestureInertiaActive) { double xDirection = gestureEvent.xDirection; double yDirection = gestureEvent.yDirection; if (panGestureInertiaActive) { // calculate inertia values for scrollX and scrollY, as // SWT (at least on MacOSX) provides zero values if (xDirection == 0 && yDirection == 0) { double delta = Math.max(0.0, Math.min(1.0, (gestureEvent.time - inertiaTime) / 1500.0)); xDirection = (1.0 - delta) * inertiaXScroll; yDirection = (1.0 - delta) * inertiaYScroll; } } sendScrollEventToFX(ScrollEvent.SCROLL, xDirection, yDirection, gestureEvent.x, gestureEvent.y, gestureEvent.stateMask, panGestureInertiaActive); } break; case SWT.GESTURE_ROTATE: // emulate the start of an atomic gesture if (gestureActive && !nestedGestures.contains(SWT.GESTURE_ROTATE)) { sendRotateEventToFX(RotateEvent.ROTATION_STARTED, gestureEvent); nestedGestures.push(SWT.GESTURE_ROTATE); } sendRotateEventToFX(RotateEvent.ROTATE, gestureEvent); break; case SWT.GESTURE_SWIPE: EventType<SwipeEvent> type = null; if (gestureEvent.yDirection > 0) { type = SwipeEvent.SWIPE_DOWN; } else if (gestureEvent.yDirection < 0) { type = SwipeEvent.SWIPE_UP; } else if (gestureEvent.xDirection > 0) { type = SwipeEvent.SWIPE_RIGHT; } else if (gestureEvent.xDirection < 0) { type = SwipeEvent.SWIPE_LEFT; } sendSwipeEventToFX(type, gestureEvent); break; case SWT.GESTURE_END: // finish atomic gesture(s) in reverse order of their start; // SWIPE may be ignored, // as JavaFX (like SWT) does not recognize it as a gesture while (!nestedGestures.isEmpty()) { switch (nestedGestures.pop()) { case SWT.GESTURE_MAGNIFY: sendZoomEventToFX(ZoomEvent.ZOOM_FINISHED, gestureEvent); break; case SWT.GESTURE_PAN: sendScrollEventToFX(ScrollEvent.SCROLL_FINISHED, gestureEvent.xDirection, gestureEvent.yDirection, gestureEvent.x, gestureEvent.y, gestureEvent.stateMask, false); // use the scroll values of the preceding scroll event // to compute values for inertia events inertiaXScroll = lastGestureEvent.xDirection; inertiaYScroll = lastGestureEvent.yDirection; inertiaTime = gestureEvent.time; // from now on, inertia events may occur panGestureInertiaActive = true; break; case SWT.GESTURE_ROTATE: sendRotateEventToFX(RotateEvent.ROTATION_FINISHED, gestureEvent); break; } } // compound SWT gesture has ended gestureActive = false; break; default: throw new IllegalStateException( "Unsupported gesture event type: " + gestureEvent); } // keep track of currently received gesture event; this is needed to // identify inertia events lastGestureEvent = gestureEvent; } private void sendRotateEventToFX(EventType<RotateEvent> eventType, GestureEvent gestureEvent) { Point los = toDisplay(gestureEvent.x, gestureEvent.y); // XXX: SWT uses negative angle values to indicate clockwise // rotation, while JavaFX uses positive ones. We thus have to invert // the values here double[] totalAngle = { -gestureEvent.rotation }; if (eventType == RotateEvent.ROTATION_STARTED) { totalAngle[0] = lastTotalAngle = 0.0; } else if (eventType == RotateEvent.ROTATION_FINISHED) { // SWT uses 0.0 for final event, while JavaFX still provides a // (total) rotation value totalAngle[0] = lastTotalAngle; } final double angle = eventType == RotateEvent.ROTATION_FINISHED ? 0.0 : totalAngle[0] - lastTotalAngle; lastTotalAngle = totalAngle[0]; scheduleSceneRunnable(new ISceneRunnable() { @Override public void run(TKSceneListenerWrapper sceneListener) { sceneListener.rotateEvent(eventType, angle, totalAngle[0], gestureEvent.x, gestureEvent.y, los.x, los.y, (gestureEvent.stateMask & SWT.SHIFT) != 0, (gestureEvent.stateMask & SWT.CONTROL) != 0, (gestureEvent.stateMask & SWT.ALT) != 0, (gestureEvent.stateMask & SWT.COMMAND) != 0, false, !gestureActive); } }); } private void sendScrollEventToFX(EventType<ScrollEvent> eventType, double scrollX, double scrollY, int x, int y, int stateMask, boolean inertia) { // up to and including SWT 4.5, direction was inverted for pan // gestures on the Mac // (see https://bugs.eclipse.org/bugs/show_bug.cgi?id=481331) final double multiplier = ("cocoa".equals(SWT.getPlatform()) && SWT.getVersion() < 4600) ? -5.0 : 5.0; if (eventType == ScrollEvent.SCROLL_STARTED) { totalScrollX = 0; totalScrollY = 0; } else if (inertia) { // inertia events do not belong to the gesture, // thus total scroll is not accumulated totalScrollX = scrollX; totalScrollY = scrollY; } else { // accumulate total scroll as long as the gesture occurs totalScrollX += scrollX; totalScrollY += scrollY; } final Point los = toDisplay(x, y); scheduleSceneRunnable(new ISceneRunnable() { @Override public void run(TKSceneListenerWrapper sceneListener) { sceneListener.scrollEvent(eventType, scrollX, scrollY, totalScrollX, totalScrollY, multiplier, multiplier, 0, 0, 0, 0, 0, x, y, los.x, los.y, (stateMask & SWT.SHIFT) != 0, (stateMask & SWT.CONTROL) != 0, (stateMask & SWT.ALT) != 0, (stateMask & SWT.COMMAND) != 0, false, inertia); } }); } private void sendSwipeEventToFX(EventType<SwipeEvent> eventType, GestureEvent gestureEvent) { final Point los = toDisplay(gestureEvent.x, gestureEvent.y); scheduleSceneRunnable(new ISceneRunnable() { @Override public void run(TKSceneListenerWrapper sceneListener) { sceneListener.swipeEvent(eventType, 0, gestureEvent.x, gestureEvent.y, los.x, los.y, (gestureEvent.stateMask & SWT.SHIFT) != 0, (gestureEvent.stateMask & SWT.CONTROL) != 0, (gestureEvent.stateMask & SWT.ALT) != 0, (gestureEvent.stateMask & SWT.COMMAND) != 0, false); } }); } private void sendZoomEventToFX(EventType<ZoomEvent> eventType, GestureEvent gestureEvent) { Point los = toDisplay(gestureEvent.x, gestureEvent.y); double[] totalZoom = new double[] { gestureEvent.magnification }; if (eventType == ZoomEvent.ZOOM_STARTED) { // ensure first event does not provide any zoom yet totalZoom[0] = lastTotalZoom = 1.0; } else if (eventType == ZoomEvent.ZOOM_FINISHED) { // SWT uses 0.0 for final event, while JavaFX still provides a // (total) zoom value totalZoom[0] = lastTotalZoom; } final double zoom = eventType == ZoomEvent.ZOOM_FINISHED ? 1.0 : totalZoom[0] / lastTotalZoom; lastTotalZoom = totalZoom[0]; final boolean inertia = !gestureActive; scheduleSceneRunnable(new ISceneRunnable() { @Override public void run(TKSceneListenerWrapper sceneListener) { sceneListener.zoomEvent(eventType, zoom, totalZoom[0], gestureEvent.x, gestureEvent.y, los.x, los.y, (gestureEvent.stateMask & SWT.SHIFT) != 0, (gestureEvent.stateMask & SWT.CONTROL) != 0, (gestureEvent.stateMask & SWT.ALT) != 0, (gestureEvent.stateMask & SWT.COMMAND) != 0, false, inertia); } }); } }; private TraverseListener traverseListener = null; private DisposeListener disposeListener; // XXX: JavaFX does not forward the consumption state of key events to the // embedded scene (see https://bugs.openjdk.java.net/browse/JDK-8159227). // We use an SWT listener to capture all key events, so our JavaFX event // dispatcher replacement can identify the resulting JavaFX key events that // result from them and can mark them as consumed. // We have to ensure that the (typed) super key listener, which forwards // events to the embedded scene, is the last listener (in the list of all // key listeners, including untyped ones) that is notified. The manipulation // of the doit flag by listeners that got notified later, would otherwise // not be recognized. private Listener keyListener = new Listener() { @Override public void handleEvent(org.eclipse.swt.widgets.Event e) { if (allSwtKeyEvents.isEmpty()) { throw new IllegalStateException( "Handler called but filter did not record any events."); } // dispatch previous events while (!sameEvent(allSwtKeyEvents.peek(), e)) { org.eclipse.swt.widgets.Event previousEvent = allSwtKeyEvents .poll(); // set doit to false to indicate that the event was already // processed previousEvent.doit = false; if (SWT.KeyDown == previousEvent.type) { unprocessedKeyDownEvents.add(previousEvent); for (Listener l : new ArrayList<>(keyDownListeners)) { l.handleEvent(previousEvent); } superKeyListener.keyPressed(new KeyEvent(previousEvent)); } else if (SWT.KeyUp == previousEvent.type) { unprocessedKeyUpEvents.add(previousEvent); for (Listener l : new ArrayList<>(keyUpListeners)) { l.handleEvent(previousEvent); } superKeyListener.keyReleased(new KeyEvent(previousEvent)); } } // remove e from allSwtKeyEvents allSwtKeyEvents.poll(); // dispatch e if (SWT.KeyDown == e.type) { unprocessedKeyDownEvents.add(e); for (Listener l : new ArrayList<>(keyDownListeners)) { l.handleEvent(e); } superKeyListener.keyPressed(new KeyEvent(e)); } else if (SWT.KeyUp == e.type) { unprocessedKeyUpEvents.add(e); for (Listener l : new ArrayList<>(keyUpListeners)) { l.handleEvent(e); } superKeyListener.keyReleased(new KeyEvent(e)); } } private boolean sameEvent(org.eclipse.swt.widgets.Event e, org.eclipse.swt.widgets.Event f) { return e.display == f.display && e.widget == f.widget && e.time == f.time && e.data == f.data && e.character == f.character && e.keyCode == f.keyCode && e.keyLocation == f.keyLocation && e.stateMask == f.stateMask && e.doit == f.doit; }; }; private KeyListener superKeyListener; private List<Listener> keyUpListeners = new ArrayList<>(); private List<Listener> keyDownListeners = new ArrayList<>(); // keeps track of key events that need to be marked as consumed private Queue<org.eclipse.swt.widgets.Event> unprocessedKeyDownEvents = new LinkedList<>(); private Queue<org.eclipse.swt.widgets.Event> unprocessedKeyUpEvents = new LinkedList<>(); private Queue<org.eclipse.swt.widgets.Event> allSwtKeyEvents = new LinkedList<>(); private Listener displayKeyFilter = new Listener() { private org.eclipse.swt.widgets.Event copy( org.eclipse.swt.widgets.Event event) { // create a new SWT Event org.eclipse.swt.widgets.Event copy = new org.eclipse.swt.widgets.Event(); // transfer general attributes copy.display = event.display; copy.widget = event.widget; copy.time = event.time; copy.type = event.type; copy.doit = event.doit; copy.data = event.data; // transfer keyboard attributes copy.character = event.character; copy.keyCode = event.keyCode; copy.keyLocation = event.keyLocation; copy.stateMask = event.stateMask; // ignored attributes // copy.button = event.button; // copy.count = event.count; // copy.detail = event.detail; // copy.end = event.end; // copy.gc = event.gc; // copy.height = event.height; // copy.index = event.index; // copy.item = event.item; // copy.magnification = event.magnification; // copy.rotation = event.rotation; // copy.segments = event.segments; // copy.segmentsChars = event.segmentsChars; // copy.start = event.start; // copy.text = event.text; // copy.touches = event.touches; // copy.width = event.width; // copy.x = event.x; // copy.xDirection = event.xDirection; // copy.y = event.y; // copy.yDirection = event.yDirection; return copy; } @Override public void handleEvent(org.eclipse.swt.widgets.Event event) { // XXX: Only consider events which target this FXCanvasEx if (event.widget != FXCanvasEx.this) { return; } // XXX: Copy event so that the original event data is not // compromised (e.g. "type" and "doit"). allSwtKeyEvents.add(copy(event)); } }; /** * Creates a new {@link FXCanvasEx} for the given parent and with the given * style. * * @param parent * The {@link Composite} to use as parent. * @param style * A combination of SWT styles to be applied. Note that the * {@link FXCanvas} constructor will set the * {@link SWT#NO_BACKGROUND} style before passing it to the * {@link Canvas} constructor. */ public FXCanvasEx(Composite parent, int style) { super(parent, style); // extract original filters Object filterTable = ReflectionUtils.getPrivateFieldValue(getDisplay(), "filterTable"); int[] types = ReflectionUtils.getPrivateFieldValue(filterTable, "types"); Listener[] listeners = ReflectionUtils.getPrivateFieldValue(filterTable, "listeners"); // clear filters ReflectionUtils.setPrivateFieldValue(getDisplay(), "filterTable", null); // hook our own filter getDisplay().addFilter(SWT.KeyDown, displayKeyFilter); getDisplay().addFilter(SWT.KeyUp, displayKeyFilter); // re-add original filters for (int i = 0; i < types.length && types[i] != 0; i++) { getDisplay().addFilter(types[i], listeners[i]); } // XXX: As FXCanvas uses a dispose listener, we have to use the same // mechanism here disposeListener = new DisposeListener() { @Override public void widgetDisposed(DisposeEvent de) { // XXX: unset the scene, so event dispatcher and cursor listener // are properly removed; // XXX: The super class will also unset the stage as a result of // unsetting the scene. The stagePeer will be unset through // the host container when the stage is set invisible (which // already happens through the dispose listener of the super // class). The embedded scene (scenePeer) will be unset through // the host container when unsetting the scene above; setScene(null); getDisplay().removeFilter(SWT.KeyDown, displayKeyFilter); getDisplay().removeFilter(SWT.KeyUp, displayKeyFilter); displayKeyFilter = null; cursorChangeListener = null; removeDisposeListener(disposeListener); disposeListener = null; removeTraverseListener(traverseListener); traverseListener = null; removeListener(SWT.KeyDown, keyListener); removeListener(SWT.KeyUp, keyListener); keyListener = null; superKeyListener = null; if (JAVA_8) { removeListener(SWT.MouseHorizontalWheel, mouseWheelListener); removeListener(SWT.MouseVerticalWheel, mouseWheelListener); removeGestureListener(gestureListener); } mouseWheelListener = null; gestureListener = null; } }; addDisposeListener(disposeListener); // create traverse traverseListener = new TraverseListener() { @Override public void keyTraversed(TraverseEvent e) { if ((e.detail == SWT.TRAVERSE_TAB_NEXT || e.detail == SWT.TRAVERSE_TAB_PREVIOUS) && (e.stateMask & SWT.CTRL) != 0) { e.doit = true; } } }; addTraverseListener(traverseListener); // XXX: Use a delegate to ensure the super key listener is the last one // that gets notified, which is required to properly forward the // consumption state to the embedded scene (see // https://bugs.openjdk.java.net/browse/JDK-8159227). addListener(SWT.KeyUp, keyListener); addListener(SWT.KeyDown, keyListener); if (JAVA_8) { addListener(SWT.MouseHorizontalWheel, mouseWheelListener); addListener(SWT.MouseVerticalWheel, mouseWheelListener); addGestureListener(gestureListener); } } @Override public void addKeyListener(KeyListener listener) { // XXX: The workaround for JDK-8159227 requires that the typed key // listener, registered by the superclass, is ignored. if (listener.getClass().getName() .startsWith(FXCanvas.class.getName() + "$")) { superKeyListener = listener; } else { super.addKeyListener(listener); } } @Override public void addListener(int eventType, Listener listener) { // XXX: Overwritten to ensure proper ordering of key listeners, // which is required to properly forward the consumption state to // the embedded scene (see // https://bugs.openjdk.java.net/browse/JDK-8159227). if (eventType == SWT.KeyUp) { if (listener == keyListener) { super.addListener(eventType, listener); } else { keyUpListeners.add(listener); } } else if (eventType == SWT.KeyDown) { if (listener == keyListener) { super.addListener(eventType, listener); } else { keyDownListeners.add(listener); } } else { super.addListener(eventType, listener); } } @Override public void addMouseWheelListener(MouseWheelListener listener) { // XXX: The workaround for JDK-8161282 requires, that the typed mouse // wheel listener, registered by the JavaSE-1.8 superclass, is ignored; // the JavaSE-1.8 workaround and the fix within the JavaSE-1.9 // superclass both use an untyped listener. if (JAVA_8) { if (!listener.getClass().getName() .startsWith(FXCanvas.class.getName() + "$")) { super.addMouseWheelListener(listener); } } else { super.addMouseWheelListener(listener); } } /** * Returns the stage {@link Window} hold by this {@link FXCanvas}. * * @return The stage {@link Window}. */ public Window getStage() { return ReflectionUtils.getPrivateFieldValue(this, "stage"); } @Override public void removeKeyListener(KeyListener listener) { // XXX: The workaround for JDK-8159227 requires that the typed key // listener, registered by the superclass, is ignored. if (listener.getClass().getName() .startsWith(FXCanvas.class.getName() + "$")) { superKeyListener = null; } else { super.removeKeyListener(listener); } } @Override public void removeListener(int eventType, Listener listener) { // XXX: Overwritten to ensure proper ordering of key listeners, // which is required to properly forward the consumption state to // the embedded scene (see // https://bugs.openjdk.java.net/browse/JDK-8159227). if (eventType == SWT.KeyUp) { if (listener == keyListener) { super.removeListener(eventType, listener); } else { keyUpListeners.remove(listener); } } else if (eventType == SWT.KeyDown) { if (listener == keyListener) { super.removeListener(eventType, listener); } else { keyDownListeners.remove(listener); } } else { super.removeListener(eventType, listener); } } @Override public void removeMouseWheelListener(MouseWheelListener listener) { // XXX: The workaround for JDK-8161282 requires, that the typed mouse // wheel listener, registered by the JavaSE-1.8 superclass, is ignored; // the JavaSE-1.8 workaround and the fix within the JavaSE-1.9 // superclass both use an untyped listener. if (JAVA_8) { if (!listener.getClass().getName() .startsWith(FXCanvas.class.getName() + "$")) { super.removeMouseWheelListener(listener); } } else { super.removeMouseWheelListener(listener); } } /** * Schedules the given {@link ISceneRunnable} for execution in a privileged * runnable on the JavaFX application thread. * * @param sr * The {@link ISceneRunnable} that will be executed in a * privileged runnable on the JavaFX application thread. */ private void scheduleSceneRunnable(final ISceneRunnable sr) { Platform.runLater(new Runnable() { @Override public void run() { final Object scenePeer = ReflectionUtils .getPrivateFieldValue(FXCanvasEx.this, "scenePeer"); AccessController.doPrivileged(new PrivilegedAction<Void>() { @Override public Void run() { Object sceneListener = ReflectionUtils .getPrivateFieldValue(scenePeer, "sceneListener"); if (sceneListener == null) { return null; } sr.run(new TKSceneListenerWrapper(sceneListener)); return null; } }, (AccessControlContext) ReflectionUtils .getPrivateFieldValue(scenePeer, "accessCtrlCtx")); } }); } @Override public void setScene(Scene newScene) { Scene oldScene = getScene(); if (oldScene != null) { // restore original event dispatcher EventDispatcher eventDispatcher = oldScene.getEventDispatcher(); if (eventDispatcher instanceof EventDispatcherEx) { oldScene.setEventDispatcher( ((EventDispatcherEx) eventDispatcher).dispose()); // TODO: add listener to property to keep track of changes to // event dispatcher that removes our delegate?? (throw an // exception?) } if (JAVA_8) { oldScene.cursorProperty().removeListener(cursorChangeListener); } } super.setScene(newScene); if (newScene != null) { // wrap event dispatcher newScene.setEventDispatcher( new EventDispatcherEx(newScene.getEventDispatcher())); if (JAVA_8) { newScene.cursorProperty().addListener(cursorChangeListener); } } } }