JBR-8833: Refactor Wayland data device abstraction [WLToolkit]

This commit is contained in:
Nikita Tsarev
2025-05-23 12:37:25 +02:00
parent a8b4f08808
commit 8b4249aa00
8 changed files with 1523 additions and 1043 deletions

View File

@@ -24,101 +24,74 @@
*/
package sun.awt.wl;
import jdk.internal.misc.InnocuousThread;
import sun.awt.datatransfer.DataTransferer;
import sun.awt.datatransfer.SunClipboard;
import sun.util.logging.PlatformLogger;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.FlavorTable;
import java.awt.datatransfer.Transferable;
import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.SortedMap;
public final class WLClipboard extends SunClipboard {
private static final PlatformLogger log = PlatformLogger.getLogger("sun.awt.wl.WLClipboard");
public static final int INITIAL_MIME_FORMATS_COUNT = 10;
private static final int DEFAULT_BUFFER_SIZE = 4096;
private static final int MAX_BUFFER_SIZE = Integer.MAX_VALUE - 8;
// A native handle of a Wayland queue dedicated to handling
// data offer-related events
private static final long dataOfferQueuePtr;
private final long ID;
private final WLDataDevice dataDevice;
// true if this is the "primary selection" clipboard,
// false otherwise (the regular clipboard).
private final boolean isPrimary; // used by native
private final boolean isPrimary;
private final Object dataLock = new Object();
// A handle to the native clipboard representation, 0 if not available.
private long clipboardNativePtr; // guarded by dataLock
private long ourOfferNativePtr; // guarded by dataLock
// The list of numeric format IDs the current clipboard is available in;
// could be null or empty.
private List<Long> clipboardFormats; // guarded by dataLock
// Latest active data offer to us, containing up-to-date clipboard content,
// as provided by the Wayland compositor.
// If null, there's no clipboard data available.
// Guarded by dataLock.
private WLDataOffer clipboardDataOfferedToUs;
// The "current" list formats for the new clipboard contents that is about
// to be received from Wayland. Could be empty, but never null.
private List<Long> newClipboardFormats = new ArrayList<>(INITIAL_MIME_FORMATS_COUNT); // guarded by dataLock
// Clipboard data source we are providing to the Wayland compositor.
// If null, we are not offering any clipboard data at the moment.
// Guarded by dataLock.
private WLDataSource ourDataSource;
private static final Thread clipboardDispatcherThread;
static {
initIDs();
dataOfferQueuePtr = createDataOfferQueue();
flavorTable = DataTransferer.adaptFlavorMap(getDefaultFlavorTable());
Thread t = InnocuousThread.newThread(
"AWT-Wayland-clipboard-dispatcher",
WLClipboard::dispatchDataOfferQueue);
t.setDaemon(true);
t.start();
clipboardDispatcherThread = t;
}
private final static FlavorTable flavorTable;
public WLClipboard(String name, boolean isPrimary) {
public WLClipboard(WLDataDevice dataDevice, String name, boolean isPrimary) {
super(name);
this.ID = initNative(isPrimary);
this.isPrimary = isPrimary;
this.dataDevice = dataDevice;
if (log.isLoggable(PlatformLogger.Level.FINE)) {
log.fine("Clipboard: Created " + this);
}
}
private static void dispatchDataOfferQueue() {
dispatchDataOfferQueueImpl(dataOfferQueuePtr); // does not return until error or server disconnect
if (log.isLoggable(PlatformLogger.Level.FINE)) {
log.fine("Clipboard: data offer dispatcher exited");
private int getProtocol() {
if (isPrimary) {
return WLDataDevice.DATA_TRANSFER_PROTOCOL_PRIMARY_SELECTION;
} else {
return WLDataDevice.DATA_TRANSFER_PROTOCOL_WAYLAND;
}
}
@Override
public String toString() {
return String.format("Clipboard: Wayland %s (%x)", (isPrimary ? "selection clipboard" : "clipboard"), ID);
return String.format("Clipboard: Wayland %s", (isPrimary ? "selection clipboard" : "clipboard"));
}
@Override
public long getID() {
return ID;
return isPrimary ? 2 : 1;
}
/**
* Called when we loose ownership of the clipboard.
* Called when we lose ownership of the clipboard.
*/
@Override
protected void clearNativeContext() {
@@ -164,34 +137,20 @@ public final class WLClipboard extends SunClipboard {
notifyOfNewFormats(formats);
if (formats.length > 0) {
String[] mime = new String[formats.length];
for (int i = 0; i < formats.length; i++) {
mime[i] = wlDataTransferer.getNativeForFormat(formats[i]);
if (log.isLoggable(PlatformLogger.Level.FINE)) {
log.fine("Clipboard: formats mapping " + formats[i] + " -> " + mime[i]);
}
}
if (log.isLoggable(PlatformLogger.Level.FINE)) {
log.fine("Clipboard: Offering new contents (" + contents + ") in these MIME formats: " + Arrays.toString(mime));
log.fine("Clipboard: Offering new contents (" + contents + ")");
}
WLDataSource newOffer = null;
newOffer = dataDevice.createDataSourceFromTransferable(getProtocol(), contents);
synchronized (dataLock) {
if (ourOfferNativePtr != 0) {
cancelOffer(ourOfferNativePtr);
ourOfferNativePtr = 0;
if (ourDataSource != null) {
ourDataSource.destroy();
}
ourDataSource = newOffer;
dataDevice.setSelection(getProtocol(), newOffer, eventSerial);
}
long newOfferPtr = offerData(eventSerial, mime, contents, dataOfferQueuePtr);
synchronized (dataLock) {
ourOfferNativePtr = newOfferPtr;
}
// Once we have offered the data, someone may come back and ask to provide them.
// In that event, the transferContentsWithType() will be called from native on the
// clipboard dispatch thread.
// A reference to contents is retained until we are notified of the new contents
// by the Wayland server.
}
} else {
this.owner = null;
@@ -199,75 +158,26 @@ public final class WLClipboard extends SunClipboard {
}
}
/**
* Called from native on EDT when a client has asked to provide the actual data for
* the clipboard that we own in the given format to the given file.
* NB: that client could be us, but we aren't necessarily aware of that once we
* lost keyboard focus at least once after Ctrl-C.
*
* @param contents a reference to the clipboard's contents to be transferred
* @param mime transfer the contents in this MIME format
* @param destFD transfer the contents to this file descriptor and close it afterward
*
* @throws IOException in case writing to the given file descriptor failed
*/
private void transferContentsWithType(Transferable contents, String mime, int destFD) throws IOException {
assert (Thread.currentThread() == clipboardDispatcherThread);
Objects.requireNonNull(contents);
Objects.requireNonNull(mime);
WLDataTransferer wlDataTransferer = (WLDataTransferer) DataTransferer.getInstance();
SortedMap<Long,DataFlavor> formatMap =
wlDataTransferer.getFormatsForTransferable(contents, flavorTable);
long targetFormat = wlDataTransferer.getFormatForNativeAsLong(mime);
DataFlavor flavor = formatMap.get(targetFormat);
if (log.isLoggable(PlatformLogger.Level.FINE)) {
log.fine("Clipboard: will write contents (" + contents + ") in format " + mime + " to fd=" + destFD);
log.fine("Clipboard: data flavor: " + flavor);
}
if (flavor != null) {
byte[] bytes = wlDataTransferer.translateTransferable(contents, flavor, targetFormat);
if (bytes == null) return;
FileDescriptor javaDestFD = new FileDescriptor();
jdk.internal.access.SharedSecrets.getJavaIOFileDescriptorAccess().set(javaDestFD, destFD);
try (var out = new FileOutputStream(javaDestFD)) {
if (log.isLoggable(PlatformLogger.Level.FINE)) {
log.fine("Clipboard: about to write " + bytes.length + " bytes to " + out);
}
FileChannel ch = out.getChannel();
ByteBuffer buffer = ByteBuffer.wrap(bytes);
// Need to write with retries because when a pipe is in the non-blocking mode
// writing more than its capacity (usually 16 pages or 64K) fails with EAGAIN.
// Since we receive destFD from the Wayland sever, we can't assume it
// to always be in the blocking mode.
while (buffer.hasRemaining()) {
ch.write(buffer);
}
}
}
}
/**
* @return formats the current clipboard is available in; could be null
*/
@Override
protected long[] getClipboardFormats() {
WLDataTransferer wlDataTransferer = (WLDataTransferer) DataTransferer.getInstance();
List<String> mimes;
synchronized (dataLock) {
if (clipboardFormats != null && !clipboardFormats.isEmpty()) {
long[] res = new long[clipboardFormats.size()];
for (int i = 0; i < res.length; i++) {
res[i] = clipboardFormats.get(i);
}
return res;
} else {
if (clipboardDataOfferedToUs == null || !clipboardDataOfferedToUs.isValid()) {
return null;
}
mimes = clipboardDataOfferedToUs.getMimes();
}
long[] formats = new long[mimes.size()];
for (int i = 0; i < mimes.size(); ++i) {
formats[i] = wlDataTransferer.getFormatForNativeAsLong(mimes.get(i));
}
return formats;
}
/**
@@ -279,100 +189,10 @@ public final class WLClipboard extends SunClipboard {
*/
@Override
protected byte[] getClipboardData(long format) throws IOException {
int fd = getClipboardFDIn(format);
if (fd >= 0) {
FileDescriptor javaFD = new FileDescriptor();
jdk.internal.access.SharedSecrets.getJavaIOFileDescriptorAccess().set(javaFD, fd);
try (var in = new FileInputStream(javaFD)) {
byte[] bytes = readAllBytesFrom(in);
if (log.isLoggable(PlatformLogger.Level.FINE)) {
log.fine("Clipboard: read data from " + fd + ": "
+ (bytes != null ? bytes.length : 0) + " bytes");
}
return bytes;
}
}
return null;
}
private int getClipboardFDIn(long format) {
synchronized (dataLock) {
if (log.isLoggable(PlatformLogger.Level.FINE)) {
log.fine("Clipboard: requested content of clipboard with handle "
+ clipboardNativePtr + " in format " + format);
}
if (clipboardNativePtr != 0) {
WLDataTransferer wlDataTransferer = (WLDataTransferer) DataTransferer.getInstance();
String mime = wlDataTransferer.getNativeForFormat(format);
int fd = requestDataInFormat(clipboardNativePtr, mime);
if (log.isLoggable(PlatformLogger.Level.FINE)) {
log.fine("Clipboard: will read data from " + fd + " in format " + mime);
}
return fd;
}
}
return -1;
}
/**
* Called from native to notify us of the availability of a new clipboard
* denoted by the native handle in a specific MIME format.
* This method is usually called repeatedly with the same nativePtr and
* different formats. When all formats are announces in this way,
* handleNewClipboard() is called.
*
* @param nativePtr a native handle to the clipboard
* @param mime the MIME format in which this clipboard is available.
*/
private void handleClipboardFormat(long nativePtr, String mime) {
WLDataTransferer wlDataTransferer = (WLDataTransferer) DataTransferer.getInstance();
Long format = wlDataTransferer.getFormatForNativeAsLong(mime);
if (log.isLoggable(PlatformLogger.Level.FINE)) {
log.fine("Clipboard: new format is available for " + nativePtr + ": " + mime);
}
String mime = wlDataTransferer.getNativeForFormat(format);
synchronized (dataLock) {
newClipboardFormats.add(format);
}
}
/**
* Called from native to notify us that a new clipboard content
* has been made available. The list of supported formats
* should have already been received and saved in newClipboardFormats.
*
* @param newClipboardNativePtr a native handle to the clipboard
*/
private void handleNewClipboard(long newClipboardNativePtr) {
// Since we have a new clipboard, the existing one is no longer valid.
// We have no way of knowing whether this "new" one is the same as the "old" one.
lostOwnershipNow(null);
if (log.isLoggable(PlatformLogger.Level.FINE)) {
log.fine("Clipboard: new clipboard is available: " + newClipboardNativePtr);
}
synchronized (dataLock) {
long oldClipboardNativePtr = clipboardNativePtr;
if (oldClipboardNativePtr != 0) {
// "The client must destroy the previous selection data_offer, if any, upon receiving this event."
destroyClipboard(oldClipboardNativePtr);
}
clipboardFormats = newClipboardFormats;
clipboardNativePtr = newClipboardNativePtr; // Could be NULL
newClipboardFormats = new ArrayList<>(INITIAL_MIME_FORMATS_COUNT);
}
notifyOfNewFormats(getClipboardFormats());
}
private void handleOfferCancelled(long offerNativePtr) {
synchronized (dataLock) {
assert offerNativePtr == ourOfferNativePtr;
ourOfferNativePtr = 0;
return clipboardDataOfferedToUs.receiveData(mime);
}
}
@@ -397,76 +217,14 @@ public final class WLClipboard extends SunClipboard {
}
}
/**
* Reads the given input stream until EOF and returns its contents as an array of bytes.
*/
private byte[] readAllBytesFrom(FileInputStream inputStream) throws IOException {
int len = Integer.MAX_VALUE;
List<byte[]> bufs = null;
byte[] result = null;
int total = 0;
int remaining = len;
int n;
do {
byte[] buf = new byte[Math.min(remaining, DEFAULT_BUFFER_SIZE)];
int nread = 0;
void handleClipboardOffer(WLDataOffer offer /* nullable */) {
lostOwnershipNow(null);
while ((n = inputStream.read(buf, nread,
Math.min(buf.length - nread, remaining))) > 0) {
nread += n;
remaining -= n;
synchronized (dataLock) {
if (clipboardDataOfferedToUs != null && clipboardDataOfferedToUs.isValid()) {
clipboardDataOfferedToUs.destroy();
}
if (nread > 0) {
if (MAX_BUFFER_SIZE - total < nread) {
throw new OutOfMemoryError("Required array size too large");
}
if (nread < buf.length) {
buf = Arrays.copyOfRange(buf, 0, nread);
}
total += nread;
if (result == null) {
result = buf;
} else {
if (bufs == null) {
bufs = new ArrayList<>();
bufs.add(result);
}
bufs.add(buf);
}
}
// if the last call to read returned -1 or the number of bytes
// requested have been read then break
} while (n >= 0 && remaining > 0);
if (bufs == null) {
if (result == null) {
return new byte[0];
}
return result.length == total ?
result : Arrays.copyOf(result, total);
clipboardDataOfferedToUs = offer;
}
result = new byte[total];
int offset = 0;
remaining = total;
for (byte[] b : bufs) {
int count = Math.min(b.length, remaining);
System.arraycopy(b, 0, result, offset, count);
offset += count;
remaining -= count;
}
return result;
}
private static native void initIDs();
private static native long createDataOfferQueue();
private static native void dispatchDataOfferQueueImpl(long dataOfferQueuePtr);
private native long initNative(boolean isPrimary);
private native long offerData(long eventSerial, String[] mime, Object data, long dataOfferQueuePtr);
private native void cancelOffer(long offerNativePtr);
private native int requestDataInFormat(long clipboardNativePtr, String mime);
private native void destroyClipboard(long clipboardNativePtr);
}

View File

@@ -0,0 +1,249 @@
/*
* Copyright 2025 JetBrains s.r.o.
* 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 sun.awt.wl;
import jdk.internal.misc.InnocuousThread;
import sun.util.logging.PlatformLogger;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.Transferable;
import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.SortedMap;
public class WLDataDevice {
private static final PlatformLogger log = PlatformLogger.getLogger("sun.awt.wl.WLDataDevice");
private long nativePtr;
private final WLClipboard systemClipboard;
private final WLClipboard primarySelectionClipboard;
public static final int DND_COPY = 0x01;
public static final int DND_MOVE = 0x02;
public static final int DND_ASK = 0x04;
public static final int DND_ALL = DND_COPY | DND_MOVE | DND_ASK;
public static final int DATA_TRANSFER_PROTOCOL_WAYLAND = 1;
public static final int DATA_TRANSFER_PROTOCOL_PRIMARY_SELECTION = 2;
WLDataDevice(long wlSeatNativePtr) {
nativePtr = initNative(wlSeatNativePtr);
var queueThread = InnocuousThread.newThread("AWT-Wayland-data-transferer-dispatcher", () -> {
dispatchDataTransferQueueImpl(nativePtr);
if (log.isLoggable(PlatformLogger.Level.FINE)) {
log.fine("data transfer dispatcher exited");
}
});
queueThread.setDaemon(true);
queueThread.start();
systemClipboard = new WLClipboard(this, "System", false);
if (isProtocolSupported(DATA_TRANSFER_PROTOCOL_PRIMARY_SELECTION)) {
primarySelectionClipboard = new WLClipboard(this, "Selection", true);
} else {
primarySelectionClipboard = null;
}
}
private native static void initIDs();
static {
initIDs();
}
private native long initNative(long wlSeatNativePtr);
private static native boolean isProtocolSupportedImpl(long nativePtr, int protocol);
private static native void dispatchDataTransferQueueImpl(long nativePtr);
private static native void setSelectionImpl(int protocol, long nativePtr, long dataOfferNativePtr, long serial);
public WLDataSource createDataSourceFromTransferable(int protocol, Transferable transferable) {
return new WLDataSource(nativePtr, protocol, transferable);
}
public boolean isProtocolSupported(int protocol) {
return isProtocolSupportedImpl(nativePtr, protocol);
}
public void setSelection(int protocol, WLDataSource source, long serial) {
setSelectionImpl(protocol, nativePtr, source.getNativePtr(), serial);
}
public WLClipboard getSystemClipboard() {
return systemClipboard;
}
public WLClipboard getPrimarySelectionClipboard() {
return primarySelectionClipboard;
}
static void transferContentsWithType(Transferable contents, String mime, int fd) {
Objects.requireNonNull(contents);
Objects.requireNonNull(mime);
WLDataTransferer wlDataTransferer = (WLDataTransferer) WLDataTransferer.getInstance();
SortedMap<Long, DataFlavor> formatMap = wlDataTransferer.getFormatsForTransferable(contents, wlDataTransferer.getFlavorTable());
long targetFormat = wlDataTransferer.getFormatForNativeAsLong(mime);
DataFlavor flavor = formatMap.get(targetFormat);
if (log.isLoggable(PlatformLogger.Level.FINE)) {
log.fine("will write contents (" + contents + ") in format " + mime + " to fd=" + fd);
log.fine("data flavor: " + flavor);
}
if (flavor != null) {
try {
byte[] bytes = wlDataTransferer.translateTransferable(contents, flavor, targetFormat);
if (bytes == null) return;
FileDescriptor javaDestFD = new FileDescriptor();
jdk.internal.access.SharedSecrets.getJavaIOFileDescriptorAccess().set(javaDestFD, fd);
try (var out = new FileOutputStream(javaDestFD)) {
if (log.isLoggable(PlatformLogger.Level.FINE)) {
log.fine("about to write " + bytes.length + " bytes to " + out);
}
FileChannel ch = out.getChannel();
ByteBuffer buffer = ByteBuffer.wrap(bytes);
// Need to write with retries because when a pipe is in the non-blocking mode
// writing more than its capacity (usually 16 pages or 64K) fails with EAGAIN.
// Since we receive fd from the Wayland sever, we can't assume it
// to always be in the blocking mode.
while (buffer.hasRemaining()) {
ch.write(buffer);
}
}
} catch (IOException e) {
log.warning("failed to write contents (" + contents + ") in format " + mime + " to fd=" + fd);
}
}
}
private static final int DEFAULT_BUFFER_SIZE = 4096;
private static final int MAX_BUFFER_SIZE = Integer.MAX_VALUE - 8;
/**
* Reads the given input stream until EOF and returns its contents as an array of bytes.
*/
static byte[] readAllBytesFrom(FileInputStream inputStream) throws IOException {
int len = Integer.MAX_VALUE;
List<byte[]> bufs = null;
byte[] result = null;
int total = 0;
int remaining = len;
int n;
do {
byte[] buf = new byte[Math.min(remaining, DEFAULT_BUFFER_SIZE)];
int nread = 0;
while ((n = inputStream.read(buf, nread,
Math.min(buf.length - nread, remaining))) > 0) {
nread += n;
remaining -= n;
}
if (nread > 0) {
if (MAX_BUFFER_SIZE - total < nread) {
throw new OutOfMemoryError("Required array size too large");
}
if (nread < buf.length) {
buf = Arrays.copyOfRange(buf, 0, nread);
}
total += nread;
if (result == null) {
result = buf;
} else {
if (bufs == null) {
bufs = new ArrayList<>();
bufs.add(result);
}
bufs.add(buf);
}
}
// if the last call to read returned -1 or the number of bytes
// requested have been read then break
} while (n >= 0 && remaining > 0);
if (bufs == null) {
if (result == null) {
return new byte[0];
}
return result.length == total ?
result : Arrays.copyOf(result, total);
}
result = new byte[total];
int offset = 0;
remaining = total;
for (byte[] b : bufs) {
int count = Math.min(b.length, remaining);
System.arraycopy(b, 0, result, offset, count);
offset += count;
remaining -= count;
}
return result;
}
private WLDataOffer currentDnDOffer = null;
// Event handlers, called from native on the EDT
private void handleDnDEnter(WLDataOffer offer, long serial, long surfacePtr, double x, double y) {
if (currentDnDOffer != null) {
currentDnDOffer.destroy();
}
currentDnDOffer = offer;
}
private void handleDnDLeave() {
if (currentDnDOffer != null) {
currentDnDOffer.destroy();
currentDnDOffer = null;
}
}
private void handleDnDMotion(long timestamp, double x, double y) {
}
private void handleDnDDrop() {
}
private void handleSelection(WLDataOffer offer /* nullable */, int protocol) {
WLClipboard clipboard = (protocol == DATA_TRANSFER_PROTOCOL_PRIMARY_SELECTION) ? primarySelectionClipboard : systemClipboard;
if (clipboard != null) {
clipboard.handleClipboardOffer(offer);
}
}
}

View File

@@ -0,0 +1,126 @@
/*
* Copyright 2025 JetBrains s.r.o.
* 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 sun.awt.wl;
import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
public class WLDataOffer {
private long nativePtr;
private final List<String> mimes = new ArrayList<>();
private int sourceActions = -1;
private int selectedAction = -1;
private static native void destroyImpl(long nativePtr);
private static native void acceptImpl(long nativePtr, long serial, String mime);
private static native int openReceivePipe(long nativePtr, String mime);
private static native void finishDnDImpl(long nativePtr);
private static native void setDnDActionsImpl(long nativePtr, int actions, int preferredAction);
private WLDataOffer(long nativePtr) {
this.nativePtr = nativePtr;
}
public boolean isValid() {
return nativePtr != 0;
}
public void destroy() {
if (nativePtr != 0) {
destroyImpl(nativePtr);
nativePtr = 0;
}
}
public byte[] receiveData(String mime) throws IOException {
if (nativePtr == 0) {
throw new IllegalStateException("nativePtr is 0");
}
int fd = openReceivePipe(nativePtr, mime);
assert(fd != -1); // Otherwise an exception should be thrown from native code
FileDescriptor javaFD = new FileDescriptor();
jdk.internal.access.SharedSecrets.getJavaIOFileDescriptorAccess().set(javaFD, fd);
try (var in = new FileInputStream(javaFD)) {
return WLDataDevice.readAllBytesFrom(in);
}
}
public void accept(long serial, String mime) {
if (nativePtr == 0) {
throw new IllegalStateException("nativePtr is 0");
}
acceptImpl(nativePtr, serial, mime);
}
public void finishDnD() {
if (nativePtr == 0) {
throw new IllegalStateException("nativePtr is 0");
}
finishDnDImpl(nativePtr);
}
public void setDnDActions(int actions, int preferredAction) {
if (nativePtr == 0) {
throw new IllegalStateException("nativePtr is 0");
}
if (actions != 0) {
if ((actions & preferredAction) == 0) {
throw new IllegalArgumentException("preferredAction is not a valid action");
}
}
setDnDActionsImpl(nativePtr, actions, preferredAction);
}
public List<String> getMimes() {
return mimes;
}
// Event handlers, called from native code on the data device dispatch thread
private void handleOfferMime(String mime) {
mimes.add(mime);
}
private void handleSourceActions(int actions) {
sourceActions = actions;
}
private void handleAction(int action) {
selectedAction = action;
}
}

View File

@@ -0,0 +1,112 @@
/*
* Copyright 2025 JetBrains s.r.o.
* 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 sun.awt.wl;
import java.awt.datatransfer.Transferable;
public class WLDataSource {
private long nativePtr;
private final Transferable data;
private native long initNative(long dataDeviceNativePtr, int protocol);
private static native void offerMimeImpl(long nativePtr, String mime);
private static native void destroyImpl(long nativePtr);
private static native void setDnDActionsImpl(long nativePtr, int actions);
WLDataSource(long dataDeviceNativePtr, int protocol, Transferable data) {
var wlDataTransferer = (WLDataTransferer) WLDataTransferer.getInstance();
nativePtr = initNative(dataDeviceNativePtr, protocol);
assert nativePtr != 0; // should've already thrown in native
this.data = data;
try {
if (data != null) {
long[] formats = wlDataTransferer.getFormatsForTransferableAsArray(data, wlDataTransferer.getFlavorTable());
for (long format : formats) {
String mime = wlDataTransferer.getNativeForFormat(format);
offerMime(mime);
}
}
} catch (Throwable e) {
destroyImpl(nativePtr);
throw e;
}
}
public boolean isValid() {
return nativePtr != 0;
}
public long getNativePtr() {
return nativePtr;
}
public void offerMime(String mime) {
if (nativePtr == 0) {
throw new IllegalStateException("Native pointer is null");
}
offerMimeImpl(nativePtr, mime);
}
public void destroy() {
if (nativePtr != 0) {
destroyImpl(nativePtr);
nativePtr = 0;
}
}
public void setDnDActions(int actions) {
if (nativePtr == 0) {
throw new IllegalStateException("Native pointer is null");
}
setDnDActionsImpl(nativePtr, actions);
}
// Event handlers, called from native code on the data transferer dispatch thread
protected void handleTargetAcceptsMime(String mime) {
}
protected void handleSend(String mime, int fd) {
WLDataDevice.transferContentsWithType(data, mime, fd);
}
protected void handleCancelled() {
destroy();
}
protected void handleDnDDropPerformed() {
}
protected void handleDnDFinished() {
}
protected void handleDnDAction(int action) {
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2023 JetBrains s.r.o.
* Copyright 2025 JetBrains s.r.o.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@@ -24,6 +24,7 @@
*/
package sun.awt.wl;
import jdk.internal.misc.InnocuousThread;
import sun.awt.datatransfer.DataTransferer;
import sun.awt.datatransfer.ToolkitThreadBlockedHandler;
import sun.datatransfer.DataFlavorUtil;
@@ -39,24 +40,18 @@ import javax.imageio.stream.ImageInputStream;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.FlavorTable;
import java.awt.datatransfer.SystemFlavorMap;
import java.awt.datatransfer.Transferable;
import java.awt.image.BufferedImage;
import java.awt.image.ColorModel;
import java.awt.image.WritableRaster;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.*;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.HashMap;
import java.util.Objects;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.util.*;
/**
* Facilitates data conversion between formats for the use with the clipboard.
@@ -66,8 +61,6 @@ import java.util.Objects;
* once established, this mapping never changes
*/
public class WLDataTransferer extends DataTransferer {
private static final PlatformLogger log = PlatformLogger.getLogger("sun.awt.wl.WLDataTransferer");
private static ImageTypeSpecifier defaultImageSpec = null;
// Maps the "native" format (MIME) to its numeric ID
@@ -79,6 +72,12 @@ public class WLDataTransferer extends DataTransferer {
private final Map<Long, Boolean> imageFormats = new HashMap<>();
private final Map<Long, Boolean> textFormats = new HashMap<>();
private final FlavorTable flavorTable;
public FlavorTable getFlavorTable() {
return flavorTable;
}
private static class HOLDER {
static WLDataTransferer instance = new WLDataTransferer();
}
@@ -87,6 +86,10 @@ public class WLDataTransferer extends DataTransferer {
return HOLDER.instance;
}
private WLDataTransferer() {
flavorTable = adaptFlavorMap(SystemFlavorMap.getDefaultFlavorMap());
}
@Override
public String getDefaultUnicodeEncoding() {
return "UTF-8";

View File

@@ -156,8 +156,7 @@ public class WLToolkit extends UNIXToolkit implements Runnable {
private static boolean initialized = false;
private static Thread toolkitThread;
private final WLClipboard clipboard;
private final WLClipboard selection;
private final WLDataDevice dataDevice;
private static Cursor currentCursor;
@@ -191,17 +190,9 @@ public class WLToolkit extends UNIXToolkit implements Runnable {
toolkitSystemThread.setDaemon(true);
toolkitSystemThread.start();
WLClipboard selectionClipboard = null;
try {
selectionClipboard = new WLClipboard("Selection", true);
} catch (UnsupportedOperationException ignored) {
}
clipboard = new WLClipboard("System", false);
selection = selectionClipboard;
dataDevice = new WLDataDevice(0); // TODO: for multiseat support pass wl_seat pointer here
} else {
clipboard = null;
selection = null;
dataDevice = null;
}
}
@@ -812,7 +803,7 @@ public class WLToolkit extends UNIXToolkit implements Runnable {
security.checkPermission(AWTPermissions.ACCESS_CLIPBOARD_PERMISSION);
}
return clipboard;
return dataDevice.getSystemClipboard();
}
@Override
@@ -822,7 +813,7 @@ public class WLToolkit extends UNIXToolkit implements Runnable {
if (security != null) {
security.checkPermission(AWTPermissions.ACCESS_CLIPBOARD_PERMISSION);
}
return selection;
return dataDevice.getPrimarySelectionClipboard();
}
@Override

View File

@@ -1,724 +0,0 @@
/*
* Copyright (c) 2023, JetBrains s.r.o.. 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.
*/
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <assert.h>
#include "JNIUtilities.h"
#include "sun_awt_wl_WLClipboard.h"
#include "wayland-client-protocol.h"
#include "WLToolkit.h"
// A type both zwp_primary_selection_source_v1* and wl_data_source*
// are convertible to.
typedef void* data_source_t;
static jmethodID transferContentsWithTypeMID; // WLCipboard.transferContentsWithType()
static jmethodID handleClipboardFormatMID; // WLCipboard.handleClipboardFormat()
static jmethodID handleNewClipboardMID; // WLCipboard.handleNewClipboard()
static jmethodID handleOfferCancelledMID; // WLCipboard.handleOfferCancelled()
static jfieldID isPrimaryFID; // WLClipboard.isPrimary
typedef struct DataSourcePayload {
data_source_t source;
jobject clipboard; // a global reference to WLClipboard
jobject content; // a global reference to Transferable
jboolean isPrimary;
} DataSourcePayload;
static DataSourcePayload *
DataSourcePayload_Create(jobject clipboard, jobject content)
{
DataSourcePayload * payload = malloc(sizeof(struct DataSourcePayload));
if (payload) {
payload->source = NULL;
payload->clipboard = clipboard;
payload->content = content;
}
return payload;
}
static void
DataSourcePayload_Destroy(DataSourcePayload* payload)
{
free(payload);
}
typedef struct DataOfferPayload {
jobject clipboard; // a global reference to WLClipboard
} DataOfferPayload;
static DataOfferPayload *
DataOfferPayload_Create(jobject clipboard)
{
// NB: this payload is associated with the clipboard and, once created,
// is never destroyed, much like the clipboard itself.
DataOfferPayload * payload = malloc(sizeof(struct DataOfferPayload));
if (payload) {
payload->clipboard = clipboard;
}
return payload;
}
// Clipboard "devices", one for the actual clipboard and one for the selection clipboard.
// Implicitly assumed that WLClipboard can only create once instance of each.
static struct wl_data_device *wl_data_device;
static struct zwp_primary_selection_device_v1 *zwp_selection_device;
static void data_device_handle_data_offer(
void *data,
struct wl_data_device *data_device,
struct wl_data_offer *offer);
static void data_device_handle_selection(
void *data,
struct wl_data_device *data_device,
struct wl_data_offer *offer);
static void
data_device_handle_enter(
void *data,
struct wl_data_device *wl_data_device,
uint32_t serial,
struct wl_surface *surface,
wl_fixed_t x,
wl_fixed_t y,
struct wl_data_offer *id)
{
// TODO
}
static void
data_device_handle_leave(void *data, struct wl_data_device *wl_data_device)
{
// TODO
}
static void
data_device_handle_motion(
void *data,
struct wl_data_device *wl_data_device,
uint32_t time,
wl_fixed_t x,
wl_fixed_t y)
{
// TODO
}
static void
data_device_handle_drop(void *data, struct wl_data_device *wl_data_device)
{
// TODO
}
static const struct wl_data_device_listener wl_data_device_listener = {
.data_offer = data_device_handle_data_offer,
.selection = data_device_handle_selection,
.enter = data_device_handle_enter,
.leave = data_device_handle_leave,
.motion = data_device_handle_motion,
.drop = data_device_handle_drop
};
static void
RegisterDataOfferWithMimeType(DataOfferPayload *payload, void *offer, const char *mime_type)
{
JNIEnv *env = getEnv();
jstring mimeTypeString = (*env)->NewStringUTF(env, mime_type);
EXCEPTION_CLEAR(env);
if (mimeTypeString) {
(*env)->CallVoidMethod(env, payload->clipboard, handleClipboardFormatMID, ptr_to_jlong(offer), mimeTypeString);
EXCEPTION_CLEAR(env);
(*env)->DeleteLocalRef(env, mimeTypeString);
}
}
static void
RegisterDataOffer(DataOfferPayload *payload, void *offer)
{
JNIEnv *env = getEnv();
(*env)->CallVoidMethod(env, payload->clipboard, handleNewClipboardMID, ptr_to_jlong(offer));
EXCEPTION_CLEAR(env);
}
static void
zwp_selection_offer(
void *data,
struct zwp_primary_selection_offer_v1 *offer,
const char *mime_type)
{
assert (data != NULL);
RegisterDataOfferWithMimeType(data, offer, mime_type);
}
const struct zwp_primary_selection_offer_v1_listener zwp_selection_offer_listener = {
.offer = zwp_selection_offer
};
static void
zwp_selection_device_handle_data_offer(
void *data,
struct zwp_primary_selection_device_v1 *device,
struct zwp_primary_selection_offer_v1 *offer)
{
zwp_primary_selection_offer_v1_add_listener(offer, &zwp_selection_offer_listener, data);
}
static void
zwp_selection_device_handle_selection(
void *data,
struct zwp_primary_selection_device_v1 *device,
struct zwp_primary_selection_offer_v1 *offer)
{
assert (data != NULL);
RegisterDataOffer(data, offer);
}
static const struct zwp_primary_selection_device_v1_listener zwp_selection_device_listener = {
.data_offer = zwp_selection_device_handle_data_offer,
.selection = zwp_selection_device_handle_selection
};
static void
wl_action(void *data, struct wl_data_offer *wl_data_offer, uint32_t dnd_action)
{
// TODO: this is for DnD
}
static void
wl_offer(void *data, struct wl_data_offer *offer, const char *mime_type)
{
assert (data != NULL);
RegisterDataOfferWithMimeType(data, offer, mime_type);
}
static void
wl_source_actions(void *data, struct wl_data_offer *wl_data_offer, uint32_t source_actions)
{
// TODO: this is for DnD
}
static const struct wl_data_offer_listener wl_data_offer_listener = {
.action = wl_action,
.offer = wl_offer,
.source_actions = wl_source_actions
};
static void
data_device_handle_data_offer(
void *data,
struct wl_data_device *data_device,
struct wl_data_offer *offer)
{
wl_data_offer_add_listener(offer, &wl_data_offer_listener, data);
}
static void data_device_handle_selection(
void *data,
struct wl_data_device *data_device,
struct wl_data_offer *offer)
{
assert (data != NULL);
RegisterDataOffer(data, offer);
}
static void
SendClipboardToFD(DataSourcePayload *payload, const char *mime_type, int fd)
{
JNIEnv *env = getEnv();
jstring mime_type_string = (*env)->NewStringUTF(env, mime_type);
EXCEPTION_CLEAR(env);
if (payload->clipboard != NULL && payload->content != NULL && mime_type_string != NULL && fd >= 0) {
(*env)->CallVoidMethod(env,
payload->clipboard,
transferContentsWithTypeMID,
payload->content,
mime_type_string,
fd);
EXCEPTION_CLEAR(env);
} else {
// The file is normally closed on the Java side, so only close here
// if the Java side wasn't involved.
close(fd);
}
if (mime_type_string != NULL) {
(*env)->DeleteLocalRef(env, mime_type_string);
}
}
static void
CleanupClipboard(DataSourcePayload *payload)
{
if (payload != NULL) {
JNIEnv* env = getEnv();
if (payload->clipboard != NULL) (*env)->DeleteGlobalRef(env, payload->clipboard);
if (payload->content != NULL) (*env)->DeleteGlobalRef(env, payload->content);
if (payload->source != NULL) {
if (payload->isPrimary) {
zwp_primary_selection_source_v1_destroy(payload->source);
} else {
wl_data_source_destroy(payload->source);
}
}
DataSourcePayload_Destroy(payload);
}
}
static void
OfferCancelled(DataSourcePayload *payload) {
JNIEnv *env = getEnv();
(*env)->CallVoidMethod(env, payload->clipboard, handleOfferCancelledMID, ptr_to_jlong(payload));
EXCEPTION_CLEAR(env);
CleanupClipboard(payload);
}
static void
wl_data_source_target(void *data,
struct wl_data_source *wl_data_source,
const char *mime_type)
{
// TODO
}
static void
wl_data_source_handle_send(
void *data,
struct wl_data_source *source,
const char *mime_type,
int fd)
{
assert(data);
SendClipboardToFD(data, mime_type, fd);
}
static void
wl_data_source_handle_cancelled(
void *data,
struct wl_data_source *source)
{
JNU_RUNTIME_ASSERT(getEnv(), data != NULL && source == ((DataSourcePayload*)data)->source, "Unexpected data source cancelled");
OfferCancelled(data);
}
static const struct wl_data_source_listener wl_data_source_listener = {
.target = wl_data_source_target,
.send = wl_data_source_handle_send,
.cancelled = wl_data_source_handle_cancelled
};
static void
zwp_selection_source_handle_send(
void *data,
struct zwp_primary_selection_source_v1 *source,
const char *mime_type,
int32_t fd)
{
DataSourcePayload * payload = data;
assert(payload);
SendClipboardToFD(payload, mime_type, fd);
}
static void
zwp_selection_source_handle_cancelled(
void *data,
struct zwp_primary_selection_source_v1 *source)
{
JNU_RUNTIME_ASSERT(getEnv(), data != NULL && source == ((DataSourcePayload*)data)->source, "Unexpected selection source cancelled");
OfferCancelled(data);
}
static const struct zwp_primary_selection_source_v1_listener zwp_selection_source_listener = {
.send = zwp_selection_source_handle_send,
.cancelled = zwp_selection_source_handle_cancelled
};
static jboolean
initJavaRefs(JNIEnv* env, jclass wlClipboardClass)
{
GET_METHOD_RETURN(transferContentsWithTypeMID,
wlClipboardClass,
"transferContentsWithType",
"(Ljava/awt/datatransfer/Transferable;Ljava/lang/String;I)V",
JNI_FALSE);
GET_METHOD_RETURN(handleClipboardFormatMID,
wlClipboardClass,
"handleClipboardFormat",
"(JLjava/lang/String;)V",
JNI_FALSE);
GET_METHOD_RETURN(handleNewClipboardMID,
wlClipboardClass,
"handleNewClipboard",
"(J)V",
JNI_FALSE);
GET_METHOD_RETURN(handleOfferCancelledMID,
wlClipboardClass,
"handleOfferCancelled",
"(J)V",
JNI_FALSE);
GET_FIELD_RETURN(isPrimaryFID,
wlClipboardClass,
"isPrimary",
"Z",
JNI_FALSE);
return JNI_TRUE;
}
/**
* Returns JNI_TRUE if the WLClipboard referred to by wlClipboard
* corresponds to the "primary selection" clipboard and JNI_FALSE
* otherwise.
* Depending on that, a different protocol must be used to communicate
* with Wayland (zwp_primary_selection_device_manager_v1 and
* wl_data_device_manager correspondingly).
*/
static jboolean
isPrimarySelectionClipboard(JNIEnv* env, jobject wlClipboard)
{
return (*env)->GetBooleanField(env, wlClipboard, isPrimaryFID);
}
/**
* Initializes data common to all clipboard objects. Called once in
* static initialization time of the WLClipboard class.
*
* Throws InternalError in case of any errors.
*/
JNIEXPORT void JNICALL
Java_sun_awt_wl_WLClipboard_initIDs(
JNIEnv *env,
jclass wlClipboardClass)
{
if (!initJavaRefs(env, wlClipboardClass)) {
JNU_ThrowInternalError(env, "Failed to find WLClipboard members");
}
}
JNIEXPORT jlong JNICALL
Java_sun_awt_wl_WLClipboard_createDataOfferQueue(
JNIEnv *env,
jclass wlClipboardClass)
{
struct wl_event_queue * queue = wl_display_create_queue(wl_display);
if (queue == NULL) {
JNU_ThrowInternalError(env, "Couldn't create an event queue for the clipboard");
}
return ptr_to_jlong(queue);
}
JNIEXPORT void JNICALL
Java_sun_awt_wl_WLClipboard_dispatchDataOfferQueueImpl(
JNIEnv *env,
jclass wlClipboardClass,
jlong dataOfferQueuePtr)
{
struct wl_event_queue * queue = jlong_to_ptr(dataOfferQueuePtr);
assert (queue != NULL);
while (wl_display_dispatch_queue(wl_display, queue) != -1) {
}
}
/**
* Initializes data for a specific clipboard object (the primary selection
* or the regular one). Called once per clipboard type.
*
* Returns the native handle to the corresponding clipboard.
*
* Throws UnsupportedOperationException in case of the primary selection
* clipboard is not available and isPrimary is true.
* Throws IllegalStateException in case of double-initialization.
*/
JNIEXPORT jlong JNICALL
Java_sun_awt_wl_WLClipboard_initNative(
JNIEnv *env,
jobject obj,
jboolean isPrimary)
{
if (!isPrimary) {
if (wl_data_device != NULL) {
JNU_ThrowByName(env, "java/lang/IllegalStateException", "one data device has already been created");
return 0;
}
} else {
if (zwp_selection_device != NULL) {
JNU_ThrowByName(env,
"java/lang/IllegalStateException",
"one primary selection device has already been created");
return 0;
}
}
jobject clipboardGlobalRef = (*env)->NewGlobalRef(env, obj); // normally never deleted
CHECK_NULL_RETURN(clipboardGlobalRef, 0);
DataOfferPayload *payload = DataOfferPayload_Create(clipboardGlobalRef);
if (payload == NULL) {
(*env)->DeleteGlobalRef(env, clipboardGlobalRef);
}
CHECK_NULL_THROW_OOME_RETURN(env, payload, "Failed to allocate memory for DataOfferPayload", 0);
if (!isPrimary) {
// TODO: may be needed by DnD also, initialize in a common place
wl_data_device = wl_data_device_manager_get_data_device(wl_ddm, wl_seat);
if (wl_data_device == NULL) {
(*env)->DeleteGlobalRef(env, clipboardGlobalRef);
JNU_ThrowByName(env,
"java/awt/AWTError",
"wl_data_device_manager_get_data_device() failed");
return 0;
}
wl_data_device_add_listener(wl_data_device, &wl_data_device_listener, payload);
} else {
if (zwp_selection_dm != NULL) {
zwp_selection_device = zwp_primary_selection_device_manager_v1_get_device(zwp_selection_dm, wl_seat);
if (zwp_selection_device == NULL) {
(*env)->DeleteGlobalRef(env, clipboardGlobalRef);
JNU_ThrowByName(env,
"java/awt/AWTError",
"zwp_primary_selection_device_manager_v1_get_device() failed");
return 0;
}
zwp_primary_selection_device_v1_add_listener(zwp_selection_device, &zwp_selection_device_listener, payload);
} else {
(*env)->DeleteGlobalRef(env, clipboardGlobalRef);
JNU_ThrowByName(env,
"java/lang/UnsupportedOperationException",
"zwp_primary_selection_device_manager_v1 not available");
}
}
return ptr_to_jlong(wl_data_device);
}
static jboolean
announceMimeTypesForSource(
JNIEnv * env,
jboolean isPrimary,
jobjectArray mimeTypes,
data_source_t source)
{
jint length = (*env)->GetArrayLength(env, mimeTypes);
for (jint i = 0; i < length; i++) {
jstring s = (*env)->GetObjectArrayElement(env, mimeTypes, i);
JNU_CHECK_EXCEPTION_RETURN(env, JNI_FALSE);
const char *mimeType = (*env)->GetStringUTFChars(env, s, JNI_FALSE);
CHECK_NULL_RETURN(mimeType, JNI_FALSE);
if (isPrimary) {
zwp_primary_selection_source_v1_offer((struct zwp_primary_selection_source_v1 *)source, mimeType);
} else {
wl_data_source_offer((struct wl_data_source *)source, mimeType);
}
(*env)->ReleaseStringUTFChars(env, s, mimeType);
(*env)->DeleteLocalRef(env, s);
}
wlFlushToServer(env);
return JNI_TRUE;
}
static jboolean
offerData(
JNIEnv* env,
DataSourcePayload * payload,
jboolean isPrimary,
jlong eventSerial,
jobjectArray mimeTypes,
jlong dataOfferQueuePtr)
{
data_source_t source = isPrimary
? (data_source_t)zwp_primary_selection_device_manager_v1_create_source(zwp_selection_dm)
: (data_source_t)wl_data_device_manager_create_data_source(wl_ddm);
if (source != NULL) {
payload->source = source;
payload->isPrimary = isPrimary;
wl_proxy_set_queue((struct wl_proxy*)source, jlong_to_ptr(dataOfferQueuePtr));
if (isPrimary) {
zwp_primary_selection_source_v1_add_listener(
(struct zwp_primary_selection_source_v1 *)source,
&zwp_selection_source_listener,
payload);
} else {
wl_data_source_add_listener(
(struct wl_data_source *)source,
&wl_data_source_listener,
payload);
}
if (mimeTypes != NULL) {
if (!announceMimeTypesForSource(env, isPrimary, mimeTypes, source)) {
if (isPrimary) {
zwp_primary_selection_source_v1_destroy(source);
} else {
wl_data_source_destroy(source);
}
return JNI_FALSE;
}
}
if (isPrimary) {
zwp_primary_selection_device_v1_set_selection(
zwp_selection_device,
(struct zwp_primary_selection_source_v1 *)source,
eventSerial);
}
else {
wl_data_device_set_selection(
wl_data_device,
(struct wl_data_source *)source,
eventSerial);
}
wlFlushToServer(env);
}
return source != NULL;
}
/**
* Makes Wayland aware of the availability of new clipboard content in the
* given MIME formats.
* Retains the reference to clipboard content for the further use when the actual
* clipboard data get requested.
*/
JNIEXPORT jlong JNICALL
Java_sun_awt_wl_WLClipboard_offerData(
JNIEnv *env,
jobject obj,
jlong eventSerial,
jobjectArray mimeTypes,
jobject content,
jlong dataOfferQueuePtr)
{
jobject clipboardGlobalRef = (*env)->NewGlobalRef(env, obj); // deleted by ...source_handle_cancelled()
CHECK_NULL_RETURN(clipboardGlobalRef, 0);
jobject contentGlobalRef = (*env)->NewGlobalRef(env, content); // deleted by ...source_handle_cancelled()
CHECK_NULL_RETURN(contentGlobalRef, 0);
DataSourcePayload * payload = DataSourcePayload_Create(clipboardGlobalRef, contentGlobalRef);
if (payload == NULL) {
(*env)->DeleteGlobalRef(env, clipboardGlobalRef);
(*env)->DeleteGlobalRef(env, contentGlobalRef);
}
CHECK_NULL_THROW_OOME_RETURN(env, payload, "failed to allocate memory for DataSourcePayload", 0);
const jboolean isPrimary = isPrimarySelectionClipboard(env, obj);
if (!offerData(env, payload, isPrimary, eventSerial, mimeTypes, dataOfferQueuePtr)) {
// Failed to create a data source; give up and cleanup.
CleanupClipboard(payload);
}
return ptr_to_jlong(payload);
}
JNIEXPORT void JNICALL
Java_sun_awt_wl_WLClipboard_cancelOffer(
JNIEnv *env,
jobject obj,
jlong payloadNativePtr)
{
JNU_RUNTIME_ASSERT(env, payloadNativePtr != 0, "NULL pointer to clipboard data source");
CleanupClipboard(jlong_to_ptr(payloadNativePtr));
}
/**
* Asks Wayland to provide the data for the clipboard in the given MIME
* format.
*
* Returns the file descriptor from which the data must be read or -1
* in case of an error.
*
* NB: the returned file descriptor must be closed by the caller.
*/
JNIEXPORT jint JNICALL
Java_sun_awt_wl_WLClipboard_requestDataInFormat(
JNIEnv *env,
jobject obj,
jlong clipboardNativePtr,
jstring mimeTypeJava)
{
assert (clipboardNativePtr != 0);
const jboolean isPrimary = isPrimarySelectionClipboard(env, obj);
int fd = -1; // The file descriptor the clipboard data will be read from by Java
const char * mimeType = (*env)->GetStringUTFChars(env, mimeTypeJava, NULL);
if (mimeType) {
int fds[2];
int rc = pipe2(fds, O_CLOEXEC);
if (rc == 0) {
if (isPrimary) {
struct zwp_primary_selection_offer_v1 * offer = jlong_to_ptr(clipboardNativePtr);
zwp_primary_selection_offer_v1_receive(offer, mimeType, fds[1]);
} else {
struct wl_data_offer * offer = jlong_to_ptr(clipboardNativePtr);
wl_data_offer_receive(offer, mimeType, fds[1]);
}
// Since the request for the clipboard contents can and usually is blocking,
// make sure that the server has received it right away.
wlFlushToServer(env);
close(fds[1]); // close the "sender" end of the pipe
fd = fds[0];
}
(*env)->ReleaseStringUTFChars(env, mimeTypeJava, mimeType);
}
return fd;
}
/**
* Destroys the corresponding Wayland proxy objects pointed to by clipboardNativePtr.
*/
JNIEXPORT void JNICALL
Java_sun_awt_wl_WLClipboard_destroyClipboard(
JNIEnv *env,
jobject obj,
jlong clipboardNativePtr)
{
assert(clipboardNativePtr != 0);
if (isPrimarySelectionClipboard(env, obj)) {
struct zwp_primary_selection_offer_v1 * offer = jlong_to_ptr(clipboardNativePtr);
zwp_primary_selection_offer_v1_destroy(offer);
} else {
struct wl_data_offer * offer = jlong_to_ptr(clipboardNativePtr);
wl_data_offer_destroy(offer);
}
}

View File

@@ -0,0 +1,965 @@
/*
* Copyright (c) 2025, JetBrains s.r.o.. 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.
*/
#include <assert.h>
#include <stdbool.h>
#include <stdlib.h>
#include "JNIUtilities.h"
#include "WLToolkit.h"
#include "sun_awt_wl_WLDataDevice.h"
#include "sun_awt_wl_WLDataSource.h"
#include "sun_awt_wl_WLDataOffer.h"
#include "wayland-client-protocol.h"
// Types
enum DataTransferProtocol {
DATA_TRANSFER_PROTOCOL_WAYLAND = sun_awt_wl_WLDataDevice_DATA_TRANSFER_PROTOCOL_WAYLAND,
DATA_TRANSFER_PROTOCOL_PRIMARY_SELECTION = sun_awt_wl_WLDataDevice_DATA_TRANSFER_PROTOCOL_PRIMARY_SELECTION,
};
// native part of WLDataDevice, one instance per seat
// seat's wl_data_device and zwp_primary_selection_device_v1 have user pointers to this struct
struct DataDevice {
// global reference to the corresponding WLDataDevice object
jobject javaObject;
struct wl_event_queue *dataTransferQueue;
struct wl_data_device *wlDataDevice;
struct zwp_primary_selection_device_v1 *zwpPrimarySelectionDevice;
};
// native part of WLDataSource, remains alive until WLDataSource.destroy() is called
// pointer to this structure is the wl_data_source's (zwp_primary_selection_source_v1's) user pointer
struct DataSource {
enum DataTransferProtocol protocol;
// global reference to the corresponding WLDataSource object
jobject javaObject;
union {
void *anyPtr;
struct wl_data_source *wlDataSource;
struct zwp_primary_selection_source_v1 *zwpPrimarySelectionSource;
};
};
// native part of WLDataOffer, remains alive until WLDataOffer.destroy() is called
// pointer to this structure is the wl_data_offer's (zwp_primary_selection_offer_v1's) user pointer
struct DataOffer {
enum DataTransferProtocol protocol;
// global reference to the corresponding WLDataOffer object
jobject javaObject;
union {
void *anyPtr;
struct wl_data_offer *wlDataOffer;
struct zwp_primary_selection_offer_v1 *zwpPrimarySelectionOffer;
};
};
// Java refs
static jclass wlDataOfferClass;
static jmethodID wlDataDeviceHandleDnDEnterMID;
static jmethodID wlDataDeviceHandleDnDLeaveMID;
static jmethodID wlDataDeviceHandleDnDMotionMID;
static jmethodID wlDataDeviceHandleDnDDropMID;
static jmethodID wlDataDeviceHandleSelectionMID;
static jmethodID wlDataSourceHandleTargetAcceptsMimeMID;
static jmethodID wlDataSourceHandleSendMID;
static jmethodID wlDataSourceHandleCancelledMID;
static jmethodID wlDataSourceHandleDnDDropPerformedMID;
static jmethodID wlDataSourceHandleDnDFinishedMID;
static jmethodID wlDataSourceHandleDnDActionMID;
static jmethodID wlDataOfferConstructorMID;
static jmethodID wlDataOfferHandleOfferMimeMID;
static jmethodID wlDataOfferHandleSourceActionsMID;
static jmethodID wlDataOfferHandleActionMID;
static bool initJavaRefs(JNIEnv *env) {
jclass wlDataDeviceClass = NULL;
jclass wlDataSourceClass = NULL;
GET_CLASS_RETURN(wlDataDeviceClass, "sun/awt/wl/WLDataDevice", false);
GET_CLASS_RETURN(wlDataSourceClass, "sun/awt/wl/WLDataSource", false);
GET_CLASS_RETURN(wlDataOfferClass, "sun/awt/wl/WLDataOffer", false);
GET_METHOD_RETURN(wlDataDeviceHandleDnDEnterMID, wlDataDeviceClass, "handleDnDEnter",
"(Lsun/awt/wl/WLDataOffer;JJDD)V", false);
GET_METHOD_RETURN(wlDataDeviceHandleDnDLeaveMID, wlDataDeviceClass, "handleDnDLeave", "()V", false);
GET_METHOD_RETURN(wlDataDeviceHandleDnDMotionMID, wlDataDeviceClass, "handleDnDMotion", "(JDD)V", false);
GET_METHOD_RETURN(wlDataDeviceHandleDnDDropMID, wlDataDeviceClass, "handleDnDDrop", "()V", false);
GET_METHOD_RETURN(wlDataDeviceHandleSelectionMID, wlDataDeviceClass, "handleSelection",
"(Lsun/awt/wl/WLDataOffer;I)V", false);
GET_METHOD_RETURN(wlDataSourceHandleTargetAcceptsMimeMID, wlDataSourceClass, "handleTargetAcceptsMime",
"(Ljava/lang/String;)V", false);
GET_METHOD_RETURN(wlDataSourceHandleSendMID, wlDataSourceClass, "handleSend", "(Ljava/lang/String;I)V", false);
GET_METHOD_RETURN(wlDataSourceHandleCancelledMID, wlDataSourceClass, "handleCancelled", "()V", false);
GET_METHOD_RETURN(wlDataSourceHandleDnDDropPerformedMID, wlDataSourceClass, "handleDnDDropPerformed", "()V",
false);
GET_METHOD_RETURN(wlDataSourceHandleDnDFinishedMID, wlDataSourceClass, "handleDnDFinished", "()V", false);
GET_METHOD_RETURN(wlDataSourceHandleDnDActionMID, wlDataSourceClass, "handleDnDAction", "(I)V", false);
GET_METHOD_RETURN(wlDataOfferConstructorMID, wlDataOfferClass, "<init>", "(J)V", false);
GET_METHOD_RETURN(wlDataOfferHandleOfferMimeMID, wlDataOfferClass, "handleOfferMime", "(Ljava/lang/String;)V",
false);
GET_METHOD_RETURN(wlDataOfferHandleSourceActionsMID, wlDataOfferClass, "handleSourceActions", "(I)V", false);
GET_METHOD_RETURN(wlDataOfferHandleActionMID, wlDataOfferClass, "handleAction", "(I)V", false);
return true;
}
// Event handlers declarations
static void wl_data_source_handle_target(void *user, struct wl_data_source *source, const char *mime);
static void wl_data_source_handle_send(void *user, struct wl_data_source *source, const char *mime, int32_t fd);
static void wl_data_source_handle_cancelled(void *user, struct wl_data_source *source);
static void wl_data_source_handle_dnd_drop_performed(void *user, struct wl_data_source *source);
static void wl_data_source_handle_dnd_finished(void *user, struct wl_data_source *source);
static void wl_data_source_handle_action(void *user, struct wl_data_source *source, uint32_t action);
static const struct wl_data_source_listener wl_data_source_listener = {
.target = wl_data_source_handle_target,
.send = wl_data_source_handle_send,
.cancelled = wl_data_source_handle_cancelled,
.dnd_drop_performed = wl_data_source_handle_dnd_drop_performed,
.dnd_finished = wl_data_source_handle_dnd_finished,
.action = wl_data_source_handle_action,
};
static void zwp_primary_selection_source_handle_send(void *user,
struct zwp_primary_selection_source_v1 *source,
const char *mime,
int32_t fd);
static void zwp_primary_selection_source_handle_cancelled(void *user, struct zwp_primary_selection_source_v1 *source);
static const struct zwp_primary_selection_source_v1_listener zwp_primary_selection_source_v1_listener = {
.send = zwp_primary_selection_source_handle_send,
.cancelled = zwp_primary_selection_source_handle_cancelled,
};
static void wl_data_offer_handle_offer(void *user, struct wl_data_offer *offer, const char *mime);
static void wl_data_offer_handle_source_actions(void *user, struct wl_data_offer *offer, uint32_t source_actions);
static void wl_data_offer_handle_action(void *user, struct wl_data_offer *offer, uint32_t action);
static const struct wl_data_offer_listener wlDataOfferListener = {
.offer = wl_data_offer_handle_offer,
.source_actions = wl_data_offer_handle_source_actions,
.action = wl_data_offer_handle_action,
};
static void
zwp_primary_selection_offer_handle_offer(void *user, struct zwp_primary_selection_offer_v1 *offer, const char *mime);
static const struct zwp_primary_selection_offer_v1_listener zwpPrimarySelectionOfferListener = {
.offer = zwp_primary_selection_offer_handle_offer,
};
static void
wl_data_device_handle_data_offer(void *user, struct wl_data_device *wl_data_device, struct wl_data_offer *id);
static void wl_data_device_handle_enter(void *user,
struct wl_data_device *wl_data_device,
uint32_t serial,
struct wl_surface *surface,
wl_fixed_t x,
wl_fixed_t y,
struct wl_data_offer *id);
static void wl_data_device_handle_leave(void *user, struct wl_data_device *wl_data_device);
static void wl_data_device_handle_motion(
void *user, struct wl_data_device *wl_data_device, uint32_t time, wl_fixed_t x, wl_fixed_t y);
static void wl_data_device_handle_drop(void *user, struct wl_data_device *wl_data_device);
static void
wl_data_device_handle_selection(void *user, struct wl_data_device *wl_data_device, struct wl_data_offer *id);
static const struct wl_data_device_listener wlDataDeviceListener = {
.data_offer = wl_data_device_handle_data_offer,
.enter = wl_data_device_handle_enter,
.leave = wl_data_device_handle_leave,
.motion = wl_data_device_handle_motion,
.drop = wl_data_device_handle_drop,
.selection = wl_data_device_handle_selection,
};
static void zwp_primary_selection_device_handle_data_offer(void *user,
struct zwp_primary_selection_device_v1 *device,
struct zwp_primary_selection_offer_v1 *id);
static void zwp_primary_selection_device_handle_selection(void *user,
struct zwp_primary_selection_device_v1 *device,
struct zwp_primary_selection_offer_v1 *id);
static const struct zwp_primary_selection_device_v1_listener zwpPrimarySelectionDeviceListener = {
.data_offer = zwp_primary_selection_device_handle_data_offer,
.selection = zwp_primary_selection_device_handle_selection,
};
static void DataSource_offer(const struct DataSource *source, const char *mime);
static void DataSource_setDnDActions(const struct DataSource *source, uint32_t actions);
static struct DataOffer *DataOffer_create(struct DataDevice* dataDevice, enum DataTransferProtocol protocol, void *waylandObject);
static void DataOffer_destroy(struct DataOffer *offer);
static void DataOffer_receive(struct DataOffer *offer, const char *mime, int fd);
static void DataOffer_accept(struct DataOffer *offer, uint32_t serial, const char *mime);
static void DataOffer_finishDnD(struct DataOffer *offer);
static void DataOffer_setDnDActions(struct DataOffer *offer, int dnd_actions, int preferred_action);
static void DataOffer_callOfferHandler(struct DataOffer *offer, const char *mime);
static void DataOffer_callSelectionHandler(struct DataDevice* dataDevice, struct DataOffer *offer, enum DataTransferProtocol protocol);
// Implementation
static void DataSource_offer(const struct DataSource *source, const char *mime) {
if (source->protocol == DATA_TRANSFER_PROTOCOL_WAYLAND) {
wl_data_source_offer(source->wlDataSource, mime);
} else if (source->protocol == DATA_TRANSFER_PROTOCOL_PRIMARY_SELECTION) {
zwp_primary_selection_source_v1_offer(source->zwpPrimarySelectionSource, mime);
}
}
static void DataSource_setDnDActions(const struct DataSource *source, uint32_t actions) {
if (source->protocol == DATA_TRANSFER_PROTOCOL_WAYLAND) {
wl_data_source_set_actions(source->wlDataSource, actions);
}
}
static struct DataOffer *DataOffer_create(struct DataDevice* dataDevice, enum DataTransferProtocol protocol, void *waylandObject) {
struct DataOffer *offer = calloc(1, sizeof(struct DataOffer));
if (offer == NULL) {
return NULL;
}
JNIEnv* env = getEnv();
assert(env != NULL);
jobject obj = (*env)->NewObject(env, wlDataOfferClass, wlDataOfferConstructorMID, ptr_to_jlong(offer));
// Can't throw Java exceptions during Wayland event dispatch
EXCEPTION_CLEAR(env);
if (obj == NULL) {
free(offer);
return NULL;
}
// Cleared in DataOffer_destroy
jobject globalRef = (*env)->NewGlobalRef(env, obj);
EXCEPTION_CLEAR(env);
if (globalRef == NULL) {
(*env)->DeleteLocalRef(env, obj);
free(offer);
return NULL;
}
offer->javaObject = globalRef;
if (protocol == DATA_TRANSFER_PROTOCOL_WAYLAND) {
struct wl_data_offer *wlDataOffer = waylandObject;
wl_proxy_set_queue((struct wl_proxy *)wlDataOffer, dataDevice->dataTransferQueue);
wl_data_offer_add_listener(wlDataOffer, &wlDataOfferListener, offer);
offer->wlDataOffer = wlDataOffer;
offer->protocol = DATA_TRANSFER_PROTOCOL_WAYLAND;
}
if (protocol == DATA_TRANSFER_PROTOCOL_PRIMARY_SELECTION) {
struct zwp_primary_selection_offer_v1 *zwpPrimarySelectionOffer = waylandObject;
wl_proxy_set_queue((struct wl_proxy *)zwpPrimarySelectionOffer, dataDevice->dataTransferQueue);
zwp_primary_selection_offer_v1_add_listener(zwpPrimarySelectionOffer, &zwpPrimarySelectionOfferListener, offer);
offer->zwpPrimarySelectionOffer = zwpPrimarySelectionOffer;
offer->protocol = DATA_TRANSFER_PROTOCOL_PRIMARY_SELECTION;
}
return offer;
}
static void DataOffer_destroy(struct DataOffer *offer) {
if (offer == NULL) {
return;
}
if (offer->javaObject != NULL) {
JNIEnv* env = getEnv();
assert(env != NULL);
(*env)->DeleteGlobalRef(env, offer->javaObject);
offer->javaObject = NULL;
}
if (offer->protocol == DATA_TRANSFER_PROTOCOL_WAYLAND) {
wl_data_offer_destroy(offer->wlDataOffer);
} else if (offer->protocol == DATA_TRANSFER_PROTOCOL_PRIMARY_SELECTION) {
zwp_primary_selection_offer_v1_destroy(offer->zwpPrimarySelectionOffer);
}
free(offer);
}
static void DataOffer_receive(struct DataOffer *offer, const char *mime, int fd) {
if (offer->protocol == DATA_TRANSFER_PROTOCOL_WAYLAND) {
wl_data_offer_receive(offer->wlDataOffer, mime, fd);
} else if (offer->protocol == DATA_TRANSFER_PROTOCOL_PRIMARY_SELECTION) {
zwp_primary_selection_offer_v1_receive(offer->zwpPrimarySelectionOffer, mime, fd);
}
}
static void DataOffer_accept(struct DataOffer *offer, uint32_t serial, const char *mime) {
if (offer->protocol == DATA_TRANSFER_PROTOCOL_WAYLAND) {
wl_data_offer_accept(offer->wlDataOffer, serial, mime);
}
}
static void DataOffer_finishDnD(struct DataOffer *offer) {
if (offer->protocol == DATA_TRANSFER_PROTOCOL_WAYLAND) {
wl_data_offer_finish(offer->wlDataOffer);
}
}
static void DataOffer_setDnDActions(struct DataOffer *offer, int dnd_actions, int preferred_action) {
if (offer->protocol == DATA_TRANSFER_PROTOCOL_WAYLAND) {
wl_data_offer_set_actions(offer->wlDataOffer, dnd_actions, preferred_action);
}
}
static void DataOffer_callOfferHandler(struct DataOffer *offer, const char *mime) {
assert(offer != NULL);
if (offer->javaObject == NULL) {
return;
}
JNIEnv *env = getEnv();
assert(env != NULL);
jstring mimeJavaString = (*env)->NewStringUTF(env, mime);
EXCEPTION_CLEAR(env);
if (mimeJavaString != NULL) {
(*env)->CallVoidMethod(env, offer->javaObject, wlDataOfferHandleOfferMimeMID, mimeJavaString);
EXCEPTION_CLEAR(env);
(*env)->DeleteLocalRef(env, mimeJavaString);
}
}
static void DataOffer_callSelectionHandler(struct DataDevice* dataDevice, struct DataOffer *offer, enum DataTransferProtocol protocol) {
assert(dataDevice != NULL);
// offer can be NULL, this means that the selection was cleared
jobject offerObject = NULL;
if (offer != NULL) {
offerObject = offer->javaObject;
}
JNIEnv *env = getEnv();
assert(env != NULL);
(*env)->CallVoidMethod(env, dataDevice->javaObject, wlDataDeviceHandleSelectionMID, offerObject, protocol);
EXCEPTION_CLEAR(env);
}
// Event handlers
static void wl_data_source_handle_target(void *user, struct wl_data_source *wl_data_source, const char *mime) {
struct DataSource *source = user;
assert(source != NULL);
if (source->javaObject == NULL) {
return;
}
JNIEnv *env = getEnv();
assert(env != NULL);
jstring mimeJavaString = (*env)->NewStringUTF(env, mime);
EXCEPTION_CLEAR(env);
if (mimeJavaString != NULL) {
(*env)->CallVoidMethod(env, source->javaObject, wlDataSourceHandleTargetAcceptsMimeMID, mimeJavaString);
EXCEPTION_CLEAR(env);
(*env)->DeleteLocalRef(env, mimeJavaString);
}
}
static void
wl_data_source_handle_send(void *user, struct wl_data_source *wl_data_source, const char *mime, int32_t fd) {
struct DataSource *source = user;
assert(source != NULL);
if (source->javaObject == NULL) {
return;
}
JNIEnv *env = getEnv();
assert(env != NULL);
jstring mimeJavaString = (*env)->NewStringUTF(env, mime);
EXCEPTION_CLEAR(env);
if (mimeJavaString != NULL) {
(*env)->CallVoidMethod(env, source->javaObject, wlDataSourceHandleSendMID, mimeJavaString, fd);
EXCEPTION_CLEAR(env);
(*env)->DeleteLocalRef(env, mimeJavaString);
}
}
static void wl_data_source_handle_cancelled(void *user, struct wl_data_source *wl_data_source) {
struct DataSource *source = user;
assert(source != NULL);
if (source->javaObject == NULL) {
return;
}
JNIEnv *env = getEnv();
assert(env != NULL);
(*env)->CallVoidMethod(env, source->javaObject, wlDataSourceHandleCancelledMID);
EXCEPTION_CLEAR(env);
}
static void wl_data_source_handle_dnd_drop_performed(void *user, struct wl_data_source *wl_data_source) {
struct DataSource *source = user;
assert(source != NULL);
if (source->javaObject == NULL) {
return;
}
JNIEnv *env = getEnv();
assert(env != NULL);
(*env)->CallVoidMethod(env, source->javaObject, wlDataSourceHandleDnDDropPerformedMID);
EXCEPTION_CLEAR(env);
}
static void wl_data_source_handle_dnd_finished(void *user, struct wl_data_source *wl_data_source) {
struct DataSource *source = user;
assert(source != NULL);
if (source->javaObject == NULL) {
return;
}
JNIEnv *env = getEnv();
assert(env != NULL);
(*env)->CallVoidMethod(env, source->javaObject, wlDataSourceHandleDnDFinishedMID);
EXCEPTION_CLEAR(env);
}
static void wl_data_source_handle_action(void *user, struct wl_data_source *wl_data_source, uint32_t action) {
struct DataSource *source = user;
assert(source != NULL);
if (source->javaObject == NULL) {
return;
}
JNIEnv *env = getEnv();
assert(env != NULL);
(*env)->CallVoidMethod(env, source->javaObject, wlDataSourceHandleDnDActionMID, action);
EXCEPTION_CLEAR(env);
}
static void zwp_primary_selection_source_handle_send(
void *user, struct zwp_primary_selection_source_v1 *zwp_primary_selection_source_v1, const char *mime, int32_t fd) {
struct DataSource *source = user;
assert(source != NULL);
if (source->javaObject == NULL) {
return;
}
JNIEnv *env = getEnv();
assert(env != NULL);
jstring mimeJavaString = (*env)->NewStringUTF(env, mime);
EXCEPTION_CLEAR(env);
if (mimeJavaString != NULL) {
(*env)->CallVoidMethod(env, source->javaObject, wlDataSourceHandleSendMID, mimeJavaString, fd);
EXCEPTION_CLEAR(env);
(*env)->DeleteLocalRef(env, mimeJavaString);
}
}
static void
zwp_primary_selection_source_handle_cancelled(void *user,
struct zwp_primary_selection_source_v1 *zwp_primary_selection_source_v1) {
struct DataSource *source = user;
assert(source != NULL);
if (source->javaObject == NULL) {
return;
}
JNIEnv *env = getEnv();
assert(env != NULL);
(*env)->CallVoidMethod(env, source->javaObject, wlDataSourceHandleCancelledMID);
EXCEPTION_CLEAR(env);
}
static void wl_data_offer_handle_offer(void *user, struct wl_data_offer *wl_data_offer, const char *mime) {
DataOffer_callOfferHandler((struct DataOffer *)user, mime);
}
static void
wl_data_offer_handle_source_actions(void *user, struct wl_data_offer *wl_data_offer, uint32_t source_actions) {
struct DataOffer *offer = user;
assert(offer != NULL);
if (offer->javaObject == NULL) {
return;
}
JNIEnv *env = getEnv();
assert(env != NULL);
(*env)->CallVoidMethod(env, offer->javaObject, wlDataOfferHandleSourceActionsMID, (jint)source_actions);
EXCEPTION_CLEAR(env);
}
static void wl_data_offer_handle_action(void *user, struct wl_data_offer *wl_data_offer, uint32_t action) {
struct DataOffer *offer = user;
assert(offer != NULL);
if (offer->javaObject == NULL) {
return;
}
JNIEnv *env = getEnv();
assert(env != NULL);
(*env)->CallVoidMethod(env, offer->javaObject, wlDataOfferHandleActionMID, (jint)action);
EXCEPTION_CLEAR(env);
}
static void zwp_primary_selection_offer_handle_offer(
void *user, struct zwp_primary_selection_offer_v1 *zwp_primary_selection_offer_v1, const char *mime) {
DataOffer_callOfferHandler((struct DataOffer *)user, mime);
}
static void
wl_data_device_handle_data_offer(void *user, struct wl_data_device *wl_data_device, struct wl_data_offer *id) {
struct DataDevice* dataDevice = user;
assert(dataDevice != NULL);
struct DataOffer *offer = DataOffer_create(dataDevice, DATA_TRANSFER_PROTOCOL_WAYLAND, id);
if (offer == NULL) {
// This can only happen in OOM scenarios.
// We can't throw a Java exception here.
// Destroy the offer, since we won't be able to do anything useful with it.
wl_data_offer_destroy(id);
return;
}
// no memory leak here: allocated DataOffer object will be
// associated with the wl_data_offer
}
static void wl_data_device_handle_enter(void *user,
struct wl_data_device *wl_data_device,
uint32_t serial,
struct wl_surface *surface,
wl_fixed_t x,
wl_fixed_t y,
struct wl_data_offer *id) {
struct DataDevice* dataDevice = user;
assert(dataDevice != NULL);
struct DataOffer *offer = (struct DataOffer *)wl_data_offer_get_user_data(id);
assert(offer != NULL);
if (offer->javaObject == NULL) {
return;
}
JNIEnv *env = getEnv();
assert(env != NULL);
(*env)->CallVoidMethod(env, dataDevice->javaObject, wlDataDeviceHandleDnDEnterMID, offer->javaObject, (jlong)serial,
ptr_to_jlong(surface), wl_fixed_to_double(x), wl_fixed_to_double(y));
EXCEPTION_CLEAR(env);
}
static void wl_data_device_handle_leave(void *user, struct wl_data_device *wl_data_device) {
struct DataDevice* dataDevice = user;
assert(dataDevice != NULL);
JNIEnv *env = getEnv();
assert(env != NULL);
(*env)->CallVoidMethod(env, dataDevice->javaObject, wlDataDeviceHandleDnDLeaveMID);
EXCEPTION_CLEAR(env);
}
static void wl_data_device_handle_motion(
void *user, struct wl_data_device *wl_data_device, uint32_t time, wl_fixed_t x, wl_fixed_t y) {
struct DataDevice* dataDevice = user;
assert(dataDevice != NULL);
JNIEnv *env = getEnv();
assert(env != NULL);
(*env)->CallVoidMethod(env, dataDevice->javaObject, wlDataDeviceHandleDnDMotionMID, (jlong)time, (jdouble)x / 256.0,
(jdouble)y / 256.0);
EXCEPTION_CLEAR(env);
}
static void wl_data_device_handle_drop(void *user, struct wl_data_device *wl_data_device) {
struct DataDevice* dataDevice = user;
assert(dataDevice != NULL);
JNIEnv *env = getEnv();
assert(env != NULL);
(*env)->CallVoidMethod(env, dataDevice->javaObject, wlDataDeviceHandleDnDDropMID);
EXCEPTION_CLEAR(env);
}
static void
wl_data_device_handle_selection(void *user, struct wl_data_device *wl_data_device, struct wl_data_offer *id) {
struct DataDevice* dataDevice = user;
assert(dataDevice != NULL);
struct DataOffer *offer = NULL;
// id can be NULL, this means that the selection was cleared
if (id != NULL) {
offer = (struct DataOffer *)wl_data_offer_get_user_data(id);
assert(offer != NULL);
}
DataOffer_callSelectionHandler(dataDevice, offer, DATA_TRANSFER_PROTOCOL_WAYLAND);
}
static void
zwp_primary_selection_device_handle_data_offer(void *user,
struct zwp_primary_selection_device_v1 *zwp_primary_selection_device_v1,
struct zwp_primary_selection_offer_v1 *id) {
struct DataDevice* dataDevice = user;
struct DataOffer *offer = DataOffer_create(dataDevice, DATA_TRANSFER_PROTOCOL_PRIMARY_SELECTION, id);
if (offer == NULL) {
// This can only happen in OOM scenarios.
// We can't throw a Java exception here.
// Destroy the offer, since we won't be able to do anything useful with it.
zwp_primary_selection_offer_v1_destroy(id);
return;
}
// no memory leak here: allocated DataOffer object will be
// associated with the zwp_primary_selection_offer_v1
}
static void
zwp_primary_selection_device_handle_selection(void *user,
struct zwp_primary_selection_device_v1 *zwp_primary_selection_device_v1,
struct zwp_primary_selection_offer_v1 *id) {
struct DataDevice* dataDevice = user;
assert(dataDevice != NULL);
struct DataOffer *offer = NULL;
// id can be NULL, this means that the selection was cleared
if (id != NULL) {
offer = (struct DataOffer *)zwp_primary_selection_offer_v1_get_user_data(id);
assert(offer != NULL);
}
DataOffer_callSelectionHandler(dataDevice, offer, DATA_TRANSFER_PROTOCOL_PRIMARY_SELECTION);
}
// JNI functions
JNIEXPORT void JNICALL Java_sun_awt_wl_WLDataDevice_initIDs(JNIEnv* env, jclass clazz)
{
if (!initJavaRefs(env)) {
JNU_ThrowInternalError(env, "Failed to initialize WLDataDevice java refs");
}
}
JNIEXPORT jlong JNICALL Java_sun_awt_wl_WLDataDevice_initNative(JNIEnv *env, jobject obj, jlong wlSeatPtr) {
struct wl_seat* seat = (wlSeatPtr == 0) ? wl_seat : (struct wl_seat*)jlong_to_ptr(wlSeatPtr);
struct DataDevice *dataDevice = calloc(1, sizeof(struct DataDevice));
if (dataDevice == NULL) {
JNU_ThrowOutOfMemoryError(env, "Failed to allocate DataDevice");
return 0;
}
dataDevice->javaObject = (*env)->NewGlobalRef(env, obj);
if ((*env)->ExceptionCheck(env)) {
goto error_cleanup;
}
if (dataDevice->javaObject == NULL) {
JNU_ThrowInternalError(env, "Failed to initialize WLDataDevice");
goto error_cleanup;
}
dataDevice->wlDataDevice = wl_data_device_manager_get_data_device(wl_ddm, seat);
if (dataDevice->wlDataDevice == NULL) {
JNU_ThrowInternalError(env, "Couldn't get a Wayland data device");
goto error_cleanup;
}
dataDevice->zwpPrimarySelectionDevice = NULL;
if (zwp_selection_dm != NULL) {
dataDevice->zwpPrimarySelectionDevice = zwp_primary_selection_device_manager_v1_get_device(zwp_selection_dm, seat);
if (dataDevice->zwpPrimarySelectionDevice == NULL) {
JNU_ThrowInternalError(env, "Couldn't get zwp_primary_selection_device");
goto error_cleanup;
}
}
dataDevice->dataTransferQueue = wl_display_create_queue(wl_display);
if (dataDevice->dataTransferQueue == NULL) {
JNU_ThrowInternalError(env, "Couldn't create an event queue for the data device");
goto error_cleanup;
}
wl_data_device_add_listener(dataDevice->wlDataDevice, &wlDataDeviceListener, dataDevice);
if (dataDevice->zwpPrimarySelectionDevice != NULL) {
zwp_primary_selection_device_v1_add_listener(dataDevice->zwpPrimarySelectionDevice, &zwpPrimarySelectionDeviceListener,
dataDevice);
}
return ptr_to_jlong(dataDevice);
error_cleanup:
if (dataDevice->dataTransferQueue != NULL) {
wl_event_queue_destroy(dataDevice->dataTransferQueue);
}
if (dataDevice->zwpPrimarySelectionDevice != NULL) {
zwp_primary_selection_device_v1_destroy(dataDevice->zwpPrimarySelectionDevice);
}
if (dataDevice->wlDataDevice != NULL) {
wl_data_device_destroy(dataDevice->wlDataDevice);
}
if (dataDevice->javaObject != NULL) {
(*env)->DeleteGlobalRef(env, dataDevice->javaObject);
}
if (dataDevice != NULL) {
free(dataDevice);
}
return 0;
}
JNIEXPORT jboolean JNICALL Java_sun_awt_wl_WLDataDevice_isProtocolSupportedImpl(JNIEnv *env, jclass clazz, jlong nativePtr, jint protocol) {
struct DataDevice *dataDevice = jlong_to_ptr(nativePtr);
assert(dataDevice != NULL);
if (protocol == DATA_TRANSFER_PROTOCOL_WAYLAND) {
return true;
}
if (protocol == DATA_TRANSFER_PROTOCOL_PRIMARY_SELECTION) {
return dataDevice->zwpPrimarySelectionDevice != NULL;
}
return false;
}
JNIEXPORT void JNICALL Java_sun_awt_wl_WLDataDevice_dispatchDataTransferQueueImpl(JNIEnv *env, jclass clazz, jlong nativePtr) {
struct DataDevice *dataDevice = jlong_to_ptr(nativePtr);
assert(dataDevice != NULL);
while (wl_display_dispatch_queue(wl_display, dataDevice->dataTransferQueue) != -1) {
}
}
JNIEXPORT void JNICALL Java_sun_awt_wl_WLDataDevice_setSelectionImpl(JNIEnv *env,
jclass clazz,
jint protocol,
jlong dataDeviceNativePtr,
jlong dataSourceNativePtr,
jlong serial) {
struct DataDevice *dataDevice = jlong_to_ptr(dataDeviceNativePtr);
assert(dataDevice != NULL);
struct DataSource *source = jlong_to_ptr(dataSourceNativePtr);
if (protocol == DATA_TRANSFER_PROTOCOL_WAYLAND) {
wl_data_device_set_selection(dataDevice->wlDataDevice, (source == NULL) ? NULL : source->wlDataSource, serial);
} else if (protocol == DATA_TRANSFER_PROTOCOL_PRIMARY_SELECTION) {
zwp_primary_selection_device_v1_set_selection(
dataDevice->zwpPrimarySelectionDevice, (source == NULL) ? NULL : source->zwpPrimarySelectionSource, serial);
}
}
JNIEXPORT jlong JNICALL Java_sun_awt_wl_WLDataSource_initNative(JNIEnv *env, jobject javaObject, jlong dataDeviceNativePtr, jint protocol) {
struct DataDevice *dataDevice = jlong_to_ptr(dataDeviceNativePtr);
assert(dataDevice != NULL);
struct DataSource *dataSource = calloc(1, sizeof(struct DataSource));
if (dataSource == NULL) {
JNU_ThrowOutOfMemoryError(env, "Failed to allocate DataSource");
return 0;
}
// cleaned up in WLDataSource.destroy()
dataSource->javaObject = (*env)->NewGlobalRef(env, javaObject);
if ((*env)->ExceptionCheck(env)) {
free(dataSource);
return 0;
}
if (dataSource->javaObject == NULL) {
free(dataSource);
JNU_ThrowInternalError(env, "Failed to create a reference to WLDataSource");
return 0;
}
if (protocol == DATA_TRANSFER_PROTOCOL_PRIMARY_SELECTION && !zwp_selection_dm) {
protocol = DATA_TRANSFER_PROTOCOL_WAYLAND;
}
if (protocol == DATA_TRANSFER_PROTOCOL_WAYLAND) {
struct wl_data_source *wlDataSource = wl_data_device_manager_create_data_source(wl_ddm);
if (wlDataSource == NULL) {
free(dataSource);
JNU_ThrowByName(env, "java/awt/AWTError", "Wayland error creating wl_data_source proxy");
return 0;
}
wl_proxy_set_queue((struct wl_proxy *)wlDataSource, dataDevice->dataTransferQueue);
wl_data_source_add_listener(wlDataSource, &wl_data_source_listener, dataSource);
dataSource->protocol = DATA_TRANSFER_PROTOCOL_WAYLAND;
dataSource->wlDataSource = wlDataSource;
}
if (protocol == DATA_TRANSFER_PROTOCOL_PRIMARY_SELECTION) {
struct zwp_primary_selection_source_v1 *zwpPrimarySelectionSource =
zwp_primary_selection_device_manager_v1_create_source(zwp_selection_dm);
if (zwpPrimarySelectionSource == NULL) {
free(dataSource);
JNU_ThrowByName(env, "java/awt/AWTError", "Wayland error creating zwp_primary_selection_source_v1 proxy");
return 0;
}
wl_proxy_set_queue((struct wl_proxy *)zwpPrimarySelectionSource, dataDevice->dataTransferQueue);
zwp_primary_selection_source_v1_add_listener(zwpPrimarySelectionSource,
&zwp_primary_selection_source_v1_listener, dataSource);
dataSource->protocol = DATA_TRANSFER_PROTOCOL_PRIMARY_SELECTION;
dataSource->zwpPrimarySelectionSource = zwpPrimarySelectionSource;
}
return ptr_to_jlong(dataSource);
}
JNIEXPORT void JNICALL Java_sun_awt_wl_WLDataSource_offerMimeImpl(JNIEnv *env,
jclass clazz,
jlong nativePtr,
jstring mimeJavaString) {
struct DataSource *source = jlong_to_ptr(nativePtr);
const char *mime = (*env)->GetStringUTFChars(env, mimeJavaString, NULL);
JNU_CHECK_EXCEPTION(env);
DataSource_offer(source, mime);
(*env)->ReleaseStringUTFChars(env, mimeJavaString, mime);
}
JNIEXPORT void JNICALL Java_sun_awt_wl_WLDataSource_destroyImpl(JNIEnv *env, jclass clazz, jlong nativePtr) {
struct DataSource *source = jlong_to_ptr(nativePtr);
if (source == NULL) {
return;
}
if (source->javaObject != NULL) {
(*env)->DeleteGlobalRef(env, source->javaObject);
source->javaObject = NULL;
}
if (source->protocol == DATA_TRANSFER_PROTOCOL_WAYLAND) {
wl_data_source_destroy(source->wlDataSource);
} else if (source->protocol == DATA_TRANSFER_PROTOCOL_PRIMARY_SELECTION) {
zwp_primary_selection_source_v1_destroy(source->zwpPrimarySelectionSource);
}
free(source);
}
JNIEXPORT void JNICALL Java_sun_awt_wl_WLDataSource_setDnDActionsImpl(JNIEnv *env,
jclass clazz,
jlong nativePtr,
jint actions) {
struct DataSource *source = jlong_to_ptr(nativePtr);
DataSource_setDnDActions(source, actions);
}
JNIEXPORT void JNICALL Java_sun_awt_wl_WLDataOffer_destroyImpl(JNIEnv *env, jclass clazz, jlong nativePtr) {
struct DataOffer *offer = jlong_to_ptr(nativePtr);
DataOffer_destroy(offer);
}
JNIEXPORT void JNICALL Java_sun_awt_wl_WLDataOffer_acceptImpl(
JNIEnv *env, jclass clazz, jlong nativePtr, jlong serial, jstring mimeJavaString) {
struct DataOffer *offer = jlong_to_ptr(nativePtr);
const char *mime = NULL;
if (mimeJavaString != NULL) {
mime = (*env)->GetStringUTFChars(env, mimeJavaString, NULL);
JNU_CHECK_EXCEPTION(env);
}
DataOffer_accept(offer, (uint32_t)serial, mime);
if (mime != NULL) {
(*env)->ReleaseStringUTFChars(env, mimeJavaString, mime);
}
}
JNIEXPORT jint JNICALL Java_sun_awt_wl_WLDataOffer_openReceivePipe(JNIEnv *env,
jclass clazz,
jlong nativePtr,
jstring mimeJavaString) {
struct DataOffer *offer = jlong_to_ptr(nativePtr);
const char *mime = (*env)->GetStringUTFChars(env, mimeJavaString, NULL);
JNU_CHECK_EXCEPTION_RETURN(env, -1);
int fds[2];
if (pipe2(fds, O_CLOEXEC) != 0) {
JNU_ThrowIOExceptionWithMessageAndLastError(env, "pipe2");
return -1;
}
DataOffer_receive(offer, mime, fds[1]);
// Flush the request to receive
wlFlushToServer(env);
close(fds[1]);
(*env)->ReleaseStringUTFChars(env, mimeJavaString, mime);
return fds[0];
}
JNIEXPORT void JNICALL Java_sun_awt_wl_WLDataOffer_finishDnDImpl(JNIEnv *env, jclass clazz, jlong nativePtr) {
struct DataOffer *offer = jlong_to_ptr(nativePtr);
DataOffer_finishDnD(offer);
}
JNIEXPORT void JNICALL Java_sun_awt_wl_WLDataOffer_setDnDActionsImpl(
JNIEnv *env, jclass clazz, jlong nativePtr, jint actions, jint preferredAction) {
struct DataOffer *offer = jlong_to_ptr(nativePtr);
DataOffer_setDnDActions(offer, actions, preferredAction);
}