PyClickerRPA 0.1.0__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.
- autoclicker/__init__.py +14 -0
- autoclicker/app.py +21 -0
- autoclicker/capture.py +94 -0
- autoclicker/clicker.py +212 -0
- autoclicker/dpi.py +30 -0
- autoclicker/gui/__init__.py +1 -0
- autoclicker/gui/flow_connection.py +91 -0
- autoclicker/gui/flow_editor.py +340 -0
- autoclicker/gui/flow_node.py +107 -0
- autoclicker/gui/hotkeys.py +40 -0
- autoclicker/gui/main_window.py +651 -0
- autoclicker/gui/monitor.py +84 -0
- autoclicker/gui/preview_label.py +176 -0
- autoclicker/gui/qt_runner.py +71 -0
- autoclicker/gui/recorder.py +163 -0
- autoclicker/gui/scheduler_widget.py +160 -0
- autoclicker/gui/snippet_library.py +117 -0
- autoclicker/gui/task_widget.py +547 -0
- autoclicker/matcher.py +152 -0
- autoclicker/models.py +468 -0
- autoclicker/ocr.py +63 -0
- autoclicker/run.py +64 -0
- autoclicker/runner.py +666 -0
- autoclicker/windows.py +53 -0
- pyclickerrpa-0.1.0.dist-info/METADATA +90 -0
- pyclickerrpa-0.1.0.dist-info/RECORD +29 -0
- pyclickerrpa-0.1.0.dist-info/WHEEL +5 -0
- pyclickerrpa-0.1.0.dist-info/entry_points.txt +5 -0
- pyclickerrpa-0.1.0.dist-info/top_level.txt +1 -0
autoclicker/__init__.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""AutoClicker — Python port of the Qt/C++ visual auto-click / RPA tool.
|
|
2
|
+
|
|
3
|
+
Phase 1 exposes the headless engine (models + capture + clicker + matcher +
|
|
4
|
+
windows + runner). The GUI (PySide6) lands in later phases.
|
|
5
|
+
"""
|
|
6
|
+
from autoclicker.models import (
|
|
7
|
+
Step, Task, StepType, UntilCondition, RetryPolicy,
|
|
8
|
+
RepeatConfig, UntilConfig, RetryConfig,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"Step", "Task", "StepType", "UntilCondition", "RetryPolicy",
|
|
13
|
+
"RepeatConfig", "UntilConfig", "RetryConfig",
|
|
14
|
+
]
|
autoclicker/app.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""GUI entry point: python -m autoclicker.app"""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def main() -> int:
|
|
8
|
+
from autoclicker.dpi import set_dpi_aware
|
|
9
|
+
set_dpi_aware()
|
|
10
|
+
|
|
11
|
+
from PySide6.QtWidgets import QApplication
|
|
12
|
+
from autoclicker.gui.main_window import MainWindow
|
|
13
|
+
|
|
14
|
+
app = QApplication(sys.argv)
|
|
15
|
+
win = MainWindow()
|
|
16
|
+
win.show()
|
|
17
|
+
return app.exec()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
if __name__ == "__main__":
|
|
21
|
+
raise SystemExit(main())
|
autoclicker/capture.py
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""Screen / window capture -> BGR numpy arrays (OpenCV order).
|
|
2
|
+
|
|
3
|
+
Port of capture.cpp: ``capture_window`` grabs a specific HWND via PrintWindow
|
|
4
|
+
(works for background windows that aren't fully occluded); ``capture_screen``
|
|
5
|
+
grabs the whole virtual desktop via GDI BitBlt. No mss dependency — win32 GDI
|
|
6
|
+
covers both.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import numpy as np
|
|
11
|
+
|
|
12
|
+
try:
|
|
13
|
+
import win32gui
|
|
14
|
+
import win32ui
|
|
15
|
+
import win32con
|
|
16
|
+
_HAS_WIN32 = True
|
|
17
|
+
except ImportError: # allow import on non-Windows for tests that mock capture
|
|
18
|
+
_HAS_WIN32 = False
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# PrintWindow flag that renders the full content (incl. DWM-composited/partly hidden)
|
|
22
|
+
PW_RENDERFULLCONTENT = 0x00000002
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _bitmap_to_bgr(bmp, width: int, height: int) -> np.ndarray:
|
|
26
|
+
"""Convert a win32ui bitmap's BGRA bits into an (H, W, 3) BGR ndarray."""
|
|
27
|
+
bits = bmp.GetBitmapBits(True)
|
|
28
|
+
arr = np.frombuffer(bits, dtype=np.uint8).reshape((height, width, 4))
|
|
29
|
+
return arr[:, :, :3].copy() # drop alpha; BGRA -> BGR
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def capture_window(hwnd) -> np.ndarray:
|
|
33
|
+
"""Capture the client area of *hwnd* as a BGR ndarray."""
|
|
34
|
+
if not _HAS_WIN32:
|
|
35
|
+
raise RuntimeError("win32 modules unavailable")
|
|
36
|
+
|
|
37
|
+
left, top, right, bottom = win32gui.GetClientRect(hwnd)
|
|
38
|
+
width, height = right - left, bottom - top
|
|
39
|
+
if width <= 0 or height <= 0:
|
|
40
|
+
return np.empty((0, 0, 3), dtype=np.uint8)
|
|
41
|
+
|
|
42
|
+
hwnd_dc = win32gui.GetWindowDC(hwnd)
|
|
43
|
+
mfc_dc = win32ui.CreateDCFromHandle(hwnd_dc)
|
|
44
|
+
save_dc = mfc_dc.CreateCompatibleDC()
|
|
45
|
+
bmp = win32ui.CreateBitmap()
|
|
46
|
+
bmp.CreateCompatibleBitmap(mfc_dc, width, height)
|
|
47
|
+
save_dc.SelectObject(bmp)
|
|
48
|
+
|
|
49
|
+
try:
|
|
50
|
+
import ctypes
|
|
51
|
+
ok = ctypes.windll.user32.PrintWindow(hwnd, save_dc.GetSafeHdc(), PW_RENDERFULLCONTENT)
|
|
52
|
+
if not ok:
|
|
53
|
+
# Fallback: BitBlt the window DC
|
|
54
|
+
save_dc.BitBlt((0, 0), (width, height), mfc_dc, (0, 0), win32con.SRCCOPY)
|
|
55
|
+
img = _bitmap_to_bgr(bmp, width, height)
|
|
56
|
+
finally:
|
|
57
|
+
win32gui.DeleteObject(bmp.GetHandle())
|
|
58
|
+
save_dc.DeleteDC()
|
|
59
|
+
mfc_dc.DeleteDC()
|
|
60
|
+
win32gui.ReleaseDC(hwnd, hwnd_dc)
|
|
61
|
+
return img
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def capture_screen() -> np.ndarray:
|
|
65
|
+
"""Capture the entire virtual desktop as a BGR ndarray."""
|
|
66
|
+
if not _HAS_WIN32:
|
|
67
|
+
raise RuntimeError("win32 modules unavailable")
|
|
68
|
+
|
|
69
|
+
x = win32api_GetSystemMetrics(76) # SM_XVIRTUALSCREEN
|
|
70
|
+
y = win32api_GetSystemMetrics(77) # SM_YVIRTUALSCREEN
|
|
71
|
+
width = win32api_GetSystemMetrics(78) # SM_CXVIRTUALSCREEN
|
|
72
|
+
height = win32api_GetSystemMetrics(79) # SM_CYVIRTUALSCREEN
|
|
73
|
+
|
|
74
|
+
hdesktop = win32gui.GetDesktopWindow()
|
|
75
|
+
desktop_dc = win32gui.GetWindowDC(hdesktop)
|
|
76
|
+
mfc_dc = win32ui.CreateDCFromHandle(desktop_dc)
|
|
77
|
+
save_dc = mfc_dc.CreateCompatibleDC()
|
|
78
|
+
bmp = win32ui.CreateBitmap()
|
|
79
|
+
bmp.CreateCompatibleBitmap(mfc_dc, width, height)
|
|
80
|
+
save_dc.SelectObject(bmp)
|
|
81
|
+
try:
|
|
82
|
+
save_dc.BitBlt((0, 0), (width, height), mfc_dc, (x, y), win32con.SRCCOPY)
|
|
83
|
+
img = _bitmap_to_bgr(bmp, width, height)
|
|
84
|
+
finally:
|
|
85
|
+
win32gui.DeleteObject(bmp.GetHandle())
|
|
86
|
+
save_dc.DeleteDC()
|
|
87
|
+
mfc_dc.DeleteDC()
|
|
88
|
+
win32gui.ReleaseDC(hdesktop, desktop_dc)
|
|
89
|
+
return img
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def win32api_GetSystemMetrics(index: int) -> int:
|
|
93
|
+
import win32api
|
|
94
|
+
return win32api.GetSystemMetrics(index)
|
autoclicker/clicker.py
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
"""Mouse / keyboard synthesis via Win32 SendInput — port of clicker.cpp.
|
|
2
|
+
|
|
3
|
+
Window-local coordinates are converted to screen coords (ClientToScreen) then
|
|
4
|
+
normalised to the 0..65535 absolute range over the primary screen, matching the
|
|
5
|
+
C++ implementation exactly (incl. its single-monitor assumption).
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import ctypes
|
|
10
|
+
import time
|
|
11
|
+
|
|
12
|
+
try:
|
|
13
|
+
import win32gui
|
|
14
|
+
import win32con
|
|
15
|
+
_HAS_WIN32 = True
|
|
16
|
+
except ImportError:
|
|
17
|
+
_HAS_WIN32 = False
|
|
18
|
+
|
|
19
|
+
WHEEL_DELTA = 120
|
|
20
|
+
|
|
21
|
+
# ---- ctypes SendInput plumbing ------------------------------------------------
|
|
22
|
+
if _HAS_WIN32:
|
|
23
|
+
from ctypes import wintypes
|
|
24
|
+
|
|
25
|
+
ULONG_PTR = ctypes.wintypes.WPARAM
|
|
26
|
+
|
|
27
|
+
class _MOUSEINPUT(ctypes.Structure):
|
|
28
|
+
_fields_ = [("dx", wintypes.LONG), ("dy", wintypes.LONG),
|
|
29
|
+
("mouseData", wintypes.DWORD), ("dwFlags", wintypes.DWORD),
|
|
30
|
+
("time", wintypes.DWORD), ("dwExtraInfo", ULONG_PTR)]
|
|
31
|
+
|
|
32
|
+
class _KEYBDINPUT(ctypes.Structure):
|
|
33
|
+
_fields_ = [("wVk", wintypes.WORD), ("wScan", wintypes.WORD),
|
|
34
|
+
("dwFlags", wintypes.DWORD), ("time", wintypes.DWORD),
|
|
35
|
+
("dwExtraInfo", ULONG_PTR)]
|
|
36
|
+
|
|
37
|
+
class _INPUTunion(ctypes.Union):
|
|
38
|
+
_fields_ = [("mi", _MOUSEINPUT), ("ki", _KEYBDINPUT)]
|
|
39
|
+
|
|
40
|
+
class _INPUT(ctypes.Structure):
|
|
41
|
+
_fields_ = [("type", wintypes.DWORD), ("u", _INPUTunion)]
|
|
42
|
+
|
|
43
|
+
INPUT_MOUSE, INPUT_KEYBOARD = 0, 1
|
|
44
|
+
MOUSEEVENTF_MOVE, MOUSEEVENTF_ABSOLUTE = 0x0001, 0x8000
|
|
45
|
+
MOUSEEVENTF_LEFTDOWN, MOUSEEVENTF_LEFTUP = 0x0002, 0x0004
|
|
46
|
+
MOUSEEVENTF_RIGHTDOWN, MOUSEEVENTF_RIGHTUP = 0x0008, 0x0010
|
|
47
|
+
MOUSEEVENTF_WHEEL = 0x0800
|
|
48
|
+
MOUSEEVENTF_VIRTUALDESK = 0x4000
|
|
49
|
+
KEYEVENTF_KEYUP, KEYEVENTF_UNICODE = 0x0002, 0x0004
|
|
50
|
+
SM_CXSCREEN, SM_CYSCREEN = 0, 1
|
|
51
|
+
SM_XVIRTUALSCREEN, SM_YVIRTUALSCREEN = 76, 77
|
|
52
|
+
SM_CXVIRTUALSCREEN, SM_CYVIRTUALSCREEN = 78, 79
|
|
53
|
+
# Absolute move across the whole virtual desktop (multi-monitor correct)
|
|
54
|
+
_ABS_MOVE = MOUSEEVENTF_MOVE | MOUSEEVENTF_ABSOLUTE | MOUSEEVENTF_VIRTUALDESK
|
|
55
|
+
|
|
56
|
+
def _send(*inputs: _INPUT) -> None:
|
|
57
|
+
n = len(inputs)
|
|
58
|
+
arr = (_INPUT * n)(*inputs)
|
|
59
|
+
ctypes.windll.user32.SendInput(n, arr, ctypes.sizeof(_INPUT))
|
|
60
|
+
|
|
61
|
+
def _mouse(dx=0, dy=0, data=0, flags=0) -> _INPUT:
|
|
62
|
+
return _INPUT(type=INPUT_MOUSE, u=_INPUTunion(
|
|
63
|
+
mi=_MOUSEINPUT(dx, dy, data & 0xFFFFFFFF, flags, 0, 0)))
|
|
64
|
+
|
|
65
|
+
def _kbd(vk=0, scan=0, flags=0) -> _INPUT:
|
|
66
|
+
return _INPUT(type=INPUT_KEYBOARD, u=_INPUTunion(
|
|
67
|
+
ki=_KEYBDINPUT(vk, scan, flags, 0, 0)))
|
|
68
|
+
|
|
69
|
+
def _metric(i: int) -> int:
|
|
70
|
+
return ctypes.windll.user32.GetSystemMetrics(i)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _to_screen(hwnd, x: int, y: int) -> tuple[int, int]:
|
|
74
|
+
if hwnd:
|
|
75
|
+
return win32gui.ClientToScreen(hwnd, (x, y))
|
|
76
|
+
return x, y
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _norm(sx: int, sy: int) -> tuple[int, int]:
|
|
80
|
+
# Normalize to 0..65535 over the whole virtual desktop (all monitors),
|
|
81
|
+
# to be paired with MOUSEEVENTF_VIRTUALDESK. Fixes the primary-screen-only
|
|
82
|
+
# assumption that misplaced clicks on multi-monitor / scaled setups.
|
|
83
|
+
x0, y0 = _metric(SM_XVIRTUALSCREEN), _metric(SM_YVIRTUALSCREEN)
|
|
84
|
+
cx = _metric(SM_CXVIRTUALSCREEN) or 1
|
|
85
|
+
cy = _metric(SM_CYVIRTUALSCREEN) or 1
|
|
86
|
+
nx = int((sx - x0) * 65535.0 / cx)
|
|
87
|
+
ny = int((sy - y0) * 65535.0 / cy)
|
|
88
|
+
return nx, ny
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class Clicker:
|
|
92
|
+
"""Thin, injectable wrapper over Win32 input synthesis."""
|
|
93
|
+
|
|
94
|
+
# ---- mouse ----
|
|
95
|
+
def click(self, hwnd, x: int, y: int) -> None:
|
|
96
|
+
sx, sy = _to_screen(hwnd, x, y)
|
|
97
|
+
nx, ny = _norm(sx, sy)
|
|
98
|
+
_send(_mouse(nx, ny, flags=_ABS_MOVE),
|
|
99
|
+
_mouse(flags=MOUSEEVENTF_LEFTDOWN),
|
|
100
|
+
_mouse(flags=MOUSEEVENTF_LEFTUP))
|
|
101
|
+
|
|
102
|
+
def double_click(self, hwnd, x: int, y: int) -> None:
|
|
103
|
+
self.click(hwnd, x, y)
|
|
104
|
+
time.sleep(0.05)
|
|
105
|
+
self.click(hwnd, x, y)
|
|
106
|
+
|
|
107
|
+
def right_click(self, hwnd, x: int, y: int) -> None:
|
|
108
|
+
sx, sy = _to_screen(hwnd, x, y)
|
|
109
|
+
nx, ny = _norm(sx, sy)
|
|
110
|
+
_send(_mouse(nx, ny, flags=_ABS_MOVE),
|
|
111
|
+
_mouse(flags=MOUSEEVENTF_RIGHTDOWN),
|
|
112
|
+
_mouse(flags=MOUSEEVENTF_RIGHTUP))
|
|
113
|
+
|
|
114
|
+
def long_press(self, hwnd, x: int, y: int, hold_ms: int) -> None:
|
|
115
|
+
sx, sy = _to_screen(hwnd, x, y)
|
|
116
|
+
nx, ny = _norm(sx, sy)
|
|
117
|
+
_send(_mouse(nx, ny, flags=_ABS_MOVE))
|
|
118
|
+
_send(_mouse(flags=MOUSEEVENTF_LEFTDOWN))
|
|
119
|
+
time.sleep(hold_ms / 1000.0)
|
|
120
|
+
_send(_mouse(flags=MOUSEEVENTF_LEFTUP))
|
|
121
|
+
|
|
122
|
+
def swipe(self, hwnd, x1, y1, x2, y2, duration_ms: int = 500) -> None:
|
|
123
|
+
steps = 20
|
|
124
|
+
step_delay = duration_ms / steps / 1000.0
|
|
125
|
+
sx1, sy1 = _to_screen(hwnd, x1, y1)
|
|
126
|
+
sx2, sy2 = _to_screen(hwnd, x2, y2)
|
|
127
|
+
nx, ny = _norm(sx1, sy1)
|
|
128
|
+
_send(_mouse(nx, ny, flags=_ABS_MOVE))
|
|
129
|
+
_send(_mouse(flags=MOUSEEVENTF_LEFTDOWN))
|
|
130
|
+
for i in range(1, steps + 1):
|
|
131
|
+
cx = sx1 + (sx2 - sx1) * i // steps
|
|
132
|
+
cy = sy1 + (sy2 - sy1) * i // steps
|
|
133
|
+
mx, my = _norm(cx, cy)
|
|
134
|
+
_send(_mouse(mx, my, flags=_ABS_MOVE))
|
|
135
|
+
time.sleep(step_delay)
|
|
136
|
+
_send(_mouse(flags=MOUSEEVENTF_LEFTUP))
|
|
137
|
+
|
|
138
|
+
def drag(self, hwnd, x1, y1, x2, y2, duration_ms: int = 300) -> None:
|
|
139
|
+
self.swipe(hwnd, x1, y1, x2, y2, duration_ms)
|
|
140
|
+
|
|
141
|
+
def pinch(self, hwnd, cx, cy, start_dist, end_dist, duration_ms: int = 400) -> None:
|
|
142
|
+
sx, sy = _to_screen(hwnd, cx, cy)
|
|
143
|
+
nx, ny = _norm(sx, sy)
|
|
144
|
+
_send(_mouse(nx, ny, flags=_ABS_MOVE))
|
|
145
|
+
steps = 10
|
|
146
|
+
step_delay = duration_ms / steps / 1000.0
|
|
147
|
+
scroll_per_step = ((end_dist - start_dist) * WHEEL_DELTA) // (steps * 50)
|
|
148
|
+
for _ in range(steps):
|
|
149
|
+
_send(_mouse(data=scroll_per_step, flags=MOUSEEVENTF_WHEEL))
|
|
150
|
+
time.sleep(step_delay)
|
|
151
|
+
|
|
152
|
+
def scroll(self, hwnd, x, y, amount: int) -> None:
|
|
153
|
+
sx, sy = _to_screen(hwnd, x, y)
|
|
154
|
+
nx, ny = _norm(sx, sy)
|
|
155
|
+
_send(_mouse(nx, ny, flags=_ABS_MOVE))
|
|
156
|
+
_send(_mouse(data=amount * WHEEL_DELTA, flags=MOUSEEVENTF_WHEEL))
|
|
157
|
+
|
|
158
|
+
# ---- keyboard ----
|
|
159
|
+
def key_down(self, vk: int) -> None:
|
|
160
|
+
_send(_kbd(vk=vk))
|
|
161
|
+
|
|
162
|
+
def key_up(self, vk: int) -> None:
|
|
163
|
+
_send(_kbd(vk=vk, flags=KEYEVENTF_KEYUP))
|
|
164
|
+
|
|
165
|
+
def key_press(self, vk: int) -> None:
|
|
166
|
+
self.key_down(vk)
|
|
167
|
+
time.sleep(0.03)
|
|
168
|
+
self.key_up(vk)
|
|
169
|
+
|
|
170
|
+
def type_text(self, text: str) -> None:
|
|
171
|
+
for ch in text:
|
|
172
|
+
code = ord(ch)
|
|
173
|
+
_send(_kbd(scan=code, flags=KEYEVENTF_UNICODE),
|
|
174
|
+
_kbd(scan=code, flags=KEYEVENTF_UNICODE | KEYEVENTF_KEYUP))
|
|
175
|
+
time.sleep(0.02)
|
|
176
|
+
|
|
177
|
+
def hotkey(self, modifier: int, vk: int) -> None:
|
|
178
|
+
if modifier:
|
|
179
|
+
self.key_down(modifier)
|
|
180
|
+
self.key_press(vk)
|
|
181
|
+
if modifier:
|
|
182
|
+
self.key_up(modifier)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
# ---- vk parsing (port of Clicker::parseVkCode) --------------------------------
|
|
186
|
+
_NAMED_VK = {
|
|
187
|
+
"ENTER": 0x0D, "RETURN": 0x0D, "ESC": 0x1B, "ESCAPE": 0x1B, "TAB": 0x09,
|
|
188
|
+
"SPACE": 0x20, "BACKSPACE": 0x08, "BS": 0x08, "DELETE": 0x2E, "DEL": 0x2E,
|
|
189
|
+
"UP": 0x26, "DOWN": 0x28, "LEFT": 0x25, "RIGHT": 0x27, "HOME": 0x24, "END": 0x23,
|
|
190
|
+
"PAGEUP": 0x21, "PGUP": 0x21, "PAGEDOWN": 0x22, "PGDN": 0x22,
|
|
191
|
+
"CTRL": 0x11, "ALT": 0x12, "SHIFT": 0x10, "WIN": 0x5B,
|
|
192
|
+
}
|
|
193
|
+
_VK_F1 = 0x70
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def parse_vk(key_name: str) -> int:
|
|
197
|
+
"""Map a key name to a virtual-key code (0 if unknown)."""
|
|
198
|
+
k = (key_name or "").upper().strip()
|
|
199
|
+
if k in _NAMED_VK:
|
|
200
|
+
return _NAMED_VK[k]
|
|
201
|
+
if k.startswith("F") and len(k) <= 3:
|
|
202
|
+
try:
|
|
203
|
+
n = int(k[1:])
|
|
204
|
+
except ValueError:
|
|
205
|
+
n = 0
|
|
206
|
+
if 1 <= n <= 24:
|
|
207
|
+
return _VK_F1 + n - 1
|
|
208
|
+
if len(k) == 1:
|
|
209
|
+
c = k[0]
|
|
210
|
+
if "A" <= c <= "Z" or "0" <= c <= "9":
|
|
211
|
+
return ord(c)
|
|
212
|
+
return 0
|
autoclicker/dpi.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Process DPI awareness.
|
|
2
|
+
|
|
3
|
+
Making the process per-monitor DPI-aware keeps window coordinates, screenshots
|
|
4
|
+
and synthesized clicks in the same physical-pixel space on scaled displays
|
|
5
|
+
(>100% scaling), avoiding offset clicks. Call once at startup.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def set_dpi_aware() -> None:
|
|
13
|
+
if os.name != "nt":
|
|
14
|
+
return
|
|
15
|
+
import ctypes
|
|
16
|
+
# PER_MONITOR_AWARE_V2 (Win10 1703+) -> PER_MONITOR (Win8.1) -> system-DPI (legacy)
|
|
17
|
+
try:
|
|
18
|
+
ctypes.windll.user32.SetProcessDpiAwarenessContext(ctypes.c_void_p(-4))
|
|
19
|
+
return
|
|
20
|
+
except Exception:
|
|
21
|
+
pass
|
|
22
|
+
try:
|
|
23
|
+
ctypes.windll.shcore.SetProcessDpiAwareness(2)
|
|
24
|
+
return
|
|
25
|
+
except Exception:
|
|
26
|
+
pass
|
|
27
|
+
try:
|
|
28
|
+
ctypes.windll.user32.SetProcessDPIAware()
|
|
29
|
+
except Exception:
|
|
30
|
+
pass
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""PySide6 GUI for AutoClicker (Phase 2+)."""
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""Flow graph connection (edge) — port of flowconnection.cpp.
|
|
2
|
+
|
|
3
|
+
Forward edges are drawn as downward bezier curves; back-edges (target above
|
|
4
|
+
source) are dashed loop arrows and carry an optional repeat-count label.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import math
|
|
9
|
+
|
|
10
|
+
from PySide6.QtCore import QPointF, Qt
|
|
11
|
+
from PySide6.QtGui import QColor, QFont, QPainterPath, QPen, QPolygonF
|
|
12
|
+
from PySide6.QtWidgets import QGraphicsItem, QGraphicsPathItem, QGraphicsTextItem
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class FlowConnection(QGraphicsPathItem):
|
|
16
|
+
def __init__(self, from_port, to_port, parent=None):
|
|
17
|
+
super().__init__(parent)
|
|
18
|
+
self.from_port = from_port
|
|
19
|
+
self.to_port = to_port
|
|
20
|
+
self.repeat_count = 1
|
|
21
|
+
self.setPen(QPen(QColor(200, 200, 200), 2))
|
|
22
|
+
self.setFlag(QGraphicsItem.ItemIsSelectable)
|
|
23
|
+
self.setZValue(-1)
|
|
24
|
+
|
|
25
|
+
from_port.connections.append(self)
|
|
26
|
+
to_port.connections.append(self)
|
|
27
|
+
|
|
28
|
+
self._label = QGraphicsTextItem(self)
|
|
29
|
+
self._label.setDefaultTextColor(QColor(255, 200, 50))
|
|
30
|
+
self._label.setFont(QFont("Consolas", 9, QFont.Bold))
|
|
31
|
+
self._label.setVisible(False)
|
|
32
|
+
|
|
33
|
+
self.update_path()
|
|
34
|
+
|
|
35
|
+
def detach(self):
|
|
36
|
+
if self in self.from_port.connections:
|
|
37
|
+
self.from_port.connections.remove(self)
|
|
38
|
+
if self in self.to_port.connections:
|
|
39
|
+
self.to_port.connections.remove(self)
|
|
40
|
+
|
|
41
|
+
def set_repeat_count(self, count: int):
|
|
42
|
+
self.repeat_count = count
|
|
43
|
+
if count > 1 or count < 0:
|
|
44
|
+
self._label.setPlainText("∞" if count < 0 else f"×{count}")
|
|
45
|
+
self._label.setVisible(True)
|
|
46
|
+
else:
|
|
47
|
+
self._label.setVisible(False)
|
|
48
|
+
self.update_path()
|
|
49
|
+
|
|
50
|
+
def update_path(self):
|
|
51
|
+
start = self.from_port.center_scene_pos()
|
|
52
|
+
end = self.to_port.center_scene_pos()
|
|
53
|
+
path = QPainterPath()
|
|
54
|
+
path.moveTo(start)
|
|
55
|
+
|
|
56
|
+
if end.y() < start.y(): # back-edge (loop)
|
|
57
|
+
offset = 40 + abs(start.y() - end.y()) * 0.15
|
|
58
|
+
mid_x = min(start.x(), end.x()) - offset
|
|
59
|
+
path.cubicTo(start.x() - offset * 0.5, start.y() + 30,
|
|
60
|
+
mid_x, (start.y() + end.y()) / 2,
|
|
61
|
+
end.x() - offset * 0.5, end.y() - 30)
|
|
62
|
+
path.lineTo(end)
|
|
63
|
+
self.setPen(QPen(QColor(234, 67, 53), 2, Qt.DashLine))
|
|
64
|
+
else:
|
|
65
|
+
dy = end.y() - start.y()
|
|
66
|
+
ctrl = min(dy * 0.5, 80.0)
|
|
67
|
+
path.cubicTo(start.x(), start.y() + ctrl, end.x(), end.y() - ctrl, end.x(), end.y())
|
|
68
|
+
self.setPen(QPen(QColor(200, 200, 200), 2))
|
|
69
|
+
|
|
70
|
+
self.setPath(path)
|
|
71
|
+
mid = path.pointAtPercent(0.5)
|
|
72
|
+
self._label.setPos(mid + QPointF(5, -10))
|
|
73
|
+
|
|
74
|
+
def paint(self, painter, option, widget=None):
|
|
75
|
+
super().paint(painter, option, widget)
|
|
76
|
+
p = self.path()
|
|
77
|
+
if p.isEmpty():
|
|
78
|
+
return
|
|
79
|
+
before = p.pointAtPercent(0.95)
|
|
80
|
+
tip = p.pointAtPercent(1.0)
|
|
81
|
+
angle = math.atan2(tip.y() - before.y(), tip.x() - before.x())
|
|
82
|
+
size = 10
|
|
83
|
+
p1 = tip - QPointF(math.cos(angle - math.pi / 6) * size, math.sin(angle - math.pi / 6) * size)
|
|
84
|
+
p2 = tip - QPointF(math.cos(angle + math.pi / 6) * size, math.sin(angle + math.pi / 6) * size)
|
|
85
|
+
painter.setPen(Qt.NoPen)
|
|
86
|
+
painter.setBrush(self.pen().color())
|
|
87
|
+
painter.drawPolygon(QPolygonF([tip, p1, p2]))
|
|
88
|
+
if self.isSelected():
|
|
89
|
+
painter.setPen(QPen(Qt.yellow, 2, Qt.DotLine))
|
|
90
|
+
painter.setBrush(Qt.NoBrush)
|
|
91
|
+
painter.drawPath(p)
|