espanso/espanso-detect/src/win32/native.cpp
2021-03-14 18:24:37 +01:00

363 lines
13 KiB
C++

/*
* This file is part of espanso.
*
* Copyright (C) 2019-2021 Federico Terzi
*
* espanso is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* espanso is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
*/
#include "native.h"
#include <iostream>
#include <stdio.h>
#include <string>
#include <vector>
#include <memory>
#include <array>
#define UNICODE
#ifdef __MINGW32__
#ifndef WINVER
#define WINVER 0x0606
#endif
#define STRSAFE_NO_DEPRECATE
#endif
#include <windows.h>
#include <winuser.h>
#include <strsafe.h>
#include <Windows.h>
// How many milliseconds must pass between events before refreshing the keyboard layout
const long DETECT_REFRESH_KEYBOARD_LAYOUT_INTERVAL = 2000;
const wchar_t *const DETECT_WINCLASS = L"EspansoDetect";
const USHORT MOUSE_DOWN_FLAGS = RI_MOUSE_LEFT_BUTTON_DOWN | RI_MOUSE_RIGHT_BUTTON_DOWN | RI_MOUSE_MIDDLE_BUTTON_DOWN |
RI_MOUSE_BUTTON_1_DOWN | RI_MOUSE_BUTTON_2_DOWN | RI_MOUSE_BUTTON_3_DOWN |
RI_MOUSE_BUTTON_4_DOWN | RI_MOUSE_BUTTON_5_DOWN;
const USHORT MOUSE_UP_FLAGS = RI_MOUSE_LEFT_BUTTON_UP | RI_MOUSE_RIGHT_BUTTON_UP | RI_MOUSE_MIDDLE_BUTTON_UP |
RI_MOUSE_BUTTON_1_UP | RI_MOUSE_BUTTON_2_UP | RI_MOUSE_BUTTON_3_UP |
RI_MOUSE_BUTTON_4_UP | RI_MOUSE_BUTTON_5_UP;
typedef struct {
HKL current_keyboard_layout;
DWORD last_key_press_tick;
// Rust interop
void * rust_instance;
EventCallback event_callback;
} DetectVariables;
/*
* Message handler procedure for the window
*/
LRESULT CALLBACK detect_window_procedure(HWND window, unsigned int msg, WPARAM wp, LPARAM lp)
{
DetectVariables * variables = reinterpret_cast<DetectVariables*>(GetWindowLongPtrW(window, GWLP_USERDATA));
switch (msg)
{
case WM_DESTROY:
PostQuitMessage(0);
// Free the window variables
delete variables;
SetWindowLongPtrW(window, GWLP_USERDATA, NULL);
return 0L;
case WM_HOTKEY: // Hotkeys
{
InputEvent event = {};
event.event_type = INPUT_EVENT_TYPE_HOTKEY;
event.key_code = (int32_t) wp;
if (variables->rust_instance != NULL && variables->event_callback != NULL)
{
variables->event_callback(variables->rust_instance, event);
}
break;
}
case WM_INPUT: // Message relative to the RAW INPUT events
{
InputEvent event = {};
// Get the input size
UINT dwSize;
GetRawInputData(
(HRAWINPUT)lp,
RID_INPUT,
NULL,
&dwSize,
sizeof(RAWINPUTHEADER));
// Create a proper sized structure to hold the data
std::vector<BYTE> lpb(dwSize);
// Request the Raw input data
if (GetRawInputData((HRAWINPUT)lp, RID_INPUT, lpb.data(), &dwSize,
sizeof(RAWINPUTHEADER)) != dwSize)
{
return 0;
}
// Convert the input data
RAWINPUT *raw = reinterpret_cast<RAWINPUT *>(lpb.data());
if (raw->header.dwType == RIM_TYPEKEYBOARD) // Keyboard events
{
// We only want KEY UP AND KEY DOWN events
if (raw->data.keyboard.Message != WM_KEYDOWN && raw->data.keyboard.Message != WM_KEYUP &&
raw->data.keyboard.Message != WM_SYSKEYDOWN)
{
return 0;
}
// The alt key sends a SYSKEYDOWN instead of KEYDOWN event
int is_key_down = raw->data.keyboard.Message == WM_KEYDOWN ||
raw->data.keyboard.Message == WM_SYSKEYDOWN;
DWORD currentTick = GetTickCount();
// If enough time has passed between the last keypress and now, refresh the keyboard layout
if ((currentTick - variables->last_key_press_tick) > DETECT_REFRESH_KEYBOARD_LAYOUT_INTERVAL)
{
// Because keyboard layouts on windows are Window-specific, to get the current
// layout we need to get the foreground window and get its layout.
HWND hwnd = GetForegroundWindow();
if (hwnd)
{
DWORD threadID = GetWindowThreadProcessId(hwnd, NULL);
HKL newKeyboardLayout = GetKeyboardLayout(threadID);
// It's not always valid, so update the current value only if available.
if (newKeyboardLayout != 0)
{
variables->current_keyboard_layout = newKeyboardLayout;
}
}
variables->last_key_press_tick = currentTick;
}
// Get keyboard state ( necessary to decode the associated Unicode char )
std::vector<BYTE> lpKeyState(256);
if (GetKeyboardState(lpKeyState.data()))
{
// This flag is needed to avoid chaning the keyboard state for some layouts.
// Refer to issue: https://github.com/federico-terzi/espanso/issues/86
UINT flags = 1 << 2;
int result = ToUnicodeEx(raw->data.keyboard.VKey, raw->data.keyboard.MakeCode, lpKeyState.data(), reinterpret_cast<LPWSTR>(event.buffer), (sizeof(event.buffer)/sizeof(event.buffer[0])) - 1, flags, variables->current_keyboard_layout);
// Handle the corresponding string if present
if (result >= 1)
{
event.buffer_len = result;
}
else
{
// If the given key does not have a correspondent string, reset the buffer
memset(event.buffer, 0, sizeof(event.buffer));
event.buffer_len = 0;
}
event.event_type = INPUT_EVENT_TYPE_KEYBOARD;
event.key_code = raw->data.keyboard.VKey;
event.status = is_key_down ? INPUT_STATUS_PRESSED : INPUT_STATUS_RELEASED;
// Load the key variants when appropriate
if (raw->data.keyboard.VKey == VK_SHIFT)
{
// To discriminate between the left and right shift, we need to employ a workaround.
// See: https://stackoverflow.com/questions/5920301/distinguish-between-left-and-right-shift-keys-using-rawinput
if (raw->data.keyboard.MakeCode == 42)
{ // Left shift
event.variant = INPUT_LEFT_VARIANT;
}
if (raw->data.keyboard.MakeCode == 54)
{ // Right shift
event.variant = INPUT_RIGHT_VARIANT;
}
}
else
{
// Also the ALT and CTRL key are special cases
// Check out the previous Stackoverflow question for more information
if (raw->data.keyboard.VKey == VK_CONTROL || raw->data.keyboard.VKey == VK_MENU)
{
if ((raw->data.keyboard.Flags & RI_KEY_E0) != 0)
{
event.variant = INPUT_RIGHT_VARIANT;
}
else
{
event.variant = INPUT_LEFT_VARIANT;
}
}
}
}
}
else if (raw->header.dwType == RIM_TYPEMOUSE) // Mouse events
{
// Make sure the mouse event belongs to the supported ones
if ((raw->data.mouse.usButtonFlags & (MOUSE_DOWN_FLAGS | MOUSE_UP_FLAGS)) == 0) {
return 0;
}
event.event_type = INPUT_EVENT_TYPE_MOUSE;
if ((raw->data.mouse.usButtonFlags & MOUSE_DOWN_FLAGS) != 0)
{
event.status = INPUT_STATUS_PRESSED;
} else if ((raw->data.mouse.usButtonFlags & MOUSE_UP_FLAGS) != 0) {
event.status = INPUT_STATUS_RELEASED;
}
// Convert the mouse flags into custom button mappings
if ((raw->data.mouse.usButtonFlags & (RI_MOUSE_LEFT_BUTTON_DOWN | RI_MOUSE_LEFT_BUTTON_UP)) != 0) {
event.key_code = INPUT_MOUSE_LEFT_BUTTON;
} else if ((raw->data.mouse.usButtonFlags & (RI_MOUSE_RIGHT_BUTTON_DOWN | RI_MOUSE_RIGHT_BUTTON_UP)) != 0) {
event.key_code = INPUT_MOUSE_RIGHT_BUTTON;
} else if ((raw->data.mouse.usButtonFlags & (RI_MOUSE_MIDDLE_BUTTON_DOWN | RI_MOUSE_MIDDLE_BUTTON_UP)) != 0) {
event.key_code = INPUT_MOUSE_MIDDLE_BUTTON;
} else if ((raw->data.mouse.usButtonFlags & (RI_MOUSE_BUTTON_1_DOWN | RI_MOUSE_BUTTON_1_UP)) != 0) {
event.key_code = INPUT_MOUSE_BUTTON_1;
} else if ((raw->data.mouse.usButtonFlags & (RI_MOUSE_BUTTON_2_DOWN | RI_MOUSE_BUTTON_2_UP)) != 0) {
event.key_code = INPUT_MOUSE_BUTTON_2;
} else if ((raw->data.mouse.usButtonFlags & (RI_MOUSE_BUTTON_3_DOWN | RI_MOUSE_BUTTON_3_UP)) != 0) {
event.key_code = INPUT_MOUSE_BUTTON_3;
} else if ((raw->data.mouse.usButtonFlags & (RI_MOUSE_BUTTON_4_DOWN | RI_MOUSE_BUTTON_4_UP)) != 0) {
event.key_code = INPUT_MOUSE_BUTTON_4;
} else if ((raw->data.mouse.usButtonFlags & (RI_MOUSE_BUTTON_5_DOWN | RI_MOUSE_BUTTON_5_UP)) != 0) {
event.key_code = INPUT_MOUSE_BUTTON_5;
}
}
// If valid, send the event to the Rust layer
if (event.event_type != 0 && variables->rust_instance != NULL && variables->event_callback != NULL)
{
variables->event_callback(variables->rust_instance, event);
}
return 0;
}
default:
return DefWindowProc(window, msg, wp, lp);
}
}
void * detect_initialize(void *_self, int32_t *error_code)
{
HWND window = NULL;
// Initialize the Worker window
// Docs: https://docs.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-wndclassexa
WNDCLASSEX wndclass = {
sizeof(WNDCLASSEX), // cbSize: Size of this structure
0, // style: Class styles
detect_window_procedure, // lpfnWndProc: Pointer to the window procedure
0, // cbClsExtra: Number of extra bytes to allocate following the window-class structure
0, // cbWndExtra: The number of extra bytes to allocate following the window instance.
GetModuleHandle(0), // hInstance: A handle to the instance that contains the window procedure for the class.
NULL, // hIcon: A handle to the class icon.
LoadCursor(0, IDC_ARROW), // hCursor: A handle to the class cursor.
NULL, // hbrBackground: A handle to the class background brush.
NULL, // lpszMenuName: Pointer to a null-terminated character string that specifies the resource name of the class menu
DETECT_WINCLASS, // lpszClassName: A pointer to a null-terminated string or is an atom.
NULL // hIconSm: A handle to a small icon that is associated with the window class.
};
if (RegisterClassEx(&wndclass))
{
DetectVariables * variables = new DetectVariables();
variables->rust_instance = _self;
// Initialize the default keyboard layout
variables->current_keyboard_layout = GetKeyboardLayout(0);
// Docs: https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-createwindowexw
window = CreateWindowEx(
0, // dwExStyle: The extended window style of the window being created.
DETECT_WINCLASS, // lpClassName: A null-terminated string or a class atom created by a previous call to the RegisterClass
L"Espanso Worker Window", // lpWindowName: The window name.
WS_OVERLAPPEDWINDOW, // dwStyle: The style of the window being created.
CW_USEDEFAULT, // X: The initial horizontal position of the window.
CW_USEDEFAULT, // Y: The initial vertical position of the window.
100, // nWidth: The width, in device units, of the window.
100, // nHeight: The height, in device units, of the window.
NULL, // hWndParent: handle to the parent or owner window of the window being created.
NULL, // hMenu: A handle to a menu, or specifies a child-window identifier, depending on the window style.
GetModuleHandle(0), // hInstance: A handle to the instance of the module to be associated with the window.
NULL // lpParam: Pointer to a value to be passed to the window
);
SetWindowLongPtrW(window, GWLP_USERDATA, reinterpret_cast<::LONG_PTR>(variables));
// Register raw inputs
RAWINPUTDEVICE Rid[2];
Rid[0].usUsagePage = 0x01;
Rid[0].usUsage = 0x06;
Rid[0].dwFlags = RIDEV_NOLEGACY | RIDEV_INPUTSINK;
Rid[0].hwndTarget = window;
Rid[1].usUsagePage = 0x01;
Rid[1].usUsage = 0x02;
Rid[1].dwFlags = RIDEV_INPUTSINK;
Rid[1].hwndTarget = window;
if (RegisterRawInputDevices(Rid, 2, sizeof(Rid[0])) == FALSE)
{ // Something went wrong, error.
*error_code = -2;
return nullptr;
}
}
else
{
// Something went wrong, error.
*error_code = -1;
return nullptr;
}
return window;
}
int32_t detect_register_hotkey(void * window, HotKey hotkey) {
return RegisterHotKey((HWND)window, hotkey.hk_id, hotkey.flags, hotkey.key_code);
}
int32_t detect_eventloop(void * window, EventCallback _callback)
{
if (window)
{
DetectVariables * variables = reinterpret_cast<DetectVariables*>(GetWindowLongPtrW((HWND) window, GWLP_USERDATA));
variables->event_callback = _callback;
// Hide the window
ShowWindow((HWND) window, SW_HIDE);
// Enter the Event loop
MSG msg;
while (GetMessage(&msg, 0, 0, 0))
DispatchMessage(&msg);
}
return 1;
}
int32_t detect_destroy(void * window) {
if (window) {
return DestroyWindow((HWND) window);
}
}