/*
 * Copyright (c) 2013, 2018, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

package javafx.embed.swing;

import javax.swing.JComponent;
import javax.swing.Timer;
import java.awt.AWTEvent;
import java.awt.EventQueue;
import java.awt.Toolkit;
import java.awt.event.InputEvent;
import java.awt.event.MouseWheelEvent;
import java.awt.event.WindowEvent;
import java.awt.event.WindowFocusListener;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.locks.ReentrantLock;
import javafx.beans.InvalidationListener;
import javafx.beans.value.ChangeListener;
import javafx.event.Event;
import javafx.event.EventHandler;
import javafx.event.EventType;
import javafx.geometry.Point2D;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
import javafx.scene.input.ScrollEvent;
import javafx.stage.Window;
import java.lang.ref.WeakReference;
import java.nio.IntBuffer;
import java.security.AccessController;
import java.security.PrivilegedAction;
import com.sun.javafx.embed.swing.Disposer;
import com.sun.javafx.embed.swing.DisposerRecord;
import com.sun.javafx.geom.BaseBounds;
import com.sun.javafx.geom.transform.BaseTransform;
import com.sun.javafx.scene.DirtyBits;
import com.sun.javafx.sg.prism.NGExternalNode;
import com.sun.javafx.sg.prism.NGNode;
import com.sun.javafx.stage.FocusUngrabEvent;
import com.sun.javafx.stage.WindowHelper;
import com.sun.javafx.tk.TKStage;
import com.sun.javafx.PlatformUtil;
import com.sun.javafx.scene.NodeHelper;
import static javafx.stage.WindowEvent.WINDOW_HIDDEN;

import com.sun.javafx.embed.swing.SwingNodeHelper;
import com.sun.javafx.embed.swing.SwingEvents;
import com.sun.javafx.embed.swing.InteropFactory;
import com.sun.javafx.embed.swing.SwingNodeInterop;

This class is used to embed a Swing content into a JavaFX application. The content to be displayed is specified with the setContent method that accepts an instance of Swing JComponent. The hierarchy of components contained in the JComponent instance should not contain any heavyweight components, otherwise SwingNode may fail to paint it. The content gets repainted automatically. All the input and focus events are forwarded to the JComponent instance transparently to the developer.

Here is a typical pattern which demonstrates how SwingNode can be used:

    public class SwingFx extends Application {
        @Override
        public void start(Stage stage) {
            final SwingNode swingNode = new SwingNode();
            createAndSetSwingContent(swingNode);
            StackPane pane = new StackPane();
            pane.getChildren().add(swingNode);
            stage.setScene(new Scene(pane, 100, 50));
            stage.show();
        }
        private void createAndSetSwingContent(final SwingNode swingNode) {
            SwingUtilities.invokeLater(new Runnable() {
                @Override
                public void run() {
                    swingNode.setContent(new JButton("Click me!"));
                }
            });
        }
        public static void main(String[] args) {
            launch(args);
        }
    }
Since:JavaFX 8.0
/** * This class is used to embed a Swing content into a JavaFX application. * The content to be displayed is specified with the {@link #setContent} method * that accepts an instance of Swing {@code JComponent}. The hierarchy of components * contained in the {@code JComponent} instance should not contain any heavyweight * components, otherwise {@code SwingNode} may fail to paint it. The content gets * repainted automatically. All the input and focus events are forwarded to the * {@code JComponent} instance transparently to the developer. * <p> * Here is a typical pattern which demonstrates how {@code SwingNode} can be used: * <pre> * public class SwingFx extends Application { * * &#064;Override * public void start(Stage stage) { * final SwingNode swingNode = new SwingNode(); * createAndSetSwingContent(swingNode); * * StackPane pane = new StackPane(); * pane.getChildren().add(swingNode); * * stage.setScene(new Scene(pane, 100, 50)); * stage.show(); * } * * private void createAndSetSwingContent(final SwingNode swingNode) { * SwingUtilities.invokeLater(new Runnable() { * &#064;Override * public void run() { * swingNode.setContent(new JButton("Click me!")); * } * }); * } * * public static void main(String[] args) { * launch(args); * } * } * </pre> * @since JavaFX 8.0 */
public class SwingNode extends Node { private static boolean isThreadMerged; private SwingNodeInterop swNodeIOP; private static InteropFactory iopFactoryInstance = null; static { try { iopFactoryInstance = InteropFactory.getInstance(); } catch (Exception e) { throw new ExceptionInInitializerError(e); } AccessController.doPrivileged(new PrivilegedAction<Object>() { public Object run() { isThreadMerged = Boolean.valueOf( System.getProperty("javafx.embed.singleThread")); return null; } }); // This is used by classes in different packages to get access to // private and package private methods. SwingNodeHelper.setSwingNodeAccessor(new SwingNodeHelper.SwingNodeAccessor() { @Override public NGNode doCreatePeer(Node node) { return ((SwingNode) node).doCreatePeer(); } @Override public void doUpdatePeer(Node node) { ((SwingNode) node).doUpdatePeer(); } @Override public BaseBounds doComputeGeomBounds(Node node, BaseBounds bounds, BaseTransform tx) { return ((SwingNode) node).doComputeGeomBounds(bounds, tx); } @Override public boolean doComputeContains(Node node, double localX, double localY) { return ((SwingNode) node).doComputeContains(localX, localY); } @Override public Object getLightweightFrame(SwingNode node) { return node.getLightweightFrame(); } @Override public ReentrantLock getPaintLock(SwingNode node) { return node.getPaintLock(); } @Override public void setImageBuffer(SwingNode node, final int[] data, final int x, final int y, final int w, final int h, final int linestride, final double scaleX, final double scaleY) { node.setImageBuffer(data, x, y, w, h, linestride, scaleX, scaleY); } @Override public void setImageBounds(SwingNode node, final int x, final int y, final int w, final int h) { node.setImageBounds(x, y, w, h); } @Override public void repaintDirtyRegion(SwingNode node, final int dirtyX, final int dirtyY, final int dirtyWidth, final int dirtyHeight) { node.repaintDirtyRegion(dirtyX, dirtyY, dirtyWidth, dirtyHeight); } @Override public void ungrabFocus(SwingNode node, boolean postUngrabEvent) { node.ungrabFocus(postUngrabEvent); } @Override public void setSwingPrefWidth(SwingNode node, int swingPrefWidth) { node.swingPrefWidth = swingPrefWidth; } @Override public void setSwingPrefHeight(SwingNode node, int swingPrefHeight) { node.swingPrefHeight = swingPrefHeight; } @Override public void setSwingMaxWidth(SwingNode node, int swingMaxWidth) { node.swingMaxWidth = swingMaxWidth; } @Override public void setSwingMaxHeight(SwingNode node, int swingMaxHeight) { node.swingMaxHeight = swingMaxHeight; } @Override public void setSwingMinWidth(SwingNode node, int swingMinWidth) { node.swingMinWidth = swingMinWidth; } @Override public void setSwingMinHeight(SwingNode node, int swingMinHeight) { node.swingMinHeight = swingMinHeight; } @Override public void setGrabbed(SwingNode node, boolean grab) { node.grabbed = grab; } }); } private double fxWidth; private double fxHeight; private int swingPrefWidth; private int swingPrefHeight; private int swingMaxWidth; private int swingMaxHeight; private int swingMinWidth; private int swingMinHeight; private volatile JComponent content; private volatile Object lwFrame; private final Object getLightweightFrame() { return lwFrame; } private NGExternalNode peer; private final ReentrantLock paintLock = new ReentrantLock(); private ReentrantLock getPaintLock() { return paintLock; } private boolean skipBackwardUnrgabNotification; private boolean grabbed; // lwframe initiated grab private Timer deactivate; // lwFrame deactivate delay for Linux { // To initialize the class helper at the begining each constructor of this class SwingNodeHelper.initHelper(this); }
Constructs a new instance of SwingNode.
/** * Constructs a new instance of {@code SwingNode}. */
public SwingNode() { swNodeIOP = iopFactoryInstance.createSwingNodeImpl(); setFocusTraversable(true); setEventHandler(MouseEvent.ANY, new SwingMouseEventHandler()); setEventHandler(KeyEvent.ANY, new SwingKeyEventHandler()); setEventHandler(ScrollEvent.SCROLL, new SwingScrollEventHandler()); focusedProperty().addListener((observable, oldValue, newValue) -> { activateLwFrame(newValue); }); //Workaround for RT-34170 javafx.scene.text.Font.getFamilies(); } private EventHandler windowHiddenHandler = (Event event) -> { if (lwFrame != null && event.getTarget() instanceof Window) { final Window w = (Window) event.getTarget(); TKStage tk = WindowHelper.getPeer(w); if (tk != null) { if (isThreadMerged) { swNodeIOP.overrideNativeWindowHandle(lwFrame, 0L, null); } else { // Postpone actual window closing to ensure that // a native window handler is valid on a Swing side tk.postponeClose(); SwingNodeHelper.runOnEDT(() -> { swNodeIOP.overrideNativeWindowHandle(lwFrame, 0L, (Runnable) () -> SwingNodeHelper.runOnFxThread( () -> tk.closePostponed())); }); } } } }; private Window hWindow = null; private void notifyNativeHandle(Window window) { if (hWindow != window) { if (hWindow != null) { hWindow.removeEventHandler(WINDOW_HIDDEN, windowHiddenHandler); } if (window != null) { window.addEventHandler(WINDOW_HIDDEN, windowHiddenHandler); } hWindow = window; } if (lwFrame != null) { long rawHandle = 0L; if (window != null) { TKStage tkStage = WindowHelper.getPeer(window); if (tkStage != null) { rawHandle = tkStage.getRawHandle(); } } swNodeIOP.overrideNativeWindowHandle(lwFrame, rawHandle, null); } }
Attaches a JComponent instance to display in this SwingNode.

The method can be called either on the JavaFX Application thread or the Event Dispatch thread. Note however, that access to a Swing component must occur from the Event Dispatch thread according to the Swing threading restrictions.

Params:
  • content – a Swing component to display in this SwingNode
See Also:
/** * Attaches a {@code JComponent} instance to display in this {@code SwingNode}. * <p> * The method can be called either on the JavaFX Application thread or the Event Dispatch thread. * Note however, that access to a Swing component must occur from the Event Dispatch thread * according to the Swing threading restrictions. * * @param content a Swing component to display in this {@code SwingNode} * * @see java.awt.EventQueue#isDispatchThread() * @see javafx.application.Platform#isFxApplicationThread() */
public void setContent(final JComponent content) { this.content = content; SwingNodeHelper.runOnEDT(() -> setContentImpl(content)); }
Returns the JComponent instance attached to this SwingNode.

The method can be called either on the JavaFX Application thread or the Event Dispatch thread. Note however, that access to a Swing component must occur from the Event Dispatch thread according to the Swing threading restrictions.

See Also:
Returns:the Swing component attached to this SwingNode
/** * Returns the {@code JComponent} instance attached to this {@code SwingNode}. * <p> * The method can be called either on the JavaFX Application thread or the Event Dispatch thread. * Note however, that access to a Swing component must occur from the Event Dispatch thread * according to the Swing threading restrictions. * * @see java.awt.EventQueue#isDispatchThread() * @see javafx.application.Platform#isFxApplicationThread() * * @return the Swing component attached to this {@code SwingNode} */
public JComponent getContent() { return content; } /* * Called on EDT */ private void setContentImpl(JComponent content) { if (lwFrame != null) { swNodeIOP.disposeFrame(lwFrame); lwFrame = null; } if (content != null) { lwFrame = swNodeIOP.createLightweightFrame(); SwingNodeWindowFocusListener snfListener = new SwingNodeWindowFocusListener(this); swNodeIOP.addWindowFocusListener(lwFrame, snfListener); if (getScene() != null) { Window window = getScene().getWindow(); if (window != null) { swNodeIOP.notifyDisplayChanged(lwFrame, window.getRenderScaleX(), window.getRenderScaleY()); } } swNodeIOP.setContent(lwFrame, swNodeIOP.createSwingNodeContent(content, this)); swNodeIOP.setVisible(lwFrame, true); Disposer.addRecord(this, swNodeIOP.createSwingNodeDisposer(lwFrame)); if (getScene() != null) { notifyNativeHandle(getScene().getWindow()); } SwingNodeHelper.runOnFxThread(() -> { locateLwFrame();// initialize location if (focusedProperty().get()) { activateLwFrame(true); } }); } } private List<Runnable> peerRequests = new ArrayList<>(); /* * Called on EDT */ void setImageBuffer(final int[] data, final int x, final int y, final int w, final int h, final int linestride, final double scaleX, final double scaleY) { Runnable r = () -> peer.setImageBuffer(IntBuffer.wrap(data), x, y, w, h, w, h, linestride, scaleX, scaleY); SwingNodeHelper.runOnFxThread(() -> { if (peer != null) { r.run(); } else { peerRequests.clear(); peerRequests.add(r); } }); } /* * Called on EDT */ void setImageBounds(final int x, final int y, final int w, final int h) { Runnable r = () -> peer.setImageBounds(x, y, w, h, w, h); SwingNodeHelper.runOnFxThread(() -> { if (peer != null) { r.run(); } else { peerRequests.add(r); } }); } /* * Called on EDT */ void repaintDirtyRegion(final int dirtyX, final int dirtyY, final int dirtyWidth, final int dirtyHeight) { Runnable r = () -> { peer.repaintDirtyRegion(dirtyX, dirtyY, dirtyWidth, dirtyHeight); NodeHelper.markDirty(this, DirtyBits.NODE_CONTENTS); }; SwingNodeHelper.runOnFxThread(() -> { if (peer != null) { r.run(); } else { peerRequests.add(r); } }); } @Override public boolean isResizable() { return true; }
Invoked by the SwingNode's parent during layout to set the SwingNode's width and height. Applications should not invoke this method directly. If an application needs to directly set the size of the SwingNode, it should set the Swing component's minimum/preferred/maximum size constraints which will be propagated correspondingly to the SwingNode and it's parent will honor those settings during layout.
Params:
  • width – the target layout bounds width
  • height – the target layout bounds height
/** * Invoked by the {@code SwingNode}'s parent during layout to set the {@code SwingNode}'s * width and height. <b>Applications should not invoke this method directly</b>. * If an application needs to directly set the size of the {@code SwingNode}, it should * set the Swing component's minimum/preferred/maximum size constraints which will * be propagated correspondingly to the {@code SwingNode} and it's parent will honor those * settings during layout. * * @param width the target layout bounds width * @param height the target layout bounds height */
@Override public void resize(final double width, final double height) { super.resize(width, height); if (width != this.fxWidth || height != this.fxHeight) { this.fxWidth = width; this.fxHeight = height; NodeHelper.geomChanged(this); NodeHelper.markDirty(this, DirtyBits.NODE_GEOMETRY); SwingNodeHelper.runOnEDT(() -> { if (lwFrame != null) { locateLwFrame(); } }); } }
Returns the SwingNode's preferred width for use in layout calculations. This value corresponds to the preferred width of the Swing component.
Returns:the preferred width that the node should be resized to during layout
/** * Returns the {@code SwingNode}'s preferred width for use in layout calculations. * This value corresponds to the preferred width of the Swing component. * * @return the preferred width that the node should be resized to during layout */
@Override public double prefWidth(double height) { return swingPrefWidth; }
Returns the SwingNode's preferred height for use in layout calculations. This value corresponds to the preferred height of the Swing component.
Returns:the preferred height that the node should be resized to during layout
/** * Returns the {@code SwingNode}'s preferred height for use in layout calculations. * This value corresponds to the preferred height of the Swing component. * * @return the preferred height that the node should be resized to during layout */
@Override public double prefHeight(double width) { return swingPrefHeight; }
Returns the SwingNode's maximum width for use in layout calculations. This value corresponds to the maximum width of the Swing component.
Returns:the maximum width that the node should be resized to during layout
/** * Returns the {@code SwingNode}'s maximum width for use in layout calculations. * This value corresponds to the maximum width of the Swing component. * * @return the maximum width that the node should be resized to during layout */
@Override public double maxWidth(double height) { return swingMaxWidth; }
Returns the SwingNode's maximum height for use in layout calculations. This value corresponds to the maximum height of the Swing component.
Returns:the maximum height that the node should be resized to during layout
/** * Returns the {@code SwingNode}'s maximum height for use in layout calculations. * This value corresponds to the maximum height of the Swing component. * * @return the maximum height that the node should be resized to during layout */
@Override public double maxHeight(double width) { return swingMaxHeight; }
Returns the SwingNode's minimum width for use in layout calculations. This value corresponds to the minimum width of the Swing component.
Returns:the minimum width that the node should be resized to during layout
/** * Returns the {@code SwingNode}'s minimum width for use in layout calculations. * This value corresponds to the minimum width of the Swing component. * * @return the minimum width that the node should be resized to during layout */
@Override public double minWidth(double height) { return swingMinWidth; }
Returns the SwingNode's minimum height for use in layout calculations. This value corresponds to the minimum height of the Swing component.
Returns:the minimum height that the node should be resized to during layout
/** * Returns the {@code SwingNode}'s minimum height for use in layout calculations. * This value corresponds to the minimum height of the Swing component. * * @return the minimum height that the node should be resized to during layout */
@Override public double minHeight(double width) { return swingMinHeight; } /* * Note: This method MUST only be called via its accessor method. */ private boolean doComputeContains(double localX, double localY) { return true; } private final InvalidationListener locationListener = observable -> { locateLwFrame(); }; private final EventHandler<FocusUngrabEvent> ungrabHandler = event -> { if (!skipBackwardUnrgabNotification) { if (lwFrame != null) { AccessController.doPrivileged(new PostEventAction( swNodeIOP.createUngrabEvent(lwFrame))); } } }; private final ChangeListener<Boolean> windowVisibleListener = (observable, oldValue, newValue) -> { if (!newValue) { disposeLwFrame(); } else { setContent(content); } }; private final ChangeListener<Window> sceneWindowListener = (observable, oldValue, newValue) -> { if (oldValue != null) { removeWindowListeners(oldValue); } notifyNativeHandle(newValue); if (newValue != null) { addWindowListeners(newValue); } }; private void removeSceneListeners(Scene scene) { Window window = scene.getWindow(); if (window != null) { removeWindowListeners(window); } scene.windowProperty().removeListener(sceneWindowListener); } private void addSceneListeners(final Scene scene) { Window window = scene.getWindow(); if (window != null) { addWindowListeners(window); notifyNativeHandle(window); } scene.windowProperty().addListener(sceneWindowListener); } private void addWindowListeners(final Window window) { window.xProperty().addListener(locationListener); window.yProperty().addListener(locationListener); window.widthProperty().addListener(locationListener); window.heightProperty().addListener(locationListener); window.renderScaleXProperty().addListener(locationListener); window.addEventHandler(FocusUngrabEvent.FOCUS_UNGRAB, ungrabHandler); window.showingProperty().addListener(windowVisibleListener); setLwFrameScale(window.getRenderScaleX(), window.getRenderScaleY()); } private void removeWindowListeners(final Window window) { window.xProperty().removeListener(locationListener); window.yProperty().removeListener(locationListener); window.widthProperty().removeListener(locationListener); window.heightProperty().removeListener(locationListener); window.renderScaleXProperty().removeListener(locationListener); window.removeEventHandler(FocusUngrabEvent.FOCUS_UNGRAB, ungrabHandler); window.showingProperty().removeListener(windowVisibleListener); } /* * Note: This method MUST only be called via its accessor method. */ private NGNode doCreatePeer() { peer = new NGExternalNode(); peer.setLock(paintLock); for (Runnable request : peerRequests) { request.run(); } peerRequests = null; if (getScene() != null) { addSceneListeners(getScene()); } sceneProperty().addListener((observable, oldValue, newValue) -> { if (oldValue != null) { // Removed from scene removeSceneListeners(oldValue); disposeLwFrame(); } if (newValue != null) { // Added to another scene if (content != null && lwFrame == null) { setContent(content); } addSceneListeners(newValue); } }); NodeHelper.treeVisibleProperty(this).addListener((observable, oldValue, newValue) -> { setLwFrameVisible(newValue); }); return peer; } /* * Note: This method MUST only be called via its accessor method. */ private void doUpdatePeer() { if (NodeHelper.isDirty(this, DirtyBits.NODE_VISIBLE) || NodeHelper.isDirty(this, DirtyBits.NODE_BOUNDS)) { locateLwFrame(); // initialize location } if (NodeHelper.isDirty(this, DirtyBits.NODE_CONTENTS)) { peer.markContentDirty(); } } private void locateLwFrame() { if (getScene() == null || lwFrame == null || getScene().getWindow() == null || !getScene().getWindow().isShowing()) { // Not initialized yet. Skip the update to set the real values later return; } Window w = getScene().getWindow(); double renderScaleX = w.getRenderScaleX(); double renderScaleY = w.getRenderScaleY(); final Point2D loc = localToScene(0, 0); final int windowX = (int) (w.getX()); final int windowY = (int) (w.getY()); final int windowW = (int) (w.getWidth()); final int windowH = (int) (w.getHeight()); final int frameX = (int) Math.round(w.getX() + getScene().getX() + loc.getX()); final int frameY = (int) Math.round(w.getY() + getScene().getY() + loc.getY()); final int frameW = (int) (fxWidth); final int frameH = (int) (fxHeight); SwingNodeHelper.runOnEDT(() -> { if (lwFrame != null) { swNodeIOP.notifyDisplayChanged(lwFrame, renderScaleX, renderScaleY); swNodeIOP.setBounds(lwFrame, frameX, frameY, frameW, frameH); swNodeIOP.setHostBounds(lwFrame, windowX, windowY, windowW, windowH); } }); } private void activateLwFrame(final boolean activate) { if (lwFrame == null) { return; } if (PlatformUtil.isLinux()) { // Workaround to block FocusOut/FocusIn notifications from Unity // focus grabbing upon Alt press if (deactivate == null || !deactivate.isRunning()) { if (!activate) { deactivate = new Timer(50, (e) -> { { if (lwFrame != null) { swNodeIOP.emulateActivation(lwFrame, false); } } }); deactivate.start(); return; } } else { deactivate.stop(); } } SwingNodeHelper.runOnEDT(() -> { if (lwFrame != null) { swNodeIOP.emulateActivation(lwFrame, activate); } }); } private void disposeLwFrame() { if (lwFrame == null) { return; } SwingNodeHelper.runOnEDT(() -> { if (lwFrame != null) { swNodeIOP.disposeFrame(lwFrame); lwFrame = null; } }); } private void setLwFrameVisible(final boolean visible) { if (lwFrame == null) { return; } SwingNodeHelper.runOnEDT(() -> { if (lwFrame != null) { swNodeIOP.setVisible(lwFrame, visible); } }); } private void setLwFrameScale(final double scaleX, final double scaleY) { if (lwFrame == null) { return; } SwingNodeHelper.runOnEDT(() -> { if (lwFrame != null) { swNodeIOP.notifyDisplayChanged(lwFrame, scaleX, scaleY); } }); } /* * Note: This method MUST only be called via its accessor method. */ private BaseBounds doComputeGeomBounds(BaseBounds bounds, BaseTransform tx) { bounds.deriveWithNewBounds(0, 0, 0, (float)fxWidth, (float)fxHeight, 0); tx.transform(bounds, bounds); return bounds; } private static class SwingNodeWindowFocusListener implements WindowFocusListener { private WeakReference<SwingNode> swingNodeRef; SwingNodeWindowFocusListener(SwingNode swingNode) { this.swingNodeRef = new WeakReference<SwingNode>(swingNode); } @Override public void windowGainedFocus(WindowEvent e) { SwingNodeHelper.runOnFxThread(() -> { SwingNode swingNode = swingNodeRef.get(); if (swingNode != null) { swingNode.requestFocus(); } }); } @Override public void windowLostFocus(WindowEvent e) { SwingNodeHelper.runOnFxThread(() -> { SwingNode swingNode = swingNodeRef.get(); if (swingNode != null) { swingNode.ungrabFocus(true); } }); } } private void ungrabFocus(boolean postUngrabEvent) { // On X11 grab is limited to a single XDisplay connection, // so we can't delegate it to another GUI toolkit. if (PlatformUtil.isLinux()) return; if (grabbed && getScene() != null && getScene().getWindow() != null && WindowHelper.getPeer(getScene().getWindow()) != null) { skipBackwardUnrgabNotification = !postUngrabEvent; WindowHelper.getPeer(getScene().getWindow()).ungrabFocus(); skipBackwardUnrgabNotification = false; grabbed = false; } } private class PostEventAction implements PrivilegedAction<Void> { private AWTEvent event; PostEventAction(AWTEvent event) { this.event = event; } @Override public Void run() { EventQueue eq = Toolkit.getDefaultToolkit().getSystemEventQueue(); eq.postEvent(event); return null; } } private class SwingMouseEventHandler implements EventHandler<MouseEvent> { private final Set<MouseButton> mouseClickedAllowed = new HashSet<>(); @Override public void handle(MouseEvent event) { Object frame = swNodeIOP.getLightweightFrame(); if (frame == null) { return; } int swingID = SwingEvents.fxMouseEventTypeToMouseID(event); if (swingID < 0) { return; } // Prevent ancestors of the SwingNode from stealing the focus event.consume(); final EventType<?> type = event.getEventType(); if (type == MouseEvent.MOUSE_PRESSED) { mouseClickedAllowed.add(event.getButton()); } else if (type == MouseEvent.MOUSE_RELEASED) { // RELEASED comes before CLICKED, so we don't remove the button from the set //mouseClickedAllowed.remove(event.getButton()); } else if (type == MouseEvent.MOUSE_DRAGGED) { // This is what AWT/Swing do mouseClickedAllowed.clear(); } else if (type == MouseEvent.MOUSE_CLICKED) { if (event.getClickCount() == 1 && !mouseClickedAllowed.contains(event.getButton())) { // RT-34610: In FX, CLICKED events are generated even after dragging the mouse pointer // Note that this is only relevant for single clicks. Double clicks use a smudge factor. return; } mouseClickedAllowed.remove(event.getButton()); } int swingModifiers = SwingEvents.fxMouseModsToMouseMods(event); boolean swingPopupTrigger = event.isPopupTrigger(); int swingButton = SwingEvents.fxMouseButtonToMouseButton(event); long swingWhen = System.currentTimeMillis(); int relX = (int) Math.round(event.getX()); int relY = (int) Math.round(event.getY()); int absX = (int) Math.round(event.getScreenX()); int absY = (int) Math.round(event.getScreenY()); java.awt.event.MouseEvent mouseEvent = swNodeIOP.createMouseEvent( frame, swingID, swingWhen, swingModifiers, relX, relY, absX, absY, event.getClickCount(), swingPopupTrigger, swingButton); AccessController.doPrivileged(new PostEventAction(mouseEvent)); } } private class SwingScrollEventHandler implements EventHandler<ScrollEvent> { @Override public void handle(ScrollEvent event) { Object frame = swNodeIOP.getLightweightFrame(); if (frame == null) { return; } int swingModifiers = SwingEvents.fxScrollModsToMouseWheelMods(event); final boolean isShift = (swingModifiers & InputEvent.SHIFT_DOWN_MASK) != 0; // Vertical scroll. if (!isShift && event.getDeltaY() != 0.0) { sendMouseWheelEvent(frame, event.getX(), event.getY(), swingModifiers, event.getDeltaY() / event.getMultiplierY()); } // Horizontal scroll or shirt+vertical scroll. final double delta = isShift && event.getDeltaY() != 0.0 ? event.getDeltaY() / event.getMultiplierY() : event.getDeltaX() / event.getMultiplierX(); if (delta != 0.0) { swingModifiers |= InputEvent.SHIFT_DOWN_MASK; sendMouseWheelEvent(frame, event.getX(), event.getY(), swingModifiers, delta); } } private void sendMouseWheelEvent(Object source, double fxX, double fxY, int swingModifiers, double delta) { int wheelRotation = (int) delta; int signum = (int) Math.signum(delta); if (signum * delta < 1) { wheelRotation = signum; } int x = (int) Math.round(fxX); int y = (int) Math.round(fxY); MouseWheelEvent mouseWheelEvent = swNodeIOP.createMouseWheelEvent(source, swingModifiers, x, y, -wheelRotation); AccessController.doPrivileged(new PostEventAction(mouseWheelEvent)); } } private class SwingKeyEventHandler implements EventHandler<KeyEvent> { @Override public void handle(KeyEvent event) { Object frame = swNodeIOP.getLightweightFrame(); if (frame == null) { return; } if (event.getCharacter().isEmpty()) { // TODO: should we post an "empty" character? return; } // Don't let Arrows, Tab, Shift+Tab traverse focus out. if (event.getCode() == KeyCode.LEFT || event.getCode() == KeyCode.RIGHT || event.getCode() == KeyCode.UP || event.getCode() == KeyCode.DOWN || event.getCode() == KeyCode.TAB) { event.consume(); } int swingID = SwingEvents.fxKeyEventTypeToKeyID(event); if (swingID < 0) { return; } int swingModifiers = SwingEvents.fxKeyModsToKeyMods(event); int swingKeyCode = event.getCode().getCode(); char swingChar = event.getCharacter().charAt(0); // A workaround. Some swing L&F's process mnemonics on KEY_PRESSED, // for which swing provides a keychar. Extracting it from the text. if (event.getEventType() == javafx.scene.input.KeyEvent.KEY_PRESSED) { String text = event.getText(); if (text.length() == 1) { swingChar = text.charAt(0); } } long swingWhen = System.currentTimeMillis(); java.awt.event.KeyEvent keyEvent = swNodeIOP.createKeyEvent(frame, swingID, swingWhen, swingModifiers, swingKeyCode, swingChar); AccessController.doPrivileged(new PostEventAction(keyEvent)); } } }