--- old/modules/swing/src/main/java/javafx/embed/swing/DataFlavorUtils.java 2014-07-18 18:28:53.282460700 +0400 +++ new/modules/swing/src/main/java/javafx/embed/swing/DataFlavorUtils.java 2014-07-18 18:28:52.202399000 +0400 @@ -34,6 +34,7 @@ import java.util.HashMap; import java.util.HashSet; import java.util.Collections; +import java.util.ArrayList; import java.awt.datatransfer.DataFlavor; import java.awt.datatransfer.Transferable; @@ -54,6 +55,44 @@ return flavor.getPrimaryType() + "/" + flavor.getSubType(); } + static DataFlavor[] getDataFlavors(String[] mimeTypes) { + final ArrayList flavors = + new ArrayList(mimeTypes.length); + for (String mime : mimeTypes) { + DataFlavor flavor = null; + try { + flavor = new DataFlavor(mime); + } catch (ClassNotFoundException e) { + // FIXME: what to do? + continue; + } + flavors.add(flavor); + } + return flavors.toArray(new DataFlavor[0]); + } + + static DataFlavor getDataFlavor(final DataFormat format) { + DataFlavor[] flavors = getDataFlavors(format.getIdentifiers().toArray(new String[1])); + + // Well, that's our best guess... + return flavors.length == 0 ? null : flavors[0]; + } + + static String getMimeType(final DataFormat format) { + // Well, that's our best guess... + for (String id : format.getIdentifiers()) return id; + return null; + } + + static DataFormat getDataFormat(final DataFlavor flavor) { + String mimeType = getFxMimeType(flavor); + DataFormat dataFormat = DataFormat.lookupMimeType(mimeType); + if (dataFormat == null) { + dataFormat = new DataFormat(mimeType); // are we ready for this yet? + } + return dataFormat; + } + /** * InputStream implementation backed by a ByteBuffer. * It can handle byte buffers that are backed by arrays --- old/modules/swing/src/main/java/javafx/embed/swing/SwingDnD.java 2014-07-18 18:28:59.433812600 +0400 +++ new/modules/swing/src/main/java/javafx/embed/swing/SwingDnD.java 2014-07-18 18:28:58.347750500 +0400 @@ -70,6 +70,7 @@ /** * An utility class to connect DnD mechanism of Swing and FX. + * It allows FX content to use the AWT machinery for performing DnD. */ final class SwingDnD { --- old/modules/swing/src/main/java/javafx/embed/swing/SwingDragSource.java 2014-07-18 18:29:05.470157800 +0400 +++ new/modules/swing/src/main/java/javafx/embed/swing/SwingDragSource.java 2014-07-18 18:29:04.362094500 +0400 @@ -40,10 +40,9 @@ /** * Drag source to deliver data from Swing environment to embedded FX scene. */ -final class SwingDragSource implements EmbeddedSceneDSInterface { +final class SwingDragSource extends CachingTransferable implements EmbeddedSceneDSInterface { private int sourceActions; - private Map mimeType2Data = Collections.EMPTY_MAP; SwingDragSource() { } @@ -58,42 +57,6 @@ updateData(e.getTransferable(), fetchData); } - private void updateData(Transferable t, boolean fetchData) { - final Map mimeType2DataFlavor = - DataFlavorUtils.adjustSwingDataFlavors( - t.getTransferDataFlavors()); - - // If we keep reference to source Transferable in SwingDragSource and - // call Transferable#getTransferData() on it from - // SwingDragSource#getData() we may run into - // "java.awt.dnd.InvalidDnDOperationException" issue as - // SwingDragSource#getData() is called from FX user code and from - // QuantumClipboard#getContent() (sik!). These calls usually take - // place in the context of - // EmbeddedSceneDTInterface#handleDragDrop() method as the - // normal handling of DnD. - // Instead of keeping reference to source Transferable we just read - // all its data while in the context safe for calling - // Transferable#getTransferData(). - // - // This observation is true for standard AWT Transferable-s. - // Things may be totally broken for custom Transferable-s though. - - // For performance reasons, the DRAG_ENTERED and DRAG_OVER event - // handlers pass fetchData == false so as to update the set of - // available MIME types only. The DRAG_DROPPED handler passes - // fetchData == true which also fetches all the data. - // NOTE: Due to JDK-8028585 this code won't be able to fetch data - // when invoked from handlers other than DROPPED in any case. - - try { - mimeType2Data = DataFlavorUtils.readAllData(t, mimeType2DataFlavor, - fetchData); - } catch (Exception e) { - mimeType2Data = Collections.EMPTY_MAP; - } - } - @Override public Set getSupportedActions() { assert Toolkit.getToolkit().isFxUserThread(); @@ -101,24 +64,6 @@ } @Override - public Object getData(final String mimeType) { - assert Toolkit.getToolkit().isFxUserThread(); - return mimeType2Data.get(mimeType); - } - - @Override - public String[] getMimeTypes() { - assert Toolkit.getToolkit().isFxUserThread(); - return mimeType2Data.keySet().toArray(new String[0]); - } - - @Override - public boolean isMimeTypeAvailable(final String mimeType) { - assert Toolkit.getToolkit().isFxUserThread(); - return Arrays.asList(getMimeTypes()).contains(mimeType); - } - - @Override public void dragDropEnd(TransferMode performedAction) { throw new UnsupportedOperationException(); } --- old/modules/swing/src/main/java/javafx/embed/swing/SwingFXUtils.java 2014-07-18 18:29:11.434499000 +0400 +++ new/modules/swing/src/main/java/javafx/embed/swing/SwingFXUtils.java 2014-07-18 18:29:10.354437200 +0400 @@ -35,6 +35,8 @@ import java.nio.IntBuffer; import java.security.AccessController; import java.security.PrivilegedAction; +import java.util.Set; +import java.util.HashSet; import java.util.concurrent.atomic.AtomicBoolean; import javafx.application.Platform; import javafx.scene.image.Image; @@ -224,6 +226,37 @@ } } + private static final Set eventLoopKeys = new HashSet<>(); + + /** + * The runnable is responsible for leaving the nested event loop. + */ + static void runOnEDTAndWait(Object nestedLoopKey, Runnable r) { + Toolkit.getToolkit().checkFxUserThread(); + + if (SwingUtilities.isEventDispatchThread()) { + r.run(); + } else { + eventLoopKeys.add(nestedLoopKey); + SwingUtilities.invokeLater(r); + Toolkit.getToolkit().enterNestedEventLoop(nestedLoopKey); + } + } + + static void leaveFXNestedLoop(Object nestedLoopKey) { + if (!eventLoopKeys.contains(nestedLoopKey)) return; + + if (Platform.isFxApplicationThread()) { + Toolkit.getToolkit().exitNestedEventLoop(nestedLoopKey, null); + } else { + Platform.runLater(() -> { + Toolkit.getToolkit().exitNestedEventLoop(nestedLoopKey, null); + }); + } + + eventLoopKeys.remove(nestedLoopKey); + } + private static class FwSecondaryLoop implements SecondaryLoop { private final AtomicBoolean isRunning = new AtomicBoolean(false); --- old/modules/swing/src/main/java/javafx/embed/swing/SwingNode.java 2014-07-18 18:29:17.571850000 +0400 +++ new/modules/swing/src/main/java/javafx/embed/swing/SwingNode.java 2014-07-18 18:29:16.450785900 +0400 @@ -47,6 +47,13 @@ import java.awt.Cursor; import java.awt.EventQueue; import java.awt.Toolkit; +import java.awt.dnd.DragGestureEvent; +import java.awt.dnd.DragGestureListener; +import java.awt.dnd.DragGestureRecognizer; +import java.awt.dnd.DragSource; +import java.awt.dnd.DropTarget; +import java.awt.dnd.InvalidDnDOperationException; +import java.awt.dnd.peer.DragSourceContextPeer; import java.awt.event.InputEvent; import java.awt.event.MouseWheelEvent; import java.awt.event.WindowEvent; @@ -132,6 +139,7 @@ private volatile JComponent content; private volatile JLightweightFrame lwFrame; + final JLightweightFrame getLightweightFrame() { return lwFrame; } private volatile NGExternalNode peer; @@ -636,6 +644,8 @@ private class SwingNodeContent implements LightweightContent { private JComponent comp; + private volatile FXDnD dnd; + public SwingNodeContent(JComponent comp) { this.comp = comp; } @@ -654,9 +664,11 @@ // Note: we skip @Override annotation and implement both pre-hiDPI and post-hiDPI versions // of the method for compatibility. + //@Override public void imageBufferReset(int[] data, int x, int y, int width, int height, int linestride) { imageBufferReset(data, x, y, width, height, linestride, 1); } + //@Override public void imageBufferReset(int[] data, int x, int y, int width, int height, int linestride, int scale) { SwingNode.this.setImageBuffer(data, x, y, width, height, linestride, scale); } @@ -714,11 +726,50 @@ }); } + //@Override public void setCursor(Cursor cursor) { SwingFXUtils.runOnFxThread(() -> { SwingNode.this.setCursor(SwingCursors.embedCursorToCursor(cursor)); }); } + + private void initDnD() { + // This is a part of AWT API, so the method may be invoked on any thread + synchronized (SwingNodeContent.this) { + if (this.dnd == null) { + this.dnd = new FXDnD(SwingNode.this); + } + } + } + + //@Override + public synchronized T createDragGestureRecognizer( + Class abstractRecognizerClass, + DragSource ds, Component c, int srcActions, + DragGestureListener dgl) + { + initDnD(); + return dnd.createDragGestureRecognizer(abstractRecognizerClass, ds, c, srcActions, dgl); + } + + //@Override + public DragSourceContextPeer createDragSourceContextPeer(DragGestureEvent dge) throws InvalidDnDOperationException + { + initDnD(); + return dnd.createDragSourceContextPeer(dge); + } + + //@Override + public void addDropTarget(DropTarget dt) { + initDnD(); + dnd.addDropTarget(dt); + } + + //@Override + public void removeDropTarget(DropTarget dt) { + initDnD(); + dnd.removeDropTarget(dt); + } } private void ungrabFocus(boolean postUngrabEvent) { --- /dev/null 2014-07-18 18:29:23.000000000 +0400 +++ new/modules/swing/src/main/java/javafx/embed/swing/CachingTransferable.java 2014-07-18 18:29:22.504132100 +0400 @@ -0,0 +1,128 @@ +/* + * Copyright (c) 2014, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package javafx.embed.swing; + +import com.sun.javafx.embed.EmbeddedSceneDSInterface; +import com.sun.javafx.tk.Toolkit; +import java.awt.datatransfer.DataFlavor; +import java.awt.datatransfer.Transferable; +import java.awt.dnd.DropTargetDragEvent; +import java.awt.dnd.DropTargetDropEvent; +import java.util.Arrays; +import java.util.Collections; +import java.util.Map; +import java.util.HashMap; +import java.util.Set; +import javafx.scene.input.TransferMode; +import java.io.UnsupportedEncodingException; +import javafx.scene.input.Clipboard; +import javafx.scene.input.DataFormat; + +/** + * A Transferable implementation backed by a Map. + * The data can be populated either from AWT Transferable + * or from FX Clipboard. + */ +class CachingTransferable implements Transferable { + + @Override + public Object getTransferData(final DataFlavor flavor) throws UnsupportedEncodingException + { + String mimeType = DataFlavorUtils.getFxMimeType(flavor); + return DataFlavorUtils.adjustFxData( + flavor, getData(mimeType)); + } + + @Override + public DataFlavor[] getTransferDataFlavors() { + final String mimeTypes[] = getMimeTypes(); + return DataFlavorUtils.getDataFlavors(mimeTypes); + } + + @Override + public boolean isDataFlavorSupported(final DataFlavor flavor) { + return isMimeTypeAvailable( + DataFlavorUtils.getFxMimeType(flavor)); + } + + private Map mimeType2Data = Collections.EMPTY_MAP; + + void updateData(Transferable t, boolean fetchData) { + final Map mimeType2DataFlavor = + DataFlavorUtils.adjustSwingDataFlavors( + t.getTransferDataFlavors()); + + // If we keep reference to source Transferable in SwingDragSource and + // call Transferable#getTransferData() on it from + // SwingDragSource#getData() we may run into + // "java.awt.dnd.InvalidDnDOperationException" issue as + // SwingDragSource#getData() is called from FX user code and from + // QuantumClipboard#getContent() (sik!). These calls usually take + // place in the context of + // EmbeddedSceneDTInterface#handleDragDrop() method as the + // normal handling of DnD. + // Instead of keeping reference to source Transferable we just read + // all its data while in the context safe for calling + // Transferable#getTransferData(). + // + // This observation is true for standard AWT Transferable-s. + // Things may be totally broken for custom Transferable-s though. + + // For performance reasons, the DRAG_ENTERED and DRAG_OVER event + // handlers pass fetchData == false so as to update the set of + // available MIME types only. The DRAG_DROPPED handler passes + // fetchData == true which also fetches all the data. + // NOTE: Due to JDK-8028585 this code won't be able to fetch data + // when invoked from handlers other than DROPPED in any case. + + try { + mimeType2Data = DataFlavorUtils.readAllData(t, mimeType2DataFlavor, + fetchData); + } catch (Exception e) { + mimeType2Data = Collections.EMPTY_MAP; + } + } + + void updateData(Clipboard cb, boolean fetchData) { + mimeType2Data = new HashMap<>(); + for (DataFormat f : cb.getContentTypes()) { + mimeType2Data.put(DataFlavorUtils.getMimeType(f), + fetchData ? cb.getContent(f) : null); + } + } + + public Object getData(final String mimeType) { + return mimeType2Data.get(mimeType); + } + + public String[] getMimeTypes() { + return mimeType2Data.keySet().toArray(new String[0]); + } + + public boolean isMimeTypeAvailable(final String mimeType) { + return Arrays.asList(getMimeTypes()).contains(mimeType); + } +} --- /dev/null 2014-07-18 18:29:28.000000000 +0400 +++ new/modules/swing/src/main/java/javafx/embed/swing/FXDnD.java 2014-07-18 18:29:27.132396800 +0400 @@ -0,0 +1,560 @@ +/* + * Copyright (c) 2014, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package javafx.embed.swing; + +import javafx.event.EventHandler; +import javafx.event.EventType; +import javafx.scene.input.MouseEvent; +import javafx.scene.input.ClipboardContent; +import javafx.scene.input.Dragboard; +import javafx.scene.input.DragEvent; +import javafx.scene.input.TransferMode; +import javafx.application.Platform; +import javafx.scene.input.DataFormat; + +import com.sun.javafx.tk.Toolkit; + +import java.awt.Component; +import java.awt.Cursor; +import java.awt.EventQueue; +import java.awt.Image; +import java.awt.Point; +import java.awt.SecondaryLoop; +import java.awt.datatransfer.DataFlavor; +import java.awt.datatransfer.Transferable; +import java.awt.datatransfer.FlavorTable; +import java.awt.datatransfer.SystemFlavorMap; +import java.awt.dnd.DnDConstants; +import java.awt.dnd.DragSourceEvent; +import java.awt.dnd.DragSourceDropEvent; +import java.awt.dnd.DragSourceDragEvent; +import java.awt.dnd.DragGestureEvent; +import java.awt.dnd.DragGestureListener; +import java.awt.dnd.DragGestureRecognizer; +import java.awt.dnd.DragSource; +import java.awt.dnd.DragSourceContext; +import java.awt.dnd.DropTarget; +import java.awt.dnd.DropTargetContext; +import java.awt.dnd.DropTargetEvent; +import java.awt.dnd.DropTargetDragEvent; +import java.awt.dnd.DropTargetDropEvent; +import java.awt.dnd.DropTargetListener; +import java.awt.dnd.MouseDragGestureRecognizer; +import java.awt.dnd.InvalidDnDOperationException; +import java.awt.dnd.peer.DragSourceContextPeer; +import java.awt.dnd.peer.DropTargetContextPeer; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; + +import java.util.Collections; +import java.util.ArrayList; +import java.util.Map; +import java.util.SortedMap; +import java.util.TreeMap; +import java.util.TreeSet; +import java.util.HashMap; +import java.util.concurrent.atomic.AtomicBoolean; + +import sun.awt.AWTAccessor; +import sun.awt.SunToolkit; +import sun.awt.dnd.SunDragSourceContextPeer; +import sun.awt.dnd.SunDropTargetEvent; +import sun.awt.datatransfer.DataTransferer; +import sun.awt.datatransfer.ToolkitThreadBlockedHandler; + +import sun.swing.JLightweightFrame; + + +/** + * A utility class to connect DnD mechanism of Swing and FX. + * It allows Swing content to use the FX machinery for performing DnD. + */ +final class FXDnD { + private final SwingNode node; + private SwingNode getNode() { return node; } + + FXDnD(SwingNode node) { + this.node = node; + } + + /** + * Utility class that operates on Maps with Components as keys. + * Useful when processing mouse events to choose an object from the map + * based on the component located at the given coordinates. + */ + private class ComponentMapper { + private int x, y; + private T object = null; + + private ComponentMapper(Map map, int xArg, int yArg) { + this.x = xArg; + this.y = yArg; + + final JLightweightFrame lwFrame = node.getLightweightFrame(); + Component c = AWTAccessor.getContainerAccessor().findComponentAt( + lwFrame, x, y, false); + if (c == null) return; + + synchronized (c.getTreeLock()) { + do { + object = map.get(c); + } while (object == null && (c = c.getParent()) != null); + + if (object != null) { + // The object is either a DropTarget or a DragSource, so: + //assert c == object.getComponent(); + + // Translate x, y from lwFrame to component coordinates + while (c != lwFrame && c != null) { + x -= c.getX(); + y -= c.getY(); + c = c.getParent(); + } + } + } + } + } + public ComponentMapper mapComponent(Map map, int x, int y) { + return new ComponentMapper(map, x, y); + } + + + + + + /////////////////////////////////////////////////////////////////////////// + // DRAG SOURCE IMPLEMENTATION + /////////////////////////////////////////////////////////////////////////// + + + private boolean isDragSourceListenerInstalled = false; + + // To keep track of where the DnD gesture actually started + private MouseEvent pressEvent = null; + private long pressTime = 0; + + private volatile SecondaryLoop loop; + + private final Map recognizers = new HashMap<>(); + + // Note that we don't really use the MouseDragGestureRecognizer facilities, + // however some code in JDK may expect a descendant of this class rather + // than a generic DragGestureRecognizer. So we inherit from it. + private class FXDragGestureRecognizer extends MouseDragGestureRecognizer { + FXDragGestureRecognizer(DragSource ds, Component c, int srcActions, + DragGestureListener dgl) + { + super(ds, c, srcActions, dgl); + + if (c != null) recognizers.put(c, this); + } + + @Override public void setComponent(Component c) { + final Component old = getComponent(); + if (old != null) recognizers.remove(old); + super.setComponent(c); + if (c != null) recognizers.put(c, this); + } + + protected void registerListeners() { + SwingFXUtils.runOnFxThread(() -> { + if (!isDragSourceListenerInstalled) { + node.addEventHandler(MouseEvent.MOUSE_PRESSED, onMousePressHandler); + node.addEventHandler(MouseEvent.DRAG_DETECTED, onDragStartHandler); + node.addEventHandler(DragEvent.DRAG_DONE, onDragDoneHandler); + + isDragSourceListenerInstalled = true; + } + }); + } + + protected void unregisterListeners() { + SwingFXUtils.runOnFxThread(() -> { + if (isDragSourceListenerInstalled) { + node.removeEventHandler(MouseEvent.MOUSE_PRESSED, onMousePressHandler); + node.removeEventHandler(MouseEvent.DRAG_DETECTED, onDragStartHandler); + node.removeEventHandler(DragEvent.DRAG_DONE, onDragDoneHandler); + + isDragSourceListenerInstalled = false; + } + }); + } + + private void fireEvent(int x, int y, long evTime, int modifiers) { + // In theory we should register all the events that trigger the gesture (like PRESS, DRAG, DRAG, BINGO!) + // But we can live with this hack for now. + appendEvent(new java.awt.event.MouseEvent(getComponent(), java.awt.event.MouseEvent.MOUSE_PRESSED, + evTime, modifiers, x, y, 0, false)); + + // Also, the modifiers here should've actually come from the last known mouse event (last MOVE or DRAG). + // But we're OK with using the initial PRESS modifiers for now + int initialAction = SunDragSourceContextPeer.convertModifiersToDropAction( + modifiers, getSourceActions()); + + fireDragGestureRecognized(initialAction, new java.awt.Point(x, y)); + } + } + + // Invoked on EDT + private void fireEvent(int x, int y, long evTime, int modifiers) { + ComponentMapper mapper = mapComponent(recognizers, x, y); + + final FXDragGestureRecognizer r = mapper.object; + if (r != null) { + r.fireEvent(mapper.x, mapper.y, evTime, modifiers); + } else { + // No recognizer, no DnD, no startDrag, so release the FX loop now + SwingFXUtils.leaveFXNestedLoop(FXDnD.this); + } + } + + private MouseEvent getInitialGestureEvent() { + return pressEvent; + } + + private final EventHandler onMousePressHandler = (event) -> { + // It would be nice to maintain a list of all the events that initiate + // a DnD gesture (see a comment in FXDragGestureRecognizer.fireEvent(). + // For now, we simply use the initial PRESS event for this purpose. + pressEvent = event; + pressTime = System.currentTimeMillis(); + }; + + + private volatile FXDragSourceContextPeer activeDSContextPeer; + + private final EventHandler onDragStartHandler = (event) -> { + // Call to AWT and determine the active DragSourceContextPeer + activeDSContextPeer = null; + final MouseEvent firstEv = getInitialGestureEvent(); + SwingFXUtils.runOnEDTAndWait(FXDnD.this, () -> fireEvent( + (int)firstEv.getX(), (int)firstEv.getY(), pressTime, + SwingEvents.fxMouseModsToMouseMods(firstEv))); + if (activeDSContextPeer == null) return; + + // Since we're going to start DnD, consume the event. + event.consume(); + + Dragboard db = getNode().startDragAndDrop(SwingDnD.dropActionsToTransferModes( + activeDSContextPeer.sourceActions).toArray(new TransferMode[1])); + + // At this point the activeDSContextPeer.transferable contains all the data from AWT + Map fxData = new HashMap<>(); + for (String mt : activeDSContextPeer.transferable.getMimeTypes()) { + DataFormat f = DataFormat.lookupMimeType(mt); + //TODO: what to do if f == null? + if (f != null) fxData.put(f, activeDSContextPeer.transferable.getData(mt)); + } + + final boolean hasContent = db.setContent(fxData); + if (!hasContent) { + // No data, no DnD, no onDragDoneHandler, so release the AWT loop now + loop.exit(); + } + }; + + private final EventHandler onDragDoneHandler = (event) -> { + event.consume(); + + // Release FXDragSourceContextPeer.startDrag() + loop.exit(); + + if (activeDSContextPeer != null) { + final TransferMode mode = event.getTransferMode(); + activeDSContextPeer.dragDone( + mode == null ? 0 : SwingDnD.transferModeToDropAction(mode), + (int)event.getX(), (int)event.getY()); + } + }; + + + private final class FXDragSourceContextPeer extends SunDragSourceContextPeer { + private volatile int sourceActions = 0; + + private final CachingTransferable transferable = new CachingTransferable(); + + @Override public void startSecondaryEventLoop(){ + Toolkit.getToolkit().enterNestedEventLoop(this); + } + @Override public void quitSecondaryEventLoop(){ + assert !Platform.isFxApplicationThread(); + Platform.runLater(() -> Toolkit.getToolkit().exitNestedEventLoop(FXDragSourceContextPeer.this, null)); + } + + @Override protected void setNativeCursor(long nativeCtxt, Cursor c, int cType) { + //TODO + } + + + private void dragDone(int operation, int x, int y) { + dragDropFinished(operation != 0, operation, x, y); + } + + FXDragSourceContextPeer(DragGestureEvent dge) { + super(dge); + } + + + // It's Map actually, but javac complains if the type isn't erased... + @Override protected void startDrag(Transferable trans, long[] formats, Map formatMap) + { + activeDSContextPeer = this; + + // NOTE: we ignore the formats[] and the formatMap altogether. + // AWT provides those to allow for more flexible representations of + // e.g. text data (in various formats, encodings, etc.) However, FX + // code isn't ready to handle those (e.g. it can't digest a + // StringReader as data, etc.) So instead we perform our internal + // translation. + // Note that fetchData == true. FX doesn't support delayed data + // callbacks yet anyway, so we have to fetch all the data from AWT upfront. + transferable.updateData(trans, true); + + sourceActions = getDragSourceContext().getSourceActions(); + + // Release the FX nested loop to allow onDragDetected to start the actual DnD operation, + // and then start an AWT nested loop to wait until DnD finishes. + loop = java.awt.Toolkit.getDefaultToolkit().getSystemEventQueue().createSecondaryLoop(); + SwingFXUtils.leaveFXNestedLoop(FXDnD.this); + if (!loop.enter()) { + // An error occured, but there's little we can do here... + } + } + }; + + public T createDragGestureRecognizer( + Class abstractRecognizerClass, + DragSource ds, Component c, int srcActions, + DragGestureListener dgl) + { + return (T) new FXDragGestureRecognizer(ds, c, srcActions, dgl); + } + + public DragSourceContextPeer createDragSourceContextPeer(DragGestureEvent dge) throws InvalidDnDOperationException + { + return new FXDragSourceContextPeer(dge); + } + + + + + + /////////////////////////////////////////////////////////////////////////// + // DROP TARGET IMPLEMENTATION + /////////////////////////////////////////////////////////////////////////// + + + private boolean isDropTargetListenerInstalled = false; + private volatile FXDropTargetContextPeer activeDTContextPeer = null; + private final Map dropTargets = new HashMap<>(); + + private final EventHandler onDragEnteredHandler = (event) -> { + if (activeDTContextPeer == null) activeDTContextPeer = new FXDropTargetContextPeer(); + + int action = activeDTContextPeer.postDropTargetEvent(event); + + // If AWT doesn't accept anything, let parent nodes handle the event + if (action != 0) event.consume(); + }; + + private final EventHandler onDragExitedHandler = (event) -> { + if (activeDTContextPeer == null) activeDTContextPeer = new FXDropTargetContextPeer(); + + activeDTContextPeer.postDropTargetEvent(event); + + activeDTContextPeer = null; + }; + + private final EventHandler onDragOverHandler = (event) -> { + if (activeDTContextPeer == null) activeDTContextPeer = new FXDropTargetContextPeer(); + + int action = activeDTContextPeer.postDropTargetEvent(event); + + // If AWT doesn't accept anything, let parent nodes handle the event + if (action != 0) { + // NOTE: in FX the acceptTransferModes() may ONLY be called from DRAG_OVER. + // If the AWT app always reports NONE and suddenly decides to accept the + // data in its DRAG_DROPPED handler, this just won't work. There's no way + // to workaround this other than by modifing the AWT application code. + event.acceptTransferModes(SwingDnD.dropActionsToTransferModes(action).toArray(new TransferMode[1])); + event.consume(); + } + }; + + private final EventHandler onDragDroppedHandler = (event) -> { + if (activeDTContextPeer == null) activeDTContextPeer = new FXDropTargetContextPeer(); + + int action = activeDTContextPeer.postDropTargetEvent(event); + + if (action != 0) { + // NOTE: the dropAction is ignored since we use the action last + // reported from the DRAG_OVER handler. + // + // We might want to: + // + // assert activeDTContextPeer.dropAction == onDragDroppedHandler.currentAction; + // + // and maybe print a diagnostic message if they differ. + event.setDropCompleted(activeDTContextPeer.success); + + event.consume(); + } + + activeDTContextPeer = null; + }; + + private final class FXDropTargetContextPeer implements DropTargetContextPeer { + + private int targetActions = DnDConstants.ACTION_NONE; + private int currentAction = DnDConstants.ACTION_NONE; + private DropTarget dt = null; + private DropTargetContext ctx = null; + + private final CachingTransferable transferable = new CachingTransferable(); + + // Drop result + private boolean success = false; + private int dropAction = 0; + + @Override public synchronized void setTargetActions(int actions) { targetActions = actions; } + @Override public synchronized int getTargetActions() { return targetActions; } + + @Override public synchronized DropTarget getDropTarget() { return dt; } + + @Override public synchronized boolean isTransferableJVMLocal() { return false; } + + @Override public synchronized DataFlavor[] getTransferDataFlavors() { return transferable.getTransferDataFlavors(); } + @Override public synchronized Transferable getTransferable() { return transferable; } + + @Override public synchronized void acceptDrag(int dragAction) { currentAction = dragAction; } + @Override public synchronized void rejectDrag() { currentAction = DnDConstants.ACTION_NONE; } + + @Override public synchronized void acceptDrop(int dropAction) { this.dropAction = dropAction; } + @Override public synchronized void rejectDrop() { dropAction = DnDConstants.ACTION_NONE; } + + @Override public synchronized void dropComplete(boolean success) { this.success = success; } + + + private int postDropTargetEvent(DragEvent event) + { + ComponentMapper mapper = mapComponent(dropTargets, (int)event.getX(), (int)event.getY()); + + final EventType fxEvType = event.getEventType(); + + Dragboard db = event.getDragboard(); + transferable.updateData(db, DragEvent.DRAG_DROPPED.equals(fxEvType)); + + final int sourceActions = SwingDnD.transferModesToDropActions(db.getTransferModes()); + final int userAction = event.getTransferMode() == null ? DnDConstants.ACTION_NONE + : SwingDnD.transferModeToDropAction(event.getTransferMode()); + + // A target for the AWT DnD event + DropTarget target = mapper.object != null ? mapper.object : dt; + + SwingFXUtils.runOnEDTAndWait(FXDnD.this, () -> { + if (target != dt) { + if (ctx != null) ctx.removeNotify(); + ctx = null; + + currentAction = dropAction = DnDConstants.ACTION_NONE; + } + + if (target != null) { + if (ctx == null) { + ctx = target.getDropTargetContext(); + ctx.addNotify(FXDropTargetContextPeer.this); + } + + DropTargetListener dtl = (DropTargetListener)target; + + if (DragEvent.DRAG_DROPPED.equals(fxEvType)) { + DropTargetDropEvent awtEvent = new DropTargetDropEvent( + ctx, new Point(mapper.x, mapper.y), userAction, sourceActions); + + dtl.drop(awtEvent); + } else { + DropTargetDragEvent awtEvent = new DropTargetDragEvent( + ctx, new Point(mapper.x, mapper.y), userAction, sourceActions); + + if (DragEvent.DRAG_OVER.equals(fxEvType)) dtl.dragOver(awtEvent); + else if (DragEvent.DRAG_ENTERED.equals(fxEvType)) dtl.dragEnter(awtEvent); + else if (DragEvent.DRAG_EXITED.equals(fxEvType)) dtl.dragExit(awtEvent); + } + } + + dt = mapper.object; + if (dt == null) { + if (ctx != null) ctx.removeNotify(); + ctx = null; + + currentAction = dropAction = DnDConstants.ACTION_NONE; + } + if (DragEvent.DRAG_DROPPED.equals(fxEvType) || DragEvent.DRAG_EXITED.equals(fxEvType)) { + // This must be done to ensure that the data isn't being + // cached in AWT. Otherwise subsequent DnD operations will + // see the old data only. + if (ctx != null) ctx.removeNotify(); + ctx = null; + } + + SwingFXUtils.leaveFXNestedLoop(FXDnD.this); + }); + + if (DragEvent.DRAG_DROPPED.equals(fxEvType)) return dropAction; + + return currentAction; + } + } + + public void addDropTarget(DropTarget dt) { + dropTargets.put(dt.getComponent(), dt); + Platform.runLater(() -> { + if (!isDropTargetListenerInstalled) { + node.addEventHandler(DragEvent.DRAG_ENTERED, onDragEnteredHandler); + node.addEventHandler(DragEvent.DRAG_EXITED, onDragExitedHandler); + node.addEventHandler(DragEvent.DRAG_OVER, onDragOverHandler); + node.addEventHandler(DragEvent.DRAG_DROPPED, onDragDroppedHandler); + + isDropTargetListenerInstalled = true; + } + }); + } + + public void removeDropTarget(DropTarget dt) { + dropTargets.remove(dt.getComponent()); + Platform.runLater(() -> { + if (isDropTargetListenerInstalled && dropTargets.isEmpty()) { + node.removeEventHandler(DragEvent.DRAG_ENTERED, onDragEnteredHandler); + node.removeEventHandler(DragEvent.DRAG_EXITED, onDragExitedHandler); + node.removeEventHandler(DragEvent.DRAG_OVER, onDragOverHandler); + node.removeEventHandler(DragEvent.DRAG_DROPPED, onDragDroppedHandler); + + isDropTargetListenerInstalled = true; + } + }); + } +}