/* Looking Glass - KVM FrameRelay (KVMFR) Client Copyright (C) 2017-2021 Geoffrey McRae https://looking-glass.hostfission.com This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #include "clipboard.h" #include "x11.h" #include #include #include #include #include "app.h" #include "common/debug.h" struct X11ClipboardState { Atom aSelection; Atom aCurSelection; Atom aTargets; Atom aSelData; Atom aIncr; Atom aTypes[LG_CLIPBOARD_DATA_NONE]; LG_ClipboardData type; bool haveRequest; bool incrStart; unsigned int lowerBound; }; static const char * atomTypes[] = { "UTF8_STRING", "image/png", "image/bmp", "image/tiff", "image/jpeg" }; static struct X11ClipboardState x11cb; // forwards static void x11CBSelectionRequest(const XSelectionRequestEvent e); static void x11CBSelectionClear(const XSelectionClearEvent e); static void x11CBSelectionIncr(const XPropertyEvent e); static void x11CBSelectionNotify(const XSelectionEvent e); static void x11CBXFixesSelectionNotify(const XFixesSelectionNotifyEvent e); void x11CBEventThread(const XEvent xe) { switch(xe.type) { case SelectionRequest: x11CBSelectionRequest(xe.xselectionrequest); break; case SelectionClear: x11CBSelectionClear(xe.xselectionclear); break; case SelectionNotify: x11CBSelectionNotify(xe.xselection); break; case PropertyNotify: if (xe.xproperty.atom == x11cb.aSelData) { if (x11cb.lowerBound == 0) break; x11CBSelectionIncr(xe.xproperty); break; } break; default: if (xe.type == x11.eventBase + XFixesSelectionNotify) { XFixesSelectionNotifyEvent * sne = (XFixesSelectionNotifyEvent *)&xe; x11CBXFixesSelectionNotify(*sne); } break; } } bool x11CBInit() { x11cb.aSelection = XInternAtom(x11.display, "CLIPBOARD" , False); x11cb.aTargets = XInternAtom(x11.display, "TARGETS" , False); x11cb.aSelData = XInternAtom(x11.display, "SEL_DATA" , False); x11cb.aIncr = XInternAtom(x11.display, "INCR" , False); x11cb.aCurSelection = BadValue; for(int i = 0; i < LG_CLIPBOARD_DATA_NONE; ++i) { x11cb.aTypes[i] = XInternAtom(x11.display, atomTypes[i], False); if (x11cb.aTypes[i] == BadAlloc || x11cb.aTypes[i] == BadValue) { DEBUG_ERROR("failed to get atom for type: %s", atomTypes[i]); return false; } } // use xfixes to get clipboard change notifications if (!XFixesQueryExtension(x11.display, &x11.eventBase, &x11.errorBase)) { DEBUG_ERROR("failed to initialize xfixes"); return false; } XFixesSelectSelectionInput(x11.display, x11.window, XA_PRIMARY, XFixesSetSelectionOwnerNotifyMask); XFixesSelectSelectionInput(x11.display, x11.window, x11cb.aSelection, XFixesSetSelectionOwnerNotifyMask); return true; } static void x11CBReplyFn(void * opaque, LG_ClipboardData type, uint8_t * data, uint32_t size) { XEvent *s = (XEvent *)opaque; XChangeProperty( x11.display , s->xselection.requestor, s->xselection.property , s->xselection.target , 8, PropModeReplace, data, size); XSendEvent(x11.display, s->xselection.requestor, 0, 0, s); XFlush(x11.display); free(s); } static void x11CBSelectionRequest(const XSelectionRequestEvent e) { XEvent * s = (XEvent *)malloc(sizeof(XEvent)); s->xselection.type = SelectionNotify; s->xselection.requestor = e.requestor; s->xselection.selection = e.selection; s->xselection.target = e.target; s->xselection.property = e.property; s->xselection.time = e.time; if (!x11cb.haveRequest) goto nodata; // target list requested if (e.target == x11cb.aTargets) { Atom targets[2]; targets[0] = x11cb.aTargets; targets[1] = x11cb.aTypes[x11cb.type]; XChangeProperty( e.display, e.requestor, e.property, XA_ATOM, 32, PropModeReplace, (unsigned char*)targets, sizeof(targets) / sizeof(Atom)); goto send; } // look to see if we can satisfy the data type for(int i = 0; i < LG_CLIPBOARD_DATA_NONE; ++i) if (x11cb.aTypes[i] == e.target && x11cb.type == i) { // request the data app_clipboardRequest(x11CBReplyFn, s); return; } nodata: // report no data s->xselection.property = None; send: XSendEvent(x11.display, e.requestor, 0, 0, s); XFlush(x11.display); free(s); } static void x11CBSelectionClear(const XSelectionClearEvent e) { if (e.selection != XA_PRIMARY && e.selection != x11cb.aSelection) return; x11cb.aCurSelection = BadValue; app_clipboardRelease(); return; } static void x11CBSelectionIncr(const XPropertyEvent e) { Atom type; int format; unsigned long itemCount, after; unsigned char *data; if (XGetWindowProperty( e.display, e.window, e.atom, 0, ~0L, // start and length True, // delete the property x11cb.aIncr, &type, &format, &itemCount, &after, &data) != Success) { DEBUG_INFO("GetProp Failed"); app_clipboardNotify(LG_CLIPBOARD_DATA_NONE, 0); goto out; } LG_ClipboardData dataType; for(dataType = 0; dataType < LG_CLIPBOARD_DATA_NONE; ++dataType) if (x11cb.aTypes[dataType] == type) break; if (dataType == LG_CLIPBOARD_DATA_NONE) { DEBUG_WARN("clipboard data (%s) not in a supported format", XGetAtomName(x11.display, type)); x11cb.lowerBound = 0; app_clipboardNotify(LG_CLIPBOARD_DATA_NONE, 0); goto out; } if (x11cb.incrStart) { app_clipboardNotify(dataType, x11cb.lowerBound); x11cb.incrStart = false; } XFree(data); data = NULL; if (XGetWindowProperty( e.display, e.window, e.atom, 0, ~0L, // start and length True, // delete the property type, &type, &format, &itemCount, &after, &data) != Success) { DEBUG_ERROR("XGetWindowProperty Failed"); app_clipboardNotify(LG_CLIPBOARD_DATA_NONE, 0); goto out; } app_clipboardData(dataType, data, itemCount); x11cb.lowerBound -= itemCount; out: if (data) XFree(data); } static void x11CBXFixesSelectionNotify(const XFixesSelectionNotifyEvent e) { // check if the selection is valid and it isn't ourself if ((e.selection != XA_PRIMARY && e.selection != x11cb.aSelection) || e.owner == x11.window || e.owner == 0) { return; } // remember which selection we are working with x11cb.aCurSelection = e.selection; XConvertSelection( x11.display, e.selection, x11cb.aTargets, x11cb.aTargets, x11.window, CurrentTime); return; } static void x11CBSelectionNotify(const XSelectionEvent e) { if (e.property == None) return; Atom type; int format; unsigned long itemCount, after; unsigned char *data; if (XGetWindowProperty( e.display, e.requestor, e.property, 0, ~0L, // start and length True , // delete the property AnyPropertyType, &type, &format, &itemCount, &after, &data) != Success) { app_clipboardNotify(LG_CLIPBOARD_DATA_NONE, 0); goto out; } if (type == x11cb.aIncr) { x11cb.incrStart = true; x11cb.lowerBound = *(unsigned int *)data; goto out; } // the target list if (e.property == x11cb.aTargets) { // the format is 32-bit and we must have data // this is technically incorrect however as it's // an array of padded 64-bit values if (!data || format != 32) goto out; // see if we support any of the targets listed const uint64_t * targets = (const uint64_t *)data; for(unsigned long i = 0; i < itemCount; ++i) { for(int n = 0; n < LG_CLIPBOARD_DATA_NONE; ++n) if (x11cb.aTypes[n] == targets[i]) { // we have a match, so send the notification app_clipboardNotify(n, 0); goto out; } } // no matches app_clipboardNotify(LG_CLIPBOARD_DATA_NONE, 0); goto out; } if (e.property == x11cb.aSelData) { LG_ClipboardData dataType; for(dataType = 0; dataType < LG_CLIPBOARD_DATA_NONE; ++dataType) if (x11cb.aTypes[dataType] == type) break; if (dataType == LG_CLIPBOARD_DATA_NONE) { DEBUG_WARN("clipboard data (%s) not in a supported format", XGetAtomName(x11.display, type)); goto out; } app_clipboardData(dataType, data, itemCount); goto out; } out: if (data) XFree(data); } void x11CBNotice(LG_ClipboardData type) { x11cb.haveRequest = true; x11cb.type = type; XSetSelectionOwner(x11.display, XA_PRIMARY , x11.window, CurrentTime); XSetSelectionOwner(x11.display, x11cb.aSelection, x11.window, CurrentTime); XFlush(x11.display); } void x11CBRelease(void) { x11cb.haveRequest = false; XSetSelectionOwner(x11.display, XA_PRIMARY , None, CurrentTime); XSetSelectionOwner(x11.display, x11cb.aSelection, None, CurrentTime); XFlush(x11.display); } void x11CBRequest(LG_ClipboardData type) { if (x11cb.aCurSelection == BadValue) return; XConvertSelection( x11.display, x11cb.aCurSelection, x11cb.aTypes[type], x11cb.aSelData, x11.window, CurrentTime); }