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