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/views.py
CHANGED
|
@@ -1,116 +1,142 @@
|
|
|
1
|
-
from dataclasses import dataclass,field
|
|
2
|
-
from tabulate import tabulate
|
|
3
|
-
from typing import Optional
|
|
4
|
-
|
|
5
|
-
@dataclass
|
|
6
|
-
class
|
|
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
|
-
def
|
|
50
|
-
return
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
return f'({
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
name: str
|
|
87
|
-
control_type: str
|
|
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
|
-
|
|
1
|
+
from dataclasses import dataclass,field
|
|
2
|
+
from tabulate import tabulate
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
@dataclass
|
|
6
|
+
class TreeState:
|
|
7
|
+
root_node:Optional['TreeElementNode']=None
|
|
8
|
+
dom_node:Optional['ScrollElementNode']=None
|
|
9
|
+
interactive_nodes:list['TreeElementNode']=field(default_factory=list)
|
|
10
|
+
scrollable_nodes:list['ScrollElementNode']=field(default_factory=list)
|
|
11
|
+
dom_informative_nodes:list['TextElementNode']=field(default_factory=list)
|
|
12
|
+
|
|
13
|
+
def interactive_elements_to_string(self) -> str:
|
|
14
|
+
if not self.interactive_nodes:
|
|
15
|
+
return "No interactive elements"
|
|
16
|
+
# TOON-like format: Pipe-separated values with clear header
|
|
17
|
+
# Using abbreviations in header to save tokens
|
|
18
|
+
header = "# id|app|type|name|coords|focus"
|
|
19
|
+
rows = [header]
|
|
20
|
+
for idx, node in enumerate(self.interactive_nodes):
|
|
21
|
+
row = f"{idx}|{node.app_name}|{node.control_type}|{node.name}|{node.center.to_string()}|{node.is_focused}"
|
|
22
|
+
rows.append(row)
|
|
23
|
+
return "\n".join(rows)
|
|
24
|
+
|
|
25
|
+
def scrollable_elements_to_string(self) -> str:
|
|
26
|
+
if not self.scrollable_nodes:
|
|
27
|
+
return "No scrollable elements"
|
|
28
|
+
# TOON-like format
|
|
29
|
+
header = "# id|app|type|name|coords|h_scroll|h_pct|v_scroll|v_pct|focus"
|
|
30
|
+
rows = [header]
|
|
31
|
+
base_index = len(self.interactive_nodes)
|
|
32
|
+
for idx, node in enumerate(self.scrollable_nodes):
|
|
33
|
+
row = (f"{base_index + idx}|{node.app_name}|{node.control_type}|{node.name}|"
|
|
34
|
+
f"{node.center.to_string()}|{node.horizontal_scrollable}|{node.horizontal_scroll_percent}|"
|
|
35
|
+
f"{node.vertical_scrollable}|{node.vertical_scroll_percent}|{node.is_focused}")
|
|
36
|
+
rows.append(row)
|
|
37
|
+
return "\n".join(rows)
|
|
38
|
+
|
|
39
|
+
@dataclass
|
|
40
|
+
class BoundingBox:
|
|
41
|
+
left:int
|
|
42
|
+
top:int
|
|
43
|
+
right:int
|
|
44
|
+
bottom:int
|
|
45
|
+
width:int
|
|
46
|
+
height:int
|
|
47
|
+
|
|
48
|
+
@classmethod
|
|
49
|
+
def from_bounding_rectangle(cls,bounding_rectangle:'BoundingRectangle')->'BoundingBox':
|
|
50
|
+
return cls(
|
|
51
|
+
left=bounding_rectangle.left,
|
|
52
|
+
top=bounding_rectangle.top,
|
|
53
|
+
right=bounding_rectangle.right,
|
|
54
|
+
bottom=bounding_rectangle.bottom,
|
|
55
|
+
width=bounding_rectangle.width(),
|
|
56
|
+
height=bounding_rectangle.height()
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
def get_center(self)->'Center':
|
|
60
|
+
return Center(x=self.left+self.width//2,y=self.top+self.height//2)
|
|
61
|
+
|
|
62
|
+
def xywh_to_string(self):
|
|
63
|
+
return f'({self.left},{self.top},{self.width},{self.height})'
|
|
64
|
+
|
|
65
|
+
def xyxy_to_string(self):
|
|
66
|
+
x1,y1,x2,y2=self.convert_xywh_to_xyxy()
|
|
67
|
+
return f'({x1},{y1},{x2},{y2})'
|
|
68
|
+
|
|
69
|
+
def convert_xywh_to_xyxy(self)->tuple[int,int,int,int]:
|
|
70
|
+
x1,y1=self.left,self.top
|
|
71
|
+
x2,y2=self.left+self.width,self.top+self.height
|
|
72
|
+
return x1,y1,x2,y2
|
|
73
|
+
|
|
74
|
+
@dataclass
|
|
75
|
+
class Center:
|
|
76
|
+
x:int
|
|
77
|
+
y:int
|
|
78
|
+
|
|
79
|
+
def to_string(self)->str:
|
|
80
|
+
return f'({self.x},{self.y})'
|
|
81
|
+
|
|
82
|
+
@dataclass
|
|
83
|
+
class TreeElementNode:
|
|
84
|
+
bounding_box: BoundingBox
|
|
85
|
+
center: Center
|
|
86
|
+
name: str=''
|
|
87
|
+
control_type: str=''
|
|
88
|
+
app_name: str=''
|
|
89
|
+
value:str=''
|
|
90
|
+
shortcut: str=''
|
|
91
|
+
xpath:str=''
|
|
92
|
+
is_focused:bool=False
|
|
93
|
+
|
|
94
|
+
def update_from_node(self,node:'TreeElementNode'):
|
|
95
|
+
self.name=node.name
|
|
96
|
+
self.control_type=node.control_type
|
|
97
|
+
self.app_name=node.app_name
|
|
98
|
+
self.value=node.value
|
|
99
|
+
self.shortcut=node.shortcut
|
|
100
|
+
self.bounding_box=node.bounding_box
|
|
101
|
+
self.center=node.center
|
|
102
|
+
self.xpath=node.xpath
|
|
103
|
+
self.is_focused=node.is_focused
|
|
104
|
+
|
|
105
|
+
# Legacy method kept for compatibility if needed, but not used in new format
|
|
106
|
+
def to_row(self, index: int):
|
|
107
|
+
return [index, self.app_name, self.control_type, self.name, self.value, self.shortcut, self.center.to_string(),self.is_focused]
|
|
108
|
+
|
|
109
|
+
@dataclass
|
|
110
|
+
class ScrollElementNode:
|
|
111
|
+
name: str
|
|
112
|
+
control_type: str
|
|
113
|
+
xpath:str
|
|
114
|
+
app_name: str
|
|
115
|
+
bounding_box: BoundingBox
|
|
116
|
+
center: Center
|
|
117
|
+
horizontal_scrollable: bool
|
|
118
|
+
horizontal_scroll_percent: float
|
|
119
|
+
vertical_scrollable: bool
|
|
120
|
+
vertical_scroll_percent: float
|
|
121
|
+
is_focused: bool
|
|
122
|
+
|
|
123
|
+
# Legacy method kept for compatibility
|
|
124
|
+
def to_row(self, index: int, base_index: int):
|
|
125
|
+
return [
|
|
126
|
+
base_index + index,
|
|
127
|
+
self.app_name,
|
|
128
|
+
self.control_type,
|
|
129
|
+
self.name,
|
|
130
|
+
self.center.to_string(),
|
|
131
|
+
self.horizontal_scrollable,
|
|
132
|
+
self.horizontal_scroll_percent,
|
|
133
|
+
self.vertical_scrollable,
|
|
134
|
+
self.vertical_scroll_percent,
|
|
135
|
+
self.is_focused
|
|
136
|
+
]
|
|
137
|
+
|
|
138
|
+
@dataclass
|
|
139
|
+
class TextElementNode:
|
|
140
|
+
text:str
|
|
141
|
+
|
|
142
|
+
ElementNode=TreeElementNode|ScrollElementNode|TextElementNode
|
windows_mcp/uia/controls.py
CHANGED
|
@@ -242,9 +242,10 @@ class Control():
|
|
|
242
242
|
return self.Element.CachedAutomationId
|
|
243
243
|
|
|
244
244
|
@property
|
|
245
|
-
def CachedBoundingRectangle(self) ->
|
|
245
|
+
def CachedBoundingRectangle(self) -> Rect:
|
|
246
246
|
"""Get the cached bounding rectangle."""
|
|
247
|
-
|
|
247
|
+
rect = self.Element.CachedBoundingRectangle
|
|
248
|
+
return Rect(rect.left, rect.top, rect.right, rect.bottom)
|
|
248
249
|
|
|
249
250
|
@property
|
|
250
251
|
def CachedClassName(self) -> str:
|
|
@@ -256,6 +257,14 @@ class Control():
|
|
|
256
257
|
"""Get the cached control type."""
|
|
257
258
|
return self.Element.CachedControlType
|
|
258
259
|
|
|
260
|
+
@property
|
|
261
|
+
def CachedControlTypeName(self) -> str:
|
|
262
|
+
"""Get the cached control type name."""
|
|
263
|
+
try:
|
|
264
|
+
return ControlTypeNames.get(self.CachedControlType, "Unknown")
|
|
265
|
+
except:
|
|
266
|
+
return "Unknown"
|
|
267
|
+
|
|
259
268
|
@property
|
|
260
269
|
def CachedControllerFor(self) -> Any:
|
|
261
270
|
"""Get the cached controller for."""
|
windows_mcp/uia/core.py
CHANGED
|
@@ -62,6 +62,15 @@ def WheelUp(wheelTimes: int = 1, interval: float = 0.05, waitTime: float = OPERA
|
|
|
62
62
|
def GetScreenSize() -> Tuple[int, int]:
|
|
63
63
|
return ctypes.windll.user32.GetSystemMetrics(0), ctypes.windll.user32.GetSystemMetrics(1)
|
|
64
64
|
|
|
65
|
+
def GetVirtualScreenRect() -> Tuple[int, int, int, int]:
|
|
66
|
+
"""Returns (left, top, width, height) of the virtual screen."""
|
|
67
|
+
return (
|
|
68
|
+
ctypes.windll.user32.GetSystemMetrics(76), # SM_XVIRTUALSCREEN
|
|
69
|
+
ctypes.windll.user32.GetSystemMetrics(77), # SM_YVIRTUALSCREEN
|
|
70
|
+
ctypes.windll.user32.GetSystemMetrics(78), # SM_CXVIRTUALSCREEN
|
|
71
|
+
ctypes.windll.user32.GetSystemMetrics(79) # SM_CYVIRTUALSCREEN
|
|
72
|
+
)
|
|
73
|
+
|
|
65
74
|
def IsTopLevelWindow(handle: int) -> bool:
|
|
66
75
|
return ctypes.windll.user32.GetParent(handle) == 0
|
|
67
76
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .core import *
|