363 lines
13 KiB
C++
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);
|
|
}
|
|
} |