diff options
author | David Robillard <d@drobilla.net> | 2022-05-22 16:02:04 -0400 |
---|---|---|
committer | David Robillard <d@drobilla.net> | 2023-11-11 10:20:03 -0500 |
commit | 91ba4a52701db0a43ffc7769d2fda510ca2ebfa3 (patch) | |
tree | be2dca91d457f28e0c9c324736f24d85a5ed57c2 | |
parent | 91051e9059b67b8d633e385afb48a36d4f9467ba (diff) | |
download | pugl-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.hpp | 28 | ||||
-rw-r--r-- | doc/c/clipboards.rst | 119 | ||||
-rw-r--r-- | examples/pugl_clipboard_demo.c | 8 | ||||
-rw-r--r-- | include/pugl/pugl.h | 40 | ||||
-rw-r--r-- | src/common.c | 1 | ||||
-rw-r--r-- | src/mac.m | 204 | ||||
-rw-r--r-- | src/win.c | 99 | ||||
-rw-r--r-- | src/win.h | 2 | ||||
-rw-r--r-- | src/x11.c | 310 | ||||
-rw-r--r-- | src/x11.h | 14 | ||||
-rw-r--r-- | test/test_utils.h | 2 |
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* @@ -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; @@ -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; } @@ -27,6 +27,8 @@ struct PuglInternalsImpl { WINDOWPLACEMENT oldPlacement; PAINTSTRUCT paint; PuglBlob clipboard; + char* droppedUris; + size_t droppedUrisLen; PuglSurface* surface; double scaleFactor; bool mapped; @@ -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) @@ -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"; |