diff options
author | David Robillard <d@drobilla.net> | 2019-08-17 20:50:20 +0200 |
---|---|---|
committer | David Robillard <d@drobilla.net> | 2019-09-07 09:51:48 +0200 |
commit | 7162fa4f5656ad7dfe2d6fea02f9f33c5aa1b1cf (patch) | |
tree | 0f885354b2d75875003401379d5aaf9aadda226f | |
parent | 27e43183d89aad98f6000ee187b05547776ae4c2 (diff) |
Add clipboard support
-rw-r--r-- | pugl/detail/implementation.c | 46 | ||||
-rw-r--r-- | pugl/detail/implementation.h | 15 | ||||
-rw-r--r-- | pugl/detail/mac.m | 40 | ||||
-rw-r--r-- | pugl/detail/types.h | 7 | ||||
-rw-r--r-- | pugl/detail/win.c | 82 | ||||
-rw-r--r-- | pugl/detail/x11.c | 103 | ||||
-rw-r--r-- | pugl/detail/x11.h | 1 | ||||
-rw-r--r-- | pugl/pugl.h | 27 | ||||
-rw-r--r-- | test/pugl_test.c | 14 |
9 files changed, 332 insertions, 3 deletions
diff --git a/pugl/detail/implementation.c b/pugl/detail/implementation.c index 6ff71e2..f1fd57a 100644 --- a/pugl/detail/implementation.c +++ b/pugl/detail/implementation.c @@ -34,6 +34,20 @@ puglSetString(char** dest, const char* string) strncpy(*dest, string, len + 1); } +void +puglSetBlob(PuglBlob* const dest, const void* const data, const size_t len) +{ + if (data) { + dest->len = len; + dest->data = realloc(dest->data, len + 1); + memcpy(dest->data, data, len); + ((char*)dest->data)[len] = 0; + } else { + dest->len = 0; + dest->data = NULL; + } +} + static void puglSetDefaultHints(PuglHints hints) { @@ -126,6 +140,7 @@ puglFreeView(PuglView* view) } } + free(view->clipboard.data); puglFreeViewInternals(view); free(view); } @@ -267,3 +282,34 @@ puglDispatchEvent(PuglView* view, const PuglEvent* event) view->eventFunc(view, event); } } + +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_ERR_UNSUPPORTED_TYPE; + } + + puglSetBlob(&view->clipboard, data, len); + return PUGL_SUCCESS; +} + diff --git a/pugl/detail/implementation.h b/pugl/detail/implementation.h index 7f50ecf..874e7e1 100644 --- a/pugl/detail/implementation.h +++ b/pugl/detail/implementation.h @@ -24,12 +24,16 @@ #include "pugl/detail/types.h" #include "pugl/pugl.h" +#include <stddef.h> #include <stdint.h> #ifdef __cplusplus extern "C" { #endif +/** Set `blob` to `data` with length `len`, reallocating if necessary. */ +void puglSetBlob(PuglBlob* blob, const void* data, size_t len); + /** Reallocate and set `*dest` to `string`. */ void puglSetString(char** dest, const char* string); @@ -51,6 +55,17 @@ uint32_t puglDecodeUTF8(const uint8_t* buf); /** Dispatch `event` to `view`, optimising configure/expose if possible. */ void 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. */ +PuglStatus +puglSetInternalClipboard(PuglView* view, + const char* type, + const void* data, + size_t len); + #ifdef __cplusplus } /* extern "C" */ #endif diff --git a/pugl/detail/mac.m b/pugl/detail/mac.m index 8570709..ab13452 100644 --- a/pugl/detail/mac.m +++ b/pugl/detail/mac.m @@ -1024,3 +1024,43 @@ puglSetAspectRatio(PuglView* const view, return PUGL_SUCCESS; } + +const void* +puglGetClipboard(PuglView* const view, + const char** const type, + size_t* const len) +{ + NSPasteboard* const pasteboard = [NSPasteboard generalPasteboard]; + + if ([[pasteboard types] containsObject:NSStringPboardType]) { + const NSString* str = [pasteboard stringForType:NSStringPboardType]; + const char* utf8 = [str UTF8String]; + + puglSetBlob(&view->clipboard, utf8, strlen(utf8) + 1); + } + + return puglGetInternalClipboard(view, type, len); +} + +PuglStatus +puglSetClipboard(PuglView* const 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; + } + + [pasteboard declareTypes:[NSArray arrayWithObjects:NSStringPboardType, nil] + owner:nil]; + + [pasteboard setString:[NSString stringWithUTF8String:str] + forType:NSStringPboardType]; + + return PUGL_SUCCESS; +} diff --git a/pugl/detail/types.h b/pugl/detail/types.h index 7c11f42..4ac224b 100644 --- a/pugl/detail/types.h +++ b/pugl/detail/types.h @@ -45,6 +45,12 @@ typedef struct PuglInternalsImpl PuglInternals; /** View hints. */ typedef int PuglHints[PUGL_NUM_WINDOW_HINTS]; +/** Blob of arbitrary data. */ +typedef struct { + void* data; //!< Dynamically allocated data + size_t len; //!< Length of data in bytes +} PuglBlob; + /** Cross-platform view definition. */ struct PuglViewImpl { PuglWorld* world; @@ -53,6 +59,7 @@ struct PuglViewImpl { PuglHandle handle; PuglEventFunc eventFunc; char* title; + PuglBlob clipboard; PuglNativeWindow parent; uintptr_t transientParent; PuglHints hints; diff --git a/pugl/detail/win.c b/pugl/detail/win.c index 9ed1e5d..25780e0 100644 --- a/pugl/detail/win.c +++ b/pugl/detail/win.c @@ -67,6 +67,20 @@ puglUtf8ToWideChar(const char* const utf8) return NULL; } +static char* +puglWideCharToUtf8(const wchar_t* const wstr, size_t* len) +{ + int n = WideCharToMultiByte(CP_UTF8, 0, wstr, -1, NULL, 0, NULL, NULL); + 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; + return result; + } + + return NULL; +} + static bool puglRegisterWindowClass(const char* name) { @@ -842,3 +856,71 @@ puglSetAspectRatio(PuglView* const view, view->maxAspectY = maxY; return PUGL_SUCCESS; } + +const void* +puglGetClipboard(PuglView* const view, + const char** const type, + size_t* const len) +{ + PuglInternals* const impl = view->impl; + + if (!IsClipboardFormatAvailable(CF_UNICODETEXT) || + !OpenClipboard(impl->hwnd)) { + return NULL; + } + + HGLOBAL mem = GetClipboardData(CF_UNICODETEXT); + wchar_t* wstr = mem ? (wchar_t*)GlobalLock(mem) : NULL; + if (!wstr) { + CloseClipboard(); + return NULL; + } + + free(view->clipboard.data); + view->clipboard.data = puglWideCharToUtf8(wstr, &view->clipboard.len); + GlobalUnlock(mem); + CloseClipboard(); + + return puglGetInternalClipboard(view, type, len); +} + +PuglStatus +puglSetClipboard(PuglView* const view, + const char* const type, + const void* const data, + const size_t len) +{ + PuglInternals* const impl = view->impl; + + PuglStatus st = puglSetInternalClipboard(view, type, data, len); + if (st) { + return st; + } else if (!OpenClipboard(impl->hwnd)) { + return PUGL_ERR_UNKNOWN; + } + + // Measure string and allocate global memory for clipboard + const char* str = (const char*)data; + const int wlen = MultiByteToWideChar(CP_UTF8, 0, str, -1, NULL, 0); + HGLOBAL mem = GlobalAlloc(GMEM_MOVEABLE, (wlen + 1) * sizeof(wchar_t)); + if (!mem) { + CloseClipboard(); + return PUGL_ERR_UNKNOWN; + } + + // Lock global memory + wchar_t* wstr = (wchar_t*)GlobalLock(mem); + if (!wstr) { + GlobalFree(mem); + CloseClipboard(); + return PUGL_ERR_UNKNOWN; + } + + // Convert string into global memory and set it as clipboard data + MultiByteToWideChar(CP_UTF8, 0, str, (int)len, wstr, wlen); + wstr[wlen] = 0; + GlobalUnlock(mem); + SetClipboardData(CF_UNICODETEXT, mem); + CloseClipboard(); + return PUGL_SUCCESS; +} diff --git a/pugl/detail/x11.c b/pugl/detail/x11.c index 43cfb18..adf8c9d 100644 --- a/pugl/detail/x11.c +++ b/pugl/detail/x11.c @@ -28,6 +28,7 @@ #include "pugl/pugl.h" #include <X11/X.h> +#include <X11/Xatom.h> #include <X11/Xlib.h> #include <X11/Xutil.h> #include <X11/keysym.h> @@ -35,6 +36,7 @@ #include <sys/select.h> #include <sys/time.h> +#include <limits.h> #include <stdbool.h> #include <stdint.h> #include <stdio.h> @@ -75,6 +77,7 @@ puglInitWorldInternals(void) impl->display = display; // Intern the various atoms we will need + impl->atoms.CLIPBOARD = XInternAtom(display, "CLIPBOARD", 0); impl->atoms.UTF8_STRING = XInternAtom(display, "UTF8_STRING", 0); impl->atoms.WM_PROTOCOLS = XInternAtom(display, "WM_PROTOCOLS", 0); impl->atoms.WM_DELETE_WINDOW = XInternAtom(display, "WM_DELETE_WINDOW", 0); @@ -600,6 +603,8 @@ sendRedisplayEvent(PuglView* view) PUGL_API PuglStatus puglDispatchEvents(PuglWorld* world) { + const PuglX11Atoms* const atoms = &world->impl->atoms; + // Send expose events for any views with pending redisplays for (size_t i = 0; i < world->numViews; ++i) { if (world->views[i]->redisplay) { @@ -636,6 +641,55 @@ puglDispatchEvents(PuglWorld* world) XSetICFocus(impl->xic); } else if (xevent.type == FocusOut) { XUnsetICFocus(impl->xic); + } else if (xevent.type == SelectionClear) { + puglSetBlob(&view->clipboard, NULL, 0); + continue; + } else if (xevent.type == SelectionNotify && + xevent.xselection.selection == atoms->CLIPBOARD && + xevent.xselection.target == atoms->UTF8_STRING && + xevent.xselection.property == XA_PRIMARY) { + + uint8_t* str = NULL; + Atom type = 0; + int fmt = 0; + unsigned long len = 0; + unsigned long left = 0; + XGetWindowProperty(impl->display, impl->win, XA_PRIMARY, + 0, 8, False, AnyPropertyType, + &type, &fmt, &len, &left, &str); + + if (str && fmt == 8 && type == atoms->UTF8_STRING && left == 0) { + puglSetBlob(&view->clipboard, str, len); + } + + XFree(str); + continue; + } else if (xevent.type == SelectionRequest) { + const XSelectionRequestEvent* request = &xevent.xselectionrequest; + + XSelectionEvent note = {0}; + note.type = SelectionNotify; + note.requestor = request->requestor; + note.selection = request->selection; + note.target = request->target; + note.time = request->time; + + const char* type = NULL; + size_t len = 0; + const void* data = puglGetInternalClipboard(view, &type, &len); + if (data && + request->selection == atoms->CLIPBOARD && + request->target == atoms->UTF8_STRING) { + note.property = request->property; + XChangeProperty(impl->display, note.requestor, + note.property, note.target, 8, PropModeReplace, + (const uint8_t*)data, len); + } else { + note.property = None; + } + + XSendEvent(impl->display, note.requestor, True, 0, (XEvent*)¬e); + continue; } // Translate X11 event to Pugl event @@ -797,3 +851,52 @@ puglSetTransientFor(PuglView* view, PuglNativeWindow parent) return PUGL_SUCCESS; } + +const void* +puglGetClipboard(PuglView* const view, + const char** const type, + size_t* const len) +{ + PuglInternals* const impl = view->impl; + const PuglX11Atoms* const atoms = &view->world->impl->atoms; + + const Window owner = XGetSelectionOwner(impl->display, atoms->CLIPBOARD); + if (owner != None && owner != impl->win) { + // Clear internal selection + puglSetBlob(&view->clipboard, NULL, 0); + + // Request selection from the owner + XConvertSelection(impl->display, + atoms->CLIPBOARD, + atoms->UTF8_STRING, + XA_PRIMARY, + impl->win, + CurrentTime); + + // Run event loop until data is received + while (!view->clipboard.data) { + puglPollEvents(view->world, -1); + puglDispatchEvents(view->world); + } + } + + return puglGetInternalClipboard(view, type, len); +} + +PuglStatus +puglSetClipboard(PuglView* const view, + const char* const type, + const void* const data, + const size_t len) +{ + PuglInternals* const impl = view->impl; + const PuglX11Atoms* const atoms = &view->world->impl->atoms; + + PuglStatus st = puglSetInternalClipboard(view, type, data, len); + if (st) { + return st; + } + + XSetSelectionOwner(impl->display, atoms->CLIPBOARD, impl->win, CurrentTime); + return PUGL_SUCCESS; +} diff --git a/pugl/detail/x11.h b/pugl/detail/x11.h index be5f3e0..bfdbf60 100644 --- a/pugl/detail/x11.h +++ b/pugl/detail/x11.h @@ -24,6 +24,7 @@ #include <X11/Xutil.h> typedef struct { + Atom CLIPBOARD; Atom UTF8_STRING; Atom WM_PROTOCOLS; Atom WM_DELETE_WINDOW; diff --git a/pugl/pugl.h b/pugl/pugl.h index 4182225..580e9dc 100644 --- a/pugl/pugl.h +++ b/pugl/pugl.h @@ -22,6 +22,7 @@ #define PUGL_H_INCLUDED #include <stdbool.h> +#include <stddef.h> #include <stdint.h> #ifdef PUGL_SHARED @@ -85,6 +86,7 @@ typedef enum { PUGL_ERR_CREATE_WINDOW, PUGL_ERR_SET_FORMAT, PUGL_ERR_CREATE_CONTEXT, + PUGL_ERR_UNSUPPORTED_TYPE, } PuglStatus; /** @@ -761,6 +763,31 @@ PUGL_API PuglStatus puglGrabFocus(PuglView* view); /** + Get clipboard contents. + + @param view The view. + @param[out] type Set to the MIME type of the data. + @param[out] len Set to the length of the data in bytes. + @return The clipboard contents. +*/ +PUGL_API const void* +puglGetClipboard(PuglView* view, const char** type, size_t* len); + +/** + Set clipboard contents. + + @param view The view. + @param type The MIME type of the data, "text/plain" is assumed if NULL. + @param data The data to copy to the clipboard. + @param len The length of data in bytes (including terminator if necessary). +*/ +PUGL_API PuglStatus +puglSetClipboard(PuglView* view, + const char* type, + const void* data, + size_t len); + +/** Request user attention. This hints to the system that the window or application requires attention diff --git a/test/pugl_test.c b/test/pugl_test.c index ee58cf8..4bd5f80 100644 --- a/test/pugl_test.c +++ b/test/pugl_test.c @@ -147,7 +147,7 @@ swapFocus(PuglTestApp* app) } static void -onKeyPress(PuglView* view, const PuglEventKey* event) +onKeyPress(PuglView* view, const PuglEventKey* event, const char* prefix) { PuglTestApp* app = (PuglTestApp*)puglGetHandle(view); PuglRect frame = puglGetFrame(view); @@ -156,6 +156,14 @@ onKeyPress(PuglView* view, const PuglEventKey* event) swapFocus(app); } else 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, "Pugl test", strlen("Pugl test") + 1); + fprintf(stderr, "%sCopy \"Pugl test\"\n", prefix); + } else if (event->state & PUGL_MOD_CTRL && event->key == 'v') { + const char* type = NULL; + size_t len = 0; + const char* text = (const char*)puglGetClipboard(view, &type, &len); + fprintf(stderr, "%sPaste \"%s\"\n", prefix, text); } else if (event->state & PUGL_MOD_SHIFT) { if (event->key == PUGL_KEY_UP) { frame.height += 10; @@ -220,7 +228,7 @@ onParentEvent(PuglView* view, const PuglEvent* event) } break; case PUGL_KEY_PRESS: - onKeyPress(view, &event->key); + onKeyPress(view, &event->key, "Parent: "); break; case PUGL_MOTION_NOTIFY: break; @@ -252,7 +260,7 @@ onEvent(PuglView* view, const PuglEvent* event) app->quit = 1; break; case PUGL_KEY_PRESS: - onKeyPress(view, &event->key); + onKeyPress(view, &event->key, "Child: "); break; case PUGL_MOTION_NOTIFY: app->xAngle = fmodf(app->xAngle - (float)(event->motion.x - app->lastMouseX), 360.0f); |