/*
* Copyright (c) 2014, 2017, Oracle and/or its affiliates. All rights reserved.
* ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*/
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.security.
AccessController;
import java.security.
PrivilegedAction;
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 static boolean
fxAppThreadIsDispatchThread;
private
SwingNode getNode() { return
node; }
static {
AccessController.
doPrivileged(new
PrivilegedAction<
Object>() {
public
Object run() {
fxAppThreadIsDispatchThread =
"true".
equals(
System.
getProperty("javafx.embed.singleThread"));
return null;
}
});
}
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<T> {
private int
x,
y;
private T
object = null;
private
ComponentMapper(
Map<
Component, T>
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 <T>
ComponentMapper<T>
mapComponent(
Map<
Component, T>
map, int
x, int
y) {
return new
ComponentMapper<T>(
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<
Component,
FXDragGestureRecognizer>
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<
FXDragGestureRecognizer>
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<
MouseEvent>
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<
MouseEvent>
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<
DataFormat,
Object>
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
if (!
fxAppThreadIsDispatchThread) {
loop.
exit();
}
}
};
private final
EventHandler<
DragEvent>
onDragDoneHandler = (
event) -> {
event.
consume();
// Release FXDragSourceContextPeer.startDrag()
if (!
fxAppThreadIsDispatchThread) {
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<Long, DataFlavor> 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.
if (!
fxAppThreadIsDispatchThread) {
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 extends
DragGestureRecognizer> T
createDragGestureRecognizer(
Class<T>
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<
Component,
DropTarget>
dropTargets = new
HashMap<>();
private final
EventHandler<
DragEvent>
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<
DragEvent>
onDragExitedHandler = (
event) -> {
if (
activeDTContextPeer == null)
activeDTContextPeer = new
FXDropTargetContextPeer();
activeDTContextPeer.
postDropTargetEvent(
event);
activeDTContextPeer = null;
};
private final
EventHandler<
DragEvent>
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<
DragEvent>
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<
DropTarget>
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;
}
});
}
}