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