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