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