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