diff --git a/windows/Core/LocationPicker.cs b/windows/Core/LocationPicker.cs index b177fba..2c8fd7d 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,30 @@ 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; + // 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 ─────────────────────────────────────────────────────────── + // 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/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(); } diff --git a/windows/PInvoke/NativeMethods.cs b/windows/PInvoke/NativeMethods.cs index dc3e570..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)]