From 4c6b959beb6d049da99c78172d702d5cee23c931 Mon Sep 17 00:00:00 2001 From: Quadstronaut <2138096+Quadstronaut@users.noreply.github.com> Date: Sat, 25 Apr 2026 23:49:21 -0700 Subject: [PATCH 1/3] Windows: restore main window after Pick location (#3) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BeginPick minimizes the owner so the user can pick coordinates beneath the app. Window_StateChanged turns that minimize into Hide(), which leaves the window with Visibility.Hidden. The pick callbacks only set WindowState = Normal, which doesn't unhide the window — Activate() on a hidden window is a no-op too. Result: coordinates were captured but the app appeared to vanish. Fix: call Show() in OnLocationPicked and OnPickCancelled, matching what RestoreFromTray already does. Fixes #3 Co-Authored-By: Claude Opus 4.7 (1M context) --- windows/MainWindow.xaml.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/windows/MainWindow.xaml.cs b/windows/MainWindow.xaml.cs index 299575f..d413c11 100644 --- a/windows/MainWindow.xaml.cs +++ b/windows/MainWindow.xaml.cs @@ -245,12 +245,15 @@ private void OnLocationPicked(int x, int y) { XBox.Text = x.ToString(); YBox.Text = y.ToString(); + // Window_StateChanged hid the window when BeginPick minimized it; Show() is needed to undo that. + Show(); WindowState = WindowState.Normal; Activate(); } private void OnPickCancelled() { + Show(); WindowState = WindowState.Normal; Activate(); } From c10a9e86f7acae16ec9b25d85b4a1d3d589d1a82 Mon Sep 17 00:00:00 2001 From: Quadstronaut <2138096+Quadstronaut@users.noreply.github.com> Date: Sat, 25 Apr 2026 23:59:30 -0700 Subject: [PATCH 2/3] Windows: fix WH_MOUSE_LL hMod and add picker diagnostics (#3) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The pick overlay was showing but clicks were never captured. Symptom: overlay's "Click to select location | ESC to cancel" hint stays on screen, X/Y boxes are never populated, and the previous OnLocationPicked restore code never runs (which is why the earlier Show() fix alone did not help — the callback was never reaching it). Root cause (likely): SetHook passed GetModuleHandle(MainModule.ModuleName) — i.e. GetModuleHandle("QuadClicker.exe"). On .NET 10 apphost / single-file deployments the loaded EXE module can register under a name that does not match Process.MainModule.ModuleName, so the lookup silently returns 0. SetWindowsHookEx then refuses to install with hMod=0, returns IntPtr.Zero, and the hook never fires. Fix: pass NULL for hMod (the Win32-documented form: Windows substitutes the EXE's HMODULE). Required widening NativeMethods.GetModuleHandle to accept string?. Also adds file-based diagnostics at %APPDATA%\QuadClicker\picker.log covering the BeginPick → ShowOverlay → SetHook → HookCallback → dispatch chain, plus a visible red error in the overlay when SetHook fails — so if the picker still misbehaves we have evidence instead of guessing. Refs #3 Co-Authored-By: Claude Opus 4.7 (1M context) --- windows/Core/LocationPicker.cs | 77 ++++++++++++++++++++++++-------- windows/PInvoke/NativeMethods.cs | 2 +- 2 files changed, 59 insertions(+), 20 deletions(-) diff --git a/windows/Core/LocationPicker.cs b/windows/Core/LocationPicker.cs index b177fba..fe3cfb6 100644 --- a/windows/Core/LocationPicker.cs +++ b/windows/Core/LocationPicker.cs @@ -1,4 +1,6 @@ using QuadClicker.PInvoke; +using System.IO; +using System.Runtime.InteropServices; using System.Windows; using System.Windows.Controls; using System.Windows.Input; @@ -35,6 +37,7 @@ internal void BeginPick(Window owner) _pickCts = new CancellationTokenSource(); var cts = _pickCts; + Log("BeginPick: minimizing owner"); owner.WindowState = WindowState.Minimized; Application.Current.Dispatcher.BeginInvoke(async () => @@ -44,12 +47,27 @@ internal void BeginPick(Window owner) await Task.Delay(300, cts.Token); ShowOverlay(owner); } - catch (OperationCanceledException) { } + catch (OperationCanceledException) { Log("BeginPick: cancelled during delay"); } + catch (Exception ex) { Log($"BeginPick: exception {ex.GetType().Name}: {ex.Message}"); } }); } private void ShowOverlay(Window owner) { + Log("ShowOverlay: creating overlay window"); + var hintLabel = new TextBlock + { + Text = "Click to select location | ESC to cancel", + Foreground = Brushes.White, + Background = new SolidColorBrush(Color.FromArgb(200, 20, 20, 20)), + FontSize = 14, + FontFamily = new FontFamily("Segoe UI"), + Padding = new Thickness(14, 8, 14, 8), + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Top, + Margin = new Thickness(0, 40, 0, 0) + }; + _overlay = new Window { Background = new SolidColorBrush(Color.FromArgb(1, 0, 0, 0)), @@ -59,18 +77,7 @@ private void ShowOverlay(Window owner) Topmost = true, Cursor = Cursors.Cross, ShowInTaskbar = false, - Content = new TextBlock - { - Text = "Click to select location | ESC to cancel", - Foreground = Brushes.White, - Background = new SolidColorBrush(Color.FromArgb(200, 20, 20, 20)), - FontSize = 14, - FontFamily = new FontFamily("Segoe UI"), - Padding = new Thickness(14, 8, 14, 8), - HorizontalAlignment = HorizontalAlignment.Center, - VerticalAlignment = VerticalAlignment.Top, - Margin = new Thickness(0, 40, 0, 0) - } + Content = hintLabel }; _overlay.KeyDown += (_, e) => @@ -79,9 +86,18 @@ private void ShowOverlay(Window owner) }; _overlay.Show(); + Log("ShowOverlay: overlay shown"); _proc = HookCallback; _hookId = SetHook(_proc); + int err = Marshal.GetLastWin32Error(); + Log($"ShowOverlay: SetHook returned 0x{_hookId.ToInt64():X}, GetLastError={err}"); + + if (_hookId == IntPtr.Zero) + { + hintLabel.Text = "Hook install failed — see %APPDATA%\\QuadClicker\\picker.log. ESC to cancel."; + hintLabel.Foreground = Brushes.Red; + } } private void CancelPick(Window owner) @@ -94,16 +110,20 @@ private IntPtr HookCallback(int nCode, IntPtr wParam, IntPtr lParam) { if (nCode >= 0 && wParam == (IntPtr)NativeMethods.WM_LBUTTONUP) { + Log($"HookCallback: WM_LBUTTONUP captured"); + // Unhook first — prevents re-entry var hookId = _hookId; _hookId = IntPtr.Zero; NativeMethods.UnhookWindowsHookEx(hookId); NativeMethods.GetCursorPos(out var p); + Log($"HookCallback: cursor at ({p.X}, {p.Y}), dispatching"); // BeginInvoke (async) — never block inside a low-level hook callback Application.Current.Dispatcher.BeginInvoke(() => { + Log("HookCallback: dispatcher action running, closing overlay and raising LocationPicked"); _overlay?.Close(); _overlay = null; LocationPicked?.Invoke(p.X, p.Y); @@ -132,12 +152,31 @@ private void Cleanup(Window owner) private static IntPtr SetHook(NativeMethods.LowLevelMouseProc proc) { - using var process = System.Diagnostics.Process.GetCurrentProcess(); - var module = process.MainModule; - return module is not null - ? NativeMethods.SetWindowsHookEx(NativeMethods.WH_MOUSE_LL, proc, - NativeMethods.GetModuleHandle(module.ModuleName), 0) - : IntPtr.Zero; + // Pass NULL for hMod — Win32 returns the EXE's HMODULE. This is the + // documented-safe form for WH_MOUSE_LL and avoids name-lookup edge + // cases on apphost / single-file deployments where the previous + // GetModuleHandle("QuadClicker.exe") call could silently return 0. + return NativeMethods.SetWindowsHookEx(NativeMethods.WH_MOUSE_LL, proc, + NativeMethods.GetModuleHandle(null), 0); + } + + // ── Diagnostics ─────────────────────────────────────────────────────────── + // Writes to %APPDATA%\QuadClicker\picker.log so we can see what happens + // across the BeginPick → ShowOverlay → SetHook → HookCallback → dispatch + // chain when picking misbehaves on a user's machine. + private static readonly string LogPath = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), + "QuadClicker", "picker.log"); + + private static void Log(string msg) + { + try + { + Directory.CreateDirectory(Path.GetDirectoryName(LogPath)!); + File.AppendAllText(LogPath, + $"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] {msg}{Environment.NewLine}"); + } + catch { /* never let diagnostics break the picker */ } } public void Dispose() diff --git a/windows/PInvoke/NativeMethods.cs b/windows/PInvoke/NativeMethods.cs index dc3e570..dafcd52 100644 --- a/windows/PInvoke/NativeMethods.cs +++ b/windows/PInvoke/NativeMethods.cs @@ -41,7 +41,7 @@ internal static partial class NativeMethods internal static partial IntPtr CallNextHookEx(IntPtr hhk, int nCode, IntPtr wParam, IntPtr lParam); [LibraryImport("kernel32.dll", SetLastError = true, StringMarshalling = StringMarshalling.Utf16)] - internal static partial IntPtr GetModuleHandle(string lpModuleName); + internal static partial IntPtr GetModuleHandle(string? lpModuleName); // ── Hotkeys ─────────────────────────────────────────────────────────────── [LibraryImport("user32.dll")] From ae79f48304a1ac2b2f29817596886754ad51bfa7 Mon Sep 17 00:00:00 2001 From: Quadstronaut <2138096+Quadstronaut@users.noreply.github.com> Date: Sun, 26 Apr 2026 00:12:42 -0700 Subject: [PATCH 3/3] Windows: replace broken GetModuleHandle P/Invoke with Marshal.GetHINSTANCE (#3) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Diagnostic log captured the actual root cause: EntryPointNotFoundException: Unable to find an entry point named 'GetModuleHandle' in DLL 'kernel32.dll'. kernel32.dll only exports GetModuleHandleA / GetModuleHandleW. The [DllImport(CharSet=Auto)] convention auto-appended the W suffix, but the [LibraryImport] source generator does not — even with StringMarshalling.Utf16 you must set EntryPoint explicitly. Calling NativeMethods.GetModuleHandle(...) therefore threw at runtime, SetHook blew up before installing the WH_MOUSE_LL hook (after the overlay had already been shown), and clicks went nowhere. Fix: drop the GetModuleHandle P/Invoke entirely and use Marshal.GetHINSTANCE(typeof(LocationPicker).Module) for hMod. WH_MOUSE_LL hooks run in the calling thread regardless of hMod, so any valid module handle works — and this avoids the marshaler edge case entirely. The diagnostics added in c10a9e8 stay; they are what surfaced this within one user-test cycle. Refs #3 Co-Authored-By: Claude Opus 4.7 (1M context) --- windows/Core/LocationPicker.cs | 11 +++++------ windows/PInvoke/NativeMethods.cs | 3 --- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/windows/Core/LocationPicker.cs b/windows/Core/LocationPicker.cs index fe3cfb6..2c8fd7d 100644 --- a/windows/Core/LocationPicker.cs +++ b/windows/Core/LocationPicker.cs @@ -152,12 +152,11 @@ private void Cleanup(Window owner) private static IntPtr SetHook(NativeMethods.LowLevelMouseProc proc) { - // Pass NULL for hMod — Win32 returns the EXE's HMODULE. This is the - // documented-safe form for WH_MOUSE_LL and avoids name-lookup edge - // cases on apphost / single-file deployments where the previous - // GetModuleHandle("QuadClicker.exe") call could silently return 0. - return NativeMethods.SetWindowsHookEx(NativeMethods.WH_MOUSE_LL, proc, - NativeMethods.GetModuleHandle(null), 0); + // Use the assembly's HINSTANCE directly. Avoids GetModuleHandle, whose + // [LibraryImport] form doesn't auto-resolve the A/W suffix and would + // throw EntryPointNotFoundException at runtime. + var hMod = Marshal.GetHINSTANCE(typeof(LocationPicker).Module); + return NativeMethods.SetWindowsHookEx(NativeMethods.WH_MOUSE_LL, proc, hMod, 0); } // ── Diagnostics ─────────────────────────────────────────────────────────── diff --git a/windows/PInvoke/NativeMethods.cs b/windows/PInvoke/NativeMethods.cs index dafcd52..98170b6 100644 --- a/windows/PInvoke/NativeMethods.cs +++ b/windows/PInvoke/NativeMethods.cs @@ -40,9 +40,6 @@ internal static partial class NativeMethods [LibraryImport("user32.dll", SetLastError = true)] internal static partial IntPtr CallNextHookEx(IntPtr hhk, int nCode, IntPtr wParam, IntPtr lParam); - [LibraryImport("kernel32.dll", SetLastError = true, StringMarshalling = StringMarshalling.Utf16)] - internal static partial IntPtr GetModuleHandle(string? lpModuleName); - // ── Hotkeys ─────────────────────────────────────────────────────────────── [LibraryImport("user32.dll")] [return: MarshalAs(UnmanagedType.Bool)]