diff options
-rw-r--r-- | bindings/cpp/include/pugl/pugl.hpp | 44 | ||||
-rw-r--r-- | doc/c/clipboards.rst | 118 | ||||
-rw-r--r-- | doc/c/meson.build | 3 | ||||
-rw-r--r-- | doc/c/overview.rst | 1 | ||||
-rw-r--r-- | examples/pugl_clipboard_demo.c | 50 | ||||
-rw-r--r-- | include/pugl/pugl.h | 92 | ||||
-rw-r--r-- | meson.build | 7 | ||||
-rw-r--r-- | resources/Info.plist.in | 2 | ||||
-rw-r--r-- | src/implementation.c | 52 | ||||
-rw-r--r-- | src/implementation.h | 16 | ||||
-rw-r--r-- | src/mac.m | 196 | ||||
-rw-r--r-- | src/types.h | 1 | ||||
-rw-r--r-- | src/win.c | 81 | ||||
-rw-r--r-- | src/win.h | 1 | ||||
-rw-r--r-- | src/x11.c | 370 | ||||
-rw-r--r-- | src/x11.h | 31 | ||||
-rw-r--r-- | test/test_local_copy_paste.c | 48 | ||||
-rw-r--r-- | test/test_remote_copy_paste.c | 28 | ||||
-rw-r--r-- | test/test_utils.h | 6 |
19 files changed, 946 insertions, 201 deletions
diff --git a/bindings/cpp/include/pugl/pugl.hpp b/bindings/cpp/include/pugl/pugl.hpp index 5b47fe6..8e74278 100644 --- a/bindings/cpp/include/pugl/pugl.hpp +++ b/bindings/cpp/include/pugl/pugl.hpp @@ -1,4 +1,4 @@ -// Copyright 2012-2020 David Robillard <d@drobilla.net> +// Copyright 2012-2022 David Robillard <d@drobilla.net> // SPDX-License-Identifier: ISC #ifndef PUGL_PUGL_HPP @@ -6,6 +6,7 @@ #include "pugl/pugl.h" +#include <cstddef> // IWYU pragma: keep #include <cstdint> // IWYU pragma: keep #if defined(PUGL_HPP_THROW_FAILED_CONSTRUCTION) @@ -176,6 +177,12 @@ using LoopEnterEvent = Event<PUGL_LOOP_ENTER, PuglLoopEnterEvent>; /// @copydoc PuglLoopLeaveEvent using LoopLeaveEvent = Event<PUGL_LOOP_LEAVE, PuglLoopLeaveEvent>; +/// @copydoc PuglDataOfferEvent +using DataOfferEvent = Event<PUGL_DATA_OFFER, PuglDataOfferEvent>; + +/// @copydoc PuglDataEvent +using DataEvent = Event<PUGL_DATA, PuglDataEvent>; + /** @} @defgroup statuspp Status @@ -560,6 +567,37 @@ public: puglSetCursor(cobj(), static_cast<PuglCursor>(cursor))); } + /// @copydoc puglGetNumClipboardTypes + uint32_t numClipboardTypes() const + { + return puglGetNumClipboardTypes(cobj()); + } + + /// @copydoc puglGetClipboardType + const char* clipboardType(const uint32_t typeIndex) const + { + return puglGetClipboardType(cobj(), typeIndex); + } + + /** + Accept data offered from a clipboard. + + To accept data, this must be called while handling a #PUGL_DATA_OFFER + event. Doing so will request the data from the source as the specified + type. When the data is available, a #PUGL_DATA event will be sent to the + view which can then retrieve the data with puglGetClipboard(). + + @param offer The data offer event. + + @param typeIndex The index of the type that the view will accept. This is + the `typeIndex` argument to the call of puglGetClipboardType() that + returned the accepted type. + */ + Status acceptOffer(const DataOfferEvent& offer, const size_t typeIndex) + { + return static_cast<Status>(puglAcceptOffer(cobj(), &offer, typeIndex)); + } + /// @copydoc puglRequestAttention Status requestAttention() noexcept { @@ -678,6 +716,10 @@ private: return target.onEvent(LoopEnterEvent{event->any}); case PUGL_LOOP_LEAVE: return target.onEvent(LoopLeaveEvent{event->any}); + case PUGL_DATA_OFFER: + return target.onEvent(DataOfferEvent{event->offer}); + case PUGL_DATA: + return target.onEvent(DataEvent{event->data}); } return Status::failure; diff --git a/doc/c/clipboards.rst b/doc/c/clipboards.rst new file mode 100644 index 0000000..ec66bd5 --- /dev/null +++ b/doc/c/clipboards.rst @@ -0,0 +1,118 @@ +.. default-domain:: c +.. highlight:: c + +################ +Using Clipboards +################ + +Clipboards provide a way to transfer data 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 +******* + +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). + +For example, a string can be copied to the clipboard by setting the general clipboard to ``text/plain`` data: + +.. code-block:: c + + const char* someString = "Copied string"; + + puglSetClipboard(view, + "text/plain", + someString, + strlen(someString)); + +******* +Pasting +******* + +Data from a clipboard can be pasted to a view using :func:`puglPaste`: + +.. 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 +************** + +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: + +.. code-block:: c + + case PUGL_DATA_OFFER: + onDataOffer(view, &event->offer); + break; + +When handling this event, +:func:`puglGetNumClipboardTypes` and :func:`puglGetClipboardType` can be used to enumerate the available data types: + +.. code-block:: c + + static void + onDataOffer(PuglView* view, const PuglEventDataOffer* event) + { + size_t numTypes = puglGetNumClipboardTypes(view, clipboard); + + for (uint32_t t = 0; t < numTypes; ++t) { + const char* type = puglGetClipboardType(view, t); + printf("Offered type: %s\n", type); + } + } + +If the view supports one of the data types, +it can accept the offer with :func:`puglAcceptOffer`: + +.. code-block:: c + + for (uint32_t t = 0; t < numTypes; ++t) { + const char* type = puglGetClipboardType(view, t); + if (!strcmp(type, "text/uri-list")) { + puglAcceptOffer(view, event, t); + } + } + +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`: + +.. code-block:: c + + case PUGL_DATA: + onData(view, &event->data); + break; + + // ... + + static void + onData(PuglView* view, const PuglEventData* event) + { + uint32_t typeIndex = event->typeIndex; + + const char* type = puglGetClipboardType(view, typeIndex); + + fprintf(stderr, "Received data type: %s\n", type); + + if (!strcmp(type, "text/plain")) { + size_t len = 0; + const void* data = puglGetClipboard(view, typeIndex, &len); + + printf("Dropped: %s\n", (const char*)data); + } + } diff --git a/doc/c/meson.build b/doc/c/meson.build index 8592f75..f8b4626 100644 --- a/doc/c/meson.build +++ b/doc/c/meson.build @@ -1,4 +1,4 @@ -# Copyright 2021 David Robillard <d@drobilla.net> +# Copyright 2021-2022 David Robillard <d@drobilla.net> # SPDX-License-Identifier: CC0-1.0 OR ISC config = configuration_data() @@ -18,6 +18,7 @@ c_rst_files = files( 'view.rst', 'events.rst', 'event-loop.rst', + 'clipboards.rst', 'shutting-down.rst' ) diff --git a/doc/c/overview.rst b/doc/c/overview.rst index 4bd024d..2bab700 100644 --- a/doc/c/overview.rst +++ b/doc/c/overview.rst @@ -21,6 +21,7 @@ The core API (excluding backend-specific components) is declared in ``pugl.h``: view events event-loop + clipboards shutting-down .. _pkg-config: https://www.freedesktop.org/wiki/Software/pkg-config/ diff --git a/examples/pugl_clipboard_demo.c b/examples/pugl_clipboard_demo.c index 76e42f8..9b5fb28 100644 --- a/examples/pugl_clipboard_demo.c +++ b/examples/pugl_clipboard_demo.c @@ -11,6 +11,7 @@ #include <math.h> #include <stdbool.h> +#include <stdint.h> #include <stdio.h> #include <string.h> @@ -64,15 +65,50 @@ onKeyPress(PuglView* const view, const PuglKeyEvent* const event) if (event->key == 'q' || event->key == PUGL_KEY_ESCAPE) { app->quit = 1; } else if ((event->state & PUGL_MOD_CTRL) && event->key == 'c') { - puglSetClipboard(view, NULL, copyString, strlen(copyString) + 1); + puglSetClipboard(view, "text/plain", copyString, strlen(copyString)); fprintf(stderr, "Copy \"%s\"\n", copyString); } else if ((event->state & PUGL_MOD_CTRL) && event->key == 'v') { - const char* type = NULL; + puglPaste(view); + } +} + +static void +onDataOffer(PuglView* view, const PuglDataOfferEvent* event) +{ + const uint32_t numTypes = puglGetNumClipboardTypes(view); + + // Print all offered types to be useful as a testing program + fprintf(stderr, "Offered %u types:\n", numTypes); + for (uint32_t t = 0; t < numTypes; ++t) { + const char* type = puglGetClipboardType(view, t); + fprintf(stderr, "\t%s\n", type); + } + + // Accept the first type found that we support (namely text) + for (uint32_t t = 0; t < numTypes; ++t) { + const char* type = puglGetClipboardType(view, t); + if (!strncmp(type, "text/", 5)) { + puglAcceptOffer(view, event, t); + return; + } + } +} + +static void +onData(PuglView* view, const PuglDataEvent* event) +{ + const uint32_t typeIndex = event->typeIndex; + + const char* const type = puglGetClipboardType(view, typeIndex); + + fprintf(stderr, "Received data type: %s\n", type); + if (!strncmp(type, "text/", 5)) { + // Accept any text type size_t len = 0; - const char* text = (const char*)puglGetClipboard(view, &type, &len); + const void* data = puglGetClipboard(view, typeIndex, &len); - fprintf(stderr, "Paste \"%s\"\n", text); + fprintf(stderr, "Data:\n%s\n", (const char*)data); } } @@ -143,6 +179,12 @@ onEvent(PuglView* view, const PuglEvent* event) case PUGL_FOCUS_OUT: redisplayView(app, view); break; + case PUGL_DATA_OFFER: + onDataOffer(view, &event->offer); + break; + case PUGL_DATA: + onData(view, &event->data); + break; default: break; } diff --git a/include/pugl/pugl.h b/include/pugl/pugl.h index 0d4642c..26cc76c 100644 --- a/include/pugl/pugl.h +++ b/include/pugl/pugl.h @@ -1,4 +1,4 @@ -// Copyright 2012-2020 David Robillard <d@drobilla.net> +// Copyright 2012-2022 David Robillard <d@drobilla.net> // SPDX-License-Identifier: ISC #ifndef PUGL_PUGL_H @@ -206,6 +206,8 @@ typedef enum { PUGL_TIMER, ///< Timer triggered, a #PuglTimerEvent PUGL_LOOP_ENTER, ///< Recursive loop entered, a #PuglLoopEnterEvent PUGL_LOOP_LEAVE, ///< Recursive loop left, a #PuglLoopLeaveEvent + PUGL_DATA_OFFER, ///< Data offered from clipboard, a #PuglDataOfferEvent + PUGL_DATA, ///< Data available from clipboard, a #PuglDataEvent } PuglEventType; /// Common flags for all event types @@ -529,6 +531,34 @@ typedef struct { } PuglTimerEvent; /** + Clipboard data offer event. + + This event is sent when a clipboard has data present, possibly with several + datatypes. While handling this event, the types can be investigated with + puglGetClipboardType() to decide whether to accept the offer with + puglAcceptOffer(). +*/ +typedef struct { + PuglEventType type; ///< #PUGL_DATA_OFFER + PuglEventFlags flags; ///< Bitwise OR of #PuglEventFlag values + double time; ///< Time in seconds +} PuglDataOfferEvent; + +/** + Clipboard data event. + + This event is sent after accepting a data offer when the data has been + retrieved and converted. While handling this event, the data can be + accessed with puglGetClipboard(). +*/ +typedef struct { + PuglEventType type; ///< #PUGL_DATA + PuglEventFlags flags; ///< Bitwise OR of #PuglEventFlag values + double time; ///< Time in seconds + uint32_t typeIndex; ///< Index of datatype +} PuglDataEvent; + +/** Recursive loop enter event. This event is sent when the window system enters a recursive loop. The main @@ -585,6 +615,8 @@ typedef union { PuglFocusEvent focus; ///< #PUGL_FOCUS_IN, #PUGL_FOCUS_OUT PuglClientEvent client; ///< #PUGL_CLIENT PuglTimerEvent timer; ///< #PUGL_TIMER + PuglDataOfferEvent offer; ///< #PUGL_DATA_OFFER + PuglDataEvent data; ///< #PUGL_DATA } PuglEvent; /** @@ -610,6 +642,7 @@ typedef enum { PUGL_SET_FORMAT_FAILED, ///< Failed to set pixel format PUGL_CREATE_CONTEXT_FAILED, ///< Failed to create drawing context PUGL_UNSUPPORTED, ///< Unsupported operation + PUGL_NO_MEMORY, ///< Failed to allocate memory } PuglStatus; /// Return a string describing a status code @@ -1245,6 +1278,59 @@ bool puglHasFocus(const PuglView* view); /** + Request data from the general copy/paste clipboard. + + A #PUGL_DATA_OFFER event will be sent if data is available. +*/ +PUGL_API +PuglStatus +puglPaste(PuglView* view); + +/** + Return the number of types available for the data in a clipboard. + + Returns zero if the clipboard is empty. +*/ +PUGL_API +uint32_t +puglGetNumClipboardTypes(const PuglView* view); + +/** + Return the identifier of a type available in a clipboard. + + This is usually a MIME type, but may also be another platform-specific type + identifier. Applications must ignore any type they do not recognize. + + Returns null if `typeIndex` is out of bounds according to + puglGetNumClipboardTypes(). +*/ +PUGL_API +const char* +puglGetClipboardType(const PuglView* view, uint32_t typeIndex); + +/** + Accept data offered from a clipboard. + + To accept data, this must be called while handling a #PUGL_DATA_OFFER event. + Doing so will request the data from the source as the specified type. When + the data is available, a #PUGL_DATA event will be sent to the view which can + then retrieve the data with puglGetClipboard(). + + @param view The view. + + @param offer The data offer event. + + @param typeIndex The index of the type that the view will accept. This is + the `typeIndex` argument to the call of puglGetClipboardType() that returned + the accepted type. +*/ +PUGL_API +PuglStatus +puglAcceptOffer(PuglView* view, + const PuglDataOfferEvent* offer, + uint32_t typeIndex); + +/** Set the clipboard contents. This sets the system clipboard contents, which can be retrieved with @@ -1269,13 +1355,13 @@ puglSetClipboard(PuglView* view, puglSetClipboard() or copied from another application. @param view The view. - @param[out] type Set to the MIME type of the data. + @param typeIndex Index of the data type to get the item as. @param[out] len Set to the length of the data in bytes. @return The clipboard contents, or null. */ PUGL_API const void* -puglGetClipboard(PuglView* view, const char** type, size_t* len); +puglGetClipboard(PuglView* view, uint32_t typeIndex, size_t* len); /** Set the mouse cursor. diff --git a/meson.build b/meson.build index 7a8690d..e84bde1 100644 --- a/meson.build +++ b/meson.build @@ -1,4 +1,4 @@ -# Copyright 2021 David Robillard <d@drobilla.net> +# Copyright 2021-2022 David Robillard <d@drobilla.net> # SPDX-License-Identifier: CC0-1.0 OR ISC project('pugl', ['c'], @@ -172,9 +172,12 @@ elif host_machine.system() == 'windows' add_project_arguments(win_args, language: ['c', 'cpp']) + user32_dep = cc.find_library('user32') + shlwapi_dep = cc.find_library('shlwapi') + platform = 'win' platform_sources = files('src/win.c') - core_deps = [] + core_deps = [user32_dep, shlwapi_dep] extension = '.c' else # X11 diff --git a/resources/Info.plist.in b/resources/Info.plist.in index a08dbd0..0f81b05 100644 --- a/resources/Info.plist.in +++ b/resources/Info.plist.in @@ -12,6 +12,8 @@ <string>10.6</string> <key>CFBundleDisplayName</key> <string>@NAME@</string> + <key>CFBundleExecutable</key> + <string>@NAME@</string> <key>CFBundleName</key> <string>@NAME@</string> <key>NSHighResolutionCapable</key> diff --git a/src/implementation.c b/src/implementation.c index 373060c..e7ae5e4 100644 --- a/src/implementation.c +++ b/src/implementation.c @@ -29,6 +29,7 @@ puglStrerror(const PuglStatus status) case PUGL_SET_FORMAT_FAILED: return "Failed to set pixel format"; case PUGL_CREATE_CONTEXT_FAILED: return "Failed to create drawing context"; case PUGL_UNSUPPORTED: return "Unsupported operation"; + case PUGL_NO_MEMORY: return "Failed to allocate memory"; } // clang-format on @@ -46,18 +47,28 @@ puglSetString(char** dest, const char* string) } } -void +PuglStatus puglSetBlob(PuglBlob* const dest, const void* const data, const size_t len) { if (data) { + void* const newData = realloc(dest->data, len + 1); + if (!newData) { + free(dest->data); + dest->len = 0; + return PUGL_NO_MEMORY; + } + + memcpy(newData, data, len); + ((char*)newData)[len] = 0; + dest->len = len; - dest->data = realloc(dest->data, len + 1); - memcpy(dest->data, data, len); - ((char*)dest->data)[len] = 0; + dest->data = newData; } else { dest->len = 0; dest->data = NULL; } + + return PUGL_SUCCESS; } static void @@ -134,7 +145,7 @@ PuglView* puglNewView(PuglWorld* const world) { PuglView* view = (PuglView*)calloc(1, sizeof(PuglView)); - if (!view || !(view->impl = puglInitViewInternals())) { + if (!view || !(view->impl = puglInitViewInternals(world))) { free(view); return NULL; } @@ -179,7 +190,6 @@ puglFreeView(PuglView* view) } free(view->title); - free(view->clipboard.data); puglFreeViewInternals(view); free(view); } @@ -465,33 +475,3 @@ puglDispatchEvent(PuglView* view, const PuglEvent* event) return st0 ? st0 : st1; } - -const void* -puglGetInternalClipboard(const PuglView* const view, - const char** const type, - size_t* const len) -{ - if (len) { - *len = view->clipboard.len; - } - - if (type) { - *type = "text/plain"; - } - - return view->clipboard.data; -} - -PuglStatus -puglSetInternalClipboard(PuglView* const view, - const char* const type, - const void* const data, - const size_t len) -{ - if (type && !!strcmp(type, "text/plain")) { - return PUGL_UNSUPPORTED; - } - - puglSetBlob(&view->clipboard, data, len); - return PUGL_SUCCESS; -} diff --git a/src/implementation.h b/src/implementation.h index 0fcba02..7c95fd2 100644 --- a/src/implementation.h +++ b/src/implementation.h @@ -15,7 +15,7 @@ PUGL_BEGIN_DECLS /// Set `blob` to `data` with length `len`, reallocating if necessary -void +PuglStatus puglSetBlob(PuglBlob* dest, const void* data, size_t len); /// Reallocate and set `*dest` to `string` @@ -32,7 +32,7 @@ puglFreeWorldInternals(PuglWorld* world); /// Allocate and initialise view internals (implemented once per platform) PuglInternals* -puglInitViewInternals(void); +puglInitViewInternals(PuglWorld* world); /// Destroy and free view internals (implemented once per platform) void @@ -60,18 +60,6 @@ puglExpose(PuglView* view, const PuglEvent* event); PuglStatus puglDispatchEvent(PuglView* view, const PuglEvent* event); -/// Set internal (stored in view) clipboard contents -const void* -puglGetInternalClipboard(const PuglView* view, const char** type, size_t* len); - -/// Set internal (stored in view) clipboard contents -PUGL_WARN_UNUSED_RESULT -PuglStatus -puglSetInternalClipboard(PuglView* view, - const char* type, - const void* data, - size_t len); - PUGL_END_DECLS #endif // PUGL_IMPLEMENTATION_H @@ -1,4 +1,4 @@ -// Copyright 2012-2020 David Robillard <d@drobilla.net> +// Copyright 2012-2022 David Robillard <d@drobilla.net> // Copyright 2017 Hanspeter Portner <dev@open-music-kontrollers.ch> // SPDX-License-Identifier: ISC @@ -25,6 +25,70 @@ typedef NSUInteger NSEventSubtype; typedef NSUInteger NSWindowStyleMask; #endif +typedef struct { + const char* uti; + const char* mimeType; +} Datatype; + +#define NUM_DATATYPES 16 + +static const Datatype datatypes[NUM_DATATYPES + 1] = { + {"com.apple.pasteboard.promised-file-url", "text/uri-list"}, + {"org.7-zip.7-zip-archive", "application/x-7z-compressed"}, + {"org.gnu.gnu-zip-tar-archive", "application/tar+gzip"}, + {"public.7z-archive", "application/x-7z-compressed"}, + {"public.cpio-archive", "application/x-cpio"}, + {"public.deb-archive", "application/vnd.debian.binary-package"}, + {"public.file-url", "text/uri-list"}, + {"public.html", "text/html"}, + {"public.png", "image/png"}, + {"public.rar-archive", "application/x-rar-compressed"}, + {"public.rpm-archive", "application/x-rpm"}, + {"public.rtf", "text/rtf"}, + {"public.url", "text/uri-list"}, + {"public.utf8-plain-text", "text/plain"}, + {"public.utf8-tab-separated-values-text", "text/tab-separated-values"}, + {"public.xz-archive", "application/x-xz"}, + {NULL, NULL}, +}; + +static NSString* +mimeTypeForUti(const NSString* const uti) +{ + const char* const utiString = [uti UTF8String]; + + // First try internal map to override types the system won't convert sensibly + for (const Datatype* datatype = datatypes; datatype->uti; ++datatype) { + if (!strcmp(utiString, datatype->uti)) { + return [NSString stringWithUTF8String:datatype->mimeType]; + } + } + + // Try to get the MIME type from the system + return (NSString*)CFBridgingRelease(UTTypeCopyPreferredTagWithClass( + (__bridge CFStringRef)uti, kUTTagClassMIMEType)); +} + +static NSString* +utiForMimeType(const NSString* const mimeType) +{ + const char* const mimeTypeString = [mimeType UTF8String]; + + // First try internal map to override types the system won't convert sensibly + for (const Datatype* datatype = datatypes; datatype->mimeType; ++datatype) { + if (!strcmp(mimeTypeString, datatype->mimeType)) { + return [NSString stringWithUTF8String:datatype->uti]; + } + } + + // Try to get the UTI from the system + CFStringRef uti = UTTypeCreatePreferredIdentifierForTag( + kUTTagClassMIMEType, (__bridge CFStringRef)mimeType, NULL); + + return (uti && UTTypeIsDynamic(uti)) ? (NSString*)CFBridgingRelease(uti) + : NULL; +} + static NSRect rectToScreen(NSScreen* screen, NSRect rect) { @@ -179,6 +243,10 @@ updateViewRect(PuglView* view) NSTrackingArea* trackingArea; NSMutableAttributedString* markedText; NSMutableDictionary* userTimers; + id<NSDraggingInfo> dragSource; + NSDragOperation dragOperation; + size_t dragTypeIndex; + NSString* droppedUriList; bool reshaped; } @@ -846,6 +914,8 @@ puglInitWorldInternals(PuglWorldType type, PuglWorldFlags PUGL_UNUSED(flags)) if (type == PUGL_PROGRAM) { impl->autoreleasePool = [NSAutoreleasePool new]; + + [impl->app setActivationPolicy:NSApplicationActivationPolicyRegular]; } return impl; @@ -868,7 +938,7 @@ puglGetNativeWorld(PuglWorld* world) } PuglInternals* -puglInitViewInternals(void) +puglInitViewInternals(PuglWorld* PUGL_UNUSED(world)) { PuglInternals* impl = (PuglInternals*)calloc(1, sizeof(PuglInternals)); @@ -1493,21 +1563,106 @@ puglSetTransientParent(PuglView* view, PuglNativeView parent) return PUGL_FAILURE; } +PuglStatus +puglPaste(PuglView* const view) +{ + const PuglDataOfferEvent offer = { + PUGL_DATA_OFFER, + 0, + mach_absolute_time() / 1e9, + }; + + PuglEvent offerEvent; + offerEvent.offer = offer; + puglDispatchEvent(view, &offerEvent); + return PUGL_SUCCESS; +} + +uint32_t +puglGetNumClipboardTypes(const PuglView* PUGL_UNUSED(view)) +{ + NSPasteboard* const pasteboard = [NSPasteboard generalPasteboard]; + + return pasteboard ? (uint32_t)[[pasteboard types] count] : 0; +} + +const char* +puglGetClipboardType(const PuglView* PUGL_UNUSED(view), + const uint32_t typeIndex) +{ + NSPasteboard* const pasteboard = [NSPasteboard generalPasteboard]; + if (!pasteboard) { + return NULL; + } + + const NSArray<NSPasteboardType>* const types = [pasteboard types]; + if (typeIndex >= [types count]) { + return NULL; + } + + NSString* const uti = [types objectAtIndex:typeIndex]; + NSString* const mimeType = mimeTypeForUti(uti); + + // FIXME: lifetime? + return mimeType ? [mimeType UTF8String] : [uti UTF8String]; +} + +PuglStatus +puglAcceptOffer(PuglView* const view, + const PuglDataOfferEvent* const PUGL_UNUSED(offer), + const uint32_t typeIndex) +{ + PuglWrapperView* const wrapper = view->impl->wrapperView; + NSPasteboard* const pasteboard = [NSPasteboard generalPasteboard]; + if (!pasteboard) { + return PUGL_BAD_PARAMETER; + } + + const NSArray<NSPasteboardType>* const types = [pasteboard types]; + if (typeIndex >= [types count]) { + return PUGL_BAD_PARAMETER; + } + + wrapper->dragOperation = NSDragOperationCopy; + wrapper->dragTypeIndex = typeIndex; + + const PuglDataEvent data = { + PUGL_DATA, 0u, mach_absolute_time() / 1e9, (uint32_t)typeIndex}; + + PuglEvent dataEvent; + dataEvent.data = data; + puglDispatchEvent(view, &dataEvent); + return PUGL_SUCCESS; +} + const void* -puglGetClipboard(PuglView* const view, - const char** const type, - size_t* const len) +puglGetClipboard(PuglView* const view, + const uint32_t typeIndex, + size_t* const len) { + *len = 0; + NSPasteboard* const pasteboard = [NSPasteboard generalPasteboard]; + if (!pasteboard) { + return NULL; + } - if ([[pasteboard types] containsObject:NSStringPboardType]) { - const NSString* str = [pasteboard stringForType:NSStringPboardType]; - const char* utf8 = [str UTF8String]; + const NSArray<NSPasteboardType>* const types = [pasteboard types]; + if (typeIndex >= [types count]) { + return NULL; + } - puglSetBlob(&view->clipboard, utf8, strlen(utf8) + 1); + NSString* const uti = [types objectAtIndex:typeIndex]; + if ([uti isEqualToString:@"public.file-url"] || + [uti isEqualToString:@"com.apple.pasteboard.promised-file-url"]) { + *len = [view->impl->wrapperView->droppedUriList length]; + return [view->impl->wrapperView->droppedUriList UTF8String]; } - return puglGetInternalClipboard(view, type, len); + const NSData* const data = [pasteboard dataForType:uti]; + + *len = [data length]; + return [data bytes]; } static NSCursor* @@ -1552,28 +1707,21 @@ puglSetCursor(PuglView* view, PuglCursor cursor) } PuglStatus -puglSetClipboard(PuglView* const view, +puglSetClipboard(PuglView* PUGL_UNUSED(view), const char* const type, const void* const data, const size_t len) { NSPasteboard* const pasteboard = [NSPasteboard generalPasteboard]; - const char* const str = (const char*)data; - - PuglStatus st = puglSetInternalClipboard(view, type, data, len); - if (st) { - return st; - } + NSString* const mimeType = [NSString stringWithUTF8String:type]; + NSString* const uti = utiForMimeType(mimeType); + NSData* const blob = [NSData dataWithBytes:data length:len]; - NSString* nsString = [NSString stringWithUTF8String:str]; - if (nsString) { - [pasteboard declareTypes:[NSArray arrayWithObjects:NSStringPboardType, nil] - owner:nil]; - - [pasteboard setString:nsString forType:NSStringPboardType]; + [pasteboard declareTypes:[NSArray arrayWithObjects:uti, nil] owner:nil]; + if ([pasteboard setData:blob forType:uti]) { return PUGL_SUCCESS; } - return PUGL_UNKNOWN_ERROR; + return PUGL_FAILURE; } diff --git a/src/types.h b/src/types.h index 0187dad..aa028b1 100644 --- a/src/types.h +++ b/src/types.h @@ -41,7 +41,6 @@ struct PuglViewImpl { PuglHandle handle; PuglEventFunc eventFunc; char* title; - PuglBlob clipboard; PuglNativeView parent; uintptr_t transientParent; PuglRect frame; @@ -1,4 +1,4 @@ -// Copyright 2012-2021 David Robillard <d@drobilla.net> +// Copyright 2012-2022 David Robillard <d@drobilla.net> // SPDX-License-Identifier: ISC #include "win.h" @@ -61,7 +61,7 @@ puglWideCharToUtf8(const wchar_t* const wstr, size_t* len) if (n > 0) { char* result = (char*)calloc((size_t)n, sizeof(char)); WideCharToMultiByte(CP_UTF8, 0, wstr, -1, result, n, NULL, NULL); - *len = (size_t)n; + *len = (size_t)n - 1; return result; } @@ -179,7 +179,7 @@ puglGetNativeWorld(PuglWorld* PUGL_UNUSED(world)) } PuglInternals* -puglInitViewInternals(void) +puglInitViewInternals(PuglWorld* PUGL_UNUSED(world)) { return (PuglInternals*)calloc(1, sizeof(PuglInternals)); } @@ -1153,14 +1153,51 @@ puglSetTransientParent(PuglView* view, PuglNativeView parent) return PUGL_SUCCESS; } +uint32_t +puglGetNumClipboardTypes(const PuglView* const PUGL_UNUSED(view)) +{ + return IsClipboardFormatAvailable(CF_UNICODETEXT) ? 1u : 0u; +} + +const char* +puglGetClipboardType(const PuglView* const PUGL_UNUSED(view), + const uint32_t typeIndex) +{ + return (typeIndex == 0 && IsClipboardFormatAvailable(CF_UNICODETEXT)) + ? "text/plain" + : NULL; +} + +PuglStatus +puglAcceptOffer(PuglView* const view, + const PuglDataOfferEvent* const PUGL_UNUSED(offer), + const uint32_t typeIndex) +{ + if (typeIndex != 0) { + return PUGL_UNSUPPORTED; + } + + const PuglDataEvent data = { + PUGL_DATA, + 0, + GetMessageTime() / 1e3, + 0, + }; + + PuglEvent dataEvent; + dataEvent.data = data; + puglDispatchEvent(view, &dataEvent); + return PUGL_SUCCESS; +} + const void* -puglGetClipboard(PuglView* const view, - const char** const type, - size_t* const len) +puglGetClipboard(PuglView* const view, + const uint32_t typeIndex, + size_t* const len) { PuglInternals* const impl = view->impl; - if (!IsClipboardFormatAvailable(CF_UNICODETEXT) || + if (typeIndex > 0u || !IsClipboardFormatAvailable(CF_UNICODETEXT) || !OpenClipboard(impl->hwnd)) { return NULL; } @@ -1172,12 +1209,15 @@ puglGetClipboard(PuglView* const view, return NULL; } - free(view->clipboard.data); - view->clipboard.data = puglWideCharToUtf8(wstr, &view->clipboard.len); + free(view->impl->clipboard.data); + view->impl->clipboard.data = + puglWideCharToUtf8(wstr, &view->impl->clipboard.len); + GlobalUnlock(mem); CloseClipboard(); - return puglGetInternalClipboard(view, type, len); + *len = view->impl->clipboard.len; + return view->impl->clipboard.data; } PuglStatus @@ -1188,11 +1228,15 @@ puglSetClipboard(PuglView* const view, { PuglInternals* const impl = view->impl; - PuglStatus st = puglSetInternalClipboard(view, type, data, len); + PuglStatus st = puglSetBlob(&view->impl->clipboard, data, len); if (st) { return st; } + if (!!strcmp(type, "text/plain")) { + return PUGL_UNSUPPORTED; + } + if (!OpenClipboard(impl->hwnd)) { return PUGL_UNKNOWN_ERROR; } @@ -1224,6 +1268,21 @@ puglSetClipboard(PuglView* const view, return PUGL_SUCCESS; } +PuglStatus +puglPaste(PuglView* const view) +{ + const PuglDataOfferEvent offer = { + PUGL_DATA_OFFER, + 0, + GetMessageTime() / 1e3, + }; + + PuglEvent offerEvent; + offerEvent.offer = offer; + puglDispatchEvent(view, &offerEvent); + return PUGL_SUCCESS; +} + static const char* const cursor_ids[] = { IDC_ARROW, // ARROW IDC_IBEAM, // CARET @@ -24,6 +24,7 @@ struct PuglInternalsImpl { HWND hwnd; HCURSOR cursor; HDC hdc; + PuglBlob clipboard; PuglSurface* surface; double scaleFactor; bool flashing; @@ -165,6 +165,9 @@ puglInitWorldInternals(const PuglWorldType type, const PuglWorldFlags flags) impl->atoms.NET_WM_STATE_HIDDEN = XInternAtom(display, "_NET_WM_STATE_HIDDEN", 0); + impl->atoms.TARGETS = XInternAtom(display, "TARGETS", 0); + impl->atoms.text_uri_list = XInternAtom(display, "text/uri-list", 0); + // Open input method XSetLocaleModifiers(""); if (!(impl->xim = XOpenIM(display, NULL, NULL, NULL))) { @@ -185,10 +188,13 @@ puglGetNativeWorld(PuglWorld* const world) } PuglInternals* -puglInitViewInternals(void) +puglInitViewInternals(PuglWorld* const world) { PuglInternals* impl = (PuglInternals*)calloc(1, sizeof(PuglInternals)); + impl->clipboard.selection = world->impl->atoms.CLIPBOARD; + impl->clipboard.property = XA_PRIMARY; + #ifdef HAVE_XCURSOR impl->cursorName = cursor_names[PUGL_CURSOR_ARROW]; #endif @@ -483,10 +489,29 @@ puglHide(PuglView* const view) return PUGL_SUCCESS; } +static void +clearX11Clipboard(PuglX11Clipboard* const board) +{ + for (unsigned long i = 0; i < board->numFormats; ++i) { + free(board->formatStrings[i]); + board->formatStrings[i] = NULL; + } + + board->source = None; + board->numFormats = 0; + board->acceptedFormatIndex = UINT32_MAX; + board->acceptedFormat = None; + board->data.len = 0; +} + void puglFreeViewInternals(PuglView* const view) { if (view && view->impl) { + clearX11Clipboard(&view->impl->clipboard); + free(view->impl->clipboard.data.data); + free(view->impl->clipboard.formats); + free(view->impl->clipboard.formatStrings); if (view->impl->xic) { XDestroyIC(view->impl->xic); } @@ -646,8 +671,66 @@ getAtomProperty(PuglView* const view, : PUGL_FAILURE; } +static PuglX11Clipboard* +getX11SelectionClipboard(PuglView* const view, const Atom selection) +{ + return (selection == view->world->impl->atoms.CLIPBOARD) + ? &view->impl->clipboard + : NULL; +} + +static void +setClipboardFormats(PuglView* const view, + PuglX11Clipboard* const board, + const unsigned long numFormats, + const Atom* const formats) +{ + Atom* const newFormats = + (Atom*)realloc(board->formats, numFormats * sizeof(Atom)); + if (!newFormats) { + return; + } + + for (unsigned long i = 0; i < board->numFormats; ++i) { + free(board->formatStrings[i]); + board->formatStrings[i] = NULL; + } + + board->formats = newFormats; + board->numFormats = 0; + + board->formatStrings = + (char**)realloc(board->formatStrings, numFormats * sizeof(char*)); + + for (unsigned long i = 0; i < numFormats; ++i) { + if (formats[i]) { + char* const name = XGetAtomName(view->world->impl->display, formats[i]); + const char* type = NULL; + + if (strchr(name, '/')) { // MIME type (hopefully) + type = name; + } else if (!strcmp(name, "UTF8_STRING")) { // Plain text + type = "text/plain"; + } + + if (type) { + const size_t typeLen = strlen(type); + char* const formatString = (char*)calloc(typeLen + 1, 1); + + memcpy(formatString, type, typeLen + 1); + + board->formats[board->numFormats] = formats[i]; + board->formatStrings[board->numFormats] = formatString; + ++board->numFormats; + } + + XFree(name); + } + } +} + static PuglEvent -translateClientMessage(PuglView* const view, const XClientMessageEvent message) +translateClientMessage(PuglView* const view, XClientMessageEvent message) { const PuglX11Atoms* const atoms = &view->world->impl->atoms; PuglEvent event = {{PUGL_NOTHING, 0}}; @@ -1069,33 +1152,88 @@ mergeExposeEvents(PuglExposeEvent* const dst, const PuglExposeEvent* const src) } } +static PuglStatus +retrieveSelection(const PuglWorld* const world, + PuglView* const view, + const Atom property, + const Atom type, + PuglBlob* const result) +{ + uint8_t* value = NULL; + Atom actualType = 0u; + int actualFormat = 0; + unsigned long actualNumItems = 0u; + unsigned long bytesAfter = 0u; + + if (XGetWindowProperty(world->impl->display, + view->impl->win, + property, + 0, + 0x1FFFFFFF, + False, + type, + &actualType, + &actualFormat, + &actualNumItems, + &bytesAfter, + &value) != Success) { + return PUGL_FAILURE; + } + + if (value && actualFormat == 8 && bytesAfter == 0) { + puglSetBlob(result, value, actualNumItems); + } + + XFree(value); + return PUGL_SUCCESS; +} + static void -handleSelectionNotify(const PuglWorld* const world, PuglView* const view) +handleSelectionNotify(const PuglWorld* const world, + PuglView* const view, + const XSelectionEvent* const event) { - uint8_t* str = NULL; - Atom type = 0; - int fmt = 0; - unsigned long len = 0; - unsigned long left = 0; - - XGetWindowProperty(world->impl->display, - view->impl->win, - XA_PRIMARY, - 0, - 0x1FFFFFFF, - False, - AnyPropertyType, - &type, - &fmt, - &len, - &left, - &str); - - if (str && fmt == 8 && type == world->impl->atoms.UTF8_STRING && left == 0) { - puglSetBlob(&view->clipboard, str, len); - } - - XFree(str); + 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 (event->target == atoms->TARGETS) { + // Notification of available datatypes + unsigned long numFormats = 0; + Atom* formats = NULL; + if (!getAtomProperty( + view, event->requestor, event->property, &numFormats, &formats)) { + setClipboardFormats(view, board, numFormats, formats); + + const PuglDataOfferEvent offer = { + PUGL_DATA_OFFER, 0, (double)event->time / 1e3}; + + puglEvent.offer = offer; + board->acceptedFormatIndex = UINT32_MAX; + board->acceptedFormat = None; + + XFree(formats); + } + + } else if (event->selection == atoms->CLIPBOARD && + event->property == XA_PRIMARY && + board->acceptedFormatIndex < board->numFormats) { + // Notification of data from the clipboard + if (!retrieveSelection( + world, view, event->property, event->target, &board->data)) { + board->source = XGetSelectionOwner(display, board->selection); + + const PuglDataEvent data = { + PUGL_DATA, 0u, (double)event->time / 1e3, board->acceptedFormatIndex}; + + puglEvent.data = data; + } + } + + puglDispatchEvent(view, &puglEvent); } static PuglStatus @@ -1103,34 +1241,46 @@ handleSelectionRequest(const PuglWorld* const world, PuglView* const view, const XSelectionRequestEvent* const request) { + Display* const display = world->impl->display; + const PuglX11Atoms* const atoms = &world->impl->atoms; + + PuglX11Clipboard* const board = + getX11SelectionClipboard(view, request->selection); + + if (!board) { + return PUGL_UNKNOWN_ERROR; + } + + if (request->target == atoms->TARGETS) { + XChangeProperty(world->impl->display, + request->requestor, + request->property, + XA_ATOM, + 32, + PropModeReplace, + (const uint8_t*)board->formats, + (int)board->numFormats); + } else { + XChangeProperty(world->impl->display, + request->requestor, + request->property, + request->target, + 8, + PropModeReplace, + (const uint8_t*)board->data.data, + (int)board->data.len); + } + XSelectionEvent note = {SelectionNotify, request->serial, False, - world->impl->display, + display, request->requestor, request->selection, request->target, - None, + request->property, request->time}; - const char* type = NULL; - size_t len = 0; - const void* data = puglGetInternalClipboard(view, &type, &len); - if (data && request->selection == world->impl->atoms.CLIPBOARD && - request->target == world->impl->atoms.UTF8_STRING) { - note.property = request->property; - XChangeProperty(world->impl->display, - note.requestor, - note.property, - note.target, - 8, - PropModeReplace, - (const uint8_t*)data, - (int)len); - } else { - note.property = None; - } - return XSendEvent( world->impl->display, note.requestor, True, 0, (XEvent*)¬e) ? PUGL_SUCCESS @@ -1213,8 +1363,6 @@ dispatchX11Events(PuglWorld* const world) PuglStatus st0 = PUGL_SUCCESS; PuglStatus st1 = PUGL_SUCCESS; - const PuglX11Atoms* const atoms = &world->impl->atoms; - // Flush output to the server once at the start Display* display = world->impl->display; XFlush(display); @@ -1247,12 +1395,13 @@ dispatchX11Events(PuglWorld* const world) } else if (xevent.type == FocusOut) { XUnsetICFocus(impl->xic); } else if (xevent.type == SelectionClear) { - puglSetBlob(&view->clipboard, NULL, 0); - } else if (xevent.type == SelectionNotify && - xevent.xselection.selection == atoms->CLIPBOARD && - xevent.xselection.target == atoms->UTF8_STRING && - xevent.xselection.property == XA_PRIMARY) { - handleSelectionNotify(world, view); + PuglX11Clipboard* const board = + getX11SelectionClipboard(view, xevent.xselectionclear.selection); + if (board) { + clearX11Clipboard(board); + } + } else if (xevent.type == SelectionNotify) { + handleSelectionNotify(world, view, &xevent.xselection); } else if (xevent.type == SelectionRequest) { handleSelectionRequest(world, view, &xevent.xselectionrequest); } @@ -1490,34 +1639,82 @@ puglSetTransientParent(PuglView* const view, const PuglNativeView parent) } const void* -puglGetClipboard(PuglView* const view, - const char** const type, - size_t* const len) +puglGetClipboard(PuglView* const view, + const uint32_t typeIndex, + size_t* const len) { - PuglInternals* const impl = view->impl; - Display* const display = view->world->impl->display; - const PuglX11Atoms* const atoms = &view->world->impl->atoms; + Display* const display = view->world->impl->display; + PuglX11Clipboard* const board = &view->impl->clipboard; - const Window owner = XGetSelectionOwner(display, atoms->CLIPBOARD); - if (owner != None && owner != impl->win) { - // Clear internal selection - puglSetBlob(&view->clipboard, NULL, 0); - - // Request selection from the owner - XConvertSelection(display, - atoms->CLIPBOARD, - atoms->UTF8_STRING, - XA_PRIMARY, - impl->win, - CurrentTime); - - // Run event loop until data is received - while (!view->clipboard.data) { - puglUpdate(view->world, -1.0); - } + if (typeIndex != board->acceptedFormatIndex) { + return NULL; } - return puglGetInternalClipboard(view, type, len); + const Window owner = XGetSelectionOwner(display, board->selection); + if (!owner || owner != board->source) { + *len = 0; + return NULL; + } + + *len = board->data.len; + return board->data.data; +} + +PuglStatus +puglAcceptOffer(PuglView* const view, + const PuglDataOfferEvent* const offer, + const uint32_t typeIndex) +{ + (void)offer; + + PuglInternals* const impl = view->impl; + Display* const display = view->world->impl->display; + PuglX11Clipboard* const board = &view->impl->clipboard; + + board->acceptedFormatIndex = typeIndex; + board->acceptedFormat = board->formats[typeIndex]; + + // Request the data in the specified type from the general clipboard + XConvertSelection(display, + board->selection, + board->acceptedFormat, + board->property, + impl->win, + CurrentTime); + + return PUGL_SUCCESS; +} + +PuglStatus +puglPaste(PuglView* const view) +{ + Display* const display = view->world->impl->display; + const PuglX11Atoms* atoms = &view->world->impl->atoms; + const PuglX11Clipboard* board = &view->impl->clipboard; + + // Request a SelectionNotify for TARGETS (available datatypes) + XConvertSelection(display, + board->selection, + atoms->TARGETS, + board->property, + view->impl->win, + CurrentTime); + + return PUGL_SUCCESS; +} + +uint32_t +puglGetNumClipboardTypes(const PuglView* const view) +{ + return (uint32_t)view->impl->clipboard.numFormats; +} + +const char* +puglGetClipboardType(const PuglView* const view, const uint32_t typeIndex) +{ + const PuglX11Clipboard* const board = &view->impl->clipboard; + + return typeIndex < board->numFormats ? board->formatStrings[typeIndex] : NULL; } PuglStatus @@ -1526,13 +1723,18 @@ puglSetClipboard(PuglView* const view, const void* const data, const size_t len) { - PuglInternals* const impl = view->impl; - Display* const display = view->world->impl->display; - const PuglX11Atoms* const atoms = &view->world->impl->atoms; + PuglInternals* const impl = view->impl; + Display* const display = view->world->impl->display; + PuglX11Clipboard* const board = &view->impl->clipboard; + const PuglStatus st = puglSetBlob(&board->data, data, len); - PuglStatus st = puglSetInternalClipboard(view, type, data, len); if (!st) { - XSetSelectionOwner(display, atoms->CLIPBOARD, impl->win, CurrentTime); + const Atom format = {XInternAtom(display, type, 0)}; + + setClipboardFormats(view, board, 1, &format); + XSetSelectionOwner(display, board->selection, impl->win, CurrentTime); + + board->source = impl->win; } return st; @@ -27,6 +27,8 @@ typedef struct { Atom NET_WM_STATE; Atom NET_WM_STATE_DEMANDS_ATTENTION; Atom NET_WM_STATE_HIDDEN; + Atom TARGETS; + Atom text_uri_list; } PuglX11Atoms; typedef struct { @@ -35,6 +37,18 @@ typedef struct { uintptr_t id; } PuglTimer; +typedef struct { + Atom selection; + Atom property; + Window source; + Atom* formats; + char** formatStrings; + unsigned long numFormats; + uint32_t acceptedFormatIndex; + Atom acceptedFormat; + PuglBlob data; +} PuglX11Clipboard; + struct PuglWorldInternalsImpl { Display* display; PuglX11Atoms atoms; @@ -49,14 +63,15 @@ struct PuglWorldInternalsImpl { }; struct PuglInternalsImpl { - XVisualInfo* vi; - Window win; - XIC xic; - PuglSurface* surface; - PuglEvent pendingConfigure; - PuglEvent pendingExpose; - int screen; - const char* cursorName; + XVisualInfo* vi; + Window win; + XIC xic; + PuglSurface* surface; + PuglEvent pendingConfigure; + PuglEvent pendingExpose; + PuglX11Clipboard clipboard; + int screen; + const char* cursorName; }; PUGL_WARN_UNUSED_RESULT diff --git a/test/test_local_copy_paste.c b/test/test_local_copy_paste.c index f5c75c4..4a2f4d9 100644 --- a/test/test_local_copy_paste.c +++ b/test/test_local_copy_paste.c @@ -1,4 +1,4 @@ -// Copyright 2020-2021 David Robillard <d@drobilla.net> +// Copyright 2020-2022 David Robillard <d@drobilla.net> // SPDX-License-Identifier: ISC // Tests copy and paste within the same view @@ -21,6 +21,9 @@ static const uintptr_t timerId = 1u; typedef enum { START, EXPOSED, + PASTED, + RECEIVED_OFFER, + RECEIVED_DATA, FINISHED, } State; @@ -57,20 +60,55 @@ onEvent(PuglView* view, const PuglEvent* event) puglSetClipboard( view, "text/plain", "Copied Text", strlen("Copied Text") + 1); + // Check that the new type is available immediately + assert(puglGetNumClipboardTypes(view) >= 1); + assert(!strcmp(puglGetClipboardType(view, 0), "text/plain")); + + size_t len = 0; + const char* text = (const char*)puglGetClipboard(view, 0, &len); + + // Check that the new contents are available immediately + assert(text); + assert(!strcmp(text, "Copied Text")); + } else if (test->iteration == 1) { - const char* type = NULL; size_t len = 0; - const char* text = (const char*)puglGetClipboard(view, &type, &len); + const char* text = (const char*)puglGetClipboard(view, 0, &len); - assert(!strcmp(type, "text/plain")); + // Check that the contents we pasted last iteration are still there + assert(text); assert(!strcmp(text, "Copied Text")); - test->state = FINISHED; + } else if (test->iteration == 2) { + // Start a "proper" paste + test->state = PASTED; + assert(!puglPaste(view)); } ++test->iteration; break; + case PUGL_DATA_OFFER: + if (test->state == PASTED) { + test->state = RECEIVED_OFFER; + + assert(!puglAcceptOffer(view, &event->offer, 0)); + } + break; + + case PUGL_DATA: + if (test->state == RECEIVED_OFFER) { + size_t len = 0; + const char* text = (const char*)puglGetClipboard(view, 0, &len); + + // Check that the offered data is what we copied earlier + assert(text); + assert(!strcmp(text, "Copied Text")); + + test->state = FINISHED; + } + break; + default: break; } diff --git a/test/test_remote_copy_paste.c b/test/test_remote_copy_paste.c index 2ee90f7..07e75d9 100644 --- a/test/test_remote_copy_paste.c +++ b/test/test_remote_copy_paste.c @@ -1,4 +1,4 @@ -// Copyright 2020-2021 David Robillard <d@drobilla.net> +// Copyright 2020-2022 David Robillard <d@drobilla.net> // SPDX-License-Identifier: ISC // Tests copy and paste from one view to another @@ -23,6 +23,8 @@ typedef enum { START, EXPOSED, COPIED, + PASTED, + RECEIVED_OFFER, FINISHED, } State; @@ -60,6 +62,7 @@ onCopierEvent(PuglView* const view, const PuglEvent* const event) if (test->state < COPIED) { puglSetClipboard( view, "text/plain", "Copied Text", strlen("Copied Text") + 1); + test->state = COPIED; } @@ -92,18 +95,31 @@ onPasterEvent(PuglView* const view, const PuglEvent* const event) case PUGL_TIMER: assert(event->timer.id == pasterTimerId); - if (test->state == COPIED) { - const char* type = NULL; + test->state = PASTED; + assert(!puglPaste(view)); + } + break; + + case PUGL_DATA_OFFER: + if (test->state == PASTED) { + test->state = RECEIVED_OFFER; + + assert(!puglAcceptOffer(view, &event->offer, 0)); + } + break; + + case PUGL_DATA: + if (test->state == RECEIVED_OFFER) { size_t len = 0; - const char* text = (const char*)puglGetClipboard(view, &type, &len); + const char* text = (const char*)puglGetClipboard(view, 0, &len); - assert(!strcmp(type, "text/plain")); + // Check that the offered data is what we copied earlier + assert(text); assert(!strcmp(text, "Copied Text")); test->state = FINISHED; } - break; default: diff --git a/test/test_utils.h b/test/test_utils.h index 2597537..b7a8f42 100644 --- a/test/test_utils.h +++ b/test/test_utils.h @@ -1,4 +1,4 @@ -// Copyright 2012-2020 David Robillard <d@drobilla.net> +// Copyright 2012-2022 David Robillard <d@drobilla.net> // SPDX-License-Identifier: ISC #ifndef TEST_TEST_UTILS_H @@ -170,6 +170,10 @@ printEvent(const PuglEvent* event, const char* prefix, const bool verbose) return PRINT("%sLoop enter\n", prefix); case PUGL_LOOP_LEAVE: return PRINT("%sLoop leave\n", prefix); + case PUGL_DATA_OFFER: + return PRINT("%sData offer\n", prefix); + case PUGL_DATA: + return PRINT("%sData\n", prefix); default: break; } |