PyClickerRPA 0.3.0__tar.gz → 0.4.0__tar.gz
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.
- {pyclickerrpa-0.3.0 → pyclickerrpa-0.4.0}/PKG-INFO +25 -4
- {pyclickerrpa-0.3.0 → pyclickerrpa-0.4.0}/PyClickerRPA.egg-info/PKG-INFO +25 -4
- {pyclickerrpa-0.3.0 → pyclickerrpa-0.4.0}/PyClickerRPA.egg-info/SOURCES.txt +4 -0
- {pyclickerrpa-0.3.0 → pyclickerrpa-0.4.0}/README.md +23 -2
- pyclickerrpa-0.4.0/autoclicker/expr.py +94 -0
- {pyclickerrpa-0.3.0 → pyclickerrpa-0.4.0}/autoclicker/gui/flow_editor.py +60 -3
- {pyclickerrpa-0.3.0 → pyclickerrpa-0.4.0}/autoclicker/gui/main_window.py +115 -8
- {pyclickerrpa-0.3.0 → pyclickerrpa-0.4.0}/autoclicker/gui/qt_runner.py +16 -1
- pyclickerrpa-0.4.0/autoclicker/gui/recorder.py +290 -0
- {pyclickerrpa-0.3.0 → pyclickerrpa-0.4.0}/autoclicker/gui/task_widget.py +86 -18
- {pyclickerrpa-0.3.0 → pyclickerrpa-0.4.0}/autoclicker/models.py +3 -0
- {pyclickerrpa-0.3.0 → pyclickerrpa-0.4.0}/autoclicker/runner.py +102 -1
- pyclickerrpa-0.4.0/autoclicker/templatize.py +119 -0
- {pyclickerrpa-0.3.0 → pyclickerrpa-0.4.0}/pyproject.toml +2 -2
- pyclickerrpa-0.4.0/tests/test_recorder_gui.py +321 -0
- pyclickerrpa-0.4.0/tests/test_templatize.py +133 -0
- pyclickerrpa-0.3.0/autoclicker/gui/recorder.py +0 -163
- {pyclickerrpa-0.3.0 → pyclickerrpa-0.4.0}/LICENSE +0 -0
- {pyclickerrpa-0.3.0 → pyclickerrpa-0.4.0}/PyClickerRPA.egg-info/dependency_links.txt +0 -0
- {pyclickerrpa-0.3.0 → pyclickerrpa-0.4.0}/PyClickerRPA.egg-info/entry_points.txt +0 -0
- {pyclickerrpa-0.3.0 → pyclickerrpa-0.4.0}/PyClickerRPA.egg-info/requires.txt +0 -0
- {pyclickerrpa-0.3.0 → pyclickerrpa-0.4.0}/PyClickerRPA.egg-info/top_level.txt +0 -0
- {pyclickerrpa-0.3.0 → pyclickerrpa-0.4.0}/autoclicker/__init__.py +0 -0
- {pyclickerrpa-0.3.0 → pyclickerrpa-0.4.0}/autoclicker/adb.py +0 -0
- {pyclickerrpa-0.3.0 → pyclickerrpa-0.4.0}/autoclicker/app.py +0 -0
- {pyclickerrpa-0.3.0 → pyclickerrpa-0.4.0}/autoclicker/capture.py +0 -0
- {pyclickerrpa-0.3.0 → pyclickerrpa-0.4.0}/autoclicker/clicker.py +0 -0
- {pyclickerrpa-0.3.0 → pyclickerrpa-0.4.0}/autoclicker/dpi.py +0 -0
- {pyclickerrpa-0.3.0 → pyclickerrpa-0.4.0}/autoclicker/gui/__init__.py +0 -0
- {pyclickerrpa-0.3.0 → pyclickerrpa-0.4.0}/autoclicker/gui/flow_connection.py +0 -0
- {pyclickerrpa-0.3.0 → pyclickerrpa-0.4.0}/autoclicker/gui/flow_node.py +0 -0
- {pyclickerrpa-0.3.0 → pyclickerrpa-0.4.0}/autoclicker/gui/hotkeys.py +0 -0
- {pyclickerrpa-0.3.0 → pyclickerrpa-0.4.0}/autoclicker/gui/monitor.py +0 -0
- {pyclickerrpa-0.3.0 → pyclickerrpa-0.4.0}/autoclicker/gui/preview_label.py +0 -0
- {pyclickerrpa-0.3.0 → pyclickerrpa-0.4.0}/autoclicker/gui/scheduler_widget.py +0 -0
- {pyclickerrpa-0.3.0 → pyclickerrpa-0.4.0}/autoclicker/gui/snippet_library.py +0 -0
- {pyclickerrpa-0.3.0 → pyclickerrpa-0.4.0}/autoclicker/gui/testkit_widget.py +0 -0
- {pyclickerrpa-0.3.0 → pyclickerrpa-0.4.0}/autoclicker/matcher.py +0 -0
- {pyclickerrpa-0.3.0 → pyclickerrpa-0.4.0}/autoclicker/ocr.py +0 -0
- {pyclickerrpa-0.3.0 → pyclickerrpa-0.4.0}/autoclicker/run.py +0 -0
- {pyclickerrpa-0.3.0 → pyclickerrpa-0.4.0}/autoclicker/testkit/__init__.py +0 -0
- {pyclickerrpa-0.3.0 → pyclickerrpa-0.4.0}/autoclicker/testkit/__main__.py +0 -0
- {pyclickerrpa-0.3.0 → pyclickerrpa-0.4.0}/autoclicker/testkit/assertions.py +0 -0
- {pyclickerrpa-0.3.0 → pyclickerrpa-0.4.0}/autoclicker/testkit/case.py +0 -0
- {pyclickerrpa-0.3.0 → pyclickerrpa-0.4.0}/autoclicker/testkit/loader.py +0 -0
- {pyclickerrpa-0.3.0 → pyclickerrpa-0.4.0}/autoclicker/testkit/pytest_plugin.py +0 -0
- {pyclickerrpa-0.3.0 → pyclickerrpa-0.4.0}/autoclicker/testkit/report.py +0 -0
- {pyclickerrpa-0.3.0 → pyclickerrpa-0.4.0}/autoclicker/testkit/runner.py +0 -0
- {pyclickerrpa-0.3.0 → pyclickerrpa-0.4.0}/autoclicker/testkit/visual.py +0 -0
- {pyclickerrpa-0.3.0 → pyclickerrpa-0.4.0}/autoclicker/web/__init__.py +0 -0
- {pyclickerrpa-0.3.0 → pyclickerrpa-0.4.0}/autoclicker/web/__main__.py +0 -0
- {pyclickerrpa-0.3.0 → pyclickerrpa-0.4.0}/autoclicker/web/run_manager.py +0 -0
- {pyclickerrpa-0.3.0 → pyclickerrpa-0.4.0}/autoclicker/web/scheduler.py +0 -0
- {pyclickerrpa-0.3.0 → pyclickerrpa-0.4.0}/autoclicker/web/serialize.py +0 -0
- {pyclickerrpa-0.3.0 → pyclickerrpa-0.4.0}/autoclicker/web/server.py +0 -0
- {pyclickerrpa-0.3.0 → pyclickerrpa-0.4.0}/autoclicker/web/static/app.js +0 -0
- {pyclickerrpa-0.3.0 → pyclickerrpa-0.4.0}/autoclicker/web/static/index.html +0 -0
- {pyclickerrpa-0.3.0 → pyclickerrpa-0.4.0}/autoclicker/web/static/style.css +0 -0
- {pyclickerrpa-0.3.0 → pyclickerrpa-0.4.0}/autoclicker/windows.py +0 -0
- {pyclickerrpa-0.3.0 → pyclickerrpa-0.4.0}/setup.cfg +0 -0
- {pyclickerrpa-0.3.0 → pyclickerrpa-0.4.0}/tests/test_adb.py +0 -0
- {pyclickerrpa-0.3.0 → pyclickerrpa-0.4.0}/tests/test_flow_branch.py +0 -0
- {pyclickerrpa-0.3.0 → pyclickerrpa-0.4.0}/tests/test_matcher.py +0 -0
- {pyclickerrpa-0.3.0 → pyclickerrpa-0.4.0}/tests/test_models.py +0 -0
- {pyclickerrpa-0.3.0 → pyclickerrpa-0.4.0}/tests/test_runner.py +0 -0
- {pyclickerrpa-0.3.0 → pyclickerrpa-0.4.0}/tests/test_testkit.py +0 -0
- {pyclickerrpa-0.3.0 → pyclickerrpa-0.4.0}/tests/test_testkit_gui.py +0 -0
- {pyclickerrpa-0.3.0 → pyclickerrpa-0.4.0}/tests/test_web.py +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: PyClickerRPA
|
|
3
|
-
Version: 0.
|
|
4
|
-
Summary: Visual UI automation & testing tool: Windows + Android(ADB) automation, robust/self-healing image matching, OCR, node flow editor, recorder, a visual test framework (assertions, visual regression, HTML/JUnit reports) and a web console.
|
|
3
|
+
Version: 0.4.0
|
|
4
|
+
Summary: Visual UI automation & testing tool: Windows + Android(ADB) automation, robust/self-healing image matching, OCR, node flow editor, a smart recorder that records images instead of coordinates, a visual test framework (assertions, visual regression, HTML/JUnit reports) and a web console.
|
|
5
5
|
Author: Equinox
|
|
6
6
|
License-Expression: MIT
|
|
7
7
|
Project-URL: Homepage, https://github.com/Equinox/PyClickerRPA
|
|
@@ -82,7 +82,7 @@ autoclicker-web --suites-dir examples # web console at http://127.0.0.1
|
|
|
82
82
|
- **Smart wait**: `wait_stable` waits until the screen (or an ROI) stops
|
|
83
83
|
changing — replaces guessed `sleep`s (fast when ready, patient when slow).
|
|
84
84
|
- **~30+ step types**: click image/text/position, click-all, relative/offset
|
|
85
|
-
clicks, wait / random / wait-stable, drag / long-press / pinch / scroll,
|
|
85
|
+
clicks, wait / random / wait-stable, drag / **drag-from-image** / long-press / pinch / scroll,
|
|
86
86
|
key / type / hotkey, loop / goto / while / do-until, if image/text/color/
|
|
87
87
|
count/var with **true/false branching** (if-else), set-var, read-text→variable,
|
|
88
88
|
activate-window, HTTP request, run-Python-script, call sub-task, groups, and a
|
|
@@ -93,12 +93,33 @@ autoclicker-web --suites-dir examples # web console at http://127.0.0.1
|
|
|
93
93
|
Drag-and-drop **node flow graph** (condition nodes have **true/false branch
|
|
94
94
|
ports** for visual if-else; a terminal **stop** node ends the flow) and an
|
|
95
95
|
interoperable **step list** with a full
|
|
96
|
-
per-step dialog;
|
|
96
|
+
per-step dialog; snippet library, task
|
|
97
97
|
packages (`.zip` with templates), on-preview coordinate/color picking, template
|
|
98
98
|
crop, single-step test run; global hotkeys (F6 run/stop, ESC stop), scheduler,
|
|
99
99
|
and an image/color **monitor/trigger** with tray icon; a **Test** tab to load and
|
|
100
100
|
run visual test suites.
|
|
101
101
|
|
|
102
|
+
### Smart recorder
|
|
103
|
+
|
|
104
|
+
The action **recorder** (global hooks) doesn't just replay coordinates and
|
|
105
|
+
sleeps — with **智能录制 / smart mode** on (default) it records what things *look
|
|
106
|
+
like* instead of where they *were*:
|
|
107
|
+
|
|
108
|
+
- **Clicks become image steps.** A patch is cropped around the click and grown
|
|
109
|
+
(48 → 192 px) until it is both textured enough and **unique on screen**, then
|
|
110
|
+
emitted as a `click_image` step — so the flow still works after the window
|
|
111
|
+
moves or the resolution changes. Identical patches are de-duplicated by
|
|
112
|
+
content hash.
|
|
113
|
+
- **Drags become `drag_image`.** The drag's *start* is anchored to an image and
|
|
114
|
+
the *displacement* is stored, so start and end move together.
|
|
115
|
+
- **Pauses become `wait_stable`.** A long pause is only converted if the
|
|
116
|
+
recorder actually **observed the screen settle** during it; on a permanently
|
|
117
|
+
animating screen it keeps the fixed wait (a `wait_stable` that never settles
|
|
118
|
+
would fail the run).
|
|
119
|
+
|
|
120
|
+
Anything that can't be made robust falls back to the plain coordinate/fixed-wait
|
|
121
|
+
step and says why in the log.
|
|
122
|
+
|
|
102
123
|
## Visual test framework (`autoclicker.testkit`)
|
|
103
124
|
|
|
104
125
|
Turn automation into tests:
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: PyClickerRPA
|
|
3
|
-
Version: 0.
|
|
4
|
-
Summary: Visual UI automation & testing tool: Windows + Android(ADB) automation, robust/self-healing image matching, OCR, node flow editor, recorder, a visual test framework (assertions, visual regression, HTML/JUnit reports) and a web console.
|
|
3
|
+
Version: 0.4.0
|
|
4
|
+
Summary: Visual UI automation & testing tool: Windows + Android(ADB) automation, robust/self-healing image matching, OCR, node flow editor, a smart recorder that records images instead of coordinates, a visual test framework (assertions, visual regression, HTML/JUnit reports) and a web console.
|
|
5
5
|
Author: Equinox
|
|
6
6
|
License-Expression: MIT
|
|
7
7
|
Project-URL: Homepage, https://github.com/Equinox/PyClickerRPA
|
|
@@ -82,7 +82,7 @@ autoclicker-web --suites-dir examples # web console at http://127.0.0.1
|
|
|
82
82
|
- **Smart wait**: `wait_stable` waits until the screen (or an ROI) stops
|
|
83
83
|
changing — replaces guessed `sleep`s (fast when ready, patient when slow).
|
|
84
84
|
- **~30+ step types**: click image/text/position, click-all, relative/offset
|
|
85
|
-
clicks, wait / random / wait-stable, drag / long-press / pinch / scroll,
|
|
85
|
+
clicks, wait / random / wait-stable, drag / **drag-from-image** / long-press / pinch / scroll,
|
|
86
86
|
key / type / hotkey, loop / goto / while / do-until, if image/text/color/
|
|
87
87
|
count/var with **true/false branching** (if-else), set-var, read-text→variable,
|
|
88
88
|
activate-window, HTTP request, run-Python-script, call sub-task, groups, and a
|
|
@@ -93,12 +93,33 @@ autoclicker-web --suites-dir examples # web console at http://127.0.0.1
|
|
|
93
93
|
Drag-and-drop **node flow graph** (condition nodes have **true/false branch
|
|
94
94
|
ports** for visual if-else; a terminal **stop** node ends the flow) and an
|
|
95
95
|
interoperable **step list** with a full
|
|
96
|
-
per-step dialog;
|
|
96
|
+
per-step dialog; snippet library, task
|
|
97
97
|
packages (`.zip` with templates), on-preview coordinate/color picking, template
|
|
98
98
|
crop, single-step test run; global hotkeys (F6 run/stop, ESC stop), scheduler,
|
|
99
99
|
and an image/color **monitor/trigger** with tray icon; a **Test** tab to load and
|
|
100
100
|
run visual test suites.
|
|
101
101
|
|
|
102
|
+
### Smart recorder
|
|
103
|
+
|
|
104
|
+
The action **recorder** (global hooks) doesn't just replay coordinates and
|
|
105
|
+
sleeps — with **智能录制 / smart mode** on (default) it records what things *look
|
|
106
|
+
like* instead of where they *were*:
|
|
107
|
+
|
|
108
|
+
- **Clicks become image steps.** A patch is cropped around the click and grown
|
|
109
|
+
(48 → 192 px) until it is both textured enough and **unique on screen**, then
|
|
110
|
+
emitted as a `click_image` step — so the flow still works after the window
|
|
111
|
+
moves or the resolution changes. Identical patches are de-duplicated by
|
|
112
|
+
content hash.
|
|
113
|
+
- **Drags become `drag_image`.** The drag's *start* is anchored to an image and
|
|
114
|
+
the *displacement* is stored, so start and end move together.
|
|
115
|
+
- **Pauses become `wait_stable`.** A long pause is only converted if the
|
|
116
|
+
recorder actually **observed the screen settle** during it; on a permanently
|
|
117
|
+
animating screen it keeps the fixed wait (a `wait_stable` that never settles
|
|
118
|
+
would fail the run).
|
|
119
|
+
|
|
120
|
+
Anything that can't be made robust falls back to the plain coordinate/fixed-wait
|
|
121
|
+
step and says why in the log.
|
|
122
|
+
|
|
102
123
|
## Visual test framework (`autoclicker.testkit`)
|
|
103
124
|
|
|
104
125
|
Turn automation into tests:
|
|
@@ -13,11 +13,13 @@ autoclicker/app.py
|
|
|
13
13
|
autoclicker/capture.py
|
|
14
14
|
autoclicker/clicker.py
|
|
15
15
|
autoclicker/dpi.py
|
|
16
|
+
autoclicker/expr.py
|
|
16
17
|
autoclicker/matcher.py
|
|
17
18
|
autoclicker/models.py
|
|
18
19
|
autoclicker/ocr.py
|
|
19
20
|
autoclicker/run.py
|
|
20
21
|
autoclicker/runner.py
|
|
22
|
+
autoclicker/templatize.py
|
|
21
23
|
autoclicker/windows.py
|
|
22
24
|
autoclicker/gui/__init__.py
|
|
23
25
|
autoclicker/gui/flow_connection.py
|
|
@@ -55,7 +57,9 @@ tests/test_adb.py
|
|
|
55
57
|
tests/test_flow_branch.py
|
|
56
58
|
tests/test_matcher.py
|
|
57
59
|
tests/test_models.py
|
|
60
|
+
tests/test_recorder_gui.py
|
|
58
61
|
tests/test_runner.py
|
|
62
|
+
tests/test_templatize.py
|
|
59
63
|
tests/test_testkit.py
|
|
60
64
|
tests/test_testkit_gui.py
|
|
61
65
|
tests/test_web.py
|
|
@@ -44,7 +44,7 @@ autoclicker-web --suites-dir examples # web console at http://127.0.0.1
|
|
|
44
44
|
- **Smart wait**: `wait_stable` waits until the screen (or an ROI) stops
|
|
45
45
|
changing — replaces guessed `sleep`s (fast when ready, patient when slow).
|
|
46
46
|
- **~30+ step types**: click image/text/position, click-all, relative/offset
|
|
47
|
-
clicks, wait / random / wait-stable, drag / long-press / pinch / scroll,
|
|
47
|
+
clicks, wait / random / wait-stable, drag / **drag-from-image** / long-press / pinch / scroll,
|
|
48
48
|
key / type / hotkey, loop / goto / while / do-until, if image/text/color/
|
|
49
49
|
count/var with **true/false branching** (if-else), set-var, read-text→variable,
|
|
50
50
|
activate-window, HTTP request, run-Python-script, call sub-task, groups, and a
|
|
@@ -55,12 +55,33 @@ autoclicker-web --suites-dir examples # web console at http://127.0.0.1
|
|
|
55
55
|
Drag-and-drop **node flow graph** (condition nodes have **true/false branch
|
|
56
56
|
ports** for visual if-else; a terminal **stop** node ends the flow) and an
|
|
57
57
|
interoperable **step list** with a full
|
|
58
|
-
per-step dialog;
|
|
58
|
+
per-step dialog; snippet library, task
|
|
59
59
|
packages (`.zip` with templates), on-preview coordinate/color picking, template
|
|
60
60
|
crop, single-step test run; global hotkeys (F6 run/stop, ESC stop), scheduler,
|
|
61
61
|
and an image/color **monitor/trigger** with tray icon; a **Test** tab to load and
|
|
62
62
|
run visual test suites.
|
|
63
63
|
|
|
64
|
+
### Smart recorder
|
|
65
|
+
|
|
66
|
+
The action **recorder** (global hooks) doesn't just replay coordinates and
|
|
67
|
+
sleeps — with **智能录制 / smart mode** on (default) it records what things *look
|
|
68
|
+
like* instead of where they *were*:
|
|
69
|
+
|
|
70
|
+
- **Clicks become image steps.** A patch is cropped around the click and grown
|
|
71
|
+
(48 → 192 px) until it is both textured enough and **unique on screen**, then
|
|
72
|
+
emitted as a `click_image` step — so the flow still works after the window
|
|
73
|
+
moves or the resolution changes. Identical patches are de-duplicated by
|
|
74
|
+
content hash.
|
|
75
|
+
- **Drags become `drag_image`.** The drag's *start* is anchored to an image and
|
|
76
|
+
the *displacement* is stored, so start and end move together.
|
|
77
|
+
- **Pauses become `wait_stable`.** A long pause is only converted if the
|
|
78
|
+
recorder actually **observed the screen settle** during it; on a permanently
|
|
79
|
+
animating screen it keeps the fixed wait (a `wait_stable` that never settles
|
|
80
|
+
would fail the run).
|
|
81
|
+
|
|
82
|
+
Anything that can't be made robust falls back to the plain coordinate/fixed-wait
|
|
83
|
+
step and says why in the log.
|
|
84
|
+
|
|
64
85
|
## Visual test framework (`autoclicker.testkit`)
|
|
65
86
|
|
|
66
87
|
Turn automation into tests:
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""Safe arithmetic/string expression evaluator for the SetVar step.
|
|
2
|
+
|
|
3
|
+
`SetVar` values starting with ``=`` are evaluated here (e.g. ``=count + 1``,
|
|
4
|
+
``=max(a, b) * 2``, ``=upper(name)``). Variable names resolve against the
|
|
5
|
+
runner's variable dict — numeric-looking values become numbers, everything
|
|
6
|
+
else stays a string; unknown names default to 0.
|
|
7
|
+
|
|
8
|
+
Security: this never calls the builtin ``eval``. It walks a whitelisted AST
|
|
9
|
+
and rejects attribute access, subscripting, comprehensions, and any call to a
|
|
10
|
+
function outside a fixed safe set — so ``__import__(...)`` / ``().__class__``
|
|
11
|
+
style escapes raise ``ValueError`` instead of executing.
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import ast
|
|
16
|
+
import operator
|
|
17
|
+
|
|
18
|
+
_BIN_OPS = {
|
|
19
|
+
ast.Add: operator.add, ast.Sub: operator.sub, ast.Mult: operator.mul,
|
|
20
|
+
ast.Div: operator.truediv, ast.FloorDiv: operator.floordiv,
|
|
21
|
+
ast.Mod: operator.mod, ast.Pow: operator.pow,
|
|
22
|
+
}
|
|
23
|
+
_UNARY_OPS = {ast.UAdd: operator.pos, ast.USub: operator.neg, ast.Not: operator.not_}
|
|
24
|
+
_CMP_OPS = {
|
|
25
|
+
ast.Eq: operator.eq, ast.NotEq: operator.ne, ast.Lt: operator.lt,
|
|
26
|
+
ast.LtE: operator.le, ast.Gt: operator.gt, ast.GtE: operator.ge,
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
_FUNCS = {
|
|
30
|
+
"int": int, "float": float, "str": str, "round": round, "abs": abs,
|
|
31
|
+
"min": min, "max": max, "len": len,
|
|
32
|
+
"upper": lambda s: str(s).upper(), "lower": lambda s: str(s).lower(),
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _coerce(value: str):
|
|
37
|
+
"""A variable value string → number when it looks numeric, else the string."""
|
|
38
|
+
try:
|
|
39
|
+
f = float(value)
|
|
40
|
+
return int(f) if f.is_integer() else f
|
|
41
|
+
except (TypeError, ValueError):
|
|
42
|
+
return value
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _eval(node, variables):
|
|
46
|
+
if isinstance(node, ast.Expression):
|
|
47
|
+
return _eval(node.body, variables)
|
|
48
|
+
if isinstance(node, ast.Constant): # numbers, strings, True/False/None
|
|
49
|
+
return node.value
|
|
50
|
+
if isinstance(node, ast.Name):
|
|
51
|
+
return _coerce(variables.get(node.id, "0"))
|
|
52
|
+
if isinstance(node, ast.BinOp) and type(node.op) in _BIN_OPS:
|
|
53
|
+
return _BIN_OPS[type(node.op)](_eval(node.left, variables), _eval(node.right, variables))
|
|
54
|
+
if isinstance(node, ast.UnaryOp) and type(node.op) in _UNARY_OPS:
|
|
55
|
+
return _UNARY_OPS[type(node.op)](_eval(node.operand, variables))
|
|
56
|
+
if isinstance(node, ast.BoolOp):
|
|
57
|
+
vals = [_eval(v, variables) for v in node.values]
|
|
58
|
+
result = vals[0]
|
|
59
|
+
for v in vals[1:]:
|
|
60
|
+
result = (result and v) if isinstance(node.op, ast.And) else (result or v)
|
|
61
|
+
return result
|
|
62
|
+
if isinstance(node, ast.Compare):
|
|
63
|
+
left = _eval(node.left, variables)
|
|
64
|
+
for op, comparator in zip(node.ops, node.comparators):
|
|
65
|
+
if type(op) not in _CMP_OPS:
|
|
66
|
+
raise ValueError(f"不支持的比较运算符: {type(op).__name__}")
|
|
67
|
+
right = _eval(comparator, variables)
|
|
68
|
+
if not _CMP_OPS[type(op)](left, right):
|
|
69
|
+
return False
|
|
70
|
+
left = right
|
|
71
|
+
return True
|
|
72
|
+
if isinstance(node, ast.Call):
|
|
73
|
+
if not isinstance(node.func, ast.Name) or node.func.id not in _FUNCS:
|
|
74
|
+
raise ValueError("只允许调用: " + ", ".join(sorted(_FUNCS)))
|
|
75
|
+
if node.keywords:
|
|
76
|
+
raise ValueError("函数不支持关键字参数")
|
|
77
|
+
args = [_eval(a, variables) for a in node.args]
|
|
78
|
+
return _FUNCS[node.func.id](*args)
|
|
79
|
+
raise ValueError(f"表达式包含不允许的语法: {type(node).__name__}")
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def safe_eval(expr: str, variables: dict) -> str:
|
|
83
|
+
"""Evaluate `expr` against `variables`, returning a string result.
|
|
84
|
+
|
|
85
|
+
Integer-valued floats render without a trailing ``.0`` so counters stay clean.
|
|
86
|
+
Raises ``ValueError`` (or SyntaxError) on anything outside the whitelist.
|
|
87
|
+
"""
|
|
88
|
+
tree = ast.parse(expr, mode="eval")
|
|
89
|
+
result = _eval(tree, variables)
|
|
90
|
+
if isinstance(result, float) and result.is_integer():
|
|
91
|
+
return str(int(result))
|
|
92
|
+
if isinstance(result, bool):
|
|
93
|
+
return "1" if result else "0"
|
|
94
|
+
return str(result)
|
|
@@ -7,10 +7,11 @@ recreates loop/do-until back-edges.
|
|
|
7
7
|
"""
|
|
8
8
|
from __future__ import annotations
|
|
9
9
|
|
|
10
|
+
import copy
|
|
10
11
|
from collections import deque
|
|
11
12
|
|
|
12
|
-
from PySide6.QtCore import QLineF, Qt, Signal
|
|
13
|
-
from PySide6.QtGui import QColor, QPainter, QPen
|
|
13
|
+
from PySide6.QtCore import QLineF, QPointF, Qt, Signal
|
|
14
|
+
from PySide6.QtGui import QColor, QKeySequence, QPainter, QPen
|
|
14
15
|
from PySide6.QtWidgets import (
|
|
15
16
|
QDialog, QGraphicsScene, QGraphicsView, QInputDialog, QMenu,
|
|
16
17
|
)
|
|
@@ -52,7 +53,7 @@ class FlowEditor(QGraphicsView):
|
|
|
52
53
|
self._scene.setSceneRect(-2000, -2000, 4000, 4000)
|
|
53
54
|
self.setScene(self._scene)
|
|
54
55
|
self.setRenderHint(QPainter.Antialiasing)
|
|
55
|
-
self.setDragMode(QGraphicsView.
|
|
56
|
+
self.setDragMode(QGraphicsView.RubberBandDrag)
|
|
56
57
|
self.setBackgroundBrush(QColor(30, 30, 30))
|
|
57
58
|
self.setMinimumSize(400, 300)
|
|
58
59
|
|
|
@@ -64,6 +65,7 @@ class FlowEditor(QGraphicsView):
|
|
|
64
65
|
|
|
65
66
|
self.nodes: list[FlowNode] = []
|
|
66
67
|
self.connections: list[FlowConnection] = []
|
|
68
|
+
self._clip: dict | None = None
|
|
67
69
|
self._dragging = False
|
|
68
70
|
self._drag_start_port: FlowPort | None = None
|
|
69
71
|
self._drag_line = None
|
|
@@ -165,11 +167,61 @@ class FlowEditor(QGraphicsView):
|
|
|
165
167
|
self.scale(factor, factor)
|
|
166
168
|
|
|
167
169
|
def keyPressEvent(self, event):
|
|
170
|
+
if event.matches(QKeySequence.Copy):
|
|
171
|
+
self._copy_selected()
|
|
172
|
+
return
|
|
173
|
+
if event.matches(QKeySequence.Paste):
|
|
174
|
+
self._paste_clip()
|
|
175
|
+
return
|
|
168
176
|
if event.key() in (Qt.Key_Delete, Qt.Key_Backspace):
|
|
169
177
|
self._delete_selected()
|
|
170
178
|
return
|
|
171
179
|
super().keyPressEvent(event)
|
|
172
180
|
|
|
181
|
+
# ---- copy / paste selected nodes ----
|
|
182
|
+
def selected_nodes(self) -> list[FlowNode]:
|
|
183
|
+
return [n for n in self.nodes if n.isSelected()]
|
|
184
|
+
|
|
185
|
+
def get_selected_steps(self) -> list[Step]:
|
|
186
|
+
nodes = sorted(self.selected_nodes(), key=lambda n: (n.pos().y(), n.pos().x()))
|
|
187
|
+
return [copy.deepcopy(n.step()) for n in nodes]
|
|
188
|
+
|
|
189
|
+
def _copy_selected(self):
|
|
190
|
+
nodes = self.selected_nodes()
|
|
191
|
+
if not nodes:
|
|
192
|
+
return
|
|
193
|
+
idx = {n: i for i, n in enumerate(nodes)}
|
|
194
|
+
edges = []
|
|
195
|
+
for conn in self.connections:
|
|
196
|
+
fn = conn.from_port.parentItem()
|
|
197
|
+
tn = conn.to_port.parentItem()
|
|
198
|
+
if fn in idx and tn in idx:
|
|
199
|
+
# 记录出边走的是「真」还是「假」分支(条件节点有 false_port)
|
|
200
|
+
branch = "false" if conn.from_port is fn.false_port else "true"
|
|
201
|
+
edges.append((idx[fn], idx[tn], branch, conn.repeat_count))
|
|
202
|
+
self._clip = {
|
|
203
|
+
"steps": [copy.deepcopy(n.step()) for n in nodes],
|
|
204
|
+
"pos": [(n.pos().x(), n.pos().y()) for n in nodes],
|
|
205
|
+
"edges": edges,
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
def _paste_clip(self):
|
|
209
|
+
if not self._clip or not self._clip["steps"]:
|
|
210
|
+
return
|
|
211
|
+
new_nodes = []
|
|
212
|
+
for step, (px, py) in zip(self._clip["steps"], self._clip["pos"]):
|
|
213
|
+
new_nodes.append(self.add_node(copy.deepcopy(step), QPointF(px + 40, py + 40)))
|
|
214
|
+
for fi, ti, branch, rc in self._clip["edges"]:
|
|
215
|
+
src = new_nodes[fi].false_port if branch == "false" else new_nodes[fi].output_port
|
|
216
|
+
if src is None: # 终止节点没有输出口
|
|
217
|
+
continue
|
|
218
|
+
conn = self.connect_nodes(src, new_nodes[ti].input_port)
|
|
219
|
+
if conn:
|
|
220
|
+
conn.set_repeat_count(rc)
|
|
221
|
+
self._scene.clearSelection()
|
|
222
|
+
for n in new_nodes:
|
|
223
|
+
n.setSelected(True)
|
|
224
|
+
|
|
173
225
|
def contextMenuEvent(self, event):
|
|
174
226
|
menu = QMenu(self)
|
|
175
227
|
scene_pos = self.mapToScene(event.pos())
|
|
@@ -183,6 +235,11 @@ class FlowEditor(QGraphicsView):
|
|
|
183
235
|
menu.addAction("设置重复次数", lambda: self._set_repeat(conn))
|
|
184
236
|
if node or conn:
|
|
185
237
|
menu.addAction("删除", self._delete_selected)
|
|
238
|
+
if self.selected_nodes():
|
|
239
|
+
menu.addAction("复制选中节点 (Ctrl+C)", self._copy_selected)
|
|
240
|
+
if self._clip and self._clip["steps"]:
|
|
241
|
+
menu.addAction("粘贴 (Ctrl+V)", self._paste_clip)
|
|
242
|
+
if node or conn or self.selected_nodes() or self._clip:
|
|
186
243
|
menu.addSeparator()
|
|
187
244
|
|
|
188
245
|
add_menu = menu.addMenu("添加节点")
|
|
@@ -12,10 +12,10 @@ import tempfile
|
|
|
12
12
|
import uuid
|
|
13
13
|
import zipfile
|
|
14
14
|
|
|
15
|
-
from PySide6.QtCore import Qt, QThread, Signal
|
|
15
|
+
from PySide6.QtCore import Qt, QSettings, QThread, Signal
|
|
16
16
|
from PySide6.QtGui import QAction, QGuiApplication, QKeySequence
|
|
17
17
|
from PySide6.QtWidgets import (
|
|
18
|
-
QComboBox, QDockWidget, QFileDialog, QHBoxLayout, QInputDialog, QLabel,
|
|
18
|
+
QCheckBox, QComboBox, QDockWidget, QFileDialog, QHBoxLayout, QInputDialog, QLabel,
|
|
19
19
|
QMainWindow, QMessageBox, QPushButton, QSplitter, QTableWidget,
|
|
20
20
|
QTableWidgetItem, QTabWidget, QTextEdit, QVBoxLayout, QWidget,
|
|
21
21
|
)
|
|
@@ -67,9 +67,13 @@ class MainWindow(QMainWindow):
|
|
|
67
67
|
self._runner.matchFound.connect(
|
|
68
68
|
lambda x, y, c: self._append_log(f" → 命中 ({x},{y}) {c:.3f}"))
|
|
69
69
|
self._runner.runFinished.connect(self._on_finished)
|
|
70
|
+
self._runner.pausedAt.connect(self._on_paused)
|
|
71
|
+
|
|
72
|
+
self._settings = QSettings("AutoClicker", "AutoClicker")
|
|
70
73
|
|
|
71
74
|
self._recorder = Recorder(self)
|
|
72
75
|
self._recorder.stepRecorded.connect(self._on_step_recorded)
|
|
76
|
+
self._recorder.note.connect(self._append_log)
|
|
73
77
|
|
|
74
78
|
self._build_ui()
|
|
75
79
|
self._build_menu()
|
|
@@ -228,6 +232,16 @@ class MainWindow(QMainWindow):
|
|
|
228
232
|
capture_btn.clicked.connect(self.capture)
|
|
229
233
|
self.record_btn = QPushButton("● 录制")
|
|
230
234
|
self.record_btn.clicked.connect(self.toggle_record)
|
|
235
|
+
self.template_mode_chk = QCheckBox("智能录制")
|
|
236
|
+
self.template_mode_chk.setToolTip(
|
|
237
|
+
"录出来的任务尽量不依赖绝对坐标和绝对时间:\n"
|
|
238
|
+
"· 点击处裁一小块图当模板 → 位置无关的「找图点击」\n"
|
|
239
|
+
"· 长间隔改成「等画面稳定」→ 换台机器也不会等不够\n"
|
|
240
|
+
"抽不出好模板、或录制时画面一直在动,则各自退回坐标点击/固定等待。")
|
|
241
|
+
self.template_mode_chk.setChecked(
|
|
242
|
+
self._settings.value("recorder/templateMode", True, type=bool))
|
|
243
|
+
self.template_mode_chk.toggled.connect(
|
|
244
|
+
lambda on: self._settings.setValue("recorder/templateMode", on))
|
|
231
245
|
self.run_btn = QPushButton("▶ 运行")
|
|
232
246
|
self.run_btn.clicked.connect(self.run_task)
|
|
233
247
|
self.pause_btn = QPushButton("⏸ 暂停")
|
|
@@ -236,15 +250,27 @@ class MainWindow(QMainWindow):
|
|
|
236
250
|
self.stop_btn = QPushButton("■ 停止")
|
|
237
251
|
self.stop_btn.clicked.connect(self.stop_task)
|
|
238
252
|
self.stop_btn.setEnabled(False)
|
|
253
|
+
self.debug_btn = QPushButton("🐞 调试")
|
|
254
|
+
self.debug_btn.clicked.connect(self.debug_run)
|
|
255
|
+
self.step_btn = QPushButton("单步")
|
|
256
|
+
self.step_btn.clicked.connect(lambda: self._runner.debug_step())
|
|
257
|
+
self.step_btn.setEnabled(False)
|
|
258
|
+
self.continue_btn = QPushButton("继续")
|
|
259
|
+
self.continue_btn.clicked.connect(lambda: self._runner.debug_continue())
|
|
260
|
+
self.continue_btn.setEnabled(False)
|
|
239
261
|
bar.addWidget(QLabel("目标窗口"))
|
|
240
262
|
bar.addWidget(self.window_combo, 1)
|
|
241
263
|
bar.addWidget(refresh_btn)
|
|
242
264
|
bar.addWidget(capture_btn)
|
|
243
265
|
bar.addStretch()
|
|
266
|
+
bar.addWidget(self.template_mode_chk)
|
|
244
267
|
bar.addWidget(self.record_btn)
|
|
245
268
|
bar.addWidget(self.run_btn)
|
|
246
269
|
bar.addWidget(self.pause_btn)
|
|
247
270
|
bar.addWidget(self.stop_btn)
|
|
271
|
+
bar.addWidget(self.debug_btn)
|
|
272
|
+
bar.addWidget(self.step_btn)
|
|
273
|
+
bar.addWidget(self.continue_btn)
|
|
248
274
|
root.addLayout(bar)
|
|
249
275
|
|
|
250
276
|
splitter = QSplitter(Qt.Horizontal)
|
|
@@ -327,6 +353,9 @@ class MainWindow(QMainWindow):
|
|
|
327
353
|
e = self.menuBar().addMenu("编辑")
|
|
328
354
|
e.addAction(QAction("选中步骤存为片段…", self, triggered=self.save_snippet))
|
|
329
355
|
e.addSeparator()
|
|
356
|
+
e.addAction(QAction("导出选中步骤为文件(.json)…", self, triggered=self.export_steps_file))
|
|
357
|
+
e.addAction(QAction("从文件导入步骤(.json)…", self, triggered=self.import_steps_file))
|
|
358
|
+
e.addSeparator()
|
|
330
359
|
e.addAction(QAction("复制步骤到剪贴板(JSON)", self, triggered=self.copy_steps_json))
|
|
331
360
|
e.addAction(QAction("从剪贴板粘贴步骤(JSON)", self, triggered=self.paste_steps_json))
|
|
332
361
|
|
|
@@ -402,6 +431,29 @@ class MainWindow(QMainWindow):
|
|
|
402
431
|
self._runner.start(task, target_hwnd=self._target_hwnd(),
|
|
403
432
|
screenshot_on_fail=True, screenshot_dir=".failures")
|
|
404
433
|
|
|
434
|
+
def debug_run(self):
|
|
435
|
+
if self._runner.is_running():
|
|
436
|
+
return
|
|
437
|
+
self.tabs.setCurrentWidget(self.task_widget)
|
|
438
|
+
task = self._current_task()
|
|
439
|
+
if not task.steps:
|
|
440
|
+
self._append_log("任务为空")
|
|
441
|
+
return
|
|
442
|
+
self.log.clear()
|
|
443
|
+
self._clear_vars()
|
|
444
|
+
self.preview.clear_markers()
|
|
445
|
+
self._set_running(True)
|
|
446
|
+
self._append_log("调试模式:停在第 1 步,用「单步」/「继续」推进")
|
|
447
|
+
self._runner.start(task, target_hwnd=self._target_hwnd(),
|
|
448
|
+
screenshot_on_fail=True, screenshot_dir=".failures",
|
|
449
|
+
breakpoints=self.task_widget.breakpoints(), step_mode=True)
|
|
450
|
+
|
|
451
|
+
def _on_paused(self, index: int, desc: str):
|
|
452
|
+
self._append_log(f"⏸ 暂停于 [{index + 1}] {desc}")
|
|
453
|
+
self.task_widget.set_running_row(index)
|
|
454
|
+
self.step_btn.setEnabled(True)
|
|
455
|
+
self.continue_btn.setEnabled(True)
|
|
456
|
+
|
|
405
457
|
def stop_task(self):
|
|
406
458
|
self._runner.stop()
|
|
407
459
|
|
|
@@ -415,16 +467,20 @@ class MainWindow(QMainWindow):
|
|
|
415
467
|
|
|
416
468
|
def _set_running(self, running: bool):
|
|
417
469
|
self.run_btn.setEnabled(not running)
|
|
470
|
+
self.debug_btn.setEnabled(not running)
|
|
418
471
|
self.stop_btn.setEnabled(running)
|
|
419
472
|
self.pause_btn.setEnabled(running)
|
|
420
473
|
if not running:
|
|
421
474
|
self.pause_btn.setText("⏸ 暂停")
|
|
475
|
+
self.step_btn.setEnabled(False)
|
|
476
|
+
self.continue_btn.setEnabled(False)
|
|
422
477
|
|
|
423
478
|
def _on_step_started(self, index: int, desc: str):
|
|
424
479
|
self._append_log(f"[{index + 1}] {desc}")
|
|
425
480
|
|
|
426
481
|
def _on_finished(self):
|
|
427
482
|
self._set_running(False)
|
|
483
|
+
self.task_widget.clear_running_row()
|
|
428
484
|
|
|
429
485
|
def _append_log(self, msg: str):
|
|
430
486
|
self.log.append(msg)
|
|
@@ -471,10 +527,12 @@ class MainWindow(QMainWindow):
|
|
|
471
527
|
self._append_log("录制结束")
|
|
472
528
|
return
|
|
473
529
|
hwnd = self._target_hwnd()
|
|
474
|
-
self.
|
|
530
|
+
tmpl = self.template_mode_chk.isChecked()
|
|
531
|
+
self._recorder.start(hwnd, template_mode=tmpl)
|
|
475
532
|
self.record_btn.setText("■ 停止录制")
|
|
476
533
|
self.tabs.setCurrentWidget(self.task_widget)
|
|
477
|
-
self._append_log("开始录制(在目标窗口内操作;再次点击结束)"
|
|
534
|
+
self._append_log("开始录制(在目标窗口内操作;再次点击结束)"
|
|
535
|
+
+ ("[智能录制]" if tmpl else ""))
|
|
478
536
|
|
|
479
537
|
def _on_step_recorded(self, step: Step):
|
|
480
538
|
self.task_widget.append_step(step)
|
|
@@ -503,10 +561,16 @@ class MainWindow(QMainWindow):
|
|
|
503
561
|
self.tabs.setCurrentWidget(self.task_widget)
|
|
504
562
|
self._append_log(f"插入片段: {len(steps)} 步")
|
|
505
563
|
|
|
564
|
+
def _selected_steps(self) -> list[Step]:
|
|
565
|
+
"""Selected steps from whichever editor tab is active."""
|
|
566
|
+
if self.tabs.currentWidget() is self.flow_editor:
|
|
567
|
+
return self.flow_editor.get_selected_steps()
|
|
568
|
+
return self.task_widget.get_selected_steps()
|
|
569
|
+
|
|
506
570
|
def save_snippet(self):
|
|
507
|
-
steps = self.
|
|
571
|
+
steps = self._selected_steps()
|
|
508
572
|
if not steps:
|
|
509
|
-
self._append_log("
|
|
573
|
+
self._append_log("请先在「步骤列表」或「流程图」中选中要保存的步骤")
|
|
510
574
|
return
|
|
511
575
|
name, ok = QInputDialog.getText(self, "存为片段", "片段名称:")
|
|
512
576
|
if ok and name:
|
|
@@ -516,13 +580,56 @@ class MainWindow(QMainWindow):
|
|
|
516
580
|
# ---- step clipboard (JSON) ----
|
|
517
581
|
def copy_steps_json(self):
|
|
518
582
|
import json
|
|
519
|
-
steps = self.
|
|
583
|
+
steps = self._selected_steps()
|
|
520
584
|
if not steps:
|
|
521
|
-
self._append_log("
|
|
585
|
+
self._append_log("请先在「步骤列表」或「流程图」中选中要复制的步骤")
|
|
522
586
|
return
|
|
523
587
|
QGuiApplication.clipboard().setText(json.dumps([s.to_json() for s in steps], ensure_ascii=False, indent=2))
|
|
524
588
|
self._append_log(f"已复制 {len(steps)} 步到剪贴板(JSON)")
|
|
525
589
|
|
|
590
|
+
def export_steps_file(self):
|
|
591
|
+
import json
|
|
592
|
+
steps = self._selected_steps()
|
|
593
|
+
if not steps:
|
|
594
|
+
self._append_log("请先在「步骤列表」或「流程图」中选中要导出的步骤")
|
|
595
|
+
return
|
|
596
|
+
path, _ = QFileDialog.getSaveFileName(self, "导出选中步骤", "", "步骤文件 (*.json)")
|
|
597
|
+
if not path:
|
|
598
|
+
return
|
|
599
|
+
if not path.lower().endswith(".json"):
|
|
600
|
+
path += ".json"
|
|
601
|
+
try:
|
|
602
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
603
|
+
json.dump([s.to_json() for s in steps], f, ensure_ascii=False, indent=4)
|
|
604
|
+
except OSError as ex:
|
|
605
|
+
QMessageBox.warning(self, "导出失败", str(ex))
|
|
606
|
+
return
|
|
607
|
+
self._append_log(f"已导出 {len(steps)} 步到: {path}")
|
|
608
|
+
|
|
609
|
+
def import_steps_file(self):
|
|
610
|
+
import json
|
|
611
|
+
path, _ = QFileDialog.getOpenFileName(self, "导入步骤", "", "步骤文件 (*.json)")
|
|
612
|
+
if not path:
|
|
613
|
+
return
|
|
614
|
+
try:
|
|
615
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
616
|
+
data = json.load(f)
|
|
617
|
+
except (OSError, json.JSONDecodeError) as ex:
|
|
618
|
+
QMessageBox.warning(self, "导入失败", str(ex))
|
|
619
|
+
return
|
|
620
|
+
if isinstance(data, dict) and "steps" in data: # accept a full task file too
|
|
621
|
+
data = data["steps"]
|
|
622
|
+
if not isinstance(data, list):
|
|
623
|
+
QMessageBox.warning(self, "导入失败", "文件内容既不是步骤数组也不是任务")
|
|
624
|
+
return
|
|
625
|
+
steps = [Step.from_json(o) for o in data]
|
|
626
|
+
if not steps:
|
|
627
|
+
self._append_log("文件里没有步骤")
|
|
628
|
+
return
|
|
629
|
+
self.task_widget.insert_steps(steps)
|
|
630
|
+
self.tabs.setCurrentWidget(self.task_widget)
|
|
631
|
+
self._append_log(f"已从文件导入 {len(steps)} 步: {path}")
|
|
632
|
+
|
|
526
633
|
def paste_steps_json(self):
|
|
527
634
|
import json
|
|
528
635
|
text = QGuiApplication.clipboard().text()
|
|
@@ -32,6 +32,9 @@ class _SignalCallbacks(RunnerCallbacks):
|
|
|
32
32
|
def var_changed(self, name, value):
|
|
33
33
|
self._o.varChanged.emit(name, value)
|
|
34
34
|
|
|
35
|
+
def paused(self, index, description):
|
|
36
|
+
self._o.pausedAt.emit(index, description)
|
|
37
|
+
|
|
35
38
|
def finished(self):
|
|
36
39
|
self._o.runFinished.emit()
|
|
37
40
|
|
|
@@ -42,19 +45,31 @@ class QtRunner(QObject):
|
|
|
42
45
|
matchFound = Signal(int, int, float)
|
|
43
46
|
logMessage = Signal(str)
|
|
44
47
|
varChanged = Signal(str, str)
|
|
48
|
+
pausedAt = Signal(int, str)
|
|
45
49
|
runFinished = Signal()
|
|
46
50
|
|
|
47
51
|
def __init__(self, parent=None) -> None:
|
|
48
52
|
super().__init__(parent)
|
|
49
53
|
self._runner: TaskRunner | None = None
|
|
50
54
|
|
|
51
|
-
def start(self, task: Task, target_hwnd=None, screenshot_on_fail=False, screenshot_dir=None
|
|
55
|
+
def start(self, task: Task, target_hwnd=None, screenshot_on_fail=False, screenshot_dir=None,
|
|
56
|
+
breakpoints=None, step_mode=False) -> None:
|
|
52
57
|
self._runner = TaskRunner(task, target_hwnd=target_hwnd, callbacks=_SignalCallbacks(self))
|
|
53
58
|
self._runner.screenshot_on_fail = screenshot_on_fail
|
|
54
59
|
if screenshot_dir:
|
|
55
60
|
self._runner.screenshot_dir = screenshot_dir
|
|
61
|
+
self._runner.set_breakpoints(breakpoints)
|
|
62
|
+
self._runner.enable_step_mode(step_mode)
|
|
56
63
|
self._runner.start()
|
|
57
64
|
|
|
65
|
+
def debug_step(self) -> None:
|
|
66
|
+
if self._runner:
|
|
67
|
+
self._runner.debug_step()
|
|
68
|
+
|
|
69
|
+
def debug_continue(self) -> None:
|
|
70
|
+
if self._runner:
|
|
71
|
+
self._runner.debug_continue()
|
|
72
|
+
|
|
58
73
|
def stop(self) -> None:
|
|
59
74
|
if self._runner:
|
|
60
75
|
self._runner.stop()
|