copilot-liku-cli 0.0.4 → 0.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/QUICKSTART.md +24 -0
- package/README.md +85 -33
- package/package.json +23 -14
- package/scripts/postinstall.js +63 -0
- package/src/cli/commands/window.js +66 -0
- package/src/main/agents/base-agent.js +15 -7
- package/src/main/agents/builder.js +211 -0
- package/src/main/agents/index.js +7 -4
- package/src/main/agents/orchestrator.js +13 -0
- package/src/main/agents/producer.js +891 -0
- package/src/main/agents/researcher.js +78 -0
- package/src/main/agents/state-manager.js +134 -2
- package/src/main/agents/verifier.js +201 -0
- package/src/main/ai-service.js +349 -35
- package/src/main/index.js +680 -110
- package/src/main/inspect-service.js +24 -1
- package/src/main/python-bridge.js +395 -0
- package/src/main/system-automation.js +849 -131
- package/src/main/ui-automation/core/ui-provider.js +99 -0
- package/src/main/ui-automation/core/uia-host.js +214 -0
- package/src/main/ui-automation/index.js +30 -0
- package/src/main/ui-automation/interactions/element-click.js +6 -6
- package/src/main/ui-automation/interactions/high-level.js +28 -6
- package/src/main/ui-automation/interactions/index.js +21 -0
- package/src/main/ui-automation/interactions/pattern-actions.js +236 -0
- package/src/main/ui-automation/window/index.js +6 -0
- package/src/main/ui-automation/window/manager.js +173 -26
- package/src/main/ui-watcher.js +401 -58
- package/src/main/visual-awareness.js +18 -1
- package/src/native/windows-uia/Program.cs +89 -0
- package/src/native/windows-uia/build.ps1 +24 -0
- package/src/native/windows-uia-dotnet/Program.cs +920 -0
- package/src/native/windows-uia-dotnet/WindowsUIA.csproj +11 -0
- package/src/native/windows-uia-dotnet/build.ps1 +24 -0
- package/src/renderer/chat/chat.js +915 -671
- package/src/renderer/chat/index.html +2 -4
- package/src/renderer/chat/preload.js +8 -1
- package/src/renderer/overlay/overlay.js +157 -8
- package/src/renderer/overlay/preload.js +4 -0
- package/src/shared/inspect-types.js +82 -6
- package/ARCHITECTURE.md +0 -411
- package/CONFIGURATION.md +0 -302
- package/CONTRIBUTING.md +0 -225
- package/ELECTRON_README.md +0 -121
- package/PROJECT_STATUS.md +0 -229
- package/TESTING.md +0 -274
|
@@ -0,0 +1,920 @@
|
|
|
1
|
+
using System;
|
|
2
|
+
using System.Collections.Generic;
|
|
3
|
+
using System.IO;
|
|
4
|
+
using System.Linq;
|
|
5
|
+
using System.Runtime.InteropServices;
|
|
6
|
+
using System.Text.Json;
|
|
7
|
+
using System.Threading;
|
|
8
|
+
using System.Timers;
|
|
9
|
+
using System.Windows;
|
|
10
|
+
using System.Windows.Automation;
|
|
11
|
+
|
|
12
|
+
namespace UIAWrapper
|
|
13
|
+
{
|
|
14
|
+
class Program
|
|
15
|
+
{
|
|
16
|
+
[DllImport("user32.dll")]
|
|
17
|
+
static extern IntPtr GetForegroundWindow();
|
|
18
|
+
|
|
19
|
+
static readonly JsonSerializerOptions JsonOpts = new() { WriteIndented = false };
|
|
20
|
+
|
|
21
|
+
// ── Thread-safe output (Phase 4) ─────────────────────────────────────
|
|
22
|
+
static readonly object _writeLock = new object();
|
|
23
|
+
|
|
24
|
+
// ── Event subscription state (Phase 4) ──────────────────────────────
|
|
25
|
+
static bool _eventsSubscribed = false;
|
|
26
|
+
static AutomationElement? _subscribedWindow = null;
|
|
27
|
+
static int _subscribedWindowHandle = 0;
|
|
28
|
+
static readonly int MaxWalkElements = 300;
|
|
29
|
+
|
|
30
|
+
// Debounce timers
|
|
31
|
+
static System.Timers.Timer? _structureDebounce = null;
|
|
32
|
+
static System.Timers.Timer? _propertyDebounce = null;
|
|
33
|
+
static readonly List<Dictionary<string, object?>> _pendingPropertyChanges = new();
|
|
34
|
+
static readonly object _propLock = new object();
|
|
35
|
+
|
|
36
|
+
// Adaptive backoff: if >10 structure events in 1s, increase debounce
|
|
37
|
+
static int _structureEventBurst = 0;
|
|
38
|
+
static DateTime _structureBurstWindowStart = DateTime.UtcNow;
|
|
39
|
+
static int _structureDebounceMs = 100;
|
|
40
|
+
|
|
41
|
+
// Event handler references (for removal)
|
|
42
|
+
static AutomationFocusChangedEventHandler? _focusHandler = null;
|
|
43
|
+
static StructureChangedEventHandler? _structureHandler = null;
|
|
44
|
+
static AutomationPropertyChangedEventHandler? _propertyHandler = null;
|
|
45
|
+
|
|
46
|
+
static void Main(string[] args)
|
|
47
|
+
{
|
|
48
|
+
// Legacy one-shot mode: no args → dump foreground tree and exit
|
|
49
|
+
if (!Console.IsInputRedirected && args.Length == 0)
|
|
50
|
+
{
|
|
51
|
+
IntPtr handle = GetForegroundWindow();
|
|
52
|
+
if (handle == IntPtr.Zero) return;
|
|
53
|
+
AutomationElement root = AutomationElement.FromHandle(handle);
|
|
54
|
+
var node = BuildTree(root);
|
|
55
|
+
Console.WriteLine(JsonSerializer.Serialize(node, new JsonSerializerOptions { WriteIndented = true }));
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Persistent command-loop mode (JSONL over stdin/stdout)
|
|
60
|
+
string? line;
|
|
61
|
+
while ((line = Console.ReadLine()) != null)
|
|
62
|
+
{
|
|
63
|
+
if (string.IsNullOrWhiteSpace(line)) continue;
|
|
64
|
+
try
|
|
65
|
+
{
|
|
66
|
+
using var doc = JsonDocument.Parse(line);
|
|
67
|
+
var root = doc.RootElement;
|
|
68
|
+
var cmd = root.GetProperty("cmd").GetString() ?? "";
|
|
69
|
+
|
|
70
|
+
switch (cmd)
|
|
71
|
+
{
|
|
72
|
+
case "getTree":
|
|
73
|
+
HandleGetTree();
|
|
74
|
+
break;
|
|
75
|
+
case "elementFromPoint":
|
|
76
|
+
HandleElementFromPoint(root);
|
|
77
|
+
break;
|
|
78
|
+
case "setValue":
|
|
79
|
+
HandleSetValue(root);
|
|
80
|
+
break;
|
|
81
|
+
case "scroll":
|
|
82
|
+
HandleScroll(root);
|
|
83
|
+
break;
|
|
84
|
+
case "expandCollapse":
|
|
85
|
+
HandleExpandCollapse(root);
|
|
86
|
+
break;
|
|
87
|
+
case "getText":
|
|
88
|
+
HandleGetText(root);
|
|
89
|
+
break;
|
|
90
|
+
case "subscribeEvents":
|
|
91
|
+
HandleSubscribeEvents();
|
|
92
|
+
break;
|
|
93
|
+
case "unsubscribeEvents":
|
|
94
|
+
HandleUnsubscribeEvents();
|
|
95
|
+
break;
|
|
96
|
+
case "exit":
|
|
97
|
+
Reply(new { ok = true, cmd = "exit" });
|
|
98
|
+
return;
|
|
99
|
+
default:
|
|
100
|
+
Reply(new { ok = false, error = $"Unknown command: {cmd}" });
|
|
101
|
+
break;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
catch (Exception ex)
|
|
105
|
+
{
|
|
106
|
+
Reply(new { ok = false, error = ex.Message });
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
static void Reply(object obj)
|
|
112
|
+
{
|
|
113
|
+
lock (_writeLock)
|
|
114
|
+
{
|
|
115
|
+
Console.WriteLine(JsonSerializer.Serialize(obj, JsonOpts));
|
|
116
|
+
Console.Out.Flush();
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ── getTree ──────────────────────────────────────────────────────────
|
|
121
|
+
static void HandleGetTree()
|
|
122
|
+
{
|
|
123
|
+
IntPtr handle = GetForegroundWindow();
|
|
124
|
+
if (handle == IntPtr.Zero)
|
|
125
|
+
{
|
|
126
|
+
Reply(new { ok = false, error = "No foreground window" });
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
AutomationElement root = AutomationElement.FromHandle(handle);
|
|
130
|
+
var node = BuildTree(root);
|
|
131
|
+
Reply(new { ok = true, cmd = "getTree", tree = node });
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// ── elementFromPoint ─────────────────────────────────────────────────
|
|
135
|
+
static void HandleElementFromPoint(JsonElement root)
|
|
136
|
+
{
|
|
137
|
+
double x = root.GetProperty("x").GetDouble();
|
|
138
|
+
double y = root.GetProperty("y").GetDouble();
|
|
139
|
+
|
|
140
|
+
AutomationElement element;
|
|
141
|
+
try
|
|
142
|
+
{
|
|
143
|
+
element = AutomationElement.FromPoint(new Point(x, y));
|
|
144
|
+
}
|
|
145
|
+
catch (Exception ex)
|
|
146
|
+
{
|
|
147
|
+
Reply(new { ok = false, error = $"FromPoint failed: {ex.Message}" });
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (element == null)
|
|
152
|
+
{
|
|
153
|
+
Reply(new { ok = false, error = "No element at point" });
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
var payload = BuildRichElement(element);
|
|
158
|
+
payload["queryPoint"] = new Dictionary<string, double> { ["x"] = x, ["y"] = y };
|
|
159
|
+
Reply(new { ok = true, cmd = "elementFromPoint", element = payload });
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// ── Helper: resolve element at x,y ───────────────────────────────────
|
|
163
|
+
static AutomationElement? ResolveElement(JsonElement root, out double x, out double y)
|
|
164
|
+
{
|
|
165
|
+
x = root.GetProperty("x").GetDouble();
|
|
166
|
+
y = root.GetProperty("y").GetDouble();
|
|
167
|
+
return AutomationElement.FromPoint(new Point(x, y));
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// ── setValue (Phase 3) ───────────────────────────────────────────────
|
|
171
|
+
static void HandleSetValue(JsonElement root)
|
|
172
|
+
{
|
|
173
|
+
try
|
|
174
|
+
{
|
|
175
|
+
var el = ResolveElement(root, out double x, out double y);
|
|
176
|
+
if (el == null) { Reply(new { ok = false, cmd = "setValue", error = "No element at point" }); return; }
|
|
177
|
+
|
|
178
|
+
string value = root.GetProperty("value").GetString() ?? "";
|
|
179
|
+
|
|
180
|
+
if ((bool)el.GetCurrentPropertyValue(AutomationElement.IsValuePatternAvailableProperty))
|
|
181
|
+
{
|
|
182
|
+
var vp = (ValuePattern)el.GetCurrentPattern(ValuePattern.Pattern);
|
|
183
|
+
vp.SetValue(value);
|
|
184
|
+
Reply(new { ok = true, cmd = "setValue", method = "ValuePattern", element = BuildRichElement(el) });
|
|
185
|
+
}
|
|
186
|
+
else
|
|
187
|
+
{
|
|
188
|
+
Reply(new { ok = false, cmd = "setValue", error = "ValuePattern not supported", patterns = GetPatternNames(el) });
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
catch (Exception ex) { Reply(new { ok = false, cmd = "setValue", error = ex.Message }); }
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// ── scroll (Phase 3) ─────────────────────────────────────────────────
|
|
195
|
+
static void HandleScroll(JsonElement root)
|
|
196
|
+
{
|
|
197
|
+
try
|
|
198
|
+
{
|
|
199
|
+
var el = ResolveElement(root, out double x, out double y);
|
|
200
|
+
if (el == null) { Reply(new { ok = false, cmd = "scroll", error = "No element at point" }); return; }
|
|
201
|
+
|
|
202
|
+
string direction = root.TryGetProperty("direction", out var dirProp) ? dirProp.GetString() ?? "down" : "down";
|
|
203
|
+
double amount = root.TryGetProperty("amount", out var amtProp) ? amtProp.GetDouble() : -1;
|
|
204
|
+
|
|
205
|
+
if (!(bool)el.GetCurrentPropertyValue(AutomationElement.IsScrollPatternAvailableProperty))
|
|
206
|
+
{
|
|
207
|
+
Reply(new { ok = false, cmd = "scroll", error = "ScrollPattern not supported", patterns = GetPatternNames(el) });
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
var sp = (ScrollPattern)el.GetCurrentPattern(ScrollPattern.Pattern);
|
|
212
|
+
|
|
213
|
+
if (amount >= 0)
|
|
214
|
+
{
|
|
215
|
+
// SetScrollPercent mode
|
|
216
|
+
double hPct = sp.Current.HorizontalScrollPercent;
|
|
217
|
+
double vPct = sp.Current.VerticalScrollPercent;
|
|
218
|
+
switch (direction)
|
|
219
|
+
{
|
|
220
|
+
case "left": hPct = Math.Max(0, amount); break;
|
|
221
|
+
case "right": hPct = Math.Min(100, amount); break;
|
|
222
|
+
case "up": vPct = Math.Max(0, amount); break;
|
|
223
|
+
default: vPct = Math.Min(100, amount); break; // down
|
|
224
|
+
}
|
|
225
|
+
sp.SetScrollPercent(hPct, vPct);
|
|
226
|
+
}
|
|
227
|
+
else
|
|
228
|
+
{
|
|
229
|
+
// Scroll by amount (SmallIncrement)
|
|
230
|
+
switch (direction)
|
|
231
|
+
{
|
|
232
|
+
case "up": sp.ScrollVertical(ScrollAmount.SmallDecrement); break;
|
|
233
|
+
case "down": sp.ScrollVertical(ScrollAmount.SmallIncrement); break;
|
|
234
|
+
case "left": sp.ScrollHorizontal(ScrollAmount.SmallDecrement); break;
|
|
235
|
+
case "right": sp.ScrollHorizontal(ScrollAmount.SmallIncrement); break;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
Reply(new
|
|
240
|
+
{
|
|
241
|
+
ok = true,
|
|
242
|
+
cmd = "scroll",
|
|
243
|
+
method = "ScrollPattern",
|
|
244
|
+
direction,
|
|
245
|
+
scrollInfo = new
|
|
246
|
+
{
|
|
247
|
+
horizontalPercent = sp.Current.HorizontalScrollPercent,
|
|
248
|
+
verticalPercent = sp.Current.VerticalScrollPercent,
|
|
249
|
+
horizontalViewSize = sp.Current.HorizontalViewSize,
|
|
250
|
+
verticalViewSize = sp.Current.VerticalViewSize
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
catch (Exception ex) { Reply(new { ok = false, cmd = "scroll", error = ex.Message }); }
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// ── expandCollapse (Phase 3) ─────────────────────────────────────────
|
|
258
|
+
static void HandleExpandCollapse(JsonElement root)
|
|
259
|
+
{
|
|
260
|
+
try
|
|
261
|
+
{
|
|
262
|
+
var el = ResolveElement(root, out double x, out double y);
|
|
263
|
+
if (el == null) { Reply(new { ok = false, cmd = "expandCollapse", error = "No element at point" }); return; }
|
|
264
|
+
|
|
265
|
+
string action = root.TryGetProperty("action", out var actProp) ? actProp.GetString() ?? "toggle" : "toggle";
|
|
266
|
+
|
|
267
|
+
if (!(bool)el.GetCurrentPropertyValue(AutomationElement.IsExpandCollapsePatternAvailableProperty))
|
|
268
|
+
{
|
|
269
|
+
Reply(new { ok = false, cmd = "expandCollapse", error = "ExpandCollapsePattern not supported", patterns = GetPatternNames(el) });
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
var ecp = (ExpandCollapsePattern)el.GetCurrentPattern(ExpandCollapsePattern.Pattern);
|
|
274
|
+
var stateBefore = ecp.Current.ExpandCollapseState.ToString();
|
|
275
|
+
|
|
276
|
+
switch (action)
|
|
277
|
+
{
|
|
278
|
+
case "expand": ecp.Expand(); break;
|
|
279
|
+
case "collapse": ecp.Collapse(); break;
|
|
280
|
+
default: // toggle
|
|
281
|
+
if (ecp.Current.ExpandCollapseState == ExpandCollapseState.Collapsed)
|
|
282
|
+
ecp.Expand();
|
|
283
|
+
else
|
|
284
|
+
ecp.Collapse();
|
|
285
|
+
break;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
Reply(new
|
|
289
|
+
{
|
|
290
|
+
ok = true,
|
|
291
|
+
cmd = "expandCollapse",
|
|
292
|
+
method = "ExpandCollapsePattern",
|
|
293
|
+
action,
|
|
294
|
+
stateBefore,
|
|
295
|
+
stateAfter = ecp.Current.ExpandCollapseState.ToString()
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
catch (Exception ex) { Reply(new { ok = false, cmd = "expandCollapse", error = ex.Message }); }
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// ── getText (Phase 3) ────────────────────────────────────────────────
|
|
302
|
+
static void HandleGetText(JsonElement root)
|
|
303
|
+
{
|
|
304
|
+
try
|
|
305
|
+
{
|
|
306
|
+
var el = ResolveElement(root, out double x, out double y);
|
|
307
|
+
if (el == null) { Reply(new { ok = false, cmd = "getText", error = "No element at point" }); return; }
|
|
308
|
+
|
|
309
|
+
// Try TextPattern first
|
|
310
|
+
if ((bool)el.GetCurrentPropertyValue(AutomationElement.IsTextPatternAvailableProperty))
|
|
311
|
+
{
|
|
312
|
+
var tp = (TextPattern)el.GetCurrentPattern(TextPattern.Pattern);
|
|
313
|
+
string text = tp.DocumentRange.GetText(-1);
|
|
314
|
+
Reply(new { ok = true, cmd = "getText", method = "TextPattern", text, element = BuildRichElement(el) });
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Fallback: try ValuePattern
|
|
319
|
+
if ((bool)el.GetCurrentPropertyValue(AutomationElement.IsValuePatternAvailableProperty))
|
|
320
|
+
{
|
|
321
|
+
var vp = (ValuePattern)el.GetCurrentPattern(ValuePattern.Pattern);
|
|
322
|
+
string text = vp.Current.Value;
|
|
323
|
+
Reply(new { ok = true, cmd = "getText", method = "ValuePattern", text, element = BuildRichElement(el) });
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Fallback: Name property
|
|
328
|
+
string name = el.Current.Name;
|
|
329
|
+
if (!string.IsNullOrEmpty(name))
|
|
330
|
+
{
|
|
331
|
+
Reply(new { ok = true, cmd = "getText", method = "Name", text = name, element = BuildRichElement(el) });
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
Reply(new { ok = false, cmd = "getText", error = "No text source available", patterns = GetPatternNames(el) });
|
|
336
|
+
}
|
|
337
|
+
catch (Exception ex) { Reply(new { ok = false, cmd = "getText", error = ex.Message }); }
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// ── Helper: get pattern short names ──────────────────────────────────
|
|
341
|
+
|
|
342
|
+
// ── Phase 4: Event streaming ─────────────────────────────────────────
|
|
343
|
+
|
|
344
|
+
static void HandleSubscribeEvents()
|
|
345
|
+
{
|
|
346
|
+
if (_eventsSubscribed)
|
|
347
|
+
{
|
|
348
|
+
Reply(new { ok = true, cmd = "subscribeEvents", note = "already subscribed" });
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
_eventsSubscribed = true;
|
|
353
|
+
|
|
354
|
+
// Register system-wide focus changed handler
|
|
355
|
+
_focusHandler = new AutomationFocusChangedEventHandler(OnFocusChanged);
|
|
356
|
+
Automation.AddAutomationFocusChangedEventHandler(_focusHandler);
|
|
357
|
+
|
|
358
|
+
// Set up debounce timers
|
|
359
|
+
_structureDebounce = new System.Timers.Timer(_structureDebounceMs) { AutoReset = false };
|
|
360
|
+
_structureDebounce.Elapsed += OnStructureDebounceElapsed;
|
|
361
|
+
|
|
362
|
+
_propertyDebounce = new System.Timers.Timer(50) { AutoReset = false };
|
|
363
|
+
_propertyDebounce.Elapsed += OnPropertyDebounceElapsed;
|
|
364
|
+
|
|
365
|
+
// Immediately attach to current foreground window
|
|
366
|
+
try
|
|
367
|
+
{
|
|
368
|
+
IntPtr fgHwnd = GetForegroundWindow();
|
|
369
|
+
if (fgHwnd != IntPtr.Zero)
|
|
370
|
+
{
|
|
371
|
+
var win = AutomationElement.FromHandle(fgHwnd);
|
|
372
|
+
AttachToWindow(win);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
catch { /* ignore — will pick up on next focus change */ }
|
|
376
|
+
|
|
377
|
+
// Return initial snapshot
|
|
378
|
+
var initialElements = WalkFocusedWindowElements();
|
|
379
|
+
var activeWindow = GetActiveWindowInfo();
|
|
380
|
+
Reply(new
|
|
381
|
+
{
|
|
382
|
+
ok = true,
|
|
383
|
+
cmd = "subscribeEvents",
|
|
384
|
+
initial = new { activeWindow, elements = initialElements }
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
static void HandleUnsubscribeEvents()
|
|
389
|
+
{
|
|
390
|
+
if (!_eventsSubscribed)
|
|
391
|
+
{
|
|
392
|
+
Reply(new { ok = true, cmd = "unsubscribeEvents", note = "not subscribed" });
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
DetachFromWindow();
|
|
397
|
+
|
|
398
|
+
if (_focusHandler != null)
|
|
399
|
+
{
|
|
400
|
+
try { Automation.RemoveAutomationFocusChangedEventHandler(_focusHandler); } catch { }
|
|
401
|
+
_focusHandler = null;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
_structureDebounce?.Stop();
|
|
405
|
+
_structureDebounce?.Dispose();
|
|
406
|
+
_structureDebounce = null;
|
|
407
|
+
|
|
408
|
+
_propertyDebounce?.Stop();
|
|
409
|
+
_propertyDebounce?.Dispose();
|
|
410
|
+
_propertyDebounce = null;
|
|
411
|
+
|
|
412
|
+
lock (_propLock) { _pendingPropertyChanges.Clear(); }
|
|
413
|
+
|
|
414
|
+
_eventsSubscribed = false;
|
|
415
|
+
_structureDebounceMs = 100;
|
|
416
|
+
_structureEventBurst = 0;
|
|
417
|
+
|
|
418
|
+
Reply(new { ok = true, cmd = "unsubscribeEvents" });
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
static void OnFocusChanged(object sender, AutomationFocusChangedEventArgs e)
|
|
422
|
+
{
|
|
423
|
+
if (!_eventsSubscribed) return;
|
|
424
|
+
|
|
425
|
+
try
|
|
426
|
+
{
|
|
427
|
+
var focused = sender as AutomationElement;
|
|
428
|
+
if (focused == null) return;
|
|
429
|
+
|
|
430
|
+
// Walk up to find the top-level window
|
|
431
|
+
var topWindow = FindTopLevelWindow(focused);
|
|
432
|
+
if (topWindow == null) return;
|
|
433
|
+
|
|
434
|
+
int hwnd = topWindow.Current.NativeWindowHandle;
|
|
435
|
+
|
|
436
|
+
// Skip if same window
|
|
437
|
+
if (hwnd == _subscribedWindowHandle && hwnd != 0) return;
|
|
438
|
+
|
|
439
|
+
// Switch windows
|
|
440
|
+
DetachFromWindow();
|
|
441
|
+
AttachToWindow(topWindow);
|
|
442
|
+
|
|
443
|
+
// Emit focus changed event with active window info
|
|
444
|
+
var winInfo = BuildWindowInfo(topWindow);
|
|
445
|
+
Reply(new
|
|
446
|
+
{
|
|
447
|
+
type = "event",
|
|
448
|
+
@event = "focusChanged",
|
|
449
|
+
ts = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
|
|
450
|
+
data = new { activeWindow = winInfo }
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
// Also trigger a structure snapshot for the new window
|
|
454
|
+
FireStructureDebounce();
|
|
455
|
+
}
|
|
456
|
+
catch (ElementNotAvailableException) { /* element vanished, ignore */ }
|
|
457
|
+
catch { /* defensive */ }
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
static void OnStructureChanged(object sender, StructureChangedEventArgs e)
|
|
461
|
+
{
|
|
462
|
+
if (!_eventsSubscribed) return;
|
|
463
|
+
FireStructureDebounce();
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
static void OnPropertyChanged(object sender, AutomationPropertyChangedEventArgs e)
|
|
467
|
+
{
|
|
468
|
+
if (!_eventsSubscribed) return;
|
|
469
|
+
|
|
470
|
+
try
|
|
471
|
+
{
|
|
472
|
+
var el = sender as AutomationElement;
|
|
473
|
+
if (el == null) return;
|
|
474
|
+
|
|
475
|
+
var light = BuildLightElement(el, _subscribedWindowHandle);
|
|
476
|
+
if (light == null) return;
|
|
477
|
+
|
|
478
|
+
lock (_propLock)
|
|
479
|
+
{
|
|
480
|
+
_pendingPropertyChanges.Add(light);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// Reset the 50ms debounce timer
|
|
484
|
+
_propertyDebounce?.Stop();
|
|
485
|
+
_propertyDebounce?.Start();
|
|
486
|
+
}
|
|
487
|
+
catch (ElementNotAvailableException) { /* vanished */ }
|
|
488
|
+
catch { /* defensive */ }
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
static void FireStructureDebounce()
|
|
492
|
+
{
|
|
493
|
+
// Adaptive backoff: track burst rate
|
|
494
|
+
var now = DateTime.UtcNow;
|
|
495
|
+
if ((now - _structureBurstWindowStart).TotalMilliseconds > 1000)
|
|
496
|
+
{
|
|
497
|
+
// New 1-second window
|
|
498
|
+
if (_structureEventBurst > 10)
|
|
499
|
+
{
|
|
500
|
+
// Too many events last second — increase debounce for 5 seconds
|
|
501
|
+
_structureDebounceMs = 200;
|
|
502
|
+
}
|
|
503
|
+
else if (_structureDebounceMs > 100)
|
|
504
|
+
{
|
|
505
|
+
// Cool down back to normal
|
|
506
|
+
_structureDebounceMs = 100;
|
|
507
|
+
}
|
|
508
|
+
_structureEventBurst = 0;
|
|
509
|
+
_structureBurstWindowStart = now;
|
|
510
|
+
}
|
|
511
|
+
_structureEventBurst++;
|
|
512
|
+
|
|
513
|
+
if (_structureDebounce != null)
|
|
514
|
+
{
|
|
515
|
+
_structureDebounce.Interval = _structureDebounceMs;
|
|
516
|
+
_structureDebounce.Stop();
|
|
517
|
+
_structureDebounce.Start();
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
static void OnStructureDebounceElapsed(object? sender, ElapsedEventArgs e)
|
|
522
|
+
{
|
|
523
|
+
if (!_eventsSubscribed) return;
|
|
524
|
+
|
|
525
|
+
try
|
|
526
|
+
{
|
|
527
|
+
var elements = WalkFocusedWindowElements();
|
|
528
|
+
Reply(new
|
|
529
|
+
{
|
|
530
|
+
type = "event",
|
|
531
|
+
@event = "structureChanged",
|
|
532
|
+
ts = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
|
|
533
|
+
data = new { elements }
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
catch (Exception ex)
|
|
537
|
+
{
|
|
538
|
+
// Window may have vanished
|
|
539
|
+
Reply(new
|
|
540
|
+
{
|
|
541
|
+
type = "event",
|
|
542
|
+
@event = "error",
|
|
543
|
+
ts = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
|
|
544
|
+
data = new { error = ex.Message }
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
static void OnPropertyDebounceElapsed(object? sender, ElapsedEventArgs e)
|
|
550
|
+
{
|
|
551
|
+
if (!_eventsSubscribed) return;
|
|
552
|
+
|
|
553
|
+
List<Dictionary<string, object?>> batch;
|
|
554
|
+
lock (_propLock)
|
|
555
|
+
{
|
|
556
|
+
if (_pendingPropertyChanges.Count == 0) return;
|
|
557
|
+
batch = new List<Dictionary<string, object?>>(_pendingPropertyChanges);
|
|
558
|
+
_pendingPropertyChanges.Clear();
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// Deduplicate by id (keep latest)
|
|
562
|
+
var deduped = new Dictionary<string, Dictionary<string, object?>>();
|
|
563
|
+
foreach (var el in batch)
|
|
564
|
+
{
|
|
565
|
+
var id = el["id"]?.ToString() ?? "";
|
|
566
|
+
deduped[id] = el; // last wins
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
Reply(new
|
|
570
|
+
{
|
|
571
|
+
type = "event",
|
|
572
|
+
@event = "propertyChanged",
|
|
573
|
+
ts = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
|
|
574
|
+
data = new { elements = deduped.Values.ToList() }
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
static void AttachToWindow(AutomationElement window)
|
|
579
|
+
{
|
|
580
|
+
_subscribedWindow = window;
|
|
581
|
+
try { _subscribedWindowHandle = window.Current.NativeWindowHandle; } catch { _subscribedWindowHandle = 0; }
|
|
582
|
+
|
|
583
|
+
_structureHandler = new StructureChangedEventHandler(OnStructureChanged);
|
|
584
|
+
_propertyHandler = new AutomationPropertyChangedEventHandler(OnPropertyChanged);
|
|
585
|
+
|
|
586
|
+
try
|
|
587
|
+
{
|
|
588
|
+
Automation.AddStructureChangedEventHandler(
|
|
589
|
+
window, TreeScope.Subtree, _structureHandler);
|
|
590
|
+
}
|
|
591
|
+
catch { /* element may have vanished */ }
|
|
592
|
+
|
|
593
|
+
try
|
|
594
|
+
{
|
|
595
|
+
Automation.AddAutomationPropertyChangedEventHandler(
|
|
596
|
+
window, TreeScope.Subtree, _propertyHandler,
|
|
597
|
+
AutomationElement.BoundingRectangleProperty,
|
|
598
|
+
AutomationElement.NameProperty,
|
|
599
|
+
AutomationElement.IsEnabledProperty,
|
|
600
|
+
AutomationElement.IsOffscreenProperty);
|
|
601
|
+
}
|
|
602
|
+
catch { /* element may have vanished */ }
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
static void DetachFromWindow()
|
|
606
|
+
{
|
|
607
|
+
if (_subscribedWindow == null) return;
|
|
608
|
+
|
|
609
|
+
if (_structureHandler != null)
|
|
610
|
+
{
|
|
611
|
+
try { Automation.RemoveStructureChangedEventHandler(_subscribedWindow, _structureHandler); } catch { }
|
|
612
|
+
_structureHandler = null;
|
|
613
|
+
}
|
|
614
|
+
if (_propertyHandler != null)
|
|
615
|
+
{
|
|
616
|
+
try { Automation.RemoveAutomationPropertyChangedEventHandler(_subscribedWindow, _propertyHandler); } catch { }
|
|
617
|
+
_propertyHandler = null;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
_subscribedWindow = null;
|
|
621
|
+
_subscribedWindowHandle = 0;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
static AutomationElement? FindTopLevelWindow(AutomationElement element)
|
|
625
|
+
{
|
|
626
|
+
try
|
|
627
|
+
{
|
|
628
|
+
var walker = TreeWalker.ControlViewWalker;
|
|
629
|
+
var current = element;
|
|
630
|
+
AutomationElement? lastWindow = null;
|
|
631
|
+
|
|
632
|
+
while (current != null && !Automation.Compare(current, AutomationElement.RootElement))
|
|
633
|
+
{
|
|
634
|
+
try
|
|
635
|
+
{
|
|
636
|
+
if (current.Current.ControlType == ControlType.Window)
|
|
637
|
+
lastWindow = current;
|
|
638
|
+
}
|
|
639
|
+
catch (ElementNotAvailableException) { break; }
|
|
640
|
+
|
|
641
|
+
current = walker.GetParent(current);
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
return lastWindow;
|
|
645
|
+
}
|
|
646
|
+
catch { return null; }
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
static Dictionary<string, object?> BuildWindowInfo(AutomationElement window)
|
|
650
|
+
{
|
|
651
|
+
try
|
|
652
|
+
{
|
|
653
|
+
var rect = window.Current.BoundingRectangle;
|
|
654
|
+
return new Dictionary<string, object?>
|
|
655
|
+
{
|
|
656
|
+
["hwnd"] = window.Current.NativeWindowHandle,
|
|
657
|
+
["title"] = window.Current.Name,
|
|
658
|
+
["processId"] = window.Current.ProcessId,
|
|
659
|
+
["bounds"] = new Dictionary<string, double>
|
|
660
|
+
{
|
|
661
|
+
["x"] = SafeNumber(rect.X),
|
|
662
|
+
["y"] = SafeNumber(rect.Y),
|
|
663
|
+
["width"] = SafeNumber(rect.Width),
|
|
664
|
+
["height"] = SafeNumber(rect.Height)
|
|
665
|
+
}
|
|
666
|
+
};
|
|
667
|
+
}
|
|
668
|
+
catch
|
|
669
|
+
{
|
|
670
|
+
return new Dictionary<string, object?> { ["hwnd"] = 0, ["title"] = "", ["bounds"] = null };
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
/// <summary>
|
|
675
|
+
/// Walk the focused window tree, returning elements in the same shape
|
|
676
|
+
/// as the PowerShell UIWatcher (id, name, type, automationId, className,
|
|
677
|
+
/// windowHandle, bounds, center, isEnabled).
|
|
678
|
+
/// </summary>
|
|
679
|
+
static List<Dictionary<string, object?>> WalkFocusedWindowElements()
|
|
680
|
+
{
|
|
681
|
+
var results = new List<Dictionary<string, object?>>();
|
|
682
|
+
|
|
683
|
+
AutomationElement? win = _subscribedWindow;
|
|
684
|
+
if (win == null)
|
|
685
|
+
{
|
|
686
|
+
try
|
|
687
|
+
{
|
|
688
|
+
IntPtr fgHwnd = GetForegroundWindow();
|
|
689
|
+
if (fgHwnd != IntPtr.Zero)
|
|
690
|
+
win = AutomationElement.FromHandle(fgHwnd);
|
|
691
|
+
}
|
|
692
|
+
catch { return results; }
|
|
693
|
+
}
|
|
694
|
+
if (win == null) return results;
|
|
695
|
+
|
|
696
|
+
int rootHwnd = 0;
|
|
697
|
+
try { rootHwnd = win.Current.NativeWindowHandle; } catch { }
|
|
698
|
+
|
|
699
|
+
try
|
|
700
|
+
{
|
|
701
|
+
var all = win.FindAll(TreeScope.Descendants, System.Windows.Automation.Condition.TrueCondition);
|
|
702
|
+
int count = 0;
|
|
703
|
+
foreach (AutomationElement el in all)
|
|
704
|
+
{
|
|
705
|
+
if (count >= MaxWalkElements) break;
|
|
706
|
+
var light = BuildLightElement(el, rootHwnd);
|
|
707
|
+
if (light != null) { results.Add(light); count++; }
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
catch (ElementNotAvailableException) { /* window vanished */ }
|
|
711
|
+
|
|
712
|
+
return results;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
/// <summary>
|
|
716
|
+
/// Build a lightweight element matching the PowerShell UIWatcher format exactly.
|
|
717
|
+
/// Returns null for elements with no useful info or zero-size bounds.
|
|
718
|
+
/// </summary>
|
|
719
|
+
static Dictionary<string, object?>? BuildLightElement(AutomationElement el, int rootHwnd)
|
|
720
|
+
{
|
|
721
|
+
try
|
|
722
|
+
{
|
|
723
|
+
var rect = el.Current.BoundingRectangle;
|
|
724
|
+
if (rect.Width <= 0 || rect.Height <= 0) return null;
|
|
725
|
+
if (rect.X < -10000 || rect.Y < -10000) return null;
|
|
726
|
+
|
|
727
|
+
string name = el.Current.Name ?? "";
|
|
728
|
+
name = name.Replace("\r", " ").Replace("\n", " ").Replace("\t", " ");
|
|
729
|
+
|
|
730
|
+
string ctrlType = el.Current.ControlType.ProgrammaticName.Replace("ControlType.", "");
|
|
731
|
+
string autoId = el.Current.AutomationId ?? "";
|
|
732
|
+
autoId = autoId.Replace("\r", " ").Replace("\n", " ").Replace("\t", " ");
|
|
733
|
+
|
|
734
|
+
// Skip elements with no useful identifying info (same filter as PS watcher)
|
|
735
|
+
if (string.IsNullOrWhiteSpace(name) && string.IsNullOrWhiteSpace(autoId)) return null;
|
|
736
|
+
|
|
737
|
+
int x = (int)rect.X, y = (int)rect.Y;
|
|
738
|
+
int w = (int)rect.Width, h = (int)rect.Height;
|
|
739
|
+
|
|
740
|
+
return new Dictionary<string, object?>
|
|
741
|
+
{
|
|
742
|
+
["id"] = $"{ctrlType}|{name}|{autoId}|{x}|{y}",
|
|
743
|
+
["name"] = name,
|
|
744
|
+
["type"] = ctrlType,
|
|
745
|
+
["automationId"] = autoId,
|
|
746
|
+
["className"] = el.Current.ClassName,
|
|
747
|
+
["windowHandle"] = rootHwnd,
|
|
748
|
+
["bounds"] = new Dictionary<string, int> { ["x"] = x, ["y"] = y, ["width"] = w, ["height"] = h },
|
|
749
|
+
["center"] = new Dictionary<string, int> { ["x"] = x + w / 2, ["y"] = y + h / 2 },
|
|
750
|
+
["isEnabled"] = el.Current.IsEnabled
|
|
751
|
+
};
|
|
752
|
+
}
|
|
753
|
+
catch (ElementNotAvailableException) { return null; }
|
|
754
|
+
catch { return null; }
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
static Dictionary<string, object?>? GetActiveWindowInfo()
|
|
758
|
+
{
|
|
759
|
+
try
|
|
760
|
+
{
|
|
761
|
+
IntPtr hwnd = GetForegroundWindow();
|
|
762
|
+
if (hwnd == IntPtr.Zero) return null;
|
|
763
|
+
var win = AutomationElement.FromHandle(hwnd);
|
|
764
|
+
return BuildWindowInfo(win);
|
|
765
|
+
}
|
|
766
|
+
catch { return null; }
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
// ── End Phase 4 ─────────────────────────────────────────────────────
|
|
770
|
+
static List<string> GetPatternNames(AutomationElement el)
|
|
771
|
+
{
|
|
772
|
+
var patterns = new List<string>();
|
|
773
|
+
if ((bool)el.GetCurrentPropertyValue(AutomationElement.IsInvokePatternAvailableProperty)) patterns.Add("Invoke");
|
|
774
|
+
if ((bool)el.GetCurrentPropertyValue(AutomationElement.IsValuePatternAvailableProperty)) patterns.Add("Value");
|
|
775
|
+
if ((bool)el.GetCurrentPropertyValue(AutomationElement.IsTogglePatternAvailableProperty)) patterns.Add("Toggle");
|
|
776
|
+
if ((bool)el.GetCurrentPropertyValue(AutomationElement.IsSelectionItemPatternAvailableProperty)) patterns.Add("SelectionItem");
|
|
777
|
+
if ((bool)el.GetCurrentPropertyValue(AutomationElement.IsExpandCollapsePatternAvailableProperty)) patterns.Add("ExpandCollapse");
|
|
778
|
+
if ((bool)el.GetCurrentPropertyValue(AutomationElement.IsScrollPatternAvailableProperty)) patterns.Add("Scroll");
|
|
779
|
+
if ((bool)el.GetCurrentPropertyValue(AutomationElement.IsTextPatternAvailableProperty)) patterns.Add("Text");
|
|
780
|
+
if ((bool)el.GetCurrentPropertyValue(AutomationElement.IsWindowPatternAvailableProperty)) patterns.Add("Window");
|
|
781
|
+
return patterns;
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
// ── Rich element payload (Phase 2) ───────────────────────────────────
|
|
785
|
+
static Dictionary<string, object?> BuildRichElement(AutomationElement el)
|
|
786
|
+
{
|
|
787
|
+
var rect = el.Current.BoundingRectangle;
|
|
788
|
+
var result = new Dictionary<string, object?>
|
|
789
|
+
{
|
|
790
|
+
["name"] = el.Current.Name,
|
|
791
|
+
["automationId"] = el.Current.AutomationId,
|
|
792
|
+
["className"] = el.Current.ClassName,
|
|
793
|
+
["role"] = el.Current.ControlType.ProgrammaticName.Replace("ControlType.", ""),
|
|
794
|
+
["bounds"] = new Dictionary<string, double>
|
|
795
|
+
{
|
|
796
|
+
["x"] = SafeNumber(rect.X),
|
|
797
|
+
["y"] = SafeNumber(rect.Y),
|
|
798
|
+
["width"] = SafeNumber(rect.Width),
|
|
799
|
+
["height"] = SafeNumber(rect.Height)
|
|
800
|
+
},
|
|
801
|
+
["isEnabled"] = el.Current.IsEnabled,
|
|
802
|
+
["isOffscreen"] = el.Current.IsOffscreen,
|
|
803
|
+
["hasKeyboardFocus"] = el.Current.HasKeyboardFocus,
|
|
804
|
+
["nativeWindowHandle"] = el.Current.NativeWindowHandle
|
|
805
|
+
};
|
|
806
|
+
|
|
807
|
+
// RuntimeId — session-scoped stable identity
|
|
808
|
+
try
|
|
809
|
+
{
|
|
810
|
+
int[] rid = el.GetRuntimeId();
|
|
811
|
+
result["runtimeId"] = rid;
|
|
812
|
+
}
|
|
813
|
+
catch { result["runtimeId"] = null; }
|
|
814
|
+
|
|
815
|
+
// TryGetClickablePoint — preferred click target
|
|
816
|
+
try
|
|
817
|
+
{
|
|
818
|
+
if (el.TryGetClickablePoint(out Point pt))
|
|
819
|
+
{
|
|
820
|
+
result["clickPoint"] = new Dictionary<string, double>
|
|
821
|
+
{
|
|
822
|
+
["x"] = pt.X,
|
|
823
|
+
["y"] = pt.Y
|
|
824
|
+
};
|
|
825
|
+
}
|
|
826
|
+
else
|
|
827
|
+
{
|
|
828
|
+
result["clickPoint"] = null;
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
catch { result["clickPoint"] = null; }
|
|
832
|
+
|
|
833
|
+
// Value (if available)
|
|
834
|
+
try
|
|
835
|
+
{
|
|
836
|
+
object val = el.GetCurrentPropertyValue(ValuePattern.ValueProperty);
|
|
837
|
+
result["value"] = val?.ToString();
|
|
838
|
+
}
|
|
839
|
+
catch { result["value"] = null; }
|
|
840
|
+
|
|
841
|
+
// Supported patterns (names only — avoids expensive GetSupportedPatterns())
|
|
842
|
+
var patterns = new List<string>();
|
|
843
|
+
if ((bool)el.GetCurrentPropertyValue(AutomationElement.IsInvokePatternAvailableProperty)) patterns.Add("Invoke");
|
|
844
|
+
if ((bool)el.GetCurrentPropertyValue(AutomationElement.IsValuePatternAvailableProperty)) patterns.Add("Value");
|
|
845
|
+
if ((bool)el.GetCurrentPropertyValue(AutomationElement.IsTogglePatternAvailableProperty)) patterns.Add("Toggle");
|
|
846
|
+
if ((bool)el.GetCurrentPropertyValue(AutomationElement.IsSelectionItemPatternAvailableProperty)) patterns.Add("SelectionItem");
|
|
847
|
+
if ((bool)el.GetCurrentPropertyValue(AutomationElement.IsExpandCollapsePatternAvailableProperty)) patterns.Add("ExpandCollapse");
|
|
848
|
+
if ((bool)el.GetCurrentPropertyValue(AutomationElement.IsScrollPatternAvailableProperty)) patterns.Add("Scroll");
|
|
849
|
+
if ((bool)el.GetCurrentPropertyValue(AutomationElement.IsTextPatternAvailableProperty)) patterns.Add("Text");
|
|
850
|
+
if ((bool)el.GetCurrentPropertyValue(AutomationElement.IsWindowPatternAvailableProperty)) patterns.Add("Window");
|
|
851
|
+
result["patterns"] = patterns;
|
|
852
|
+
|
|
853
|
+
return result;
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
// ── Tree builder (legacy path, unchanged shape) ──────────────────────
|
|
857
|
+
static UIANode BuildTree(AutomationElement element)
|
|
858
|
+
{
|
|
859
|
+
var rectangle = element.Current.BoundingRectangle;
|
|
860
|
+
var node = new UIANode
|
|
861
|
+
{
|
|
862
|
+
id = element.Current.AutomationId,
|
|
863
|
+
name = element.Current.Name,
|
|
864
|
+
role = element.Current.ControlType.ProgrammaticName.Replace("ControlType.", ""),
|
|
865
|
+
bounds = new Bounds
|
|
866
|
+
{
|
|
867
|
+
x = SafeNumber(rectangle.X),
|
|
868
|
+
y = SafeNumber(rectangle.Y),
|
|
869
|
+
width = SafeNumber(rectangle.Width),
|
|
870
|
+
height = SafeNumber(rectangle.Height)
|
|
871
|
+
},
|
|
872
|
+
isClickable = (bool)element.GetCurrentPropertyValue(AutomationElement.IsInvokePatternAvailableProperty) || element.Current.IsKeyboardFocusable,
|
|
873
|
+
isFocusable = element.Current.IsKeyboardFocusable,
|
|
874
|
+
children = new List<UIANode>()
|
|
875
|
+
};
|
|
876
|
+
|
|
877
|
+
var walker = TreeWalker.ControlViewWalker;
|
|
878
|
+
var child = walker.GetFirstChild(element);
|
|
879
|
+
while (child != null)
|
|
880
|
+
{
|
|
881
|
+
try
|
|
882
|
+
{
|
|
883
|
+
if (!child.Current.IsOffscreen)
|
|
884
|
+
{
|
|
885
|
+
node.children.Add(BuildTree(child));
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
catch (ElementNotAvailableException) { }
|
|
889
|
+
|
|
890
|
+
child = walker.GetNextSibling(child);
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
return node;
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
static double SafeNumber(double value)
|
|
897
|
+
{
|
|
898
|
+
return double.IsFinite(value) ? value : 0;
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
class UIANode
|
|
903
|
+
{
|
|
904
|
+
public string id { get; set; } = "";
|
|
905
|
+
public string name { get; set; } = "";
|
|
906
|
+
public string role { get; set; } = "";
|
|
907
|
+
public Bounds bounds { get; set; } = new();
|
|
908
|
+
public bool isClickable { get; set; }
|
|
909
|
+
public bool isFocusable { get; set; }
|
|
910
|
+
public List<UIANode> children { get; set; } = new();
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
class Bounds
|
|
914
|
+
{
|
|
915
|
+
public double x { get; set; }
|
|
916
|
+
public double y { get; set; }
|
|
917
|
+
public double width { get; set; }
|
|
918
|
+
public double height { get; set; }
|
|
919
|
+
}
|
|
920
|
+
}
|