1 /*
   2  * Copyright (c) 2012, 2013, 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.IOException;
  29 import java.util.Collections;
  30 import java.util.ArrayList;
  31 import java.util.EnumSet;
  32 import java.util.Arrays;
  33 import java.util.List;
  34 import java.util.Set;
  35 
  36 import javafx.scene.input.TransferMode;
  37 
  38 import com.sun.javafx.embed.EmbeddedSceneInterface;
  39 import com.sun.javafx.embed.EmbeddedSceneDragSourceInterface;
  40 import com.sun.javafx.embed.EmbeddedSceneDragStartListenerInterface;
  41 import com.sun.javafx.embed.EmbeddedSceneDropTargetInterface;
  42 import com.sun.javafx.tk.Toolkit;
  43 
  44 import javax.swing.JComponent;
  45 import javax.swing.SwingUtilities;
  46 
  47 import java.awt.Point;
  48 
  49 import java.awt.datatransfer.DataFlavor;
  50 import java.awt.datatransfer.Transferable;
  51 import java.awt.datatransfer.UnsupportedFlavorException;
  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 
  65 import java.awt.event.InputEvent;
  66 import java.awt.event.MouseAdapter;
  67 import java.awt.event.MouseEvent;
  68 
  69 /**
  70  * An utility class to connect DnD mechanism of Swing and FX.
  71  */
  72 final class SwingDnD {
  73 
  74     private final JFXPanelFacade facade;
  75     private final Transferable dndTransferable = new DnDTransferable();
  76     private final DragSourceListener dragSourceListener;
  77     private SwingDragSource swingDragSource;
  78     private EmbeddedSceneDragSourceInterface fxDragSource;
  79     private EmbeddedSceneDropTargetInterface dropTarget;
  80     private MouseEvent me;
  81 
  82     interface JFXPanelFacade {
  83 
  84         EmbeddedSceneInterface getScene();
  85     }
  86 
  87     SwingDnD(final JComponent comp, final JFXPanelFacade facade) {
  88         this.facade = facade;
  89 
  90         comp.addMouseListener(new MouseAdapter() {
  91 
  92             @Override
  93             public void mouseClicked(MouseEvent me) {
  94                 storeMouseEvent(me);
  95             }
  96 
  97             @Override
  98             public void mouseDragged(MouseEvent me) {
  99                 storeMouseEvent(me);
 100             }
 101 
 102             @Override
 103             public void mousePressed(MouseEvent me) {
 104                 storeMouseEvent(me);
 105             }
 106 
 107             @Override
 108             public void mouseReleased(MouseEvent me) {
 109                 storeMouseEvent(me);
 110             }
 111         });
 112 
 113         dragSourceListener = new DragSourceAdapter() {
 114 
 115             @Override
 116             public void dragDropEnd(final DragSourceDropEvent dsde) {
 117                 // Fix for RT-21836
 118                 if (fxDragSource == null) {
 119                     return;
 120                 }
 121 
 122                 assert hasFxScene();
 123 
 124                 try {
 125                     fxDragSource.dragDropEnd(dropActionToTransferMode(dsde.
 126                             getDropAction()));
 127                 } finally {
 128                     fxDragSource = null;
 129                 }
 130             }
 131         };
 132 
 133         new DropTarget(comp, DnDConstants.ACTION_COPY | DnDConstants.ACTION_MOVE |
 134                 DnDConstants.ACTION_LINK, new DropTargetAdapter() {
 135 
 136             @Override
 137             public void dragEnter(final DropTargetDragEvent e) {
 138                 if (!hasFxScene()) {
 139                     e.rejectDrag();
 140                     return;
 141                 }
 142 
 143                 if (fxDragSource == null) {
 144                     // There is no FX drag source, create wrapper for external
 145                     // drag source.
 146                     assert swingDragSource == null;
 147                     swingDragSource = new SwingDragSource(e);
 148                 }
 149 
 150                 final Point orig = e.getLocation();
 151                 final Point screen = new Point(orig);
 152                 SwingUtilities.convertPointToScreen(screen, comp);
 153                 applyDragResult(getDropTarget().handleDragEnter(orig.x, orig.y,
 154                                                                 screen.x,
 155                                                                 screen.y,
 156                                                                 dropActionToTransferMode(e.
 157                         getDropAction()), getDragSource()), e);
 158             }
 159 
 160             @Override
 161             public void dragExit(final DropTargetEvent e) {
 162                 if (!hasFxScene()) {
 163                     // The drag has been already rejected in dragEnter(), but it doesn't
 164                     // prevent dragExit(), dragOver() and drop() from being called
 165                     return;
 166                 }
 167                 
 168                 try {
 169                     dropTarget.handleDragLeave();
 170                 } finally {
 171                     endDnD();
 172                 }
 173             }
 174 
 175             @Override
 176             public void dragOver(final DropTargetDragEvent e) {
 177                 if (!hasFxScene()) {
 178                     // The drag has been already rejected in dragEnter(), but it doesn't
 179                     // prevent dragExit(), dragOver() and drop() from being called
 180                     return;
 181                 }
 182 
 183                 if (swingDragSource != null) {
 184                     swingDragSource.updateContents(e);
 185                 }
 186 
 187                 final Point orig = e.getLocation();
 188                 final Point screen = new Point(orig);
 189                 SwingUtilities.convertPointToScreen(screen, comp);
 190                 applyDragResult(dropTarget.handleDragOver(orig.x, orig.y,
 191                                                           screen.x, screen.y,
 192                                                           dropActionToTransferMode(e.
 193                         getDropAction())), e);
 194             }
 195 
 196             @Override
 197             public void drop(final DropTargetDropEvent e) {
 198                 if (!hasFxScene()) {
 199                     // The drag has been already rejected in dragEnter(), but it doesn't
 200                     // prevent dragExit(), dragOver() and drop() from being called
 201                     return;
 202                 }
 203 
 204                 final Point orig = e.getLocation();
 205                 final Point screen = new Point(orig);
 206                 SwingUtilities.convertPointToScreen(screen, comp);
 207 
 208                 try {
 209                     final TransferMode dropResult =
 210                             dropTarget.handleDragDrop(orig.x, orig.y, screen.x,
 211                                                       screen.y,
 212                                                       dropActionToTransferMode(e.
 213                             getDropAction()));
 214                     applyDropResult(dropResult, e);
 215 
 216                     e.dropComplete(dropResult != null);
 217                 } finally {
 218                     endDnD();
 219                 }
 220             }
 221         });
 222     }
 223 
 224     void addNotify() {
 225         DragSource.getDefaultDragSource().addDragSourceListener(
 226                 dragSourceListener);
 227     }
 228 
 229     void removeNotify() {
 230         // RT-22049: Multi-JFrame/JFXPanel app leaks JFXPanels
 231         // Don't forget to unregister drag source listener!
 232         DragSource.getDefaultDragSource().removeDragSourceListener(
 233                 dragSourceListener);
 234     }
 235 
 236     EmbeddedSceneDragStartListenerInterface getDragStartListener() {
 237         return new EmbeddedSceneDragStartListenerInterface() {
 238 
 239             @Override
 240             public void dragStarted(
 241                     final EmbeddedSceneDragSourceInterface dragSource,
 242                     final TransferMode dragAction) {
 243                 assert Toolkit.getToolkit().isFxUserThread();
 244                 assert dragSource != null;
 245                 
 246                 //
 247                 // The method is called from FX Scene just before entering
 248                 // nested event loop servicing DnD events.
 249                 // It should initialize DnD in AWT EDT.
 250                 //
 251 
 252                 SwingUtilities.invokeLater(new Runnable() {
 253 
 254                     @Override
 255                     public void run() {
 256                         assert fxDragSource == null;
 257                         assert swingDragSource == null;
 258                         assert dropTarget == null;
 259                         
 260                         fxDragSource = dragSource;
 261 
 262                         startDrag(me, dndTransferable, dragSource.
 263                                 getSupportedActions(), dragAction);
 264                     }
 265                 });
 266             }
 267         };
 268     }
 269 
 270     private static void startDrag(final MouseEvent e, final Transferable t,
 271                                   final Set<TransferMode> sa,
 272                                   final TransferMode dragAction) {
 273 
 274         assert sa.contains(dragAction);
 275 
 276         //
 277         // This is a replacement for the default AWT drag gesture recognizer.
 278         // Not sure DragGestureRecognizer was ever supposed to be used this way.
 279         //
 280 
 281         final class StubDragGestureRecognizer extends DragGestureRecognizer {
 282 
 283             StubDragGestureRecognizer() {
 284                 super(DragSource.getDefaultDragSource(), e.getComponent());
 285                 super.setSourceActions(transferModesToDropActions(sa));
 286                 super.appendEvent(e);
 287             }
 288 
 289             @Override
 290             protected void registerListeners() {
 291             }
 292 
 293             @Override
 294             protected void unregisterListeners() {
 295             }
 296         }
 297 
 298         final Point pt = new Point(e.getX(), e.getY());
 299 
 300         final int action = transferModeToDropAction(dragAction);
 301 
 302         final DragGestureRecognizer dgs = new StubDragGestureRecognizer();
 303 
 304         final List<InputEvent> events = Arrays.asList(new InputEvent[]{dgs.
 305                     getTriggerEvent()});
 306 
 307         final DragGestureEvent dse = new DragGestureEvent(dgs, action, pt,
 308                                                           events);
 309 
 310         dse.startDrag(null, t);
 311     }
 312 
 313     private boolean hasFxScene() {
 314         assert SwingUtilities.isEventDispatchThread();
 315         return getFxScene() != null;
 316     }
 317 
 318     private EmbeddedSceneInterface getFxScene() {
 319         return facade.getScene();
 320     }
 321 
 322     private EmbeddedSceneDragSourceInterface getDragSource() {
 323         assert hasFxScene();
 324 
 325         assert (swingDragSource == null) != (fxDragSource == null);
 326 
 327         if (swingDragSource != null) {
 328             return swingDragSource;
 329         }
 330         return fxDragSource;
 331     }
 332 
 333     private EmbeddedSceneDropTargetInterface getDropTarget() {
 334         assert hasFxScene();
 335 
 336         if (dropTarget == null) {
 337             dropTarget = getFxScene().createDropTarget();
 338         }
 339         return dropTarget;
 340     }
 341     
 342     private void endDnD() {
 343         assert dropTarget != null;
 344         
 345         dropTarget = null;
 346         if (swingDragSource != null) {
 347             swingDragSource = null;
 348         }
 349     }
 350 
 351     private void storeMouseEvent(final MouseEvent me) {
 352         this.me = me;
 353     }
 354 
 355     private static void applyDragResult(final TransferMode dragResult,
 356                                         final DropTargetDragEvent e) {
 357         if (dragResult == null) {
 358             e.rejectDrag();
 359         } else {
 360             e.acceptDrag(transferModeToDropAction(dragResult));
 361         }
 362     }
 363 
 364     private static void applyDropResult(final TransferMode dropResult,
 365                                         final DropTargetDropEvent e) {
 366         if (dropResult == null) {
 367             e.rejectDrop();
 368         } else {
 369             e.acceptDrop(transferModeToDropAction(dropResult));
 370         }
 371     }
 372 
 373     static TransferMode dropActionToTransferMode(final int dropAction) {
 374         switch (dropAction) {
 375             case DnDConstants.ACTION_COPY:
 376                 return TransferMode.COPY;
 377             case DnDConstants.ACTION_MOVE:
 378                 return TransferMode.MOVE;
 379             case DnDConstants.ACTION_LINK:
 380                 return TransferMode.LINK;
 381             case DnDConstants.ACTION_NONE:
 382                 return null;
 383             default:
 384                 throw new IllegalArgumentException();
 385         }
 386     }
 387 
 388     static int transferModeToDropAction(final TransferMode tm) {
 389         switch (tm) {
 390             case COPY:
 391                 return DnDConstants.ACTION_COPY;
 392             case MOVE:
 393                 return DnDConstants.ACTION_MOVE;
 394             case LINK:
 395                 return DnDConstants.ACTION_LINK;
 396             default:
 397                 throw new IllegalArgumentException();
 398         }
 399     }
 400 
 401     static Set<TransferMode> dropActionsToTransferModes(
 402             final int dropActions) {
 403         final Set<TransferMode> tms = EnumSet.noneOf(TransferMode.class);
 404         if ((dropActions & DnDConstants.ACTION_COPY) != 0) {
 405             tms.add(TransferMode.COPY);
 406         }
 407         if ((dropActions & DnDConstants.ACTION_MOVE) != 0) {
 408             tms.add(TransferMode.MOVE);
 409         }
 410         if ((dropActions & DnDConstants.ACTION_LINK) != 0) {
 411             tms.add(TransferMode.LINK);
 412         }
 413         return Collections.unmodifiableSet(tms);
 414     }
 415 
 416     static int transferModesToDropActions(final Set<TransferMode> tms) {
 417         int dropActions = DnDConstants.ACTION_NONE;
 418         for (TransferMode tm : tms) {
 419             dropActions |= transferModeToDropAction(tm);
 420         }
 421         return dropActions;
 422     }
 423 
 424     //
 425     // This is facade to export data from FX to outer world.
 426     //
 427     private final class DnDTransferable implements Transferable {
 428 
 429         @Override
 430         public Object getTransferData(final DataFlavor flavor) throws
 431                 UnsupportedFlavorException, IOException {
 432             checkSwingEventDispatchThread();
 433 
 434             if (!hasFxScene()) {
 435                 return null;
 436             }
 437 
 438             final String mimeType = DataFlavorUtils.getFxMimeType(flavor);
 439 
 440             return DataFlavorUtils.adjustFxData(flavor, getDragSource().getData(
 441                     mimeType));
 442         }
 443 
 444         @Override
 445         public DataFlavor[] getTransferDataFlavors() {
 446             checkSwingEventDispatchThread();
 447 
 448             if (!hasFxScene()) {
 449                 return null;
 450             }
 451 
 452             final String mimeTypes[] = getDragSource().getMimeTypes();
 453 
 454             final ArrayList<DataFlavor> flavors =
 455                     new ArrayList<DataFlavor>(mimeTypes.length);
 456             for (String mime : mimeTypes) {
 457                 DataFlavor flavor = null;
 458                 try {
 459                     flavor = new DataFlavor(mime);
 460                 } catch (ClassNotFoundException e) {
 461                     // FIXME: what to do?
 462                     continue;
 463                 }
 464                 flavors.add(flavor);
 465             }
 466             return flavors.toArray(new DataFlavor[0]);
 467         }
 468 
 469         @Override
 470         public boolean isDataFlavorSupported(final DataFlavor flavor) {
 471             checkSwingEventDispatchThread();
 472 
 473             if (!hasFxScene()) {
 474                 return false;
 475             }
 476 
 477             return getDragSource().isMimeTypeAvailable(DataFlavorUtils.
 478                     getFxMimeType(flavor));
 479         }
 480     };
 481 
 482     private static void checkSwingEventDispatchThread() {
 483         if (!SwingUtilities.isEventDispatchThread()) {
 484             throw new IllegalStateException();
 485         }
 486     }
 487 }