436 lines
11 KiB
C++
436 lines
11 KiB
C++
// Copyright 2007-2023 The Mumble Developers. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style license
|
|
// that can be found in the LICENSE file at the root of the
|
|
// Mumble source tree or at <https://www.mumble.info/LICENSE>.
|
|
|
|
#include "GlobalShortcut_unix.h"
|
|
|
|
#include "Settings.h"
|
|
#include "Global.h"
|
|
|
|
#include <QtCore/QFileSystemWatcher>
|
|
#include <QtCore/QSocketNotifier>
|
|
|
|
#include <X11/Xlib.h>
|
|
#include <X11/Xutil.h>
|
|
|
|
#ifndef NO_XINPUT2
|
|
# include <X11/extensions/XI2.h>
|
|
# include <X11/extensions/XInput2.h>
|
|
#endif
|
|
|
|
#ifdef Q_OS_LINUX
|
|
# include <fcntl.h>
|
|
# include <linux/input.h>
|
|
#endif
|
|
|
|
// We have to use a global 'diagnostic ignored' pragmas because
|
|
// we still support old versions of GCC. (FreeBSD 9.3 ships with GCC 4.2)
|
|
#if defined(__GNUC__)
|
|
// ScreenCount(...) and so on are macros that access the private structure and
|
|
// cast their return value using old-style-casts. Hence we suppress these warnings
|
|
// for this section of code.
|
|
# pragma GCC diagnostic ignored "-Wold-style-cast"
|
|
// XKeycodeToKeysym is deprecated.
|
|
// For backwards compatibility reasons we want to keep using the
|
|
// old function as long as possible. The replacement function
|
|
// XkbKeycodeToKeysym requires the XKB extension which isn't
|
|
// guaranteed to be present.
|
|
# pragma GCC diagnostic ignored "-Wdeprecated-declarations"
|
|
#endif
|
|
|
|
/**
|
|
* Returns a platform specific GlobalShortcutEngine object.
|
|
*
|
|
* @see GlobalShortcutX
|
|
* @see GlobalShortcutMac
|
|
* @see GlobalShortcutWin
|
|
*/
|
|
GlobalShortcutEngine *GlobalShortcutEngine::platformInit() {
|
|
return new GlobalShortcutX();
|
|
}
|
|
|
|
GlobalShortcutX::GlobalShortcutX() {
|
|
iXIopcode = -1;
|
|
bRunning = false;
|
|
m_enabled = true;
|
|
|
|
display = XOpenDisplay(nullptr);
|
|
|
|
if (!display) {
|
|
qWarning("GlobalShortcutX: Unable to open dedicated display connection.");
|
|
return;
|
|
}
|
|
|
|
init();
|
|
}
|
|
|
|
GlobalShortcutX::~GlobalShortcutX() {
|
|
stop();
|
|
|
|
if (display) {
|
|
XCloseDisplay(display);
|
|
}
|
|
}
|
|
|
|
void GlobalShortcutX::stop() {
|
|
bRunning = false;
|
|
wait();
|
|
|
|
if (m_watcher) {
|
|
m_watcher->deleteLater();
|
|
m_watcher = nullptr;
|
|
}
|
|
if (m_notifier) {
|
|
m_notifier->deleteLater();
|
|
m_notifier = nullptr;
|
|
}
|
|
}
|
|
|
|
bool GlobalShortcutX::init() {
|
|
#ifdef Q_OS_LINUX
|
|
if (Global::get().s.bEnableEvdev) {
|
|
QString dir = QLatin1String("/dev/input");
|
|
m_watcher = new QFileSystemWatcher(QStringList(dir), this);
|
|
connect(m_watcher, SIGNAL(directoryChanged(const QString &)), this, SLOT(directoryChanged(const QString &)));
|
|
directoryChanged(dir);
|
|
|
|
if (qsKeyboards.isEmpty()) {
|
|
for (QFile *f : qmInputDevices) {
|
|
delete f;
|
|
}
|
|
qmInputDevices.clear();
|
|
|
|
delete m_watcher;
|
|
m_watcher = nullptr;
|
|
qWarning(
|
|
"GlobalShortcutX: Unable to open any keyboard input devices under /dev/input, falling back to XInput");
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
#endif
|
|
|
|
qsRootWindows.clear();
|
|
for (int i = 0; i < ScreenCount(display); ++i)
|
|
qsRootWindows.insert(RootWindow(display, i));
|
|
|
|
#ifndef NO_XINPUT2
|
|
int evt, error;
|
|
|
|
if (Global::get().s.bEnableXInput2 && XQueryExtension(display, "XInputExtension", &iXIopcode, &evt, &error)) {
|
|
int major = XI_2_Major;
|
|
int minor = XI_2_Minor;
|
|
int rc = XIQueryVersion(display, &major, &minor);
|
|
if (rc != BadRequest) {
|
|
qWarning("GlobalShortcutX: Using XI2 %d.%d", major, minor);
|
|
|
|
queryXIMasterList();
|
|
|
|
XIEventMask evmask;
|
|
unsigned char mask[(XI_LASTEVENT + 7) / 8];
|
|
|
|
memset(&evmask, 0, sizeof(evmask));
|
|
memset(mask, 0, sizeof(mask));
|
|
|
|
XISetMask(mask, XI_RawButtonPress);
|
|
XISetMask(mask, XI_RawButtonRelease);
|
|
XISetMask(mask, XI_RawKeyPress);
|
|
XISetMask(mask, XI_RawKeyRelease);
|
|
XISetMask(mask, XI_HierarchyChanged);
|
|
|
|
evmask.deviceid = XIAllDevices;
|
|
evmask.mask_len = sizeof(mask);
|
|
evmask.mask = mask;
|
|
|
|
for (Window w : qsRootWindows) {
|
|
XISelectEvents(display, w, &evmask, 1);
|
|
}
|
|
XFlush(display);
|
|
|
|
m_notifier = new QSocketNotifier(ConnectionNumber(display), QSocketNotifier::Read, this);
|
|
connect(m_notifier, SIGNAL(activated(int)), this, SLOT(displayReadyRead(int)));
|
|
|
|
return true;
|
|
}
|
|
}
|
|
#endif
|
|
qWarning("GlobalShortcutX: No XInput support, falling back to polled input. This wastes a lot of CPU resources, so "
|
|
"please enable one of the other methods.");
|
|
bRunning = true;
|
|
start(QThread::TimeCriticalPriority);
|
|
|
|
return true;
|
|
}
|
|
|
|
// Tight loop polling
|
|
void GlobalShortcutX::run() {
|
|
Window root = XDefaultRootWindow(display);
|
|
Window root_ret, child_ret;
|
|
int root_x, root_y;
|
|
int win_x, win_y;
|
|
unsigned int mask[2];
|
|
int idx = 0;
|
|
int next = 0;
|
|
char keys[2][32];
|
|
|
|
memset(keys[0], 0, 32);
|
|
memset(keys[1], 0, 32);
|
|
mask[0] = mask[1] = 0;
|
|
|
|
while (bRunning) {
|
|
if (bNeedRemap)
|
|
remap();
|
|
|
|
msleep(10);
|
|
|
|
idx = next;
|
|
next = idx ^ 1;
|
|
if (XQueryPointer(display, root, &root_ret, &child_ret, &root_x, &root_y, &win_x, &win_y, &mask[next])
|
|
&& XQueryKeymap(display, keys[next])) {
|
|
for (int i = 0; i < 256; ++i) {
|
|
int index = i / 8;
|
|
int keymask = 1 << (i % 8);
|
|
bool oldstate = (keys[idx][index] & keymask) != 0;
|
|
bool newstate = (keys[next][index] & keymask) != 0;
|
|
if (oldstate != newstate) {
|
|
handleButton(i, newstate);
|
|
}
|
|
}
|
|
for (int i = 8; i <= 12; ++i) {
|
|
bool oldstate = (mask[idx] & static_cast< unsigned int >(1 << i)) != 0;
|
|
bool newstate = (mask[next] & static_cast< unsigned int >(1 << i)) != 0;
|
|
if (oldstate != newstate) {
|
|
handleButton(0x110 + i, newstate);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Find XI2 master devices so they can be ignored.
|
|
void GlobalShortcutX::queryXIMasterList() {
|
|
#ifndef NO_XINPUT2
|
|
XIDeviceInfo *info, *dev;
|
|
int ndevices;
|
|
|
|
qsMasterDevices.clear();
|
|
|
|
dev = info = XIQueryDevice(display, XIAllDevices, &ndevices);
|
|
for (int i = 0; i < ndevices; ++i) {
|
|
switch (dev->use) {
|
|
case XIMasterPointer:
|
|
case XIMasterKeyboard:
|
|
qsMasterDevices.insert(dev->deviceid);
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
++dev;
|
|
}
|
|
XIFreeDeviceInfo(info);
|
|
#endif
|
|
}
|
|
|
|
bool GlobalShortcutX::canDisable() {
|
|
return true;
|
|
}
|
|
|
|
bool GlobalShortcutX::enabled() {
|
|
return m_enabled;
|
|
}
|
|
|
|
void GlobalShortcutX::setEnabled(bool enabled) {
|
|
if (enabled == m_enabled && (enabled != bRunning)) {
|
|
return;
|
|
}
|
|
|
|
m_enabled = enabled;
|
|
|
|
if (!m_enabled) {
|
|
stop();
|
|
} else {
|
|
m_enabled = init();
|
|
}
|
|
}
|
|
|
|
// XInput2 event is ready on socketnotifier.
|
|
void GlobalShortcutX::displayReadyRead(int) {
|
|
#ifndef NO_XINPUT2
|
|
XEvent evt;
|
|
|
|
if (bNeedRemap)
|
|
remap();
|
|
|
|
while (XPending(display)) {
|
|
XNextEvent(display, &evt);
|
|
XGenericEventCookie *cookie = &evt.xcookie;
|
|
|
|
if ((cookie->type != GenericEvent) || (cookie->extension != iXIopcode) || !XGetEventData(display, cookie))
|
|
continue;
|
|
|
|
XIDeviceEvent *xide = reinterpret_cast< XIDeviceEvent * >(cookie->data);
|
|
|
|
switch (cookie->evtype) {
|
|
case XI_RawKeyPress:
|
|
case XI_RawKeyRelease:
|
|
if (!qsMasterDevices.contains(xide->deviceid))
|
|
handleButton(xide->detail, cookie->evtype == XI_RawKeyPress);
|
|
break;
|
|
case XI_RawButtonPress:
|
|
case XI_RawButtonRelease:
|
|
if (!qsMasterDevices.contains(xide->deviceid))
|
|
handleButton(xide->detail + 0x117, cookie->evtype == XI_RawButtonPress);
|
|
break;
|
|
case XI_HierarchyChanged:
|
|
queryXIMasterList();
|
|
}
|
|
|
|
XFreeEventData(display, cookie);
|
|
}
|
|
#endif
|
|
}
|
|
|
|
// One of the raw /dev/input devices has ready input
|
|
void GlobalShortcutX::inputReadyRead(int) {
|
|
#ifdef Q_OS_LINUX
|
|
if (!Global::get().s.bEnableEvdev) {
|
|
return;
|
|
}
|
|
|
|
struct input_event ev;
|
|
|
|
if (bNeedRemap)
|
|
remap();
|
|
|
|
QFile *f = qobject_cast< QFile * >(sender()->parent());
|
|
if (!f)
|
|
return;
|
|
|
|
bool found = false;
|
|
|
|
while (f->read(reinterpret_cast< char * >(&ev), sizeof(ev)) == sizeof(ev)) {
|
|
found = true;
|
|
if (ev.type != EV_KEY)
|
|
continue;
|
|
bool down;
|
|
switch (ev.value) {
|
|
case 0:
|
|
down = false;
|
|
break;
|
|
case 1:
|
|
down = true;
|
|
break;
|
|
default:
|
|
continue;
|
|
}
|
|
int evtcode = ev.code + 8;
|
|
handleButton(evtcode, down);
|
|
}
|
|
|
|
if (!found) {
|
|
int fd = f->handle();
|
|
int version = 0;
|
|
if ((ioctl(fd, EVIOCGVERSION, &version) < 0) || (((version >> 16) & 0xFF) < 1)) {
|
|
qWarning("GlobalShortcutX: Removing dead input device %s", qPrintable(f->fileName()));
|
|
qmInputDevices.remove(f->fileName());
|
|
qsKeyboards.remove(f->fileName());
|
|
delete f;
|
|
}
|
|
}
|
|
#endif
|
|
}
|
|
|
|
#define test_bit(bit, array) (array[bit / 8] & (1 << (bit % 8)))
|
|
|
|
// The /dev/input directory changed
|
|
void GlobalShortcutX::directoryChanged(const QString &dir) {
|
|
#ifdef Q_OS_LINUX
|
|
if (!Global::get().s.bEnableEvdev) {
|
|
return;
|
|
}
|
|
|
|
QDir d(dir, QLatin1String("event*"), 0, QDir::System);
|
|
foreach (QFileInfo fi, d.entryInfoList()) {
|
|
QString path = fi.absoluteFilePath();
|
|
if (!qmInputDevices.contains(path)) {
|
|
QFile *f = new QFile(path, this);
|
|
if (f->open(QIODevice::ReadOnly)) {
|
|
int fd = f->handle();
|
|
int version;
|
|
char name[256];
|
|
uint8_t events[EV_MAX / 8 + 1];
|
|
memset(events, 0, sizeof(events));
|
|
if ((ioctl(fd, EVIOCGVERSION, &version) >= 0) && (ioctl(fd, EVIOCGNAME(sizeof(name)), name) >= 0)
|
|
&& (ioctl(fd, EVIOCGBIT(0, sizeof(events)), &events) >= 0) && test_bit(EV_KEY, events)
|
|
&& (((version >> 16) & 0xFF) > 0)) {
|
|
name[255] = 0;
|
|
qWarning("GlobalShortcutX: %s: %s", qPrintable(f->fileName()), name);
|
|
// Is it grabbed by someone else?
|
|
if ((ioctl(fd, EVIOCGRAB, 1) < 0)) {
|
|
qWarning("GlobalShortcutX: Device exclusively grabbed by someone else (X11 using "
|
|
"exclusive-mode evdev?)");
|
|
delete f;
|
|
} else {
|
|
ioctl(fd, EVIOCGRAB, 0);
|
|
uint8_t keys[KEY_MAX / 8 + 1];
|
|
if ((ioctl(fd, EVIOCGBIT(EV_KEY, sizeof(keys)), &keys) >= 0) && test_bit(KEY_SPACE, keys))
|
|
qsKeyboards.insert(f->fileName());
|
|
|
|
fcntl(f->handle(), F_SETFL, O_NONBLOCK);
|
|
connect(new QSocketNotifier(f->handle(), QSocketNotifier::Read, f), SIGNAL(activated(int)),
|
|
this, SLOT(inputReadyRead(int)));
|
|
|
|
qmInputDevices.insert(f->fileName(), f);
|
|
}
|
|
} else {
|
|
delete f;
|
|
}
|
|
} else {
|
|
delete f;
|
|
}
|
|
}
|
|
}
|
|
#else
|
|
Q_UNUSED(dir);
|
|
#endif
|
|
}
|
|
|
|
#undef test_bit
|
|
|
|
GlobalShortcutX::ButtonInfo GlobalShortcutX::buttonInfo(const QVariant &v) {
|
|
bool ok;
|
|
unsigned int key = v.toUInt(&ok);
|
|
if (!ok) {
|
|
return ButtonInfo();
|
|
}
|
|
|
|
ButtonInfo info;
|
|
|
|
if ((key < 0x118) || (key >= 0x128)) {
|
|
info.device = tr("Keyboard");
|
|
// For backwards compatibility reasons we want to keep using the
|
|
// old function as long as possible. The replacement function
|
|
// XkbKeycodeToKeysym requires the XKB extension which isn't
|
|
// guaranteed to be present.
|
|
KeySym ks = XKeycodeToKeysym(display, static_cast< KeyCode >(key), 0);
|
|
if (ks == NoSymbol) {
|
|
info.name = QLatin1String("0x") + QString::number(key, 16);
|
|
} else {
|
|
const char *str = XKeysymToString(ks);
|
|
if (str[0] == '\0') {
|
|
info.name = QLatin1String("KS0x") + QString::number(ks, 16);
|
|
} else {
|
|
info.name = QLatin1String(str);
|
|
}
|
|
}
|
|
} else {
|
|
info.device = tr("Mouse");
|
|
info.devicePrefix = QLatin1String("M");
|
|
info.name = QString::number(key - 0x118);
|
|
}
|
|
|
|
return info;
|
|
}
|