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
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""Reusable step-snippet library — port of snippetlibrary.cpp.
|
|
2
|
+
|
|
3
|
+
Snippets are JSON files under <AppDataLocation>/snippets (same folder as the C++
|
|
4
|
+
app, so they are shared). Each holds a named list of Steps that can be inserted
|
|
5
|
+
into the current task.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import os
|
|
11
|
+
import re
|
|
12
|
+
|
|
13
|
+
from PySide6.QtCore import Qt, Signal, QStandardPaths
|
|
14
|
+
from PySide6.QtWidgets import (
|
|
15
|
+
QHBoxLayout, QInputDialog, QLabel, QListWidget, QListWidgetItem,
|
|
16
|
+
QPushButton, QVBoxLayout, QWidget,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
from autoclicker.models import Step
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class SnippetLibrary(QWidget):
|
|
23
|
+
insertRequested = Signal(object) # emits list[Step]
|
|
24
|
+
|
|
25
|
+
def __init__(self, parent=None):
|
|
26
|
+
super().__init__(parent)
|
|
27
|
+
lay = QVBoxLayout(self)
|
|
28
|
+
hint = QLabel("步骤模板库 — 保存常用步骤组合,一键插入复用")
|
|
29
|
+
hint.setStyleSheet("color: gray;")
|
|
30
|
+
lay.addWidget(hint)
|
|
31
|
+
self.list = QListWidget()
|
|
32
|
+
lay.addWidget(self.list)
|
|
33
|
+
row = QHBoxLayout()
|
|
34
|
+
for text, slot in [("插入到任务", self._insert), ("重命名", self._rename), ("删除", self._delete)]:
|
|
35
|
+
b = QPushButton(text)
|
|
36
|
+
b.clicked.connect(slot)
|
|
37
|
+
row.addWidget(b)
|
|
38
|
+
lay.addLayout(row)
|
|
39
|
+
|
|
40
|
+
os.makedirs(self._dir(), exist_ok=True)
|
|
41
|
+
self.refresh()
|
|
42
|
+
|
|
43
|
+
def _dir(self) -> str:
|
|
44
|
+
base = QStandardPaths.writableLocation(QStandardPaths.AppDataLocation)
|
|
45
|
+
return os.path.join(base, "snippets")
|
|
46
|
+
|
|
47
|
+
def save_snippet(self, name: str, steps: list[Step]):
|
|
48
|
+
if not steps:
|
|
49
|
+
return
|
|
50
|
+
filename = re.sub(r"[^\w\-]", "_", name) + ".json"
|
|
51
|
+
root = {"name": name, "step_count": len(steps), "steps": [s.to_json() for s in steps]}
|
|
52
|
+
with open(os.path.join(self._dir(), filename), "w", encoding="utf-8") as f:
|
|
53
|
+
json.dump(root, f, ensure_ascii=False, indent=4)
|
|
54
|
+
self.refresh()
|
|
55
|
+
|
|
56
|
+
def _load(self, filename: str) -> list[Step]:
|
|
57
|
+
try:
|
|
58
|
+
with open(os.path.join(self._dir(), filename), "r", encoding="utf-8") as f:
|
|
59
|
+
doc = json.load(f)
|
|
60
|
+
except (OSError, json.JSONDecodeError):
|
|
61
|
+
return []
|
|
62
|
+
return [Step.from_json(o) for o in doc.get("steps", [])]
|
|
63
|
+
|
|
64
|
+
def refresh(self):
|
|
65
|
+
self.list.clear()
|
|
66
|
+
d = self._dir()
|
|
67
|
+
if not os.path.isdir(d):
|
|
68
|
+
return
|
|
69
|
+
for fn in sorted(f for f in os.listdir(d) if f.endswith(".json")):
|
|
70
|
+
try:
|
|
71
|
+
with open(os.path.join(d, fn), "r", encoding="utf-8") as f:
|
|
72
|
+
doc = json.load(f)
|
|
73
|
+
except (OSError, json.JSONDecodeError):
|
|
74
|
+
continue
|
|
75
|
+
name = doc.get("name", os.path.splitext(fn)[0])
|
|
76
|
+
count = doc.get("step_count", len(doc.get("steps", [])))
|
|
77
|
+
item = QListWidgetItem(f"{name} ({count} 步)")
|
|
78
|
+
item.setData(Qt.UserRole, fn)
|
|
79
|
+
self.list.addItem(item)
|
|
80
|
+
|
|
81
|
+
def _insert(self):
|
|
82
|
+
item = self.list.currentItem()
|
|
83
|
+
if not item:
|
|
84
|
+
return
|
|
85
|
+
steps = self._load(item.data(Qt.UserRole))
|
|
86
|
+
if steps:
|
|
87
|
+
self.insertRequested.emit(steps)
|
|
88
|
+
|
|
89
|
+
def _delete(self):
|
|
90
|
+
item = self.list.currentItem()
|
|
91
|
+
if not item:
|
|
92
|
+
return
|
|
93
|
+
try:
|
|
94
|
+
os.remove(os.path.join(self._dir(), item.data(Qt.UserRole)))
|
|
95
|
+
except OSError:
|
|
96
|
+
pass
|
|
97
|
+
self.refresh()
|
|
98
|
+
|
|
99
|
+
def _rename(self):
|
|
100
|
+
item = self.list.currentItem()
|
|
101
|
+
if not item:
|
|
102
|
+
return
|
|
103
|
+
fn = item.data(Qt.UserRole)
|
|
104
|
+
path = os.path.join(self._dir(), fn)
|
|
105
|
+
try:
|
|
106
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
107
|
+
doc = json.load(f)
|
|
108
|
+
except (OSError, json.JSONDecodeError):
|
|
109
|
+
return
|
|
110
|
+
old = doc.get("name", "")
|
|
111
|
+
new, ok = QInputDialog.getText(self, "重命名", "新名称:", text=old)
|
|
112
|
+
if not ok or not new or new == old:
|
|
113
|
+
return
|
|
114
|
+
doc["name"] = new
|
|
115
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
116
|
+
json.dump(doc, f, ensure_ascii=False, indent=4)
|
|
117
|
+
self.refresh()
|
|
@@ -0,0 +1,547 @@
|
|
|
1
|
+
"""Linear step-list editor — port of taskwidget.cpp.
|
|
2
|
+
|
|
3
|
+
A QListWidget of steps with add/remove/move/copy/paste, plus a dynamic step
|
|
4
|
+
editor dialog that shows only the fields relevant to the chosen step type
|
|
5
|
+
(mirroring the conditional field building in the C++ TaskWidget). Advanced
|
|
6
|
+
retry / repeat / until sections are shown for action-type steps.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import copy
|
|
11
|
+
|
|
12
|
+
from PySide6.QtCore import Qt, Signal
|
|
13
|
+
from PySide6.QtWidgets import (
|
|
14
|
+
QCheckBox, QComboBox, QDialog, QDialogButtonBox, QDoubleSpinBox, QFileDialog,
|
|
15
|
+
QFormLayout, QHBoxLayout, QLabel, QLineEdit, QListWidget, QListWidgetItem,
|
|
16
|
+
QPushButton, QSpinBox, QVBoxLayout, QWidget,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
from autoclicker.models import (
|
|
20
|
+
RetryConfig, RepeatConfig, UntilConfig, Step, StepType, Task,
|
|
21
|
+
RetryPolicy, UntilCondition,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
# Human labels for the type picker (add order roughly matches C++ menu)
|
|
25
|
+
TYPE_LABELS = [
|
|
26
|
+
(StepType.ClickImage, "找图点击"),
|
|
27
|
+
(StepType.ClickText, "找字点击"),
|
|
28
|
+
(StepType.ClickPosition, "坐标点击"),
|
|
29
|
+
(StepType.Wait, "等待"),
|
|
30
|
+
(StepType.RandomWait, "随机等待"),
|
|
31
|
+
(StepType.Loop, "循环"),
|
|
32
|
+
(StepType.Goto, "跳转"),
|
|
33
|
+
(StepType.IfImageFound, "条件(图片)"),
|
|
34
|
+
(StepType.IfTextFound, "条件(文字)"),
|
|
35
|
+
(StepType.IfColorMatch, "条件(颜色)"),
|
|
36
|
+
(StepType.IfImageCount, "条件(图片数量)"),
|
|
37
|
+
(StepType.IfVar, "条件(变量)"),
|
|
38
|
+
(StepType.IfElse, "If/Else"),
|
|
39
|
+
(StepType.CallSub, "调用子任务"),
|
|
40
|
+
(StepType.SetVar, "设置变量"),
|
|
41
|
+
(StepType.KeyPress, "按键"),
|
|
42
|
+
(StepType.TypeText, "输入文字"),
|
|
43
|
+
(StepType.Hotkey, "组合键"),
|
|
44
|
+
(StepType.LongPress, "长按"),
|
|
45
|
+
(StepType.Drag, "拖拽"),
|
|
46
|
+
(StepType.MultiDrag, "多点拖拽"),
|
|
47
|
+
(StepType.Pinch, "捏合"),
|
|
48
|
+
(StepType.Scroll, "滚轮"),
|
|
49
|
+
(StepType.WaitImage, "等待图片出现"),
|
|
50
|
+
(StepType.WaitImageGone, "等待图片消失"),
|
|
51
|
+
(StepType.MergeImage, "合成拖拽"),
|
|
52
|
+
(StepType.ClickAll, "点击所有匹配"),
|
|
53
|
+
(StepType.ClickRelative, "相对点击(last+偏移)"),
|
|
54
|
+
(StepType.ReadText, "读取文字→变量"),
|
|
55
|
+
(StepType.DoUntil, "循环直到"),
|
|
56
|
+
(StepType.WhileCondition, "While 条件"),
|
|
57
|
+
(StepType.GroupBegin, "分组开始"),
|
|
58
|
+
(StepType.GroupEnd, "分组结束"),
|
|
59
|
+
]
|
|
60
|
+
|
|
61
|
+
_IMAGE_TYPES = {StepType.ClickImage, StepType.WaitImage, StepType.WaitImageGone,
|
|
62
|
+
StepType.MergeImage, StepType.ClickAll, StepType.IfImageFound,
|
|
63
|
+
StepType.IfImageCount}
|
|
64
|
+
_IF_TYPES = {StepType.IfImageFound, StepType.IfTextFound, StepType.IfElse,
|
|
65
|
+
StepType.IfColorMatch, StepType.IfImageCount, StepType.IfVar}
|
|
66
|
+
_ACTION_TYPES = {StepType.ClickImage, StepType.ClickText, StepType.ClickPosition,
|
|
67
|
+
StepType.CallSub, StepType.WaitImage, StepType.WaitImageGone,
|
|
68
|
+
StepType.MergeImage, StepType.ClickAll}
|
|
69
|
+
_UNTIL_TYPES = {StepType.DoUntil, StepType.WhileCondition}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class ImageListEditor(QWidget):
|
|
73
|
+
"""Edit one-or-more template image paths."""
|
|
74
|
+
def __init__(self, paths: list[str], parent=None):
|
|
75
|
+
super().__init__(parent)
|
|
76
|
+
lay = QVBoxLayout(self)
|
|
77
|
+
lay.setContentsMargins(0, 0, 0, 0)
|
|
78
|
+
self.list = QListWidget()
|
|
79
|
+
self.list.setMaximumHeight(90)
|
|
80
|
+
for p in paths:
|
|
81
|
+
self.list.addItem(p)
|
|
82
|
+
row = QHBoxLayout()
|
|
83
|
+
add = QPushButton("添加图片…")
|
|
84
|
+
rm = QPushButton("移除")
|
|
85
|
+
add.clicked.connect(self._add)
|
|
86
|
+
rm.clicked.connect(self._remove)
|
|
87
|
+
row.addWidget(add)
|
|
88
|
+
row.addWidget(rm)
|
|
89
|
+
lay.addWidget(self.list)
|
|
90
|
+
lay.addLayout(row)
|
|
91
|
+
|
|
92
|
+
def _add(self):
|
|
93
|
+
files, _ = QFileDialog.getOpenFileNames(self, "选择模板图片", "", "图片 (*.png *.jpg *.jpeg *.bmp)")
|
|
94
|
+
for f in files:
|
|
95
|
+
self.list.addItem(f)
|
|
96
|
+
|
|
97
|
+
def _remove(self):
|
|
98
|
+
for it in self.list.selectedItems():
|
|
99
|
+
self.list.takeItem(self.list.row(it))
|
|
100
|
+
|
|
101
|
+
def paths(self) -> list[str]:
|
|
102
|
+
return [self.list.item(i).text() for i in range(self.list.count())]
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class StepEditorDialog(QDialog):
|
|
106
|
+
def __init__(self, step: Step, parent=None):
|
|
107
|
+
super().__init__(parent)
|
|
108
|
+
self.setWindowTitle("编辑步骤")
|
|
109
|
+
self.setMinimumWidth(420)
|
|
110
|
+
self._step = copy.deepcopy(step)
|
|
111
|
+
self.w: dict = {}
|
|
112
|
+
|
|
113
|
+
outer = QVBoxLayout(self)
|
|
114
|
+
self.form = QFormLayout()
|
|
115
|
+
outer.addLayout(self.form)
|
|
116
|
+
self._build_fields(self._step.type)
|
|
117
|
+
|
|
118
|
+
buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
|
|
119
|
+
buttons.accepted.connect(self.accept)
|
|
120
|
+
buttons.rejected.connect(self.reject)
|
|
121
|
+
outer.addWidget(buttons)
|
|
122
|
+
|
|
123
|
+
# -- field builders -------------------------------------------------
|
|
124
|
+
def _spin(self, key, val, lo=-100000, hi=100000):
|
|
125
|
+
s = QSpinBox()
|
|
126
|
+
s.setRange(lo, hi)
|
|
127
|
+
s.setValue(val)
|
|
128
|
+
self.w[key] = s
|
|
129
|
+
return s
|
|
130
|
+
|
|
131
|
+
def _dspin(self, key, val):
|
|
132
|
+
s = QDoubleSpinBox()
|
|
133
|
+
s.setRange(0.0, 1.0)
|
|
134
|
+
s.setSingleStep(0.05)
|
|
135
|
+
s.setValue(val)
|
|
136
|
+
self.w[key] = s
|
|
137
|
+
return s
|
|
138
|
+
|
|
139
|
+
def _line(self, key, val):
|
|
140
|
+
e = QLineEdit(val)
|
|
141
|
+
self.w[key] = e
|
|
142
|
+
return e
|
|
143
|
+
|
|
144
|
+
def _build_fields(self, t: StepType):
|
|
145
|
+
s = self._step
|
|
146
|
+
if t in _IMAGE_TYPES:
|
|
147
|
+
paths = list(s.imagePaths) if s.imagePaths else ([s.imagePath] if s.imagePath else [])
|
|
148
|
+
self.w["images"] = ImageListEditor(paths)
|
|
149
|
+
self.form.addRow("模板图片", self.w["images"])
|
|
150
|
+
self.form.addRow("匹配阈值", self._dspin("threshold", s.threshold))
|
|
151
|
+
self.form.addRow("ROI (x,y,w,h)", self._line("roi", _roi_to_str(s.roiRect)))
|
|
152
|
+
mc = QCheckBox("多尺度匹配")
|
|
153
|
+
mc.setChecked(s.multiScale)
|
|
154
|
+
self.w["multiScale"] = mc
|
|
155
|
+
self.form.addRow("", mc)
|
|
156
|
+
if t in (StepType.ClickImage, StepType.WaitImage, StepType.WaitImageGone, StepType.MergeImage):
|
|
157
|
+
self.form.addRow("超时(ms)", self._spin("timeoutMs", s.timeoutMs, 0, 3600000))
|
|
158
|
+
if t == StepType.IfImageCount:
|
|
159
|
+
self.form.addRow("最少匹配数", self._spin("minMatchCount", s.minMatchCount, 1, 100))
|
|
160
|
+
if t == StepType.ClickAll:
|
|
161
|
+
self.form.addRow("点击间隔(ms)", self._spin("clickDelayMs", s.clickDelayMs, 0, 60000))
|
|
162
|
+
if t == StepType.MergeImage:
|
|
163
|
+
self.form.addRow("合成间隔(ms)", self._spin("mergeDelayMs", s.mergeDelayMs, 0, 60000))
|
|
164
|
+
self.form.addRow("拖拽时长(ms)", self._spin("durationMs", s.durationMs, 0, 60000))
|
|
165
|
+
|
|
166
|
+
if t in (StepType.ClickText, StepType.IfTextFound):
|
|
167
|
+
self.form.addRow("目标文字", self._line("text", s.text))
|
|
168
|
+
if t == StepType.ClickText:
|
|
169
|
+
self.form.addRow("超时(ms)", self._spin("timeoutMs", s.timeoutMs, 0, 3600000))
|
|
170
|
+
if t == StepType.TypeText:
|
|
171
|
+
self.form.addRow("输入文字", self._line("text", s.text))
|
|
172
|
+
|
|
173
|
+
if t in (StepType.ClickPosition, StepType.LongPress, StepType.Drag,
|
|
174
|
+
StepType.Pinch, StepType.Scroll, StepType.IfColorMatch):
|
|
175
|
+
self.form.addRow("X", self._spin("x", s.x))
|
|
176
|
+
self.form.addRow("Y", self._spin("y", s.y))
|
|
177
|
+
if t == StepType.ClickPosition:
|
|
178
|
+
uv = QCheckBox("使用 last_x/last_y 变量")
|
|
179
|
+
uv.setChecked(s.varName == "use_var")
|
|
180
|
+
self.w["use_var"] = uv
|
|
181
|
+
self.form.addRow("", uv)
|
|
182
|
+
if t == StepType.Drag:
|
|
183
|
+
self.form.addRow("X2", self._spin("x2", s.x2))
|
|
184
|
+
self.form.addRow("Y2", self._spin("y2", s.y2))
|
|
185
|
+
self.form.addRow("时长(ms)", self._spin("durationMs", s.durationMs, 0, 60000))
|
|
186
|
+
if t == StepType.LongPress:
|
|
187
|
+
self.form.addRow("按住(ms)", self._spin("holdMs", s.holdMs, 0, 600000))
|
|
188
|
+
if t == StepType.Pinch:
|
|
189
|
+
self.form.addRow("起始距离", self._spin("pinchStartDist", s.pinchStartDist, 0, 5000))
|
|
190
|
+
self.form.addRow("结束距离", self._spin("pinchEndDist", s.pinchEndDist, 0, 5000))
|
|
191
|
+
self.form.addRow("时长(ms)", self._spin("durationMs", s.durationMs, 0, 60000))
|
|
192
|
+
if t == StepType.Scroll:
|
|
193
|
+
self.form.addRow("滚动格数", self._spin("scrollAmount", s.scrollAmount, -100, 100))
|
|
194
|
+
|
|
195
|
+
# Click offset from the matched image center (0,0 = center)
|
|
196
|
+
if t in (StepType.ClickImage, StepType.ClickText, StepType.ClickAll):
|
|
197
|
+
self.form.addRow("点击偏移X", self._spin("x", s.x))
|
|
198
|
+
self.form.addRow("点击偏移Y", self._spin("y", s.y))
|
|
199
|
+
|
|
200
|
+
if t == StepType.ClickRelative:
|
|
201
|
+
self.form.addRow("偏移X (相对 last_x)", self._spin("x", s.x))
|
|
202
|
+
self.form.addRow("偏移Y (相对 last_y)", self._spin("y", s.y))
|
|
203
|
+
|
|
204
|
+
if t == StepType.ReadText:
|
|
205
|
+
self.form.addRow("目标变量", self._line("varName", s.varName))
|
|
206
|
+
mode = QComboBox()
|
|
207
|
+
mode.addItem("文字", "text")
|
|
208
|
+
mode.addItem("数字", "number")
|
|
209
|
+
mode.setCurrentIndex(1 if s.varValue == "number" else 0)
|
|
210
|
+
self.w["readmode"] = mode
|
|
211
|
+
self.form.addRow("读取模式", mode)
|
|
212
|
+
self.form.addRow("过滤子串(可选)", self._line("text", s.text))
|
|
213
|
+
self.form.addRow("ROI (x,y,w,h)", self._line("roi", _roi_to_str(s.roiRect)))
|
|
214
|
+
|
|
215
|
+
if t in (StepType.ClickImage, StepType.ClickText, StepType.ClickPosition,
|
|
216
|
+
StepType.ClickAll, StepType.ClickRelative):
|
|
217
|
+
self.form.addRow("随机偏移(px)", self._spin("randomOffset", s.randomOffset, 0, 500))
|
|
218
|
+
|
|
219
|
+
if t == StepType.Wait:
|
|
220
|
+
self.form.addRow("等待(ms)", self._spin("waitMs", s.waitMs, 0, 3600000))
|
|
221
|
+
if t == StepType.RandomWait:
|
|
222
|
+
self.form.addRow("最小(ms)", self._spin("waitMs", s.waitMs, 0, 3600000))
|
|
223
|
+
self.form.addRow("最大(ms)", self._spin("waitMsMax", s.waitMsMax, 0, 3600000))
|
|
224
|
+
|
|
225
|
+
if t in (StepType.Loop, StepType.DoUntil, StepType.WhileCondition):
|
|
226
|
+
self.form.addRow("回到步骤(从1)", self._spin("loopTarget1", s.loopTarget + 1, 1, 100000))
|
|
227
|
+
if t == StepType.Loop:
|
|
228
|
+
self.form.addRow("循环次数(-1=无限)", self._spin("loopCount", s.loopCount, -1, 1000000))
|
|
229
|
+
if t == StepType.Goto:
|
|
230
|
+
self.form.addRow("跳转到步骤(从1)", self._spin("gotoTarget1", s.gotoTarget + 1, 1, 100000))
|
|
231
|
+
|
|
232
|
+
if t in _IF_TYPES:
|
|
233
|
+
self.form.addRow("否则跳过步数", self._spin("elseSkip", s.elseSkip, 0, 1000))
|
|
234
|
+
|
|
235
|
+
if t == StepType.CallSub:
|
|
236
|
+
self.form.addRow("子任务路径", self._line("subTaskPath", s.subTaskPath))
|
|
237
|
+
if t == StepType.SetVar:
|
|
238
|
+
self.form.addRow("变量名", self._line("varName", s.varName))
|
|
239
|
+
self.form.addRow("值 (++/-- 支持)", self._line("varValue", s.varValue))
|
|
240
|
+
if t in (StepType.IfVar, StepType.WhileCondition):
|
|
241
|
+
self.form.addRow("变量名", self._line("varName", s.varName))
|
|
242
|
+
op = QComboBox()
|
|
243
|
+
op.addItems(["==", "!=", ">", "<", ">=", "<="])
|
|
244
|
+
if s.compareOp:
|
|
245
|
+
op.setCurrentText(s.compareOp)
|
|
246
|
+
self.w["compareOp"] = op
|
|
247
|
+
self.form.addRow("比较", op)
|
|
248
|
+
self.form.addRow("比较值", self._line("compareValue", s.compareValue))
|
|
249
|
+
if t == StepType.KeyPress:
|
|
250
|
+
self.form.addRow("按键", self._line("keyName", s.keyName))
|
|
251
|
+
if t == StepType.Hotkey:
|
|
252
|
+
self.form.addRow("修饰键", self._line("modifierKey", s.modifierKey))
|
|
253
|
+
self.form.addRow("按键", self._line("keyName", s.keyName))
|
|
254
|
+
if t == StepType.IfColorMatch:
|
|
255
|
+
self.form.addRow("目标颜色(#RRGGBB)", self._line("targetColor", s.targetColor))
|
|
256
|
+
self.form.addRow("容差", self._spin("colorTolerance", s.colorTolerance, 0, 255))
|
|
257
|
+
if t == StepType.MultiDrag:
|
|
258
|
+
self.form.addRow("拖拽点 (x,y;x,y…)", self._line("dragPoints", _points_to_str(s.dragPoints)))
|
|
259
|
+
self.form.addRow("每段时长(ms)", self._spin("durationMs", s.durationMs, 0, 60000))
|
|
260
|
+
self.form.addRow("段间延迟(ms)", self._spin("dragStepDelay", s.dragStepDelay, 0, 60000))
|
|
261
|
+
if t == StepType.GroupBegin:
|
|
262
|
+
self.form.addRow("分组名", self._line("groupName", s.groupName))
|
|
263
|
+
dc = QCheckBox("禁用该分组")
|
|
264
|
+
dc.setChecked(s.disabled)
|
|
265
|
+
self.w["disabled"] = dc
|
|
266
|
+
self.form.addRow("", dc)
|
|
267
|
+
|
|
268
|
+
if t in _UNTIL_TYPES:
|
|
269
|
+
self._build_until(s.until)
|
|
270
|
+
if t in _ACTION_TYPES:
|
|
271
|
+
self._build_retry(s.retry)
|
|
272
|
+
self._build_repeat(s.repeat)
|
|
273
|
+
|
|
274
|
+
def _build_until(self, u: UntilConfig):
|
|
275
|
+
self.form.addRow(QLabel("── 条件 ──"))
|
|
276
|
+
combo = QComboBox()
|
|
277
|
+
for uc, name in [(UntilCondition.NoneCond, "无"), (UntilCondition.ImageFound, "图片出现"),
|
|
278
|
+
(UntilCondition.ImageGone, "图片消失"), (UntilCondition.TextFound, "文字出现"),
|
|
279
|
+
(UntilCondition.ColorMatch, "颜色匹配")]:
|
|
280
|
+
combo.addItem(name, uc)
|
|
281
|
+
idx = combo.findData(u.type)
|
|
282
|
+
combo.setCurrentIndex(max(0, idx))
|
|
283
|
+
self.w["until_type"] = combo
|
|
284
|
+
self.form.addRow("条件类型", combo)
|
|
285
|
+
self.form.addRow("条件图片", self._line("until_image", u.image))
|
|
286
|
+
self.form.addRow("条件文字", self._line("until_text", u.text))
|
|
287
|
+
self.form.addRow("条件颜色", self._line("until_color", u.color))
|
|
288
|
+
self.form.addRow("条件超时(ms)", self._spin("until_timeoutMs", u.timeoutMs, 0, 3600000))
|
|
289
|
+
|
|
290
|
+
def _build_retry(self, r: RetryConfig):
|
|
291
|
+
self.form.addRow(QLabel("── 失败处理 ──"))
|
|
292
|
+
combo = QComboBox()
|
|
293
|
+
for rp, name in [(RetryPolicy.NoneP, "无"), (RetryPolicy.Retry, "重试"),
|
|
294
|
+
(RetryPolicy.Skip, "跳过"), (RetryPolicy.Abort, "中止任务")]:
|
|
295
|
+
combo.addItem(name, rp)
|
|
296
|
+
combo.setCurrentIndex(max(0, combo.findData(r.policy)))
|
|
297
|
+
self.w["retry_policy"] = combo
|
|
298
|
+
self.form.addRow("策略", combo)
|
|
299
|
+
self.form.addRow("重试次数", self._spin("retry_count", r.count, 0, 1000))
|
|
300
|
+
self.form.addRow("重试间隔(ms)", self._spin("retry_delayMs", r.delayMs, 0, 600000))
|
|
301
|
+
|
|
302
|
+
def _build_repeat(self, rp: RepeatConfig):
|
|
303
|
+
self.form.addRow(QLabel("── 重复 ──"))
|
|
304
|
+
self.form.addRow("重复次数(1=一次)", self._spin("repeat_times", rp.times, -1, 100000))
|
|
305
|
+
self.form.addRow("重复间隔(ms)", self._spin("repeat_delayMs", rp.delayMs, 0, 600000))
|
|
306
|
+
|
|
307
|
+
# -- collect --------------------------------------------------------
|
|
308
|
+
def result_step(self) -> Step:
|
|
309
|
+
s = self._step
|
|
310
|
+
t = s.type
|
|
311
|
+
w = self.w
|
|
312
|
+
|
|
313
|
+
def gi(key):
|
|
314
|
+
return w[key].value()
|
|
315
|
+
|
|
316
|
+
def gt(key):
|
|
317
|
+
return w[key].text()
|
|
318
|
+
|
|
319
|
+
if "images" in w:
|
|
320
|
+
paths = w["images"].paths()
|
|
321
|
+
if len(paths) == 1:
|
|
322
|
+
s.imagePath, s.imagePaths = paths[0], []
|
|
323
|
+
else:
|
|
324
|
+
s.imagePath, s.imagePaths = "", paths
|
|
325
|
+
if "threshold" in w:
|
|
326
|
+
s.threshold = w["threshold"].value()
|
|
327
|
+
if "roi" in w:
|
|
328
|
+
s.roiRect = _str_to_roi(gt("roi"))
|
|
329
|
+
if "multiScale" in w:
|
|
330
|
+
s.multiScale = w["multiScale"].isChecked()
|
|
331
|
+
for key in ("timeoutMs", "minMatchCount", "clickDelayMs", "mergeDelayMs",
|
|
332
|
+
"x", "y", "x2", "y2", "durationMs", "holdMs", "pinchStartDist",
|
|
333
|
+
"pinchEndDist", "scrollAmount", "randomOffset", "waitMs", "waitMsMax",
|
|
334
|
+
"loopCount", "elseSkip", "colorTolerance", "dragStepDelay"):
|
|
335
|
+
if key in w:
|
|
336
|
+
setattr(s, key, gi(key))
|
|
337
|
+
if "loopTarget1" in w:
|
|
338
|
+
s.loopTarget = gi("loopTarget1") - 1
|
|
339
|
+
if "gotoTarget1" in w:
|
|
340
|
+
s.gotoTarget = gi("gotoTarget1") - 1
|
|
341
|
+
for key in ("text", "subTaskPath", "varName", "varValue", "keyName",
|
|
342
|
+
"modifierKey", "targetColor", "groupName", "compareValue"):
|
|
343
|
+
if key in w:
|
|
344
|
+
setattr(s, key, gt(key))
|
|
345
|
+
if "compareOp" in w:
|
|
346
|
+
s.compareOp = w["compareOp"].currentText()
|
|
347
|
+
if "readmode" in w:
|
|
348
|
+
s.varValue = w["readmode"].currentData()
|
|
349
|
+
if "use_var" in w:
|
|
350
|
+
s.varName = "use_var" if w["use_var"].isChecked() else ""
|
|
351
|
+
if "disabled" in w:
|
|
352
|
+
s.disabled = w["disabled"].isChecked()
|
|
353
|
+
if "dragPoints" in w:
|
|
354
|
+
s.dragPoints = _str_to_points(gt("dragPoints"))
|
|
355
|
+
if "until_type" in w:
|
|
356
|
+
s.until.type = w["until_type"].currentData()
|
|
357
|
+
s.until.image = gt("until_image")
|
|
358
|
+
s.until.text = gt("until_text")
|
|
359
|
+
s.until.color = gt("until_color")
|
|
360
|
+
s.until.timeoutMs = gi("until_timeoutMs")
|
|
361
|
+
if "retry_policy" in w:
|
|
362
|
+
s.retry.policy = w["retry_policy"].currentData()
|
|
363
|
+
s.retry.count = gi("retry_count")
|
|
364
|
+
s.retry.delayMs = gi("retry_delayMs")
|
|
365
|
+
if "repeat_times" in w:
|
|
366
|
+
s.repeat.times = gi("repeat_times")
|
|
367
|
+
s.repeat.delayMs = gi("repeat_delayMs")
|
|
368
|
+
return s
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
class TaskWidget(QWidget):
|
|
372
|
+
taskChanged = Signal()
|
|
373
|
+
testStepRequested = Signal(object) # emits a single Step to test-run
|
|
374
|
+
|
|
375
|
+
def __init__(self, parent=None):
|
|
376
|
+
super().__init__(parent)
|
|
377
|
+
self._task = Task()
|
|
378
|
+
self._clipboard: list[Step] = []
|
|
379
|
+
|
|
380
|
+
lay = QVBoxLayout(self)
|
|
381
|
+
top = QHBoxLayout()
|
|
382
|
+
self.type_combo = QComboBox()
|
|
383
|
+
for st, label in TYPE_LABELS:
|
|
384
|
+
self.type_combo.addItem(label, st)
|
|
385
|
+
add_btn = QPushButton("添加")
|
|
386
|
+
add_btn.clicked.connect(self._add)
|
|
387
|
+
top.addWidget(self.type_combo, 1)
|
|
388
|
+
top.addWidget(add_btn)
|
|
389
|
+
lay.addLayout(top)
|
|
390
|
+
|
|
391
|
+
self.list = QListWidget()
|
|
392
|
+
self.list.itemDoubleClicked.connect(self._edit_item)
|
|
393
|
+
lay.addWidget(self.list, 1)
|
|
394
|
+
|
|
395
|
+
row = QHBoxLayout()
|
|
396
|
+
for text, slot in [("↑", self._move_up), ("↓", self._move_down),
|
|
397
|
+
("删除", self._remove), ("复制", self._copy), ("粘贴", self._paste),
|
|
398
|
+
("试跑选中步", self._test_step)]:
|
|
399
|
+
b = QPushButton(text)
|
|
400
|
+
b.clicked.connect(slot)
|
|
401
|
+
row.addWidget(b)
|
|
402
|
+
lay.addLayout(row)
|
|
403
|
+
|
|
404
|
+
# -- task <-> ui ----
|
|
405
|
+
def get_task(self) -> Task:
|
|
406
|
+
return self._task
|
|
407
|
+
|
|
408
|
+
def set_task(self, task: Task):
|
|
409
|
+
self._task = task
|
|
410
|
+
self._refresh()
|
|
411
|
+
|
|
412
|
+
def _refresh(self):
|
|
413
|
+
self.list.clear()
|
|
414
|
+
for i, step in enumerate(self._task.steps):
|
|
415
|
+
self.list.addItem(QListWidgetItem(f"{i + 1}. {step.display_name()}"))
|
|
416
|
+
|
|
417
|
+
def get_selected_steps(self) -> list[Step]:
|
|
418
|
+
rows = sorted(self.list.row(it) for it in self.list.selectedItems())
|
|
419
|
+
return [copy.deepcopy(self._task.steps[r]) for r in rows if 0 <= r < len(self._task.steps)]
|
|
420
|
+
|
|
421
|
+
def current_index(self) -> int:
|
|
422
|
+
return self.list.currentRow()
|
|
423
|
+
|
|
424
|
+
def step_at(self, i: int) -> Step | None:
|
|
425
|
+
return self._task.steps[i] if 0 <= i < len(self._task.steps) else None
|
|
426
|
+
|
|
427
|
+
def replace_step(self, i: int, step: Step):
|
|
428
|
+
if 0 <= i < len(self._task.steps):
|
|
429
|
+
self._task.steps[i] = step
|
|
430
|
+
self._refresh()
|
|
431
|
+
self.list.setCurrentRow(i)
|
|
432
|
+
self.taskChanged.emit()
|
|
433
|
+
|
|
434
|
+
def insert_steps(self, steps: list[Step]):
|
|
435
|
+
row = self._selected_row()
|
|
436
|
+
at = row + 1 if row >= 0 else len(self._task.steps)
|
|
437
|
+
for i, s in enumerate(steps):
|
|
438
|
+
self._task.steps.insert(at + i, copy.deepcopy(s))
|
|
439
|
+
self._refresh()
|
|
440
|
+
self.taskChanged.emit()
|
|
441
|
+
|
|
442
|
+
def append_step(self, step: Step):
|
|
443
|
+
self._task.steps.append(step)
|
|
444
|
+
self._refresh()
|
|
445
|
+
self.taskChanged.emit()
|
|
446
|
+
|
|
447
|
+
def _selected_row(self) -> int:
|
|
448
|
+
return self.list.currentRow()
|
|
449
|
+
|
|
450
|
+
# -- actions ----
|
|
451
|
+
def _add(self):
|
|
452
|
+
st = self.type_combo.currentData()
|
|
453
|
+
step = Step(type=st)
|
|
454
|
+
dlg = StepEditorDialog(step, self)
|
|
455
|
+
if dlg.exec() == QDialog.Accepted:
|
|
456
|
+
self._task.steps.append(dlg.result_step())
|
|
457
|
+
self._refresh()
|
|
458
|
+
self.taskChanged.emit()
|
|
459
|
+
|
|
460
|
+
def _edit_item(self, item):
|
|
461
|
+
row = self.list.row(item)
|
|
462
|
+
if 0 <= row < len(self._task.steps):
|
|
463
|
+
dlg = StepEditorDialog(self._task.steps[row], self)
|
|
464
|
+
if dlg.exec() == QDialog.Accepted:
|
|
465
|
+
self._task.steps[row] = dlg.result_step()
|
|
466
|
+
self._refresh()
|
|
467
|
+
self.list.setCurrentRow(row)
|
|
468
|
+
self.taskChanged.emit()
|
|
469
|
+
|
|
470
|
+
def _remove(self):
|
|
471
|
+
row = self._selected_row()
|
|
472
|
+
if 0 <= row < len(self._task.steps):
|
|
473
|
+
del self._task.steps[row]
|
|
474
|
+
self._refresh()
|
|
475
|
+
self.taskChanged.emit()
|
|
476
|
+
|
|
477
|
+
def _move_up(self):
|
|
478
|
+
row = self._selected_row()
|
|
479
|
+
if row > 0:
|
|
480
|
+
self._task.steps[row - 1], self._task.steps[row] = self._task.steps[row], self._task.steps[row - 1]
|
|
481
|
+
self._refresh()
|
|
482
|
+
self.list.setCurrentRow(row - 1)
|
|
483
|
+
self.taskChanged.emit()
|
|
484
|
+
|
|
485
|
+
def _move_down(self):
|
|
486
|
+
row = self._selected_row()
|
|
487
|
+
if 0 <= row < len(self._task.steps) - 1:
|
|
488
|
+
self._task.steps[row + 1], self._task.steps[row] = self._task.steps[row], self._task.steps[row + 1]
|
|
489
|
+
self._refresh()
|
|
490
|
+
self.list.setCurrentRow(row + 1)
|
|
491
|
+
self.taskChanged.emit()
|
|
492
|
+
|
|
493
|
+
def _copy(self):
|
|
494
|
+
row = self._selected_row()
|
|
495
|
+
if 0 <= row < len(self._task.steps):
|
|
496
|
+
self._clipboard = [copy.deepcopy(self._task.steps[row])]
|
|
497
|
+
|
|
498
|
+
def _test_step(self):
|
|
499
|
+
step = self.step_at(self._selected_row())
|
|
500
|
+
if step is not None:
|
|
501
|
+
self.testStepRequested.emit(copy.deepcopy(step))
|
|
502
|
+
|
|
503
|
+
def _paste(self):
|
|
504
|
+
if not self._clipboard:
|
|
505
|
+
return
|
|
506
|
+
row = self._selected_row()
|
|
507
|
+
insert_at = row + 1 if row >= 0 else len(self._task.steps)
|
|
508
|
+
for i, s in enumerate(self._clipboard):
|
|
509
|
+
self._task.steps.insert(insert_at + i, copy.deepcopy(s))
|
|
510
|
+
self._refresh()
|
|
511
|
+
self.taskChanged.emit()
|
|
512
|
+
|
|
513
|
+
|
|
514
|
+
# -- small parse helpers --------------------------------------------------------
|
|
515
|
+
def _roi_to_str(roi):
|
|
516
|
+
return "" if not roi else f"{roi[0]},{roi[1]},{roi[2]},{roi[3]}"
|
|
517
|
+
|
|
518
|
+
|
|
519
|
+
def _str_to_roi(text):
|
|
520
|
+
text = text.strip()
|
|
521
|
+
if not text:
|
|
522
|
+
return None
|
|
523
|
+
try:
|
|
524
|
+
parts = [int(p) for p in text.replace(",", ",").split(",")]
|
|
525
|
+
if len(parts) == 4:
|
|
526
|
+
return tuple(parts)
|
|
527
|
+
except ValueError:
|
|
528
|
+
pass
|
|
529
|
+
return None
|
|
530
|
+
|
|
531
|
+
|
|
532
|
+
def _points_to_str(points):
|
|
533
|
+
return ";".join(f"{x},{y}" for x, y in points)
|
|
534
|
+
|
|
535
|
+
|
|
536
|
+
def _str_to_points(text):
|
|
537
|
+
pts = []
|
|
538
|
+
for chunk in text.replace(";", ";").split(";"):
|
|
539
|
+
chunk = chunk.strip()
|
|
540
|
+
if not chunk:
|
|
541
|
+
continue
|
|
542
|
+
try:
|
|
543
|
+
x, y = (int(v) for v in chunk.replace(",", ",").split(","))
|
|
544
|
+
pts.append((x, y))
|
|
545
|
+
except ValueError:
|
|
546
|
+
pass
|
|
547
|
+
return pts
|