LookingGlass/client/displayservers/X11/x11.c
Tudor Brindus 56c80a15e6 [client] wayland: gracefully degrade when protocols are unsupported
zwp_relative_pointer_manager_v1 and zwp_pointer_constraints_v1 are
supported by GNOME/KDE/sway (and most other compositors), but they are
not a required part of the protocol.

Some users also run software in one-off nested compositors like cage[0]
for an extra layer of isolation; cage, at least, does not support
pointer captures.

This commit makes Looking Glass warn when an optional protocol is
unsupported, and fail if a required one is missing. Pointer grab paths
have a new guard against the aforementioned protocols being missing.

[0]: https://github.com/Hjdskes/cage
2021-01-17 10:19:22 +11:00

795 lines
18 KiB
C

/*
Looking Glass - KVM FrameRelay (KVMFR) Client
Copyright (C) 2017-2021 Geoffrey McRae <geoff@hostfission.com>
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 "interface/displayserver.h"
#include <SDL2/SDL.h>
#include <X11/Xlib.h>
#include <GL/glx.h>
#include <stdbool.h>
#include <string.h>
#include <unistd.h>
#if SDL_VIDEO_DRIVER_X11_XINPUT2
#include <X11/extensions/XInput2.h>
#endif
#include <X11/extensions/Xfixes.h>
#include "app.h"
#include "common/debug.h"
struct X11DSState
{
Display * display;
Window window;
int xinputOp;
bool pointerGrabbed;
bool keyboardGrabbed;
// clipboard members
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;
// XFixes vars
int eventBase;
int errorBase;
};
static const char * atomTypes[] =
{
"UTF8_STRING",
"image/png",
"image/bmp",
"image/tiff",
"image/jpeg"
};
static struct X11DSState x11;
// 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);
static bool x11Init(SDL_SysWMinfo * info)
{
memset(&x11, 0, sizeof(x11));
x11.display = info->info.x11.display;
x11.window = info->info.x11.window;
int event, error;
// enable X11 events to work around SDL2 bugs
SDL_EventState(SDL_SYSWMEVENT, SDL_ENABLE);
#if SDL_VIDEO_DRIVER_X11_XINPUT2
XQueryExtension(x11.display, "XInputExtension", &x11.xinputOp, &event, &error);
#endif
Atom NETWM_BYPASS_COMPOSITOR = XInternAtom(x11.display,
"NETWM_BYPASS_COMPOSITOR", False);
unsigned long value = 1;
XChangeProperty(
x11.display,
x11.window,
NETWM_BYPASS_COMPOSITOR,
XA_CARDINAL,
32,
PropModeReplace,
(unsigned char *)&value,
1
);
return true;
}
static void x11Startup(void)
{
}
static void x11Shutdown(void)
{
}
static void x11Free(void)
{
}
static bool x11GetProp(LG_DSProperty prop, void *ret)
{
if (prop != LG_DS_MAX_MULTISAMPLE)
return false;
Display * dpy = XOpenDisplay(NULL);
if (!dpy)
return false;
XVisualInfo queryTemplate;
queryTemplate.screen = 0;
int visualCount;
int maxSamples = -1;
XVisualInfo * visuals = XGetVisualInfo(dpy, VisualScreenMask,
&queryTemplate, &visualCount);
for (int i = 0; i < visualCount; i++)
{
XVisualInfo * visual = &visuals[i];
int res, supportsGL;
// Some GLX visuals do not use GL, and these must be ignored in our search.
if ((res = glXGetConfig(dpy, visual, GLX_USE_GL, &supportsGL)) != 0 || !supportsGL)
continue;
int sampleBuffers, samples;
if ((res = glXGetConfig(dpy, visual, GLX_SAMPLE_BUFFERS, &sampleBuffers)) != 0)
continue;
// Will be 1 if this visual supports multisampling
if (sampleBuffers != 1)
continue;
if ((res = glXGetConfig(dpy, visual, GLX_SAMPLES, &samples)) != 0)
continue;
// Track the largest number of samples supported
if (samples > maxSamples)
maxSamples = samples;
}
XCloseDisplay(dpy);
*(int*)ret = maxSamples;
return true;
}
static bool x11EventFilter(SDL_Event * event)
{
/* prevent the default processing for the following events */
switch(event->type)
{
case SDL_WINDOWEVENT:
switch(event->window.event)
{
case SDL_WINDOWEVENT_SIZE_CHANGED:
case SDL_WINDOWEVENT_RESIZED:
case SDL_WINDOWEVENT_CLOSE:
return true;
}
return false;
#if SDL_VIDEO_DRIVER_X11_XINPUT2
case SDL_MOUSEMOTION:
return true;
#endif
}
if (event->type != SDL_SYSWMEVENT)
return false;
XEvent xe = event->syswm.msg->msg.x11.event;
switch(xe.type)
{
case ConfigureNotify:
{
int x, y;
/* the window may have been re-parented so we need to translate to
* ensure we get the screen top left position of the window */
Window child;
XTranslateCoordinates(
x11.display,
x11.window,
DefaultRootWindow(x11.display),
0, 0,
&x,
&y,
&child);
app_updateWindowPos(x, y);
app_handleResizeEvent(xe.xconfigure.width, xe.xconfigure.height);
return true;
}
case EnterNotify:
{
int x, y;
Window child;
XTranslateCoordinates(
x11.display,
DefaultRootWindow(x11.display),
x11.window,
xe.xcrossing.x_root, xe.xcrossing.y_root,
&x, &y,
&child);
app_updateCursorPos(x, y);
app_handleWindowEnter();
return true;
}
case LeaveNotify:
{
if (xe.xcrossing.mode != NotifyNormal)
return true;
int x, y;
Window child;
XTranslateCoordinates(x11.display,
DefaultRootWindow(x11.display),
x11.window,
xe.xcrossing.x_root, xe.xcrossing.y_root,
&x, &y,
&child);
app_updateCursorPos(x, y);
app_handleWindowLeave();
return true;
}
#if SDL_VIDEO_DRIVER_X11_XINPUT2
/* support movements via XInput2 */
case GenericEvent:
{
XGenericEventCookie *cookie = (XGenericEventCookie*)&xe.xcookie;
if (cookie->extension != x11.xinputOp)
return false;
if (!app_inputEnabled())
return true;
switch(cookie->evtype)
{
case XI_Motion:
{
if (!app_cursorInWindow())
return true;
XIDeviceEvent *device = cookie->data;
app_updateCursorPos(device->event_x, device->event_y);
return true;
}
case XI_RawMotion:
{
if (!app_cursorInWindow())
return true;
XIRawEvent *raw = cookie->data;
double raw_axis[2];
double axis[2];
/* select the active validators for the X & Y axis */
double *valuator = raw->valuators.values;
double *r_value = raw->raw_values;
int count = 0;
for(int i = 0; i < raw->valuators.mask_len * 8; ++i)
{
if (XIMaskIsSet(raw->valuators.mask, i))
{
raw_axis[count] = *r_value;
axis [count] = *valuator;
++count;
if (count == 2)
break;
++valuator;
++r_value;
}
}
/* filter out scroll wheel and other events */
if (count < 2)
return true;
/* filter out duplicate events */
static Time prev_time = 0;
static double prev_axis[2] = {0};
if (raw->time == prev_time &&
axis[0] == prev_axis[0] &&
axis[1] == prev_axis[1])
return true;
prev_time = raw->time;
prev_axis[0] = axis[0];
prev_axis[1] = axis[1];
if (app_cursorIsGrabbed())
{
if (app_cursorWantsRaw())
app_handleMouseGrabbed(raw_axis[0], raw_axis[1]);
else
app_handleMouseGrabbed(axis[0], axis[1]);
}
else
if (app_cursorInWindow())
app_handleMouseNormal(axis[0], axis[1]);
return true;
}
}
return false;
}
#endif
// clipboard events
case SelectionRequest:
x11CBSelectionRequest(xe.xselectionrequest);
return true;
case SelectionClear:
x11CBSelectionClear(xe.xselectionclear);
return true;
case SelectionNotify:
x11CBSelectionNotify(xe.xselection);
return true;
case PropertyNotify:
if (xe.xproperty.display != x11.display ||
xe.xproperty.window != x11.window ||
xe.xproperty.atom != x11.aSelData ||
xe.xproperty.state != PropertyNewValue ||
x11.lowerBound == 0)
return false;
x11CBSelectionIncr(xe.xproperty);
return true;
default:
if (xe.type == x11.eventBase + XFixesSelectionNotify)
{
XFixesSelectionNotifyEvent * sne = (XFixesSelectionNotifyEvent *)&xe;
x11CBXFixesSelectionNotify(*sne);
return true;
}
return false;
}
}
static void x11GrabPointer(void)
{
if (x11.pointerGrabbed)
return;
XGrabPointer(
x11.display,
x11.window,
true,
None,
GrabModeAsync,
GrabModeAsync,
x11.window,
None,
CurrentTime);
x11.pointerGrabbed = true;
}
static void x11UngrabPointer(void)
{
if (!x11.pointerGrabbed)
return;
XUngrabPointer(x11.display, CurrentTime);
x11.pointerGrabbed = false;
}
static void x11GrabKeyboard(void)
{
if (x11.keyboardGrabbed)
return;
XGrabKeyboard(
x11.display,
x11.window,
true,
GrabModeAsync,
GrabModeAsync,
CurrentTime);
x11.keyboardGrabbed = true;
}
static void x11UngrabKeyboard(void)
{
if (!x11.keyboardGrabbed)
return;
XUngrabKeyboard(x11.display, CurrentTime);
x11.keyboardGrabbed = false;
}
static void x11WarpMouse(int x, int y, bool exiting)
{
XWarpPointer(
x11.display,
None,
x11.window,
0, 0, 0, 0,
x, y);
XSync(x11.display, False);
}
static bool x11CBInit()
{
x11.aSelection = XInternAtom(x11.display, "CLIPBOARD" , False);
x11.aTargets = XInternAtom(x11.display, "TARGETS" , False);
x11.aSelData = XInternAtom(x11.display, "SEL_DATA" , False);
x11.aIncr = XInternAtom(x11.display, "INCR" , False);
x11.aCurSelection = BadValue;
for(int i = 0; i < LG_CLIPBOARD_DATA_NONE; ++i)
{
x11.aTypes[i] = XInternAtom(x11.display, atomTypes[i], False);
if (x11.aTypes[i] == BadAlloc || x11.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,
x11.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 (!x11.haveRequest)
goto nodata;
// target list requested
if (e.target == x11.aTargets)
{
Atom targets[2];
targets[0] = x11.aTargets;
targets[1] = x11.aTypes[x11.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 (x11.aTypes[i] == e.target && x11.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 != x11.aSelection)
return;
x11.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
x11.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 (x11.aTypes[dataType] == type)
break;
if (dataType == LG_CLIPBOARD_DATA_NONE)
{
DEBUG_WARN("clipboard data (%s) not in a supported format",
XGetAtomName(x11.display, type));
x11.lowerBound = 0;
app_clipboardNotify(LG_CLIPBOARD_DATA_NONE, 0);
goto out;
}
if (x11.incrStart)
{
app_clipboardNotify(dataType, x11.lowerBound);
x11.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);
x11.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 != x11.aSelection) ||
e.owner == x11.window || e.owner == 0)
{
return;
}
// remember which selection we are working with
x11.aCurSelection = e.selection;
XConvertSelection(
x11.display,
e.selection,
x11.aTargets,
x11.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 == x11.aIncr)
{
x11.incrStart = true;
x11.lowerBound = *(unsigned int *)data;
goto out;
}
// the target list
if (e.property == x11.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 (x11.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 == x11.aSelData)
{
LG_ClipboardData dataType;
for(dataType = 0; dataType < LG_CLIPBOARD_DATA_NONE; ++dataType)
if (x11.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);
}
static void x11CBNotice(LG_ClipboardData type)
{
x11.haveRequest = true;
x11.type = type;
XSetSelectionOwner(x11.display, XA_PRIMARY , x11.window, CurrentTime);
XSetSelectionOwner(x11.display, x11.aSelection, x11.window, CurrentTime);
XFlush(x11.display);
}
static void x11CBRelease(void)
{
x11.haveRequest = false;
XSetSelectionOwner(x11.display, XA_PRIMARY , None, CurrentTime);
XSetSelectionOwner(x11.display, x11.aSelection, None, CurrentTime);
XFlush(x11.display);
}
static void x11CBRequest(LG_ClipboardData type)
{
if (x11.aCurSelection == BadValue)
return;
XConvertSelection(
x11.display,
x11.aCurSelection,
x11.aTypes[type],
x11.aSelData,
x11.window,
CurrentTime);
}
struct LG_DisplayServerOps LGDS_X11 =
{
.subsystem = SDL_SYSWM_X11,
.init = x11Init,
.startup = x11Startup,
.shutdown = x11Shutdown,
.free = x11Free,
.getProp = x11GetProp,
.eventFilter = x11EventFilter,
.grabPointer = x11GrabPointer,
.ungrabPointer = x11UngrabPointer,
.grabKeyboard = x11GrabKeyboard,
.ungrabKeyboard = x11UngrabKeyboard,
.warpMouse = x11WarpMouse,
.cbInit = x11CBInit,
.cbNotice = x11CBNotice,
.cbRelease = x11CBRelease,
.cbRequest = x11CBRequest
};