cafali / SnapKey

SnapKey provides a user-friendly Razer Snap Tap/SOCD alternative, making it accessible across all keyboards!
https://github.com/cafali/SnapKey/wiki
MIT License
233 stars 13 forks source link

Feature Request: Sticky Keys #1

Closed BeardDKing closed 1 month ago

BeardDKing commented 1 month ago

Hello,

I am very pleased with this application greatly. There is a feature I would love to have added and I don't think it will require too many lines of code. I am interested in adding a feature that retains the pressed state of the originally held button after releasing the newly pressed button.

https://youtu.be/Feny5bs2JCg?t=273 4:37 - 4:51

The following YouTube link is to a video that explains the feature I would love to have in detail. As such would be unnecessary to type it all out.

Thank you.

chase-clingman commented 1 month ago

Here is the corrected code to include that feature:

// g++ -o SnapKey SnapKey.cpp -mwindows -std=c++11

#include <windows.h>
#include <shellapi.h>

#define ID_TRAY_APP_ICON                1001
#define ID_TRAY_EXIT_CONTEXT_MENU_ITEM  3000
#define WM_TRAYICON                     (WM_USER + 1)

// Global variables
bool keyA_pressed = false;
bool keyD_pressed = false;
bool keyA_was_pressed = false;
bool keyD_was_pressed = false;
HHOOK hHook = NULL;
NOTIFYICONDATA nid;

// Function declarations
LRESULT CALLBACK KeyboardProc(int nCode, WPARAM wParam, LPARAM lParam);
LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam);
void InitNotifyIconData(HWND hwnd);

int main()
{
    // Create a named mutex
    HANDLE hMutex = CreateMutex(NULL, TRUE, TEXT("SnapKeyMutex"));
    if (GetLastError() == ERROR_ALREADY_EXISTS)
    {
        MessageBox(NULL, TEXT("SnapKey is already running!"), TEXT("Error"), MB_ICONINFORMATION | MB_OK);
        return 1; // Exit the program
    }

    // Create a window class
    WNDCLASSEX wc = {0};
    wc.cbSize = sizeof(WNDCLASSEX);
    wc.lpfnWndProc = WndProc;
    wc.hInstance = GetModuleHandle(NULL);
    wc.lpszClassName = TEXT("SnapKeyClass");

    if (!RegisterClassEx(&wc)) {
        MessageBox(NULL, TEXT("Window Registration Failed!"), TEXT("Error"), MB_ICONEXCLAMATION | MB_OK);
        ReleaseMutex(hMutex); 
        CloseHandle(hMutex); 
        return 1;
    }

    // Create a window
    HWND hwnd = CreateWindowEx(
        0,
        wc.lpszClassName,
        TEXT("SnapKey"),
        WS_OVERLAPPEDWINDOW,
        CW_USEDEFAULT, CW_USEDEFAULT, 240, 120,
        NULL, NULL, wc.hInstance, NULL);

    if (hwnd == NULL) {
        MessageBox(NULL, TEXT("Window Creation Failed!"), TEXT("Error"), MB_ICONEXCLAMATION | MB_OK);
        ReleaseMutex(hMutex); 
        CloseHandle(hMutex); 
        return 1;
    }

    // Initialize and add the system tray icon
    InitNotifyIconData(hwnd);

    // Set the hook
    hHook = SetWindowsHookEx(WH_KEYBOARD_LL, KeyboardProc, NULL, 0);
    if (hHook == NULL)
    {
        MessageBox(NULL, TEXT("Failed to install hook!"), TEXT("Error"), MB_ICONEXCLAMATION | MB_OK);
        ReleaseMutex(hMutex); // Release the mutex before exiting
        CloseHandle(hMutex); // Close the handle
        return 1;
    }

    // Message loop
    MSG msg;
    while (GetMessage(&msg, NULL, 0, 0))
    {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }

    // Unhook the hook
    UnhookWindowsHookEx(hHook);

    // Remove the system tray icon
    Shell_NotifyIcon(NIM_DELETE, &nid);

    // Release and close the mutex
    ReleaseMutex(hMutex);
    CloseHandle(hMutex);

    return 0;
}

LRESULT CALLBACK KeyboardProc(int nCode, WPARAM wParam, LPARAM lParam)
{
    if (nCode == HC_ACTION)
    {
        KBDLLHOOKSTRUCT *pKeyBoard = (KBDLLHOOKSTRUCT *)lParam;
        switch (wParam)
        {
        case WM_KEYDOWN:
            if (pKeyBoard->vkCode == 'A')
            {
                if (keyD_pressed)
                {
                    // Release D
                    keyD_pressed = false;
                    keyD_was_pressed = true;
                    INPUT input = {0};
                    input.type = INPUT_KEYBOARD;
                    input.ki.wVk = 'D';
                    input.ki.dwFlags = KEYEVENTF_KEYUP;
                    SendInput(1, &input, sizeof(INPUT));
                }
                keyA_pressed = true;
                keyA_was_pressed = false;
            }
            else if (pKeyBoard->vkCode == 'D')
            {
                if (keyA_pressed)
                {
                    // Release A
                    keyA_pressed = false;
                    keyA_was_pressed = true;
                    INPUT input = {0};
                    input.type = INPUT_KEYBOARD;
                    input.ki.wVk = 'A';
                    input.ki.dwFlags = KEYEVENTF_KEYUP;
                    SendInput(1, &input, sizeof(INPUT));
                }
                keyD_pressed = true;
                keyD_was_pressed = false;
            }
            break;

        case WM_KEYUP:
            if (pKeyBoard->vkCode == 'A')
            {
                keyA_pressed = false;
                if (keyD_was_pressed)
                {
                    // Press D again
                    keyD_was_pressed = false;
                    keyD_pressed = true;
                    INPUT input = {0};
                    input.type = INPUT_KEYBOARD;
                    input.ki.wVk = 'D';
                    input.ki.dwFlags = 0;
                    SendInput(1, &input, sizeof(INPUT));
                }
            }
            else if (pKeyBoard->vkCode == 'D')
            {
                keyD_pressed = false;
                if (keyA_was_pressed)
                {
                    // Press A again
                    keyA_was_pressed = false;
                    keyA_pressed = true;
                    INPUT input = {0};
                    input.type = INPUT_KEYBOARD;
                    input.ki.wVk = 'A';
                    input.ki.dwFlags = 0;
                    SendInput(1, &input, sizeof(INPUT));
                }
            }
            break;

        default:
            break;
        }
    }
    return CallNextHookEx(hHook, nCode, wParam, lParam);
}

void InitNotifyIconData(HWND hwnd)
{
    memset(&nid, 0, sizeof(NOTIFYICONDATA));

    nid.cbSize = sizeof(NOTIFYICONDATA);
    nid.hWnd = hwnd;
    nid.uID = ID_TRAY_APP_ICON;
    nid.uFlags = NIF_ICON | NIF_MESSAGE | NIF_TIP;
    nid.uCallbackMessage = WM_TRAYICON;

    // Load the icon from the current directory
    HICON hIcon = (HICON)LoadImage(NULL, TEXT("icon.ico"), IMAGE_ICON, 0, 0, LR_LOADFROMFILE);
    if (hIcon)
    {
        nid.hIcon = hIcon;
    }
    else
    {
        // If loading the icon fails, fallback to a default icon
        nid.hIcon = LoadIcon(NULL, IDI_APPLICATION);
    }

    lstrcpy(nid.szTip, TEXT("SnapKey"));

    Shell_NotifyIcon(NIM_ADD, &nid);
}

LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
    switch (msg)
    {
    case WM_TRAYICON:
        if (lParam == WM_RBUTTONDOWN)
        {
            POINT curPoint;
            GetCursorPos(&curPoint);
            SetForegroundWindow(hwnd);

            // Create a context menu
            HMENU hMenu = CreatePopupMenu();
            AppendMenu(hMenu, MF_STRING, ID_TRAY_EXIT_CONTEXT_MENU_ITEM, TEXT("Exit SnapKey"));

            // Display the context menu
            TrackPopupMenu(hMenu, TPM_BOTTOMALIGN | TPM_LEFTALIGN, curPoint.x, curPoint.y, 0, hwnd, NULL);
            DestroyMenu(hMenu);
        }
        break;

    case WM_COMMAND:
        if (LOWORD(wParam) == ID_TRAY_EXIT_CONTEXT_MENU_ITEM)
        {
            PostQuitMessage(0);
        }
        break;

    case WM_DESTROY:
        PostQuitMessage(0);
        break;

    default:
        return DefWindowProc(hwnd, msg, wParam, lParam);
    }
    return 0;
}
cafali commented 1 month ago

Hi,

First of all, thank you very much for providing the code. It's working better than the code I attempted earlier today.

However, there's an issue with key inputs in-game. When holding the 'A' key and tapping the 'D' key, then releasing 'A' first followed by 'D', the 'A' key is automatically registered in-game, causing the character to move left without the 'A' key being actually pressed.

The same issue occurs in reverse: holding 'D', then 'A', and releasing 'D' makes the character move right.

The bug appears in these scenarios:

Hold 'A' -> Hold 'D' -> Release 'A' (while still holding 'D') -> "A" is active, without being physically pressed. Hold 'D' -> Hold 'A' -> Release 'D'(while still holding 'A') -> "D" is active, without being physically pressed.

Thanks again for your help!

retrolyze52 commented 1 month ago

I'm having the same problem with the provided code.

Hold 'A' -> Hold 'D' -> Release 'A' (while still holding 'D') -> "A" is active, without being physically pressed. Hold 'D' -> Hold 'A' -> Release 'D'(while still holding 'A') -> "D" is active, without being physically pressed.

Any solution?

Fqmane commented 1 month ago

well, can't wait for it to be solved!!! <3 @cafali you are fucking amazing for making SnapKey available for everyone

Fqmane commented 1 month ago

there's already some null binds for CS i'm just going to leave it here, maybe logic might help (i dont know anything about coding)

// de-subticked movement
alias +forward_ "+forward;+forward"
alias -forward_ "-forward;-forward;-forward"
alias +left_ "+left;+left"
alias -left_ "-left;-left;-left"
alias +back_ "+back;+back"
alias -back_ "-back;-back;-back"
alias +right_ "+right;+right"
alias -right_ "-right;-right;-right"

// null binds
alias checkfwd ""
alias checkback ""
alias checkleft ""
alias checkright ""

alias +mfwd "-back_; +forward_; alias checkfwd +forward_"
alias +mback "-forward_; +back_; alias checkback +back_"
alias +mleft "-right_; +left_; alias checkleft +left_"
alias +mright "-left_; +right_; alias checkright +right_"

alias -mfwd "-forward_; -back_; checkback; alias checkfwd"
alias -mback "-back_; -forward_; checkfwd; alias checkback"
alias -mleft "-left_; -right_; checkright; alias checkleft"
alias -mright "-right_; -left_; checkleft; alias checkright"

bind W +mfwd
bind A +mleft
bind S +mback
bind D +mright
cafali commented 1 month ago

Hello everyone! The Sticky Key feature is now available in SnapKey version 1.1.4. Enjoy!

1.1.4 - 25/07/2024

Sticky Keys:

Special thanks to @minteeaa for making this possible.

Config: