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