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