aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDavid Robillard <d@drobilla.net>2022-05-22 12:24:59 -0400
committerDavid Robillard <d@drobilla.net>2022-05-23 16:50:43 -0400
commit2501218801437ea413091007b535d7c097801713 (patch)
treecf938dd335f8aa9b547b458f97f05e7e18d8b9d3
parent0093196a4c624da6d7f78a909a442f2e784c37aa (diff)
downloadpugl-2501218801437ea413091007b535d7c097801713.tar.gz
pugl-2501218801437ea413091007b535d7c097801713.tar.bz2
pugl-2501218801437ea413091007b535d7c097801713.zip
Add rich clipboard support
This implements a more powerful protocol for working with clipboards, which supports datatype negotiation, and fixes various issues by mapping more directly to how things work on X11.
-rw-r--r--bindings/cpp/include/pugl/pugl.hpp44
-rw-r--r--doc/c/clipboards.rst118
-rw-r--r--doc/c/meson.build3
-rw-r--r--doc/c/overview.rst1
-rw-r--r--examples/pugl_clipboard_demo.c50
-rw-r--r--include/pugl/pugl.h92
-rw-r--r--meson.build7
-rw-r--r--resources/Info.plist.in2
-rw-r--r--src/implementation.c52
-rw-r--r--src/implementation.h16
-rw-r--r--src/mac.m196
-rw-r--r--src/types.h1
-rw-r--r--src/win.c81
-rw-r--r--src/win.h1
-rw-r--r--src/x11.c370
-rw-r--r--src/x11.h31
-rw-r--r--test/test_local_copy_paste.c48
-rw-r--r--test/test_remote_copy_paste.c28
-rw-r--r--test/test_utils.h6
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
diff --git a/src/mac.m b/src/mac.m
index 9cd3e1c..2a54134 100644
--- a/src/mac.m
+++ b/src/mac.m
@@ -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;
diff --git a/src/win.c b/src/win.c
index 5e70b6f..2cf2781 100644
--- a/src/win.c
+++ b/src/win.c
@@ -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
diff --git a/src/win.h b/src/win.h
index f71dcf8..e733c10 100644
--- a/src/win.h
+++ b/src/win.h
@@ -24,6 +24,7 @@ struct PuglInternalsImpl {
HWND hwnd;
HCURSOR cursor;
HDC hdc;
+ PuglBlob clipboard;
PuglSurface* surface;
double scaleFactor;
bool flashing;
diff --git a/src/x11.c b/src/x11.c
index c2e9e96..1b83d98 100644
--- a/src/x11.c
+++ b/src/x11.c
@@ -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*)&note)
? 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;
diff --git a/src/x11.h b/src/x11.h
index 1be54ad..27cfb73 100644
--- a/src/x11.h
+++ b/src/x11.h
@@ -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;
}