Skip to content

Commit 3aba242

Browse files
Quadstronautclaude
andauthored
Windows: restore main window after Pick location (fixes #3) (#4)
* Windows: restore main window after Pick location (#3) 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) <noreply@anthropic.com> * Windows: fix WH_MOUSE_LL hMod and add picker diagnostics (#3) 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) <noreply@anthropic.com> * Windows: replace broken GetModuleHandle P/Invoke with Marshal.GetHINSTANCE (#3) 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) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent dec9f7c commit 3aba242

3 files changed

Lines changed: 60 additions & 22 deletions

File tree

windows/Core/LocationPicker.cs

Lines changed: 57 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
using QuadClicker.PInvoke;
2+
using System.IO;
3+
using System.Runtime.InteropServices;
24
using System.Windows;
35
using System.Windows.Controls;
46
using System.Windows.Input;
@@ -35,6 +37,7 @@ internal void BeginPick(Window owner)
3537
_pickCts = new CancellationTokenSource();
3638
var cts = _pickCts;
3739

40+
Log("BeginPick: minimizing owner");
3841
owner.WindowState = WindowState.Minimized;
3942

4043
Application.Current.Dispatcher.BeginInvoke(async () =>
@@ -44,12 +47,27 @@ internal void BeginPick(Window owner)
4447
await Task.Delay(300, cts.Token);
4548
ShowOverlay(owner);
4649
}
47-
catch (OperationCanceledException) { }
50+
catch (OperationCanceledException) { Log("BeginPick: cancelled during delay"); }
51+
catch (Exception ex) { Log($"BeginPick: exception {ex.GetType().Name}: {ex.Message}"); }
4852
});
4953
}
5054

5155
private void ShowOverlay(Window owner)
5256
{
57+
Log("ShowOverlay: creating overlay window");
58+
var hintLabel = new TextBlock
59+
{
60+
Text = "Click to select location | ESC to cancel",
61+
Foreground = Brushes.White,
62+
Background = new SolidColorBrush(Color.FromArgb(200, 20, 20, 20)),
63+
FontSize = 14,
64+
FontFamily = new FontFamily("Segoe UI"),
65+
Padding = new Thickness(14, 8, 14, 8),
66+
HorizontalAlignment = HorizontalAlignment.Center,
67+
VerticalAlignment = VerticalAlignment.Top,
68+
Margin = new Thickness(0, 40, 0, 0)
69+
};
70+
5371
_overlay = new Window
5472
{
5573
Background = new SolidColorBrush(Color.FromArgb(1, 0, 0, 0)),
@@ -59,18 +77,7 @@ private void ShowOverlay(Window owner)
5977
Topmost = true,
6078
Cursor = Cursors.Cross,
6179
ShowInTaskbar = false,
62-
Content = new TextBlock
63-
{
64-
Text = "Click to select location | ESC to cancel",
65-
Foreground = Brushes.White,
66-
Background = new SolidColorBrush(Color.FromArgb(200, 20, 20, 20)),
67-
FontSize = 14,
68-
FontFamily = new FontFamily("Segoe UI"),
69-
Padding = new Thickness(14, 8, 14, 8),
70-
HorizontalAlignment = HorizontalAlignment.Center,
71-
VerticalAlignment = VerticalAlignment.Top,
72-
Margin = new Thickness(0, 40, 0, 0)
73-
}
80+
Content = hintLabel
7481
};
7582

7683
_overlay.KeyDown += (_, e) =>
@@ -79,9 +86,18 @@ private void ShowOverlay(Window owner)
7986
};
8087

8188
_overlay.Show();
89+
Log("ShowOverlay: overlay shown");
8290

8391
_proc = HookCallback;
8492
_hookId = SetHook(_proc);
93+
int err = Marshal.GetLastWin32Error();
94+
Log($"ShowOverlay: SetHook returned 0x{_hookId.ToInt64():X}, GetLastError={err}");
95+
96+
if (_hookId == IntPtr.Zero)
97+
{
98+
hintLabel.Text = "Hook install failed — see %APPDATA%\\QuadClicker\\picker.log. ESC to cancel.";
99+
hintLabel.Foreground = Brushes.Red;
100+
}
85101
}
86102

87103
private void CancelPick(Window owner)
@@ -94,16 +110,20 @@ private IntPtr HookCallback(int nCode, IntPtr wParam, IntPtr lParam)
94110
{
95111
if (nCode >= 0 && wParam == (IntPtr)NativeMethods.WM_LBUTTONUP)
96112
{
113+
Log($"HookCallback: WM_LBUTTONUP captured");
114+
97115
// Unhook first — prevents re-entry
98116
var hookId = _hookId;
99117
_hookId = IntPtr.Zero;
100118
NativeMethods.UnhookWindowsHookEx(hookId);
101119

102120
NativeMethods.GetCursorPos(out var p);
121+
Log($"HookCallback: cursor at ({p.X}, {p.Y}), dispatching");
103122

104123
// BeginInvoke (async) — never block inside a low-level hook callback
105124
Application.Current.Dispatcher.BeginInvoke(() =>
106125
{
126+
Log("HookCallback: dispatcher action running, closing overlay and raising LocationPicked");
107127
_overlay?.Close();
108128
_overlay = null;
109129
LocationPicked?.Invoke(p.X, p.Y);
@@ -132,12 +152,30 @@ private void Cleanup(Window owner)
132152

133153
private static IntPtr SetHook(NativeMethods.LowLevelMouseProc proc)
134154
{
135-
using var process = System.Diagnostics.Process.GetCurrentProcess();
136-
var module = process.MainModule;
137-
return module is not null
138-
? NativeMethods.SetWindowsHookEx(NativeMethods.WH_MOUSE_LL, proc,
139-
NativeMethods.GetModuleHandle(module.ModuleName), 0)
140-
: IntPtr.Zero;
155+
// Use the assembly's HINSTANCE directly. Avoids GetModuleHandle, whose
156+
// [LibraryImport] form doesn't auto-resolve the A/W suffix and would
157+
// throw EntryPointNotFoundException at runtime.
158+
var hMod = Marshal.GetHINSTANCE(typeof(LocationPicker).Module);
159+
return NativeMethods.SetWindowsHookEx(NativeMethods.WH_MOUSE_LL, proc, hMod, 0);
160+
}
161+
162+
// ── Diagnostics ───────────────────────────────────────────────────────────
163+
// Writes to %APPDATA%\QuadClicker\picker.log so we can see what happens
164+
// across the BeginPick → ShowOverlay → SetHook → HookCallback → dispatch
165+
// chain when picking misbehaves on a user's machine.
166+
private static readonly string LogPath = Path.Combine(
167+
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
168+
"QuadClicker", "picker.log");
169+
170+
private static void Log(string msg)
171+
{
172+
try
173+
{
174+
Directory.CreateDirectory(Path.GetDirectoryName(LogPath)!);
175+
File.AppendAllText(LogPath,
176+
$"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] {msg}{Environment.NewLine}");
177+
}
178+
catch { /* never let diagnostics break the picker */ }
141179
}
142180

143181
public void Dispose()

windows/MainWindow.xaml.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,12 +245,15 @@ private void OnLocationPicked(int x, int y)
245245
{
246246
XBox.Text = x.ToString();
247247
YBox.Text = y.ToString();
248+
// Window_StateChanged hid the window when BeginPick minimized it; Show() is needed to undo that.
249+
Show();
248250
WindowState = WindowState.Normal;
249251
Activate();
250252
}
251253

252254
private void OnPickCancelled()
253255
{
256+
Show();
254257
WindowState = WindowState.Normal;
255258
Activate();
256259
}

windows/PInvoke/NativeMethods.cs

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,6 @@ internal static partial class NativeMethods
4040
[LibraryImport("user32.dll", SetLastError = true)]
4141
internal static partial IntPtr CallNextHookEx(IntPtr hhk, int nCode, IntPtr wParam, IntPtr lParam);
4242

43-
[LibraryImport("kernel32.dll", SetLastError = true, StringMarshalling = StringMarshalling.Utf16)]
44-
internal static partial IntPtr GetModuleHandle(string lpModuleName);
45-
4643
// ── Hotkeys ───────────────────────────────────────────────────────────────
4744
[LibraryImport("user32.dll")]
4845
[return: MarshalAs(UnmanagedType.Bool)]

0 commit comments

Comments
 (0)