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