From 3dc7b00d0919cf2400071deba54ff7f64ee3d9ea Mon Sep 17 00:00:00 2001 From: Davide Beatrici Date: Wed, 28 Apr 2021 00:56:14 +0200 Subject: [PATCH] FEAT(client): Implement raw input, get rid of hooks and DirectInput (Windows) Fixes #4039. Several users complained about the global shortcuts engine on Windows, mainly because it affects the keyboard and mouse behavior for all applications: 1. It reportedly causes the polling rate to drop significantly. 2. Causes accessibility programs such as NVDA to read Mumble's UI slow. Workaround implemented in a310c7f. 3. Using the debugger's "pause" function is nearly impossible: the OS has to wait for the callback to timeout before registering the input, causing a consistent and heavy slowdown. Disabling hooks fixes all issues described above, however it only leaves DirectInput for standard devices (i.e. not relying on XInput and G-keys). This commit completely rewrites GlobalShortcutWin, implementing raw input and getting rid of hooks and DirectInput. Controllers are supported, but only basic buttons are handled for now. Support for analog input (including d-pad) will be added in the future. XInput can still be enabled, in order for input from triggers and d-pad on Xbox controllers to be registered. As a bonus, the main thread doesn't wait for the input processing function(s) to return anymore. The reason why this wasn't the case before is because we needed to know whether to block the message (returning true) from reaching other applications ("suppress" feature). That cannot work with raw input due to its nature. Returning true simply tells Windows that we didn't process the message. --- src/mumble/CMakeLists.txt | 2 +- src/mumble/GlobalShortcut.cpp | 7 +- src/mumble/GlobalShortcut.ui | 16 +- src/mumble/GlobalShortcut_win.cpp | 1383 +++++++++++------------------ src/mumble/GlobalShortcut_win.h | 168 ++-- src/mumble/MumbleApplication.cpp | 46 +- src/mumble/Settings.cpp | 6 - src/mumble/Settings.h | 7 +- src/mumble/main.cpp | 4 +- 9 files changed, 618 insertions(+), 1021 deletions(-) diff --git a/src/mumble/CMakeLists.txt b/src/mumble/CMakeLists.txt index cf28c1dd5..053d388ee 100644 --- a/src/mumble/CMakeLists.txt +++ b/src/mumble/CMakeLists.txt @@ -533,8 +533,8 @@ if(WIN32) target_link_libraries(mumble PRIVATE dbghelp.lib - dinput8.lib dxguid.lib + hid.lib wintrust.lib ) else() diff --git a/src/mumble/GlobalShortcut.cpp b/src/mumble/GlobalShortcut.cpp index 338be677e..4aa52cd62 100644 --- a/src/mumble/GlobalShortcut.cpp +++ b/src/mumble/GlobalShortcut.cpp @@ -686,7 +686,6 @@ void GlobalShortcutConfig::load(const Settings &r) { } qcbEnableUIAccess->setChecked(r.bEnableUIAccess); - qcbEnableWinHooks->setChecked(r.bEnableWinHooks); qcbEnableGKey->setChecked(r.bEnableGKey); qcbEnableXboxInput->setChecked(r.bEnableXboxInput); @@ -702,17 +701,13 @@ void GlobalShortcutConfig::save() const { bool oldUIAccess = s.bEnableUIAccess; s.bEnableUIAccess = qcbEnableUIAccess->checkState() == Qt::Checked; - bool oldWinHooks = s.bEnableWinHooks; - s.bEnableWinHooks = qcbEnableWinHooks->checkState() == Qt::Checked; - bool oldGKey = s.bEnableGKey; s.bEnableGKey = qcbEnableGKey->checkState() == Qt::Checked; bool oldXboxInput = s.bEnableXboxInput; s.bEnableXboxInput = qcbEnableXboxInput->checkState() == Qt::Checked; - if (s.bEnableUIAccess != oldUIAccess || s.bEnableWinHooks != oldWinHooks || s.bEnableGKey != oldGKey - || s.bEnableXboxInput != oldXboxInput) { + if (s.bEnableUIAccess != oldUIAccess || s.bEnableGKey != oldGKey || s.bEnableXboxInput != oldXboxInput) { s.requireRestartToApply = true; } } diff --git a/src/mumble/GlobalShortcut.ui b/src/mumble/GlobalShortcut.ui index e4598d164..e2796c53e 100644 --- a/src/mumble/GlobalShortcut.ui +++ b/src/mumble/GlobalShortcut.ui @@ -127,12 +127,12 @@ true - - 100 - 50 + + 100 + false @@ -227,16 +227,6 @@ Without this option enabled, using Mumble's global shortcuts in privileged appli - - - - <b>Enable Windows hooks</b>.<br />This enables the Windows hooks shortcut engine. Using this engine allows Mumble to suppress keypresses and mouse clicks. - - - Enable Windows hooks - - - diff --git a/src/mumble/GlobalShortcut_win.cpp b/src/mumble/GlobalShortcut_win.cpp index f061ed046..af211609f 100644 --- a/src/mumble/GlobalShortcut_win.cpp +++ b/src/mumble/GlobalShortcut_win.cpp @@ -3,124 +3,87 @@ // that can be found in the LICENSE file at the root of the // Mumble source tree or at . +// For detailed info about RAWKEYBOARD handling: +// https://blog.molecular-matters.com/2011/09/05/properly-handling-keyboard-input + #include "GlobalShortcut_win.h" -#include "MainWindow.h" -#include "Utils.h" #include "Global.h" -// MinGW does not support std::future/std::promise -// at present. Use Boost's implementation for now. -#define BOOST_THREAD_VERSION 4 -#include -#include +#ifdef USE_GKEY +# include "GKey.h" +#endif -#include -#include +#include +#include -// 3rdparty/xinputcheck-src. -#include +#include -// Used to detect screen readers -#include - -#define HAS_FAILED(Status) (static_cast< HRESULT >(Status) < 0) - -#define DX_SAMPLE_BUFFER_SIZE 512 - -// from os_win.cpp -extern HWND mumble_mw_hwnd; - -/// The QEvent::Type value to use for InjectKeyboardMessageEvent. -#define INJECTKEYBOARDMESSAGE_QEVENT (QEvent::User + 200) -/// The QEvent::Type value to use for InjectKeyboardMouseEvent. -#define INJECTMOUSEMESSAGE_QEVENT (QEvent::User + 201) - -/// InjectKeyboardMessageEvent is an event that can be sent to -/// the GlobalShortcutWin engine to inject a native Windows keyboard -/// event into GlobalShortcutWin's event stream. -class InjectKeyboardMessageEvent : public QEvent { - Q_DISABLE_COPY(InjectKeyboardMessageEvent); - -public: - boost::promise< bool > m_suppressionPromise; - DWORD m_scancode; - DWORD m_vkcode; - bool m_extended; - bool m_down; - - /// Construct a new InjectKeyboardMessageEvent. - /// - /// @param scancode The Windows scancode of the button. - /// @param vkcode The Windows virtual keycode of the button. - /// @param extended Indicates whether the button is an extended key in - /// Windows nomenclature. ("[...] such as the right-hand ALT - /// and CTRL keys that appear on an enhanced 101- or 102-key - /// keyboard") - /// @param down The down/pressed status of the keyboard button. - InjectKeyboardMessageEvent(DWORD scancode, DWORD vkcode, bool extended, bool down) - : QEvent(static_cast< QEvent::Type >(INJECTKEYBOARDMESSAGE_QEVENT)), m_scancode(scancode), m_vkcode(vkcode), - m_extended(extended), m_down(down) {} - - inline boost::future< bool > shouldSuppress() { return m_suppressionPromise.get_future(); } -}; - -/// InjectMouseMessageEvent is an event that can be sent to -/// the GlobalShortcutWin engine to inject a native Windows mouse -/// event into GlobalShortcutWin's event stream. -class InjectMouseMessageEvent : public QEvent { - Q_DISABLE_COPY(InjectMouseMessageEvent); - -public: - boost::promise< bool > m_suppressionPromise; - unsigned int m_btn; - bool m_down; - - /// Construct a new InjectMouseMessageEvent. - /// - /// @param btn The DirectInput button index of the mouse event. - /// @param down The down/pressed status of the mouse button. - InjectMouseMessageEvent(unsigned int btn, bool down) - : QEvent(static_cast< QEvent::Type >(INJECTMOUSEMESSAGE_QEVENT)), m_btn(btn), m_down(down) {} - - inline boost::future< bool > shouldSuppress() { return m_suppressionPromise.get_future(); } -}; - -uint qHash(const GUID &a) { - uint val = a.Data1 ^ a.Data2 ^ a.Data3; - for (int i = 0; i < 8; i++) - val += a.Data4[i]; - return val; +extern "C" { +// clang-format off +// Do NOT change the order of the includes below or compile errors will occur! +#include +#include +// clang-format on } -/** - * Returns a platform specific GlobalShortcutEngine object. - * - * @see GlobalShortcutX - * @see GlobalShortcutMac - * @see GlobalShortcutWin - */ +// From os_win.cpp +extern HWND mumble_mw_hwnd; + +struct InputHid { + uint32_t button; + std::string deviceName; + std::string devicePrefix; +}; +Q_DECLARE_METATYPE(InputHid) + +struct InputKeyboard { + bool e0; + uint16_t code; +}; +Q_DECLARE_METATYPE(InputKeyboard) + +enum class InputMouse { Left = 1, Right, Middle, Side_1, Side_2 }; +Q_DECLARE_METATYPE(InputMouse) + +#ifdef USE_XBOXINPUT +struct InputXinput { + uint8_t device; + uint8_t code; +}; +Q_DECLARE_METATYPE(InputXinput) +#endif +#ifdef USE_GKEY +struct InputGkey { + bool keyboard; + uint8_t button; + uint8_t mode; +}; +Q_DECLARE_METATYPE(InputGkey) +#endif + GlobalShortcutEngine *GlobalShortcutEngine::platformInit() { return new GlobalShortcutWin(); } - GlobalShortcutWin::GlobalShortcutWin() - : pDI(nullptr), hhMouse(nullptr), hhKeyboard(nullptr), uiHardwareDevices(0) -#ifdef USE_GKEY - , - gkey(nullptr) -#endif #ifdef USE_XBOXINPUT - , - xboxinput(nullptr), nxboxinput(0) + : m_xinputDevices(0), m_xinputLastPacket() #endif { - // Hidden setting to disable hooking - // Also disable hooking if a screen reader is running - bHook = Global::get().qs->value(QLatin1String("winhooks"), true).toBool() && !areScreenReadersActive(); + qRegisterMetaType< InputHid >(); + qRegisterMetaType< InputKeyboard >(); + qRegisterMetaType< InputMouse >(); +#ifdef USE_XBOXINPUT + qRegisterMetaType< InputXinput >(); +#endif +#ifdef USE_GKEY + qRegisterMetaType< InputGkey >(); +#endif + connect(this, &GlobalShortcutWin::hidMessage, this, &GlobalShortcutWin::on_hidMessage); + connect(this, &GlobalShortcutWin::keyboardMessage, this, &GlobalShortcutWin::on_keyboardMessage); + connect(this, &GlobalShortcutWin::mouseMessage, this, &GlobalShortcutWin::on_mouseMessage); - moveToThread(this); start(QThread::LowestPriority); } @@ -130,800 +93,532 @@ GlobalShortcutWin::~GlobalShortcutWin() { } void GlobalShortcutWin::run() { - if (HAS_FAILED(DirectInput8Create(GetModuleHandle(nullptr), DIRECTINPUT_VERSION, IID_IDirectInput8, - reinterpret_cast< void ** >(&pDI), nullptr))) { - qFatal("GlobalShortcutWin: Failed to create d8input"); +#ifdef USE_XBOXINPUT + if (Global::get().s.bEnableXboxInput) { + static_assert(std::numeric_limits< uint8_t >::max() >= XBOXINPUT_MAX_DEVICES, + "Higher XBOXINPUT_MAX_DEVICES than anticipated."); + + m_xinput = std::make_unique< XboxInput >(); + if (!m_xinput->isValid()) { + qWarning("GlobalShortcutWin: Failed to initialize XboxInput!"); + m_xinput.reset(); + } + } +#endif +#ifdef USE_GKEY + if (Global::get().s.bEnableGKey) { + static_assert(std::numeric_limits< uint8_t >::max() >= GKEY_MAX_KEYBOARD_MODE, + "GKEY_MAX_KEYBOARD_MODE is higher than anticipated."); + static_assert(std::numeric_limits< uint8_t >::max() >= GKEY_MAX_MOUSE_BUTTON, + "GKEY_MAX_MOUSE_BUTTON is higher than anticipated."); + + m_gkey = std::make_unique< GKeyLibrary >(); + if (!m_gkey->isValid()) { + qWarning("GlobalShortcutWin: Failed to initialize GKey!"); + m_gkey.reset(); + } + } +#endif + // Wait for MainWindow's constructor to finish, so that we can get the window's handle. + while (!mumble_mw_hwnd) { + yieldCurrentThread(); + } + + constexpr uint8_t nRid = 5; + RAWINPUTDEVICE rid[nRid]; + + rid[0].usUsagePage = HID_USAGE_PAGE_GENERIC; + rid[0].usUsage = HID_USAGE_GENERIC_MOUSE; + rid[0].dwFlags = RIDEV_INPUTSINK; + rid[0].hwndTarget = mumble_mw_hwnd; + + rid[1].usUsagePage = HID_USAGE_PAGE_GENERIC; + rid[1].usUsage = HID_USAGE_GENERIC_JOYSTICK; + rid[1].dwFlags = RIDEV_INPUTSINK | RIDEV_DEVNOTIFY; + rid[1].hwndTarget = mumble_mw_hwnd; + + rid[2].usUsagePage = HID_USAGE_PAGE_GENERIC; + rid[2].usUsage = HID_USAGE_GENERIC_GAMEPAD; + rid[2].dwFlags = RIDEV_INPUTSINK | RIDEV_DEVNOTIFY; + rid[2].hwndTarget = mumble_mw_hwnd; + + rid[3].usUsagePage = HID_USAGE_PAGE_GENERIC; + rid[3].usUsage = HID_USAGE_GENERIC_KEYBOARD; + rid[3].dwFlags = RIDEV_INPUTSINK | RIDEV_DEVNOTIFY; + rid[3].hwndTarget = mumble_mw_hwnd; + + rid[4].usUsagePage = HID_USAGE_PAGE_GENERIC; + rid[4].usUsage = HID_USAGE_GENERIC_MULTI_AXIS_CONTROLLER; + rid[4].dwFlags = RIDEV_INPUTSINK | RIDEV_DEVNOTIFY; + rid[4].hwndTarget = mumble_mw_hwnd; + + if (!RegisterRawInputDevices(rid, nRid, sizeof(RAWINPUTDEVICE))) { + qWarning("GlobalShortcutWindows: RegisterRawInputDevices() failed with error %u!", GetLastError()); + } + + QTimer timer; + connect(&timer, &QTimer::timeout, this, &GlobalShortcutWin::timeTicked); + timer.start(20); + + exec(); +} + +void GlobalShortcutWin::injectRawInputMessage(HRAWINPUT handle) { + UINT size; + if (GetRawInputData(handle, RID_INPUT, nullptr, &size, sizeof(RAWINPUTHEADER)) != 0) { return; } - // Print the user's LowLevelHooksTimeout registry key for debugging purposes. - // On Windows 7 and greater, Windows will silently remove badly behaving hooks - // without telling the application. Users can tweak the timeout themselves - // with this registry key. - HKEY key = nullptr; - DWORD type = 0; - DWORD value = 0; - DWORD len = sizeof(DWORD); - if (RegOpenKeyExA(HKEY_CURRENT_USER, "Control Panel\\Desktop", 0, KEY_READ, &key) == ERROR_SUCCESS) { - LONG err = - RegQueryValueExA(key, "LowLevelHooksTimeout", nullptr, &type, reinterpret_cast< LPBYTE >(&value), &len); - if (err == ERROR_SUCCESS && type == REG_DWORD) { - qWarning("GlobalShortcutWin: Found LowLevelHooksTimeout with value = 0x%lx", - static_cast< unsigned long >(value)); - } else if (err == ERROR_FILE_NOT_FOUND) { - qWarning("GlobalShortcutWin: No LowLevelHooksTimeout registry key found."); - } else { - qWarning( - "GlobalShortcutWin: Error looking up LowLevelHooksTimeout. (Error: 0x%lx, Type: 0x%lx, Value: 0x%lx)", - static_cast< unsigned long >(err), static_cast< unsigned long >(type), - static_cast< unsigned long >(value)); - } + auto buffer = std::make_unique< uint8_t[] >(size); + if (GetRawInputData(handle, RID_INPUT, buffer.get(), &size, sizeof(RAWINPUTHEADER)) <= 0) { + return; } - // Wait for MainWindow's constructor to finish before we enumerate DirectInput devices. - // We need to do this because adding a new device requires a Window handle. (SetCooperativeLevel()) - while (!Global::get().mw) - this->yieldCurrentThread(); - -#ifdef USE_GKEY - if (Global::get().s.bEnableGKey) { - gkey = new GKeyLibrary(); - qWarning("GlobalShortcutWin: GKeys initialized, isValid: %d", gkey->isValid()); - } -#endif - -#ifdef USE_XBOXINPUT - if (Global::get().s.bEnableXboxInput) { - xboxinput = new XboxInput(); - ZeroMemory(&xboxinputLastPacket, sizeof(xboxinputLastPacket)); - qWarning("GlobalShortcutWin: XboxInput initialized, isValid: %d", xboxinput->isValid()); - } -#endif - - QTimer *timer = new QTimer; - connect(timer, SIGNAL(timeout()), this, SLOT(timeTicked())); - timer->start(20); - - setPriority(QThread::TimeCriticalPriority); - - exec(); - - delete timer; - -#ifdef USE_GKEY - delete gkey; -#endif - -#ifdef USE_XBOXINPUT - delete xboxinput; -#endif - - if (bHook) { - if (hhMouse) { - UnhookWindowsHookEx(hhMouse); - } - if (hhKeyboard) { - UnhookWindowsHookEx(hhKeyboard); - } - } - - foreach (InputDevice *id, qhInputDevices) { - if (id->pDID) { - id->pDID->Unacquire(); - id->pDID->Release(); - } - delete id; - } - pDI->Release(); -} - -bool GlobalShortcutWin::event(QEvent *event) { - QEvent::Type type = event->type(); - if (type == INJECTKEYBOARDMESSAGE_QEVENT) { - InjectKeyboardMessageEvent *ikme = static_cast< InjectKeyboardMessageEvent * >(event); - bool suppress = handleKeyboardMessage(ikme->m_scancode, ikme->m_vkcode, ikme->m_extended, ikme->m_down); - ikme->m_suppressionPromise.set_value(suppress); - return true; - } else if (type == INJECTMOUSEMESSAGE_QEVENT) { - InjectMouseMessageEvent *imme = static_cast< InjectMouseMessageEvent * >(event); - bool suppress = handleMouseMessage(imme->m_btn, imme->m_down); - imme->m_suppressionPromise.set_value(suppress); - return true; - } - return GlobalShortcutEngine::event(event); -} - -bool GlobalShortcutWin::injectKeyboardMessage(MSG *msg) { - if (!bHook) { - return false; - } - - // Only allow keyboard messages. - switch (msg->message) { - case WM_KEYDOWN: - case WM_KEYUP: - case WM_SYSKEYDOWN: - case WM_SYSKEYUP: + auto input = reinterpret_cast< const PRAWINPUT >(buffer.get()); + switch (input->header.dwType) { + case RIM_TYPEMOUSE: { + const RAWMOUSE &mouse = input->data.mouse; + emit mouseMessage(mouse.usButtonFlags, mouse.usButtonData); break; - default: - return false; - } - - DWORD scancode = (msg->lParam >> 16) & 0xff; - DWORD vkcode = msg->wParam; - bool extended = !!(msg->lParam & 0x01000000); - bool up = !!(msg->lParam & 0x80000000); - - InjectKeyboardMessageEvent *ikme = new InjectKeyboardMessageEvent(scancode, vkcode, extended, !up); - boost::future< bool > suppress = ikme->shouldSuppress(); - qApp->postEvent(this, ikme); - return suppress.get(); -} - -bool GlobalShortcutWin::injectMouseMessage(MSG *msg) { - if (!bHook) { - return false; - } - - bool down = false; - unsigned int btn = 0; - - // Convert the Windows mouse message into a DirectInput - // button index, and store the pressed state of the button. - switch (msg->message) { - case WM_LBUTTONDOWN: - down = true; - case WM_LBUTTONUP: - btn = 3; - break; - case WM_RBUTTONDOWN: - down = true; - case WM_RBUTTONUP: - btn = 4; - break; - case WM_MBUTTONDOWN: - down = true; - case WM_MBUTTONUP: - btn = 5; - break; - case WM_XBUTTONDOWN: - down = true; - case WM_XBUTTONUP: { - unsigned int offset = (msg->wParam >> 16) & 0xffff; - btn = 5 + offset; } - default: - // Non-mouse event. Return early. - return false; - } - - InjectMouseMessageEvent *imme = new InjectMouseMessageEvent(btn, down); - boost::future< bool > suppress = imme->shouldSuppress(); - qApp->postEvent(this, imme); - return suppress.get(); -} - -bool GlobalShortcutWin::handleKeyboardMessage(DWORD scancode, DWORD vkcode, bool extended, bool down) { - GlobalShortcutWin *gsw = static_cast< GlobalShortcutWin * >(engine); - - QList< QVariant > ql; - - // Convert the low-level key event to - // a DirectInput key ID. - unsigned int keyid = static_cast< unsigned int >((scancode << 8) | 0x4); - if (extended) { - keyid |= 0x8000U; - } - - // NumLock and Pause need special handlinGlobal::get(). - // For those keys, the method above of setting - // bit 15 high when the LLKHF_EXTENDED flag is - // set on the low-level key event does not work. - // - // When we receive a low-level Windows - // Pause key event, the extended flag isn't - // set, but DirectInput expects it to be. - // - // The opposite is true for NumLock key, - // where the extended flag for the low-level - // Windows event is set, but DirectInput expects - // it not to be. - // - // Without this fix-up, we would emit Pause as - // NumLock, and NumLock as pause. That was - // problematic, because at the same time, - // DirectInput would emit the correct key. - // This meant that when pressing one of Pause - // and NumLock, shortcut actions for both keys - // would be triggered. - // - // Originally reported in mumble-voip/mumble#1353 - if (vkcode == VK_PAUSE) { - // Always set the extended bit for Pause. - keyid |= 0x8000U; - } else if (vkcode == VK_NUMLOCK) { - // Never set the extended bit for NumLock. - keyid &= ~0x8000U; - } - - ql << keyid; - ql << QVariant(QUuid(GUID_SysKeyboard)); - bool suppress = gsw->handleButton(ql, down); - - return suppress; -} - -bool GlobalShortcutWin::handleMouseMessage(unsigned int btn, bool down) { - GlobalShortcutWin *gsw = static_cast< GlobalShortcutWin * >(engine); - - bool suppress = false; - - if (btn > 0) { - QList< QVariant > ql; - ql << static_cast< unsigned int >((btn << 8) | 0x4); - ql << QVariant(QUuid(GUID_SysMouse)); - - // Do not suppress LBUTTONUP though (so suppression can be deactivated via mouse). - bool wantsuppress = gsw->handleButton(ql, down); - suppress = wantsuppress && (btn != 3); - } - - return suppress; -} - -LRESULT CALLBACK GlobalShortcutWin::HookKeyboard(int nCode, WPARAM wParam, LPARAM lParam) { - GlobalShortcutWin *gsw = static_cast< GlobalShortcutWin * >(engine); - KBDLLHOOKSTRUCT *key = reinterpret_cast< KBDLLHOOKSTRUCT * >(lParam); - -#ifndef QT_NO_DEBUG - static int safety = 0; - - if ((++safety < 100) && (nCode >= 0)) { -#else - if (nCode >= 0) { -#endif - DWORD scancode = key->scanCode; - DWORD vkcode = key->vkCode; - bool extended = !!(key->flags & LLKHF_EXTENDED); - bool up = !!(key->flags & LLKHF_UP); - bool suppress = handleKeyboardMessage(scancode, vkcode, extended, !up); - if (suppress) { - return 1; - } - } - return CallNextHookEx(gsw->hhKeyboard, nCode, wParam, lParam); -} - -LRESULT CALLBACK GlobalShortcutWin::HookMouse(int nCode, WPARAM wParam, LPARAM lParam) { - GlobalShortcutWin *gsw = static_cast< GlobalShortcutWin * >(engine); - MSLLHOOKSTRUCT *mouse = reinterpret_cast< MSLLHOOKSTRUCT * >(lParam); - - if (nCode >= 0) { - bool suppress = false; - UINT msg = wParam; - // Convert the hooked Windows mouse message into a DirectInput - // button index, and store the pressed state of the button. - bool down = false; - unsigned int btn = 0; - switch (msg) { - case WM_LBUTTONDOWN: - down = true; - case WM_LBUTTONUP: - btn = 3; - break; - case WM_RBUTTONDOWN: - down = true; - case WM_RBUTTONUP: - btn = 4; - break; - case WM_MBUTTONDOWN: - down = true; - case WM_MBUTTONUP: - btn = 5; - break; - case WM_XBUTTONDOWN: - down = true; - case WM_XBUTTONUP: - btn = 5 + (mouse->mouseData >> 16); - default: - break; - } - if (btn > 0) { - suppress = handleMouseMessage(btn, down); - if (suppress) { - return 1; - } - } - } - return CallNextHookEx(gsw->hhMouse, nCode, wParam, lParam); -} - -BOOL CALLBACK GlobalShortcutWin::EnumDeviceObjectsCallback(LPCDIDEVICEOBJECTINSTANCE lpddoi, LPVOID pvRef) { - InputDevice *id = static_cast< InputDevice * >(pvRef); - QString name = QString::fromUtf16(reinterpret_cast< const ushort * >(lpddoi->tszName)); - id->qhNames[lpddoi->dwType] = name; - - if (Global::get().s.bDirectInputVerboseLogging) { - qWarning("GlobalShortcutWin: EnumObjects: device %s %s object 0x%.8lx %s", - qPrintable(QUuid(id->guid).toString()), qPrintable(id->name), - static_cast< unsigned long >(lpddoi->dwType), qPrintable(name)); - } - - return DIENUM_CONTINUE; -} - -BOOL GlobalShortcutWin::EnumDevicesCB(LPCDIDEVICEINSTANCE pdidi, LPVOID pContext) { - GlobalShortcutWin *cbgsw = static_cast< GlobalShortcutWin * >(pContext); - HRESULT hr; - - QString name = QString::fromUtf16(reinterpret_cast< const ushort * >(pdidi->tszProductName)); - QString sname = QString::fromUtf16(reinterpret_cast< const ushort * >(pdidi->tszInstanceName)); - - InputDevice *id = new InputDevice; - - id->pDID = nullptr; - - id->name = name; - - id->guid = pdidi->guidInstance; - id->vguid = QVariant(QUuid(id->guid).toString()); - - id->guidproduct = pdidi->guidProduct; - id->vguidproduct = QVariant(QUuid(id->guidproduct).toString()); - - // Is it an XInput device? Skip it. - // - // This check is not restricted to USE_XBOXINPUT because - // Windows 10 (10586.122, ~March 2016) has issues with - // using XInput devices via DirectInput. - // - // See issues mumble-voip/mumble#2104 and mumble-voip/mumble#2147 - // for more information. - if (XInputCheck_IsGuidProductXInputDevice(&id->guidproduct)) { -#ifdef USE_XBOXINPUT - cbgsw->nxboxinput += 1; -#endif - qWarning("GlobalShortcutWin: excluded XInput device '%s' (guid %s guid product %s) from DirectInput", - qPrintable(id->name), qPrintable(id->vguid.toString()), qPrintable(id->vguidproduct.toString())); - delete id; - return DIENUM_CONTINUE; - } - - // Check for PIDVID at the end of the GUID, as - // per http://stackoverflow.com/q/25622780. - BYTE pidvid[8] = { 0, 0, 'P', 'I', 'D', 'V', 'I', 'D' }; - if (memcmp(id->guidproduct.Data4, pidvid, 8) == 0) { - uint16_t vendor_id = id->guidproduct.Data1 & 0xffff; - uint16_t product_id = (id->guidproduct.Data1 >> 16) & 0xffff; - - id->vendor_id = vendor_id; - id->product_id = product_id; - } else { - id->vendor_id = 0x00; - id->product_id = 0x00; - } - - // Reject devices if they are blacklisted. - // - // Device Name: ODAC-revB - // Vendor/Product ID: 0x262A, 0x1048 - // https://github.com/mumble-voip/mumble/issues/1977 - // - // Device Name: Aune T1 MK2 - HID-compliant consumer control device - // Vendor/Product ID: 0x262A, 0x1168 - // https://github.com/mumble-voip/mumble/issues/1880 - // - // For now, we simply disable the 0x262A vendor ID. - // - // 0x26A is SAVITECH Corp. - // http://www.savitech-ic.com/, or - // http://www.saviaudio.com/product.html - // (via https://usb-ids.gowdy.us/read/UD/262a) - // - // In the future, if there are more devices in the - // blacklist, we need a more structured aproach. - { - if (id->vendor_id == 0x262A) { - qWarning("GlobalShortcutWin: rejected blacklisted device %s (GUID: %s, PGUID: %s, VID: 0x%.4x, PID: " - "0x%.4x, TYPE: 0x%.8lx)", - qPrintable(id->name), qPrintable(id->vguid.toString()), qPrintable(id->vguidproduct.toString()), - id->vendor_id, id->product_id, static_cast< unsigned long >(pdidi->dwDevType)); - delete id; - return DIENUM_CONTINUE; - } - } - - foreach (InputDevice *dev, cbgsw->qhInputDevices) { - if (dev->guid == id->guid) { - delete id; - return DIENUM_CONTINUE; - } - } - - if (HAS_FAILED(hr = cbgsw->pDI->CreateDevice(pdidi->guidInstance, &id->pDID, nullptr))) - qFatal("GlobalShortcutWin: CreateDevice: %lx", hr); - - if (HAS_FAILED(hr = id->pDID->EnumObjects(EnumDeviceObjectsCallback, static_cast< void * >(id), DIDFT_BUTTON))) - qFatal("GlobalShortcutWin: EnumObjects: %lx", hr); - - if (id->qhNames.count() > 0) { - QList< DWORD > types = id->qhNames.keys(); - std::sort(types.begin(), types.end()); - - int nbuttons = types.count(); - STACKVAR(DIOBJECTDATAFORMAT, rgodf, nbuttons); - DIDATAFORMAT df; - ZeroMemory(&df, sizeof(df)); - df.dwSize = sizeof(df); - df.dwObjSize = sizeof(DIOBJECTDATAFORMAT); - df.dwFlags = DIDF_ABSAXIS; - df.dwDataSize = (nbuttons + 3) & (~0x3); - df.dwNumObjs = nbuttons; - df.rgodf = rgodf; - for (int i = 0; i < nbuttons; i++) { - ZeroMemory(&rgodf[i], sizeof(DIOBJECTDATAFORMAT)); - DWORD dwType = types[i]; - DWORD dwOfs = i; - rgodf[i].dwOfs = dwOfs; - rgodf[i].dwType = dwType; - id->qhOfsToType[dwOfs] = dwType; - id->qhTypeToOfs[dwType] = dwOfs; - } - - if (HAS_FAILED(hr = id->pDID->SetCooperativeLevel(mumble_mw_hwnd, DISCL_NONEXCLUSIVE | DISCL_BACKGROUND))) - qFatal("GlobalShortcutWin: SetCooperativeLevel: %lx", hr); - - if (HAS_FAILED(hr = id->pDID->SetDataFormat(&df))) - qFatal("GlobalShortcutWin: SetDataFormat: %lx", hr); - - DIPROPDWORD dipdw; - - dipdw.diph.dwSize = sizeof(DIPROPDWORD); - dipdw.diph.dwHeaderSize = sizeof(DIPROPHEADER); - dipdw.diph.dwObj = 0; - dipdw.diph.dwHow = DIPH_DEVICE; - dipdw.dwData = DX_SAMPLE_BUFFER_SIZE; - - if (HAS_FAILED(hr = id->pDID->SetProperty(DIPROP_BUFFERSIZE, &dipdw.diph))) - qFatal("GlobalShortcutWin: SetProperty: %lx", hr); - - qWarning("Adding device %s %s %s:%d type 0x%.8lx guid product %s", qPrintable(QUuid(id->guid).toString()), - qPrintable(name), qPrintable(sname), id->qhNames.count(), - static_cast< unsigned long >(pdidi->dwDevType), qPrintable(id->vguidproduct.toString())); - - cbgsw->qhInputDevices[id->guid] = id; - } else { - id->pDID->Release(); - delete id; - } - - return DIENUM_CONTINUE; -} - -void GlobalShortcutWin::timeTicked() { - if (Global::get().mw->uiNewHardware != uiHardwareDevices) { - uiHardwareDevices = Global::get().mw->uiNewHardware; - - XInputCheck_ClearDeviceCache(); -#ifdef USE_XBOXINPUT - nxboxinput = 0; -#endif - pDI->EnumDevices(DI8DEVCLASS_ALL, EnumDevicesCB, static_cast< void * >(this), DIEDFL_ATTACHEDONLY); - } - - if (bNeedRemap) - remap(); - - foreach (InputDevice *id, qhInputDevices) { - DIDEVICEOBJECTDATA rgdod[DX_SAMPLE_BUFFER_SIZE]; - DWORD dwItems = DX_SAMPLE_BUFFER_SIZE; - HRESULT hr; - - hr = id->pDID->Acquire(); - - switch (hr) { - case DI_OK: - case S_FALSE: - break; - case DIERR_UNPLUGGED: - case DIERR_GENERIC: - qWarning("Removing device %s", qPrintable(QUuid(id->guid).toString())); - id->pDID->Release(); - qhInputDevices.remove(id->guid); - delete id; + case RIM_TYPEKEYBOARD: { + const RAWKEYBOARD &keyboard = input->data.keyboard; + if (keyboard.MakeCode == KEYBOARD_OVERRUN_MAKE_CODE) { + // Invalid or unrecognizable combination of keys is pressed or + // the number of keys pressed exceeds the limit for this keyboard. return; - case DIERR_OTHERAPPHASPRIO: - continue; - default: - break; - } - - { - QElapsedTimer timer; - timer.start(); - - id->pDID->Poll(); - - // If a call to Poll takes more than - // a second, warn the user that they - // might have a misbehaving device. - if (timer.elapsed() > 1000) { - qWarning("GlobalShortcut_win: Poll() for device %s took %li msec. This is abnormal, the device is " - "possibly misbehavinGlobal::get()...", - qPrintable(QUuid(id->guid).toString()), static_cast< long >(timer.elapsed())); } - } - hr = id->pDID->GetDeviceData(sizeof(DIDEVICEOBJECTDATA), rgdod, &dwItems, 0); - if (HAS_FAILED(hr)) - continue; - - if (dwItems <= 0) - continue; - - for (DWORD j = 0; j < dwItems; j++) { - QList< QVariant > ql; - - quint32 uiType = id->qhOfsToType.value(rgdod[j].dwOfs); - ql << uiType; - ql << id->vguid; - handleButton(ql, rgdod[j].dwData & 0x80); - } - } - -#ifdef USE_GKEY - if (Global::get().s.bEnableGKey && gkey && gkey->isValid()) { - for (int button = GKEY_MIN_MOUSE_BUTTON; button <= GKEY_MAX_MOUSE_BUTTON; button++) { - QList< QVariant > ql; - ql << button; - ql << GKeyLibrary::quMouse; - handleButton(ql, gkey->isMouseButtonPressed(button)); - } - for (int mode = GKEY_MIN_KEYBOARD_MODE; mode <= GKEY_MAX_KEYBOARD_MODE; mode++) { - for (int key = GKEY_MIN_KEYBOARD_BUTTON; key <= GKEY_MAX_KEYBOARD_BUTTON; key++) { - QList< QVariant > ql; - // Store the key and mode in one int - // bit 0..15: mode, bit 16..31: key - ql << (key | (mode << 16)); - ql << GKeyLibrary::quKeyboard; - handleButton(ql, gkey->isKeyboardGkeyPressed(key, mode)); + if (keyboard.VKey == 0xFF) { + // Discard "fake keys" which are part of an escaped sequence. + return; } + + emit keyboardMessage(keyboard.Flags, keyboard.MakeCode, keyboard.VKey); + break; } - } -#endif + case RIM_TYPEHID: { + const RAWHID &hid = input->data.hid; + std::vector< char > reports(hid.dwSizeHid * hid.dwCount); + memcpy(reports.data(), hid.bRawData, reports.size()); -#ifdef USE_XBOXINPUT - if (Global::get().s.bEnableXboxInput && xboxinput && xboxinput->isValid() && nxboxinput > 0) { - XboxInputState state; - for (uint32_t i = 0; i < XBOXINPUT_MAX_DEVICES; i++) { - if (xboxinput->GetState(i, &state) == 0) { - // Skip the result of GetState() if the packet number hasn't changed, - // or if we're at the first packet. - if (xboxinputLastPacket[i] != 0 && state.packetNumber == xboxinputLastPacket[i]) { - continue; - } - - // The buttons field of XboxInputState contains a bit - // for each button on the Xbox controller. The official - // headers enumerate the bits via XINPUT_GAMEPAD_*. - // The official mapping uses all 16-bits, but leaves - // bit 10 and 11 (counting from 0) undocumented. - // - // It turns out that bit 10 is the guide button, - // which can be queried using the non-public - // XInputGetStateEx() function. - // - // Our mapping uses the bit number as a button index. - // So 0x1 -> 0, 0x2 -> 1, 0x4 -> 2, and so on... - // - // However, since the buttons field is only a 16-bit value, - // and we also want to use the left and right triggers as - // buttons, we assign them the button indexes 16 and 17. - uint32_t buttonMask = state.buttons; - for (uint32_t j = 0; j < 18; j++) { - QList< QVariant > ql; - - bool pressed = false; - if (j >= 16) { - if (j == 16) { // LeftTrigger - pressed = state.leftTrigger > XBOXINPUT_TRIGGER_THRESHOLD; - } else if (j == 17) { // RightTrigger - pressed = state.rightTrigger > XBOXINPUT_TRIGGER_THRESHOLD; - } - } else { - uint32_t currentButtonMask = (1 << j); - pressed = (buttonMask & currentButtonMask) != 0; - } - - uint32_t type = (i << 24) | j; - ql << static_cast< uint >(type); - ql << XboxInput::s_XboxInputGuid; - handleButton(ql, pressed); - } - - xboxinputLastPacket[i] = state.packetNumber; - } + emit hidMessage(input->header.hDevice, std::move(reports), hid.dwSizeHid); } } -#endif - - // Initialize winhooks. - // - // We do this here, because at this point, we've just run our - // first timeTicked() slot. The GlobalShortcut_win thread's event - // loop has nothing else to do at this point, so there is nothing - // that blocks the callbacks of the hooks. - // - // That is, if we initialize here, our callbacks *can* be called - // immediately after initialization, which gives the best results - // as far as interactivity and user experience goes. The initialization - // cannot be "felt". - // - // Let me explain... - // - // Originally, this code lived in the body of run, ::run(), just - // before exec() was called. - // - // It turns out that if our hooks are initialized there, it can take - // a short while before the mouse and keyboard callbacks can be processed. - // - // During this time, where the mouse callback is not able to be called, - // the mouse in Windows becomes laggy. It makes the whole computer feel - // like it has locked up -- because input "stops". - // - // As explained above, initializing the hooks here yields a much superior - // experience, where this initialization has no observable effect on the - // behavior of the system's mouse input. - if (bHook && !hhMouse && !hhKeyboard) { - HMODULE hSelf; - GetModuleHandleEx(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, - reinterpret_cast< LPCTSTR >(&HookKeyboard), &hSelf); - hhMouse = SetWindowsHookEx(WH_MOUSE_LL, HookMouse, hSelf, 0); - hhKeyboard = SetWindowsHookEx(WH_KEYBOARD_LL, HookKeyboard, hSelf, 0); - } } -GlobalShortcutWin::ButtonInfo GlobalShortcutWin::buttonInfo(const QVariant &v) { - GlobalShortcutWin *gsw = static_cast< GlobalShortcutWin * >(GlobalShortcutEngine::engine); - - const QList< QVariant > &sublist = v.toList(); - if (sublist.count() != 2) { - return ButtonInfo(); - } - - bool ok = false; - DWORD type = sublist.at(0).toUInt(&ok); - QUuid guid(sublist.at(1).toString()); - - if (guid.isNull() || (!ok)) { - return ButtonInfo(); - } - - ButtonInfo info; - info.device = guid.toString(); - -#ifdef USE_GKEY - if (Global::get().s.bEnableGKey && gkey && gkey->isValid()) { - bool isGKey = false; - if (guid == GKeyLibrary::quMouse) { - isGKey = true; - info.name = gkey->getMouseButtonString(type); - } else if (guid == GKeyLibrary::quKeyboard) { - isGKey = true; - info.name = gkey->getKeyboardGkeyString(type & 0xFFFF, type >> 16); +void GlobalShortcutWin::on_hidMessage(const HANDLE deviceHandle, std::vector< char > reports, + const uint32_t reportSize) { + auto iter = m_devices.find(deviceHandle); + if (iter == m_devices.end()) { + iter = addDevice(deviceHandle); + if (iter == m_devices.cend()) { + return; } - if (isGKey) { - // Example output: - // "Logitech G-keys" - // "G6/M1" - info.device = QLatin1String("Logitech G-keys"); - info.devicePrefix = QLatin1String("GKey:"); - return info; + } + + auto &device = (*iter).second; +#ifdef USE_XBOXINPUT + if (device.xinput && m_xinput) { + return; + } +#endif + auto data = reinterpret_cast< PHIDP_PREPARSED_DATA >(&device.data[0]); + + ULONG nUsages = device.buttons.size(); + std::vector< USAGE > usages(nUsages); + if (HidP_GetUsages(HidP_Input, HID_USAGE_PAGE_BUTTON, 0, &usages[0], &nUsages, data, &reports[0], reportSize) + != HIDP_STATUS_SUCCESS) { + return; + } + + device.buttons.assign(device.buttons.size(), false); + for (ULONG i = 0; i < nUsages; ++i) { + device.buttons[usages[i] - device.usageRange.first] = true; + } + + for (uint32_t i = 0; i < device.buttons.size(); ++i) { + InputHid input; + input.button = i; + input.deviceName = device.name; + input.devicePrefix = device.prefix; + + handleButton(QVariant::fromValue(input), device.buttons[i]); + } +} + +void GlobalShortcutWin::on_keyboardMessage(const uint16_t flags, uint16_t scanCode, const uint16_t virtualKey) { + if (virtualKey == VK_NUMLOCK) { + // Keys like “Pause / Break” and “Numlock” act strangely, + // sometimes like they are not even the same physical key. + // They use the so called escaped sequences, which we have to decipher. + scanCode = MapVirtualKey(virtualKey, MAPVK_VK_TO_VSC) | 0x100; + } + + // E0 and E1 are escape sequences used for certain special keys, such as PRINT and PAUSE/BREAK. + // See https://www.win.tue.nl/~aeb/linux/kbd/scancodes-1.html. + if (flags & RI_KEY_E1) { + // For escaped sequences, turn the virtual key into the correct scan code using MapVirtualKey(). + // MapVirtualKey() is unable to map VK_PAUSE (this is a known bug), hence we map that by hand. + scanCode = virtualKey != VK_PAUSE ? MapVirtualKey(virtualKey, MAPVK_VK_TO_VSC) : 0x45; + } + + InputKeyboard input; + input.code = scanCode; + input.e0 = flags & RI_KEY_E0; + + handleButton(QVariant::fromValue(input), !(flags & RI_KEY_BREAK)); +} + +void GlobalShortcutWin::on_mouseMessage(const uint16_t flags, const uint16_t data) { + InputMouse input; + bool down; + + switch (flags) { + case RI_MOUSE_BUTTON_1_DOWN: + input = InputMouse::Left; + down = true; + break; + case RI_MOUSE_BUTTON_1_UP: + input = InputMouse::Left; + down = false; + break; + case RI_MOUSE_BUTTON_2_DOWN: + input = InputMouse::Right; + down = true; + break; + case RI_MOUSE_BUTTON_2_UP: + input = InputMouse::Right; + down = false; + break; + case RI_MOUSE_BUTTON_3_DOWN: + input = InputMouse::Middle; + down = true; + break; + case RI_MOUSE_BUTTON_3_UP: + input = InputMouse::Middle; + down = false; + break; + case RI_MOUSE_BUTTON_4_DOWN: + input = InputMouse::Side_1; + down = true; + break; + case RI_MOUSE_BUTTON_4_UP: + input = InputMouse::Side_1; + down = false; + break; + case RI_MOUSE_BUTTON_5_DOWN: + input = InputMouse::Side_2; + down = true; + break; + case RI_MOUSE_BUTTON_5_UP: + input = InputMouse::Side_2; + down = false; + break; + default: + return; + } + + handleButton(QVariant::fromValue(input), down); +} + +GlobalShortcutWin::DeviceMap::iterator GlobalShortcutWin::addDevice(const HANDLE deviceHandle) { + RID_DEVICE_INFO deviceInfo; + UINT size = sizeof(deviceInfo); + if (GetRawInputDeviceInfo(deviceHandle, RIDI_DEVICEINFO, &deviceInfo, &size) <= 0) { + return m_devices.end(); + } + + Device device; + + // E.g. "[045E:02EA]" + std::stringstream nameStream; + nameStream << std::uppercase << std::hex << std::setfill('0'); + nameStream << '['; + nameStream << std::setw(4) << deviceInfo.hid.dwVendorId; + nameStream << ':'; + nameStream << std::setw(4) << deviceInfo.hid.dwProductId; + nameStream << ']'; + + device.prefix = nameStream.str() + ':'; + + if (GetRawInputDeviceInfo(deviceHandle, RIDI_DEVICENAME, nullptr, &size) == 0) { + std::wstring name; + name.resize(size); + // E.g. "\\?\HID#VID_045E&PID_02FF&IG_00#8&15c9c520&0&0000#{4d1e55b2-f16f-11cf-88cb-001111000030}" + if (GetRawInputDeviceInfo(deviceHandle, RIDI_DEVICENAME, &name[0], &size) > 0) { +#ifdef USE_XBOXINPUT + if (name.find(L"IG_") != name.npos) { + device.xinput = true; + } +#endif + auto handle = CreateFile(name.c_str(), GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, + nullptr, OPEN_EXISTING, 0, nullptr); + if (handle != INVALID_HANDLE_VALUE) { + // From + // https://docs.microsoft.com/en-us/windows-hardware/drivers/ddi/hidsdi/nf-hidsdi-hidd_getproductstring: + // "For USB devices, the maximum string length is 126 wide characters (not including the terminating + // NULL character)." + // + // std::string::resize() doesn't take the NULL character into account, but an extra byte doesn't hurt. + name.clear(); + name.resize(127); + + std::wstring_convert< std::codecvt_utf8_utf16< wchar_t > > conv; + + if (HidD_GetManufacturerString(handle, &name[0], sizeof(wchar_t) * name.size())) { + nameStream << ' ' << conv.to_bytes(name); + name.clear(); + } + + if (HidD_GetProductString(handle, &name[0], sizeof(wchar_t) * name.size())) { + nameStream << ' ' << conv.to_bytes(name); + } + + CloseHandle(handle); + } + } + } + + device.name = nameStream.str(); + + if (GetRawInputDeviceInfo(deviceHandle, RIDI_PREPARSEDDATA, nullptr, &size) != 0) { + return m_devices.end(); + } + + device.data.resize(size); + auto data = reinterpret_cast< PHIDP_PREPARSED_DATA >(&device.data[0]); + if (GetRawInputDeviceInfo(deviceHandle, RIDI_PREPARSEDDATA, data, &size) <= 0) { + return m_devices.end(); + } + + HIDP_CAPS caps; + if (HidP_GetCaps(data, &caps) != HIDP_STATUS_SUCCESS) { + return m_devices.end(); + } + + USHORT nCaps = caps.NumberInputButtonCaps; + std::vector< HIDP_BUTTON_CAPS > buttonCaps(nCaps); + if (HidP_GetSpecificButtonCaps(HidP_Input, HID_USAGE_PAGE_BUTTON, 0, 0, &buttonCaps[0], &nCaps, data) + != HIDP_STATUS_SUCCESS) { + return m_devices.end(); + } + + device.usageRange.first = buttonCaps[0].Range.UsageMin; + device.usageRange.second = buttonCaps[0].Range.UsageMax; + device.buttons.resize(device.usageRange.second - device.usageRange.first + 1); + + if (device.xinput) { + ++m_xinputDevices; + } + + qInfo("GlobalShortcutWin: \"%s\" added", device.name.c_str()); + + return m_devices.insert({ deviceHandle, device }).first; +} + +void GlobalShortcutWin::deviceRemoved(const HANDLE deviceHandle) { + const auto iter = m_devices.find(deviceHandle); + if (iter == m_devices.cend()) { + return; + } + + const auto &device = (*iter).second; + if (device.xinput) { + --m_xinputDevices; + } + + qInfo("GlobalShortcutWin: \"%s\" removed", device.name.c_str()); + + m_devices.erase(iter); +} +#ifdef USE_XBOXINPUT +bool GlobalShortcutWin::xinputIsPressed(const uint8_t bit, const XboxInputState &state) { + // The buttons field of XboxInputState contains a bit + // for each button on the Xbox controller. The official + // headers enumerate the bits via XINPUT_GAMEPAD_*. + // The official mapping uses all 16-bits, but leaves + // bit 10 and 11 (counting from 0) undocumented. + // + // It turns out that bit 10 is the guide button, + // which can be queried using the non-public + // XInputGetStateEx() function. + // + // Our mapping uses the bit number as a button index. + // So 0x1 -> 0, 0x2 -> 1, 0x4 -> 2, and so on... + // + // However, since the buttons field is only a 16-bit value, + // and we also want to use the left and right triggers as + // buttons, we assign them the button indexes 16 and 17. + switch (bit) { + case 16: + return state.leftTrigger > XBOXINPUT_TRIGGER_THRESHOLD; + case 17: + return state.rightTrigger > XBOXINPUT_TRIGGER_THRESHOLD; + default: + return state.buttons & (1 << bit); + } +} +#endif +void GlobalShortcutWin::timeTicked() { + if (bNeedRemap) { + remap(); + } +#ifdef USE_XBOXINPUT + if (m_xinput && m_xinputDevices > 0) { + for (uint8_t i = 0; i < XBOXINPUT_MAX_DEVICES; ++i) { + XboxInputState state; + if (m_xinput->GetState(i, &state) != ERROR_SUCCESS) { + continue; + } + + // Skip the result of GetState() if the packet number hasn't changed, + // or if we're at the first packet. + if (m_xinputLastPacket[i] != 0 && state.packetNumber == m_xinputLastPacket[i]) { + continue; + } + + InputXinput input; + input.device = i; + + for (uint8_t j = 0; j < 18; ++j) { + input.code = j; + handleButton(QVariant::fromValue(input), xinputIsPressed(j, state)); + } + + m_xinputLastPacket[i] = state.packetNumber; } } #endif +#ifdef USE_GKEY + if (m_gkey) { + InputGkey input; + input.keyboard = false; + for (uint8_t button = GKEY_MIN_MOUSE_BUTTON; button <= GKEY_MAX_MOUSE_BUTTON; ++button) { + input.button = button; + handleButton(QVariant::fromValue(input), m_gkey->isMouseButtonPressed(button)); + } + + input.keyboard = true; + + for (uint8_t mode = GKEY_MIN_KEYBOARD_MODE; mode <= GKEY_MAX_KEYBOARD_MODE; ++mode) { + input.mode = mode; + + for (uint8_t button = GKEY_MIN_KEYBOARD_BUTTON; button <= GKEY_MAX_KEYBOARD_BUTTON; ++button) { + input.button = button; + handleButton(QVariant::fromValue(input), m_gkey->isKeyboardGkeyPressed(button, mode)); + } + } + } +#endif +} + +GlobalShortcutWin::ButtonInfo GlobalShortcutWin::buttonInfo(const QVariant &button) { + ButtonInfo info; + + // Unfortunately qMetaTypeId() is not a constexpr function. + // As a result, we cannot use a switch-case. + if (button.userType() == qMetaTypeId< InputHid >()) { + const auto input = button.value< InputHid >(); + info.device = QString::fromStdString(input.deviceName); + info.devicePrefix = QString::fromStdString(input.devicePrefix); + info.name = QString::number(input.button); + } else if (button.userType() == qMetaTypeId< InputKeyboard >()) { + info.device = tr("Keyboard"); + + const auto input = button.value< InputKeyboard >(); + wchar_t buffer[MAX_PATH]; + if (GetKeyNameText((input.code << 16) | (input.e0 << 24), buffer, MAX_PATH)) { + info.name = QString::fromWCharArray(buffer); + } + } else if (button.userType() == qMetaTypeId< InputMouse >()) { + info.device = tr("Mouse"); + info.devicePrefix = QStringLiteral("M"); + + const auto input = button.value< InputMouse >(); + info.name = QString::number(static_cast< uint >(input)); + } #ifdef USE_XBOXINPUT - if (Global::get().s.bEnableXboxInput && xboxinput && xboxinput->isValid() && guid == XboxInput::s_XboxInputGuid) { - uint32_t idx = (type >> 24) & 0xff; - uint32_t button = (type & 0x00ffffff); - - info.device = QString::fromLatin1("Xbox controller #%1").arg(idx + 1); - info.devicePrefix = QString::fromLatin1("Xbox%1:").arg(idx + 1); + else if (button.userType() == qMetaTypeId< InputXinput >()) { + const auto input = button.value< InputXinput >(); + info.device = QString::fromLatin1("Xbox controller #%1").arg(input.device + 1); + info.devicePrefix = QString::fromLatin1("Xbox%1:").arg(input.device + 1); // Translate from our own button index mapping to // the actual Xbox controller button names. // For a description of the mapping, see the state // querying code in GlobalShortcutWin::timeTicked(). - switch (button) { + switch (input.code) { case 0: - info.name = QLatin1String("Up"); + info.name = QStringLiteral("Up"); + break; case 1: - info.name = QLatin1String("Down"); + info.name = QStringLiteral("Down"); + break; case 2: - info.name = QLatin1String("Left"); + info.name = QStringLiteral("Left"); + break; case 3: - info.name = QLatin1String("Right"); + info.name = QStringLiteral("Right"); + break; case 4: - info.name = QLatin1String("Start"); + info.name = QStringLiteral("Start"); + break; case 5: - info.name = QLatin1String("Back"); + info.name = QStringLiteral("Back"); + break; case 6: - info.name = QLatin1String("LeftThumb"); + info.name = QStringLiteral("LeftThumb"); + break; case 7: - info.name = QLatin1String("RightThumb"); + info.name = QStringLiteral("RightThumb"); + break; case 8: - info.name = QLatin1String("LeftShoulder"); + info.name = QStringLiteral("LeftShoulder"); + break; case 9: - info.name = QLatin1String("RightShoulder"); + info.name = QStringLiteral("RightShoulder"); + break; case 10: - info.name = QLatin1String("Guide"); + info.name = QStringLiteral("Guide"); + break; case 11: - info.name = QLatin1String("11"); + info.name = QStringLiteral("11"); + break; case 12: - info.name = QLatin1String("A"); + info.name = QStringLiteral("A"); + break; case 13: - info.name = QLatin1String("B"); + info.name = QStringLiteral("B"); + break; case 14: - info.name = QLatin1String("X"); + info.name = QStringLiteral("X"); + break; case 15: - info.name = QLatin1String("Y"); + info.name = QStringLiteral("Y"); + break; case 16: - info.name = QLatin1String("LeftTrigger"); + info.name = QStringLiteral("LeftTrigger"); + break; case 17: - info.name = QLatin1String("RightTrigger"); + info.name = QStringLiteral("RightTrigger"); } + } +#endif +#ifdef USE_GKEY + else if (button.userType() == qMetaTypeId< InputGkey >()) { + info.device = QStringLiteral("Logitech G-keys"); + info.devicePrefix = QStringLiteral("GKey:"); - return info; + const auto input = button.value< InputGkey >(); + if (input.keyboard) { + info.name = m_gkey->getKeyboardGkeyString(input.button, input.mode); + } else { + info.name = m_gkey->getMouseButtonString(input.button); + } } #endif - InputDevice *id = gsw->qhInputDevices.value(guid); - if (guid == GUID_SysMouse) { - info.device = tr("Mouse"); - info.devicePrefix = QLatin1String("M"); - } else if (guid == GUID_SysKeyboard) { - info.device = tr("Keyboard"); - } else if (id) { - info.device = id->name; - } - - if (id) { - QString result = id->qhNames.value(type); - if (!result.isEmpty()) { - info.name = result; - } - } - return info; } - -bool GlobalShortcutWin::canSuppress() { - return bHook; -} - -bool GlobalShortcutWin::areScreenReadersActive() { - // This list contains valid executables we consider to be 'screen readers'. - // Todo: perhaps make this a configuration option in mumble to let users dynamically add executables. - const QStringList executables = { QLatin1String("nvda.exe"), QLatin1String("jfw.exe") }; - - const auto snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); - if (snapshot != INVALID_HANDLE_VALUE) { - PROCESSENTRY32 p; - p.dwSize = sizeof(p); - auto ok = Process32First(snapshot, &p); - while (ok) { - if (executables.contains(QString::fromWCharArray(p.szExeFile))) { - CloseHandle(snapshot); - return true; - } - ok = Process32Next(snapshot, &p); - } - CloseHandle(snapshot); - } - return false; -} - -#undef BOOST_THREAD_VERSION -#undef HAS_FAILED -#undef DX_SAMPLE_BUFFER_SIZE diff --git a/src/mumble/GlobalShortcut_win.h b/src/mumble/GlobalShortcut_win.h index 6671596bc..c1027e761 100644 --- a/src/mumble/GlobalShortcut_win.h +++ b/src/mumble/GlobalShortcut_win.h @@ -6,135 +6,79 @@ #ifndef MUMBLE_MUMBLE_GLOBALSHORTCUT_WIN_H_ #define MUMBLE_MUMBLE_GLOBALSHORTCUT_WIN_H_ -#include "Timer.h" #include "GlobalShortcut.h" -#ifdef USE_GKEY -# include "GKey.h" -#endif - #ifdef USE_XBOXINPUT # include "XboxInput.h" #endif -#define DIRECTINPUT_VERSION 0x0800 -#include +#include +#include -typedef QPair< GUID, DWORD > qpButton; - -struct InputDevice { - LPDIRECTINPUTDEVICE8 pDID; - - QString name; - - GUID guid; - QVariant vguid; - - GUID guidproduct; - QVariant vguidproduct; - - uint16_t vendor_id; - uint16_t product_id; - - // dwType to name - QHash< DWORD, QString > qhNames; - - // Map dwType to dwOfs in our structure - QHash< DWORD, DWORD > qhTypeToOfs; - - // Map dwOfs in our structure to dwType - QHash< DWORD, DWORD > qhOfsToType; - - // Buttons active since last reset - QSet< DWORD > activeMap; -}; +#ifdef USE_GKEY +class GKeyLibrary; +#endif class GlobalShortcutWin : public GlobalShortcutEngine { -private: Q_OBJECT Q_DISABLE_COPY(GlobalShortcutWin) -public: - LPDIRECTINPUT8 pDI; - QHash< GUID, InputDevice * > qhInputDevices; - HHOOK hhMouse, hhKeyboard; - unsigned int uiHardwareDevices; - Timer tDoubleClick; - bool bHook; -#ifdef USE_GKEY - GKeyLibrary *gkey; -#endif -#ifdef USE_XBOXINPUT - /// xboxinputLastPacket holds the last packet number - /// that was processed. Any new data queried for a - /// device is only valid if the packet number is - /// different than last time we queried it. - uint32_t xboxinputLastPacket[XBOXINPUT_MAX_DEVICES]; - XboxInput *xboxinput; - /// nxboxinput holds the number of XInput devices - /// available on the system. It is filled out by - /// our EnumDevices callback. - int nxboxinput; -#endif - - static BOOL CALLBACK EnumSuitableDevicesCB(LPCDIDEVICEINSTANCE, LPDIRECTINPUTDEVICE8, DWORD, DWORD, LPVOID); - static BOOL CALLBACK EnumDevicesCB(LPCDIDEVICEINSTANCE, LPVOID); - static BOOL CALLBACK EnumDeviceObjectsCallback(LPCDIDEVICEOBJECTINSTANCE lpddoi, LPVOID pvRef); - static LRESULT CALLBACK HookKeyboard(int, WPARAM, LPARAM); - static LRESULT CALLBACK HookMouse(int, WPARAM, LPARAM); - - /// Handle an incoming Windows keyboard message. - /// - /// Returns true if the GlobalShortcut engine signals that the - /// button should be suppressed. Returns false otherwise. - static bool handleKeyboardMessage(DWORD scancode, DWORD vkcode, bool extended, bool down); - - /// Handle an incoming Windows mouse message. - /// - /// Returns true if the GlobalShortcut engine signals that the - /// button should be suppressed. Returns false otherwise. - static bool handleMouseMessage(unsigned int btn, bool down); - - virtual bool canSuppress() Q_DECL_OVERRIDE; - void run() Q_DECL_OVERRIDE; - bool event(QEvent *e) Q_DECL_OVERRIDE; -public slots: - void timeTicked(); public: - GlobalShortcutWin(); - ~GlobalShortcutWin() Q_DECL_OVERRIDE; - void unacquire(); - ButtonInfo buttonInfo(const QVariant &) Q_DECL_OVERRIDE; - - /// Inject a native Windows keyboard message into GlobalShortcutWin's - /// event stream. This method is meant to be called from the main thread + /// Inject a native Windows raw input message into GlobalShortcutWin's + /// thread. This method is meant to be called from the main thread /// to pass native Windows keyboard messages to GlobalShortcutWin. /// - /// @param msg The keyboard message to inject into GlobalShortcutWin. - /// Must be WM_KEYDOWN, WM_KEYUP, WM_SYSKEYDOWN or WM_SYSKEYUP. - /// Otherwise the message will be ignored. - /// - /// @return Returns true if the GlobalShortcut engine signalled that - /// the button should be suppressed. Returns false otherwise. - bool injectKeyboardMessage(MSG *msg); + /// @param msg The raw input handle to inject into GlobalShortcutWin. + void injectRawInputMessage(HRAWINPUT handle); - /// Inject a native Windows mouse message into GlobalShortcutWin's - /// event stream. This method is meant to be called from the main thread - /// to pass native Windows mouse messages to GlobalShortcutWin. - /// - /// @param msg The keyboard message to inject into GlobalShortcutWin. - /// Must be WM_LBUTTONDOWN, WM_LBUTTONUP, WM_RBUTTONDOWN, - /// WM_RBUTTONUP, WM_MBUTTONDOWN, WM_MBUTTONUP, WM_XBUTTONDOWN - /// or WM_XBUTTONUP. Otherwise the message will be ignored. - /// - /// @return Returns true if the GlobalShortcut engine signalled that - /// the button should be suppressed. Returns false otherwise. - bool injectMouseMessage(MSG *msg); + ButtonInfo buttonInfo(const QVariant &button) override; + void run() override; -private: - bool areScreenReadersActive(); + GlobalShortcutWin(); + ~GlobalShortcutWin() override; +public slots: + void deviceRemoved(const HANDLE deviceHandle); + void timeTicked(); + + void on_hidMessage(const HANDLE deviceHandle, std::vector< char > reports, const uint32_t reportSize); + void on_keyboardMessage(const uint16_t flags, uint16_t scanCode, const uint16_t virtualKey); + void on_mouseMessage(const uint16_t flags, const uint16_t data); +signals: + void hidMessage(const HANDLE deviceHandle, std::vector< char > reports, const uint32_t reportSize); + void keyboardMessage(const uint16_t flags, uint16_t scanCode, const uint16_t virtualKey); + void mouseMessage(const uint16_t flags, const uint16_t data); + +protected: + struct Device { + std::string name; + std::string prefix; + std::vector< uint8_t > data; + std::vector< bool > buttons; + std::pair< uint16_t, uint16_t > usageRange; +#ifdef USE_XBOXINPUT + bool xinput; + Device() : xinput(false){}; +#endif + }; + + typedef std::unordered_map< HANDLE, Device > DeviceMap; + DeviceMap m_devices; +#ifdef USE_XBOXINPUT + std::unique_ptr< XboxInput > m_xinput; + /// Holds the number of XInput devices available on the system. + /// It is filled out by addDevice(). + uint8_t m_xinputDevices; + /// Holds the last packet number that was processed. + /// Any new data queried for a device is only valid + /// if the packet number is different than last time we queried it. + uint32_t m_xinputLastPacket[XBOXINPUT_MAX_DEVICES]; + + static bool xinputIsPressed(const uint8_t bit, const XboxInputState &state); +#endif +#ifdef USE_GKEY + std::unique_ptr< GKeyLibrary > m_gkey; +#endif + DeviceMap::iterator addDevice(const HANDLE deviceHandle); }; -uint qHash(const GUID &); - #endif diff --git a/src/mumble/MumbleApplication.cpp b/src/mumble/MumbleApplication.cpp index f729438ce..a3ab44331 100644 --- a/src/mumble/MumbleApplication.cpp +++ b/src/mumble/MumbleApplication.cpp @@ -56,43 +56,25 @@ bool MumbleApplication::event(QEvent *e) { } #ifdef Q_OS_WIN -/// gswForward forwards a native Windows keyboard/mouse message -/// into GlobalShortcutWin's event stream. -/// -/// @return Returns true if the forwarded event was suppressed -/// by GlobalShortcutWin. Otherwise, returns false. -static bool gswForward(MSG *msg) { - GlobalShortcutWin *gsw = static_cast< GlobalShortcutWin * >(GlobalShortcutEngine::engine); +bool MumbleApplication::nativeEventFilter(const QByteArray &, void *message, long *) { + auto gsw = static_cast< GlobalShortcutWin * >(GlobalShortcutEngine::engine); if (!gsw) { return false; } - switch (msg->message) { - case WM_LBUTTONDOWN: - case WM_LBUTTONUP: - case WM_RBUTTONDOWN: - case WM_RBUTTONUP: - case WM_MBUTTONDOWN: - case WM_MBUTTONUP: - case WM_XBUTTONDOWN: - case WM_XBUTTONUP: - return gsw->injectMouseMessage(msg); - case WM_KEYDOWN: - case WM_KEYUP: - case WM_SYSKEYDOWN: - case WM_SYSKEYUP: - return gsw->injectKeyboardMessage(msg); - } - return false; -} -bool MumbleApplication::nativeEventFilter(const QByteArray &, void *message, long *) { - MSG *msg = reinterpret_cast< MSG * >(message); - if (QThread::currentThread() == thread()) { - bool suppress = gswForward(msg); - if (suppress) { - return true; - } + auto msg = reinterpret_cast< const MSG * >(message); + switch (msg->message) { + case WM_INPUT: + gsw->injectRawInputMessage(reinterpret_cast< HRAWINPUT >(msg->lParam)); + break; + case WM_INPUT_DEVICE_CHANGE: + // We don't care about GIDC_ARRIVAL because we add a device only when we receive input from it. + if (msg->wParam == GIDC_REMOVAL) { + // The device is not available anymore, free resources allocated for it. + gsw->deviceRemoved(reinterpret_cast< const HANDLE >(msg->lParam)); + } } + return false; } #endif diff --git a/src/mumble/Settings.cpp b/src/mumble/Settings.cpp index 4bf80c107..000da5d72 100644 --- a/src/mumble/Settings.cpp +++ b/src/mumble/Settings.cpp @@ -516,8 +516,6 @@ Settings::Settings() { bEnableXInput2 = true; bEnableGKey = false; bEnableXboxInput = true; - bEnableWinHooks = true; - bDirectInputVerboseLogging = false; bEnableUIAccess = true; for (int i = Log::firstMsgType; i <= Log::lastMsgType; ++i) { @@ -975,8 +973,6 @@ void Settings::load(QSettings *settings_ptr) { LOAD(bEnableXInput2, "shortcut/x11/xinput2/enable"); LOAD(bEnableGKey, "shortcut/gkey"); LOAD(bEnableXboxInput, "shortcut/windows/xbox/enable"); - LOAD(bEnableWinHooks, "winhooks"); - LOAD(bDirectInputVerboseLogging, "shortcut/windows/directinput/verboselogging"); LOAD(bEnableUIAccess, "shortcut/windows/uiaccess/enable"); int nshorts = settings_ptr->beginReadArray(QLatin1String("shortcuts")); @@ -1371,8 +1367,6 @@ void Settings::save() { SAVE(bEnableXInput2, "shortcut/x11/xinput2/enable"); SAVE(bEnableGKey, "shortcut/gkey"); SAVE(bEnableXboxInput, "shortcut/windows/xbox/enable"); - SAVE(bEnableWinHooks, "winhooks"); - SAVE(bDirectInputVerboseLogging, "shortcut/windows/directinput/verboselogging"); SAVE(bEnableUIAccess, "shortcut/windows/uiaccess/enable"); settings_ptr->beginWriteArray(QLatin1String("shortcuts")); diff --git a/src/mumble/Settings.h b/src/mumble/Settings.h index 285da8d5d..062c9a02d 100644 --- a/src/mumble/Settings.h +++ b/src/mumble/Settings.h @@ -284,11 +284,8 @@ struct Settings { bool bEnableXInput2; bool bEnableGKey; bool bEnableXboxInput; - bool bEnableWinHooks; - /// Enable verbose logging in GlobalShortcutWin's DirectInput backend. - bool bDirectInputVerboseLogging; - /// Enable use of UIAccess (Windows's UI automation feature). This allows - /// Mumble greater access to global shortcuts. + /// Enable use of UIAccess (Windows's UI automation feature). This allows Mumble to receive WM_INPUT messages when + /// an application with elevated privileges is in foreground. bool bEnableUIAccess; QList< Shortcut > qlShortcuts; diff --git a/src/mumble/main.cpp b/src/mumble/main.cpp index a0d3f337d..120e7874c 100644 --- a/src/mumble/main.cpp +++ b/src/mumble/main.cpp @@ -680,8 +680,8 @@ int main(int argc, char **argv) { #ifdef Q_OS_WIN // Set mumble_mw_hwnd in os_win.cpp. - // Used by APIs in ASIOInput and GlobalShortcut_win that require a HWND. - mumble_mw_hwnd = GetForegroundWindow(); + // Used in ASIOInput and GlobalShortcut_win by APIs that require a HWND. + mumble_mw_hwnd = reinterpret_cast< HWND >(Global::get().mw->winId()); #endif #ifdef USE_DBUS