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