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.scene.traversal.Direction;
  34 import com.sun.javafx.sg.PGNode;
  35 import com.sun.javafx.sg.PGExternalNode;
  36 import com.sun.javafx.stage.FocusUngrabEvent;
  37 
  38 import javafx.application.Platform;
  39 import javafx.beans.InvalidationListener;
  40 import javafx.beans.Observable;
  41 import javafx.beans.value.ObservableValue;
  42 import javafx.event.EventHandler;
  43 import javafx.scene.Node;
  44 import javafx.scene.input.KeyEvent;
  45 import javafx.scene.input.MouseButton;
  46 import javafx.scene.input.MouseEvent;
  47 import javafx.beans.value.ChangeListener;
  48 import javafx.geometry.Point2D;
  49 import javafx.scene.input.KeyCode;
  50 
  51 import javax.swing.JComponent;
  52 import javax.swing.SwingUtilities;
  53 import java.awt.AWTEvent;
  54 import java.awt.EventQueue;
  55 import java.awt.Toolkit;
  56 
  57 import java.nio.IntBuffer;
  58 import java.security.AccessController;
  59 import java.security.PrivilegedAction;
  60 import java.util.ArrayList;
  61 import java.util.List;
  62 import java.util.concurrent.locks.ReentrantLock;
  63 
  64 import javafx.scene.Scene;
  65 import javafx.stage.Window;
  66 import sun.awt.UngrabEvent;
  67 import sun.swing.LightweightContent;
  68 import sun.swing.JLightweightFrame;
  69 
  70 /**
  71  * This class is used to embed a Swing content into a JavaFX application.
  72  * The content to be displayed is specified with the {@link #setContent} method
  73  * that accepts an instance of Swing {@code JComponent}. The hierarchy of components
  74  * contained in the {@code JComponent} instance should not contain any heavyweight
  75  * components, otherwise {@code SwingNode} may fail to paint it. The content gets
  76  * repainted automatically. All the input and focus events are forwarded to the
  77  * {@code JComponent} instance transparently to the developer.
  78  * <p>
  79  * Here is a typical pattern which demonstrates how {@code SwingNode} can be used:
  80  * <pre>
  81  *     public class SwingFx extends Application {
  82  *
  83  *         private SwingNode swingNode;
  84  *
  85  *         &#064;Override
  86  *         public void start(Stage stage) {
  87  *             swingNode = new SwingNode();
  88  *
  89  *             createAndSetSwingContent();
  90  *
  91  *             StackPane pane = new StackPane();
  92  *             pane.getChildren().add(swingNode);
  93  *
  94  *             stage.setScene(new Scene(pane, 100, 50));
  95  *             stage.show();
  96  *         }
  97  *
  98  *         private void createAndSetSwingContent() {
  99  *             SwingUtilities.invokeLater(new Runnable() {
 100  *                 &#064;Override
 101  *                 public void run() {
 102  *                     swingNode.setContent(new JButton("Click me!"));
 103  *                 }
 104  *             });
 105  *         }
 106  *     }
 107  * </pre>
 108  */
 109 public class SwingNode extends Node {
 110 
 111     private double width;
 112     private double height;
 113 
 114     private volatile JComponent content;
 115     private SwingNodeContent contentProvider;
 116     private JLightweightFrame lwFrame;
 117 
 118     private volatile PGExternalNode peer;
 119     
 120     private final ReentrantLock paintLock = new ReentrantLock();
 121     
 122     private boolean skipBackwardUnrgabNotification;    
 123         
 124     /**
 125      * Constructs a new instance of {@code SwingNode}.
 126      */ 
 127     public SwingNode() {                
 128         setFocusTraversable(true);
 129         setEventHandler(MouseEvent.ANY, new SwingMouseEventHandler());
 130         setEventHandler(KeyEvent.ANY, new SwingKeyEventHandler());
 131         
 132         focusedProperty().addListener(new ChangeListener<Boolean>() {
 133             @Override
 134             public void changed(ObservableValue<? extends Boolean> observable, Boolean oldValue, final Boolean newValue) {
 135                 activateLwFrame(newValue);
 136             }
 137         });
 138     }
 139     
 140     /**
 141      * Attaches a {@code JComponent} instance to display in this {@code SwingNode}.
 142      * <p>
 143      * The method can be called either on the JavaFX Application thread or the Swing thread.
 144      * Note however, that access to a Swing component must occur from the Swing thread according
 145      * to the Swing threading restrictions.
 146      *
 147      * @param content a Swing component to display in this {@code SwingNode}
 148      *
 149      * @see java.awt.EventQueue#isDispatchThread()
 150      * @see javafx.application.Platform#isFxApplicationThread()
 151      */ 
 152     public void setContent(final JComponent content) {
 153         this.content = content;
 154         
 155         invokeOnEDT(new Runnable() {
 156             @Override
 157             public void run() {
 158                 setContentImpl(content);
 159             }
 160         });
 161     }
 162     
 163    /**
 164      * Returns the {@code JComponent} instance attached to this {@code SwingNode}.
 165      * <p>
 166      * The method can be called either on the JavaFX Application thread or the Swing thread.
 167      * Note however, that access to a Swing component must occur from the Swing thread according
 168      * to the Swing threading restrictions.
 169      *
 170      * @see java.awt.EventQueue#isDispatchThread()
 171      * @see javafx.application.Platform#isFxApplicationThread()
 172      *
 173      * @return the Swing component attached to this {@code SwingNode}
 174      */
 175     public JComponent getContent() {
 176         return content;
 177     }
 178     
 179     /*
 180      * Called on Swing thread
 181      */
 182     private void setContentImpl(JComponent content) {
 183         if (lwFrame != null) {
 184             lwFrame.dispose();
 185             lwFrame = null;
 186         }
 187         if (content != null) {
 188             lwFrame = new JLightweightFrame();
 189             contentProvider = new SwingNodeContent(content);
 190             lwFrame.setContent(contentProvider);
 191             lwFrame.setVisible(true);
 192                         
 193             locateLwFrame(); // initialize location
 194             
 195             if (focusedProperty().get()) {
 196                 activateLwFrame(true);
 197             }
 198         }
 199     }
 200     
 201     private List<Runnable> peerRequests = new ArrayList<>();
 202 
 203     /*
 204      * Called on Swing thread
 205      */
 206     void setImageBuffer(final int[] data,
 207                         final int x, final int y,
 208                         final int w, final int h,
 209                         final int linestride)
 210     {
 211         Runnable r = new Runnable() {
 212             @Override
 213             public void run() {
 214                 peer.setImageBuffer(IntBuffer.wrap(data), x, y, w, h, linestride);
 215                 impl_markDirty(DirtyBits.NODE_CONTENTS);
 216             }
 217         };
 218         if (peer != null) {
 219             Platform.runLater(r);
 220         } else {
 221             peerRequests.clear();
 222             peerRequests.add(r);
 223         }
 224     }
 225 
 226     /*
 227      * Called on Swing thread
 228      */
 229     void setImageBounds(final int x, final int y, final int w, final int h) {
 230         Runnable r = new Runnable() {
 231             @Override
 232             public void run() {
 233                 peer.setImageBounds(x, y, w, h);
 234                 impl_markDirty(DirtyBits.NODE_CONTENTS);
 235             }
 236         };
 237         if (peer != null) {
 238             Platform.runLater(r);
 239         } else {
 240             peerRequests.add(r);
 241         }
 242     }
 243 
 244     /*
 245      * Called on Swing thread
 246      */
 247     void repaintDirtyRegion(final int dirtyX, final int dirtyY, final int dirtyWidth, final int dirtyHeight) {
 248         Runnable r = new Runnable() {
 249             @Override
 250             public void run() {
 251                 peer.repaintDirtyRegion(dirtyX, dirtyY, dirtyWidth, dirtyHeight);
 252                 impl_markDirty(DirtyBits.NODE_CONTENTS);
 253             }
 254         };
 255         if (peer != null) {
 256             Platform.runLater(r);
 257         } else {
 258             peerRequests.add(r);
 259         }
 260     }
 261 
 262     @Override public boolean isResizable() {
 263         return true;
 264     }
 265 
 266     @Override public void resize(final double width, final double height) {
 267         this.width = width;
 268         this.height = height;
 269         super.resize(width, height);
 270         impl_geomChanged();
 271         impl_markDirty(DirtyBits.NODE_GEOMETRY);
 272         SwingUtilities.invokeLater(new Runnable() {
 273             @Override
 274             public void run() {
 275                 if (lwFrame != null) {
 276                     lwFrame.setSize((int)width, (int)height);
 277                 }
 278             }
 279         });
 280     }
 281 
 282     @Override
 283     public double maxWidth(double height) {
 284         return Double.MAX_VALUE;
 285     }
 286 
 287     @Override
 288     public double maxHeight(double width) {
 289         return Double.MAX_VALUE;
 290     }
 291 
 292     @Override
 293     public double prefWidth(double height) {
 294         return -1;
 295     }
 296 
 297     @Override
 298     public double prefHeight(double width) {
 299         return -1;
 300     }
 301 
 302     @Override
 303     public double minWidth(double height) {
 304         return 0;
 305     }
 306 
 307     @Override
 308     public double minHeight(double width) {
 309         return 0;
 310     }
 311 
 312     @Override
 313     protected boolean impl_computeContains(double localX, double localY) {
 314         return true;
 315     }
 316     
 317     private InvalidationListener locationListener = new InvalidationListener() {
 318         @Override
 319         public void invalidated(Observable observable) {
 320             locateLwFrame();
 321         }
 322     };
 323     
 324     private EventHandler<FocusUngrabEvent> ungrabHandler = new EventHandler<FocusUngrabEvent>() {
 325         @Override
 326         public void handle(FocusUngrabEvent event) {
 327             if (!skipBackwardUnrgabNotification) {
 328                 AccessController.doPrivileged(new PostEventAction(new UngrabEvent(lwFrame)));
 329             }
 330         }                
 331     };
 332     
 333     private ChangeListener<Boolean> windowVisibleListener = new ChangeListener<Boolean>() {
 334         @Override
 335         public void changed(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) {
 336             if (!newValue) {
 337                 disposeLwFrame();
 338 
 339             } else {
 340                 setContent(content);
 341             }
 342         }
 343     };
 344     
 345     private void removeListeners(Scene scene) {
 346         Window window = scene.getWindow();
 347         if (window != null) {
 348             window.xProperty().removeListener(locationListener);
 349             window.yProperty().removeListener(locationListener);
 350             window.removeEventHandler(FocusUngrabEvent.FOCUS_UNGRAB, ungrabHandler);
 351             window.showingProperty().removeListener(windowVisibleListener);        
 352         }
 353     }
 354     
 355     private void addListeners(Scene scene) {
 356         Window window = scene.getWindow();
 357         if (window != null) {
 358             window.xProperty().addListener(locationListener);
 359             window.yProperty().addListener(locationListener);
 360             window.addEventHandler(FocusUngrabEvent.FOCUS_UNGRAB, ungrabHandler);
 361             window.showingProperty().addListener(windowVisibleListener);
 362         }
 363     }
 364 
 365     @Override
 366     protected PGNode impl_createPGNode() {
 367         peer = com.sun.javafx.tk.Toolkit.getToolkit().createPGExternalNode();
 368         peer.setLock(paintLock);
 369         for (Runnable request : peerRequests) {
 370             request.run();
 371         }
 372         peerRequests = null;
 373         
 374         if (content != null) {
 375             setContent(content); // in case the Node is re-added to Scene
 376         }
 377         addListeners(getScene());
 378         
 379         sceneProperty().addListener(new ChangeListener<Scene>() {
 380             @Override
 381             public void changed(ObservableValue<? extends Scene> observable, Scene oldValue, Scene newValue) {
 382                 // Removed from scene, or added to another scene.
 383                 // The lwFrame will be recreated from impl_createPGNode().
 384                 removeListeners(oldValue);
 385                 disposeLwFrame();
 386             }
 387         });
 388         
 389         impl_treeVisibleProperty().addListener(new ChangeListener<Boolean>() {
 390             @Override
 391             public void changed(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) {
 392                 setLwFrameVisible(newValue);
 393             }
 394         });
 395         
 396         return peer;
 397     }
 398     
 399     @Override
 400     public void impl_updatePG() {
 401         super.impl_updatePG();
 402 
 403         if (impl_isDirty(DirtyBits.NODE_VISIBLE)) {
 404             locateLwFrame(); // initialize location
 405         }
 406     }
 407     
 408     private void locateLwFrame() {
 409         if (getScene() == null || lwFrame == null) {
 410             return;
 411         }
 412         final Point2D loc = localToScene(0, 0);
 413         final int windowX = (int)getScene().getWindow().getX();
 414         final int windowY = (int)getScene().getWindow().getY();
 415         final int sceneX = (int)getScene().getX();
 416         final int sceneY = (int)getScene().getY();
 417         
 418         invokeOnEDT(new Runnable() {
 419             @Override
 420             public void run() {
 421                 if (lwFrame != null) {
 422                     lwFrame.setLocation(windowX + sceneX + (int)loc.getX(),
 423                                         windowY + sceneY + (int)loc.getY());
 424                 }
 425             }
 426         });
 427     }
 428     
 429     private void activateLwFrame(final boolean activate) {
 430         if (lwFrame == null) {
 431             return;
 432         }
 433         invokeOnEDT(new Runnable() {
 434             @Override
 435             public void run() {
 436                 if (lwFrame != null) {
 437                     lwFrame.emulateActivation(activate);
 438                 }
 439             }
 440         });        
 441     }
 442     
 443     private void disposeLwFrame() {
 444         if (lwFrame == null) {
 445             return;
 446         }
 447         invokeOnEDT(new Runnable() {
 448             @Override
 449             public void run() {
 450                 if (lwFrame != null) {
 451                     lwFrame.dispose();
 452                     lwFrame = null;
 453                 }
 454             }
 455         });
 456     }    
 457 
 458     private void setLwFrameVisible(final boolean visible) {
 459         if (lwFrame == null) {
 460             return;
 461         }
 462         invokeOnEDT(new Runnable() {
 463             @Override
 464             public void run() {
 465                 if (lwFrame != null) {
 466                     lwFrame.setVisible(visible);
 467                 }
 468             }
 469         });
 470     }    
 471     
 472     @Override
 473     public BaseBounds impl_computeGeomBounds(BaseBounds bounds, BaseTransform tx) {
 474         bounds.deriveWithNewBounds(0, 0, 0, (float)width, (float)height, 0);
 475         tx.transform(bounds, bounds);
 476         return bounds;
 477     }
 478 
 479     @Override
 480     public Object impl_processMXNode(MXNodeAlgorithm alg, MXNodeAlgorithmContext ctx) {
 481         return alg.processLeafNode(this, ctx);
 482     }
 483 
 484     private class SwingNodeContent implements LightweightContent {
 485         private JComponent comp;
 486         public SwingNodeContent(JComponent comp) {
 487             this.comp = comp;
 488         }
 489         @Override
 490         public JComponent getComponent() {
 491             return comp;
 492         }
 493         @Override
 494         public void paintLock() {
 495             paintLock.lock();
 496         }
 497         @Override
 498         public void paintUnlock() {
 499             paintLock.unlock();
 500         }
 501         @Override
 502         public void imageBufferReset(int[] data, int x, int y, int width, int height, int linestride) {
 503             SwingNode.this.setImageBuffer(data, x, y, width, height, linestride);
 504         }
 505         @Override
 506         public void imageReshaped(int x, int y, int width, int height) {
 507             SwingNode.this.setImageBounds(x, y, width, height);
 508         }
 509         @Override
 510         public void imageUpdated(int dirtyX, int dirtyY, int dirtyWidth, int dirtyHeight) {
 511             SwingNode.this.repaintDirtyRegion(dirtyX, dirtyY, dirtyWidth, dirtyHeight);
 512         }
 513         @Override
 514         public void focusGrabbed() {
 515             Platform.runLater(new Runnable() {
 516                 @Override
 517                 public void run() {
 518                     if (getScene() != null && getScene().getWindow() != null) {
 519                         getScene().getWindow().impl_getPeer().grabFocus();
 520                     }
 521                 }
 522             });
 523         }
 524         @Override
 525         public void focusUngrabbed() {
 526             Platform.runLater(new Runnable() {
 527                 @Override
 528                 public void run() {            
 529                     if (getScene() != null && getScene().getWindow() != null) {
 530                         skipBackwardUnrgabNotification = true;
 531                         getScene().getWindow().impl_getPeer().ungrabFocus();
 532                         skipBackwardUnrgabNotification = false;                        
 533                     }
 534                 }
 535             });
 536         }
 537     }
 538 
 539     private class PostEventAction implements PrivilegedAction<Void> {
 540         private AWTEvent event;
 541         public PostEventAction(AWTEvent event) {
 542             this.event = event;
 543         }
 544         @Override
 545         public Void run() {
 546             EventQueue eq = Toolkit.getDefaultToolkit().getSystemEventQueue();
 547             eq.postEvent(event);
 548             return null;
 549         }
 550     }
 551 
 552     private class SwingMouseEventHandler implements EventHandler<MouseEvent> {
 553         @Override
 554         public void handle(MouseEvent event) {
 555             if (event.getEventType() == MouseEvent.MOUSE_PRESSED &&
 556                 !SwingNode.this.isFocused() && SwingNode.this.isFocusTraversable())
 557             {
 558                 SwingNode.this.requestFocus();
 559             }
 560             int swingID = SwingEvents.fxMouseEventTypeToMouseID(event);
 561             if (swingID < 0) {
 562                 return;
 563             }
 564             int swingModifiers = SwingEvents.fxMouseModsToMouseMods(event);
 565             // TODO: popupTrigger
 566             boolean swingPopupTrigger = event.getButton() == MouseButton.SECONDARY;
 567             int swingButton = SwingEvents.fxMouseButtonToMouseButton(event);
 568             long swingWhen = System.currentTimeMillis();
 569             java.awt.event.MouseEvent mouseEvent =
 570                     new java.awt.event.MouseEvent(
 571                         lwFrame, swingID, swingWhen, swingModifiers,
 572                         (int)event.getX(), (int)event.getY(), (int)event.getScreenX(), (int)event.getSceneY(),
 573                         event.getClickCount(), swingPopupTrigger, swingButton);
 574             AccessController.doPrivileged(new PostEventAction(mouseEvent));
 575         }
 576     }
 577 
 578     private class SwingKeyEventHandler implements EventHandler<KeyEvent> {
 579         @Override
 580         public void handle(KeyEvent event) {
 581             if (event.getCharacter().isEmpty()) {
 582                 // TODO: should we post an "empty" character?
 583                 return;
 584             }
 585             // Let Ctrl+Tab, Shift+Strl+Tab traverse focus out. 
 586             if (event.getCode() == KeyCode.TAB && event.isControlDown()) {
 587                 Direction d = event.isShiftDown() ? Direction.PREVIOUS : Direction.NEXT;
 588                 getParent().getImpl_traversalEngine().trav(SwingNode.this, d);
 589                 return;
 590             }
 591             // Don't let Arrows, Tab, Shift+Tab traverse focus out.
 592             if (event.getCode() == KeyCode.LEFT  ||
 593                 event.getCode() == KeyCode.RIGHT ||
 594                 event.getCode() == KeyCode.TAB)
 595             {
 596                 event.consume();
 597             }
 598 
 599             int swingID = SwingEvents.fxKeyEventTypeToKeyID(event);
 600             if (swingID < 0) {
 601                 return;
 602             }
 603             int swingModifiers = SwingEvents.fxKeyModsToKeyMods(event);
 604             int swingKeyCode = event.getCode().impl_getCode();            
 605             char swingChar = event.getCharacter().charAt(0);            
 606             long swingWhen = System.currentTimeMillis();
 607             java.awt.event.KeyEvent keyEvent = new java.awt.event.KeyEvent(
 608                     lwFrame, swingID, swingWhen, swingModifiers,
 609                     swingKeyCode, swingChar);
 610             AccessController.doPrivileged(new PostEventAction(keyEvent));
 611         }
 612     }
 613     
 614     private static void invokeOnEDT(final Runnable r) {
 615         if (SwingUtilities.isEventDispatchThread()) {
 616             r.run();
 617         } else {
 618             SwingUtilities.invokeLater(new Runnable() {
 619                 @Override
 620                 public void run() {
 621                     r.run();
 622                 }
 623             });
 624         }        
 625     }
 626 }