-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathMainWindow.Scroll.cs
More file actions
134 lines (114 loc) · 5.36 KB
/
Copy pathMainWindow.Scroll.cs
File metadata and controls
134 lines (114 loc) · 5.36 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
using System.Windows;
using System.Windows.Input;
namespace ScreenSearchOverlay;
// Scroll state machine — see header comment in BeginScrollSession.
public partial class MainWindow : Window
{
private const int ScrollDebounceMs = 400;
private const int ScrollSettleMs = 50;
// ─── Scroll state machine ─────────────────────────────────────────────
// Idle: content visible, click-through OFF, search bar interactive
// Scrolling: content hidden, click-through ON (WS_EX_TRANSPARENT), the
// global mouse hook keeps the debounce alive.
//
// Why this design: Chromium/Electron ignore synthetic WM_MOUSEWHEEL. The
// only way to scroll them is real OS-level wheel input. By going
// click-through during a scroll session, the user's physical wheels go
// natively to the app underneath. We can't listen via WPF anymore (we
// don't receive events while click-through), so a global low-level hook
// (WH_MOUSE_LL) keeps the debounce alive.
private void Window_PreviewMouseWheel(object sender, MouseWheelEventArgs e)
{
// Only entered when click-through is OFF — start of a session.
if (!_scrollSessionActive)
BeginScrollSession(e.Delta);
e.Handled = true;
}
private void OnGlobalWheelDetected()
{
// Hook fires for every wheel system-wide. Only react when the session
// is active AND the cursor is over our window (multi-monitor sanity).
if (!_scrollSessionActive) return;
if (!IsCursorOverThisWindow()) return;
_wheelGeneration++;
try { _scrollDebounce.Change(ScrollDebounceMs, System.Threading.Timeout.Infinite); }
catch (Exception ex) { Log($"hook timer-change EX: {ex}"); }
}
private void BeginScrollSession(int firstDelta)
{
Log($"session BEGIN delta={firstDelta}");
_scrollSessionActive = true;
_wheelGeneration++;
OverlayContent.Visibility = Visibility.Hidden;
// Click-through MUST be set BEFORE injecting, so the injected wheel
// hits the app below (not us → no self-loop).
SetClickThrough(true);
try { InjectMouseWheel(firstDelta); }
catch (Exception ex) { Log($"InjectMouseWheel EX: {ex}"); }
try { _scrollDebounce.Change(ScrollDebounceMs, System.Threading.Timeout.Infinite); }
catch (Exception ex) { Log($"timer-change EX: {ex}"); }
}
private async void OnScrollIdle()
{
if (!_scrollSessionActive) return;
var myGen = _wheelGeneration;
Log($"idle enter gen={myGen}");
try
{
// Let smooth-scroll animations settle
await Task.Delay(ScrollSettleMs);
if (_wheelGeneration != myGen) { Log("idle: aborted (settle)"); return; }
// CaptureScreen runs on a background thread so the UI thread stays
// responsive. The pixel push into the WriteableBitmap MUST happen
// on the UI thread (WriteableBitmap has thread affinity), but it's
// a single memcpy — sub-5 ms even at 4K.
var sx = _screenInfo.X;
var sy = _screenInfo.Y;
var sw = _screenInfo.Width;
var sh = _screenInfo.Height;
var newBmp = await Task.Run(() => CaptureScreen(sx, sy, sw, sh));
if (_wheelGeneration != myGen)
{
// User started scrolling again — throw away the work
newBmp.Dispose();
Log("idle: aborted (capture)");
return;
}
// Atomic swap on UI thread + dispose the previous frame to keep
// GDI handle / native memory usage flat across many sessions.
HighlightCanvas.Children.Clear();
var oldBmp = _screenshot;
_screenshot = newBmp;
UpdateScreenshotBitmap(newBmp);
oldBmp?.Dispose();
// ── Transition Scrolling → Idle ──
SetClickThrough(false);
OverlayContent.Visibility = Visibility.Visible;
_scrollSessionActive = false;
Log("session END");
// Cache check: if the captured pixels hash to the same value as
// the previous frame, the OCR result is still valid — skip the
// expensive OCR pass and just re-apply existing highlights.
var newHash = ComputeHash(newBmp);
if (newHash == _lastScreenshotHash && _ocrWords.Count > 0)
{
Log("OCR cache HIT");
SearchBox_TextChanged(SearchBox, null!);
return;
}
_lastScreenshotHash = newHash;
// OCR is expensive (enhance + convert + recognize). Runs entirely
// off the UI thread; we only touch UI again to apply highlights.
await RunOcrAsync(newBmp);
if (_wheelGeneration != myGen) { Log("OCR: aborted (new session)"); return; }
SearchBox_TextChanged(SearchBox, null!);
}
catch (Exception ex)
{
Log($"OnScrollIdle EX: {ex}");
// Safety net: never strand the user with a click-through invisible window
try { SetClickThrough(false); OverlayContent.Visibility = Visibility.Visible; } catch { }
_scrollSessionActive = false;
}
}
}