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 * @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 * @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 }