PyClickerRPA 0.5.0__tar.gz → 0.6.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.5.0 → pyclickerrpa-0.6.0}/PKG-INFO +22 -12
- {pyclickerrpa-0.5.0 → pyclickerrpa-0.6.0}/PyClickerRPA.egg-info/PKG-INFO +22 -12
- {pyclickerrpa-0.5.0 → pyclickerrpa-0.6.0}/PyClickerRPA.egg-info/SOURCES.txt +2 -0
- {pyclickerrpa-0.5.0 → pyclickerrpa-0.6.0}/README.md +20 -10
- {pyclickerrpa-0.5.0 → pyclickerrpa-0.6.0}/autoclicker/gui/flow_editor.py +76 -18
- {pyclickerrpa-0.5.0 → pyclickerrpa-0.6.0}/autoclicker/gui/flow_node.py +21 -2
- {pyclickerrpa-0.5.0 → pyclickerrpa-0.6.0}/autoclicker/gui/main_window.py +2 -1
- {pyclickerrpa-0.5.0 → pyclickerrpa-0.6.0}/autoclicker/gui/task_widget.py +281 -36
- pyclickerrpa-0.6.0/autoclicker/lint.py +203 -0
- {pyclickerrpa-0.5.0 → pyclickerrpa-0.6.0}/autoclicker/models.py +108 -9
- {pyclickerrpa-0.5.0 → pyclickerrpa-0.6.0}/autoclicker/runner.py +272 -62
- {pyclickerrpa-0.5.0 → pyclickerrpa-0.6.0}/pyproject.toml +2 -2
- {pyclickerrpa-0.5.0 → pyclickerrpa-0.6.0}/tests/test_lint.py +45 -0
- {pyclickerrpa-0.5.0 → pyclickerrpa-0.6.0}/tests/test_recorder_gui.py +7 -1
- pyclickerrpa-0.6.0/tests/test_structured_flow.py +377 -0
- pyclickerrpa-0.6.0/tests/test_structured_gui.py +223 -0
- pyclickerrpa-0.5.0/autoclicker/lint.py +0 -163
- {pyclickerrpa-0.5.0 → pyclickerrpa-0.6.0}/LICENSE +0 -0
- {pyclickerrpa-0.5.0 → pyclickerrpa-0.6.0}/PyClickerRPA.egg-info/dependency_links.txt +0 -0
- {pyclickerrpa-0.5.0 → pyclickerrpa-0.6.0}/PyClickerRPA.egg-info/entry_points.txt +0 -0
- {pyclickerrpa-0.5.0 → pyclickerrpa-0.6.0}/PyClickerRPA.egg-info/requires.txt +0 -0
- {pyclickerrpa-0.5.0 → pyclickerrpa-0.6.0}/PyClickerRPA.egg-info/top_level.txt +0 -0
- {pyclickerrpa-0.5.0 → pyclickerrpa-0.6.0}/autoclicker/__init__.py +0 -0
- {pyclickerrpa-0.5.0 → pyclickerrpa-0.6.0}/autoclicker/adb.py +0 -0
- {pyclickerrpa-0.5.0 → pyclickerrpa-0.6.0}/autoclicker/app.py +0 -0
- {pyclickerrpa-0.5.0 → pyclickerrpa-0.6.0}/autoclicker/capture.py +0 -0
- {pyclickerrpa-0.5.0 → pyclickerrpa-0.6.0}/autoclicker/clicker.py +0 -0
- {pyclickerrpa-0.5.0 → pyclickerrpa-0.6.0}/autoclicker/diagnose.py +0 -0
- {pyclickerrpa-0.5.0 → pyclickerrpa-0.6.0}/autoclicker/dpi.py +0 -0
- {pyclickerrpa-0.5.0 → pyclickerrpa-0.6.0}/autoclicker/expr.py +0 -0
- {pyclickerrpa-0.5.0 → pyclickerrpa-0.6.0}/autoclicker/gui/__init__.py +0 -0
- {pyclickerrpa-0.5.0 → pyclickerrpa-0.6.0}/autoclicker/gui/flow_connection.py +0 -0
- {pyclickerrpa-0.5.0 → pyclickerrpa-0.6.0}/autoclicker/gui/hotkeys.py +0 -0
- {pyclickerrpa-0.5.0 → pyclickerrpa-0.6.0}/autoclicker/gui/lint_dialog.py +0 -0
- {pyclickerrpa-0.5.0 → pyclickerrpa-0.6.0}/autoclicker/gui/monitor.py +0 -0
- {pyclickerrpa-0.5.0 → pyclickerrpa-0.6.0}/autoclicker/gui/preview_label.py +0 -0
- {pyclickerrpa-0.5.0 → pyclickerrpa-0.6.0}/autoclicker/gui/qt_runner.py +0 -0
- {pyclickerrpa-0.5.0 → pyclickerrpa-0.6.0}/autoclicker/gui/recorder.py +0 -0
- {pyclickerrpa-0.5.0 → pyclickerrpa-0.6.0}/autoclicker/gui/scheduler_widget.py +0 -0
- {pyclickerrpa-0.5.0 → pyclickerrpa-0.6.0}/autoclicker/gui/snippet_library.py +0 -0
- {pyclickerrpa-0.5.0 → pyclickerrpa-0.6.0}/autoclicker/gui/testkit_widget.py +0 -0
- {pyclickerrpa-0.5.0 → pyclickerrpa-0.6.0}/autoclicker/matcher.py +0 -0
- {pyclickerrpa-0.5.0 → pyclickerrpa-0.6.0}/autoclicker/ocr.py +0 -0
- {pyclickerrpa-0.5.0 → pyclickerrpa-0.6.0}/autoclicker/run.py +0 -0
- {pyclickerrpa-0.5.0 → pyclickerrpa-0.6.0}/autoclicker/templatize.py +0 -0
- {pyclickerrpa-0.5.0 → pyclickerrpa-0.6.0}/autoclicker/testkit/__init__.py +0 -0
- {pyclickerrpa-0.5.0 → pyclickerrpa-0.6.0}/autoclicker/testkit/__main__.py +0 -0
- {pyclickerrpa-0.5.0 → pyclickerrpa-0.6.0}/autoclicker/testkit/assertions.py +0 -0
- {pyclickerrpa-0.5.0 → pyclickerrpa-0.6.0}/autoclicker/testkit/case.py +0 -0
- {pyclickerrpa-0.5.0 → pyclickerrpa-0.6.0}/autoclicker/testkit/loader.py +0 -0
- {pyclickerrpa-0.5.0 → pyclickerrpa-0.6.0}/autoclicker/testkit/pytest_plugin.py +0 -0
- {pyclickerrpa-0.5.0 → pyclickerrpa-0.6.0}/autoclicker/testkit/report.py +0 -0
- {pyclickerrpa-0.5.0 → pyclickerrpa-0.6.0}/autoclicker/testkit/runner.py +0 -0
- {pyclickerrpa-0.5.0 → pyclickerrpa-0.6.0}/autoclicker/testkit/visual.py +0 -0
- {pyclickerrpa-0.5.0 → pyclickerrpa-0.6.0}/autoclicker/web/__init__.py +0 -0
- {pyclickerrpa-0.5.0 → pyclickerrpa-0.6.0}/autoclicker/web/__main__.py +0 -0
- {pyclickerrpa-0.5.0 → pyclickerrpa-0.6.0}/autoclicker/web/run_manager.py +0 -0
- {pyclickerrpa-0.5.0 → pyclickerrpa-0.6.0}/autoclicker/web/scheduler.py +0 -0
- {pyclickerrpa-0.5.0 → pyclickerrpa-0.6.0}/autoclicker/web/serialize.py +0 -0
- {pyclickerrpa-0.5.0 → pyclickerrpa-0.6.0}/autoclicker/web/server.py +0 -0
- {pyclickerrpa-0.5.0 → pyclickerrpa-0.6.0}/autoclicker/web/static/app.js +0 -0
- {pyclickerrpa-0.5.0 → pyclickerrpa-0.6.0}/autoclicker/web/static/index.html +0 -0
- {pyclickerrpa-0.5.0 → pyclickerrpa-0.6.0}/autoclicker/web/static/style.css +0 -0
- {pyclickerrpa-0.5.0 → pyclickerrpa-0.6.0}/autoclicker/windows.py +0 -0
- {pyclickerrpa-0.5.0 → pyclickerrpa-0.6.0}/setup.cfg +0 -0
- {pyclickerrpa-0.5.0 → pyclickerrpa-0.6.0}/tests/test_adb.py +0 -0
- {pyclickerrpa-0.5.0 → pyclickerrpa-0.6.0}/tests/test_diagnose.py +0 -0
- {pyclickerrpa-0.5.0 → pyclickerrpa-0.6.0}/tests/test_diagnose_gui.py +0 -0
- {pyclickerrpa-0.5.0 → pyclickerrpa-0.6.0}/tests/test_flow_branch.py +0 -0
- {pyclickerrpa-0.5.0 → pyclickerrpa-0.6.0}/tests/test_matcher.py +0 -0
- {pyclickerrpa-0.5.0 → pyclickerrpa-0.6.0}/tests/test_models.py +0 -0
- {pyclickerrpa-0.5.0 → pyclickerrpa-0.6.0}/tests/test_runner.py +0 -0
- {pyclickerrpa-0.5.0 → pyclickerrpa-0.6.0}/tests/test_templatize.py +0 -0
- {pyclickerrpa-0.5.0 → pyclickerrpa-0.6.0}/tests/test_testkit.py +0 -0
- {pyclickerrpa-0.5.0 → pyclickerrpa-0.6.0}/tests/test_testkit_gui.py +0 -0
- {pyclickerrpa-0.5.0 → pyclickerrpa-0.6.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,
|
|
3
|
+
Version: 0.6.0
|
|
4
|
+
Summary: Visual UI automation & testing tool: Windows + Android(ADB) automation, robust/self-healing image matching, OCR, structured conditions and loops with array iteration, node flow editor, smart recorder, task health checks and failure attribution, 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
|
|
@@ -44,8 +44,8 @@ ADB) input — then turn those flows into a **visual test framework** with
|
|
|
44
44
|
assertions, visual-regression baselines, HTML/JUnit reports, and a **web console**
|
|
45
45
|
for remote runs.
|
|
46
46
|
|
|
47
|
-
Python/PySide6, started as a port of a Qt/C++ AutoClicker
|
|
48
|
-
|
|
47
|
+
Python/PySide6, started as a port of a Qt/C++ AutoClicker. Legacy `.json` tasks
|
|
48
|
+
remain supported; structured control-flow blocks are a Python-only extension.
|
|
49
49
|
|
|
50
50
|
## Install
|
|
51
51
|
|
|
@@ -91,14 +91,24 @@ autoclicker-web --suites-dir examples # web console at http://127.0.0.1
|
|
|
91
91
|
|
|
92
92
|
## Authoring (desktop GUI)
|
|
93
93
|
|
|
94
|
-
Drag-and-drop **node flow graph**
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
94
|
+
Drag-and-drop **node flow graph** and an interoperable **step list**, with
|
|
95
|
+
nested **If/Else**, **While**, fixed-count loops, **ForEach** arrays, and
|
|
96
|
+
**Break/Continue** — no step numbers required. Double-click a block to edit its
|
|
97
|
+
body or branches. Legacy condition nodes retain their true/false ports.
|
|
98
|
+
|
|
99
|
+
Select consecutive steps and choose **包进… → 遍历数组** to reuse them for each
|
|
100
|
+
item. Enter a custom JSON array or generate a numeric range, then reference the
|
|
101
|
+
current item in text actions, assignments, and conditions. For example, iterate
|
|
102
|
+
`[1,2,3,4,5,6,7,8,9,10]` with variable `server`, click **`{server}服`**, perform
|
|
103
|
+
the actions, and return to the server-selection screen inside the same body.
|
|
104
|
+
Enable exact text matching so `1服` does not also match `11服`. A failed action
|
|
105
|
+
inside a block stops execution unless its failure policy explicitly skips it.
|
|
106
|
+
|
|
107
|
+
Also includes a snippet library, task packages (`.zip` with nested templates),
|
|
108
|
+
on-preview coordinate/color picking, template crop, single-step test run;
|
|
109
|
+
global hotkeys (F6 run/stop, ESC stop), scheduler, and an image/color
|
|
110
|
+
**monitor/trigger** with tray icon; a **Test** tab to load and run visual test
|
|
111
|
+
suites.
|
|
102
112
|
|
|
103
113
|
### Smart recorder
|
|
104
114
|
|
|
@@ -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,
|
|
3
|
+
Version: 0.6.0
|
|
4
|
+
Summary: Visual UI automation & testing tool: Windows + Android(ADB) automation, robust/self-healing image matching, OCR, structured conditions and loops with array iteration, node flow editor, smart recorder, task health checks and failure attribution, 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
|
|
@@ -44,8 +44,8 @@ ADB) input — then turn those flows into a **visual test framework** with
|
|
|
44
44
|
assertions, visual-regression baselines, HTML/JUnit reports, and a **web console**
|
|
45
45
|
for remote runs.
|
|
46
46
|
|
|
47
|
-
Python/PySide6, started as a port of a Qt/C++ AutoClicker
|
|
48
|
-
|
|
47
|
+
Python/PySide6, started as a port of a Qt/C++ AutoClicker. Legacy `.json` tasks
|
|
48
|
+
remain supported; structured control-flow blocks are a Python-only extension.
|
|
49
49
|
|
|
50
50
|
## Install
|
|
51
51
|
|
|
@@ -91,14 +91,24 @@ autoclicker-web --suites-dir examples # web console at http://127.0.0.1
|
|
|
91
91
|
|
|
92
92
|
## Authoring (desktop GUI)
|
|
93
93
|
|
|
94
|
-
Drag-and-drop **node flow graph**
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
94
|
+
Drag-and-drop **node flow graph** and an interoperable **step list**, with
|
|
95
|
+
nested **If/Else**, **While**, fixed-count loops, **ForEach** arrays, and
|
|
96
|
+
**Break/Continue** — no step numbers required. Double-click a block to edit its
|
|
97
|
+
body or branches. Legacy condition nodes retain their true/false ports.
|
|
98
|
+
|
|
99
|
+
Select consecutive steps and choose **包进… → 遍历数组** to reuse them for each
|
|
100
|
+
item. Enter a custom JSON array or generate a numeric range, then reference the
|
|
101
|
+
current item in text actions, assignments, and conditions. For example, iterate
|
|
102
|
+
`[1,2,3,4,5,6,7,8,9,10]` with variable `server`, click **`{server}服`**, perform
|
|
103
|
+
the actions, and return to the server-selection screen inside the same body.
|
|
104
|
+
Enable exact text matching so `1服` does not also match `11服`. A failed action
|
|
105
|
+
inside a block stops execution unless its failure policy explicitly skips it.
|
|
106
|
+
|
|
107
|
+
Also includes a snippet library, task packages (`.zip` with nested templates),
|
|
108
|
+
on-preview coordinate/color picking, template crop, single-step test run;
|
|
109
|
+
global hotkeys (F6 run/stop, ESC stop), scheduler, and an image/color
|
|
110
|
+
**monitor/trigger** with tray icon; a **Test** tab to load and run visual test
|
|
111
|
+
suites.
|
|
102
112
|
|
|
103
113
|
### Smart recorder
|
|
104
114
|
|
|
@@ -6,8 +6,8 @@ ADB) input — then turn those flows into a **visual test framework** with
|
|
|
6
6
|
assertions, visual-regression baselines, HTML/JUnit reports, and a **web console**
|
|
7
7
|
for remote runs.
|
|
8
8
|
|
|
9
|
-
Python/PySide6, started as a port of a Qt/C++ AutoClicker
|
|
10
|
-
|
|
9
|
+
Python/PySide6, started as a port of a Qt/C++ AutoClicker. Legacy `.json` tasks
|
|
10
|
+
remain supported; structured control-flow blocks are a Python-only extension.
|
|
11
11
|
|
|
12
12
|
## Install
|
|
13
13
|
|
|
@@ -53,14 +53,24 @@ autoclicker-web --suites-dir examples # web console at http://127.0.0.1
|
|
|
53
53
|
|
|
54
54
|
## Authoring (desktop GUI)
|
|
55
55
|
|
|
56
|
-
Drag-and-drop **node flow graph**
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
56
|
+
Drag-and-drop **node flow graph** and an interoperable **step list**, with
|
|
57
|
+
nested **If/Else**, **While**, fixed-count loops, **ForEach** arrays, and
|
|
58
|
+
**Break/Continue** — no step numbers required. Double-click a block to edit its
|
|
59
|
+
body or branches. Legacy condition nodes retain their true/false ports.
|
|
60
|
+
|
|
61
|
+
Select consecutive steps and choose **包进… → 遍历数组** to reuse them for each
|
|
62
|
+
item. Enter a custom JSON array or generate a numeric range, then reference the
|
|
63
|
+
current item in text actions, assignments, and conditions. For example, iterate
|
|
64
|
+
`[1,2,3,4,5,6,7,8,9,10]` with variable `server`, click **`{server}服`**, perform
|
|
65
|
+
the actions, and return to the server-selection screen inside the same body.
|
|
66
|
+
Enable exact text matching so `1服` does not also match `11服`. A failed action
|
|
67
|
+
inside a block stops execution unless its failure policy explicitly skips it.
|
|
68
|
+
|
|
69
|
+
Also includes a snippet library, task packages (`.zip` with nested templates),
|
|
70
|
+
on-preview coordinate/color picking, template crop, single-step test run;
|
|
71
|
+
global hotkeys (F6 run/stop, ESC stop), scheduler, and an image/color
|
|
72
|
+
**monitor/trigger** with tray icon; a **Test** tab to load and run visual test
|
|
73
|
+
suites.
|
|
64
74
|
|
|
65
75
|
### Smart recorder
|
|
66
76
|
|
|
@@ -13,10 +13,10 @@ from collections import deque
|
|
|
13
13
|
from PySide6.QtCore import QLineF, QPointF, Qt, Signal
|
|
14
14
|
from PySide6.QtGui import QColor, QKeySequence, QPainter, QPen
|
|
15
15
|
from PySide6.QtWidgets import (
|
|
16
|
-
QDialog, QGraphicsScene, QGraphicsView, QInputDialog, QMenu,
|
|
16
|
+
QDialog, QGraphicsScene, QGraphicsView, QInputDialog, QMenu, QMessageBox,
|
|
17
17
|
)
|
|
18
18
|
|
|
19
|
-
from autoclicker.models import Step, StepType, Task
|
|
19
|
+
from autoclicker.models import Step, StepType, Task, BLOCK_TYPES, iter_steps
|
|
20
20
|
from autoclicker.gui.flow_node import FlowNode, FlowPort
|
|
21
21
|
from autoclicker.gui.flow_connection import FlowConnection
|
|
22
22
|
|
|
@@ -29,16 +29,13 @@ _ADD_MENU = {
|
|
|
29
29
|
"手势": [("拖拽", StepType.Drag), ("多点拖拽", StepType.MultiDrag),
|
|
30
30
|
("捏合", StepType.Pinch), ("滚轮", StepType.Scroll)],
|
|
31
31
|
"识别/变量": [("读取文字→变量", StepType.ReadText), ("设置变量", StepType.SetVar)],
|
|
32
|
-
"
|
|
33
|
-
|
|
34
|
-
|
|
32
|
+
"条件/循环": [("如果 / 否则", StepType.IfBlock), ("遍历数组", StepType.ForEach),
|
|
33
|
+
("重复指定次数", StepType.RepeatBlock), ("条件循环 (While)", StepType.WhileBlock),
|
|
34
|
+
("退出当前循环", StepType.Break), ("进入下一轮", StepType.Continue)],
|
|
35
35
|
"流程": [("等待", StepType.Wait), ("随机等待", StepType.RandomWait),
|
|
36
36
|
("等待画面稳定", StepType.WaitStable),
|
|
37
37
|
("等待图片出现", StepType.WaitImage), ("等待图片消失", StepType.WaitImageGone),
|
|
38
|
-
("
|
|
39
|
-
("跳转", StepType.Goto), ("调用子任务", StepType.CallSub),
|
|
40
|
-
("分组开始", StepType.GroupBegin), ("分组结束", StepType.GroupEnd),
|
|
41
|
-
("结束任务", StepType.Stop)],
|
|
38
|
+
("调用子任务", StepType.CallSub), ("结束任务", StepType.Stop)],
|
|
42
39
|
"窗口/高级": [("刷新/激活窗口", StepType.ActivateWindow), ("合成拖拽", StepType.MergeImage),
|
|
43
40
|
("HTTP 请求", StepType.HttpRequest), ("运行脚本(Python)", StepType.RunScript)],
|
|
44
41
|
}
|
|
@@ -143,14 +140,12 @@ class FlowEditor(QGraphicsView):
|
|
|
143
140
|
self._drag_line = None
|
|
144
141
|
item = self.itemAt(event.position().toPoint())
|
|
145
142
|
if isinstance(item, FlowPort) and item.port_type == FlowPort.INPUT and item is not self._drag_start_port:
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
if ok:
|
|
153
|
-
conn.set_repeat_count(count)
|
|
143
|
+
from_node = self._drag_start_port.parentItem()
|
|
144
|
+
to_node = item.parentItem()
|
|
145
|
+
if from_node in self._reachable(to_node, set()):
|
|
146
|
+
QMessageBox.information(self, "使用循环块", "无需连接回线:选中需要重复的步骤,右键选择「将选中步骤包进…」。")
|
|
147
|
+
else:
|
|
148
|
+
self.connect_nodes(self._drag_start_port, item)
|
|
154
149
|
self._drag_start_port = None
|
|
155
150
|
return
|
|
156
151
|
super().mouseReleaseEvent(event)
|
|
@@ -237,6 +232,10 @@ class FlowEditor(QGraphicsView):
|
|
|
237
232
|
menu.addAction("删除", self._delete_selected)
|
|
238
233
|
if self.selected_nodes():
|
|
239
234
|
menu.addAction("复制选中节点 (Ctrl+C)", self._copy_selected)
|
|
235
|
+
wrap_menu = menu.addMenu("将选中步骤包进…")
|
|
236
|
+
for label, st in [("遍历数组", StepType.ForEach), ("重复指定次数", StepType.RepeatBlock),
|
|
237
|
+
("条件循环", StepType.WhileBlock), ("如果 / 否则", StepType.IfBlock)]:
|
|
238
|
+
wrap_menu.addAction(label, lambda st=st: self.wrap_selection(st))
|
|
240
239
|
if self._clip and self._clip["steps"]:
|
|
241
240
|
menu.addAction("粘贴 (Ctrl+V)", self._paste_clip)
|
|
242
241
|
if node or conn or self.selected_nodes() or self._clip:
|
|
@@ -246,9 +245,68 @@ class FlowEditor(QGraphicsView):
|
|
|
246
245
|
for group, items in _ADD_MENU.items():
|
|
247
246
|
sub = add_menu.addMenu(group)
|
|
248
247
|
for label, st in items:
|
|
249
|
-
sub.addAction(label, lambda st=st, p=scene_pos: self.
|
|
248
|
+
sub.addAction(label, lambda st=st, p=scene_pos: self._add_configured_node(st, p))
|
|
250
249
|
menu.exec(event.globalPos())
|
|
251
250
|
|
|
251
|
+
def _add_configured_node(self, step_type, pos):
|
|
252
|
+
from autoclicker.gui.task_widget import StepEditorDialog, new_step
|
|
253
|
+
dlg = StepEditorDialog(new_step(step_type), self)
|
|
254
|
+
if dlg.exec() == QDialog.Accepted:
|
|
255
|
+
self.add_node(dlg.result_step(), pos)
|
|
256
|
+
|
|
257
|
+
def wrap_selection(self, step_type):
|
|
258
|
+
from autoclicker.gui.task_widget import StepEditorDialog, new_step
|
|
259
|
+
selected = set(self.selected_nodes())
|
|
260
|
+
if not selected or step_type not in BLOCK_TYPES:
|
|
261
|
+
return
|
|
262
|
+
starts = [n for n in selected if not any(
|
|
263
|
+
c.from_port.parentItem() in selected for c in n.input_port.connections)]
|
|
264
|
+
ordered = []
|
|
265
|
+
node = starts[0] if len(starts) == 1 else None
|
|
266
|
+
while node is not None and node not in ordered:
|
|
267
|
+
ordered.append(node)
|
|
268
|
+
edges = [c for p in node.output_ports() for c in p.connections
|
|
269
|
+
if c.to_port.parentItem() in selected]
|
|
270
|
+
node = edges[0].to_port.parentItem() if len(edges) == 1 else None
|
|
271
|
+
incoming = [c for n in selected for c in n.input_port.connections
|
|
272
|
+
if c.from_port.parentItem() not in selected]
|
|
273
|
+
outgoing = [c for n in selected for p in n.output_ports() for c in p.connections
|
|
274
|
+
if c.to_port.parentItem() not in selected]
|
|
275
|
+
internal = [c for n in selected for p in n.output_ports() for c in p.connections
|
|
276
|
+
if c.to_port.parentItem() in selected]
|
|
277
|
+
linear = (len(ordered) == len(selected) and len(internal) == len(selected) - 1
|
|
278
|
+
and all(c.to_port.parentItem() is ordered[0] for c in incoming)
|
|
279
|
+
and all(c.from_port.parentItem() is ordered[-1] for c in outgoing)
|
|
280
|
+
and all(c.repeat_count == 1 for c in internal + incoming + outgoing)
|
|
281
|
+
and not any(n.is_condition() for n in selected))
|
|
282
|
+
legacy = {StepType.Loop, StepType.Goto, StepType.DoUntil, StepType.WhileCondition,
|
|
283
|
+
StepType.IfImageFound, StepType.IfTextFound, StepType.IfColorMatch,
|
|
284
|
+
StepType.IfImageCount, StepType.IfVar, StepType.IfElse,
|
|
285
|
+
StepType.GroupBegin, StepType.GroupEnd}
|
|
286
|
+
if not linear or any(s.type in legacy or s.failGoto >= 0
|
|
287
|
+
for s in iter_steps([n.step() for n in self.nodes])):
|
|
288
|
+
QMessageBox.warning(self, "无法包进块", "请选择连续、无分叉的步骤;流程中的旧式步骤号跳转须先改为条件/循环块。")
|
|
289
|
+
return
|
|
290
|
+
block = new_step(step_type)
|
|
291
|
+
block.body = [copy.deepcopy(n.step()) for n in ordered]
|
|
292
|
+
dlg = StepEditorDialog(block, self)
|
|
293
|
+
if dlg.exec() != QDialog.Accepted:
|
|
294
|
+
return
|
|
295
|
+
block = dlg.result_step()
|
|
296
|
+
pos = ordered[0].pos()
|
|
297
|
+
sources = [c.from_port for c in incoming]
|
|
298
|
+
targets = [c.to_port for c in outgoing]
|
|
299
|
+
self._scene.clearSelection()
|
|
300
|
+
for n in selected:
|
|
301
|
+
n.setSelected(True)
|
|
302
|
+
self._delete_selected()
|
|
303
|
+
wrapped = self.add_node(block, pos)
|
|
304
|
+
for source in sources:
|
|
305
|
+
self.connect_nodes(source, wrapped.input_port)
|
|
306
|
+
for target in targets:
|
|
307
|
+
self.connect_nodes(wrapped.output_port, target)
|
|
308
|
+
wrapped.setSelected(True)
|
|
309
|
+
|
|
252
310
|
# ---- helpers ----
|
|
253
311
|
def _node_at(self, view_pos) -> FlowNode | None:
|
|
254
312
|
item = self.itemAt(view_pos)
|
|
@@ -7,9 +7,15 @@ from PySide6.QtCore import QRectF, Qt
|
|
|
7
7
|
from PySide6.QtGui import QColor, QPen
|
|
8
8
|
from PySide6.QtWidgets import QGraphicsEllipseItem, QGraphicsItem, QGraphicsTextItem
|
|
9
9
|
|
|
10
|
-
from autoclicker.models import Step, StepType
|
|
10
|
+
from autoclicker.models import Step, StepType, BLOCK_TYPES
|
|
11
11
|
|
|
12
12
|
_COLORS = {
|
|
13
|
+
StepType.IfBlock: (123, 65, 156),
|
|
14
|
+
StepType.ForEach: (0, 121, 107),
|
|
15
|
+
StepType.RepeatBlock: (192, 92, 40),
|
|
16
|
+
StepType.WhileBlock: (192, 92, 40),
|
|
17
|
+
StepType.Break: (96, 125, 139),
|
|
18
|
+
StepType.Continue: (96, 125, 139),
|
|
13
19
|
StepType.ClickImage: (66, 133, 244),
|
|
14
20
|
StepType.ClickText: (52, 168, 83),
|
|
15
21
|
StepType.ClickPosition: (251, 188, 4),
|
|
@@ -141,7 +147,20 @@ class FlowNode(QGraphicsItem):
|
|
|
141
147
|
painter.drawRoundedRect(QRectF(0, 0, self.WIDTH, self.HEIGHT), 8, 8)
|
|
142
148
|
|
|
143
149
|
def _update_label(self):
|
|
144
|
-
self.
|
|
150
|
+
label = self._step.display_name()
|
|
151
|
+
if self._step.type in BLOCK_TYPES:
|
|
152
|
+
title = {StepType.IfBlock: "如果 / 否则", StepType.ForEach: "遍历数组",
|
|
153
|
+
StepType.RepeatBlock: "重复指定次数", StepType.WhileBlock: "条件循环"}[self._step.type]
|
|
154
|
+
count = len(self._step.body) + len(self._step.elseBody)
|
|
155
|
+
self._label.setPlainText(f"{title} · {count} 步\n双击编辑块内步骤")
|
|
156
|
+
body = "\n".join(f" {s.display_name()}" for s in self._step.body)
|
|
157
|
+
other = "\n".join(f" {s.display_name()}" for s in self._step.elseBody)
|
|
158
|
+
label += f"\n执行步骤:\n{body or ' (空)'}"
|
|
159
|
+
if self._step.type == StepType.IfBlock:
|
|
160
|
+
label += f"\n否则:\n{other or ' (空)'}"
|
|
161
|
+
else:
|
|
162
|
+
self._label.setPlainText(label)
|
|
163
|
+
self.setToolTip(label)
|
|
145
164
|
self._label.setPos(8, 8)
|
|
146
165
|
|
|
147
166
|
def _color(self) -> QColor:
|
|
@@ -955,7 +955,8 @@ class MainWindow(QMainWindow):
|
|
|
955
955
|
# ---- task package (.zip: task.json + images/) ----
|
|
956
956
|
def _iter_image_slots(self, task: Task):
|
|
957
957
|
"""Yield (get, set) callables for every image path field in the task."""
|
|
958
|
-
|
|
958
|
+
from autoclicker.models import iter_steps
|
|
959
|
+
for s in iter_steps(task.steps):
|
|
959
960
|
if s.imagePath:
|
|
960
961
|
yield (lambda s=s: s.imagePath), (lambda v, s=s: setattr(s, "imagePath", v))
|
|
961
962
|
for i in range(len(s.imagePaths)):
|