windows-mcp 0.5.8__py3-none-any.whl → 0.5.9__py3-none-any.whl
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.
- windows_mcp/__main__.py +299 -314
- windows_mcp/analytics.py +0 -5
- windows_mcp/desktop/service.py +638 -458
- windows_mcp/desktop/views.py +7 -5
- windows_mcp/tree/cache_utils.py +126 -0
- windows_mcp/tree/config.py +25 -0
- windows_mcp/tree/service.py +543 -601
- windows_mcp/tree/views.py +142 -116
- windows_mcp/uia/controls.py +11 -2
- windows_mcp/uia/core.py +9 -0
- windows_mcp/vdm/__init__.py +1 -0
- windows_mcp/vdm/core.py +490 -0
- windows_mcp/watchdog/event_handlers.py +13 -9
- windows_mcp/watchdog/service.py +15 -4
- {windows_mcp-0.5.8.dist-info → windows_mcp-0.5.9.dist-info}/METADATA +27 -21
- windows_mcp-0.5.9.dist-info/RECORD +29 -0
- windows_mcp-0.5.8.dist-info/RECORD +0 -26
- {windows_mcp-0.5.8.dist-info → windows_mcp-0.5.9.dist-info}/WHEEL +0 -0
- {windows_mcp-0.5.8.dist-info → windows_mcp-0.5.9.dist-info}/entry_points.txt +0 -0
- {windows_mcp-0.5.8.dist-info → windows_mcp-0.5.9.dist-info}/licenses/LICENSE.md +0 -0
windows_mcp/tree/service.py
CHANGED
|
@@ -1,601 +1,543 @@
|
|
|
1
|
-
from windows_mcp.
|
|
2
|
-
from windows_mcp.tree.
|
|
3
|
-
from windows_mcp.
|
|
4
|
-
from
|
|
5
|
-
from windows_mcp.tree.utils import random_point_within_bounding_box
|
|
6
|
-
from
|
|
7
|
-
from typing import TYPE_CHECKING,Optional
|
|
8
|
-
from
|
|
9
|
-
|
|
10
|
-
import logging
|
|
11
|
-
import random
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
self.
|
|
26
|
-
self.
|
|
27
|
-
self.
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
def get_state(self,
|
|
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
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
'
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
dom_interactive_nodes.
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
original_width = screenshot.width
|
|
546
|
-
original_height = screenshot.height
|
|
547
|
-
|
|
548
|
-
scaled_width = int(original_width * scale)
|
|
549
|
-
scaled_height = int(original_height * scale)
|
|
550
|
-
screenshot = screenshot.resize((scaled_width, scaled_height), Image.Resampling.LANCZOS)
|
|
551
|
-
|
|
552
|
-
# Add padding
|
|
553
|
-
padding = 5
|
|
554
|
-
width = int(screenshot.width + (1.5 * padding))
|
|
555
|
-
height = int(screenshot.height + (1.5 * padding))
|
|
556
|
-
padded_screenshot = Image.new("RGB", (width, height), color=(255, 255, 255))
|
|
557
|
-
padded_screenshot.paste(screenshot, (padding, padding))
|
|
558
|
-
|
|
559
|
-
draw = ImageDraw.Draw(padded_screenshot)
|
|
560
|
-
font_size = 12
|
|
561
|
-
try:
|
|
562
|
-
font = ImageFont.truetype('arial.ttf', font_size)
|
|
563
|
-
except IOError:
|
|
564
|
-
font = ImageFont.load_default()
|
|
565
|
-
|
|
566
|
-
def get_random_color():
|
|
567
|
-
return "#{:06x}".format(random.randint(0, 0xFFFFFF))
|
|
568
|
-
|
|
569
|
-
def draw_annotation(label, node: TreeElementNode):
|
|
570
|
-
box = node.bounding_box
|
|
571
|
-
color = get_random_color()
|
|
572
|
-
|
|
573
|
-
# Scale and pad the bounding box coordinates
|
|
574
|
-
adjusted_box = (
|
|
575
|
-
int(box.left * scale) + padding,
|
|
576
|
-
int(box.top * scale) + padding,
|
|
577
|
-
int(box.right * scale) + padding,
|
|
578
|
-
int(box.bottom * scale) + padding
|
|
579
|
-
)
|
|
580
|
-
# Draw bounding box
|
|
581
|
-
draw.rectangle(adjusted_box, outline=color, width=2)
|
|
582
|
-
|
|
583
|
-
# Label dimensions
|
|
584
|
-
label_width = draw.textlength(str(label), font=font)
|
|
585
|
-
label_height = font_size
|
|
586
|
-
left, top, right, bottom = adjusted_box
|
|
587
|
-
|
|
588
|
-
# Label position above bounding box
|
|
589
|
-
label_x1 = right - label_width
|
|
590
|
-
label_y1 = top - label_height - 4
|
|
591
|
-
label_x2 = label_x1 + label_width
|
|
592
|
-
label_y2 = label_y1 + label_height + 4
|
|
593
|
-
|
|
594
|
-
# Draw label background and text
|
|
595
|
-
draw.rectangle([(label_x1, label_y1), (label_x2, label_y2)], fill=color)
|
|
596
|
-
draw.text((label_x1 + 2, label_y1 + 2), str(label), fill=(255, 255, 255), font=font)
|
|
597
|
-
|
|
598
|
-
# Draw annotations in parallel
|
|
599
|
-
with ThreadPoolExecutor() as executor:
|
|
600
|
-
executor.map(draw_annotation, range(len(nodes)), nodes)
|
|
601
|
-
return padded_screenshot
|
|
1
|
+
from windows_mcp.uia import Control,ImageControl,ScrollPattern,WindowControl,Rect,GetRootControl,PatternId,AccessibleRoleNames,PaneControl,GroupControl,StructureChangeType,TreeScope,ControlFromHandle
|
|
2
|
+
from windows_mcp.tree.config import INTERACTIVE_CONTROL_TYPE_NAMES,DOCUMENT_CONTROL_TYPE_NAMES,INFORMATIVE_CONTROL_TYPE_NAMES, DEFAULT_ACTIONS, INTERACTIVE_ROLES, THREAD_MAX_RETRIES
|
|
3
|
+
from windows_mcp.tree.views import TreeElementNode, ScrollElementNode, TextElementNode, Center, BoundingBox, TreeState
|
|
4
|
+
from windows_mcp.tree.cache_utils import CacheRequestFactory,CachedControlHelper
|
|
5
|
+
from windows_mcp.tree.utils import random_point_within_bounding_box
|
|
6
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
7
|
+
from typing import TYPE_CHECKING,Optional,Any
|
|
8
|
+
from time import sleep,time
|
|
9
|
+
import threading
|
|
10
|
+
import logging
|
|
11
|
+
import random
|
|
12
|
+
import weakref
|
|
13
|
+
import comtypes
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
logger.setLevel(logging.INFO)
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from windows_mcp.desktop.service import Desktop
|
|
20
|
+
|
|
21
|
+
class Tree:
|
|
22
|
+
def __init__(self,desktop:'Desktop'):
|
|
23
|
+
self.desktop=weakref.proxy(desktop)
|
|
24
|
+
self.screen_size=desktop.get_screen_size()
|
|
25
|
+
self.dom:Optional[Control]=None
|
|
26
|
+
self.dom_bounding_box:BoundingBox=None
|
|
27
|
+
self.screen_box=BoundingBox(
|
|
28
|
+
top=0, left=0, bottom=self.screen_size.height, right=self.screen_size.width,
|
|
29
|
+
width=self.screen_size.width, height=self.screen_size.height
|
|
30
|
+
)
|
|
31
|
+
self.tree_state=None
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def get_state(self,active_app_handle:int|None,other_apps_handles:list[int],use_dom:bool=False)->TreeState:
|
|
35
|
+
# Reset DOM state to prevent leaks and stale data
|
|
36
|
+
self.dom = None
|
|
37
|
+
self.dom_bounding_box = None
|
|
38
|
+
start_time = time()
|
|
39
|
+
|
|
40
|
+
active_app_flag=False
|
|
41
|
+
if active_app_handle:
|
|
42
|
+
active_app_flag=True
|
|
43
|
+
apps_handles=[active_app_handle]+other_apps_handles
|
|
44
|
+
else:
|
|
45
|
+
apps_handles=other_apps_handles
|
|
46
|
+
|
|
47
|
+
interactive_nodes,scrollable_nodes,dom_informative_nodes=self.get_appwise_nodes(apps_handles=apps_handles,active_app_flag=active_app_flag,use_dom=use_dom)
|
|
48
|
+
root_node=TreeElementNode(
|
|
49
|
+
name="Desktop",
|
|
50
|
+
control_type="PaneControl",
|
|
51
|
+
bounding_box=self.screen_box,
|
|
52
|
+
center=self.screen_box.get_center(),
|
|
53
|
+
app_name="Desktop",
|
|
54
|
+
xpath='',
|
|
55
|
+
value='',
|
|
56
|
+
shortcut='',
|
|
57
|
+
is_focused=False
|
|
58
|
+
)
|
|
59
|
+
if self.dom:
|
|
60
|
+
scroll_pattern:ScrollPattern=self.dom.GetPattern(PatternId.ScrollPattern)
|
|
61
|
+
dom_node=ScrollElementNode(
|
|
62
|
+
name="DOM",
|
|
63
|
+
control_type="DocumentControl",
|
|
64
|
+
bounding_box=self.dom_bounding_box,
|
|
65
|
+
center=self.dom_bounding_box.get_center(),
|
|
66
|
+
horizontal_scrollable=scroll_pattern.HorizontallyScrollable if scroll_pattern else False,
|
|
67
|
+
horizontal_scroll_percent=scroll_pattern.HorizontalScrollPercent if scroll_pattern and scroll_pattern.HorizontallyScrollable else 0,
|
|
68
|
+
vertical_scrollable=scroll_pattern.VerticallyScrollable if scroll_pattern else False,
|
|
69
|
+
vertical_scroll_percent=scroll_pattern.VerticalScrollPercent if scroll_pattern and scroll_pattern.VerticallyScrollable else 0,
|
|
70
|
+
xpath='',
|
|
71
|
+
app_name="DOM",
|
|
72
|
+
is_focused=False
|
|
73
|
+
)
|
|
74
|
+
else:
|
|
75
|
+
dom_node=None
|
|
76
|
+
self.tree_state=TreeState(root_node=root_node,dom_node=dom_node,interactive_nodes=interactive_nodes,scrollable_nodes=scrollable_nodes,dom_informative_nodes=dom_informative_nodes)
|
|
77
|
+
end_time = time()
|
|
78
|
+
logger.info(f"Tree State capture took {end_time - start_time:.2f} seconds")
|
|
79
|
+
return self.tree_state
|
|
80
|
+
|
|
81
|
+
def get_appwise_nodes(self,apps_handles:list[int],active_app_flag:bool,use_dom:bool=False) -> tuple[list[TreeElementNode],list[ScrollElementNode],list[TextElementNode]]:
|
|
82
|
+
interactive_nodes, scrollable_nodes, dom_informative_nodes = [], [], []
|
|
83
|
+
|
|
84
|
+
# Pre-calculate browser status in main thread to pass simple types to workers
|
|
85
|
+
task_inputs = []
|
|
86
|
+
for handle in apps_handles:
|
|
87
|
+
is_browser = False
|
|
88
|
+
try:
|
|
89
|
+
# Use temporary control for property check in main thread
|
|
90
|
+
# This is safe as we don't pass this specific COM object to the thread
|
|
91
|
+
temp_node = ControlFromHandle(handle)
|
|
92
|
+
if active_app_flag and temp_node.ClassName=='Progman':
|
|
93
|
+
continue
|
|
94
|
+
is_browser = self.desktop.is_app_browser(temp_node)
|
|
95
|
+
except Exception:
|
|
96
|
+
pass
|
|
97
|
+
task_inputs.append((handle, is_browser))
|
|
98
|
+
|
|
99
|
+
with ThreadPoolExecutor() as executor:
|
|
100
|
+
retry_counts = {handle: 0 for handle in apps_handles}
|
|
101
|
+
future_to_handle = {
|
|
102
|
+
executor.submit(self.get_nodes, handle, is_browser, use_dom): handle
|
|
103
|
+
for handle, is_browser in task_inputs
|
|
104
|
+
}
|
|
105
|
+
while future_to_handle: # keep running until no pending futures
|
|
106
|
+
for future in as_completed(list(future_to_handle)):
|
|
107
|
+
handle = future_to_handle.pop(future) # remove completed future
|
|
108
|
+
try:
|
|
109
|
+
result = future.result()
|
|
110
|
+
if result:
|
|
111
|
+
element_nodes, scroll_nodes, info_nodes = result
|
|
112
|
+
interactive_nodes.extend(element_nodes)
|
|
113
|
+
scrollable_nodes.extend(scroll_nodes)
|
|
114
|
+
dom_informative_nodes.extend(info_nodes)
|
|
115
|
+
except Exception as e:
|
|
116
|
+
retry_counts[handle] += 1
|
|
117
|
+
logger.debug(f"Error in processing handle {handle}, retry attempt {retry_counts[handle]}\nError: {e}")
|
|
118
|
+
if retry_counts[handle] < THREAD_MAX_RETRIES:
|
|
119
|
+
# Need to find is_browser again for retry
|
|
120
|
+
is_browser = next((ib for h, ib in task_inputs if h == handle), False)
|
|
121
|
+
new_future = executor.submit(self.get_nodes, handle, is_browser, use_dom)
|
|
122
|
+
future_to_handle[new_future] = handle
|
|
123
|
+
else:
|
|
124
|
+
logger.error(f"Task failed completely for handle {handle} after {THREAD_MAX_RETRIES} retries")
|
|
125
|
+
return interactive_nodes,scrollable_nodes,dom_informative_nodes
|
|
126
|
+
|
|
127
|
+
def iou_bounding_box(self,window_box: Rect,element_box: Rect,) -> BoundingBox:
|
|
128
|
+
# Step 1: Intersection of element and window (existing logic)
|
|
129
|
+
intersection_left = max(window_box.left, element_box.left)
|
|
130
|
+
intersection_top = max(window_box.top, element_box.top)
|
|
131
|
+
intersection_right = min(window_box.right, element_box.right)
|
|
132
|
+
intersection_bottom = min(window_box.bottom, element_box.bottom)
|
|
133
|
+
|
|
134
|
+
# Step 2: Clamp to screen boundaries (new addition)
|
|
135
|
+
intersection_left = max(self.screen_box.left, intersection_left)
|
|
136
|
+
intersection_top = max(self.screen_box.top, intersection_top)
|
|
137
|
+
intersection_right = min(self.screen_box.right, intersection_right)
|
|
138
|
+
intersection_bottom = min(self.screen_box.bottom, intersection_bottom)
|
|
139
|
+
|
|
140
|
+
# Step 3: Validate intersection
|
|
141
|
+
if (intersection_right > intersection_left and intersection_bottom > intersection_top):
|
|
142
|
+
bounding_box = BoundingBox(
|
|
143
|
+
left=intersection_left,
|
|
144
|
+
top=intersection_top,
|
|
145
|
+
right=intersection_right,
|
|
146
|
+
bottom=intersection_bottom,
|
|
147
|
+
width=intersection_right - intersection_left,
|
|
148
|
+
height=intersection_bottom - intersection_top
|
|
149
|
+
)
|
|
150
|
+
else:
|
|
151
|
+
# No valid visible intersection (either outside window or screen)
|
|
152
|
+
bounding_box = BoundingBox(
|
|
153
|
+
left=0,
|
|
154
|
+
top=0,
|
|
155
|
+
right=0,
|
|
156
|
+
bottom=0,
|
|
157
|
+
width=0,
|
|
158
|
+
height=0
|
|
159
|
+
)
|
|
160
|
+
return bounding_box
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def element_has_child_element(self, node:Control,control_type:str,child_control_type:str):
|
|
165
|
+
if node.LocalizedControlType==control_type:
|
|
166
|
+
first_child=node.GetFirstChildControl()
|
|
167
|
+
if first_child is None:
|
|
168
|
+
return False
|
|
169
|
+
return first_child.LocalizedControlType==child_control_type
|
|
170
|
+
|
|
171
|
+
def _dom_correction(self, node:Control, dom_interactive_nodes:list[TreeElementNode], app_name:str):
|
|
172
|
+
if self.element_has_child_element(node,'list item','link') or self.element_has_child_element(node,'item','link'):
|
|
173
|
+
dom_interactive_nodes.pop()
|
|
174
|
+
return None
|
|
175
|
+
elif node.ControlTypeName=='GroupControl':
|
|
176
|
+
dom_interactive_nodes.pop()
|
|
177
|
+
# Inlined is_keyboard_focusable logic for correction
|
|
178
|
+
control_type_name_check = node.CachedControlTypeName
|
|
179
|
+
is_kb_focusable = False
|
|
180
|
+
if control_type_name_check in set(['EditControl','ButtonControl','CheckBoxControl','RadioButtonControl','TabItemControl']):
|
|
181
|
+
is_kb_focusable = True
|
|
182
|
+
else:
|
|
183
|
+
is_kb_focusable = node.CachedIsKeyboardFocusable
|
|
184
|
+
|
|
185
|
+
if is_kb_focusable:
|
|
186
|
+
child=node
|
|
187
|
+
try:
|
|
188
|
+
while child.GetFirstChildControl() is not None:
|
|
189
|
+
if child.ControlTypeName in INTERACTIVE_CONTROL_TYPE_NAMES:
|
|
190
|
+
return None
|
|
191
|
+
child=child.GetFirstChildControl()
|
|
192
|
+
except Exception:
|
|
193
|
+
return None
|
|
194
|
+
if child.ControlTypeName!='TextControl':
|
|
195
|
+
return None
|
|
196
|
+
legacy_pattern=node.GetLegacyIAccessiblePattern()
|
|
197
|
+
value=legacy_pattern.Value
|
|
198
|
+
element_bounding_box = node.BoundingRectangle
|
|
199
|
+
bounding_box=self.iou_bounding_box(self.dom_bounding_box,element_bounding_box)
|
|
200
|
+
center = bounding_box.get_center()
|
|
201
|
+
is_focused=node.HasKeyboardFocus
|
|
202
|
+
dom_interactive_nodes.append(TreeElementNode(**{
|
|
203
|
+
'name':child.Name.strip(),
|
|
204
|
+
'control_type':node.LocalizedControlType,
|
|
205
|
+
'value':value,
|
|
206
|
+
'shortcut':node.AcceleratorKey,
|
|
207
|
+
'bounding_box':bounding_box,
|
|
208
|
+
'xpath':'',
|
|
209
|
+
'center':center,
|
|
210
|
+
'app_name':app_name,
|
|
211
|
+
'is_focused':is_focused
|
|
212
|
+
}))
|
|
213
|
+
elif self.element_has_child_element(node,'link','heading'):
|
|
214
|
+
dom_interactive_nodes.pop()
|
|
215
|
+
node=node.GetFirstChildControl()
|
|
216
|
+
control_type='link'
|
|
217
|
+
legacy_pattern=node.GetLegacyIAccessiblePattern()
|
|
218
|
+
value=legacy_pattern.Value
|
|
219
|
+
element_bounding_box = node.BoundingRectangle
|
|
220
|
+
bounding_box=self.iou_bounding_box(self.dom_bounding_box,element_bounding_box)
|
|
221
|
+
center = bounding_box.get_center()
|
|
222
|
+
is_focused=node.HasKeyboardFocus
|
|
223
|
+
dom_interactive_nodes.append(TreeElementNode(**{
|
|
224
|
+
'name':node.Name.strip(),
|
|
225
|
+
'control_type':control_type,
|
|
226
|
+
'value':node.Name.strip(),
|
|
227
|
+
'shortcut':node.AcceleratorKey,
|
|
228
|
+
'bounding_box':bounding_box,
|
|
229
|
+
'xpath':'',
|
|
230
|
+
'center':center,
|
|
231
|
+
'app_name':app_name,
|
|
232
|
+
'is_focused':is_focused
|
|
233
|
+
}))
|
|
234
|
+
|
|
235
|
+
def tree_traversal(self, node: Control, window_bounding_box:Rect, app_name:str, is_browser:bool,
|
|
236
|
+
interactive_nodes:Optional[list[TreeElementNode]]=None, scrollable_nodes:Optional[list[ScrollElementNode]]=None,
|
|
237
|
+
dom_interactive_nodes:Optional[list[TreeElementNode]]=None, dom_informative_nodes:Optional[list[TextElementNode]]=None,
|
|
238
|
+
is_dom:bool=False, is_dialog:bool=False,
|
|
239
|
+
element_cache_req:Optional[Any]=None, children_cache_req:Optional[Any]=None):
|
|
240
|
+
try:
|
|
241
|
+
# Build cached control if caching is enabled
|
|
242
|
+
if not hasattr(node, '_is_cached') and element_cache_req:
|
|
243
|
+
node = CachedControlHelper.build_cached_control(node, element_cache_req)
|
|
244
|
+
|
|
245
|
+
# Checks to skip the nodes that are not interactive
|
|
246
|
+
is_offscreen = node.CachedIsOffscreen
|
|
247
|
+
control_type_name = node.CachedControlTypeName
|
|
248
|
+
class_name = node.CachedClassName
|
|
249
|
+
|
|
250
|
+
# Scrollable check
|
|
251
|
+
if scrollable_nodes is not None:
|
|
252
|
+
if (control_type_name not in (INTERACTIVE_CONTROL_TYPE_NAMES|INFORMATIVE_CONTROL_TYPE_NAMES)) and not is_offscreen:
|
|
253
|
+
try:
|
|
254
|
+
scroll_pattern:ScrollPattern=node.GetPattern(PatternId.ScrollPattern)
|
|
255
|
+
if scroll_pattern and scroll_pattern.VerticallyScrollable:
|
|
256
|
+
box = node.CachedBoundingRectangle
|
|
257
|
+
x,y=random_point_within_bounding_box(node=node,scale_factor=0.8)
|
|
258
|
+
center = Center(x=x,y=y)
|
|
259
|
+
name = node.CachedName
|
|
260
|
+
automation_id = node.CachedAutomationId
|
|
261
|
+
localized_control_type = node.CachedLocalizedControlType
|
|
262
|
+
has_keyboard_focus = node.CachedHasKeyboardFocus
|
|
263
|
+
scrollable_nodes.append(ScrollElementNode(**{
|
|
264
|
+
'name':name.strip() or automation_id or localized_control_type.capitalize() or "''",
|
|
265
|
+
'control_type':localized_control_type.title(),
|
|
266
|
+
'bounding_box':BoundingBox(**{
|
|
267
|
+
'left':box.left,
|
|
268
|
+
'top':box.top,
|
|
269
|
+
'right':box.right,
|
|
270
|
+
'bottom':box.bottom,
|
|
271
|
+
'width':box.width(),
|
|
272
|
+
'height':box.height()
|
|
273
|
+
}),
|
|
274
|
+
'center':center,
|
|
275
|
+
'xpath':'',
|
|
276
|
+
'horizontal_scrollable':scroll_pattern.HorizontallyScrollable,
|
|
277
|
+
'horizontal_scroll_percent':scroll_pattern.HorizontalScrollPercent if scroll_pattern.HorizontallyScrollable else 0,
|
|
278
|
+
'vertical_scrollable':scroll_pattern.VerticallyScrollable,
|
|
279
|
+
'vertical_scroll_percent':scroll_pattern.VerticalScrollPercent if scroll_pattern.VerticallyScrollable else 0,
|
|
280
|
+
'app_name':app_name,
|
|
281
|
+
'is_focused':has_keyboard_focus
|
|
282
|
+
}))
|
|
283
|
+
except Exception:
|
|
284
|
+
pass
|
|
285
|
+
|
|
286
|
+
# Interactive and Informative checks
|
|
287
|
+
# Pre-calculate common properties
|
|
288
|
+
is_control_element = node.CachedIsControlElement
|
|
289
|
+
element_bounding_box = node.CachedBoundingRectangle
|
|
290
|
+
width = element_bounding_box.width()
|
|
291
|
+
height = element_bounding_box.height()
|
|
292
|
+
area = width * height
|
|
293
|
+
|
|
294
|
+
# Is Visible Check
|
|
295
|
+
is_visible = (area > 0) and (not is_offscreen or control_type_name == 'EditControl') and is_control_element
|
|
296
|
+
|
|
297
|
+
if is_visible:
|
|
298
|
+
is_enabled = node.CachedIsEnabled
|
|
299
|
+
if is_enabled:
|
|
300
|
+
# Determine is_keyboard_focusable
|
|
301
|
+
if control_type_name in set(['EditControl','ButtonControl','CheckBoxControl','RadioButtonControl','TabItemControl']):
|
|
302
|
+
is_keyboard_focusable = True
|
|
303
|
+
else:
|
|
304
|
+
is_keyboard_focusable = node.CachedIsKeyboardFocusable
|
|
305
|
+
|
|
306
|
+
# Interactive Check
|
|
307
|
+
if interactive_nodes is not None:
|
|
308
|
+
is_interactive = False
|
|
309
|
+
if is_browser and control_type_name in set(['DataItemControl','ListItemControl']) and not is_keyboard_focusable:
|
|
310
|
+
is_interactive = False
|
|
311
|
+
elif not is_browser and control_type_name == "ImageControl" and is_keyboard_focusable:
|
|
312
|
+
is_interactive = True
|
|
313
|
+
elif control_type_name in (INTERACTIVE_CONTROL_TYPE_NAMES|DOCUMENT_CONTROL_TYPE_NAMES):
|
|
314
|
+
# Role check
|
|
315
|
+
try:
|
|
316
|
+
legacy_pattern = node.GetLegacyIAccessiblePattern()
|
|
317
|
+
is_role_interactive = AccessibleRoleNames.get(legacy_pattern.Role, "Default") in INTERACTIVE_ROLES
|
|
318
|
+
except Exception:
|
|
319
|
+
is_role_interactive = False
|
|
320
|
+
|
|
321
|
+
# Image check
|
|
322
|
+
is_image = False
|
|
323
|
+
if control_type_name == 'ImageControl': # approximated
|
|
324
|
+
localized = node.CachedLocalizedControlType
|
|
325
|
+
if localized == 'graphic' or not is_keyboard_focusable:
|
|
326
|
+
is_image = True
|
|
327
|
+
|
|
328
|
+
if is_role_interactive and (not is_image or is_keyboard_focusable):
|
|
329
|
+
is_interactive = True
|
|
330
|
+
|
|
331
|
+
elif control_type_name == 'GroupControl':
|
|
332
|
+
if is_browser:
|
|
333
|
+
try:
|
|
334
|
+
legacy_pattern = node.GetLegacyIAccessiblePattern()
|
|
335
|
+
is_role_interactive = AccessibleRoleNames.get(legacy_pattern.Role, "Default") in INTERACTIVE_ROLES
|
|
336
|
+
except Exception:
|
|
337
|
+
is_role_interactive = False
|
|
338
|
+
|
|
339
|
+
is_default_action = False
|
|
340
|
+
try:
|
|
341
|
+
legacy_pattern = node.GetLegacyIAccessiblePattern()
|
|
342
|
+
if legacy_pattern.DefaultAction.title() in DEFAULT_ACTIONS:
|
|
343
|
+
is_default_action = True
|
|
344
|
+
except: pass
|
|
345
|
+
|
|
346
|
+
if is_role_interactive and (is_default_action or is_keyboard_focusable):
|
|
347
|
+
is_interactive = True
|
|
348
|
+
|
|
349
|
+
if is_interactive:
|
|
350
|
+
legacy_pattern=node.GetLegacyIAccessiblePattern()
|
|
351
|
+
value=legacy_pattern.Value.strip() if legacy_pattern.Value is not None else ""
|
|
352
|
+
is_focused = node.CachedHasKeyboardFocus
|
|
353
|
+
name = node.CachedName.strip()
|
|
354
|
+
localized_control_type = node.CachedLocalizedControlType
|
|
355
|
+
accelerator_key = node.CachedAcceleratorKey
|
|
356
|
+
|
|
357
|
+
if is_browser and is_dom:
|
|
358
|
+
bounding_box=self.iou_bounding_box(self.dom_bounding_box,element_bounding_box)
|
|
359
|
+
center = bounding_box.get_center()
|
|
360
|
+
tree_node=TreeElementNode(**{
|
|
361
|
+
'name':name,
|
|
362
|
+
'control_type':localized_control_type.title(),
|
|
363
|
+
'value':value,
|
|
364
|
+
'shortcut':accelerator_key,
|
|
365
|
+
'bounding_box':bounding_box,
|
|
366
|
+
'center':center,
|
|
367
|
+
'xpath':'',
|
|
368
|
+
'app_name':app_name,
|
|
369
|
+
'is_focused':is_focused
|
|
370
|
+
})
|
|
371
|
+
dom_interactive_nodes.append(tree_node)
|
|
372
|
+
self._dom_correction(node, dom_interactive_nodes, app_name)
|
|
373
|
+
else:
|
|
374
|
+
bounding_box=self.iou_bounding_box(window_bounding_box,element_bounding_box)
|
|
375
|
+
center = bounding_box.get_center()
|
|
376
|
+
tree_node=TreeElementNode(**{
|
|
377
|
+
'name':name,
|
|
378
|
+
'control_type':localized_control_type.title(),
|
|
379
|
+
'value':value,
|
|
380
|
+
'shortcut':accelerator_key,
|
|
381
|
+
'bounding_box':bounding_box,
|
|
382
|
+
'center':center,
|
|
383
|
+
'xpath':'',
|
|
384
|
+
'app_name':app_name,
|
|
385
|
+
'is_focused':is_focused
|
|
386
|
+
})
|
|
387
|
+
interactive_nodes.append(tree_node)
|
|
388
|
+
|
|
389
|
+
# Informative Check
|
|
390
|
+
if dom_informative_nodes is not None:
|
|
391
|
+
# is_element_text check
|
|
392
|
+
is_text = False
|
|
393
|
+
if control_type_name in INFORMATIVE_CONTROL_TYPE_NAMES:
|
|
394
|
+
# is_element_image check
|
|
395
|
+
is_image_check = False
|
|
396
|
+
if control_type_name == 'ImageControl':
|
|
397
|
+
localized = node.CachedLocalizedControlType
|
|
398
|
+
|
|
399
|
+
# Check keybord focusable again if not established, but reuse
|
|
400
|
+
if not is_keyboard_focusable:
|
|
401
|
+
# If localized is graphic OR not focusable -> image
|
|
402
|
+
# wait, is_element_image: if localized=='graphic' or not focusable -> True
|
|
403
|
+
if localized == 'graphic':
|
|
404
|
+
is_image_check = True
|
|
405
|
+
else:
|
|
406
|
+
is_image_check = True # not focusable
|
|
407
|
+
elif localized == 'graphic':
|
|
408
|
+
is_image_check = True
|
|
409
|
+
|
|
410
|
+
if not is_image_check:
|
|
411
|
+
is_text = True
|
|
412
|
+
|
|
413
|
+
if is_text:
|
|
414
|
+
if is_browser and is_dom:
|
|
415
|
+
name = node.CachedName
|
|
416
|
+
dom_informative_nodes.append(TextElementNode(
|
|
417
|
+
text=name.strip(),
|
|
418
|
+
))
|
|
419
|
+
|
|
420
|
+
# Phase 3: Cached Children Retrieval
|
|
421
|
+
children = CachedControlHelper.get_cached_children(node, children_cache_req)
|
|
422
|
+
|
|
423
|
+
# Recursively traverse the tree the right to left for normal apps and for DOM traverse from left to right
|
|
424
|
+
for child in (children if is_dom else children[::-1]):
|
|
425
|
+
# Incrementally building the xpath
|
|
426
|
+
|
|
427
|
+
# Check if the child is a DOM element
|
|
428
|
+
if is_browser and child.CachedAutomationId=="RootWebArea":
|
|
429
|
+
bounding_box=child.CachedBoundingRectangle
|
|
430
|
+
self.dom_bounding_box=BoundingBox(left=bounding_box.left,top=bounding_box.top,
|
|
431
|
+
right=bounding_box.right,bottom=bounding_box.bottom,width=bounding_box.width(),
|
|
432
|
+
height=bounding_box.height())
|
|
433
|
+
self.dom=child
|
|
434
|
+
# enter DOM subtree
|
|
435
|
+
self.tree_traversal(child, window_bounding_box, app_name, is_browser, interactive_nodes, scrollable_nodes, dom_interactive_nodes, dom_informative_nodes, is_dom=True, is_dialog=is_dialog, element_cache_req=element_cache_req, children_cache_req=children_cache_req)
|
|
436
|
+
# Check if the child is a dialog
|
|
437
|
+
elif isinstance(child,WindowControl):
|
|
438
|
+
if not child.CachedIsOffscreen:
|
|
439
|
+
if is_dom:
|
|
440
|
+
bounding_box=child.CachedBoundingRectangle
|
|
441
|
+
if bounding_box.width() > 0.8*self.dom_bounding_box.width:
|
|
442
|
+
# Because this window element covers the majority of the screen
|
|
443
|
+
dom_interactive_nodes.clear()
|
|
444
|
+
else:
|
|
445
|
+
# Inline is_window_modal
|
|
446
|
+
is_modal = False
|
|
447
|
+
try:
|
|
448
|
+
window_pattern = child.GetWindowPattern()
|
|
449
|
+
is_modal = window_pattern.IsModal
|
|
450
|
+
except Exception:
|
|
451
|
+
pass
|
|
452
|
+
|
|
453
|
+
if is_modal:
|
|
454
|
+
# Because this window element is modal
|
|
455
|
+
interactive_nodes.clear()
|
|
456
|
+
# enter dialog subtree
|
|
457
|
+
self.tree_traversal(child, window_bounding_box, app_name, is_browser, interactive_nodes, scrollable_nodes, dom_interactive_nodes, dom_informative_nodes, is_dom=is_dom, is_dialog=True, element_cache_req=element_cache_req, children_cache_req=children_cache_req)
|
|
458
|
+
else:
|
|
459
|
+
# normal non-dialog children
|
|
460
|
+
self.tree_traversal(child, window_bounding_box, app_name, is_browser, interactive_nodes, scrollable_nodes, dom_interactive_nodes, dom_informative_nodes, is_dom=is_dom, is_dialog=is_dialog, element_cache_req=element_cache_req, children_cache_req=children_cache_req)
|
|
461
|
+
except Exception as e:
|
|
462
|
+
logger.error(f"Error in tree_traversal: {e}", exc_info=True)
|
|
463
|
+
raise
|
|
464
|
+
|
|
465
|
+
def app_name_correction(self,app_name:str)->str:
|
|
466
|
+
match app_name:
|
|
467
|
+
case "Progman":
|
|
468
|
+
return "Desktop"
|
|
469
|
+
case 'Shell_TrayWnd'|'Shell_SecondaryTrayWnd':
|
|
470
|
+
return "Taskbar"
|
|
471
|
+
case 'Microsoft.UI.Content.PopupWindowSiteBridge':
|
|
472
|
+
return "Context Menu"
|
|
473
|
+
case _:
|
|
474
|
+
return app_name
|
|
475
|
+
|
|
476
|
+
def get_nodes(self, handle: int, is_browser:bool=False, use_dom:bool=False) -> tuple[list[TreeElementNode],list[ScrollElementNode],list[TextElementNode]]:
|
|
477
|
+
try:
|
|
478
|
+
comtypes.CoInitialize()
|
|
479
|
+
# Rehydrate Control from handle within the thread's COM context
|
|
480
|
+
node = ControlFromHandle(handle)
|
|
481
|
+
if not node:
|
|
482
|
+
raise Exception("Failed to create Control from handle")
|
|
483
|
+
|
|
484
|
+
# Create fresh cache requests for this traversal session
|
|
485
|
+
element_cache_req = CacheRequestFactory.create_tree_traversal_cache()
|
|
486
|
+
element_cache_req.TreeScope = TreeScope.TreeScope_Element
|
|
487
|
+
|
|
488
|
+
children_cache_req = CacheRequestFactory.create_tree_traversal_cache()
|
|
489
|
+
children_cache_req.TreeScope = TreeScope.TreeScope_Element | TreeScope.TreeScope_Children
|
|
490
|
+
|
|
491
|
+
window_bounding_box=node.BoundingRectangle
|
|
492
|
+
|
|
493
|
+
interactive_nodes, dom_interactive_nodes, dom_informative_nodes, scrollable_nodes = [], [], [], []
|
|
494
|
+
app_name=node.Name.strip()
|
|
495
|
+
app_name=self.app_name_correction(app_name)
|
|
496
|
+
|
|
497
|
+
self.tree_traversal(node, window_bounding_box, app_name, is_browser, interactive_nodes, scrollable_nodes, dom_interactive_nodes, dom_informative_nodes, is_dom=False, is_dialog=False, element_cache_req=element_cache_req, children_cache_req=children_cache_req)
|
|
498
|
+
logger.debug(f'App name:{app_name}')
|
|
499
|
+
logger.debug(f'Interactive nodes:{len(interactive_nodes)}')
|
|
500
|
+
if is_browser:
|
|
501
|
+
logger.debug(f'DOM interactive nodes:{len(dom_interactive_nodes)}')
|
|
502
|
+
logger.debug(f'DOM informative nodes:{len(dom_informative_nodes)}')
|
|
503
|
+
logger.debug(f'Scrollable nodes:{len(scrollable_nodes)}')
|
|
504
|
+
|
|
505
|
+
if use_dom:
|
|
506
|
+
if is_browser:
|
|
507
|
+
return (dom_interactive_nodes,scrollable_nodes,dom_informative_nodes)
|
|
508
|
+
else:
|
|
509
|
+
return ([],[],[])
|
|
510
|
+
else:
|
|
511
|
+
interactive_nodes.extend(dom_interactive_nodes)
|
|
512
|
+
return (interactive_nodes,scrollable_nodes,dom_informative_nodes)
|
|
513
|
+
except Exception as e:
|
|
514
|
+
logger.error(f"Error getting nodes for {node.Name}: {e}")
|
|
515
|
+
raise e
|
|
516
|
+
finally:
|
|
517
|
+
comtypes.CoUninitialize()
|
|
518
|
+
|
|
519
|
+
def _on_focus_change(self, sender:'ctypes.POINTER(IUIAutomationElement)'):
|
|
520
|
+
"""Handle focus change events."""
|
|
521
|
+
# Debounce duplicate events
|
|
522
|
+
current_time = time()
|
|
523
|
+
element = Control.CreateControlFromElement(sender)
|
|
524
|
+
runtime_id=element.GetRuntimeId()
|
|
525
|
+
event_key = tuple(runtime_id)
|
|
526
|
+
if hasattr(self, '_last_focus_event') and self._last_focus_event:
|
|
527
|
+
last_key, last_time = self._last_focus_event
|
|
528
|
+
if last_key == event_key and (current_time - last_time) < 1.0:
|
|
529
|
+
return None
|
|
530
|
+
self._last_focus_event = (event_key, current_time)
|
|
531
|
+
|
|
532
|
+
try:
|
|
533
|
+
logger.debug(f"[WatchDog] Focus changed to: '{element.Name}' ({element.ControlTypeName})")
|
|
534
|
+
except Exception:
|
|
535
|
+
pass
|
|
536
|
+
|
|
537
|
+
def _on_property_change(self, sender:'ctypes.POINTER(IUIAutomationElement)', propertyId:int, newValue):
|
|
538
|
+
"""Handle property change events."""
|
|
539
|
+
try:
|
|
540
|
+
element = Control.CreateControlFromElement(sender)
|
|
541
|
+
logger.debug(f"[WatchDog] Property changed: ID={propertyId} Value={newValue} Element: '{element.Name}' ({element.ControlTypeName})")
|
|
542
|
+
except Exception:
|
|
543
|
+
pass
|