From 2501218801437ea413091007b535d7c097801713 Mon Sep 17 00:00:00 2001 From: David Robillard Date: Sun, 22 May 2022 12:24:59 -0400 Subject: 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. --- src/implementation.c | 52 +++----- src/implementation.h | 16 +-- src/mac.m | 196 +++++++++++++++++++++++---- src/types.h | 1 - src/win.c | 81 +++++++++-- src/win.h | 1 + src/x11.c | 370 +++++++++++++++++++++++++++++++++++++++------------ src/x11.h | 31 +++-- 8 files changed, 570 insertions(+), 178 deletions(-) (limited to 'src') 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 +// Copyright 2012-2022 David Robillard // Copyright 2017 Hanspeter Portner // 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 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* 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* 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* 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 +// Copyright 2012-2022 David Robillard // 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*)¬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; 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 -- cgit v1.2.1