aboutsummaryrefslogtreecommitdiffstats
path: root/src
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 /src
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.
Diffstat (limited to 'src')
-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
8 files changed, 570 insertions, 178 deletions
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