aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDavid Robillard <d@drobilla.net>2022-05-22 16:02:04 -0400
committerDavid Robillard <d@drobilla.net>2023-11-11 10:20:03 -0500
commit91ba4a52701db0a43ffc7769d2fda510ca2ebfa3 (patch)
treebe2dca91d457f28e0c9c324736f24d85a5ed57c2
parent91051e9059b67b8d633e385afb48a36d4f9467ba (diff)
downloadpugl-91ba4a52701db0a43ffc7769d2fda510ca2ebfa3.tar.gz
pugl-91ba4a52701db0a43ffc7769d2fda510ca2ebfa3.tar.bz2
pugl-91ba4a52701db0a43ffc7769d2fda510ca2ebfa3.zip
[WIP] Add support for drag and dropdragdrop
-rw-r--r--bindings/cpp/include/pugl/pugl.hpp28
-rw-r--r--doc/c/clipboards.rst119
-rw-r--r--examples/pugl_clipboard_demo.c8
-rw-r--r--include/pugl/pugl.h40
-rw-r--r--src/common.c1
-rw-r--r--src/mac.m204
-rw-r--r--src/win.c99
-rw-r--r--src/win.h2
-rw-r--r--src/x11.c310
-rw-r--r--src/x11.h14
-rw-r--r--test/test_utils.h2
11 files changed, 717 insertions, 110 deletions
diff --git a/bindings/cpp/include/pugl/pugl.hpp b/bindings/cpp/include/pugl/pugl.hpp
index d691123..a3c26e0 100644
--- a/bindings/cpp/include/pugl/pugl.hpp
+++ b/bindings/cpp/include/pugl/pugl.hpp
@@ -641,8 +641,14 @@ public:
puglSetCursor(cobj(), static_cast<PuglCursor>(cursor)));
}
+ /// @copydoc puglRegisterDragType
+ Status registerDragType(const char* const type)
+ {
+ return static_cast<Status>(puglRegisterDragType(cobj(), type));
+ }
+
/// @copydoc puglGetNumClipboardTypes
- uint32_t numClipboardTypes(const Clipboard clipboard) const
+ size_t numClipboardTypes(const Clipboard clipboard) const
{
return puglGetNumClipboardTypes(cobj(), clipboard);
}
@@ -693,6 +699,26 @@ public:
}
/**
+ Reject data offered from a clipboard.
+
+ This can be called instead of puglAcceptOffer() to explicitly reject the
+ offer. Note that drag-and-drop will still work if this isn't called, but
+ applications should always explicitly accept or reject each data offer for
+ optimal behaviour.
+
+ @param offer The data offer event.
+
+ @param region The region of the view that will refuse this drop. This may
+ be used by the system to avoid sending redundant events when the item is
+ dragged within the region. This is only an optimization, an all-zero
+ region can safely be passed.
+ */
+ Status rejectOffer(const DataOfferEvent& offer, const Rect& region)
+ {
+ return static_cast<Status>(puglRejectOffer(cobj(), &offer, region));
+ }
+
+ /**
Activate a repeating timer event.
This starts a timer which will send a timer event to `view` every
diff --git a/doc/c/clipboards.rst b/doc/c/clipboards.rst
index ca8b4ad..5e2a771 100644
--- a/doc/c/clipboards.rst
+++ b/doc/c/clipboards.rst
@@ -5,55 +5,46 @@
Using Clipboards
################
-Clipboards provide a way to transfer data between different views,
+Clipboards provide a way to transfer information between different views,
including views in different processes.
-A clipboard transfer is a multi-step event-driven process,
-where the sender and receiver can negotiate a mutually supported data format.
-*******
-Copying
-*******
+In Pugl, both "copy and paste" and "drag and drop" interactions are supported by the same clipboard mechanism.
+Several functions are used for both,
+and take a :enum:`PuglClipboard` enumerator to distinguish which clipboard the operation applies to.
-Data can be copied to the general clipboard with :func:`puglSetClipboard`.
-The `MIME type <https://www.iana.org/assignments/media-types/media-types.xhtml>`_ of the data must be specified.
-Commonly supported types are ``text/plain`` for plain text,
-and `text/uri-list <http://amundsen.com/hypermedia/urilist/>`_ for lists of URIs (including local files).
+Because these interactions support transfer of data between processes and negotiation of the data type,
+each is a multi-step process.
+As with everything, events are used to notify the application about relevant changes.
-For example, a string can be copied to the clipboard by setting the general clipboard to ``text/plain`` data:
+*************
+Drag and Drop
+*************
-.. code-block:: c
-
- const char* someString = "Copied string";
+To enable support for receiving a particular type of dragged data,
+:func:`puglRegisterDragType` must be called during setup to register a MIME type.
+For example, the common case of accepting a list of files is supported by the `text/uri-list <http://amundsen.com/hypermedia/urilist/>`_ type.
- puglSetClipboard(view,
- "text/plain",
- someString,
- strlen(someString));
+.. code-block:: c
-*******
-Pasting
-*******
+ puglRegisterDragType(view, "text/uri-list");
-Data from a clipboard can be pasted to a view using :func:`puglPaste`:
+On Windows, this is the only type that is currently supported.
+On MacOS and X11, other data types can be used,
+such as ``text/plain`` (which is implicitly UTF-8 encoded),
+or ``application/zip``:
.. code-block:: c
- puglPaste(view);
-
-This initiates a data transfer from the clipboard to the view if possible.
-If data is available,
-the view will be sent a :enumerator:`PUGL_DATA_OFFER` event to begin the transfer.
-
-**************
-Receiving Data
-**************
+ puglRegisterDragType(view, "text/plain");
+ puglRegisterDragType(view, "application/zip");
-A data transfer from a clipboard to a view begins with the view receiving a :enumerator:`PUGL_DATA_OFFER` event.
-This indicates that data (possibly in several formats) is being offered to a view,
-which can either "accept" or "reject" it:
+Receiving dropped data begins by receiving a :enumerator:`PUGL_DATA_OFFER` event.
+This event signals that data is being "offered" by being dragged (but not yet dropped) over the view:
.. code-block:: c
+ // ...
+
case PUGL_DATA_OFFER:
onDataOffer(view, &event->offer);
break;
@@ -69,18 +60,18 @@ When handling this event,
PuglClipboard clipboard = event->clipboard;
size_t numTypes = puglGetNumClipboardTypes(view, clipboard);
- for (uint32_t t = 0; t < numTypes; ++t) {
+ for (size_t t = 0; t < numTypes; ++t) {
const char* type = puglGetClipboardType(view, clipboard, t);
printf("Offered type: %s\n", type);
}
}
-If the view supports one of the data types,
-it can accept the offer with :func:`puglAcceptOffer`:
+If the view supports dropping one of the data types at the specified cursor location,
+it can accept the drop with :func:`puglAcceptOffer`:
.. code-block:: c
- for (uint32_t t = 0; t < numTypes; ++t) {
+ for (size_t t = 0; t < numTypes; ++t) {
const char* type = puglGetClipboardType(view, clipboard, t);
if (!strcmp(type, "text/uri-list")) {
puglAcceptOffer(view,
@@ -90,17 +81,21 @@ it can accept the offer with :func:`puglAcceptOffer`:
}
}
-An :enum:`action <PuglAction>` and view region must be given,
-which the window system may use to optimize the process and/or provide user feedback.
+This process will happen repeatedly while the user drags the item around the view.
+Different actions may be given which may affect how the drag is presented to the user,
+for example by changing the mouse cursor.
+The last argument specifies the region of the view which this response applies to,
+which may be used as an optimization to send fewer events.
+It is safe, though possibly sub-optimal, to simply specify the entire frame as is done above.
-When an offer is accepted,
-the data will be transferred and converted if necessary,
-then the view will be sent a :enumerator:`PUGL_DATA` event.
-When the data event is received,
-the data can be fetched with :func:`puglGetClipboard`:
+When the item is dropped,
+Pugl will transfer the data in the appropriate datatype behind the scenes,
+and send a :enumerator:`PUGL_DATA` event to signal that the data is ready to be fetched with :func:`puglGetClipboard`:
.. code-block:: c
+ // ...
+
case PUGL_DATA:
onData(view, &event->data);
break;
@@ -124,3 +119,37 @@ the data can be fetched with :func:`puglGetClipboard`:
printf("Dropped: %s\n", (const char*)data);
}
}
+
+**************
+Copy and Paste
+**************
+
+Data can be copied to the "general" clipboard with :func:`puglSetClipboard`:
+
+.. code-block:: c
+
+ // ...
+
+ if ((event->state & PUGL_MOD_CTRL) && event->key == 'c') {
+ const char* someString = /* ... */;
+
+ puglSetClipboard(view,
+ PUGL_CLIPBOARD_GENERAL,
+ "text/plain",
+ someString,
+ strlen(someString) + 1);
+ }
+
+Pasting data works nearly the same way as receiving dropped data,
+except the events use :enumerator:`PUGL_CLIPBOARD_GENERAL` instead of :enumerator:`PUGL_CLIPBOARD_DRAG`.
+Unlike dropping, however, the receiving application must itself initiate the transfer,
+using :func:`puglPaste`:
+
+.. code-block:: c
+
+ if ((event->state & PUGL_MOD_CTRL) && event->key == 'v') {
+ puglPaste(view);
+ }
+
+This will result in a :enumerator:`PUGL_DATA_OFFER` event being sent as above,
+which must be accepted to ultimately receive the data in the desired data type.
diff --git a/examples/pugl_clipboard_demo.c b/examples/pugl_clipboard_demo.c
index 08227d1..01e96dc 100644
--- a/examples/pugl_clipboard_demo.c
+++ b/examples/pugl_clipboard_demo.c
@@ -81,10 +81,10 @@ static void
onDataOffer(PuglView* view, const PuglDataOfferEvent* event)
{
const PuglClipboard clipboard = event->clipboard;
- const uint32_t numTypes = puglGetNumClipboardTypes(view, clipboard);
+ const size_t numTypes = puglGetNumClipboardTypes(view, clipboard);
// Print all offered types to be useful as a testing program
- fprintf(stderr, "Offered %u types:\n", numTypes);
+ fprintf(stderr, "Offered %zu types:\n", numTypes);
for (uint32_t t = 0; t < numTypes; ++t) {
const char* type = puglGetClipboardType(view, clipboard, t);
fprintf(stderr, "\t%s\n", type);
@@ -230,12 +230,16 @@ main(int argc, char** argv)
puglSetSizeHint(view, PUGL_MIN_SIZE, 128, 128);
puglSetBackend(view, puglGlBackend());
+ puglRegisterDragType(view, "text/plain");
+ puglRegisterDragType(view, "text/uri-list");
+
puglSetViewHint(view, PUGL_CONTEXT_DEBUG, opts.errorChecking);
puglSetViewHint(view, PUGL_RESIZABLE, opts.resizable);
puglSetViewHint(view, PUGL_SAMPLES, opts.samples);
puglSetViewHint(view, PUGL_DOUBLE_BUFFER, opts.doubleBuffer);
puglSetViewHint(view, PUGL_SWAP_INTERVAL, opts.sync);
puglSetViewHint(view, PUGL_IGNORE_KEY_REPEAT, opts.ignoreKeyRepeat);
+ puglSetViewHint(view, PUGL_ACCEPT_DROP, true);
puglSetHandle(view, &app.cube);
puglSetEventFunc(view, onEvent);
diff --git a/include/pugl/pugl.h b/include/pugl/pugl.h
index e57f6aa..89415f6 100644
--- a/include/pugl/pugl.h
+++ b/include/pugl/pugl.h
@@ -152,10 +152,12 @@ typedef enum {
A system clipboard.
A clipboard provides a mechanism for transferring data between views,
- including views in different processes.
+ including views in different processes. Clipboards are used for both "copy
+ and paste" and "drag and drop" interactions.
*/
typedef enum {
PUGL_CLIPBOARD_GENERAL, ///< General clipboard for copy/pasted data
+ PUGL_CLIPBOARD_DRAG, ///< Drag clipboard for drag and drop data
} PuglClipboard;
/**
@@ -977,10 +979,11 @@ typedef enum {
PUGL_REFRESH_RATE, ///< Refresh rate in Hz
PUGL_VIEW_TYPE, ///< View type (a #PuglViewType)
PUGL_DARK_FRAME, ///< True if window frame should be dark
+ PUGL_ACCEPT_DROP, ///< True if view accepts dropped data
} PuglViewHint;
/// The number of #PuglViewHint values
-#define PUGL_NUM_VIEW_HINTS ((unsigned)PUGL_DARK_FRAME + 1U)
+#define PUGL_NUM_VIEW_HINTS ((unsigned)PUGL_ACCEPT_DROP + 1U)
/// A special view hint value
typedef enum {
@@ -1195,6 +1198,16 @@ double
puglGetScaleFactor(const PuglView* view);
/**
+ Register a type as supported for drag and drop.
+
+ Before realizing the view, this function should be called for every type the
+ view may accept as a drop target.
+*/
+PUGL_API
+PuglStatus
+puglRegisterDragType(PuglView* view, const char* type);
+
+/**
@}
@defgroup pugl_frame Frame
Functions for working with the position and size of a view.
@@ -1569,6 +1582,29 @@ puglAcceptOffer(PuglView* view,
PuglRect region);
/**
+ Reject data offered from a clipboard.
+
+ This can be called instead of puglAcceptOffer() to explicitly reject the
+ offer. Note that drag-and-drop will still work if this isn't called, but
+ applications should always explicitly accept or reject each data offer for
+ optimal behaviour.
+
+ @param view The view.
+
+ @param offer The data offer event.
+
+ @param region The region of the view that will refuse this drop. This may
+ be used by the system to avoid sending redundant events when the item is
+ dragged within the region. This is only an optimization, an all-zero region
+ can safely be passed.
+*/
+PUGL_API
+PuglStatus
+puglRejectOffer(PuglView* view,
+ const PuglDataOfferEvent* offer,
+ PuglRect region);
+
+/**
Set the clipboard contents.
This sets the system clipboard contents, which can be retrieved with
diff --git a/src/common.c b/src/common.c
index 46b2f3d..a313eff 100644
--- a/src/common.c
+++ b/src/common.c
@@ -127,6 +127,7 @@ puglSetDefaultHints(PuglHints hints)
hints[PUGL_IGNORE_KEY_REPEAT] = PUGL_FALSE;
hints[PUGL_REFRESH_RATE] = PUGL_DONT_CARE;
hints[PUGL_VIEW_TYPE] = PUGL_DONT_CARE;
+ hints[PUGL_ACCEPT_DROP] = PUGL_DONT_CARE;
}
PuglView*
diff --git a/src/mac.m b/src/mac.m
index 107cadb..f09081a 100644
--- a/src/mac.m
+++ b/src/mac.m
@@ -363,6 +363,111 @@ dispatchCurrentChildViewConfiguration(PuglView* const view)
reshaped = true;
}
+- (NSDragOperation)draggingEntered:(id<NSDraggingInfo>)sender
+{
+ dragSource = sender;
+ dragOperation = NSDragOperationNone;
+ droppedUriList = nil;
+
+ const NSPoint wloc = [sender draggingLocation];
+ const NSPoint rloc = {0, 0}; // FIXME
+ const PuglEventDataOffer offer = {
+ PUGL_DATA_OFFER,
+ 0,
+ mach_absolute_time() / 1e9,
+ wloc.x,
+ wloc.y,
+ rloc.x,
+ [[NSScreen mainScreen] frame].size.height - rloc.y,
+ PUGL_CLIPBOARD_DRAG,
+ };
+
+ PuglEvent offerEvent;
+ offerEvent.offer = offer;
+ puglDispatchEvent(puglview, &offerEvent);
+ return self->dragOperation;
+}
+
+- (NSDragOperation)draggingUpdated:(id<NSDraggingInfo>)sender
+{
+ assert(dragSource == sender);
+
+ const NSPoint wloc = [sender draggingLocation];
+ const NSPoint rloc = {0, 0}; // FIXME
+ const PuglEventDataOffer offer = {
+ PUGL_DATA_OFFER,
+ 0,
+ mach_absolute_time() / 1e9,
+ wloc.x,
+ wloc.y,
+ rloc.x,
+ [[NSScreen mainScreen] frame].size.height - rloc.y,
+ PUGL_CLIPBOARD_DRAG,
+ };
+
+ PuglEvent offerEvent;
+ offerEvent.offer = offer;
+ puglDispatchEvent(puglview, &offerEvent);
+ return self->dragOperation;
+}
+
+- (void)draggingEnded:(id<NSDraggingInfo>)sender
+{
+ NSPasteboard* const pasteboard = [sender draggingPasteboard];
+ const NSPoint wloc = [sender draggingLocation];
+ const NSPoint rloc = {0, 0}; // FIXME
+
+ const NSArray<NSPasteboardType>* const types = [pasteboard types];
+ if (dragTypeIndex >= [types count]) {
+ return;
+ }
+
+ NSString* const uti = [types objectAtIndex:dragTypeIndex];
+ if ([uti isEqualToString:@"public.file-url"] ||
+ [uti isEqualToString:@"com.apple.pasteboard.promised-file-url"]) {
+ // Convert file URI items into a single text/uri-list
+ droppedUriList = [NSString string];
+ for (const NSPasteboardItem* item in [pasteboard pasteboardItems]) {
+ NSString* const value = [item stringForType:uti];
+ if (!value) {
+ continue;
+ }
+
+ NSURL* const idUri = [NSURL URLWithString:value];
+ const char* const pathRep = [idUri fileSystemRepresentation];
+ NSString* const path = [NSString stringWithUTF8String:pathRep];
+ NSString* const pathUri = [[NSURL fileURLWithPath:path] absoluteString];
+
+ droppedUriList = [droppedUriList stringByAppendingString:pathUri];
+ droppedUriList = [droppedUriList stringByAppendingFormat:@"\n"];
+ }
+ }
+
+ const PuglEventData data = {
+ PUGL_DATA,
+ 0,
+ mach_absolute_time() / 1e9,
+ wloc.x,
+ wloc.y,
+ rloc.x,
+ [[NSScreen mainScreen] frame].size.height - rloc.y,
+ PUGL_CLIPBOARD_DRAG,
+ (uint32_t)dragTypeIndex, // FIXME: ?
+ };
+
+ PuglEvent dataEvent;
+ dataEvent.data = data;
+ puglDispatchEvent(puglview, &dataEvent);
+
+ dragSource = nil;
+}
+
+- (void)draggingExited:(id<NSDraggingInfo>)sender
+{
+ assert(dragSource == sender);
+ dragSource = nil;
+}
+
static uint32_t
getModifiers(const NSEvent* const ev)
{
@@ -1867,14 +1972,63 @@ puglSetTransientParent(PuglView* view, PuglNativeView parent)
return PUGL_FAILURE;
}
+static void
+addRegisteredDragType(PuglView* const view, NSString* uti)
+{
+ if (!view->impl->registeredDragTypes) {
+ view->impl->registeredDragTypes = [[NSMutableArray alloc] init];
+ }
+
+ [view->impl->registeredDragTypes addObject:uti];
+}
+
+PuglStatus
+puglRegisterDragType(PuglView* const view, const char* const type)
+{
+ PuglWrapperView* const wrapper = view->impl->wrapperView;
+
+ CFStringRef mimeType =
+ CFStringCreateWithCString(NULL, type, kCFStringEncodingUTF8);
+
+ // First register any types in the internal map that map to this MIME type
+ bool registered = false;
+ for (const Datatype* datatype = datatypes; datatype->mimeType; ++datatype) {
+ if (!strcmp(type, datatype->mimeType)) {
+ NSString* uti = [NSString stringWithUTF8String:datatype->uti];
+
+ addRegisteredDragType(view, uti);
+ registered = true;
+ }
+ }
+
+ // Try to get the UTI from the system
+ if (!registered) {
+ NSString* uti = utiForMimeType((__bridge NSString*)mimeType);
+ if (uti) {
+ addRegisteredDragType(view, uti);
+ }
+ }
+
+ if (wrapper) {
+ [wrapper registerForDraggedTypes:view->impl->registeredDragTypes];
+ }
+
+ return PUGL_SUCCESS;
+}
+
static NSPasteboard*
getPasteboard(const PuglView* const view, const PuglClipboard clipboard)
{
- (void)view;
-
switch (clipboard) {
case PUGL_CLIPBOARD_GENERAL:
return [NSPasteboard generalPasteboard];
+
+ case PUGL_CLIPBOARD_DRAG:
+ if (view->impl->wrapperView->dragSource) {
+ return [view->impl->wrapperView->dragSource draggingPasteboard];
+ }
+
+ break;
}
return NULL;
@@ -1883,10 +2037,13 @@ getPasteboard(const PuglView* const view, const PuglClipboard clipboard)
PuglStatus
puglPaste(PuglView* const view)
{
- const PuglDataOfferEvent offer = {
+ const PuglEventDataOffer offer = {
PUGL_DATA_OFFER,
0,
puglGetTime(view->world),
+ 0,
+ 0,
+ PUGL_CLIPBOARD_GENERAL,
};
PuglEvent offerEvent;
@@ -1895,19 +2052,19 @@ puglPaste(PuglView* const view)
return PUGL_SUCCESS;
}
-uint32_t
+size_t
puglGetNumClipboardTypes(const PuglView* const view,
const PuglClipboard clipboard)
{
NSPasteboard* const pasteboard = getPasteboard(view, clipboard);
- return pasteboard ? (uint32_t)[[pasteboard types] count] : 0;
+ return pasteboard ? [[pasteboard types] count] : 0;
}
const char*
-puglGetClipboardType(const PuglView* const view,
- const PuglClipboard clipboard,
- const uint32_t typeIndex)
+puglGetClipboardType(PuglView* const view,
+ const PuglClipboard clipboard,
+ const size_t typeIndex)
{
NSPasteboard* const pasteboard = getPasteboard(view, clipboard);
if (!pasteboard) {
@@ -1927,16 +2084,16 @@ puglGetClipboardType(const PuglView* const view,
}
static NSDragOperation
-getDragOperation(const PuglAction action)
+getDragOperation(const PuglDropAction action)
{
switch (action) {
- case PUGL_ACTION_COPY:
+ case PUGL_DROP_ACTION_COPY:
return NSDragOperationCopy;
- case PUGL_ACTION_LINK:
+ case PUGL_DROP_ACTION_LINK:
return NSDragOperationLink;
- case PUGL_ACTION_MOVE:
+ case PUGL_DROP_ACTION_MOVE:
return NSDragOperationMove;
- case PUGL_ACTION_PRIVATE:
+ case PUGL_DROP_ACTION_PRIVATE:
break;
}
@@ -1945,14 +2102,14 @@ getDragOperation(const PuglAction action)
PuglStatus
puglAcceptOffer(PuglView* const view,
- const PuglDataOfferEvent* const offer,
- const uint32_t typeIndex,
- const PuglAction action,
+ const PuglEventDataOffer* const offer,
+ const size_t typeIndex,
+ PuglDropAction action,
const PuglRect region)
{
PuglWrapperView* const wrapper = view->impl->wrapperView;
NSPasteboard* const pasteboard = getPasteboard(view, offer->clipboard);
- if (!pasteboard) {
+ if (!pasteboard || offer->clipboard == PUGL_CLIPBOARD_GENERAL) {
return PUGL_BAD_PARAMETER;
}
@@ -1963,24 +2120,13 @@ puglAcceptOffer(PuglView* const view,
wrapper->dragOperation = getDragOperation(action);
wrapper->dragTypeIndex = typeIndex;
-
- const PuglDataEvent data = {PUGL_DATA,
- 0U,
- puglGetTime(view->world),
- (double)region.x,
- (double)region.y,
- (uint32_t)typeIndex};
-
- PuglEvent dataEvent;
- dataEvent.data = data;
- puglDispatchEvent(view, &dataEvent);
return PUGL_SUCCESS;
}
const void*
puglGetClipboard(PuglView* const view,
const PuglClipboard clipboard,
- const uint32_t typeIndex,
+ const size_t typeIndex,
size_t* const len)
{
*len = 0;
diff --git a/src/win.c b/src/win.c
index 0b850ef..8f929c3 100644
--- a/src/win.c
+++ b/src/win.c
@@ -9,6 +9,8 @@
#include "pugl/pugl.h"
#include <dwmapi.h>
+#include <shellapi.h>
+#include <shlwapi.h>
#include <windows.h>
#include <windowsx.h>
@@ -295,6 +297,10 @@ puglRealize(PuglView* view)
// Set basic window hints and attributes
+ if (view->title) {
+ puglSetWindowTitle(view, view->title);
+ }
+
puglSetViewString(view, PUGL_WINDOW_TITLE, view->strings[PUGL_WINDOW_TITLE]);
puglSetTransientParent(view, view->transientParent);
@@ -314,6 +320,7 @@ puglRealize(PuglView* view)
}
}
+ DragAcceptFiles(impl->hwnd, view->hints[PUGL_ACCEPT_DROP] == PUGL_TRUE);
SetWindowLongPtr(impl->hwnd, GWLP_USERDATA, (LONG_PTR)view);
return puglDispatchSimpleEvent(view, PUGL_REALIZE);
@@ -712,6 +719,7 @@ constrainAspect(const PuglView* const view,
static LRESULT
handleMessage(PuglView* view, UINT message, WPARAM wParam, LPARAM lParam)
{
+ PuglInternals* impl = view->impl;
PuglEvent event = {{PUGL_NOTHING, 0}};
RECT rect = {0, 0, 0, 0};
POINT pt = {0, 0};
@@ -764,6 +772,43 @@ handleMessage(PuglView* view, UINT message, WPARAM wParam, LPARAM lParam)
return TRUE;
}
break;
+ case WM_DROPFILES:
+ if (DragQueryPoint((HDROP)wParam, &pt)) {
+ const HDROP drop = (HDROP)wParam;
+ const UINT numFiles = DragQueryFile(drop, 0xFFFFFFFF, NULL, 0);
+ TCHAR path[MAX_PATH] = {0};
+ TCHAR url[2048] = {0};
+
+ impl->droppedUrisLen = 0;
+ for (UINT i = 0; i < numFiles; ++i) {
+ const UINT pathLen = sizeof(path) / sizeof(TCHAR);
+ if (DragQueryFile(drop, i, path, pathLen)) {
+ DWORD urlLen = sizeof(url) / sizeof(TCHAR);
+ const HRESULT hres = UrlCreateFromPath(path, url, &urlLen, 0);
+ if (!FAILED(hres)) {
+ impl->droppedUris = (char*)realloc(
+ impl->droppedUris, impl->droppedUrisLen + urlLen + 2);
+
+ memcpy(impl->droppedUris + impl->droppedUrisLen, url, urlLen + 1);
+
+ impl->droppedUrisLen += urlLen;
+ impl->droppedUris[impl->droppedUrisLen++] = '\n';
+ impl->droppedUris[impl->droppedUrisLen] = 0;
+ }
+ }
+ }
+ DragFinish(drop);
+
+ POINT rpt = pt;
+ ClientToScreen(view->impl->hwnd, &rpt);
+
+ event.data.type = PUGL_DATA;
+ event.data.time = GetMessageTime() / 1e3;
+ event.data.x = pt.x;
+ event.data.y = pt.y;
+ event.data.clipboard = PUGL_CLIPBOARD_DRAG;
+ }
+ break;
case WM_ENTERSIZEMOVE:
view->resizing = true;
puglDispatchSimpleEvent(view, PUGL_LOOP_ENTER);
@@ -1347,30 +1392,48 @@ puglSetTransientParent(PuglView* view, PuglNativeView parent)
return PUGL_SUCCESS;
}
+PuglStatus
+puglRegisterDragType(PuglView* const PUGL_UNUSED(view), const char* const type)
+{
+ return !strcmp(type, "text/uri-list") ? PUGL_SUCCESS : PUGL_UNSUPPORTED;
+}
+
uint32_t
-puglGetNumClipboardTypes(const PuglView* const PUGL_UNUSED(view),
+puglGetNumClipboardTypes(const PuglView* const view,
const PuglClipboard clipboard)
{
- return (clipboard == PUGL_CLIPBOARD_GENERAL &&
- IsClipboardFormatAvailable(CF_UNICODETEXT))
- ? 1U
- : 0U;
+ switch (clipboard) {
+ case PUGL_CLIPBOARD_GENERAL:
+ return IsClipboardFormatAvailable(CF_UNICODETEXT) ? 1U : 0U;
+ case PUGL_CLIPBOARD_DRAG:
+ return view->impl->droppedUrisLen ? 1U : 0U;
+ default:
+ break;
+ }
+
+ return 0U;
}
const char*
-puglGetClipboardType(const PuglView* const PUGL_UNUSED(view),
+puglGetClipboardType(const PuglView* const view,
const PuglClipboard clipboard,
- const uint32_t PUGL_UNUSED(typeIndex))
+ const size_t typeIndex)
{
- return (clipboard == PUGL_CLIPBOARD_GENERAL && typeIndex == 0 &&
- IsClipboardFormatAvailable(CF_UNICODETEXT))
- ? "text/plain"
- : NULL;
+ switch (clipboard) {
+ case PUGL_CLIPBOARD_GENERAL:
+ return IsClipboardFormatAvailable(CF_UNICODETEXT) ? "text/plain" : NULL;
+ case PUGL_CLIPBOARD_DRAG:
+ return (view->impl->droppedUrisLen && !typeIndex) ? "text/uri-list" : NULL;
+ default:
+ break;
+ }
+
+ return NULL;
}
PuglStatus
puglAcceptOffer(PuglView* const view,
- const PuglDataOfferEvent* const PUGL_UNUSED(offer),
+ const PuglDataOfferEvent* const offer,
const uint32_t typeIndex,
PuglAction PUGL_UNUSED(action),
const PuglRect region)
@@ -1381,11 +1444,12 @@ puglAcceptOffer(PuglView* const view,
const PuglDataEvent data = {
PUGL_DATA,
- 0,
+ 0u,
GetMessageTime() / 1e3,
(double)region.x,
(double)region.y,
- 0,
+ offer->clipboard,
+ typeIndex,
};
PuglEvent dataEvent;
@@ -1402,8 +1466,9 @@ puglGetClipboard(PuglView* const view,
{
PuglInternals* const impl = view->impl;
- if (clipboard != PUGL_CLIPBOARD_GENERAL) {
- return NULL;
+ if (clipboard == PUGL_CLIPBOARD_DRAG) {
+ *len = impl->droppedUrisLen;
+ return impl->droppedUris;
}
if (typeIndex > 0U || !IsClipboardFormatAvailable(CF_UNICODETEXT) ||
@@ -1438,7 +1503,7 @@ puglSetClipboard(PuglView* const view,
{
PuglInternals* const impl = view->impl;
- if (clipboard != PUGL_CLIPBOARD_GENERAL) {
+ if (clipboard == PUGL_CLIPBOARD_DRAG) {
return PUGL_FAILURE;
}
diff --git a/src/win.h b/src/win.h
index 84f1d4e..6136450 100644
--- a/src/win.h
+++ b/src/win.h
@@ -27,6 +27,8 @@ struct PuglInternalsImpl {
WINDOWPLACEMENT oldPlacement;
PAINTSTRUCT paint;
PuglBlob clipboard;
+ char* droppedUris;
+ size_t droppedUrisLen;
PuglSurface* surface;
double scaleFactor;
bool mapped;
diff --git a/src/x11.c b/src/x11.c
index a66ac30..a6f4934 100644
--- a/src/x11.c
+++ b/src/x11.c
@@ -78,6 +78,8 @@
}
#endif
+static const Atom PUGL_DND_PROTOCOL_VERSION = 5;
+
enum WmClientStateMessageAction {
WM_STATE_REMOVE,
WM_STATE_ADD,
@@ -229,8 +231,21 @@ puglInitWorldInternals(const PuglWorldType type, const PuglWorldFlags flags)
impl->atoms.NET_WM_WINDOW_TYPE_UTILITY =
XInternAtom(display, "_NET_WM_WINDOW_TYPE_UTILITY", 0);
- impl->atoms.TARGETS = XInternAtom(display, "TARGETS", 0);
- impl->atoms.text_uri_list = XInternAtom(display, "text/uri-list", 0);
+ impl->atoms.TARGETS = XInternAtom(display, "TARGETS", 0);
+ impl->atoms.XdndActionCopy = XInternAtom(display, "XdndActionCopy", 0);
+ impl->atoms.XdndActionLink = XInternAtom(display, "XdndActionLink", 0);
+ impl->atoms.XdndActionMove = XInternAtom(display, "XdndActionMove", 0);
+ impl->atoms.XdndActionPrivate = XInternAtom(display, "XdndActionPrivate", 0);
+ impl->atoms.XdndAware = XInternAtom(display, "XdndAware", 0);
+ impl->atoms.XdndDrop = XInternAtom(display, "XdndDrop", 0);
+ impl->atoms.XdndEnter = XInternAtom(display, "XdndEnter", 0);
+ impl->atoms.XdndFinished = XInternAtom(display, "XdndFinished", 0);
+ impl->atoms.XdndLeave = XInternAtom(display, "XdndLeave", 0);
+ impl->atoms.XdndPosition = XInternAtom(display, "XdndPosition", 0);
+ impl->atoms.XdndSelection = XInternAtom(display, "XdndSelection", 0);
+ impl->atoms.XdndStatus = XInternAtom(display, "XdndStatus", 0);
+ impl->atoms.XdndTypeList = XInternAtom(display, "XdndTypeList", 0);
+ impl->atoms.text_uri_list = XInternAtom(display, "text/uri-list", 0);
// Open input method
XSetLocaleModifiers("");
@@ -260,6 +275,9 @@ puglInitViewInternals(PuglWorld* const world)
impl->clipboard.clipboard = PUGL_CLIPBOARD_GENERAL;
impl->clipboard.selection = world->impl->atoms.CLIPBOARD;
impl->clipboard.property = XA_PRIMARY;
+ impl->drag.clipboard = PUGL_CLIPBOARD_DRAG;
+ impl->drag.selection = world->impl->atoms.XdndSelection;
+ impl->drag.property = world->impl->atoms.XdndSelection;
#if USE_XCURSOR
impl->cursorName = cursorNames[PUGL_CURSOR_ARROW];
@@ -706,6 +724,16 @@ puglRealize(PuglView* const view)
(XIM)0);
}
+ // DnD Step 0: Set XdndAware property to announce that we support DnD
+ XChangeProperty(display,
+ impl->win,
+ world->impl->atoms.XdndAware,
+ XA_ATOM,
+ 32,
+ PropModeReplace,
+ (const unsigned char*)&PUGL_DND_PROTOCOL_VERSION,
+ 1);
+
st = puglDispatchSimpleEvent(view, PUGL_REALIZE);
/* Flush before returning for two reasons: so that hints are available to the
@@ -955,21 +983,46 @@ getAtomProperty(PuglView* const view,
static PuglX11Clipboard*
getX11Clipboard(PuglView* const view, const PuglClipboard clipboard)
{
- return clipboard == PUGL_CLIPBOARD_GENERAL ? &view->impl->clipboard : NULL;
+ return clipboard == PUGL_CLIPBOARD_DRAG ? &view->impl->drag
+ : &view->impl->clipboard;
}
static const PuglX11Clipboard*
getConstX11Clipboard(const PuglView* const view, const PuglClipboard clipboard)
{
- return clipboard == PUGL_CLIPBOARD_GENERAL ? &view->impl->clipboard : NULL;
+ return clipboard == PUGL_CLIPBOARD_DRAG ? &view->impl->drag
+ : &view->impl->clipboard;
}
static PuglX11Clipboard*
getX11SelectionClipboard(PuglView* const view, const Atom selection)
{
- return (selection == view->world->impl->atoms.CLIPBOARD)
- ? getX11Clipboard(view, PUGL_CLIPBOARD_GENERAL)
- : NULL;
+ if (selection == view->world->impl->atoms.CLIPBOARD) {
+ return getX11Clipboard(view, PUGL_CLIPBOARD_GENERAL);
+ }
+
+ if (selection == view->world->impl->atoms.XdndSelection) {
+ return getX11Clipboard(view, PUGL_CLIPBOARD_DRAG);
+ }
+
+ return NULL;
+}
+
+static Atom
+getX11ActionAtom(const PuglView* const view, const PuglAction action)
+{
+ switch (action) {
+ case PUGL_ACTION_COPY:
+ return view->world->impl->atoms.XdndActionCopy;
+ case PUGL_ACTION_LINK:
+ return view->world->impl->atoms.XdndActionLink;
+ case PUGL_ACTION_MOVE:
+ return view->world->impl->atoms.XdndActionMove;
+ case PUGL_ACTION_PRIVATE:
+ break;
+ }
+
+ return view->world->impl->atoms.XdndActionPrivate;
}
static PuglStatus
@@ -1036,6 +1089,7 @@ setClipboardFormats(PuglView* const view,
static PuglEvent
translateClientMessage(PuglView* const view, XClientMessageEvent message)
{
+ PuglInternals* const impl = view->impl;
Display* const display = view->world->impl->display;
const PuglX11Atoms* const atoms = &view->world->impl->atoms;
PuglEvent event = {{PUGL_NOTHING, 0}};
@@ -1055,10 +1109,102 @@ translateClientMessage(PuglView* const view, XClientMessageEvent message)
SubstructureNotifyMask | SubstructureRedirectMask,
&reply);
}
+
} else if (message.message_type == atoms->PUGL_CLIENT_MSG) {
event.type = PUGL_CLIENT;
event.client.data1 = (uintptr_t)message.data.l[0];
event.client.data2 = (uintptr_t)message.data.l[1];
+
+ } else if (message.message_type == atoms->XdndEnter) {
+ // DnD Step 2: Target receives XdndEnter (drag entered target window)
+ // int version = (int)(message.data.l[1] >> 24);
+ impl->drag.source = (Window)message.data.l[0];
+
+ unsigned long numFormats = 0;
+ Atom* formats = NULL;
+ const bool isList = message.data.l[1] & 1;
+ if (!isList) {
+ // Up to three formats can be inlined in the event itself
+ numFormats = 3;
+ formats = (Atom*)message.data.l + 2;
+ } else {
+ // Longer lists must be retrieved from a property on the source
+ if (getAtomProperty(view,
+ impl->drag.source,
+ atoms->XdndTypeList,
+ &numFormats,
+ &formats)) {
+ return event;
+ }
+ }
+
+ // Set the available formats on the drag board
+ setClipboardFormats(view, &view->impl->drag, numFormats, formats);
+ if (isList && formats) {
+ XFree(formats);
+ }
+
+ // The offer to the application will be made when the XdndPosition comes
+
+ } else if (message.message_type == atoms->XdndPosition) {
+ // DnD Step 4: Target receives XdndPosition (drag moves in target window)
+ const Time time = (Time)message.data.l[3];
+ const int root_x = (uint16_t)((message.data.l[2] >> 16) & 0xFFFF);
+ const int root_y = (uint16_t)((message.data.l[2]) & 0xFFFF);
+
+ // Translate root coordinates from message to window coordinates
+ Window child = 0;
+ int win_x = 0;
+ int win_y = 0;
+ XTranslateCoordinates(display,
+ RootWindow(display, impl->screen),
+ impl->win,
+ root_x,
+ root_y,
+ &win_x,
+ &win_y,
+ &child);
+
+ // DnD Step 5: Target tells the source whether it will accept
+ XEvent reply = {ClientMessage};
+ reply.xclient.display = display;
+ reply.xclient.window = impl->drag.source;
+ reply.xclient.message_type = atoms->XdndStatus;
+ reply.xclient.format = 32;
+ reply.xclient.data.l[0] = (long)impl->win; // Target window
+ reply.xclient.data.l[1] = 0; // Flags
+ reply.xclient.data.l[2] = 0; // Accepting rectangle X,Y
+ reply.xclient.data.l[3] = 0; // Accepting rectangle W,H
+
+ if (view->impl->drag.acceptedFormat) {
+ reply.xclient.data.l[1] = 1; // Set bit 0: target will accept drop
+ reply.xclient.data.l[4] =
+ (long)getX11ActionAtom(view, impl->drag.acceptedAction);
+ }
+
+ XSendEvent(display, impl->drag.source, False, NoEventMask, &reply);
+
+ // Send data offer event to the application
+ event.type = PUGL_DATA_OFFER;
+ event.offer.time = (double)time / 1e3;
+ event.offer.x = win_x;
+ event.offer.y = win_y;
+ event.offer.clipboard = PUGL_CLIPBOARD_DRAG;
+ } else if (message.message_type == atoms->XdndLeave) {
+ // Step 8a: Target receives XdndLeave (drag aborted)
+ clearX11Clipboard(&impl->drag);
+ } else if (message.message_type == atoms->XdndDrop) {
+ // Step 8b: Target receives XdndDrop (data dropped in target window)
+ if (impl->drag.acceptedFormat) {
+ // Request the data with the desired format at the given time
+ // The drag will be finished by the SelectionNotify response
+ XConvertSelection(display,
+ atoms->XdndSelection,
+ impl->drag.acceptedFormat,
+ atoms->XdndSelection,
+ impl->win,
+ (Time)message.data.l[2]);
+ }
}
return event;
@@ -1619,11 +1765,15 @@ handleSelectionNotify(const PuglWorld* const world,
{
const PuglX11Atoms* const atoms = &world->impl->atoms;
- Display* const display = view->world->impl->display;
const Atom selection = event->selection;
PuglX11Clipboard* const board = getX11SelectionClipboard(view, selection);
- PuglEvent puglEvent = {{PUGL_NOTHING, 0}};
+ if (!board) {
+ return PUGL_SUCCESS; // Ignore unknown selection
+ }
+ PuglInternals* const impl = view->impl;
+ Display* const display = view->world->impl->display;
+ PuglEvent puglEvent = {{PUGL_NOTHING, 0}};
if (event->target == atoms->TARGETS) {
// Notification of available datatypes
unsigned long numFormats = 0;
@@ -1663,6 +1813,37 @@ handleSelectionNotify(const PuglWorld* const world,
puglEvent.data = data;
}
+
+ } else if (event->selection == atoms->XdndSelection) {
+ if (!retrieveSelection(world,
+ view,
+ event->property,
+ impl->drag.acceptedFormat,
+ &board->data)) {
+ board->source = impl->drag.source;
+
+ // Notify the drag source that the operation is finished
+ XEvent reply = {ClientMessage};
+ reply.xclient.window = impl->win;
+ reply.xclient.message_type = atoms->XdndFinished;
+ reply.xclient.format = 32;
+ reply.xclient.data.l[0] = (long)impl->win; // Target
+ reply.xclient.data.l[1] = 1; // Accepted (TODO)
+ reply.xclient.data.l[2] = None; // Action (TODO)
+
+ XSendEvent(display, impl->drag.source, False, NoEventMask, &reply);
+
+ // Send a data event to the application so it can retrieve the data
+ const PuglDataEvent data = {PUGL_DATA,
+ 0U,
+ (double)event->time / 1e3,
+ 0.0,
+ 0.0,
+ board->clipboard,
+ board->acceptedFormatIndex};
+
+ puglEvent.data = data;
+ }
}
return puglDispatchEvent(view, &puglEvent);
@@ -1867,6 +2048,9 @@ dispatchX11Events(PuglWorld* const world)
}
}
+ // Flush any events we may have sent in this frame to reduce latency
+ XFlush(display);
+
return st;
}
@@ -2113,6 +2297,32 @@ puglGetClipboard(PuglView* const view,
return board->data.data;
}
+static XRectangle
+rootRectangle(const PuglView* const view, const PuglRect rect)
+{
+ Display* const display = view->world->impl->display;
+ const Window root = RootWindow(display, view->impl->screen);
+ const int x = (int)floor(rect.x);
+ const int y = (int)floor(rect.y);
+ const int w = (int)ceil(rect.width);
+ const int h = (int)ceil(rect.height);
+
+ Window child = 0;
+ int rootX = 0;
+ int rootY = 0;
+ XTranslateCoordinates(
+ display, view->impl->win, root, x, y, &rootX, &rootY, &child);
+
+ const XRectangle result = {
+ (short)rootX,
+ (short)rootY,
+ (unsigned short)w,
+ (unsigned short)h,
+ };
+
+ return result;
+}
+
PuglStatus
puglAcceptOffer(PuglView* const view,
const PuglDataOfferEvent* const offer,
@@ -2120,11 +2330,10 @@ puglAcceptOffer(PuglView* const view,
PuglAction action,
const PuglRect region)
{
- (void)region;
-
- PuglInternals* const impl = view->impl;
- Display* const display = view->world->impl->display;
- PuglX11Clipboard* const board = getX11Clipboard(view, offer->clipboard);
+ PuglInternals* const impl = view->impl;
+ Display* const display = view->world->impl->display;
+ const PuglX11Atoms* const atoms = &view->world->impl->atoms;
+ PuglX11Clipboard* const board = getX11Clipboard(view, offer->clipboard);
board->acceptedAction = action;
board->acceptedFormatIndex = typeIndex;
@@ -2142,6 +2351,68 @@ puglAcceptOffer(PuglView* const view,
return PUGL_SUCCESS;
}
+ // DnD Step 5a: Target tells the source it will accept the drop
+
+ if (!board->source) {
+ return PUGL_FAILURE;
+ }
+
+ const XRectangle rootRegion = rootRectangle(view, region);
+ const long rootPos = (rootRegion.x << 16) | rootRegion.y;
+ const long rootSize = (rootRegion.width << 16) | rootRegion.height;
+ XEvent reply = {ClientMessage};
+
+ reply.xclient.display = display;
+ reply.xclient.window = impl->drag.source;
+ reply.xclient.message_type = atoms->XdndStatus;
+ reply.xclient.format = 32;
+ reply.xclient.data.l[0] = (long)impl->win;
+ reply.xclient.data.l[1] = 1;
+ reply.xclient.data.l[2] = rootPos;
+ reply.xclient.data.l[3] = rootSize;
+ reply.xclient.data.l[4] =
+ (long)getX11ActionAtom(view, impl->drag.acceptedAction);
+
+ return XSendEvent(display, impl->drag.source, False, NoEventMask, &reply)
+ ? PUGL_SUCCESS
+ : PUGL_FAILURE;
+}
+
+PuglStatus
+puglRejectOffer(PuglView* const view,
+ const PuglDataOfferEvent* const offer,
+ const PuglRect region)
+{
+ PuglInternals* const impl = view->impl;
+ Display* const display = view->world->impl->display;
+ const PuglX11Atoms* const atoms = &view->world->impl->atoms;
+ PuglX11Clipboard* const board = getX11Clipboard(view, offer->clipboard);
+ if (!board->source) {
+ return PUGL_FAILURE;
+ }
+
+ if (offer->clipboard == PUGL_CLIPBOARD_DRAG) {
+ // DnD Step 5b: Target tells the source it will refuse the drop
+
+ const XRectangle rootRegion = rootRectangle(view, region);
+ const long rootPos = (rootRegion.x << 16) | rootRegion.y;
+ const long rootSize = (rootRegion.width << 16) | rootRegion.height;
+ XEvent reply = {ClientMessage};
+
+ reply.xclient.display = display;
+ reply.xclient.window = impl->drag.source;
+ reply.xclient.message_type = atoms->XdndStatus;
+ reply.xclient.format = 32;
+ reply.xclient.data.l[0] = (long)impl->win;
+ reply.xclient.data.l[1] = 0;
+ reply.xclient.data.l[2] = rootPos;
+ reply.xclient.data.l[3] = rootSize;
+
+ return XSendEvent(display, impl->drag.source, False, NoEventMask, &reply)
+ ? PUGL_SUCCESS
+ : PUGL_FAILURE;
+ }
+
return PUGL_FAILURE;
}
@@ -2163,6 +2434,17 @@ puglPaste(PuglView* const view)
return PUGL_SUCCESS;
}
+PuglStatus
+puglRegisterDragType(PuglView* const view, const char* const type)
+{
+ /* There is the XdndTypeList property for sources to declare more than three
+ types, but no need to register as a target for types ahead of time. */
+
+ (void)view;
+ (void)type;
+ return PUGL_SUCCESS;
+}
+
uint32_t
puglGetNumClipboardTypes(const PuglView* const view,
const PuglClipboard clipboard)
diff --git a/src/x11.h b/src/x11.h
index 2e14ae2..5f30fa5 100644
--- a/src/x11.h
+++ b/src/x11.h
@@ -44,6 +44,19 @@ typedef struct {
Atom NET_WM_WINDOW_TYPE_NORMAL;
Atom NET_WM_WINDOW_TYPE_UTILITY;
Atom TARGETS;
+ Atom XdndActionCopy;
+ Atom XdndActionLink;
+ Atom XdndActionMove;
+ Atom XdndActionPrivate;
+ Atom XdndAware;
+ Atom XdndDrop;
+ Atom XdndEnter;
+ Atom XdndFinished;
+ Atom XdndLeave;
+ Atom XdndPosition;
+ Atom XdndSelection;
+ Atom XdndStatus;
+ Atom XdndTypeList;
Atom text_uri_list;
} PuglX11Atoms;
@@ -90,6 +103,7 @@ struct PuglInternalsImpl {
PuglX11Clipboard clipboard;
long frameExtentLeft;
long frameExtentTop;
+ PuglX11Clipboard drag;
int screen;
const char* cursorName;
bool mapped;
diff --git a/test/test_utils.h b/test/test_utils.h
index 92b2c58..0172be0 100644
--- a/test/test_utils.h
+++ b/test/test_utils.h
@@ -458,6 +458,8 @@ puglViewHintString(const PuglViewHint hint)
return "View type";
case PUGL_DARK_FRAME:
return "Dark frame";
+ case PUGL_ACCEPT_DROP:
+ return "Accept drop";
}
return "Unknown";