1 /*
   2  * Copyright (c) 2018, 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 com.sun.javafx.embed.swing.oldimpl;
  27 
  28 import com.sun.javafx.tk.Toolkit;
  29 import java.awt.Component;
  30 import java.awt.Cursor;
  31 import java.awt.Point;
  32 import java.awt.SecondaryLoop;
  33 import java.awt.datatransfer.DataFlavor;
  34 import java.awt.datatransfer.Transferable;
  35 import java.awt.dnd.DnDConstants;
  36 import java.awt.dnd.DragGestureEvent;
  37 import java.awt.dnd.DragGestureListener;
  38 import java.awt.dnd.DragGestureRecognizer;
  39 import java.awt.dnd.DragSource;
  40 import java.awt.dnd.DropTarget;
  41 import java.awt.dnd.DropTargetContext;
  42 import java.awt.dnd.DropTargetDragEvent;
  43 import java.awt.dnd.DropTargetDropEvent;
  44 import java.awt.dnd.DropTargetListener;
  45 import java.awt.dnd.InvalidDnDOperationException;
  46 import java.awt.dnd.MouseDragGestureRecognizer;
  47 import java.awt.dnd.peer.DragSourceContextPeer;
  48 import java.awt.dnd.peer.DropTargetContextPeer;
  49 import java.security.AccessController;
  50 import java.security.PrivilegedAction;
  51 import java.util.HashMap;
  52 import java.util.Map;
  53 import javafx.application.Platform;
  54 import javafx.event.EventHandler;
  55 import javafx.event.EventType;
  56 import javafx.scene.input.DataFormat;
  57 import javafx.scene.input.DragEvent;
  58 import javafx.scene.input.Dragboard;
  59 import javafx.scene.input.MouseEvent;
  60 import javafx.scene.input.TransferMode;
  61 import sun.awt.AWTAccessor;
  62 import sun.awt.dnd.SunDragSourceContextPeer;
  63 import sun.swing.JLightweightFrame;
  64 
  65 import com.sun.javafx.embed.swing.FXDnDInterop;
  66 import com.sun.javafx.embed.swing.CachingTransferable;
  67 import com.sun.javafx.embed.swing.SwingDnD;
  68 import com.sun.javafx.embed.swing.FXDnD;
  69 import com.sun.javafx.embed.swing.SwingEvents;
  70 import com.sun.javafx.embed.swing.SwingNodeHelper;
  71 import javafx.embed.swing.SwingNode;
  72 
  73 public class FXDnDInteropO extends FXDnDInterop {
  74 
  75     public Component findComponentAt(Object frame, int x, int y,
  76                                               boolean ignoreEnabled) {
  77         JLightweightFrame lwFrame = (JLightweightFrame) frame;
  78         return AWTAccessor.getContainerAccessor().findComponentAt(lwFrame,
  79                         x, y, false);
  80     }
  81 
  82     public boolean isCompEqual(Component c, Object frame) {
  83         JLightweightFrame lwFrame = (JLightweightFrame) frame;
  84         return c != lwFrame;
  85     }
  86 
  87     public int convertModifiersToDropAction(int modifiers,
  88                                                      int supportedActions) {
  89         return SunDragSourceContextPeer.convertModifiersToDropAction(modifiers,
  90                         supportedActions);
  91     }
  92 
  93     public <T extends DragGestureRecognizer> T createDragGestureRecognizer(
  94                         DragSource ds, Component c, int srcActions,
  95                            DragGestureListener dgl) {
  96         return (T) new FXDragGestureRecognizer(ds, c, srcActions, dgl);
  97     }
  98 
  99     public DragSourceContextPeer createDragSourceContext(DragGestureEvent dge)
 100             throws InvalidDnDOperationException {
 101         return new FXDragSourceContextPeer(dge);
 102     }
 103 
 104     private void runOnFxThread(Runnable runnable) {
 105         if (Platform.isFxApplicationThread()) {
 106             runnable.run();
 107         } else {
 108             Platform.runLater(runnable);
 109         }
 110     }
 111 
 112     private SwingNode getNode() {
 113         return node;
 114     }
 115 
 116     public void setNode(SwingNode swnode) {
 117         node = swnode;
 118     }
 119 
 120     private SwingNode node = null;
 121 
 122     /**
 123      * Utility class that operates on Maps with Components as keys.
 124      * Useful when processing mouse events to choose an object from the map
 125      * based on the component located at the given coordinates.
 126      */
 127     private class ComponentMapper<T> {
 128         public int x, y;
 129         public T object = null;
 130 
 131         private ComponentMapper(Map<Component, T> map, int xArg, int yArg) {
 132             this.x = xArg;
 133             this.y = yArg;
 134 
 135             final JLightweightFrame lwFrame = (JLightweightFrame)SwingNodeHelper.getLightweightFrame(node);
 136             Component c = AWTAccessor.getContainerAccessor().findComponentAt(lwFrame, x, y, false);
 137             if (c == null) return;
 138 
 139             synchronized (c.getTreeLock()) {
 140                 do {
 141                     object = map.get(c);
 142                 } while (object == null && (c = c.getParent()) != null);
 143 
 144                 if (object != null) {
 145                     // The object is either a DropTarget or a DragSource, so:
 146                     //assert c == object.getComponent();
 147 
 148                     // Translate x, y from lwFrame to component coordinates
 149                     while (c != lwFrame && c != null) {
 150                         x -= c.getX();
 151                         y -= c.getY();
 152                         c = c.getParent();
 153                     }
 154                 }
 155             }
 156         }
 157     }
 158 
 159     public <T> ComponentMapper<T> mapComponent(Map<Component, T> map, int x, int y) {
 160         return new ComponentMapper<T>(map, x, y);
 161     }
 162 
 163     ///////////////////////////////////////////////////////////////////////////
 164     //     DRAG SOURCE IMPLEMENTATION
 165     ///////////////////////////////////////////////////////////////////////////
 166 
 167 
 168     private boolean isDragSourceListenerInstalled = false;
 169 
 170     // To keep track of where the DnD gesture actually started
 171     private MouseEvent pressEvent = null;
 172     private long pressTime = 0;
 173 
 174     private volatile SecondaryLoop loop;
 175 
 176     private final Map<Component, FXDragGestureRecognizer> recognizers = new HashMap<>();
 177 
 178     // Note that we don't really use the MouseDragGestureRecognizer facilities,
 179     // however some code in JDK may expect a descendant of this class rather
 180     // than a generic DragGestureRecognizer. So we inherit from it.
 181     private class FXDragGestureRecognizer extends MouseDragGestureRecognizer {
 182         FXDragGestureRecognizer(DragSource ds, Component c, int srcActions,
 183             DragGestureListener dgl)
 184         {
 185             super(ds, c, srcActions, dgl);
 186 
 187             if (c != null) recognizers.put(c, this);
 188         }
 189 
 190         @Override public void setComponent(Component c) {
 191             final Component old = getComponent();
 192             if (old != null) recognizers.remove(old);
 193             super.setComponent(c);
 194             if (c != null) recognizers.put(c, this);
 195         }
 196 
 197         protected void registerListeners() {
 198             runOnFxThread(() -> {
 199                 if (!isDragSourceListenerInstalled) {
 200                     node.addEventHandler(MouseEvent.MOUSE_PRESSED, onMousePressHandler);
 201                     node.addEventHandler(MouseEvent.DRAG_DETECTED, onDragStartHandler);
 202                     node.addEventHandler(DragEvent.DRAG_DONE, onDragDoneHandler);
 203 
 204                     isDragSourceListenerInstalled = true;
 205                 }
 206             });
 207         }
 208 
 209         protected void unregisterListeners() {
 210             runOnFxThread(() -> {
 211                 if (isDragSourceListenerInstalled) {
 212                     node.removeEventHandler(MouseEvent.MOUSE_PRESSED, onMousePressHandler);
 213                     node.removeEventHandler(MouseEvent.DRAG_DETECTED, onDragStartHandler);
 214                     node.removeEventHandler(DragEvent.DRAG_DONE, onDragDoneHandler);
 215 
 216                     isDragSourceListenerInstalled = false;
 217                 }
 218             });
 219         }
 220 
 221         private void fireEvent(int x, int y, long evTime, int modifiers) {
 222             // In theory we should register all the events that trigger the gesture (like PRESS, DRAG, DRAG, BINGO!)
 223             // But we can live with this hack for now.
 224             appendEvent(new java.awt.event.MouseEvent(getComponent(), java.awt.event.MouseEvent.MOUSE_PRESSED,
 225                         evTime, modifiers, x, y, 0, false));
 226 
 227             // Also, the modifiers here should've actually come from the last known mouse event (last MOVE or DRAG).
 228             // But we're OK with using the initial PRESS modifiers for now
 229             int initialAction = SunDragSourceContextPeer.convertModifiersToDropAction(
 230                     modifiers, getSourceActions());
 231 
 232             fireDragGestureRecognized(initialAction, new java.awt.Point(x, y));
 233         }
 234     }
 235 
 236     // Invoked on EDT
 237     private void fireEvent(int x, int y, long evTime, int modifiers) {
 238         ComponentMapper<FXDragGestureRecognizer> mapper = mapComponent(recognizers, x, y);
 239 
 240         final FXDragGestureRecognizer r = mapper.object;
 241         if (r != null) {
 242             r.fireEvent(mapper.x, mapper.y, evTime, modifiers);
 243         } else {
 244             // No recognizer, no DnD, no startDrag, so release the FX loop now
 245             SwingNodeHelper.leaveFXNestedLoop(this);
 246         }
 247     }
 248 
 249     private MouseEvent getInitialGestureEvent() {
 250         return pressEvent;
 251     }
 252 
 253     private final EventHandler<MouseEvent> onMousePressHandler = (event) -> {
 254         // It would be nice to maintain a list of all the events that initiate
 255         // a DnD gesture (see a comment in FXDragGestureRecognizer.fireEvent().
 256         // For now, we simply use the initial PRESS event for this purpose.
 257         pressEvent = event;
 258         pressTime = System.currentTimeMillis();
 259     };
 260 
 261 
 262     private volatile FXDragSourceContextPeer activeDSContextPeer;
 263 
 264     private final EventHandler<MouseEvent> onDragStartHandler = (event) -> {
 265         // Call to AWT and determine the active DragSourceContextPeer
 266         activeDSContextPeer = null;
 267         final MouseEvent firstEv = getInitialGestureEvent();
 268         SwingNodeHelper.runOnEDTAndWait(FXDnDInteropO.this, () -> fireEvent(
 269                     (int)firstEv.getX(), (int)firstEv.getY(), pressTime,
 270                     SwingEvents.fxMouseModsToMouseMods(firstEv)));
 271         if (activeDSContextPeer == null) return;
 272 
 273         // Since we're going to start DnD, consume the event.
 274         event.consume();
 275 
 276         Dragboard db = getNode().startDragAndDrop(SwingDnD.dropActionsToTransferModes(
 277                     activeDSContextPeer.sourceActions).toArray(new TransferMode[1]));
 278 
 279         // At this point the activeDSContextPeer.transferable contains all the data from AWT
 280         Map<DataFormat, Object> fxData = new HashMap<>();
 281         for (String mt : activeDSContextPeer.transferable.getMimeTypes()) {
 282             DataFormat f = DataFormat.lookupMimeType(mt);
 283             //TODO: what to do if f == null?
 284             if (f != null) fxData.put(f, activeDSContextPeer.transferable.getData(mt));
 285         }
 286 
 287         final boolean hasContent = db.setContent(fxData);
 288         if (!hasContent) {
 289             // No data, no DnD, no onDragDoneHandler, so release the AWT loop now
 290             if (!FXDnD.fxAppThreadIsDispatchThread) {
 291                 loop.exit();
 292             }
 293         }
 294     };
 295 
 296     private final EventHandler<DragEvent> onDragDoneHandler = (event) -> {
 297         event.consume();
 298 
 299         // Release FXDragSourceContextPeer.startDrag()
 300         if (!FXDnD.fxAppThreadIsDispatchThread) {
 301             loop.exit();
 302         }
 303 
 304         if (activeDSContextPeer != null) {
 305             final TransferMode mode = event.getTransferMode();
 306             activeDSContextPeer.dragDone(
 307                     mode == null ? 0 : SwingDnD.transferModeToDropAction(mode),
 308                     (int)event.getX(), (int)event.getY());
 309         }
 310     };
 311 
 312 
 313     private final class FXDragSourceContextPeer extends SunDragSourceContextPeer {
 314         private volatile int sourceActions = 0;
 315 
 316         private final CachingTransferable transferable = new CachingTransferable();
 317 
 318         @Override public void startSecondaryEventLoop(){
 319             Toolkit.getToolkit().enterNestedEventLoop(this);
 320         }
 321         @Override public void quitSecondaryEventLoop(){
 322             assert !Platform.isFxApplicationThread();
 323             Platform.runLater(() -> Toolkit.getToolkit().exitNestedEventLoop(FXDragSourceContextPeer.this, null));
 324         }
 325 
 326         @Override protected void setNativeCursor(long nativeCtxt, Cursor c, int cType) {
 327             //TODO
 328         }
 329 
 330 
 331         private void dragDone(int operation, int x, int y) {
 332             dragDropFinished(operation != 0, operation, x, y);
 333         }
 334 
 335         FXDragSourceContextPeer(DragGestureEvent dge) {
 336             super(dge);
 337         }
 338 
 339 
 340         // It's Map<Long, DataFlavor> actually, but javac complains if the type isn't erased...
 341         @Override protected void startDrag(Transferable trans, long[] formats, Map formatMap)
 342         {
 343             activeDSContextPeer = this;
 344 
 345             // NOTE: we ignore the formats[] and the formatMap altogether.
 346             // AWT provides those to allow for more flexible representations of
 347             // e.g. text data (in various formats, encodings, etc.) However, FX
 348             // code isn't ready to handle those (e.g. it can't digest a
 349             // StringReader as data, etc.) So instead we perform our internal
 350             // translation.
 351             // Note that fetchData == true. FX doesn't support delayed data
 352             // callbacks yet anyway, so we have to fetch all the data from AWT upfront.
 353             transferable.updateData(trans, true);
 354 
 355             sourceActions = getDragSourceContext().getSourceActions();
 356 
 357             // Release the FX nested loop to allow onDragDetected to start the actual DnD operation,
 358             // and then start an AWT nested loop to wait until DnD finishes.
 359             if (!FXDnD.fxAppThreadIsDispatchThread) {
 360                 loop = java.awt.Toolkit.getDefaultToolkit().getSystemEventQueue().createSecondaryLoop();
 361                 SwingNodeHelper.leaveFXNestedLoop(FXDnDInteropO.this);
 362                 if (!loop.enter()) {
 363                     // An error occured, but there's little we can do here...
 364                 }
 365             }
 366         }
 367     };
 368 
 369     ///////////////////////////////////////////////////////////////////////////
 370     //     DROP TARGET IMPLEMENTATION
 371     ///////////////////////////////////////////////////////////////////////////
 372 
 373 
 374     private boolean isDropTargetListenerInstalled = false;
 375     private volatile FXDropTargetContextPeer activeDTContextPeer = null;
 376     private final Map<Component, DropTarget> dropTargets = new HashMap<>();
 377 
 378     private final EventHandler<DragEvent> onDragEnteredHandler = (event) -> {
 379         if (activeDTContextPeer == null) activeDTContextPeer = new FXDropTargetContextPeer();
 380 
 381         int action = activeDTContextPeer.postDropTargetEvent(event);
 382 
 383         // If AWT doesn't accept anything, let parent nodes handle the event
 384         if (action != 0) event.consume();
 385     };
 386 
 387     private final EventHandler<DragEvent> onDragExitedHandler = (event) -> {
 388         if (activeDTContextPeer == null) activeDTContextPeer = new FXDropTargetContextPeer();
 389 
 390         activeDTContextPeer.postDropTargetEvent(event);
 391 
 392         activeDTContextPeer = null;
 393     };
 394 
 395     private final EventHandler<DragEvent> onDragOverHandler = (event) -> {
 396         if (activeDTContextPeer == null) activeDTContextPeer = new FXDropTargetContextPeer();
 397 
 398         int action = activeDTContextPeer.postDropTargetEvent(event);
 399 
 400         // If AWT doesn't accept anything, let parent nodes handle the event
 401         if (action != 0) {
 402             // NOTE: in FX the acceptTransferModes() may ONLY be called from DRAG_OVER.
 403             // If the AWT app always reports NONE and suddenly decides to accept the
 404             // data in its DRAG_DROPPED handler, this just won't work. There's no way
 405             // to workaround this other than by modifing the AWT application code.
 406             event.acceptTransferModes(SwingDnD.dropActionsToTransferModes(action).toArray(new TransferMode[1]));
 407             event.consume();
 408         }
 409     };
 410 
 411     private final EventHandler<DragEvent> onDragDroppedHandler = (event) -> {
 412         if (activeDTContextPeer == null) activeDTContextPeer = new FXDropTargetContextPeer();
 413 
 414         int action = activeDTContextPeer.postDropTargetEvent(event);
 415 
 416         if (action != 0) {
 417             // NOTE: the dropAction is ignored since we use the action last
 418             // reported from the DRAG_OVER handler.
 419             //
 420             // We might want to:
 421             //
 422             //    assert activeDTContextPeer.dropAction == onDragDroppedHandler.currentAction;
 423             //
 424             // and maybe print a diagnostic message if they differ.
 425             event.setDropCompleted(activeDTContextPeer.success);
 426 
 427             event.consume();
 428         }
 429 
 430         activeDTContextPeer = null;
 431     };
 432 
 433     private final class FXDropTargetContextPeer implements DropTargetContextPeer {
 434 
 435         private int targetActions = DnDConstants.ACTION_NONE;
 436         private int currentAction = DnDConstants.ACTION_NONE;
 437         private DropTarget dt = null;
 438         private DropTargetContext ctx = null;
 439 
 440         private final CachingTransferable transferable = new CachingTransferable();
 441 
 442         // Drop result
 443         private boolean success = false;
 444         private int dropAction = 0;
 445 
 446         @Override public synchronized void setTargetActions(int actions) { targetActions = actions; }
 447         @Override public synchronized int getTargetActions() { return targetActions; }
 448 
 449         @Override public synchronized DropTarget getDropTarget() { return dt; }
 450 
 451         @Override public synchronized boolean isTransferableJVMLocal() { return false; }
 452 
 453         @Override public synchronized DataFlavor[] getTransferDataFlavors() { return transferable.getTransferDataFlavors(); }
 454         @Override public synchronized Transferable getTransferable() { return transferable; }
 455 
 456         @Override public synchronized void acceptDrag(int dragAction) { currentAction = dragAction; }
 457         @Override public synchronized void rejectDrag() { currentAction = DnDConstants.ACTION_NONE; }
 458 
 459         @Override public synchronized void acceptDrop(int dropAction) { this.dropAction = dropAction; }
 460         @Override public synchronized void rejectDrop() { dropAction = DnDConstants.ACTION_NONE; }
 461 
 462         @Override public synchronized void dropComplete(boolean success) { this.success = success; }
 463 
 464 
 465         private int postDropTargetEvent(DragEvent event)
 466         {
 467             ComponentMapper<DropTarget> mapper = mapComponent(dropTargets, (int)event.getX(), (int)event.getY());
 468 
 469             final EventType<?> fxEvType = event.getEventType();
 470 
 471             Dragboard db = event.getDragboard();
 472             transferable.updateData(db, DragEvent.DRAG_DROPPED.equals(fxEvType));
 473 
 474             final int sourceActions = SwingDnD.transferModesToDropActions(db.getTransferModes());
 475             final int userAction = event.getTransferMode() == null ? DnDConstants.ACTION_NONE
 476                 : SwingDnD.transferModeToDropAction(event.getTransferMode());
 477 
 478             // A target for the AWT DnD event
 479             DropTarget target = mapper.object != null ? mapper.object : dt;
 480 
 481             SwingNodeHelper.runOnEDTAndWait(FXDnDInteropO.this, () -> {
 482                 if (target != dt) {
 483                     if (ctx != null) {
 484                         AWTAccessor.getDropTargetContextAccessor().reset(ctx);
 485                     }
 486                     ctx = null;
 487 
 488                     currentAction = dropAction = DnDConstants.ACTION_NONE;
 489                 }
 490 
 491                 if (target != null) {
 492                     if (ctx == null) {
 493                         ctx = target.getDropTargetContext();
 494             AWTAccessor.getDropTargetContextAccessor()
 495                             .setDropTargetContextPeer(ctx, FXDropTargetContextPeer.this);
 496                     }
 497 
 498                     DropTargetListener dtl = (DropTargetListener)target;
 499 
 500                     if (DragEvent.DRAG_DROPPED.equals(fxEvType)) {
 501                         DropTargetDropEvent awtEvent = new DropTargetDropEvent(
 502                             ctx, new Point(mapper.x, mapper.y), userAction, sourceActions);
 503 
 504                         dtl.drop(awtEvent);
 505                     } else {
 506                         DropTargetDragEvent awtEvent = new DropTargetDragEvent(
 507                             ctx, new Point(mapper.x, mapper.y), userAction, sourceActions);
 508 
 509                         if (DragEvent.DRAG_OVER.equals(fxEvType)) dtl.dragOver(awtEvent);
 510                         else if (DragEvent.DRAG_ENTERED.equals(fxEvType)) dtl.dragEnter(awtEvent);
 511                         else if (DragEvent.DRAG_EXITED.equals(fxEvType)) dtl.dragExit(awtEvent);
 512                     }
 513                 }
 514 
 515                 dt = mapper.object;
 516                 if (dt == null) {
 517                     // FIXME: once we switch to JDK 9 as the boot JDK
 518                     // we need to re-implement the following using
 519                     // available API.
 520                     /*
 521                     if (ctx != null) ctx.removeNotify();
 522                     */
 523                     ctx = null;
 524 
 525                     currentAction = dropAction = DnDConstants.ACTION_NONE;
 526                 }
 527                 if (DragEvent.DRAG_DROPPED.equals(fxEvType) || DragEvent.DRAG_EXITED.equals(fxEvType)) {
 528                     // This must be done to ensure that the data isn't being
 529                     // cached in AWT. Otherwise subsequent DnD operations will
 530                     // see the old data only.
 531                     // FIXME: once we switch to JDK 9 as the boot JDK
 532                     // we need to re-implement the following using
 533                     // available API.
 534                     /*
 535                     if (ctx != null) ctx.removeNotify();
 536                     */
 537                     ctx = null;
 538                 }
 539 
 540                 SwingNodeHelper.leaveFXNestedLoop(FXDnDInteropO.this);
 541             });
 542 
 543             if (DragEvent.DRAG_DROPPED.equals(fxEvType)) return dropAction;
 544 
 545             return currentAction;
 546         }
 547     }
 548 
 549     public void addDropTarget(DropTarget dt, SwingNode node) {
 550         dropTargets.put(dt.getComponent(), dt);
 551         Platform.runLater(() -> {
 552             if (!isDropTargetListenerInstalled) {
 553                 node.addEventHandler(DragEvent.DRAG_ENTERED, onDragEnteredHandler);
 554                 node.addEventHandler(DragEvent.DRAG_EXITED, onDragExitedHandler);
 555                 node.addEventHandler(DragEvent.DRAG_OVER, onDragOverHandler);
 556                 node.addEventHandler(DragEvent.DRAG_DROPPED, onDragDroppedHandler);
 557 
 558                 isDropTargetListenerInstalled = true;
 559             }
 560         });
 561     }
 562 
 563     public void removeDropTarget(DropTarget dt, SwingNode node) {
 564         dropTargets.remove(dt.getComponent());
 565         Platform.runLater(() -> {
 566             if (isDropTargetListenerInstalled && dropTargets.isEmpty()) {
 567                 node.removeEventHandler(DragEvent.DRAG_ENTERED, onDragEnteredHandler);
 568                 node.removeEventHandler(DragEvent.DRAG_EXITED, onDragExitedHandler);
 569                 node.removeEventHandler(DragEvent.DRAG_OVER, onDragOverHandler);
 570                 node.removeEventHandler(DragEvent.DRAG_DROPPED, onDragDroppedHandler);
 571 
 572                 isDropTargetListenerInstalled = false;
 573             }
 574         });
 575     }
 576 }