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 }