diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..1ff0c42 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,63 @@ +############################################################################### +# Set default behavior to automatically normalize line endings. +############################################################################### +* text=auto + +############################################################################### +# Set default behavior for command prompt diff. +# +# This is need for earlier builds of msysgit that does not have it on by +# default for csharp files. +# Note: This is only used by command line +############################################################################### +#*.cs diff=csharp + +############################################################################### +# Set the merge driver for project and solution files +# +# Merging from the command prompt will add diff markers to the files if there +# are conflicts (Merging from VS is not affected by the settings below, in VS +# the diff markers are never inserted). Diff markers may cause the following +# file extensions to fail to load in VS. An alternative would be to treat +# these files as binary and thus will always conflict and require user +# intervention with every merge. To do so, just uncomment the entries below +############################################################################### +#*.sln merge=binary +#*.csproj merge=binary +#*.vbproj merge=binary +#*.vcxproj merge=binary +#*.vcproj merge=binary +#*.dbproj merge=binary +#*.fsproj merge=binary +#*.lsproj merge=binary +#*.wixproj merge=binary +#*.modelproj merge=binary +#*.sqlproj merge=binary +#*.wwaproj merge=binary + +############################################################################### +# behavior for image files +# +# image files are treated as binary by default. +############################################################################### +#*.jpg binary +#*.png binary +#*.gif binary + +############################################################################### +# diff behavior for common document formats +# +# Convert binary document formats to text before diffing them. This feature +# is only available from the command line. Turn it on by uncommenting the +# entries below. +############################################################################### +#*.doc diff=astextplain +#*.DOC diff=astextplain +#*.docx diff=astextplain +#*.DOCX diff=astextplain +#*.dot diff=astextplain +#*.DOT diff=astextplain +#*.pdf diff=astextplain +#*.PDF diff=astextplain +#*.rtf diff=astextplain +#*.RTF diff=astextplain diff --git a/Mouse Debouncer/Mouse Debouncer.rc b/Mouse Debouncer/Mouse Debouncer.rc new file mode 100644 index 0000000..ca525a0 Binary files /dev/null and b/Mouse Debouncer/Mouse Debouncer.rc differ diff --git a/Mouse Debouncer/Mouse Debouncer.sln b/Mouse Debouncer/Mouse Debouncer.sln new file mode 100644 index 0000000..8bf2342 --- /dev/null +++ b/Mouse Debouncer/Mouse Debouncer.sln @@ -0,0 +1,31 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.27004.2008 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Mouse Debouncer", "Mouse Debouncer.vcxproj", "{9B364F75-77C2-439E-BCF3-8366AF5BCB0B}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {9B364F75-77C2-439E-BCF3-8366AF5BCB0B}.Debug|x64.ActiveCfg = Debug|x64 + {9B364F75-77C2-439E-BCF3-8366AF5BCB0B}.Debug|x64.Build.0 = Debug|x64 + {9B364F75-77C2-439E-BCF3-8366AF5BCB0B}.Debug|x86.ActiveCfg = Debug|Win32 + {9B364F75-77C2-439E-BCF3-8366AF5BCB0B}.Debug|x86.Build.0 = Debug|Win32 + {9B364F75-77C2-439E-BCF3-8366AF5BCB0B}.Release|x64.ActiveCfg = Release|x64 + {9B364F75-77C2-439E-BCF3-8366AF5BCB0B}.Release|x64.Build.0 = Release|x64 + {9B364F75-77C2-439E-BCF3-8366AF5BCB0B}.Release|x86.ActiveCfg = Release|Win32 + {9B364F75-77C2-439E-BCF3-8366AF5BCB0B}.Release|x86.Build.0 = Release|Win32 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {D2BD50B9-5CE1-4FA6-998F-AA86E95B3FC8} + EndGlobalSection +EndGlobal diff --git a/Mouse Debouncer/Mouse Debouncer.vcxproj b/Mouse Debouncer/Mouse Debouncer.vcxproj new file mode 100644 index 0000000..76ea9c4 --- /dev/null +++ b/Mouse Debouncer/Mouse Debouncer.vcxproj @@ -0,0 +1,129 @@ + + + + + Debug + Win32 + + + Release + Win32 + + + Debug + x64 + + + Release + x64 + + + + 15.0 + {9B364F75-77C2-439E-BCF3-8366AF5BCB0B} + MouseDebouncer + 10.0.16299.0 + + + + Application + true + v141 + Unicode + + + Application + false + v141 + true + Unicode + + + Application + true + v141 + Unicode + + + Application + false + v141 + true + Unicode + + + + + + + + + + + + + + + + + + + + + + Level3 + Disabled + true + + + + + Level3 + Disabled + true + + + + + Level3 + MaxSpeed + true + true + true + + + true + true + + + + + Level3 + MaxSpeed + true + true + true + + + true + true + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Mouse Debouncer/Mouse Debouncer.vcxproj.filters b/Mouse Debouncer/Mouse Debouncer.vcxproj.filters new file mode 100644 index 0000000..0e8e068 --- /dev/null +++ b/Mouse Debouncer/Mouse Debouncer.vcxproj.filters @@ -0,0 +1,43 @@ + + + + + {4FC737F1-C7A5-4376-A066-2A32D752A2FF} + cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx + + + {93995380-89BD-4b04-88EB-625FBE52EBFB} + h;hh;hpp;hxx;hm;inl;inc;xsd + + + {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} + rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms + + + + + Source Files + + + + + Header Files + + + Header Files + + + + + Resource Files + + + + + Resource Files + + + Resource Files + + + \ No newline at end of file diff --git a/Mouse Debouncer/icon.ico b/Mouse Debouncer/icon.ico new file mode 100644 index 0000000..f2818b9 Binary files /dev/null and b/Mouse Debouncer/icon.ico differ diff --git a/Mouse Debouncer/main.c b/Mouse Debouncer/main.c new file mode 100644 index 0000000..62a3d80 --- /dev/null +++ b/Mouse Debouncer/main.c @@ -0,0 +1,531 @@ +#pragma comment(lib, "comctl32.lib") +#pragma comment(lib, "Winmm.lib") +#pragma comment(linker,"/manifestdependency:\"type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='*' publicKeyToken='6595b64144ccf1df' language='*'\"") +#define STRICT +#define OPTPARSE_IMPLEMENTATION +#define OPTPARSE_API static +#include +#include +#include +#include +#include +#include +#include +#include +#include "optparse.h" +#include "resource.h" + +#if defined (DEBUG) | defined (_DEBUG) +#define DEBUG_PRINTF(fmt, ...) \ + { \ + WCHAR buffer[1024]; \ + StringCchPrintf(buffer, ARRAYSIZE(buffer), fmt, __VA_ARGS__); \ + OutputDebugString(buffer); \ + } +#else +#define DEBUG_PRINTF(fmt, ...) +#endif + +#define WM_NOTIFYICON (WM_USER + 1) +#define IDM_EXIT (WM_USER + 2) + + +typedef enum +{ + MOUSE_BUTTON_UNKNOWN = -1, + MOUSE_BUTTON_LEFT, + MOUSE_BUTTON_RIGHT, + MOUSE_BUTTON_MIDDLE, + MOUSE_BUTTON_X1, + MOUSE_BUTTON_X2, + MOUSE_BUTTON_COUNT +} MouseButton; + +typedef struct +{ + bool isMonitored; + bool isBlocked; + uint32_t blocks; + uint64_t previousTime; + uint64_t threshold; + uint32_t thresholdMs; +} MOUSEBUTTONDATA; + +static LRESULT CALLBACK WindowProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam); +static LRESULT CALLBACK LowLevelMouseProc(int nCode, WPARAM wParam, LPARAM lParam); +static inline bool RegisterInvisibleClass(const HINSTANCE hInstance); +static bool AddNotifyIcon(const HINSTANCE hInstance, const HWND hWnd, const bool restore); +static void RemoveNotifyIcon(); +static bool InstallMSLLHook(); +static void UninstallMSLLHook(); +static void ProcessCommandLineArgs(); +static void PrepareMouseButtonData(); +static void SetDoubleClickThreshold(const int threshold, const MouseButton button); +static void ShowContextMenu(const HWND hWnd, const int x, const int y); +static void CDECL ShowErrorMessageBox(LPCWSTR message, ...); +static MouseButton GetButtonByWParam(const WPARAM wParam, const PMSLLHOOKSTRUCT pdata); +static LPCWSTR GetButtonName(const MouseButton button); + +static LPCWSTR APPNAME = L"Mouse Debouncer"; +static LPCWSTR CLASSNAME = L"MouseDebouncerWndClass"; + +static HHOOK msll_hook = NULL; +static NOTIFYICONDATA notify_icon_data; +static MOUSEBUTTONDATA mouse_button_data[MOUSE_BUTTON_COUNT]; +static int32_t double_click_threshold_ms_max = 500; +static int32_t double_click_threshold_ms_min = 1; +static uint32_t double_click_threshold_ms = 60; +static uint64_t double_click_threshold; +static uint64_t counts_per_second; +static bool use_qpc; + +int CALLBACK WinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance, _In_ LPSTR lpCmdLine, _In_ int nCmdShow) +{ + // Limit the application to one instance. + const HANDLE mutex = CreateMutex(NULL, TRUE, L"{05B95384-625D-491A-A326-94758957C021}"); + if (!mutex) + { + ShowErrorMessageBox(L"The mutex could not be created!"); + return EXIT_FAILURE; + } + if (GetLastError() == ERROR_ALREADY_EXISTS || GetLastError() == ERROR_ACCESS_DENIED) + { + ShowErrorMessageBox(L"Only one instance at a time!"); + return EXIT_SUCCESS; + } + + ProcessCommandLineArgs(); + PrepareMouseButtonData(); + + const ATOM atom = RegisterInvisibleClass(hInstance); + if (atom) + { + // hWndParent is not HWND_MESSAGE because message-only windows don't receive broadcast messages like TaskbarCreated. + if (!CreateWindow(CLASSNAME, APPNAME, 0, 0, 0, 0, 0, NULL, NULL, hInstance, NULL)) + { + ShowErrorMessageBox(L"The window couldn't be created."); + PostQuitMessage(1); + } + } + else + { + ShowErrorMessageBox(L"The window class couldn't be registered."); + PostQuitMessage(1); + } + + // Start the message loop. + // Set hWnd to NULL because WM_QUIT will be sent to the message loop thread, not to any particular window. + // If the function retrieves WM_QUIT, the return value is zero. + MSG msg; + BOOL ret; + while ((ret = GetMessage(&msg, NULL, 0, 0)) != 0) + { + if (ret != -1) + { + TranslateMessage(&msg); + DispatchMessage(&msg); + } + else + { + // It's extremely unlikely that GetMessage() will return -1 but just in case try to release the important stuff. + ShowErrorMessageBox(L"An unknown error occured: 0x%lx", GetLastError()); + UninstallMSLLHook(); + ReleaseMutex(mutex); + CloseHandle(mutex); + return GetLastError(); + } + } + + if (atom) UnregisterClass(CLASSNAME, hInstance); + ReleaseMutex(mutex); + CloseHandle(mutex); + + // Return the exit code given in PostQuitMessage() function. + return (int)msg.wParam; +} + +static LRESULT CALLBACK WindowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) +{ + // This message is needed for restoring the notification icon after a explorer crash. + static UINT wm_taskbarcreated; + + switch (uMsg) + { + case WM_CREATE: + // Add NotifyIcon to the taskbar ("system tray") and install the hook. + if (!AddNotifyIcon(((LPCREATESTRUCT)lParam)->hInstance, hWnd, false) || !InstallMSLLHook()) + return -1; // destroy the window + wm_taskbarcreated = RegisterWindowMessage(L"TaskbarCreated"); + return 0; + case WM_NOTIFYICON: + // With NOTIFYICON_VERSION_4 LOWORD(lParam) contains notification events (WM_CONTEXTMENU, ..). + switch (LOWORD(lParam)) + { + case WM_CONTEXTMENU: + ShowContextMenu(hWnd, GET_X_LPARAM(wParam), GET_Y_LPARAM(wParam)); + return 0; + default: ; + } + break; + case WM_COMMAND: + // If the message source is a menu, the lower word of wParam contains the menu identifier (IDM_*). + switch (LOWORD(wParam)) + { + case IDM_EXIT: + PostMessage(hWnd, WM_CLOSE, 0, 0); + return 0; + default: ; + } + break; + case WM_CLOSE: + // Destroy the window and send WM_DESTROY to deactivate it. + DestroyWindow(hWnd); + break; + case WM_DESTROY: + // Free the icon and post a WM_QUIT message to the thread's message queue. + UninstallMSLLHook(); + RemoveNotifyIcon(); + PostQuitMessage(0); + return 0; + default: + if (uMsg == wm_taskbarcreated) + AddNotifyIcon((HINSTANCE)GetModuleHandle(NULL), hWnd, true); + break; + } + + // Pass all unhandled messages to DefWindowProc. + return DefWindowProc(hWnd, uMsg, wParam, lParam); +} + +static LRESULT CALLBACK LowLevelMouseProc(int nCode, WPARAM wParam, LPARAM lParam) +{ + if (nCode == HC_ACTION && wParam != WM_MOUSEMOVE) + { + // MSLLHOOKSTRUCT is needed to differentiate between XBUTTON1 & XBUTTON2 and to get the message's time stamp. + const PMSLLHOOKSTRUCT pdata = (PMSLLHOOKSTRUCT)lParam; + const MouseButton pressedButton = GetButtonByWParam(wParam, pdata); + + if (pressedButton >= 0 && mouse_button_data[pressedButton].isMonitored) + { + uint64_t currentTime; + if (use_qpc) + // Casting should be fine because LARGE_INTEGER is a union containing LONGLONG next to the parts struct, + // so the memory layout will not be a problem (?) - otherwise QuadPart of a temporal LI has to be used. + QueryPerformanceCounter((LARGE_INTEGER*)¤tTime); + else + currentTime = pdata->time; + + switch (wParam) + { + case WM_LBUTTONDOWN: case WM_RBUTTONDOWN: case WM_MBUTTONDOWN: case WM_XBUTTONDOWN: + { + const uint64_t elapsedTime = currentTime - mouse_button_data[pressedButton].previousTime; + if (!mouse_button_data[pressedButton].isBlocked && elapsedTime <= double_click_threshold) + { + mouse_button_data[pressedButton].isBlocked = true; + mouse_button_data[pressedButton].blocks++; + DEBUG_PRINTF(L"blocked 0x%04X (%I64u)\n", wParam, elapsedTime); + // Return nonzero value to prevent the system from passing the message to the rest of the hook chain. + return 1; + } + break; + } + case WM_LBUTTONUP: case WM_RBUTTONUP: case WM_MBUTTONUP: case WM_XBUTTONUP: + { + // The corresponding button up event should be blocked too.. + if (mouse_button_data[pressedButton].isBlocked) + { + mouse_button_data[pressedButton].isBlocked = false; + return 1; + } + + mouse_button_data[pressedButton].previousTime = currentTime; + break; + } + default: ; + } + } + } + // First parameter is optional and apparently ignored anyway. + return CallNextHookEx(msll_hook, nCode, wParam, lParam); +} + +static bool RegisterInvisibleClass(const HINSTANCE hInstance) +{ + // Set the few window class information required for creating an invisible window. + WNDCLASSEX wc = { 0 }; + wc.cbSize = sizeof(WNDCLASSEX); + wc.lpfnWndProc = (WNDPROC)WindowProc; + wc.hInstance = hInstance; + wc.lpszClassName = CLASSNAME; + + // Register the window class for subsequent use of CreateWindow. + return RegisterClassEx(&wc); +} + +static bool AddNotifyIcon(const HINSTANCE hInstance, const HWND hWnd, const bool restore) +{ + if (!restore) + { + notify_icon_data.cbSize = sizeof(NOTIFYICONDATA); + notify_icon_data.hWnd = hWnd; + // Using uID instead of guidItem because GUIDs are such a hassle (dependent on path, ..). + notify_icon_data.uID = IDI_MAINICON; + notify_icon_data.uFlags = NIF_MESSAGE | NIF_ICON | NIF_TIP | NIF_SHOWTIP; + notify_icon_data.uCallbackMessage = WM_NOTIFYICON; + // Using NOTIFYICON_VERSION_4 because it's recommended for Windows Vista and later. + notify_icon_data.uVersion = NOTIFYICON_VERSION_4; + + StringCchCopy(notify_icon_data.szTip, wcslen(APPNAME) + 1, APPNAME); + + // Using LoadIconMetric to ensure that the correct icon is loaded and scaled appropriately. + if (FAILED(LoadIconMetric(hInstance, MAKEINTRESOURCE(IDI_NOTIFYICON), LIM_SMALL, ¬ify_icon_data.hIcon))) + { + ShowErrorMessageBox(L"The icon couldn't be loaded."); + return false; + } + } + + // Try to add the icon to the notification area... + if (!Shell_NotifyIcon(NIM_ADD, ¬ify_icon_data)) + { + ShowErrorMessageBox(L"The notify icon couldn't be added."); + return false; + } + + // ...and to set it's version. + if (!Shell_NotifyIcon(NIM_SETVERSION, ¬ify_icon_data)) + { + ShowErrorMessageBox(L"The requested NOTIFYICON_VERSION_4 isn't supported."); + return false; + } + + return true; +} + +static void RemoveNotifyIcon() +{ + // Remove the icon from the notification area. + Shell_NotifyIcon(NIM_DELETE, ¬ify_icon_data); + // The icon has to be destroyed because it was retrieved by LoadIconMetric. + DestroyIcon(notify_icon_data.hIcon); +} + +static bool InstallMSLLHook() +{ + msll_hook = SetWindowsHookEx(WH_MOUSE_LL, LowLevelMouseProc, NULL, 0); + if (!msll_hook) + { + ShowErrorMessageBox(L"The mouse hook couldn't be installed."); + return false; + } + return true; +} + +static void UninstallMSLLHook() +{ + if (msll_hook) UnhookWindowsHookEx(msll_hook); +} + +// ToDo: Add optparse wchar support to switch to wWinMain and replace atoi? +static void ProcessCommandLineArgs() +{ + struct optparse_long longopts[] = + { + { "qpc", 'q', OPTPARSE_NONE }, + { "threshold", 't', OPTPARSE_REQUIRED }, + { "left", 'l', OPTPARSE_OPTIONAL }, + { "right", 'r', OPTPARSE_OPTIONAL }, + { "middle", 'm', OPTPARSE_OPTIONAL }, + { "four", 'b', OPTPARSE_OPTIONAL }, + { "five", 'f', OPTPARSE_OPTIONAL }, + { 0 } + }; + + int option; + struct optparse options; + + optparse_init(&options, __argv); + + while ((option = optparse_long(&options, longopts, NULL)) != -1) + { + switch (option) + { + case 'q': + use_qpc = true; + break; + case 't': + SetDoubleClickThreshold(atoi(options.optarg), MOUSE_BUTTON_COUNT); + break; + case 'l': + mouse_button_data[MOUSE_BUTTON_LEFT].isMonitored = true; + if (options.optarg) SetDoubleClickThreshold(atoi(options.optarg), MOUSE_BUTTON_LEFT); + break; + case 'r': + mouse_button_data[MOUSE_BUTTON_RIGHT].isMonitored = true; + if (options.optarg) SetDoubleClickThreshold(atoi(options.optarg), MOUSE_BUTTON_RIGHT); + break; + case 'm': + mouse_button_data[MOUSE_BUTTON_MIDDLE].isMonitored = true; + if (options.optarg) SetDoubleClickThreshold(atoi(options.optarg), MOUSE_BUTTON_MIDDLE); + break; + case 'b': + mouse_button_data[MOUSE_BUTTON_X1].isMonitored = true; + if (options.optarg) SetDoubleClickThreshold(atoi(options.optarg), MOUSE_BUTTON_X1); + break; + case 'f': + mouse_button_data[MOUSE_BUTTON_X2].isMonitored = true; + if (options.optarg) SetDoubleClickThreshold(atoi(options.optarg), MOUSE_BUTTON_X2); + break; + default:; + } + } +} + +static void PrepareMouseButtonData() +{ + if (use_qpc && !QueryPerformanceFrequency((LARGE_INTEGER*)&counts_per_second)) + { + // That shouldn't happen for systems that run on Windows XP or later. + ShowErrorMessageBox( + L"Apparently your system doesn't support the high-resolution performance counter." + L"Please remove -q; --qpc from the command line parameters." + L"Switching to the ordinary timing method now."); + use_qpc = false; + } + + if (use_qpc) + // double_click_threshold : counts per x ms + // Setting it like that will safe time in the hook callback because we do not have to convert the counts to ms everytime. + double_click_threshold = double_click_threshold_ms * counts_per_second / 1000; + else + double_click_threshold = double_click_threshold_ms; + + // Set the individual button thresholds to the global threshold if they weren't specified in the launch options. + bool isButtonSpecified = false; + for (int i = MOUSE_BUTTON_COUNT; i--;) + { + if (!mouse_button_data[i].thresholdMs) + { + mouse_button_data[i].threshold = double_click_threshold_ms; + mouse_button_data[i].thresholdMs = double_click_threshold_ms; + } + + if (use_qpc) + mouse_button_data[i].threshold *= counts_per_second / 1000; + + if (mouse_button_data[i].isMonitored) + isButtonSpecified = true; + } + + // Default the monitored button to the left one if none were specified in the launch options. + if (!isButtonSpecified) + mouse_button_data[MOUSE_BUTTON_LEFT].isMonitored = true; +} + +static void SetDoubleClickThreshold(const int threshold, const MouseButton button) +{ + // Set double click threshold for passed button if in value range. + if (threshold >= double_click_threshold_ms_min && threshold <= double_click_threshold_ms_max) + { + // Exception: Set the general threshold if MOUSE_BUTTON_COUNT was passed. + if (button == MOUSE_BUTTON_COUNT) + double_click_threshold_ms = threshold; + else + mouse_button_data[button].thresholdMs = threshold; + } + else + ShowErrorMessageBox(L"Invalid threshold for '%s Mouse Button': %d (min: %I32ums max: %I32ums)", + GetButtonName(button), threshold, double_click_threshold_ms_min, double_click_threshold_ms_max); +} + +static void ShowContextMenu(const HWND hWnd, const int x, const int y) +{ + // The current window must be the foreground window before calling TrackPopupMenu. + // Otherwise, the menu will not disappear when the user clicks outside of the menu. + SetForegroundWindow(hWnd); + + const HMENU hMenu = CreatePopupMenu(); + if (hMenu) + { + WCHAR buffer[64]; + uint32_t totalblocks = 0; + + // Calculate total blocked clicks. + for (int i = MOUSE_BUTTON_COUNT; i--;) + totalblocks += mouse_button_data[i].blocks; + + // Add general information. + StringCchPrintf(buffer, 64, L"General\t%6I32u blocks %03I32u ms", totalblocks, double_click_threshold_ms); + InsertMenu(hMenu, -1, MF_BYPOSITION | MF_STRING | MF_GRAYED, 0, buffer); + InsertMenu(hMenu, -1, MF_BYPOSITION | MF_SEPARATOR, 0, NULL); + + // Add individual button information. + for (int i = 0; i < MOUSE_BUTTON_COUNT; i++) + { + if (mouse_button_data[i].isMonitored) + { + StringCchPrintf(buffer, 64, L"%s Mouse Button\t%6I32u blocks %03I32u ms", + GetButtonName(i), mouse_button_data[i].blocks, mouse_button_data[i].thresholdMs); + InsertMenu(hMenu, -1, MF_BYPOSITION | MF_STRING | MF_GRAYED, 0, buffer); + } + } + + // Add exit item. + InsertMenu(hMenu, -1, MF_BYPOSITION | MF_SEPARATOR, 0, NULL); + InsertMenu(hMenu, -1, MF_BYPOSITION | MF_STRING, IDM_EXIT, L"Exit"); + + // The window does not receive a WM_COMMAND message from the menu until the function returns. + TrackPopupMenu(hMenu, TPM_BOTTOMALIGN, x, y, 0, hWnd, NULL); + DestroyMenu(hMenu); + } + else + { + ShowErrorMessageBox(L"The popup menu couldn't be created. Exiting now.."); + PostMessage(hWnd, WM_CLOSE, 0, 0); + } +} + +static void CDECL ShowErrorMessageBox(LPCWSTR message, ...) +{ + WCHAR buffer[1024]; + va_list args; + va_start(args, message); + StringCchVPrintf(buffer, ARRAYSIZE(buffer), message, args); + va_end(args); + + // Set hWnd to NULL, so the message box will not have a owner window, + // which is invisible anyway. With NULL it'll also show up in the taskbar. + MessageBox(NULL, buffer, APPNAME, MB_OK | MB_ICONEXCLAMATION); +} + +static inline MouseButton GetButtonByWParam(const WPARAM wParam, const PMSLLHOOKSTRUCT pdata) +{ + switch (wParam) + { + case WM_LBUTTONDOWN: case WM_LBUTTONUP: + return MOUSE_BUTTON_LEFT; + case WM_RBUTTONDOWN: case WM_RBUTTONUP: + return MOUSE_BUTTON_RIGHT; + case WM_MBUTTONDOWN: case WM_MBUTTONUP: + return MOUSE_BUTTON_MIDDLE; + case WM_XBUTTONDOWN: case WM_XBUTTONUP: + return HIWORD(pdata->mouseData) == XBUTTON1 ? MOUSE_BUTTON_X1 : MOUSE_BUTTON_X2; + default: + return MOUSE_BUTTON_UNKNOWN; // That shouldn't happen.. + } +} + +static LPCWSTR GetButtonName(const MouseButton button) +{ + switch (button) + { + case MOUSE_BUTTON_LEFT: return L"Left"; + case MOUSE_BUTTON_RIGHT: return L"Right"; + case MOUSE_BUTTON_MIDDLE: return L"Middle"; + case MOUSE_BUTTON_X1: return L"4th"; + case MOUSE_BUTTON_X2: return L"5th"; + case MOUSE_BUTTON_COUNT: return L"Every"; + default: return NULL; + } +} diff --git a/Mouse Debouncer/notify.ico b/Mouse Debouncer/notify.ico new file mode 100644 index 0000000..2a54ae5 Binary files /dev/null and b/Mouse Debouncer/notify.ico differ diff --git a/Mouse Debouncer/optparse.h b/Mouse Debouncer/optparse.h new file mode 100644 index 0000000..ed5c220 --- /dev/null +++ b/Mouse Debouncer/optparse.h @@ -0,0 +1,414 @@ +/* Optparse --- portable, reentrant, embeddable, getopt-like option parser +* +* This is free and unencumbered software released into the public domain. +* +* To get the implementation, define OPTPARSE_IMPLEMENTATION. +* Optionally define OPTPARSE_API to control the API's visibility +* and/or linkage (static, __attribute__, __declspec). +* +* The POSIX getopt() option parser has three fatal flaws. These flaws +* are solved by Optparse. +* +* 1) Parser state is stored entirely in global variables, some of +* which are static and inaccessible. This means only one thread can +* use getopt(). It also means it's not possible to recursively parse +* nested sub-arguments while in the middle of argument parsing. +* Optparse fixes this by storing all state on a local struct. +* +* 2) The POSIX standard provides no way to properly reset the parser. +* This means for portable code that getopt() is only good for one +* run, over one argv with one option string. It also means subcommand +* options cannot be processed with getopt(). Most implementations +* provide a method to reset the parser, but it's not portable. +* Optparse provides an optparse_arg() function for stepping over +* subcommands and continuing parsing of options with another option +* string. The Optparse struct itself can be passed around to +* subcommand handlers for additional subcommand option parsing. A +* full reset can be achieved by with an additional optparse_init(). +* +* 3) Error messages are printed to stderr. This can be disabled with +* opterr, but the messages themselves are still inaccessible. +* Optparse solves this by writing an error message in its errmsg +* field. The downside to Optparse is that this error message will +* always be in English rather than the current locale. +* +* Optparse should be familiar with anyone accustomed to getopt(), and +* it could be a nearly drop-in replacement. The option string is the +* same and the fields have the same names as the getopt() global +* variables (optarg, optind, optopt). +* +* Optparse also supports GNU-style long options with optparse_long(). +* The interface is slightly different and simpler than getopt_long(). +* +* By default, argv is permuted as it is parsed, moving non-option +* arguments to the end. This can be disabled by setting the `permute` +* field to 0 after initialization. +*/ +#ifndef OPTPARSE_H +#define OPTPARSE_H + +#ifndef OPTPARSE_API +# define OPTPARSE_API +#endif + +struct optparse { + char **argv; + int permute; + int optind; + int optopt; + char *optarg; + char errmsg[64]; + int subopt; +}; + +enum optparse_argtype { + OPTPARSE_NONE, + OPTPARSE_REQUIRED, + OPTPARSE_OPTIONAL +}; + +struct optparse_long { + const char *longname; + int shortname; + enum optparse_argtype argtype; +}; + +/** +* Initializes the parser state. +*/ +OPTPARSE_API +void optparse_init(struct optparse *options, char **argv); + +/** +* Read the next option in the argv array. +* @param optstring a getopt()-formatted option string. +* @return the next option character, -1 for done, or '?' for error +* +* Just like getopt(), a character followed by no colons means no +* argument. One colon means the option has a required argument. Two +* colons means the option takes an optional argument. +*/ +OPTPARSE_API +int optparse(struct optparse *options, const char *optstring); + +/** +* Handles GNU-style long options in addition to getopt() options. +* This works a lot like GNU's getopt_long(). The last option in +* longopts must be all zeros, marking the end of the array. The +* longindex argument may be NULL. +*/ +OPTPARSE_API +int optparse_long(struct optparse *options, + const struct optparse_long *longopts, + int *longindex); + +/** +* Used for stepping over non-option arguments. +* @return the next non-option argument, or NULL for no more arguments +* +* Argument parsing can continue with optparse() after using this +* function. That would be used to parse the options for the +* subcommand returned by optparse_arg(). This function allows you to +* ignore the value of optind. +*/ +OPTPARSE_API +char *optparse_arg(struct optparse *options); + +/* Implementation */ +#ifdef OPTPARSE_IMPLEMENTATION + +#define OPTPARSE_MSG_INVALID "invalid option" +#define OPTPARSE_MSG_MISSING "option requires an argument" +#define OPTPARSE_MSG_TOOMANY "option takes no arguments" + +static int +optparse_error(struct optparse *options, const char *msg, const char *data) +{ + unsigned p = 0; + const char *sep = " -- '"; + while (*msg) + options->errmsg[p++] = *msg++; + while (*sep) + options->errmsg[p++] = *sep++; + while (p < sizeof(options->errmsg) - 2 && *data) + options->errmsg[p++] = *data++; + options->errmsg[p++] = '\''; + options->errmsg[p++] = '\0'; + return '?'; +} + +OPTPARSE_API +void +optparse_init(struct optparse *options, char **argv) +{ + options->argv = argv; + options->permute = 1; + options->optind = 1; + options->subopt = 0; + options->optarg = 0; + options->errmsg[0] = '\0'; +} + +static int +optparse_is_dashdash(const char *arg) +{ + return arg != 0 && arg[0] == '-' && arg[1] == '-' && arg[2] == '\0'; +} + +static int +optparse_is_shortopt(const char *arg) +{ + return arg != 0 && arg[0] == '-' && arg[1] != '-' && arg[1] != '\0'; +} + +static int +optparse_is_longopt(const char *arg) +{ + return arg != 0 && arg[0] == '-' && arg[1] == '-' && arg[2] != '\0'; +} + +static void +optparse_permute(struct optparse *options, int index) +{ + char *nonoption = options->argv[index]; + int i; + for (i = index; i < options->optind - 1; i++) + options->argv[i] = options->argv[i + 1]; + options->argv[options->optind - 1] = nonoption; +} + +static int +optparse_argtype(const char *optstring, char c) +{ + int count = OPTPARSE_NONE; + if (c == ':') + return -1; + for (; *optstring && c != *optstring; optstring++); + if (!*optstring) + return -1; + if (optstring[1] == ':') + count += optstring[2] == ':' ? 2 : 1; + return count; +} + +OPTPARSE_API +int +optparse(struct optparse *options, const char *optstring) +{ + int type; + char *next; + char *option = options->argv[options->optind]; + options->errmsg[0] = '\0'; + options->optopt = 0; + options->optarg = 0; + if (option == 0) { + return -1; + } + else if (optparse_is_dashdash(option)) { + options->optind++; /* consume "--" */ + return -1; + } + else if (!optparse_is_shortopt(option)) { + if (options->permute) { + int index = options->optind++; + int r = optparse(options, optstring); + optparse_permute(options, index); + options->optind--; + return r; + } + else { + return -1; + } + } + option += options->subopt + 1; + options->optopt = option[0]; + type = optparse_argtype(optstring, option[0]); + next = options->argv[options->optind + 1]; + switch (type) { + case -1: { + char str[2] = { 0, 0 }; + str[0] = option[0]; + options->optind++; + return optparse_error(options, OPTPARSE_MSG_INVALID, str); + } + case OPTPARSE_NONE: + if (option[1]) { + options->subopt++; + } + else { + options->subopt = 0; + options->optind++; + } + return option[0]; + case OPTPARSE_REQUIRED: + options->subopt = 0; + options->optind++; + if (option[1]) { + options->optarg = option + 1; + } + else if (next != 0) { + options->optarg = next; + options->optind++; + } + else { + char str[2] = { 0, 0 }; + str[0] = option[0]; + options->optarg = 0; + return optparse_error(options, OPTPARSE_MSG_MISSING, str); + } + return option[0]; + case OPTPARSE_OPTIONAL: + options->subopt = 0; + options->optind++; + if (option[1]) + options->optarg = option + 1; + else + options->optarg = 0; + return option[0]; + } + return 0; +} + +OPTPARSE_API +char * +optparse_arg(struct optparse *options) +{ + char *option = options->argv[options->optind]; + options->subopt = 0; + if (option != 0) + options->optind++; + return option; +} + +static int +optparse_longopts_end(const struct optparse_long *longopts, int i) +{ + return !longopts[i].longname && !longopts[i].shortname; +} + +static void +optparse_from_long(const struct optparse_long *longopts, char *optstring) +{ + char *p = optstring; + int i; + for (i = 0; !optparse_longopts_end(longopts, i); i++) { + if (longopts[i].shortname) { + int a; + *p++ = longopts[i].shortname; + for (a = 0; a < (int)longopts[i].argtype; a++) + *p++ = ':'; + } + } + *p = '\0'; +} + +/* Unlike strcmp(), handles options containing "=". */ +static int +optparse_longopts_match(const char *longname, const char *option) +{ + const char *a = option, *n = longname; + if (longname == 0) + return 0; + for (; *a && *n && *a != '='; a++, n++) + if (*a != *n) + return 0; + return *n == '\0' && (*a == '\0' || *a == '='); +} + +/* Return the part after "=", or NULL. */ +static char * +optparse_longopts_arg(char *option) +{ + for (; *option && *option != '='; option++); + if (*option == '=') + return option + 1; + else + return 0; +} + +static int +optparse_long_fallback(struct optparse *options, + const struct optparse_long *longopts, + int *longindex) +{ + int result; + char optstring[96 * 3 + 1]; /* 96 ASCII printable characters */ + optparse_from_long(longopts, optstring); + result = optparse(options, optstring); + if (longindex != 0) { + *longindex = -1; + if (result != -1) { + int i; + for (i = 0; !optparse_longopts_end(longopts, i); i++) + if (longopts[i].shortname == options->optopt) + *longindex = i; + } + } + return result; +} + +OPTPARSE_API +int +optparse_long(struct optparse *options, + const struct optparse_long *longopts, + int *longindex) +{ + int i; + char *option = options->argv[options->optind]; + if (option == 0) { + return -1; + } + else if (optparse_is_dashdash(option)) { + options->optind++; /* consume "--" */ + return -1; + } + else if (optparse_is_shortopt(option)) { + return optparse_long_fallback(options, longopts, longindex); + } + else if (!optparse_is_longopt(option)) { + if (options->permute) { + int index = options->optind++; + int r = optparse_long(options, longopts, longindex); + optparse_permute(options, index); + options->optind--; + return r; + } + else { + return -1; + } + } + + /* Parse as long option. */ + options->errmsg[0] = '\0'; + options->optopt = 0; + options->optarg = 0; + option += 2; /* skip "--" */ + options->optind++; + for (i = 0; !optparse_longopts_end(longopts, i); i++) { + const char *name = longopts[i].longname; + if (optparse_longopts_match(name, option)) { + char *arg; + if (longindex) + *longindex = i; + options->optopt = longopts[i].shortname; + arg = optparse_longopts_arg(option); + if (longopts[i].argtype == OPTPARSE_NONE && arg != 0) { + return optparse_error(options, OPTPARSE_MSG_TOOMANY, name); + } if (arg != 0) { + options->optarg = arg; + } + else if (longopts[i].argtype == OPTPARSE_REQUIRED) { + options->optarg = options->argv[options->optind]; + if (options->optarg == 0) + return optparse_error(options, OPTPARSE_MSG_MISSING, name); + else + options->optind++; + } + return options->optopt; + } + } + return optparse_error(options, OPTPARSE_MSG_INVALID, option); +} + +#endif /* OPTPARSE_IMPLEMENTATION */ +#endif /* OPTPARSE_H */ \ No newline at end of file diff --git a/Mouse Debouncer/resource.h b/Mouse Debouncer/resource.h new file mode 100644 index 0000000..8c38c2a --- /dev/null +++ b/Mouse Debouncer/resource.h @@ -0,0 +1,17 @@ +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by Mouse Debouncer.rc +// +#define IDI_MAINICON 101 +#define IDI_NOTIFYICON 102 + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NEXT_RESOURCE_VALUE 103 +#define _APS_NEXT_COMMAND_VALUE 40001 +#define _APS_NEXT_CONTROL_VALUE 1001 +#define _APS_NEXT_SYMED_VALUE 101 +#endif +#endif