1 /*
   2  * Copyright (c) 2012, 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 java.io.UnsupportedEncodingException;
  29 
  30 import java.util.Collections;
  31 import java.util.ArrayList;
  32 import java.util.EnumSet;
  33 import java.util.Arrays;
  34 import java.util.List;
  35 import java.util.Set;
  36 
  37 import com.sun.javafx.embed.EmbeddedSceneDSInterface;
  38 import com.sun.javafx.embed.HostDragStartListener;
  39 import javafx.scene.input.TransferMode;
  40 
  41 import com.sun.javafx.embed.EmbeddedSceneInterface;
  42 import com.sun.javafx.embed.EmbeddedSceneDTInterface;
  43 import com.sun.javafx.tk.Toolkit;
  44 
  45 import javax.swing.JComponent;
  46 import javax.swing.SwingUtilities;
  47 
  48 import java.awt.Point;
  49 
  50 import java.awt.datatransfer.DataFlavor;
  51 import java.awt.datatransfer.Transferable;
  52 import java.awt.dnd.DnDConstants;
  53 import java.awt.dnd.DragGestureEvent;
  54 import java.awt.dnd.DragGestureRecognizer;
  55 import java.awt.dnd.DragSource;
  56 import java.awt.dnd.DragSourceAdapter;
  57 import java.awt.dnd.DragSourceListener;
  58 import java.awt.dnd.DragSourceDropEvent;
  59 import java.awt.dnd.DropTarget;
  60 import java.awt.dnd.DropTargetAdapter;
  61 import java.awt.dnd.DropTargetDragEvent;
  62 import java.awt.dnd.DropTargetDropEvent;
  63 import java.awt.dnd.DropTargetEvent;
  64 import java.awt.dnd.DropTargetListener;
  65 import java.awt.dnd.InvalidDnDOperationException;
  66 
  67 import java.awt.event.InputEvent;
  68 import java.awt.event.MouseAdapter;
  69 import java.awt.event.MouseEvent;
  70 
  71 /**
  72  * An utility class to connect DnD mechanism of Swing and FX.
  73  */
  74 final class SwingDnD {
  75 
  76     private final Transferable dndTransferable = new DnDTransferable();
  77 
  78     private final DragSource dragSource;
  79     private final DragSourceListener dragSourceListener;
  80 
  81     // swingDragSource and fxDropTarget are used when DnD is initiated from
  82     // Swing or external process, i.e. this SwingDnD is used as a drop target
  83     private SwingDragSource swingDragSource;
  84     private EmbeddedSceneDTInterface fxDropTarget;
  85 
  86     // fxDragSource is used when DnD is initiated from FX, i.e. this
  87     // SwingDnD acts as a drag source
  88     private EmbeddedSceneDSInterface fxDragSource;
  89 
  90     private MouseEvent me;
  91 
  92     SwingDnD(final JComponent comp, final EmbeddedSceneInterface embeddedScene) {
  93 
  94         comp.addMouseListener(new MouseAdapter() {
  95             @Override
  96             public void mouseClicked(MouseEvent me) {
  97                 storeMouseEvent(me);
  98             }
  99             @Override
 100             public void mouseDragged(MouseEvent me) {
 101                 storeMouseEvent(me);
 102             }
 103             @Override
 104             public void mousePressed(MouseEvent me) {
 105                 storeMouseEvent(me);
 106             }
 107             @Override
 108             public void mouseReleased(MouseEvent me) {
 109                 storeMouseEvent(me);
 110             }
 111         });
 112 
 113         dragSource = new DragSource();
 114         dragSourceListener = new DragSourceAdapter() {
 115             @Override
 116             public void dragDropEnd(final DragSourceDropEvent dsde) {
 117                 assert fxDragSource != null;
 118                 try {
 119                     fxDragSource.dragDropEnd(dropActionToTransferMode(dsde.getDropAction()));
 120                 } finally {
 121                     fxDragSource = null;
 122                 }
 123             }
 124         };
 125 
 126         DropTargetListener dtl = new DropTargetAdapter() {
 127             private TransferMode lastTransferMode;
 128 
 129             @Override
 130             public void dragEnter(final DropTargetDragEvent e) {
 131                 // This is a temporary workaround for JDK-8027913
 132                 if ((swingDragSource != null) || (fxDropTarget != null)) {
 133                     return;
 134                 }
 135 
 136                 assert swingDragSource == null;
 137                 swingDragSource = new SwingDragSource();
 138                 swingDragSource.updateContents(e, false);
 139 
 140                 assert fxDropTarget == null;
 141                 // Cache the Transferable data in advance, as it cannot be
 142                 // queried from drop(). See comments in dragOver() and in
 143                 // drop() below
 144                 fxDropTarget = embeddedScene.createDropTarget();
 145 
 146                 final Point orig = e.getLocation();
 147                 final Point screen = new Point(orig);
 148                 SwingUtilities.convertPointToScreen(screen, comp);
 149                 lastTransferMode = fxDropTarget.handleDragEnter(
 150                         orig.x, orig.y, screen.x, screen.y,
 151                         dropActionToTransferMode(e.getDropAction()), swingDragSource);
 152                 applyDragResult(lastTransferMode, e);
 153             }
 154 
 155             @Override
 156             public void dragExit(final DropTargetEvent e) {
 157                 assert swingDragSource != null;
 158                 assert fxDropTarget != null;
 159                 try {
 160                     fxDropTarget.handleDragLeave();
 161                 } finally {
 162                     endDnD();
 163                     lastTransferMode = null;
 164                 }
 165             }
 166 
 167             @Override
 168             public void dragOver(final DropTargetDragEvent e) {
 169                 assert swingDragSource != null;
 170                 swingDragSource.updateContents(e, false);
 171 
 172                 assert fxDropTarget != null;
 173                 final Point orig = e.getLocation();
 174                 final Point screen = new Point(orig);
 175                 SwingUtilities.convertPointToScreen(screen, comp);
 176                 lastTransferMode = fxDropTarget.handleDragOver(
 177                         orig.x, orig.y, screen.x, screen.y,
 178                         dropActionToTransferMode(e.getDropAction()));
 179                 applyDragResult(lastTransferMode, e);
 180             }
 181 
 182             @Override
 183             public void drop(final DropTargetDropEvent e) {
 184                 assert swingDragSource != null;
 185 
 186                 // This allows the subsequent call to updateContents() to
 187                 // actually fetch the data from a drag source. The actual
 188                 // and final drop result may be redefined later.
 189                 applyDropResult(lastTransferMode, e);
 190                 swingDragSource.updateContents(e, true);
 191 
 192                 final Point orig = e.getLocation();
 193                 final Point screen = new Point(orig);
 194                 SwingUtilities.convertPointToScreen(screen, comp);
 195 
 196                 assert fxDropTarget != null;
 197                 try {
 198                     lastTransferMode = fxDropTarget.handleDragDrop(
 199                             orig.x, orig.y, screen.x, screen.y,
 200                             dropActionToTransferMode(e.getDropAction()));
 201                     try {
 202                         applyDropResult(lastTransferMode, e);
 203                     } catch (InvalidDnDOperationException ignore) {
 204                         // This means the JDK doesn't contain a fix for 8029979 yet.
 205                         // DnD still works, but a drag source won't know about
 206                         // the actual drop result reported by the FX app from
 207                         // its drop() handler. It will use the dropResult from
 208                         // the last call to dragOver() instead.
 209                     }
 210                 } finally {
 211                     e.dropComplete(lastTransferMode != null);
 212                     endDnD();
 213                     lastTransferMode = null;
 214                 }
 215             }
 216         };
 217         comp.setDropTarget(new DropTarget(comp,
 218                 DnDConstants.ACTION_COPY | DnDConstants.ACTION_MOVE | DnDConstants.ACTION_LINK, dtl));
 219 
 220     }
 221 
 222     void addNotify() {
 223         dragSource.addDragSourceListener(dragSourceListener);
 224     }
 225 
 226     void removeNotify() {
 227         // RT-22049: Multi-JFrame/JFXPanel app leaks JFXPanels
 228         // Don't forget to unregister drag source listener!
 229         dragSource.removeDragSourceListener(dragSourceListener);
 230     }
 231 
 232     HostDragStartListener getDragStartListener() {
 233         return (dragSource, dragAction) -> {
 234             assert Toolkit.getToolkit().isFxUserThread();
 235             assert dragSource != null;
 236             
 237             // The method is called from FX Scene just before entering
 238             // nested event loop servicing DnD events.
 239             // It should initialize DnD in AWT EDT.
 240             SwingUtilities.invokeLater(() -> {
 241                 assert fxDragSource == null;
 242                 assert swingDragSource == null;
 243                 assert fxDropTarget == null;
 244                 
 245                 fxDragSource = dragSource;
 246                 startDrag(me, dndTransferable, dragSource.
 247                         getSupportedActions(), dragAction);
 248             });
 249         };
 250     }
 251 
 252     private void startDrag(final MouseEvent e, final Transferable t,
 253                                   final Set<TransferMode> sa,
 254                                   final TransferMode dragAction)
 255     {
 256         assert sa.contains(dragAction);
 257         // This is a replacement for the default AWT drag gesture recognizer.
 258         // Not sure DragGestureRecognizer was ever supposed to be used this way.
 259         final class StubDragGestureRecognizer extends DragGestureRecognizer {
 260             StubDragGestureRecognizer(DragSource ds) {
 261                 super(ds, e.getComponent());
 262                 setSourceActions(transferModesToDropActions(sa));
 263                 appendEvent(e);
 264             }
 265             @Override
 266             protected void registerListeners() {
 267             }
 268             @Override
 269             protected void unregisterListeners() {
 270             }
 271         }
 272 
 273         final Point pt = new Point(e.getX(), e.getY());
 274         final int action = transferModeToDropAction(dragAction);
 275         final DragGestureRecognizer dgs = new StubDragGestureRecognizer(dragSource);
 276         final List<InputEvent> events =
 277                 Arrays.asList(new InputEvent[] { dgs.getTriggerEvent() });
 278         final DragGestureEvent dse = new DragGestureEvent(dgs, action, pt, events);
 279         dse.startDrag(null, t);
 280     }
 281 
 282     private void endDnD() {
 283         assert swingDragSource != null;
 284         assert fxDropTarget != null;
 285         fxDropTarget = null;
 286         swingDragSource = null;
 287     }
 288 
 289     private void storeMouseEvent(final MouseEvent me) {
 290         this.me = me;
 291     }
 292 
 293     private void applyDragResult(final TransferMode dragResult,
 294                                  final DropTargetDragEvent e)
 295     {
 296         if (dragResult == null) {
 297             e.rejectDrag();
 298         } else {
 299             e.acceptDrag(transferModeToDropAction(dragResult));
 300         }
 301     }
 302 
 303     private void applyDropResult(final TransferMode dropResult,
 304                                  final DropTargetDropEvent e)
 305     {
 306         if (dropResult == null) {
 307             e.rejectDrop();
 308         } else {
 309             e.acceptDrop(transferModeToDropAction(dropResult));
 310         }
 311     }
 312 
 313     static TransferMode dropActionToTransferMode(final int dropAction) {
 314         switch (dropAction) {
 315             case DnDConstants.ACTION_COPY:
 316                 return TransferMode.COPY;
 317             case DnDConstants.ACTION_MOVE:
 318                 return TransferMode.MOVE;
 319             case DnDConstants.ACTION_LINK:
 320                 return TransferMode.LINK;
 321             case DnDConstants.ACTION_NONE:
 322                 return null;
 323             default:
 324                 throw new IllegalArgumentException();
 325         }
 326     }
 327 
 328     static int transferModeToDropAction(final TransferMode tm) {
 329         switch (tm) {
 330             case COPY:
 331                 return DnDConstants.ACTION_COPY;
 332             case MOVE:
 333                 return DnDConstants.ACTION_MOVE;
 334             case LINK:
 335                 return DnDConstants.ACTION_LINK;
 336             default:
 337                 throw new IllegalArgumentException();
 338         }
 339     }
 340 
 341     static Set<TransferMode> dropActionsToTransferModes(
 342             final int dropActions)
 343     {
 344         final Set<TransferMode> tms = EnumSet.noneOf(TransferMode.class);
 345         if ((dropActions & DnDConstants.ACTION_COPY) != 0) {
 346             tms.add(TransferMode.COPY);
 347         }
 348         if ((dropActions & DnDConstants.ACTION_MOVE) != 0) {
 349             tms.add(TransferMode.MOVE);
 350         }
 351         if ((dropActions & DnDConstants.ACTION_LINK) != 0) {
 352             tms.add(TransferMode.LINK);
 353         }
 354         return Collections.unmodifiableSet(tms);
 355     }
 356 
 357     static int transferModesToDropActions(final Set<TransferMode> tms) {
 358         int dropActions = DnDConstants.ACTION_NONE;
 359         for (TransferMode tm : tms) {
 360             dropActions |= transferModeToDropAction(tm);
 361         }
 362         return dropActions;
 363     }
 364 
 365     // Transferable wrapper over FX dragboard. All the calls are
 366     // forwarded to FX and executed on the FX event thread.
 367     private final class DnDTransferable implements Transferable {
 368 
 369         @Override
 370         public Object getTransferData(final DataFlavor flavor)
 371                 throws UnsupportedEncodingException
 372         {
 373             assert fxDragSource != null;
 374             assert SwingUtilities.isEventDispatchThread();
 375 
 376             String mimeType = DataFlavorUtils.getFxMimeType(flavor);
 377             return DataFlavorUtils.adjustFxData(
 378                     flavor, fxDragSource.getData(mimeType));
 379         }
 380 
 381         @Override
 382         public DataFlavor[] getTransferDataFlavors() {
 383             assert fxDragSource != null;
 384             assert SwingUtilities.isEventDispatchThread();
 385 
 386             final String mimeTypes[] = fxDragSource.getMimeTypes();
 387 
 388             final ArrayList<DataFlavor> flavors =
 389                     new ArrayList<DataFlavor>(mimeTypes.length);
 390             for (String mime : mimeTypes) {
 391                 DataFlavor flavor = null;
 392                 try {
 393                     flavor = new DataFlavor(mime);
 394                 } catch (ClassNotFoundException e) {
 395                     // FIXME: what to do?
 396                     continue;
 397                 }
 398                 flavors.add(flavor);
 399             }
 400             return flavors.toArray(new DataFlavor[0]);
 401         }
 402 
 403         @Override
 404         public boolean isDataFlavorSupported(final DataFlavor flavor) {
 405             assert fxDragSource != null;
 406             assert SwingUtilities.isEventDispatchThread();
 407 
 408             return fxDragSource.isMimeTypeAvailable(
 409                     DataFlavorUtils.getFxMimeType(flavor));
 410         }
 411     }
 412 }