aboutsummaryrefslogtreecommitdiffstats
path: root/src/x11.c
diff options
context:
space:
mode:
authorDavid Robillard <d@drobilla.net>2022-05-22 16:02:04 -0400
committerDavid Robillard <d@drobilla.net>2023-11-11 10:20:03 -0500
commit91ba4a52701db0a43ffc7769d2fda510ca2ebfa3 (patch)
treebe2dca91d457f28e0c9c324736f24d85a5ed57c2 /src/x11.c
parent91051e9059b67b8d633e385afb48a36d4f9467ba (diff)
downloadpugl-dragdrop.tar.gz
pugl-dragdrop.tar.bz2
pugl-dragdrop.zip
[WIP] Add support for drag and dropdragdrop
Diffstat (limited to 'src/x11.c')
-rw-r--r--src/x11.c310
1 files changed, 296 insertions, 14 deletions
diff --git a/src/x11.c b/src/x11.c
index a66ac30..a6f4934 100644
--- a/src/x11.c
+++ b/src/x11.c
@@ -78,6 +78,8 @@
}
#endif
+static const Atom PUGL_DND_PROTOCOL_VERSION = 5;
+
enum WmClientStateMessageAction {
WM_STATE_REMOVE,
WM_STATE_ADD,
@@ -229,8 +231,21 @@ puglInitWorldInternals(const PuglWorldType type, const PuglWorldFlags flags)
impl->atoms.NET_WM_WINDOW_TYPE_UTILITY =
XInternAtom(display, "_NET_WM_WINDOW_TYPE_UTILITY", 0);
- impl->atoms.TARGETS = XInternAtom(display, "TARGETS", 0);
- impl->atoms.text_uri_list = XInternAtom(display, "text/uri-list", 0);
+ impl->atoms.TARGETS = XInternAtom(display, "TARGETS", 0);
+ impl->atoms.XdndActionCopy = XInternAtom(display, "XdndActionCopy", 0);
+ impl->atoms.XdndActionLink = XInternAtom(display, "XdndActionLink", 0);
+ impl->atoms.XdndActionMove = XInternAtom(display, "XdndActionMove", 0);
+ impl->atoms.XdndActionPrivate = XInternAtom(display, "XdndActionPrivate", 0);
+ impl->atoms.XdndAware = XInternAtom(display, "XdndAware", 0);
+ impl->atoms.XdndDrop = XInternAtom(display, "XdndDrop", 0);
+ impl->atoms.XdndEnter = XInternAtom(display, "XdndEnter", 0);
+ impl->atoms.XdndFinished = XInternAtom(display, "XdndFinished", 0);
+ impl->atoms.XdndLeave = XInternAtom(display, "XdndLeave", 0);
+ impl->atoms.XdndPosition = XInternAtom(display, "XdndPosition", 0);
+ impl->atoms.XdndSelection = XInternAtom(display, "XdndSelection", 0);
+ impl->atoms.XdndStatus = XInternAtom(display, "XdndStatus", 0);
+ impl->atoms.XdndTypeList = XInternAtom(display, "XdndTypeList", 0);
+ impl->atoms.text_uri_list = XInternAtom(display, "text/uri-list", 0);
// Open input method
XSetLocaleModifiers("");
@@ -260,6 +275,9 @@ puglInitViewInternals(PuglWorld* const world)
impl->clipboard.clipboard = PUGL_CLIPBOARD_GENERAL;
impl->clipboard.selection = world->impl->atoms.CLIPBOARD;
impl->clipboard.property = XA_PRIMARY;
+ impl->drag.clipboard = PUGL_CLIPBOARD_DRAG;
+ impl->drag.selection = world->impl->atoms.XdndSelection;
+ impl->drag.property = world->impl->atoms.XdndSelection;
#if USE_XCURSOR
impl->cursorName = cursorNames[PUGL_CURSOR_ARROW];
@@ -706,6 +724,16 @@ puglRealize(PuglView* const view)
(XIM)0);
}
+ // DnD Step 0: Set XdndAware property to announce that we support DnD
+ XChangeProperty(display,
+ impl->win,
+ world->impl->atoms.XdndAware,
+ XA_ATOM,
+ 32,
+ PropModeReplace,
+ (const unsigned char*)&PUGL_DND_PROTOCOL_VERSION,
+ 1);
+
st = puglDispatchSimpleEvent(view, PUGL_REALIZE);
/* Flush before returning for two reasons: so that hints are available to the
@@ -955,21 +983,46 @@ getAtomProperty(PuglView* const view,
static PuglX11Clipboard*
getX11Clipboard(PuglView* const view, const PuglClipboard clipboard)
{
- return clipboard == PUGL_CLIPBOARD_GENERAL ? &view->impl->clipboard : NULL;
+ return clipboard == PUGL_CLIPBOARD_DRAG ? &view->impl->drag
+ : &view->impl->clipboard;
}
static const PuglX11Clipboard*
getConstX11Clipboard(const PuglView* const view, const PuglClipboard clipboard)
{
- return clipboard == PUGL_CLIPBOARD_GENERAL ? &view->impl->clipboard : NULL;
+ return clipboard == PUGL_CLIPBOARD_DRAG ? &view->impl->drag
+ : &view->impl->clipboard;
}
static PuglX11Clipboard*
getX11SelectionClipboard(PuglView* const view, const Atom selection)
{
- return (selection == view->world->impl->atoms.CLIPBOARD)
- ? getX11Clipboard(view, PUGL_CLIPBOARD_GENERAL)
- : NULL;
+ if (selection == view->world->impl->atoms.CLIPBOARD) {
+ return getX11Clipboard(view, PUGL_CLIPBOARD_GENERAL);
+ }
+
+ if (selection == view->world->impl->atoms.XdndSelection) {
+ return getX11Clipboard(view, PUGL_CLIPBOARD_DRAG);
+ }
+
+ return NULL;
+}
+
+static Atom
+getX11ActionAtom(const PuglView* const view, const PuglAction action)
+{
+ switch (action) {
+ case PUGL_ACTION_COPY:
+ return view->world->impl->atoms.XdndActionCopy;
+ case PUGL_ACTION_LINK:
+ return view->world->impl->atoms.XdndActionLink;
+ case PUGL_ACTION_MOVE:
+ return view->world->impl->atoms.XdndActionMove;
+ case PUGL_ACTION_PRIVATE:
+ break;
+ }
+
+ return view->world->impl->atoms.XdndActionPrivate;
}
static PuglStatus
@@ -1036,6 +1089,7 @@ setClipboardFormats(PuglView* const view,
static PuglEvent
translateClientMessage(PuglView* const view, XClientMessageEvent message)
{
+ PuglInternals* const impl = view->impl;
Display* const display = view->world->impl->display;
const PuglX11Atoms* const atoms = &view->world->impl->atoms;
PuglEvent event = {{PUGL_NOTHING, 0}};
@@ -1055,10 +1109,102 @@ translateClientMessage(PuglView* const view, XClientMessageEvent message)
SubstructureNotifyMask | SubstructureRedirectMask,
&reply);
}
+
} else if (message.message_type == atoms->PUGL_CLIENT_MSG) {
event.type = PUGL_CLIENT;
event.client.data1 = (uintptr_t)message.data.l[0];
event.client.data2 = (uintptr_t)message.data.l[1];
+
+ } else if (message.message_type == atoms->XdndEnter) {
+ // DnD Step 2: Target receives XdndEnter (drag entered target window)
+ // int version = (int)(message.data.l[1] >> 24);
+ impl->drag.source = (Window)message.data.l[0];
+
+ unsigned long numFormats = 0;
+ Atom* formats = NULL;
+ const bool isList = message.data.l[1] & 1;
+ if (!isList) {
+ // Up to three formats can be inlined in the event itself
+ numFormats = 3;
+ formats = (Atom*)message.data.l + 2;
+ } else {
+ // Longer lists must be retrieved from a property on the source
+ if (getAtomProperty(view,
+ impl->drag.source,
+ atoms->XdndTypeList,
+ &numFormats,
+ &formats)) {
+ return event;
+ }
+ }
+
+ // Set the available formats on the drag board
+ setClipboardFormats(view, &view->impl->drag, numFormats, formats);
+ if (isList && formats) {
+ XFree(formats);
+ }
+
+ // The offer to the application will be made when the XdndPosition comes
+
+ } else if (message.message_type == atoms->XdndPosition) {
+ // DnD Step 4: Target receives XdndPosition (drag moves in target window)
+ const Time time = (Time)message.data.l[3];
+ const int root_x = (uint16_t)((message.data.l[2] >> 16) & 0xFFFF);
+ const int root_y = (uint16_t)((message.data.l[2]) & 0xFFFF);
+
+ // Translate root coordinates from message to window coordinates
+ Window child = 0;
+ int win_x = 0;
+ int win_y = 0;
+ XTranslateCoordinates(display,
+ RootWindow(display, impl->screen),
+ impl->win,
+ root_x,
+ root_y,
+ &win_x,
+ &win_y,
+ &child);
+
+ // DnD Step 5: Target tells the source whether it will accept
+ XEvent reply = {ClientMessage};
+ reply.xclient.display = display;
+ reply.xclient.window = impl->drag.source;
+ reply.xclient.message_type = atoms->XdndStatus;
+ reply.xclient.format = 32;
+ reply.xclient.data.l[0] = (long)impl->win; // Target window
+ reply.xclient.data.l[1] = 0; // Flags
+ reply.xclient.data.l[2] = 0; // Accepting rectangle X,Y
+ reply.xclient.data.l[3] = 0; // Accepting rectangle W,H
+
+ if (view->impl->drag.acceptedFormat) {
+ reply.xclient.data.l[1] = 1; // Set bit 0: target will accept drop
+ reply.xclient.data.l[4] =
+ (long)getX11ActionAtom(view, impl->drag.acceptedAction);
+ }
+
+ XSendEvent(display, impl->drag.source, False, NoEventMask, &reply);
+
+ // Send data offer event to the application
+ event.type = PUGL_DATA_OFFER;
+ event.offer.time = (double)time / 1e3;
+ event.offer.x = win_x;
+ event.offer.y = win_y;
+ event.offer.clipboard = PUGL_CLIPBOARD_DRAG;
+ } else if (message.message_type == atoms->XdndLeave) {
+ // Step 8a: Target receives XdndLeave (drag aborted)
+ clearX11Clipboard(&impl->drag);
+ } else if (message.message_type == atoms->XdndDrop) {
+ // Step 8b: Target receives XdndDrop (data dropped in target window)
+ if (impl->drag.acceptedFormat) {
+ // Request the data with the desired format at the given time
+ // The drag will be finished by the SelectionNotify response
+ XConvertSelection(display,
+ atoms->XdndSelection,
+ impl->drag.acceptedFormat,
+ atoms->XdndSelection,
+ impl->win,
+ (Time)message.data.l[2]);
+ }
}
return event;
@@ -1619,11 +1765,15 @@ handleSelectionNotify(const PuglWorld* const world,
{
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 (!board) {
+ return PUGL_SUCCESS; // Ignore unknown selection
+ }
+ PuglInternals* const impl = view->impl;
+ Display* const display = view->world->impl->display;
+ PuglEvent puglEvent = {{PUGL_NOTHING, 0}};
if (event->target == atoms->TARGETS) {
// Notification of available datatypes
unsigned long numFormats = 0;
@@ -1663,6 +1813,37 @@ handleSelectionNotify(const PuglWorld* const world,
puglEvent.data = data;
}
+
+ } else if (event->selection == atoms->XdndSelection) {
+ if (!retrieveSelection(world,
+ view,
+ event->property,
+ impl->drag.acceptedFormat,
+ &board->data)) {
+ board->source = impl->drag.source;
+
+ // Notify the drag source that the operation is finished
+ XEvent reply = {ClientMessage};
+ reply.xclient.window = impl->win;
+ reply.xclient.message_type = atoms->XdndFinished;
+ reply.xclient.format = 32;
+ reply.xclient.data.l[0] = (long)impl->win; // Target
+ reply.xclient.data.l[1] = 1; // Accepted (TODO)
+ reply.xclient.data.l[2] = None; // Action (TODO)
+
+ XSendEvent(display, impl->drag.source, False, NoEventMask, &reply);
+
+ // Send a data event to the application so it can retrieve the data
+ const PuglDataEvent data = {PUGL_DATA,
+ 0U,
+ (double)event->time / 1e3,
+ 0.0,
+ 0.0,
+ board->clipboard,
+ board->acceptedFormatIndex};
+
+ puglEvent.data = data;
+ }
}
return puglDispatchEvent(view, &puglEvent);
@@ -1867,6 +2048,9 @@ dispatchX11Events(PuglWorld* const world)
}
}
+ // Flush any events we may have sent in this frame to reduce latency
+ XFlush(display);
+
return st;
}
@@ -2113,6 +2297,32 @@ puglGetClipboard(PuglView* const view,
return board->data.data;
}
+static XRectangle
+rootRectangle(const PuglView* const view, const PuglRect rect)
+{
+ Display* const display = view->world->impl->display;
+ const Window root = RootWindow(display, view->impl->screen);
+ const int x = (int)floor(rect.x);
+ const int y = (int)floor(rect.y);
+ const int w = (int)ceil(rect.width);
+ const int h = (int)ceil(rect.height);
+
+ Window child = 0;
+ int rootX = 0;
+ int rootY = 0;
+ XTranslateCoordinates(
+ display, view->impl->win, root, x, y, &rootX, &rootY, &child);
+
+ const XRectangle result = {
+ (short)rootX,
+ (short)rootY,
+ (unsigned short)w,
+ (unsigned short)h,
+ };
+
+ return result;
+}
+
PuglStatus
puglAcceptOffer(PuglView* const view,
const PuglDataOfferEvent* const offer,
@@ -2120,11 +2330,10 @@ puglAcceptOffer(PuglView* const view,
PuglAction action,
const PuglRect region)
{
- (void)region;
-
- PuglInternals* const impl = view->impl;
- Display* const display = view->world->impl->display;
- PuglX11Clipboard* const board = getX11Clipboard(view, offer->clipboard);
+ PuglInternals* const impl = view->impl;
+ Display* const display = view->world->impl->display;
+ const PuglX11Atoms* const atoms = &view->world->impl->atoms;
+ PuglX11Clipboard* const board = getX11Clipboard(view, offer->clipboard);
board->acceptedAction = action;
board->acceptedFormatIndex = typeIndex;
@@ -2142,6 +2351,68 @@ puglAcceptOffer(PuglView* const view,
return PUGL_SUCCESS;
}
+ // DnD Step 5a: Target tells the source it will accept the drop
+
+ if (!board->source) {
+ return PUGL_FAILURE;
+ }
+
+ const XRectangle rootRegion = rootRectangle(view, region);
+ const long rootPos = (rootRegion.x << 16) | rootRegion.y;
+ const long rootSize = (rootRegion.width << 16) | rootRegion.height;
+ XEvent reply = {ClientMessage};
+
+ reply.xclient.display = display;
+ reply.xclient.window = impl->drag.source;
+ reply.xclient.message_type = atoms->XdndStatus;
+ reply.xclient.format = 32;
+ reply.xclient.data.l[0] = (long)impl->win;
+ reply.xclient.data.l[1] = 1;
+ reply.xclient.data.l[2] = rootPos;
+ reply.xclient.data.l[3] = rootSize;
+ reply.xclient.data.l[4] =
+ (long)getX11ActionAtom(view, impl->drag.acceptedAction);
+
+ return XSendEvent(display, impl->drag.source, False, NoEventMask, &reply)
+ ? PUGL_SUCCESS
+ : PUGL_FAILURE;
+}
+
+PuglStatus
+puglRejectOffer(PuglView* const view,
+ const PuglDataOfferEvent* const offer,
+ const PuglRect region)
+{
+ PuglInternals* const impl = view->impl;
+ Display* const display = view->world->impl->display;
+ const PuglX11Atoms* const atoms = &view->world->impl->atoms;
+ PuglX11Clipboard* const board = getX11Clipboard(view, offer->clipboard);
+ if (!board->source) {
+ return PUGL_FAILURE;
+ }
+
+ if (offer->clipboard == PUGL_CLIPBOARD_DRAG) {
+ // DnD Step 5b: Target tells the source it will refuse the drop
+
+ const XRectangle rootRegion = rootRectangle(view, region);
+ const long rootPos = (rootRegion.x << 16) | rootRegion.y;
+ const long rootSize = (rootRegion.width << 16) | rootRegion.height;
+ XEvent reply = {ClientMessage};
+
+ reply.xclient.display = display;
+ reply.xclient.window = impl->drag.source;
+ reply.xclient.message_type = atoms->XdndStatus;
+ reply.xclient.format = 32;
+ reply.xclient.data.l[0] = (long)impl->win;
+ reply.xclient.data.l[1] = 0;
+ reply.xclient.data.l[2] = rootPos;
+ reply.xclient.data.l[3] = rootSize;
+
+ return XSendEvent(display, impl->drag.source, False, NoEventMask, &reply)
+ ? PUGL_SUCCESS
+ : PUGL_FAILURE;
+ }
+
return PUGL_FAILURE;
}
@@ -2163,6 +2434,17 @@ puglPaste(PuglView* const view)
return PUGL_SUCCESS;
}
+PuglStatus
+puglRegisterDragType(PuglView* const view, const char* const type)
+{
+ /* There is the XdndTypeList property for sources to declare more than three
+ types, but no need to register as a target for types ahead of time. */
+
+ (void)view;
+ (void)type;
+ return PUGL_SUCCESS;
+}
+
uint32_t
puglGetNumClipboardTypes(const PuglView* const view,
const PuglClipboard clipboard)