1 /*
   2  * Copyright (c) 2013, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  26 package javafx.embed.swing;
  27 
  28 import com.sun.javafx.geom.BaseBounds;
  29 import com.sun.javafx.geom.transform.BaseTransform;
  30 import com.sun.javafx.jmx.MXNodeAlgorithm;
  31 import com.sun.javafx.jmx.MXNodeAlgorithmContext;
  32 import com.sun.javafx.scene.DirtyBits;
  33 import com.sun.javafx.sg.prism.NGExternalNode;
  34 import com.sun.javafx.sg.prism.NGNode;
  35 import com.sun.javafx.stage.FocusUngrabEvent;
  36 import javafx.application.Platform;
  37 import javafx.beans.InvalidationListener;
  38 import javafx.beans.Observable;
  39 import javafx.beans.value.ChangeListener;
  40 import javafx.beans.value.ObservableValue;
  41 import javafx.event.EventHandler;
  42 import javafx.geometry.Point2D;
  43 import javafx.scene.Node;
  44 import javafx.scene.Scene;
  45 import javafx.scene.input.KeyCode;
  46 import javafx.scene.input.KeyEvent;
  47 import javafx.scene.input.MouseButton;
  48 import javafx.scene.input.MouseEvent;
  49 import javafx.stage.Window;
  50 import sun.awt.UngrabEvent;
  51 import sun.swing.JLightweightFrame;
  52 import sun.swing.LightweightContent;
  53 
  54 import javax.swing.*;
  55 import java.awt.*;
  56 import java.awt.event.WindowEvent;
  57 import java.awt.event.WindowFocusListener;
  58 import java.nio.IntBuffer;
  59 import java.security.AccessController;
  60 import java.security.PrivilegedAction;
  61 import java.util.ArrayList;
  62 import java.util.List;
  63 import java.util.concurrent.locks.ReentrantLock;
  64 
  65 /**
  66  * This class is used to embed a Swing content into a JavaFX application.
  67  * The content to be displayed is specified with the {@link #setContent} method
  68  * that accepts an instance of Swing {@code JComponent}. The hierarchy of components
  69  * contained in the {@code JComponent} instance should not contain any heavyweight
  70  * components, otherwise {@code SwingNode} may fail to paint it. The content gets
  71  * repainted automatically. All the input and focus events are forwarded to the
  72  * {@code JComponent} instance transparently to the developer.
  73  * <p>
  74  * Here is a typical pattern which demonstrates how {@code SwingNode} can be used:
  75  * <pre>
  76  *     public class SwingFx extends Application {
  77  *
  78  *         private SwingNode swingNode;
  79  *
  80  *         &#064;Override
  81  *         public void start(Stage stage) {
  82  *             swingNode = new SwingNode();
  83  *
  84  *             createAndSetSwingContent();
  85  *
  86  *             StackPane pane = new StackPane();
  87  *             pane.getChildren().add(swingNode);
  88  *
  89  *             stage.setScene(new Scene(pane, 100, 50));
  90  *             stage.show();
  91  *         }
  92  *
  93  *         private void createAndSetSwingContent() {
  94  *             SwingUtilities.invokeLater(new Runnable() {
  95  *                 &#064;Override
  96  *                 public void run() {
  97  *                     swingNode.setContent(new JButton("Click me!"));
  98  *                 }
  99  *             });
 100  *         }
 101  *     }
 102  * </pre>
 103  * @since JavaFX 8.0
 104  */
 105 public class SwingNode extends Node {
 106 
 107     private double width;
 108     private double height;
 109     
 110     private double prefWidth;
 111     private double prefHeight;
 112     private double maxWidth;
 113     private double maxHeight;
 114     private double minWidth;
 115     private double minHeight;
 116 
 117     private volatile JComponent content;
 118     private volatile JLightweightFrame lwFrame;
 119 
 120     private volatile NGExternalNode peer;
 121 
 122     private final ReentrantLock paintLock = new ReentrantLock();
 123 
 124     private boolean skipBackwardUnrgabNotification;
 125     private boolean grabbed; // lwframe initiated grab
 126     
 127     /**
 128      * Constructs a new instance of {@code SwingNode}.
 129      */
 130     public SwingNode() {
 131         setFocusTraversable(true);
 132         setEventHandler(MouseEvent.ANY, new SwingMouseEventHandler());
 133         setEventHandler(KeyEvent.ANY, new SwingKeyEventHandler());
 134 
 135         focusedProperty().addListener(new ChangeListener<Boolean>() {
 136             @Override
 137             public void changed(ObservableValue<? extends Boolean> observable, Boolean oldValue, final Boolean newValue) {
 138                  activateLwFrame(newValue);
 139             }
 140         });
 141     }
 142 
 143     /**
 144      * Attaches a {@code JComponent} instance to display in this {@code SwingNode}.
 145      * <p>
 146      * The method can be called either on the JavaFX Application thread or the Swing thread.
 147      * Note however, that access to a Swing component must occur from the Swing thread according
 148      * to the Swing threading restrictions.
 149      *
 150      * @param content a Swing component to display in this {@code SwingNode}
 151      *
 152      * @see java.awt.EventQueue#isDispatchThread()
 153      * @see javafx.application.Platform#isFxApplicationThread()
 154      */
 155     public void setContent(final JComponent content) {
 156         this.content = content;
 157 
 158         invokeOnEDT(new Runnable() {
 159             @Override
 160             public void run() {
 161                 setContentImpl(content);
 162             }
 163         });
 164     }
 165 
 166    /**
 167      * Returns the {@code JComponent} instance attached to this {@code SwingNode}.
 168      * <p>
 169      * The method can be called either on the JavaFX Application thread or the Swing thread.
 170      * Note however, that access to a Swing component must occur from the Swing thread according
 171      * to the Swing threading restrictions.
 172      *
 173      * @see java.awt.EventQueue#isDispatchThread()
 174      * @see javafx.application.Platform#isFxApplicationThread()
 175      *
 176      * @return the Swing component attached to this {@code SwingNode}
 177      */
 178     public JComponent getContent() {
 179         return content;
 180     }
 181 
 182     /*
 183      * Called on Swing thread
 184      */
 185     private void setContentImpl(JComponent content) {
 186         if (lwFrame != null) {
 187             lwFrame.dispose();
 188             lwFrame = null;
 189         }
 190         if (content != null) {
 191             lwFrame = new JLightweightFrame();
 192 
 193             lwFrame.addWindowFocusListener(new WindowFocusListener() {
 194                 @Override
 195                 public void windowGainedFocus(WindowEvent e) {
 196                 }
 197                 @Override
 198                 public void windowLostFocus(WindowEvent e) {
 199                     Platform.runLater(new Runnable() {
 200                         @Override
 201                         public void run() {
 202                             ungrabFocus(true);
 203                         }
 204                     });
 205                 }
 206             });
 207 
 208             lwFrame.setContent(new SwingNodeContent(content));
 209             lwFrame.setVisible(true);
 210 
 211             Platform.runLater(new Runnable() {
 212                 @Override
 213                 public void run() {
 214                     locateLwFrame(); // initialize location
 215 
 216                     if (focusedProperty().get()) {
 217                         activateLwFrame(true);
 218                     }
 219                 }
 220             });
 221         }
 222     }
 223 
 224     private List<Runnable> peerRequests = new ArrayList<>();
 225 
 226     /*
 227      * Called on Swing thread
 228      */
 229     void setImageBuffer(final int[] data,
 230                         final int x, final int y,
 231                         final int w, final int h,
 232                         final int linestride)
 233     {
 234         Runnable r = new Runnable() {
 235             @Override
 236             public void run() {
 237                 peer.setImageBuffer(IntBuffer.wrap(data), x, y, w, h, linestride);
 238             }
 239         };
 240         if (peer != null) {
 241             Platform.runLater(r);
 242         } else {
 243             peerRequests.clear();
 244             peerRequests.add(r);
 245         }
 246     }
 247 
 248     /*
 249      * Called on Swing thread
 250      */
 251     void setImageBounds(final int x, final int y, final int w, final int h) {
 252         Runnable r = new Runnable() {
 253             @Override
 254             public void run() {
 255                 peer.setImageBounds(x, y, w, h);
 256             }
 257         };
 258         if (peer != null) {
 259             Platform.runLater(r);
 260         } else {
 261             peerRequests.add(r);
 262         }
 263     }
 264 
 265     /*
 266      * Called on Swing thread
 267      */
 268     void repaintDirtyRegion(final int dirtyX, final int dirtyY, final int dirtyWidth, final int dirtyHeight) {
 269         Runnable r = new Runnable() {
 270             @Override
 271             public void run() {
 272                 peer.repaintDirtyRegion(dirtyX, dirtyY, dirtyWidth, dirtyHeight);
 273                 impl_markDirty(DirtyBits.NODE_CONTENTS);
 274             }
 275         };
 276         if (peer != null) {
 277             Platform.runLater(r);
 278         } else {
 279             peerRequests.add(r);
 280         }
 281     }
 282 
 283     @Override public boolean isResizable() {
 284         return true;
 285     }
 286 
 287     /**
 288      * Invoked by the {@code SwingNode}'s parent during layout to set the {@code SwingNode}'s
 289      * width and height. <b>Applications should not invoke this method directly</b>.
 290      * If an application needs to directly set the size of the {@code SwingNode}, it should
 291      * set the Swing component's minimum/preferred/maximum size constraints which will
 292      * be propagated correspondingly to the {@code SwingNode} and it's parent will honor those
 293      * settings during layout.
 294      *
 295      * @param width the target layout bounds width
 296      * @param height the target layout bounds height
 297      */    
 298     @Override public void resize(final double width, final double height) {
 299         super.resize(width, height);
 300         if (width != this.width || height != this.height) {
 301             this.width = width;
 302             this.height = height;
 303             impl_geomChanged();
 304             impl_markDirty(DirtyBits.NODE_GEOMETRY);
 305             SwingUtilities.invokeLater(new Runnable() {
 306                 @Override
 307                 public void run() {
 308                     if (lwFrame != null) {
 309                         lwFrame.setSize((int)width, (int)height);
 310                     }
 311                 }
 312             });
 313         }
 314     }
 315 
 316     /**
 317      * Returns the {@code SwingNode}'s preferred width for use in layout calculations.
 318      * This value corresponds to the preferred width of the Swing component.
 319      * 
 320      * @return the preferred width that the node should be resized to during layout
 321      */
 322     @Override
 323     public double prefWidth(double height) {
 324         return prefWidth;
 325     }
 326 
 327     /**
 328      * Returns the {@code SwingNode}'s preferred height for use in layout calculations.
 329      * This value corresponds to the preferred height of the Swing component.
 330      * 
 331      * @return the preferred height that the node should be resized to during layout
 332      */
 333     @Override
 334     public double prefHeight(double width) {
 335         return prefHeight;
 336     }
 337     
 338     /**
 339      * Returns the {@code SwingNode}'s maximum width for use in layout calculations.
 340      * This value corresponds to the maximum width of the Swing component.
 341      * 
 342      * @return the maximum width that the node should be resized to during layout
 343      */
 344     @Override public double maxWidth(double height) {
 345         return maxWidth;
 346     }
 347     
 348     /**
 349      * Returns the {@code SwingNode}'s maximum height for use in layout calculations.
 350      * This value corresponds to the maximum height of the Swing component.
 351      * 
 352      * @return the maximum height that the node should be resized to during layout
 353      */    
 354     @Override public double maxHeight(double width) {
 355         return maxHeight;
 356     }
 357     
 358     /**
 359      * Returns the {@code SwingNode}'s minimum width for use in layout calculations.
 360      * This value corresponds to the minimum width of the Swing component.
 361      * 
 362      * @return the minimum width that the node should be resized to during layout
 363      */    
 364     @Override public double minWidth(double height) {
 365         return minWidth;
 366     }
 367     
 368     /**
 369      * Returns the {@code SwingNode}'s minimum height for use in layout calculations.
 370      * This value corresponds to the minimum height of the Swing component.
 371      * 
 372      * @return the minimum height that the node should be resized to during layout
 373      */    
 374     @Override public double minHeight(double width) {
 375         return minHeight;
 376     }
 377 
 378     @Override
 379     protected boolean impl_computeContains(double localX, double localY) {
 380         return true;
 381     }
 382 
 383     private InvalidationListener locationListener = new InvalidationListener() {
 384         @Override
 385         public void invalidated(Observable observable) {
 386             locateLwFrame();
 387         }
 388     };
 389 
 390     private EventHandler<FocusUngrabEvent> ungrabHandler = new EventHandler<FocusUngrabEvent>() {
 391         @Override
 392         public void handle(FocusUngrabEvent event) {
 393             if (!skipBackwardUnrgabNotification) {
 394                 AccessController.doPrivileged(new PostEventAction(new UngrabEvent(lwFrame)));
 395             }
 396         }
 397     };
 398 
 399     private ChangeListener<Boolean> windowVisibleListener = new ChangeListener<Boolean>() {
 400         @Override
 401         public void changed(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) {
 402             if (!newValue) {
 403                 disposeLwFrame();
 404 
 405             } else {
 406                 setContent(content);
 407             }
 408         }
 409     };
 410 
 411     private void removeListeners(Scene scene) {
 412         Window window = scene.getWindow();
 413         if (window != null) {
 414             window.xProperty().removeListener(locationListener);
 415             window.yProperty().removeListener(locationListener);
 416             window.removeEventHandler(FocusUngrabEvent.FOCUS_UNGRAB, ungrabHandler);
 417             window.showingProperty().removeListener(windowVisibleListener);
 418         }
 419     }
 420 
 421     private void addListeners(Scene scene) {
 422         Window window = scene.getWindow();
 423         if (window != null) {
 424             window.xProperty().addListener(locationListener);
 425             window.yProperty().addListener(locationListener);
 426             window.addEventHandler(FocusUngrabEvent.FOCUS_UNGRAB, ungrabHandler);
 427             window.showingProperty().addListener(windowVisibleListener);
 428         }
 429     }
 430 
 431     @Override
 432     protected NGNode impl_createPeer() {
 433         peer = new NGExternalNode();
 434         peer.setLock(paintLock);
 435         for (Runnable request : peerRequests) {
 436             request.run();
 437         }
 438         peerRequests = null;
 439 
 440         if (content != null) {
 441             setContent(content); // in case the Node is re-added to Scene
 442         }
 443         addListeners(getScene());
 444 
 445         sceneProperty().addListener(new ChangeListener<Scene>() {
 446             @Override
 447             public void changed(ObservableValue<? extends Scene> observable, Scene oldValue, Scene newValue) {
 448                 // Removed from scene, or added to another scene.
 449                 // The lwFrame will be recreated from impl_createPGNode().
 450                 removeListeners(oldValue);
 451                 disposeLwFrame();
 452             }
 453         });
 454 
 455         impl_treeVisibleProperty().addListener(new ChangeListener<Boolean>() {
 456             @Override
 457             public void changed(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) {
 458                 setLwFrameVisible(newValue);
 459             }
 460         });
 461 
 462         return peer;
 463     }
 464 
 465     @Override
 466     public void impl_updatePeer() {
 467         super.impl_updatePeer();
 468 
 469         if (impl_isDirty(DirtyBits.NODE_VISIBLE)
 470                 || impl_isDirty(DirtyBits.NODE_BOUNDS)) {
 471             locateLwFrame(); // initialize location
 472         }
 473         if (impl_isDirty(DirtyBits.NODE_CONTENTS)) {
 474             peer.markContentDirty();
 475         }
 476     }
 477 
 478     private void locateLwFrame() {
 479         if (getScene() == null
 480                 || lwFrame == null
 481                 || getScene().getWindow() == null
 482                 || !getScene().getWindow().isShowing()) {
 483             // Not initialized yet. Skip the update to set the real values later
 484             return;
 485         }
 486         final Point2D loc = localToScene(0, 0);
 487         final int windowX = (int)getScene().getWindow().getX();
 488         final int windowY = (int)getScene().getWindow().getY();
 489         final int sceneX = (int)getScene().getX();
 490         final int sceneY = (int)getScene().getY();
 491 
 492         invokeOnEDT(new Runnable() {
 493             @Override
 494             public void run() {
 495                 if (lwFrame != null) {
 496                     lwFrame.setLocation(windowX + sceneX + (int)loc.getX(),
 497                                         windowY + sceneY + (int)loc.getY());
 498                 }
 499             }
 500         });
 501     }
 502 
 503     private void activateLwFrame(final boolean activate) {
 504         if (lwFrame == null) {
 505             return;
 506         }
 507         invokeOnEDT(new Runnable() {
 508             @Override
 509             public void run() {
 510                 if (lwFrame != null) {
 511                     lwFrame.emulateActivation(activate);
 512                 }
 513             }
 514         });
 515     }
 516 
 517     private void disposeLwFrame() {
 518         if (lwFrame == null) {
 519             return;
 520         }
 521         invokeOnEDT(new Runnable() {
 522             @Override
 523             public void run() {
 524                 if (lwFrame != null) {
 525                     lwFrame.dispose();
 526                     lwFrame = null;
 527                 }
 528             }
 529         });
 530     }
 531 
 532     private void setLwFrameVisible(final boolean visible) {
 533         if (lwFrame == null) {
 534             return;
 535         }
 536         invokeOnEDT(new Runnable() {
 537             @Override
 538             public void run() {
 539                 if (lwFrame != null) {
 540                     lwFrame.setVisible(visible);
 541                 }
 542             }
 543         });
 544     }
 545 
 546     @Override
 547     public BaseBounds impl_computeGeomBounds(BaseBounds bounds, BaseTransform tx) {
 548         bounds.deriveWithNewBounds(0, 0, 0, (float)width, (float)height, 0);
 549         tx.transform(bounds, bounds);
 550         return bounds;
 551     }
 552 
 553     @Override
 554     public Object impl_processMXNode(MXNodeAlgorithm alg, MXNodeAlgorithmContext ctx) {
 555         return alg.processLeafNode(this, ctx);
 556     }
 557 
 558     private class SwingNodeContent implements LightweightContent {
 559         private JComponent comp;
 560         public SwingNodeContent(JComponent comp) {
 561             this.comp = comp;
 562         }
 563         @Override
 564         public JComponent getComponent() {
 565             return comp;
 566         }
 567         @Override
 568         public void paintLock() {
 569             paintLock.lock();
 570         }
 571         @Override
 572         public void paintUnlock() {
 573             paintLock.unlock();
 574         }
 575         @Override
 576         public void imageBufferReset(int[] data, int x, int y, int width, int height, int linestride) {
 577             SwingNode.this.setImageBuffer(data, x, y, width, height, linestride);
 578         }
 579         @Override
 580         public void imageReshaped(int x, int y, int width, int height) {
 581             SwingNode.this.setImageBounds(x, y, width, height);
 582         }
 583         @Override
 584         public void imageUpdated(int dirtyX, int dirtyY, int dirtyWidth, int dirtyHeight) {
 585             SwingNode.this.repaintDirtyRegion(dirtyX, dirtyY, dirtyWidth, dirtyHeight);
 586         }
 587         @Override
 588         public void focusGrabbed() {
 589             Platform.runLater(new Runnable() {
 590                 @Override
 591                 public void run() {
 592                     if (getScene() != null &&
 593                         getScene().getWindow() != null &&
 594                         getScene().getWindow().impl_getPeer() != null)
 595                     {
 596                         getScene().getWindow().impl_getPeer().grabFocus();
 597                         grabbed = true;
 598                     }
 599                 }
 600             });
 601         }
 602         @Override
 603         public void focusUngrabbed() {
 604             Platform.runLater(new Runnable() {
 605                 @Override
 606                 public void run() {
 607                     ungrabFocus(false);
 608                 }
 609             });
 610         }
 611         @Override
 612         public void preferredSizeChanged(final int width, final int height) {
 613             Platform.runLater(new Runnable() {
 614                 @Override
 615                 public void run() {
 616                     SwingNode.this.prefWidth = width;
 617                     SwingNode.this.prefHeight = height;
 618                     SwingNode.this.impl_notifyLayoutBoundsChanged();
 619                 }
 620             });            
 621         }
 622         @Override
 623         public void maximumSizeChanged(final int width, final int height) {
 624             Platform.runLater(new Runnable() {
 625                 @Override
 626                 public void run() {
 627                     SwingNode.this.maxWidth = width;
 628                     SwingNode.this.maxHeight = height;
 629                     SwingNode.this.impl_notifyLayoutBoundsChanged();
 630                 }
 631             });            
 632         }
 633         @Override
 634         public void minimumSizeChanged(final int width, final int height) {
 635             Platform.runLater(new Runnable() {
 636                 @Override
 637                 public void run() {
 638                     SwingNode.this.minWidth = width;
 639                     SwingNode.this.minHeight = height;
 640                     SwingNode.this.impl_notifyLayoutBoundsChanged();
 641                 }
 642             });            
 643         }
 644     }
 645 
 646     private void ungrabFocus(boolean postUngrabEvent) {
 647         if (grabbed &&
 648             getScene() != null &&
 649             getScene().getWindow() != null &&
 650             getScene().getWindow().impl_getPeer() != null)
 651         {
 652             skipBackwardUnrgabNotification = !postUngrabEvent;
 653             getScene().getWindow().impl_getPeer().ungrabFocus();
 654             skipBackwardUnrgabNotification = false;
 655             grabbed = false;
 656         }
 657     }
 658 
 659     private class PostEventAction implements PrivilegedAction<Void> {
 660         private AWTEvent event;
 661         public PostEventAction(AWTEvent event) {
 662             this.event = event;
 663         }
 664         @Override
 665         public Void run() {
 666             EventQueue eq = Toolkit.getDefaultToolkit().getSystemEventQueue();
 667             eq.postEvent(event);
 668             return null;
 669         }
 670     }
 671 
 672     private class SwingMouseEventHandler implements EventHandler<MouseEvent> {
 673         @Override
 674         public void handle(MouseEvent event) {
 675             if (event.getEventType() == MouseEvent.MOUSE_PRESSED &&
 676                 !SwingNode.this.isFocused() && SwingNode.this.isFocusTraversable())
 677             {
 678                 SwingNode.this.requestFocus();
 679             }
 680             JLightweightFrame frame = lwFrame;
 681             if (frame == null) {
 682                 return;
 683             }
 684             int swingID = SwingEvents.fxMouseEventTypeToMouseID(event);
 685             if (swingID < 0) {
 686                 return;
 687             }
 688             int swingModifiers = SwingEvents.fxMouseModsToMouseMods(event);
 689             // TODO: popupTrigger
 690             boolean swingPopupTrigger = event.getButton() == MouseButton.SECONDARY;
 691             int swingButton = SwingEvents.fxMouseButtonToMouseButton(event);
 692             long swingWhen = System.currentTimeMillis();
 693             java.awt.event.MouseEvent mouseEvent =
 694                     new java.awt.event.MouseEvent(
 695                         frame, swingID, swingWhen, swingModifiers,
 696                         (int)event.getX(), (int)event.getY(), (int)event.getScreenX(), (int)event.getSceneY(),
 697                         event.getClickCount(), swingPopupTrigger, swingButton);
 698             AccessController.doPrivileged(new PostEventAction(mouseEvent));
 699         }
 700     }
 701 
 702     private class SwingKeyEventHandler implements EventHandler<KeyEvent> {
 703         @Override
 704         public void handle(KeyEvent event) {
 705             JLightweightFrame frame = lwFrame;
 706             if (frame == null) {
 707                 return;
 708             }
 709             if (event.getCharacter().isEmpty()) {
 710                 // TODO: should we post an "empty" character?
 711                 return;
 712             }
 713             // Don't let Arrows, Tab, Shift+Tab traverse focus out.
 714             if (event.getCode() == KeyCode.LEFT  ||
 715                 event.getCode() == KeyCode.RIGHT ||
 716                 event.getCode() == KeyCode.TAB)
 717             {
 718                 event.consume();
 719             }
 720 
 721             int swingID = SwingEvents.fxKeyEventTypeToKeyID(event);
 722             if (swingID < 0) {
 723                 return;
 724             }
 725             int swingModifiers = SwingEvents.fxKeyModsToKeyMods(event);
 726             int swingKeyCode = event.getCode().impl_getCode();
 727             char swingChar = event.getCharacter().charAt(0);
 728 
 729             // A workaround. Some swing L&F's process mnemonics on KEY_PRESSED,
 730             // for which swing provides a keychar. Extracting it from the text.
 731             if (event.getEventType() == javafx.scene.input.KeyEvent.KEY_PRESSED) {
 732                 String text = event.getText();
 733                 if (text.length() == 1) {
 734                     swingChar = text.charAt(0);
 735                 }
 736             }
 737             long swingWhen = System.currentTimeMillis();
 738             java.awt.event.KeyEvent keyEvent = new java.awt.event.KeyEvent(
 739                     frame, swingID, swingWhen, swingModifiers,
 740                     swingKeyCode, swingChar);
 741             AccessController.doPrivileged(new PostEventAction(keyEvent));
 742         }
 743     }
 744 
 745     private static void invokeOnEDT(final Runnable r) {
 746         if (SwingUtilities.isEventDispatchThread()) {
 747             r.run();
 748         } else {
 749             SwingUtilities.invokeLater(new Runnable() {
 750                 @Override
 751                 public void run() {
 752                     r.run();
 753                 }
 754             });
 755         }
 756     }
 757 }