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