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