1
1
mirror of https://github.com/KenanZhu/AutoLibrary.git synced 2026-06-18 07:23:03 +08:00

refactor(gui): 编排窗口迁移至新包并移除旧的预览/编排对话框

This commit is contained in:
2026-05-18 11:15:35 +08:00
parent 33c0f4414c
commit 6cf182c8c8
12 changed files with 2468 additions and 1122 deletions
+390
View File
@@ -0,0 +1,390 @@
# -*- coding: utf-8 -*-
"""
Copyright (c) 2026 KenanZhu.
All rights reserved.
This software is provided "as is", without any warranty of any kind.
You may use, modify, and distribute this file under the terms of the MIT License.
See the LICENSE file for details.
"""
from PySide6.QtCore import Qt, Slot
from PySide6.QtGui import (
QColor,
QFont,
QSyntaxHighlighter,
QTextCharFormat,
)
from PySide6.QtWidgets import (
QApplication,
QDialog,
QDialogButtonBox,
QGridLayout,
QHBoxLayout,
QLabel,
QPlainTextEdit,
QPushButton,
QStyle,
QTabWidget,
QVBoxLayout,
QWidget,
)
from autoscript import ALL_VARIABLES
class ALScriptHighlighter(QSyntaxHighlighter):
def __init__(
self,
parent = None
):
super().__init__(parent)
self._rules = []
keywordFmt = QTextCharFormat()
keywordFmt.setForeground(QColor("#007ACC"))
keywordFmt.setFontWeight(QFont.Weight.Bold)
for kw in ["IF", "ELSE IF", "ELSE", "ENDIF", "END IF",
"SET", "PASS", "THEN"]:
pattern = r"\b" + kw.replace(" ", r"\s+") + r"\b"
self._rules.append((pattern, keywordFmt))
opFmt = QTextCharFormat()
opFmt.setForeground(QColor("#AF00DB"))
opFmt.setFontWeight(QFont.Weight.Normal)
for op in [r"\.EQ\.", r"\.NEQ\.", r"\.BGT\.", r"\.BLT\.",
r"\.BGE\.", r"\.BLE\.", r"\.ADD\.", r"\.SUB\.",
r"\.AND\.", r"\.OR\."]:
self._rules.append((op, opFmt))
literalFmt = QTextCharFormat()
literalFmt.setForeground(QColor("#AF00DB"))
literalFmt.setFontWeight(QFont.Weight.Bold)
for lit in [".TRUE.", ".FALSE."]:
self._rules.append((r"\b" + lit.replace(".", r"\.") + r"\b", literalFmt))
funcFmt = QTextCharFormat()
funcFmt.setForeground(QColor("#795E26"))
funcFmt.setFontWeight(QFont.Weight.Normal)
self._rules.append((r"\b(?:DATE|TIME)\b", funcFmt))
varFmt = QTextCharFormat()
varFmt.setForeground(QColor("#267F99"))
varFmt.setFontWeight(QFont.Weight.Normal)
var_names = [name for _, (name, _) in ALL_VARIABLES.items()]
for var in var_names:
self._rules.append((r"\b" + var + r"\b", varFmt))
strFmt = QTextCharFormat()
strFmt.setForeground(QColor("#A31515"))
strFmt.setFontWeight(QFont.Weight.Normal)
self._rules.append((r"'[^']*'", strFmt))
numFmt = QTextCharFormat()
numFmt.setForeground(QColor("#098658"))
numFmt.setFontWeight(QFont.Weight.Normal)
self._rules.append((r"\b\d+\b", numFmt))
commentFmt = QTextCharFormat()
commentFmt.setForeground(QColor("#008000"))
commentFmt.setFontItalic(True)
self._rules.append((r"//[^\n]*", commentFmt))
def highlightBlock(
self,
text
):
import re
for pattern, fmt in self._rules:
for match in re.finditer(pattern, text, re.IGNORECASE):
start = match.start()
length = match.end() - match.start()
self.setFormat(start, length, fmt)
class ALAutoScriptEditDialog(QDialog):
def __init__(
self,
parent = None,
script: str = ""
):
super().__init__(parent)
self._fontSize = 19
self.modifyUi()
self.connectSignals()
self._textEdit.setPlainText(script)
self._highlighter = ALScriptHighlighter(
self._textEdit.document()
)
def modifyUi(
self
):
self.setWindowTitle("AutoScript 编辑 - AutoLibrary")
self.setMinimumSize(640, 600)
layout = QVBoxLayout(self)
layout.setSpacing(4)
layout.setContentsMargins(4, 4, 4, 4)
toolbarLayout = QHBoxLayout()
self._zoomInBtn = QPushButton("")
self._zoomInBtn.setFixedSize(25, 25)
self._zoomOutBtn = QPushButton("")
self._zoomOutBtn.setFixedSize(25, 25)
self._zoomResetBtn = QPushButton(
QApplication.style().standardIcon(
QStyle.StandardPixmap.SP_BrowserReload
), ""
)
self._zoomResetBtn.setFixedSize(25, 25)
self._zoomResetBtn.setToolTip("重置缩放")
self._zoomLabel = QLabel(f"{self._fontSize}px")
self._zoomLabel.setFixedHeight(25)
toolbarLayout.addWidget(self._zoomInBtn)
toolbarLayout.addWidget(self._zoomOutBtn)
toolbarLayout.addWidget(self._zoomResetBtn)
toolbarLayout.addWidget(self._zoomLabel)
toolbarLayout.addStretch()
self._copyBtn = QPushButton(
QApplication.style().standardIcon(
QStyle.StandardPixmap.SP_FileDialogDetailedView
), ""
)
self._copyBtn.setFixedSize(25, 25)
self._copyBtn.setToolTip("复制脚本")
toolbarLayout.addWidget(self._copyBtn)
layout.addLayout(toolbarLayout)
self._textEdit = QPlainTextEdit(self)
self._textEdit.setLineWrapMode(
QPlainTextEdit.LineWrapMode.NoWrap
)
self._textEdit.setStyleSheet(
"QPlainTextEdit {"
" font-family: 'Courier New', 'Consolas', monospace;"
f" font-size: {self._fontSize}px;"
"}"
)
layout.addWidget(self._textEdit)
self._createButtonPanel(layout)
self._btnBox = QDialogButtonBox(
QDialogButtonBox.StandardButton.Ok |
QDialogButtonBox.StandardButton.Cancel
)
self._btnBox.button(
QDialogButtonBox.StandardButton.Ok
).setText("保存")
self._btnBox.button(
QDialogButtonBox.StandardButton.Cancel
).setText("取消")
layout.addWidget(self._btnBox)
def _createButtonPanel(
self,
parent_layout
):
tab_widget = QTabWidget()
tab_widget.setMaximumHeight(200)
basic_widget = QWidget()
basic_layout = QGridLayout(basic_widget)
basic_layout.setSpacing(4)
basic_layout.setContentsMargins(4, 4, 4, 4)
basic_layout.setAlignment(Qt.AlignLeft | Qt.AlignTop)
control_buttons = [
("IF", "IF()\n \nEND IF"),
("ELSE IF", "ELSE IF()\n "),
("ELSE", "ELSE"),
("END IF", "END IF"),
("PASS", "PASS"),
]
self._addButtonsToGrid(basic_layout, control_buttons, 0, 0, 5)
assign_buttons = [
("SET", "SET = "),
]
self._addButtonsToGrid(basic_layout, assign_buttons, 0, 5, 1)
func_buttons = [
("DATE()", "DATE()"),
("TIME()", "TIME()"),
]
self._addButtonsToGrid(basic_layout, func_buttons, 1, 0, 2)
tab_widget.addTab(basic_widget, "基本语法")
operator_widget = QWidget()
operator_layout = QGridLayout(operator_widget)
operator_layout.setSpacing(4)
operator_layout.setContentsMargins(4, 4, 4, 4)
operator_layout.setAlignment(Qt.AlignLeft | Qt.AlignTop)
arithmetic_buttons = [
(".ADD.", ".ADD."),
(".SUB.", ".SUB."),
]
self._addButtonsToGrid(operator_layout, arithmetic_buttons, 0, 0, 2)
compare_buttons = [
(".EQ.", ".EQ."),
(".NEQ.", ".NEQ."),
(".BGT.", ".BGT."),
(".BLT.", ".BLT."),
(".BGE.", ".BGE."),
(".BLE.", ".BLE."),
]
self._addButtonsToGrid(operator_layout, compare_buttons, 1, 0, 6)
logic_buttons = [
(".AND.", ".AND."),
(".OR.", ".OR."),
]
self._addButtonsToGrid(operator_layout, logic_buttons, 2, 0, 2)
tab_widget.addTab(operator_widget, "运算符")
literal_widget = QWidget()
literal_layout = QGridLayout(literal_widget)
literal_layout.setSpacing(4)
literal_layout.setContentsMargins(4, 4, 4, 4)
literal_layout.setAlignment(Qt.AlignLeft | Qt.AlignTop)
bool_buttons = [
(".TRUE.", ".TRUE."),
(".FALSE.", ".FALSE."),
]
self._addButtonsToGrid(literal_layout, bool_buttons, 0, 0, 2)
hint_buttons = [
("字符串", "'文本'"),
("数字", "123"),
("注释", "// 注释"),
]
self._addButtonsToGrid(literal_layout, hint_buttons, 1, 0, 3)
tab_widget.addTab(literal_widget, "字面量")
var_widget = QWidget()
var_layout = QGridLayout(var_widget)
var_layout.setSpacing(4)
var_layout.setContentsMargins(4, 4, 4, 4)
var_layout.setAlignment(Qt.AlignLeft | Qt.AlignTop)
var_buttons = [
(display_name, name) for display_name, (name, _) in ALL_VARIABLES.items()
]
self._addButtonsToGrid(var_layout, var_buttons, 0, 0, 5)
tab_widget.addTab(var_widget, "变量")
parent_layout.addWidget(tab_widget)
def _addButtonsToGrid(
self,
grid_layout,
buttons,
start_row,
start_col,
max_columns
):
col = start_col
row = start_row
for btn_text, template in buttons:
btn = QPushButton(btn_text)
btn.setProperty("template", template)
btn.clicked.connect(self._insertTemplate)
btn.setFixedWidth(100)
btn.setFixedHeight(30)
btn.setToolTip(f"插入: {template}")
grid_layout.addWidget(btn, row, col)
col += 1
if col >= start_col + max_columns:
col = start_col
row += 1
@Slot()
def _insertTemplate(
self
):
btn = self.sender()
if not isinstance(btn, QPushButton):
return
template = btn.property("template")
if not template:
return
cursor = self._textEdit.textCursor()
cursor.insertText(template)
def connectSignals(
self
):
self._btnBox.accepted.connect(self.accept)
self._btnBox.rejected.connect(self.reject)
self._zoomInBtn.clicked.connect(self.onZoomIn)
self._zoomOutBtn.clicked.connect(self.onZoomOut)
self._zoomResetBtn.clicked.connect(self.onZoomReset)
self._copyBtn.clicked.connect(self.onCopy)
def getScript(
self
) -> str:
return self._textEdit.toPlainText()
def updateFontSize(
self
):
font = self._textEdit.font()
font.setPointSize(self._fontSize)
self._textEdit.setFont(font)
self._textEdit.setStyleSheet(
"QPlainTextEdit {"
" font-family: 'Courier New', 'Consolas', monospace;"
f" font-size: {self._fontSize}px;"
"}"
)
self._zoomLabel.setText(f"{self._fontSize}px")
@Slot()
def onZoomIn(
self
):
self._fontSize = min(self._fontSize + 2, 40)
self.updateFontSize()
@Slot()
def onZoomOut(
self
):
self._fontSize = max(self._fontSize - 2, 8)
self.updateFontSize()
@Slot()
def onZoomReset(
self
):
self._fontSize = 13
self.updateFontSize()
@Slot()
def onCopy(
self
):
clipboard = QApplication.clipboard()
clipboard.setText(self._textEdit.toPlainText())
original = self._copyBtn.text()
self._copyBtn.setText("已复制")
self._copyBtn.setEnabled(False)
from PySide6.QtCore import QTimer
QTimer.singleShot(2000, lambda: (
self._copyBtn.setText(original),
self._copyBtn.setEnabled(True)
))
-884
View File
@@ -1,884 +0,0 @@
# -*- coding: utf-8 -*-
"""
Copyright (c) 2026 KenanZhu.
All rights reserved.
This software is provided "as is", without any warranty of any kind.
You may use, modify, and distribute this file under the terms of the MIT License.
See the LICENSE file for details.
"""
from PySide6.QtCore import QTime, QDate, Slot
from PySide6.QtWidgets import (
QDialog, QWidget, QVBoxLayout, QHBoxLayout, QLabel,
QComboBox, QPushButton, QScrollArea, QTimeEdit,
QDateEdit, QLineEdit, QSpinBox, QDoubleSpinBox,
QStackedWidget, QFrame, QDialogButtonBox,
QGroupBox, QSizePolicy
)
from dsl.AutoScriptEngine import AutoScriptEngine
VARIABLE_META = AutoScriptEngine.VARIABLE_META
_VAR_COMBO_ITEMS = [
(display, varname, vartype)
for display, (varname, vartype) in VARIABLE_META.items()
]
_VAR_COMBO_ITEMS_SET = [
(display, varname, vartype)
for display, (varname, vartype) in VARIABLE_META.items()
if not varname.startswith("CURRENT_")
]
OP_ITEMS = [
("等于", ".EQ."),
("不等于", ".NEQ."),
("大于", ".BGT."),
("小于", ".BLT."),
("大于等于", ".BGE."),
("小于等于", ".BLE."),
]
def _makeVarCombo(
) -> QComboBox:
cb = QComboBox()
for display, varname, vartype in _VAR_COMBO_ITEMS:
cb.addItem(display, (varname, vartype))
cb.setMinimumWidth(120)
cb.setFixedHeight(25)
return cb
def _makeSetVarCombo(
) -> QComboBox:
cb = QComboBox()
for display, varname, vartype in _VAR_COMBO_ITEMS_SET:
cb.addItem(display, (varname, vartype))
cb.setMinimumWidth(120)
cb.setFixedHeight(25)
return cb
def _makeOpCombo(
) -> QComboBox:
cb = QComboBox()
for display, op in OP_ITEMS:
cb.addItem(display, op)
cb.setMinimumWidth(80)
cb.setFixedHeight(25)
return cb
def _makeValueWidget(
data_type: str
) -> QWidget:
if data_type == "Time":
w = QTimeEdit()
w.setDisplayFormat("HH:mm")
w.setMinimumWidth(100)
w.setFixedHeight(25)
elif data_type == "Date":
w = QDateEdit()
w.setDisplayFormat("yyyy-MM-dd")
w.setCalendarPopup(True)
w.setMinimumWidth(130)
w.setFixedHeight(25)
elif data_type == "Integer":
w = QSpinBox()
w.setMinimum(-999999)
w.setMaximum(999999)
w.setMinimumWidth(100)
w.setFixedHeight(25)
elif data_type == "Float":
w = QDoubleSpinBox()
w.setMinimum(-999999)
w.setMaximum(999999)
w.setDecimals(2)
w.setMinimumWidth(100)
w.setFixedHeight(25)
elif data_type == "Boolean":
w = QComboBox()
w.addItem(".TRUE.", ".TRUE.")
w.addItem(".FALSE.", ".FALSE.")
w.setMinimumWidth(100)
w.setFixedHeight(25)
else:
w = QLineEdit()
w.setPlaceholderText("输入值")
w.setMinimumWidth(120)
w.setFixedHeight(25)
return w
def _makeActionValueWidget(
data_type: str
) -> QWidget:
if data_type == "Date":
w = QComboBox()
w.addItem("今天", "today")
w.addItem("明天", "tomorrow")
w.setFixedHeight(25)
w.setMinimumWidth(100)
w._is_date_action = True
return w
if data_type == "Time":
container = QWidget()
layout = QHBoxLayout(container)
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(2)
modeCombo = QComboBox()
modeCombo.addItem("固定时间", "fixed")
modeCombo.addItem("相对当前", "relative")
modeCombo.setFixedHeight(25)
stack = QStackedWidget()
timeEdit = QTimeEdit()
timeEdit.setDisplayFormat("HH:mm")
timeEdit.setFixedHeight(25)
spinBox = QSpinBox()
spinBox.setRange(0, 23)
spinBox.setSuffix("小时")
spinBox.setFixedHeight(25)
stack.addWidget(timeEdit)
stack.addWidget(spinBox)
modeCombo.currentIndexChanged.connect(
lambda i: stack.setCurrentIndex(i)
)
layout.addWidget(modeCombo)
layout.addWidget(stack)
container._modeCombo = modeCombo
container._timeEdit = timeEdit
container._spinBox = spinBox
container._isActionTime = True
return container
return _makeValueWidget(data_type)
def _getValueFromWidget(
w: QWidget
) -> str:
if getattr(w, '_isActionTime', False):
if w._modeCombo.currentData() == "fixed":
return w._timeEdit.time().toString("HH:mm")
else:
return f"+{w._spinBox.value()}"
if isinstance(w, QTimeEdit):
return w.time().toString("HH:mm")
if isinstance(w, QDateEdit):
return w.date().toString("yyyy-MM-dd")
if isinstance(w, QComboBox):
return w.currentText()
if isinstance(w, QSpinBox):
return str(w.value())
if isinstance(w, QDoubleSpinBox):
return str(w.value())
if isinstance(w, QLineEdit):
return w.text()
return ""
def _encodeValueStr(
raw_value: str,
data_type: str
) -> str:
if data_type == "Time":
if raw_value.startswith("+"):
return raw_value
return f"TIME({raw_value})"
elif data_type == "Date":
if raw_value == "今天":
return "CURRENT_DATE"
elif raw_value == "明天":
return "CURRENT_DATE + 1"
return f"DATE({raw_value})"
elif data_type == "Boolean":
return raw_value
elif data_type == "String":
escaped = raw_value.replace("'", "''")
return f"'{escaped}'"
else:
return raw_value
def _setWidgetValue(
w: QWidget,
vartype: str,
expr: str
):
import re
s = expr.strip()
if getattr(w, '_isActionTime', False):
timeMatch = re.match(r"TIME\((\d{1,2}:\d{2})\)", s, re.IGNORECASE)
if timeMatch:
w._modeCombo.setCurrentIndex(0)
parts = timeMatch.group(1).split(":")
w._timeEdit.setTime(QTime(int(parts[0]), int(parts[1])))
return
relMatch = re.match(r"^\+(\d+)$", s)
if relMatch:
w._modeCombo.setCurrentIndex(1)
w._spinBox.setValue(int(relMatch.group(1)))
return
return
if getattr(w, '_is_date_action', False) and isinstance(w, QComboBox):
if s.upper() in ("CURRENT_DATE", "TODAY"):
w.setCurrentIndex(0)
elif s.upper() in ("CURRENT_DATE + 1", "TOMORROW"):
w.setCurrentIndex(1)
else:
dateMatch = re.match(
r"DATE\((\d{4}-\d{2}-\d{2})\)", s, re.IGNORECASE
)
if dateMatch:
from datetime import datetime, timedelta
dateStr = dateMatch.group(1)
today = datetime.now().strftime("%Y-%m-%d")
tomorrow = (
datetime.now() + timedelta(days=1)
).strftime("%Y-%m-%d")
if dateStr == today:
w.setCurrentIndex(0)
elif dateStr == tomorrow:
w.setCurrentIndex(1)
return
if vartype == "Time":
m = re.match(r"TIME\((\d{1,2}:\d{2})\)", s, re.IGNORECASE)
if m and isinstance(w, QTimeEdit):
parts = m.group(1).split(":")
w.setTime(QTime(int(parts[0]), int(parts[1])))
elif vartype == "Date":
m = re.match(r"DATE\((\d{4}-\d{2}-\d{2})\)", s, re.IGNORECASE)
if m and isinstance(w, QDateEdit):
parts = m.group(1).split("-")
w.setDate(QDate(int(parts[0]), int(parts[1]), int(parts[2])))
elif vartype == "Boolean" and isinstance(w, QComboBox):
for i in range(w.count()):
if w.itemData(i) == s.upper():
w.setCurrentIndex(i)
break
elif vartype == "Integer" and isinstance(w, QSpinBox):
try:
w.setValue(int(s))
except ValueError:
pass
elif vartype == "Float" and isinstance(w, QDoubleSpinBox):
try:
w.setValue(float(s))
except ValueError:
pass
elif isinstance(w, QLineEdit):
inner = s
if (inner.startswith("'") and inner.endswith("'")) or \
(inner.startswith('"') and inner.endswith('"')):
inner = inner[1:-1].replace("''", "'")
w.setText(inner)
class ActionStepFrame(QFrame):
def __init__(
self,
parent=None
):
super().__init__(parent)
self.setupUi()
self.connectSignals()
self.onTargetChanged(0)
def setupUi(
self
):
self.setFrameShape(QFrame.Shape.StyledPanel)
self.setFrameShadow(QFrame.Shadow.Raised)
self.setFixedHeight(35)
layout = QHBoxLayout(self)
layout.setContentsMargins(2, 2, 2, 2)
layout.setSpacing(4)
self.targetCombo = _makeSetVarCombo()
self.valueWidgetStack = QStackedWidget()
self.valueWidgetStack.setFixedHeight(25)
self.initValueStack()
setLabel = QLabel("设置")
setLabel.setFixedHeight(25)
layout.addWidget(setLabel)
layout.addWidget(self.targetCombo)
toLabel = QLabel("")
toLabel.setFixedHeight(25)
layout.addWidget(toLabel)
layout.addWidget(self.valueWidgetStack)
self.deleteBtn = QPushButton("×")
self.deleteBtn.setFixedSize(24, 25)
self.deleteBtn.setStyleSheet("color: red; font-weight: bold;")
layout.addWidget(self.deleteBtn)
def connectSignals(
self
):
self.targetCombo.currentIndexChanged.connect(self.onTargetChanged)
def initValueStack(
self
):
self._valueWidgets = {}
for _, _, vartype in _VAR_COMBO_ITEMS:
if vartype not in self._valueWidgets:
w = _makeActionValueWidget(vartype)
self._valueWidgets[vartype] = w
self.valueWidgetStack.addWidget(w)
self.valueWidgetStack.setCurrentWidget(
self._valueWidgets.get("String", self.valueWidgetStack.widget(0))
)
def getTarget(
self
) -> str:
data = self.targetCombo.currentData()
return data[0] if data else ""
def getTargetType(
self
) -> str:
data = self.targetCombo.currentData()
return data[1] if data else "String"
def getValueRaw(
self
) -> str:
currentType = self.getTargetType()
w = self._valueWidgets.get(currentType)
if w:
return _getValueFromWidget(w)
return ""
def toScriptLine(
self
) -> str:
target = self.getTarget()
if not target:
return ""
rawVal = self.getValueRaw()
targetType = self.getTargetType()
encoded = _encodeValueStr(rawVal, targetType)
if targetType == "Time" and rawVal.startswith("+"):
hours = rawVal[1:]
return f" {target} .ADD. {hours}"
return f" SET {target} = {encoded}"
def loadFromScript(
self,
targetVar: str,
valueExpr: str
):
for idx in range(self.targetCombo.count()):
data = self.targetCombo.itemData(idx)
if data and data[0] == targetVar:
self.targetCombo.setCurrentIndex(idx)
break
self.setValueFromExpr(valueExpr)
def setValueFromExpr(
self,
expr: str
):
targetType = self.getTargetType()
w = self._valueWidgets.get(targetType)
if not w:
return
_setWidgetValue(w, targetType, expr)
@Slot(int)
def onTargetChanged(
self,
idx
):
if idx < 0:
return
data = self.targetCombo.itemData(idx)
if data:
_, vartype = data
w = self._valueWidgets.get(vartype)
if w:
self.valueWidgetStack.setCurrentWidget(w)
class ConditionalBlock(QGroupBox):
def __init__(
self,
blockIndex: int,
parent=None
):
super().__init__(parent)
self.blockIndex = blockIndex
self._actionWidgets = []
self.setupUi()
self.connectSignals()
self.onOperandChanged(0)
def setupUi(
self
):
self.setStyleSheet(
"QGroupBox { font-weight: bold; border: 1px solid #ccc; "
"margin-top: 5px; padding-top: 5px; }"
)
self.setSizePolicy(
QSizePolicy.Policy.Preferred,
QSizePolicy.Policy.Fixed
)
mainLayout = QVBoxLayout(self)
mainLayout.setSpacing(4)
mainLayout.setContentsMargins(5, 5, 5, 5)
headerLayout = QHBoxLayout()
self.typeCombo = QComboBox()
self.typeCombo.addItem("IF", "IF")
self.typeCombo.addItem("ELSE IF", "ELSE IF")
self.typeCombo.addItem("ELSE", "ELSE")
if self.blockIndex == 0:
self.typeCombo.setEnabled(False)
typeLabel = QLabel("类型:")
typeLabel.setFixedHeight(25)
headerLayout.addWidget(typeLabel)
headerLayout.addWidget(self.typeCombo)
headerLayout.addStretch()
self.deleteBlockBtn = QPushButton("删除此块")
self.deleteBlockBtn.setStyleSheet("color: red;")
self.deleteBlockBtn.setFixedHeight(25)
headerLayout.addWidget(self.deleteBlockBtn)
mainLayout.addLayout(headerLayout)
self.conditionWidget = QWidget()
self.conditionWidget.setFixedHeight(60)
condLayout = QHBoxLayout(self.conditionWidget)
condLayout.setContentsMargins(0, 0, 0, 0)
ifLabel = QLabel("如果")
ifLabel.setFixedHeight(25)
condLayout.addWidget(ifLabel)
self.operandCombo = _makeVarCombo()
condLayout.addWidget(self.operandCombo)
self.opCombo = _makeOpCombo()
condLayout.addWidget(self.opCombo)
self.condValueStack = QStackedWidget()
self.condValueStack.setFixedHeight(25)
self._condValueWidgets = {}
for vartype in ["Time", "Date", "String", "Integer", "Float", "Boolean"]:
w = _makeValueWidget(vartype)
self._condValueWidgets[vartype] = w
self.condValueStack.addWidget(w)
self.condValueStack.setCurrentWidget(self._condValueWidgets.get("String"))
condLayout.addWidget(self.condValueStack)
mainLayout.addWidget(self.conditionWidget)
self.actionLabel = QLabel("执行步骤:")
self.actionLabel.setFixedHeight(25)
mainLayout.addWidget(self.actionLabel)
self.actionsLayout = QVBoxLayout()
self.actionsLayout.setSpacing(2)
mainLayout.addLayout(self.actionsLayout)
self.addActionBtn = QPushButton("+ 添加执行步骤")
self.addActionBtn.setFixedHeight(25)
mainLayout.addWidget(self.addActionBtn)
def connectSignals(
self
):
self.operandCombo.currentIndexChanged.connect(self.onOperandChanged)
self.typeCombo.currentIndexChanged.connect(self.onTypeChanged)
self.addActionBtn.clicked.connect(self.addActionStep)
def removeActionStep(
self,
step: ActionStepFrame
):
if step in self._actionWidgets:
self._actionWidgets.remove(step)
self.actionsLayout.removeWidget(step)
step.hide()
step.deleteLater()
def getBlockType(
self
) -> str:
return self.typeCombo.currentData()
def toScriptLines(
self
) -> list:
blockType = self.getBlockType()
lines = []
if blockType in ("IF", "ELSE IF"):
operand = self.operandCombo.currentData()
operandName = operand[0] if operand else ""
operandType = operand[1] if operand else "String"
opSym = self.opCombo.currentData()
rawVal = _getValueFromWidget(
self._condValueWidgets.get(operandType, QLineEdit())
)
encodedVal = _encodeValueStr(rawVal, operandType)
if blockType == "IF":
lines.append(f"IF({operandName} {opSym} {encodedVal}) THEN")
else:
lines.append(f"ELSE IF({operandName} {opSym} {encodedVal}) THEN")
else:
lines.append("ELSE")
for step in self._actionWidgets:
scriptLine = step.toScriptLine()
if scriptLine:
lines.append(scriptLine)
return lines
def getConditionSummary(
self
) -> str:
bt = self.getBlockType()
if bt == "ELSE":
return "ELSE"
operandData = self.operandCombo.currentData()
if not operandData:
return bt
operandDisplay = self.operandCombo.currentText()
opDisplay = self.opCombo.currentText()
rawVal = self.getConditionRawValuePreview()
return f"{bt} ({operandDisplay} {opDisplay} {rawVal})"
def getConditionRawValuePreview(
self
) -> str:
data = self.operandCombo.currentData()
if not data:
return ""
_, vartype = data
w = self._condValueWidgets.get(vartype)
if w:
return _getValueFromWidget(w)
return ""
def countActionSteps(
self
) -> int:
return len(self._actionWidgets)
@Slot(int)
def onOperandChanged(
self,
idx
):
if idx < 0:
return
data = self.operandCombo.itemData(idx)
if data:
_, vartype = data
w = self._condValueWidgets.get(vartype)
if w:
self.condValueStack.setCurrentWidget(w)
@Slot(int)
def onTypeChanged(
self,
idx
):
isCond = self.typeCombo.currentData() in ("IF", "ELSE IF")
self.conditionWidget.setVisible(isCond)
self.actionLabel.setText("执行步骤:" if isCond else "ELSE 执行步骤:")
@Slot()
def addActionStep(
self
):
step = ActionStepFrame(self)
step.deleteBtn.clicked.connect(lambda: self.removeActionStep(step))
self._actionWidgets.append(step)
self.actionsLayout.addWidget(step)
class ALAutoScriptOrchDialog(QDialog):
def __init__(
self,
parent=None,
existingScript: str = ""
):
super().__init__(parent)
self._blocks: list[ConditionalBlock] = []
self.modifyUi()
self.connectSignals()
if existingScript and existingScript.strip():
self.loadFromScript(existingScript)
else:
self.addBlock()
self._scrollLayout.addStretch()
def modifyUi(
self
):
self.setWindowTitle("AutoScript 指令编排 - AutoLibrary")
self.setMinimumSize(420, 400)
self.setModal(True)
mainLayout = QVBoxLayout(self)
scroll = QScrollArea()
scroll.setWidgetResizable(True)
scroll.setFrameShape(QFrame.Shape.NoFrame)
scrollContent = QWidget()
self._scrollLayout = QVBoxLayout(scrollContent)
self._scrollLayout.setSpacing(5)
scroll.setWidget(scrollContent)
mainLayout.addWidget(scroll)
addBlockLayout = QHBoxLayout()
self.addBlockBtn = QPushButton("+ 添加判断块")
self.addBlockBtn.setFixedHeight(25)
addBlockLayout.addStretch()
addBlockLayout.addWidget(self.addBlockBtn)
addBlockLayout.addStretch()
mainLayout.addLayout(addBlockLayout)
self.btnBox = QDialogButtonBox(
QDialogButtonBox.StandardButton.Ok |
QDialogButtonBox.StandardButton.Cancel
)
self.btnBox.button(QDialogButtonBox.StandardButton.Ok).setText("确定")
self.btnBox.button(QDialogButtonBox.StandardButton.Cancel).setText("取消")
mainLayout.addWidget(self.btnBox)
def connectSignals(
self
):
self.btnBox.accepted.connect(self.accept)
self.btnBox.rejected.connect(self.reject)
self.addBlockBtn.clicked.connect(self.addBlock)
def removeBlock(
self,
block: ConditionalBlock
):
if block in self._blocks:
self._blocks.remove(block)
self._scrollLayout.removeWidget(block)
block.hide()
block.deleteLater()
def getScript(
self
) -> str:
parts = []
for i, block in enumerate(self._blocks):
blockType = block.getBlockType()
if blockType == "IF" and i > 0:
parts.append("ENDIF")
lines = block.toScriptLines()
parts.extend(lines)
if self._blocks and self._blocks[0].getBlockType() == "IF":
parts.append("ENDIF")
return "\n".join(parts)
def getScriptPreview(
self
) -> str:
s = self.getScript()
if len(s) > 10:
return s[:7] + "..."
return s
def loadFromScript(
self,
script: str
):
import re
lines = [l.strip() for l in script.split("\n") if l.strip()]
if not lines:
self.addBlock()
return
currentBlock = None
currentBlockType = None
actionsBuffer = []
def flushBlock():
nonlocal currentBlock, currentBlockType, actionsBuffer
if currentBlock is None:
return
typeIdxMap = {"IF": 0, "ELSE IF": 1, "ELSE": 2}
idx = typeIdxMap.get(currentBlockType, 0)
currentBlock.typeCombo.setCurrentIndex(idx)
currentBlock.onTypeChanged(idx)
for oldStep in list(currentBlock._actionWidgets):
currentBlock.removeActionStep(oldStep)
for target, valueExpr in actionsBuffer:
currentBlock.addActionStep()
step = currentBlock._actionWidgets[-1]
step.loadFromScript(target, valueExpr)
self._blocks.clear()
while self._scrollLayout.count():
item = self._scrollLayout.takeAt(0)
if item.widget():
item.widget().deleteLater()
for line in lines:
upper = line.upper()
ifMatch = re.match(r"^IF\((.+)\)\s*THEN\s*$", upper)
if ifMatch:
flushBlock()
currentBlockType = "IF"
actionsBuffer = []
self.addBlock()
currentBlock = self._blocks[-1]
self.parseConditionToBlock(currentBlock, ifMatch.group(1))
continue
elifIfMatch = re.match(r"^ELSE\s+IF\((.+)\)\s*THEN\s*$", upper)
if elifIfMatch:
flushBlock()
currentBlockType = "ELSE IF"
actionsBuffer = []
self.addBlock()
currentBlock = self._blocks[-1]
self.parseConditionToBlock(currentBlock, elifIfMatch.group(1))
continue
if upper == "ELSE":
flushBlock()
currentBlockType = "ELSE"
actionsBuffer = []
self.addBlock()
currentBlock = self._blocks[-1]
currentBlock.conditionWidget.setVisible(False)
continue
setMatch = re.match(r"^SET\s+(\w+)\s*=\s*(.+)$", line, re.IGNORECASE)
if setMatch:
target = setMatch.group(1).strip()
valueExpr = setMatch.group(2).strip()
actionsBuffer.append((target, valueExpr))
continue
addMatch = re.match(r"^(\w+)\s+\.ADD\.\s+(\d+)$", line, re.IGNORECASE)
if addMatch:
target = addMatch.group(1).strip()
hours = addMatch.group(2).strip()
actionsBuffer.append((target, f"+{hours}"))
continue
if upper in ("ENDIF", "END IF"):
flushBlock()
currentBlock = None
currentBlockType = None
actionsBuffer = []
continue
flushBlock()
if not self._blocks:
self.addBlock()
def parseConditionToBlock(
self,
block: ConditionalBlock,
condStr: str
):
condStr = condStr.strip()
for _, opSym in OP_ITEMS:
idx = condStr.upper().find(opSym)
if idx >= 0:
leftPart = condStr[:idx].strip()
rightPart = condStr[idx + len(opSym):].strip()
for ci in range(block.operandCombo.count()):
data = block.operandCombo.itemData(ci)
if data and data[0] == leftPart:
block.operandCombo.setCurrentIndex(ci)
break
for oi in range(block.opCombo.count()):
if block.opCombo.itemData(oi) == opSym:
block.opCombo.setCurrentIndex(oi)
break
opData = block.operandCombo.currentData()
vartype = opData[1] if opData else "String"
w = block._condValueWidgets.get(vartype)
if w:
_setWidgetValue(w, vartype, rightPart)
return
@Slot()
def addBlock(
self
):
block = ConditionalBlock(len(self._blocks), self)
block.deleteBlockBtn.clicked.connect(lambda: self.removeBlock(block))
self._blocks.append(block)
block.addActionStep()
if self._scrollLayout.count() > 0:
lastItem = self._scrollLayout.itemAt(
self._scrollLayout.count() - 1
)
if lastItem and lastItem.spacerItem():
self._scrollLayout.insertWidget(
self._scrollLayout.count() - 1, block
)
return
self._scrollLayout.addWidget(block)
@@ -0,0 +1,3 @@
from gui.ALAutoScriptOrchDialog._dialog import ALAutoScriptOrchDialog
__all__ = ["ALAutoScriptOrchDialog"]
+272
View File
@@ -0,0 +1,272 @@
"""
Conditional block widget for the AutoScript orchestration dialog.
"""
from PySide6.QtCore import Slot
from PySide6.QtWidgets import (
QComboBox,
QGroupBox,
QHBoxLayout,
QLabel,
QPushButton,
QSizePolicy,
QVBoxLayout,
QWidget,
)
from gui.ALAutoScriptOrchDialog._widgets import (
ActionStepFrame,
ConditionRowFrame,
)
class ConditionalBlock(QGroupBox):
def __init__(
self,
blockIndex: int,
varMgr = None,
parent = None
):
super().__init__(parent)
self.blockIndex = blockIndex
self._varMgr = varMgr
self._actionWidgets = []
self._conditionRows = []
self.setupUi()
self.connectSignals()
self.addInitialConditionRow()
def setupUi(
self
):
self.setUpdatesEnabled(False)
self.setStyleSheet(
"QGroupBox { font-weight: bold; border: 1px solid #ccc; "
"margin-top: 5px; padding-top: 5px; }"
)
self.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed)
mainLayout = QVBoxLayout(self)
mainLayout.setSpacing(6)
mainLayout.setContentsMargins(8, 8, 8, 8)
headerLayout = QHBoxLayout()
headerLayout.setSpacing(8)
self.typeCombo = QComboBox(self)
self.typeCombo.addItem("IF", "IF")
self.typeCombo.addItem("ELSE IF", "ELSE IF")
self.typeCombo.addItem("ELSE", "ELSE")
self.typeCombo.setFixedHeight(25)
if self.blockIndex == 0:
self.typeCombo.setEnabled(False)
headerLayout.addWidget(QLabel("类型:", self))
headerLayout.addWidget(self.typeCombo)
headerLayout.addStretch()
self.deleteBlockBtn = QPushButton("删除此块", self)
self.deleteBlockBtn.setStyleSheet("color: red;")
self.deleteBlockBtn.setFixedHeight(25)
headerLayout.addWidget(self.deleteBlockBtn)
mainLayout.addLayout(headerLayout)
self.conditionWidget = QWidget(self)
self.conditionWidget.setSizePolicy(
QSizePolicy.Preferred, QSizePolicy.Preferred
)
condLayout = QVBoxLayout(self.conditionWidget)
condLayout.setContentsMargins(4, 4, 4, 4)
condLayout.setSpacing(6)
self._condRowsLayout = QVBoxLayout()
self._condRowsLayout.setSpacing(4)
condLayout.addLayout(self._condRowsLayout)
self.addCondBtn = QPushButton("+ 添加条件", self.conditionWidget)
self.addCondBtn.setFixedHeight(25)
condLayout.addWidget(self.addCondBtn)
mainLayout.addWidget(self.conditionWidget)
self.actionLabel = QLabel("执行步骤:", self)
self.actionLabel.setFixedHeight(25)
mainLayout.addWidget(self.actionLabel)
self._actionsLayout = QVBoxLayout()
self._actionsLayout.setSpacing(4)
mainLayout.addLayout(self._actionsLayout)
self.addActionBtn = QPushButton("+ 添加执行步骤", self)
self.addActionBtn.setFixedHeight(25)
mainLayout.addWidget(self.addActionBtn)
self.setUpdatesEnabled(True)
def connectSignals(
self
):
self.typeCombo.currentIndexChanged.connect(self.onTypeChanged)
self.addCondBtn.clicked.connect(self.addConditionRow)
self.addActionBtn.clicked.connect(self.addActionStep)
def addInitialConditionRow(
self
):
row = ConditionRowFrame(
self._varMgr, self.blockIndex,
isFirst=True, parent=self
)
self._conditionRows.append(row)
self._condRowsLayout.addWidget(row)
def addConditionRow(
self
):
row = ConditionRowFrame(
self._varMgr, self.blockIndex,
isFirst=False, parent=self
)
row.deleteBtn.clicked.connect(lambda: self.removeConditionRow(row))
self._conditionRows.append(row)
self._condRowsLayout.addWidget(row)
def removeConditionRow(
self,
row: ConditionRowFrame
):
if row in self._conditionRows and len(self._conditionRows) > 1:
self._conditionRows.remove(row)
self._condRowsLayout.removeWidget(row)
row.hide()
row.deleteLater()
def addActionStep(
self
):
step = ActionStepFrame(self._varMgr, self.blockIndex, parent=self)
step.deleteBtn.clicked.connect(lambda: self.removeActionStep(step))
self._actionWidgets.append(step)
self._actionsLayout.addWidget(step)
def removeActionStep(
self,
step: ActionStepFrame
):
if step in self._actionWidgets:
self._actionWidgets.remove(step)
self._actionsLayout.removeWidget(step)
step.hide()
step.deleteLater()
@Slot(int)
def onTypeChanged(
self,
_idx
):
isCond = self.typeCombo.currentData() in ("IF", "ELSE IF")
self.conditionWidget.setVisible(isCond)
self.actionLabel.setText(
"执行步骤:" if isCond else "ELSE 执行步骤:"
)
def getBlockType(
self
) -> str:
return self.typeCombo.currentData()
def getConditionRows(
self
):
return list(self._conditionRows)
def getActionSteps(
self
):
return list(self._actionWidgets)
def countActionSteps(
self
) -> int:
return len(self._actionWidgets)
def toScriptLines(
self
) -> list:
blockType = self.getBlockType()
lines = []
if blockType in ("IF", "ELSE IF"):
condTexts = [
r.toConditionText() for r in self._conditionRows if r.toConditionText()
]
if not condTexts:
condTexts = [".TRUE."]
if len(condTexts) == 1:
combined = condTexts[0]
else:
parts = []
for i, ct in enumerate(condTexts):
if i > 0:
logic = self._conditionRows[i].getLogic() or ".AND."
parts.append(f" {logic} ")
parts.append(f"({ct})")
combined = "".join(parts)
if blockType == "IF":
lines.append(f"IF({combined}) THEN")
else:
lines.append(f"ELSE IF({combined}) THEN")
else:
lines.append("ELSE")
for step in self._actionWidgets:
scriptLine = step.toScriptLine()
if scriptLine:
lines.append(scriptLine)
return lines
def refreshVarCombos(
self
):
for row in self._conditionRows:
row.refreshVarCombos()
for step in self._actionWidgets:
step.refreshVarCombos()
def setPrevBlockType(
self,
prevType: str | None
):
model = self.typeCombo.model()
if model is None:
return
for data in ("ELSE IF", "ELSE"):
idx = self.typeCombo.findData(data)
if idx < 0:
continue
item = model.item(idx)
shouldEnable = prevType != "ELSE"
item.setEnabled(shouldEnable)
if prevType == "ELSE" and self.typeCombo.currentData() in ("ELSE IF", "ELSE"):
self.typeCombo.setCurrentIndex(0)
+296
View File
@@ -0,0 +1,296 @@
"""
Orchestration dialog for visually composing AutoScript scripts.
"""
from PySide6.QtCore import Slot
from PySide6.QtWidgets import (
QDialog,
QDialogButtonBox,
QFrame,
QMessageBox,
QPushButton,
QScrollArea,
QVBoxLayout,
QWidget,
)
from gui.ALAutoScriptOrchDialog._precheck import precheck
from gui.ALAutoScriptOrchDialog._orchestrate import parseBlocks
from gui.ALAutoScriptOrchDialog._helpers import (
COMPARE_OPERATORS,
PRESET_NAMES,
VariableManager,
findOperatorIn,
splitTopLevel,
stripOuterParens,
)
from gui.ALAutoScriptOrchDialog._blocks import ConditionalBlock
from gui.ALAutoScriptOrchDialog._widgets import ConditionRowFrame
class ALAutoScriptOrchDialog(QDialog):
def __init__(
self,
parent = None,
existingScript: str = ""
):
super().__init__(parent)
self._blocks = []
self._varMgr = VariableManager(self)
self.setupUi()
self.connectSignals()
if existingScript and existingScript.strip():
self.loadFromScript(existingScript)
else:
self.addBlock()
self._scrollLayout.addStretch()
def setupUi(
self
):
self.setWindowTitle("AutoScript 指令编排 - AutoLibrary")
self.setMinimumSize(640, 600)
self.setModal(True)
mainLayout = QVBoxLayout(self)
scroll = QScrollArea()
scroll.setWidgetResizable(True)
scroll.setFrameShape(QFrame.NoFrame)
scrollContent = QWidget()
self._scrollLayout = QVBoxLayout(scrollContent)
self._scrollLayout.setSpacing(5)
scroll.setWidget(scrollContent)
mainLayout.addWidget(scroll)
self.addBlockBtn = QPushButton("+ 添加判断块")
self.addBlockBtn.setFixedHeight(25)
mainLayout.addWidget(self.addBlockBtn)
self.btnBox = QDialogButtonBox(
QDialogButtonBox.StandardButton.Ok |
QDialogButtonBox.StandardButton.Cancel
)
self.btnBox.button(QDialogButtonBox.StandardButton.Ok).setText("确定")
self.btnBox.button(QDialogButtonBox.StandardButton.Cancel).setText("取消")
mainLayout.addWidget(self.btnBox)
def connectSignals(
self
):
self.btnBox.accepted.connect(self.onAccept)
self.btnBox.rejected.connect(self.reject)
self.addBlockBtn.clicked.connect(self.addBlock)
def _updateBlockTypeRestrictions(
self
):
prevType = None
for block in self._blocks:
block.setPrevBlockType(prevType)
prevType = block.getBlockType()
def addBlock(
self
):
block = ConditionalBlock(
len(self._blocks), self._varMgr, parent=self
)
block.deleteBlockBtn.clicked.connect(lambda: self.removeBlock(block))
block.typeCombo.currentIndexChanged.connect(self._updateBlockTypeRestrictions)
block.addActionStep()
self._blocks.append(block)
self._updateBlockTypeRestrictions()
if self._scrollLayout.count() > 0:
lastItem = self._scrollLayout.itemAt(
self._scrollLayout.count() - 1
)
if lastItem and lastItem.spacerItem():
self._scrollLayout.insertWidget(
self._scrollLayout.count() - 1, block
)
return
self._scrollLayout.addWidget(block)
def removeBlock(
self,
block: ConditionalBlock
):
if len(self._blocks) <= 1:
QMessageBox.information(self, "提示", "至少保留一个判断块。")
return
if block in self._blocks:
self._blocks.remove(block)
self._scrollLayout.removeWidget(block)
block.hide()
block.deleteLater()
for i, blk in enumerate(self._blocks):
blk.blockIndex = i
if i == 0:
blk.typeCombo.setEnabled(False)
blk.typeCombo.setCurrentIndex(0)
else:
blk.typeCombo.setEnabled(True)
blk.refreshVarCombos()
self._updateBlockTypeRestrictions()
def getScript(
self
) -> str:
parts = []
prevType = None
for block in self._blocks:
blockType = block.getBlockType()
if blockType == "IF" and prevType is not None:
parts.append("ENDIF")
lines = block.toScriptLines()
parts.extend(lines)
prevType = blockType
if self._blocks and self._blocks[0].getBlockType() == "IF":
parts.append("ENDIF")
return "\n".join(parts)
@Slot()
def onAccept(
self
):
script = self.getScript().strip()
if not script:
QMessageBox.warning(self, "提示", "脚本内容为空,请添加至少一个操作步骤。")
return
self.accept()
@staticmethod
def precheckScriptForOrchestration(
script: str
) -> tuple[bool, str]:
return precheck(script, allowed_vars=PRESET_NAMES)
def loadFromScript(
self,
script: str
):
if not script.strip():
self.addBlock()
return
ok, err = self.precheckScriptForOrchestration(script)
if not ok:
QMessageBox.warning(
self, "无法编排",
f"脚本检查失败:\n{err}\n\n"
"请通过\"编辑\"按钮打开脚本编辑窗口进行修改。"
)
self.addBlock()
return
# Structured block data via observer-based parsing — no duplicate logic
typeIdxMap = {"IF": 0, "ELSE IF": 1, "ELSE": 2}
parsedBlocks = parseBlocks(script)
self._blocks.clear()
while self._scrollLayout.count():
item = self._scrollLayout.takeAt(0)
if item.widget():
item.widget().deleteLater()
try:
for blockType, condition, actions in parsedBlocks:
self.addBlock()
block = self._blocks[-1]
idx = typeIdxMap.get(blockType, 0)
block.typeCombo.setCurrentIndex(idx)
block.onTypeChanged(idx)
for oldStep in list(block._actionWidgets):
block.removeActionStep(oldStep)
for target, valueExpr, opType in actions:
block.addActionStep()
step = block.getActionSteps()[-1]
step.setOpType(opType)
step.loadFromScript(target, valueExpr)
if blockType in ("IF", "ELSE IF") and condition:
self._parseConditions(block, condition)
except Exception:
self._blocks.clear()
while self._scrollLayout.count():
item = self._scrollLayout.takeAt(0)
if item.widget():
item.widget().deleteLater()
self._updateBlockTypeRestrictions()
if not self._blocks:
self.addBlock()
def _parseConditions(
self,
block: ConditionalBlock,
condStr: str
):
s = condStr.strip()
if not s:
return
s = stripOuterParens(s)
orParts = splitTopLevel(s, ".OR.")
allSubConds = []
allLogics = []
for pi, part in enumerate(orParts):
part = part.strip()
if pi > 0:
allLogics.append(".OR.")
andParts = splitTopLevel(part, ".AND.")
for ai, ap in enumerate(andParts):
ap = ap.strip()
if ai > 0:
allLogics.append(".AND.")
allSubConds.append(ap)
for row in list(block._conditionRows):
block._condRowsLayout.removeWidget(row)
row.hide()
row.deleteLater()
block._conditionRows.clear()
for i, subCond in enumerate(allSubConds):
subCond = subCond.strip()
subCond = stripOuterParens(subCond)
isFirst = (i == 0)
row = ConditionRowFrame(
self._varMgr, block.blockIndex,
isFirst=isFirst, parent=block
)
if not isFirst:
row.deleteBtn.clicked.connect(
lambda _checked=False, r=row: block.removeConditionRow(r)
)
if i - 1 < len(allLogics):
logic = allLogics[i - 1]
for li in range(row.logicCombo.count()):
if row.logicCombo.itemData(li) == logic:
row.logicCombo.setCurrentIndex(li)
break
block._conditionRows.append(row)
block._condRowsLayout.addWidget(row)
subUp = subCond.upper()
if subUp in (".TRUE.", ".FALSE."):
row.loadFromParts(subUp, "", "")
else:
opSyms = [op for _, op in COMPARE_OPERATORS]
result = findOperatorIn(subCond, opSyms)
if result:
idx, op = result
leftPart = subCond[:idx].strip()
rightPart = subCond[idx + len(op):].strip()
row.loadFromParts(leftPart, op, rightPart)
else:
row.loadFromParts(subCond, "", "")
if not block._conditionRows:
block.addInitialConditionRow()
+688
View File
@@ -0,0 +1,688 @@
"""
Helper utilities and constants for the AutoScript orchestration dialog.
"""
import re
from PySide6.QtCore import QObject, QDate, QTime
from PySide6.QtWidgets import (
QComboBox,
QDateEdit,
QDoubleSpinBox,
QHBoxLayout,
QLabel,
QLineEdit,
QSizePolicy,
QSpinBox,
QStackedWidget,
QTimeEdit,
QWidget,
)
from autoscript import ALL_VARIABLES
VAR_TYPE_ORDER = [
"String",
"Int",
"Float",
"Boolean",
"Date",
"Time"
]
PRESET_VARIABLES = [
{
"name": name.upper(),
"type": vtype,
"display": display
}
for display, (name, vtype) in ALL_VARIABLES.items()
]
PRESET_NAMES = {
p["name"] for p in PRESET_VARIABLES
}
COMPARE_OPERATORS = sorted([
("等于", ".EQ."),
("不等于", ".NEQ."),
("大于", ".BGT."),
("小于", ".BLT."),
("大于等于", ".BGE."),
("小于等于", ".BLE."),
], key=lambda x: len(x[1]), reverse=True)
LOGIC_OPERATORS = [
("并且 (.AND.)", ".AND."),
("或者 (.OR.)", ".OR."),
]
ACTION_TYPES = [
("设置为", "set"),
("增加", "add"),
("减少", "sub"),
]
ARITH_TYPES = {
"Date",
"Time",
"Int",
"Float"
}
DATE_RELATIVE_OPTIONS = [
("前天", "day_before_yesterday"),
("昨天", "yesterday"),
("今天", "today"),
("明天", "tomorrow"),
("后天", "day_after_tomorrow")
]
DATE_OFFSET_UNITS = [
("", "days"),
("", "weeks"),
("", "months"),
("", "years"),
]
class VariableManager(QObject):
def __init__(
self,
parent = None
):
super().__init__(parent)
self._vars = []
self._nameMap = {}
self._initPresetVars()
def _initPresetVars(
self
):
for p in PRESET_VARIABLES:
entry = {"name": p["name"], "type": p["type"], "display": p["display"]}
self._vars.append(entry)
self._nameMap[p["name"]] = entry
def getInfoByName(
self,
name: str
):
return self._nameMap.get(name.upper().strip())
def populateCombo(
self,
combo: QComboBox
):
currentData = combo.currentData()
combo.blockSignals(True)
combo.clear()
for entry in self._vars:
combo.addItem(
entry["display"],
(entry["name"], entry["type"])
)
if currentData:
for i in range(combo.count()):
d = combo.itemData(i)
if d and d[0] == currentData[0]:
combo.setCurrentIndex(i)
break
combo.blockSignals(False)
def findExactNameEntry(
self,
combo: QComboBox,
name: str
) -> int:
name = name.upper().strip()
for i in range(combo.count()):
d = combo.itemData(i)
if d and len(d) >= 1 and d[0].upper().strip() == name:
return i
return -1
def makeValueWidget(
var_type: str,
parent: QWidget = None
) -> QWidget:
if var_type == "Int":
w = QSpinBox(parent)
w.setRange(-999999, 999999)
w.setFixedHeight(25)
w.setMinimumWidth(100)
return w
if var_type == "Float":
w = QDoubleSpinBox(parent)
w.setRange(-999999.0, 999999.0)
w.setDecimals(2)
w.setFixedHeight(25)
w.setMinimumWidth(100)
return w
if var_type == "String":
w = QLineEdit(parent)
w.setPlaceholderText("输入值")
w.setFixedHeight(25)
w.setMinimumWidth(120)
return w
if var_type == "Boolean":
w = QComboBox(parent)
w.addItem("是 (.TRUE.)", ".TRUE.")
w.addItem("否 (.FALSE.)", ".FALSE.")
w.setFixedHeight(25)
w.setMinimumWidth(100)
return w
if var_type == "Date":
return _DateInputContainer(parent)
if var_type == "Time":
return _TimeInputContainer(parent)
w = QLineEdit(parent)
w.setPlaceholderText("输入值")
w.setFixedHeight(25)
w.setMinimumWidth(120)
return w
def makeOffsetWidget(
var_type: str,
parent: QWidget = None
) -> QWidget:
if var_type == "Int":
w = QSpinBox(parent)
w.setRange(-999999, 999999)
w.setFixedHeight(25)
w.setMinimumWidth(100)
return w
if var_type == "Float":
w = QDoubleSpinBox(parent)
w.setRange(-999999.0, 999999.0)
w.setDecimals(2)
w.setFixedHeight(25)
w.setMinimumWidth(100)
return w
if var_type == "Date":
return _DateOffsetContainer(parent)
if var_type == "Time":
return _TimeOffsetContainer(parent)
w = QLabel("(不支持该操作)", parent)
w.setFixedHeight(25)
return w
def makeVarRefCombo(
parent: QWidget = None
) -> QComboBox:
cb = QComboBox(parent)
cb.setFixedHeight(25)
cb.setMinimumWidth(120)
cb.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
return cb
def makeComboWidget(
items,
min_width: int = 80,
parent: QWidget = None
) -> QComboBox:
cb = QComboBox(parent)
for display, data in items:
cb.addItem(display, data)
cb.setFixedHeight(25)
cb.setMinimumWidth(min_width)
return cb
def makeLabel(
text: str,
parent: QWidget = None,
width: int = None
) -> QLabel:
lbl = QLabel(text, parent)
lbl.setFixedHeight(25)
if width:
lbl.setFixedWidth(width)
return lbl
class _DateInputContainer(QWidget):
def __init__(
self,
parent = None
):
super().__init__(parent)
self.setupUi()
def setupUi(
self
):
layout = QHBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(4)
self._modeCombo = QComboBox(self)
self._modeCombo.addItem("相对日期", "relative")
self._modeCombo.addItem("绝对日期", "absolute")
self._modeCombo.setFixedHeight(25)
self._stack = QStackedWidget(self)
self._relCombo = QComboBox(self)
for display, data in DATE_RELATIVE_OPTIONS:
self._relCombo.addItem(display, data)
self._relCombo.setFixedHeight(25)
self._stack.addWidget(self._relCombo)
self._dateEdit = QDateEdit(self)
self._dateEdit.setDisplayFormat("yyyy-MM-dd")
self._dateEdit.setCalendarPopup(True)
self._dateEdit.setFixedHeight(25)
self._stack.addWidget(self._dateEdit)
self._modeCombo.currentIndexChanged.connect(
lambda i: self._stack.setCurrentIndex(i)
)
layout.addWidget(self._modeCombo)
layout.addWidget(self._stack)
layout.addStretch()
def getValue(
self
) -> str:
mode = self._modeCombo.currentData()
if mode == "relative":
return self._relCombo.currentText()
return self._dateEdit.date().toString("yyyy-MM-dd")
def setValue(
self,
expr: str
):
s = expr.strip().upper()
if s == "CURRENT_DATE - 2":
self._modeCombo.setCurrentIndex(0)
self._relCombo.setCurrentIndex(4)
elif s == "CURRENT_DATE - 1":
self._modeCombo.setCurrentIndex(0)
self._relCombo.setCurrentIndex(3)
elif s in ("CURRENT_DATE", "TODAY"):
self._modeCombo.setCurrentIndex(0)
self._relCombo.setCurrentIndex(0)
elif s == "CURRENT_DATE + 1" or s == "TOMORROW":
self._modeCombo.setCurrentIndex(0)
self._relCombo.setCurrentIndex(1)
elif s == "CURRENT_DATE + 2":
self._modeCombo.setCurrentIndex(0)
self._relCombo.setCurrentIndex(2)
elif s.startswith("DATE("):
self._modeCombo.setCurrentIndex(1)
m = re.match(r"DATE\((\d{4}-\d{2}-\d{2})\)", s)
if m:
parts = m.group(1).split("-")
self._dateEdit.setDate(QDate(int(parts[0]), int(parts[1]), int(parts[2])))
class _TimeInputContainer(QWidget):
def __init__(
self,
parent = None
):
super().__init__(parent)
self._timeEdit = QTimeEdit(self)
self._timeEdit.setDisplayFormat("HH:mm")
self._timeEdit.setFixedHeight(25)
layout = QHBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
layout.addWidget(self._timeEdit)
def getValue(
self
) -> str:
return self._timeEdit.time().toString("HH:mm")
def setValue(
self,
expr: str
):
s = expr.strip().upper()
m = re.match(r"TIME\((\d{1,2}:\d{2})\)", s)
if m:
parts = m.group(1).split(":")
self._timeEdit.setTime(QTime(int(parts[0]), int(parts[1])))
class _DateOffsetContainer(QWidget):
def __init__(
self,
parent = None
):
super().__init__(parent)
self._spinBox = QSpinBox(self)
self._spinBox.setRange(0, 99999)
self._spinBox.setFixedHeight(25)
self._unitCombo = QComboBox(self)
for display, data in DATE_OFFSET_UNITS:
self._unitCombo.addItem(display, data)
self._unitCombo.setFixedHeight(25)
layout = QHBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(4)
layout.addWidget(self._spinBox)
layout.addWidget(self._unitCombo)
layout.addStretch()
def getValue(
self
) -> str:
return str(self.getOffsetDays())
def setValue(
self,
expr: str
):
s = expr.strip().lstrip("+")
try:
self._spinBox.setValue(int(s))
except ValueError:
pass
def getOffsetDays(
self
) -> int:
val = self._spinBox.value()
unit = self._unitCombo.currentData()
if unit == "weeks":
return val * 7
if unit == "months":
return val * 30
if unit == "years":
return val * 365
return val
def getRawValue(
self
) -> str:
return str(self._spinBox.value())
class _TimeOffsetContainer(QWidget):
def __init__(
self,
parent = None
):
super().__init__(parent)
self._spinBox = QSpinBox(self)
self._spinBox.setRange(0, 99999)
self._spinBox.setSuffix(" 小时")
self._spinBox.setFixedHeight(25)
layout = QHBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
layout.addWidget(self._spinBox)
def getValue(
self
) -> str:
return str(self.getOffsetHours())
def setValue(
self,
expr: str
):
s = expr.strip().lstrip("+")
try:
self._spinBox.setValue(int(s))
except ValueError:
pass
def getOffsetHours(
self
) -> int:
return self._spinBox.value()
def getRawValue(
self
) -> str:
return str(self._spinBox.value())
def getValueFromWidget(
w: QWidget
) -> str:
if hasattr(w, "getValue"):
return w.getValue()
if isinstance(w, QTimeEdit):
return w.time().toString("HH:mm")
if isinstance(w, QDateEdit):
return w.date().toString("yyyy-MM-dd")
if isinstance(w, QComboBox):
return w.currentData() or w.currentText()
if isinstance(w, QSpinBox):
return str(w.value())
if isinstance(w, QDoubleSpinBox):
return str(w.value())
if isinstance(w, QLineEdit):
return w.text()
return ""
def setWidgetValue(
w: QWidget,
var_type: str,
expr: str
):
if hasattr(w, "setValue"):
w.setValue(expr)
return
s = expr.strip().upper()
if isinstance(w, QTimeEdit):
m = re.match(r"TIME\((\d{1,2}:\d{2})\)", s)
if m:
parts = m.group(1).split(":")
w.setTime(QTime(int(parts[0]), int(parts[1])))
elif isinstance(w, QDateEdit):
m = re.match(r"DATE\((\d{4}-\d{2}-\d{2})\)", s)
if m:
parts = m.group(1).split("-")
w.setDate(QDate(int(parts[0]), int(parts[1]), int(parts[2])))
elif isinstance(w, QComboBox):
for i in range(w.count()):
if w.itemData(i) == s or w.itemText(i).upper() == s:
w.setCurrentIndex(i)
return
elif isinstance(w, QSpinBox):
try:
w.setValue(int(expr))
except ValueError:
pass
elif isinstance(w, QDoubleSpinBox):
try:
w.setValue(float(expr))
except ValueError:
pass
elif isinstance(w, QLineEdit):
inner = expr.strip()
if (inner.startswith("'") and inner.endswith("'")) or \
(inner.startswith('"') and inner.endswith('"')):
inner = inner[1:-1].replace("''", "'")
w.setText(inner)
def encodeValueStr(
raw_value: str,
var_type: str
) -> str:
if var_type == "Time":
if raw_value.startswith("+") or raw_value.startswith("-"):
return raw_value
if raw_value.startswith("TIME_OFFSET"):
m = re.match(r"TIME_OFFSET\(([+-]\d+),(\w+)\)", raw_value)
if m:
return m.group(1)
return raw_value
return f"TIME({raw_value})"
if var_type == "Date":
relMap = {
"前天": "CURRENT_DATE - 2",
"昨天": "CURRENT_DATE - 1",
"今天": "CURRENT_DATE",
"明天": "CURRENT_DATE + 1",
"后天": "CURRENT_DATE + 2"
}
if raw_value in relMap:
return relMap[raw_value]
return f"DATE({raw_value})"
if var_type == "Boolean":
up = raw_value.upper().strip()
if up in (".TRUE.", ".FALSE."):
return up
return ".TRUE." if raw_value else ".FALSE."
if var_type == "String":
escaped = raw_value.replace("'", "''")
return f"'{escaped}'"
return raw_value
def splitTopLevel(
text: str,
delimiter: str
) -> list:
parts = []
depth = 0
buf = ""
i = 0
textUpper = text.upper()
delimUpper = delimiter.upper()
dlen = len(delimUpper)
while i < len(text):
if text[i] == "(":
depth += 1
buf += text[i]
elif text[i] == ")":
depth -= 1
buf += text[i]
elif depth == 0 and textUpper[i:i + dlen] == delimUpper:
parts.append(buf)
buf = ""
i += dlen
continue
else:
buf += text[i]
i += 1
if buf.strip():
parts.append(buf)
return parts
def stripOuterParens(
s: str
) -> str:
s = s.strip()
if s.startswith("(") and s.endswith(")"):
depth = 0
for i, ch in enumerate(s):
if ch == "(":
depth += 1
elif ch == ")":
depth -= 1
if depth == 0 and i < len(s) - 1:
return s
return s[1:-1].strip()
return s
def isVarReference(
expr: str
) -> bool:
s = expr.strip()
up = s.upper()
if up in (".TRUE.", ".FALSE."):
return False
if re.match(r"^TIME\(|^DATE\(|^CURRENT_", up):
return False
if up.startswith("'") or up.startswith('"'):
return False
if re.match(r"^[+-]?\d", s):
return False
return bool(re.match(r"^[A-Z_][A-Z0-9_]*$", up))
def findOperatorIn(
text: str,
operators: list
) -> tuple[int, str] | None:
for op in operators:
op_upper = op.upper()
start = 0
while True:
idx = text.upper().find(op_upper, start)
if idx < 0:
break
if _isInsideLiteral(text, idx):
start = idx + 1
continue
return (idx, op)
return None
def _isInsideLiteral(
text: str,
pos: int
) -> bool:
in_single = False
in_double = False
for i, ch in enumerate(text):
if i >= pos:
break
if ch == "'" and not in_double:
in_single = not in_single
elif ch == '"' and not in_single:
in_double = not in_double
return in_single or in_double
@@ -0,0 +1,107 @@
"""
Orchestration observer for AutoScript scripts.
Subscribes to ASTokenizer parsing events to produce a structured
block representation for the orchestration dialog UI.
"""
from autoscript.ASObserver import ParsingObserver
from autoscript.ASTokenizer import (
ASTokenizer,
K_IF,
K_ELSE_IF,
K_ELSE,
K_ENDIF,
K_SET,
K_ADD,
K_SUB,
)
__all__ = ["ScriptOrchObserver", "parseBlocks"]
class ScriptOrchObserver(ParsingObserver):
"""
Builds an ordered list of (block_type, condition, actions) tuples
from tokenization events.
Each block:
(type: str, condition: str | None, actions: list[(target, value_expr, op_type)])
"""
def __init__(
self
):
super().__init__()
self._blocks = []
self._current_type = None
self._current_condition = None
self._current_actions = []
def onTokenParsed(
self,
kind: str | None,
data,
line_num: int,
raw_line: str
):
if kind in (K_IF, K_ELSE_IF, K_ELSE):
self._flushCurrentBlock()
self._current_type = kind
self._current_condition = data if kind != K_ELSE else None
self._current_actions = []
elif kind in (K_SET, K_ADD, K_SUB):
target, value = data
if kind == K_SET:
self._current_actions.append((target, value, "set"))
elif kind == K_ADD:
self._current_actions.append((target, f"+{value}", "add"))
else:
self._current_actions.append((target, f"-{value}", "sub"))
elif kind == K_ENDIF:
self._flushCurrentBlock()
self._current_type = None
self._current_condition = None
self._current_actions = []
def onParseComplete(
self,
statements: list
):
self._flushCurrentBlock()
def _flushCurrentBlock(
self
):
if self._current_type is not None:
self._blocks.append((
self._current_type,
self._current_condition,
list(self._current_actions),
))
@property
def blocks(
self
) -> list:
return list(self._blocks)
def parseBlocks(
script: str
) -> list:
"""
Tokenize a script via observer pipeline and return its
structured block representation.
"""
observer = ScriptOrchObserver()
ASTokenizer.tokenizeWithObservers(script, [observer])
return observer.blocks
+163
View File
@@ -0,0 +1,163 @@
"""
Pre-check observer for AutoScript scripts.
Subscribes to ASTokenizer parsing events to validate script syntax
before it reaches the orchestration dialog, eliminating duplicate parsing.
"""
from autoscript.ASObserver import ParsingObserver
from autoscript.ASTokenizer import (
K_IF,
K_ELSE_IF,
K_ELSE,
K_ENDIF,
K_SET,
K_ADD,
K_SUB,
ASTokenizer,
)
__all__ = ["ScriptPrecheckObserver", "precheck"]
class ScriptPrecheckObserver(ParsingObserver):
"""
Validates script syntax and structure during tokenization.
Checks performed:
- IF/ENDIF depth matching
- No nested IF blocks (orchestration limitation)
- ELSE IF / ELSE appear only inside an IF block
- Only allowed variables appear in SET/ADD/SUB targets
- No completely unrecognized syntax lines
"""
def __init__(
self,
allowed_vars: set = None
):
super().__init__()
self._allowed = allowed_vars or set()
self._if_depth = 0
self.errors = []
self._stmts = []
def onTokenParsed(
self,
kind: str | None,
data,
line_num: int,
raw_line: str
):
if kind == K_IF:
self._if_depth += 1
if self._if_depth > 1:
self.errors.append(
f"静态检查:错误(第{line_num}行): 检测到嵌套 IF,编排窗口不支持嵌套条件块。"
)
elif kind == K_ELSE_IF:
if self._if_depth < 1:
self.errors.append(
f"静态检查:错误(第{line_num}行): ELSE IF 前缺少 IF。"
)
elif kind == K_ELSE:
if self._if_depth < 1:
self.errors.append(
f"静态检查:错误(第{line_num}行): ELSE 前缺少 IF。"
)
elif kind == K_ENDIF:
self._if_depth -= 1
if self._if_depth < 0:
self.errors.append(
f"静态检查:错误(第{line_num}行): 多余的 ENDIF。"
)
elif kind is None:
self.errors.append(
f"静态检查:错误(第{line_num}行): 无法识别的语法 '{raw_line}'"
)
elif kind in (K_SET, K_ADD, K_SUB):
target = data[0] if isinstance(data, tuple) else ""
if self._allowed and target.upper() not in self._allowed:
self.errors.append(
f"静态检查:错误(第{line_num}行): 目标变量 '{target}' 不是预设变量,"
f"编排窗口不支持。"
)
def onParseComplete(
self,
statements: list
):
if self._if_depth != 0:
self.errors.append(
f"静态检查:错误(不适用): IF 与 ENDIF 不匹配。")
self._stmts = statements
@property
def valid(
self
) -> bool:
return len(self.errors) == 0
def getErrorMessage(
self
) -> str:
return self.errors[0] if self.errors else ""
def buildSimplifiedScript(
self
) -> str:
"""Replace all non-control-flow statements with PASS for engine validation."""
lines = []
for stmt in self._stmts:
if stmt.kind in (K_IF, K_ELSE_IF, K_ELSE, K_ENDIF):
lines.append(stmt.raw_line)
else:
lines.append("PASS")
return "\n".join(lines)
def precheck(
script: str,
allowed_vars: set = None
) -> tuple[bool, str]:
"""
Run the full precheck pipeline on a script.
Steps:
1. Create a ScriptPrecheckObserver and subscribe it to an ASTokenizer.
2. Tokenize — the observer validates syntax during token events.
3. Replace action lines with PASS and run engine validation
with mock target data.
"""
if not script or not script.strip():
return True, ""
observer = ScriptPrecheckObserver(allowed_vars=allowed_vars)
ASTokenizer.tokenizeWithObservers(script, [observer])
if not observer.valid:
return False, observer.getErrorMessage()
simplified = observer.buildSimplifiedScript()
if not simplified.strip():
return True, ""
try:
from autoscript import (
registerDefaultTargetVars,
buildMockTargetData,
execute
)
registerDefaultTargetVars()
execute(simplified, buildMockTargetData())
except ValueError as e:
return False, f"运行时检查: {e}"
except Exception:
return False, "执行环境异常,请检查 AutoScript 配置。"
return True, ""
+534
View File
@@ -0,0 +1,534 @@
"""
Widget components for the AutoScript orchestration dialog.
"""
from PySide6.QtCore import Slot
from PySide6.QtWidgets import (
QComboBox,
QFrame,
QHBoxLayout,
QLabel,
QPushButton,
QSizePolicy,
QStackedWidget
)
from gui.ALAutoScriptOrchDialog._helpers import (
ACTION_TYPES,
ARITH_TYPES,
COMPARE_OPERATORS,
LOGIC_OPERATORS,
PRESET_VARIABLES,
VAR_TYPE_ORDER,
encodeValueStr,
getValueFromWidget,
isVarReference,
makeComboWidget,
makeLabel,
makeOffsetWidget,
makeValueWidget,
makeVarRefCombo,
setWidgetValue,
)
class ConditionRowFrame(QFrame):
def __init__(
self,
varMgr,
parentBlockIndex: int = 0,
isFirst: bool = False,
parent = None
):
super().__init__(parent)
self._varMgr = varMgr
self._blockIndex = parentBlockIndex
self._isFirst = isFirst
self._isBoolMode = False
self.setupUi()
self.connectSignals()
def setupUi(
self
):
self.setUpdatesEnabled(False)
self.setFrameShape(QFrame.StyledPanel)
self.setFrameShadow(QFrame.Raised)
self.setFixedHeight(32)
layout = QHBoxLayout(self)
layout.setContentsMargins(2, 2, 2, 2)
layout.setSpacing(4)
if self._isFirst:
self.logicCombo = None
else:
self.logicCombo = makeComboWidget(LOGIC_OPERATORS, min_width=110, parent=self)
layout.addWidget(self.logicCombo)
self.leftVarCombo = QComboBox(self)
self.leftVarCombo.setFixedHeight(25)
self.leftVarCombo.setMinimumWidth(120)
self.leftVarCombo.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
self.populateLeftVarCombo()
layout.addWidget(self.leftVarCombo)
self.opCombo = makeComboWidget(COMPARE_OPERATORS, min_width=80, parent=self)
layout.addWidget(self.opCombo)
self._compTypeCombo = makeComboWidget([
("特定值", "literal"),
("变量", "variable"),
], min_width=70, parent=self)
layout.addWidget(self._compTypeCombo)
self._rhsStack = QStackedWidget(self)
self._rhsStack.setFixedHeight(25)
self._literalStack = QStackedWidget(self)
self._literalStack.setFixedHeight(25)
self._literalWidgets = {}
for vt in VAR_TYPE_ORDER:
w = makeValueWidget(vt, self._literalStack)
self._literalWidgets[vt] = w
self._literalStack.addWidget(w)
self._literalStack.setCurrentWidget(self._literalWidgets.get("String"))
self._rhsStack.addWidget(self._literalStack)
self.rhsVarCombo = makeVarRefCombo(self)
self._rhsStack.addWidget(self.rhsVarCombo)
self._rhsStack.setCurrentIndex(0)
layout.addWidget(self._rhsStack)
if not self._isFirst:
self.deleteBtn = QPushButton("×", self)
self.deleteBtn.setFixedSize(25, 25)
self.deleteBtn.setStyleSheet("color: red; font-weight: bold;")
layout.addWidget(self.deleteBtn)
else:
self.deleteBtn = None
layout.addStretch()
self.setUpdatesEnabled(True)
def populateLeftVarCombo(
self
):
self._varMgr.populateCombo(self.leftVarCombo)
def populateRhsVarCombo(
self
):
self._varMgr.populateCombo(self.rhsVarCombo)
def connectSignals(
self
):
self.leftVarCombo.currentIndexChanged.connect(self.onLeftVarChanged)
self._compTypeCombo.currentIndexChanged.connect(self.onCompTypeChanged)
@Slot(int)
def onLeftVarChanged(
self,
idx
):
if idx < 0:
return
data = self.leftVarCombo.itemData(idx)
if not data:
return
_, vartype = data
self.updateRhsLiteralWidget(vartype)
def updateRhsLiteralWidget(
self,
vartype: str
):
if vartype not in self._literalWidgets:
vartype = "String"
self._literalStack.setCurrentWidget(self._literalWidgets[vartype])
@Slot(int)
def onCompTypeChanged(
self,
idx
):
isVar = (self._compTypeCombo.currentData() == "variable")
self._rhsStack.setCurrentIndex(1 if isVar else 0)
if isVar:
self.populateRhsVarCombo()
def getLogic(
self
) -> str:
return self.logicCombo.currentData() if self.logicCombo else ""
def toConditionText(
self
) -> str:
data = self.leftVarCombo.currentData()
if not data:
return ""
name, vartype = data
opSym = self.opCombo.currentData()
isVarRef = (self._compTypeCombo.currentData() == "variable")
if isVarRef:
rd = self.rhsVarCombo.currentData()
if rd:
rhsName = rd[0]
return f"{name} {opSym} {rhsName}"
rhsText = self.rhsVarCombo.currentText().strip()
if rhsText:
return f"{name} {opSym} {rhsText}"
return ""
w = self._literalWidgets.get(vartype)
if w:
rawVal = getValueFromWidget(w)
encoded = encodeValueStr(rawVal, vartype)
return f"{name} {opSym} {encoded}"
return ""
def loadFromParts(
self,
operandName: str,
opSym: str,
valueExpr: str
):
for ci in range(self.leftVarCombo.count()):
d = self.leftVarCombo.itemData(ci)
if d and d[0] == operandName:
self.leftVarCombo.setCurrentIndex(ci)
break
if opSym:
for oi in range(self.opCombo.count()):
if self.opCombo.itemData(oi) == opSym:
self.opCombo.setCurrentIndex(oi)
break
if not valueExpr:
return
up = valueExpr.strip().upper()
data = self.leftVarCombo.currentData()
vartype = data[1] if data else "String"
if isVarReference(valueExpr) or self._isKnownVar(up):
self._compTypeCombo.setCurrentIndex(1)
self.populateRhsVarCombo()
found = self._varMgr.findExactNameEntry(self.rhsVarCombo, up)
if found >= 0:
self.rhsVarCombo.setCurrentIndex(found)
else:
self.rhsVarCombo.addItem(up, (up, "String"))
self.rhsVarCombo.setCurrentIndex(self.rhsVarCombo.count() - 1)
else:
self._compTypeCombo.setCurrentIndex(0)
w = self._literalWidgets.get(vartype)
if w:
setWidgetValue(w, vartype, valueExpr)
def _isKnownVar(
self,
name: str
) -> bool:
return self._varMgr.getInfoByName(name) is not None
def refreshVarCombos(
self
):
self.populateLeftVarCombo()
self.populateRhsVarCombo()
class ActionStepFrame(QFrame):
def __init__(
self,
varMgr,
parentBlockIndex: int = 0,
parent = None
):
super().__init__(parent)
self._varMgr = varMgr
self._blockIndex = parentBlockIndex
self._currentTargetType = "String"
self.setupUi()
self.connectSignals()
def setupUi(
self
):
self.setUpdatesEnabled(False)
self.setFrameShape(QFrame.StyledPanel)
self.setFrameShadow(QFrame.Raised)
self.setFixedHeight(35)
layout = QHBoxLayout(self)
layout.setContentsMargins(2, 2, 2, 2)
layout.setSpacing(4)
self.opTypeCombo = makeComboWidget(ACTION_TYPES, min_width=70, parent=self)
layout.addWidget(self.opTypeCombo)
layout.addWidget(makeLabel("设置", self))
self.targetCombo = QComboBox(self)
self.targetCombo.setFixedHeight(25)
self.targetCombo.setMinimumWidth(120)
self.buildTargetCombo()
layout.addWidget(self.targetCombo)
layout.addWidget(makeLabel("", self))
self._valueSrcCombo = makeComboWidget([
("特定值", "literal"),
("变量", "variable"),
], min_width=70, parent=self)
layout.addWidget(self._valueSrcCombo)
self._valueStack = QStackedWidget(self)
self._valueStack.setFixedHeight(25)
self.initValueStacks()
layout.addWidget(self._valueStack)
self.existingVarCombo = makeVarRefCombo(self)
self.existingVarCombo.setVisible(False)
layout.addWidget(self.existingVarCombo)
self.deleteBtn = QPushButton("×", self)
self.deleteBtn.setFixedSize(25, 25)
self.deleteBtn.setStyleSheet("color: red; font-weight: bold;")
layout.addWidget(self.deleteBtn)
self.setUpdatesEnabled(True)
def buildTargetCombo(
self
):
self.targetCombo.blockSignals(True)
self.targetCombo.clear()
for p in PRESET_VARIABLES:
if p["name"] in ("CURRENT_TIME", "CURRENT_DATE"):
continue
info = self._varMgr.getInfoByName(p["name"])
if info:
self.targetCombo.addItem(
info["display"],
(info["name"], info["type"])
)
self.targetCombo.blockSignals(False)
def initValueStacks(
self
):
self._literalWidgets = {}
self._offsetWidgets = {}
for vt in VAR_TYPE_ORDER:
self._literalWidgets[vt] = makeValueWidget(vt, self._valueStack)
self._valueStack.addWidget(self._literalWidgets[vt])
if vt in ARITH_TYPES:
self._offsetWidgets[vt] = makeOffsetWidget(vt, self._valueStack)
self._valueStack.addWidget(self._offsetWidgets[vt])
else:
lbl = QLabel("(不支持该操作)", self._valueStack)
lbl.setFixedHeight(25)
self._offsetWidgets[vt] = lbl
self._valueStack.addWidget(lbl)
def connectSignals(
self
):
self.opTypeCombo.currentIndexChanged.connect(self.onOpTypeChanged)
self.targetCombo.currentIndexChanged.connect(self.onTargetChanged)
self._valueSrcCombo.currentIndexChanged.connect(self.onValueSrcChanged)
@Slot(int)
def onTargetChanged(
self,
idx
):
if idx < 0:
return
data = self.targetCombo.itemData(idx)
if not data:
return
_, vartype = data
self._currentTargetType = vartype
self.updateRHSWidget()
self.onValueSrcChanged(self._valueSrcCombo.currentIndex())
@Slot(int)
def onOpTypeChanged(
self,
idx
):
self.updateRHSWidget()
def updateRHSWidget(
self
):
op = self.opTypeCombo.currentData()
isArith = (op in ("add", "sub"))
actualType = self._currentTargetType
if isArith and actualType in self._offsetWidgets:
self._valueStack.setCurrentWidget(self._offsetWidgets[actualType])
elif actualType in self._literalWidgets:
self._valueStack.setCurrentWidget(self._literalWidgets[actualType])
else:
self._valueStack.setCurrentWidget(self._literalWidgets.get("String"))
@Slot(int)
def onValueSrcChanged(
self,
idx
):
isVar = (self._valueSrcCombo.currentData() == "variable")
self._valueStack.setVisible(not isVar)
self.existingVarCombo.setVisible(isVar)
if isVar:
self._varMgr.populateCombo(self.existingVarCombo)
else:
self.updateRHSWidget()
def getTargetName(
self
) -> str:
data = self.targetCombo.currentData()
return data[0] if data else ""
def toScriptLine(
self
) -> str:
target = self.getTargetName()
if not target:
return ""
op = self.opTypeCombo.currentData()
rawVal = self._getValueRaw()
if op == "set":
vartype = self._currentTargetType
encoded = encodeValueStr(rawVal, vartype)
if vartype == "Time":
if rawVal.startswith("+"):
return f" {target} .ADD. {rawVal[1:]}"
if rawVal.startswith("-"):
return f" {target} .SUB. {rawVal[1:]}"
return f" SET {target} = {encoded}"
elif op == "add":
vartype = self._currentTargetType
if vartype == "Date" and hasattr(self._valueStack.currentWidget(), "getOffsetDays"):
days = self._valueStack.currentWidget().getOffsetDays()
return f" {target} .ADD. {days}"
if vartype == "Time" and hasattr(self._valueStack.currentWidget(), "getOffsetHours"):
hours = self._valueStack.currentWidget().getOffsetHours()
return f" {target} .ADD. {hours}"
return f" {target} .ADD. {rawVal}"
elif op == "sub":
vartype = self._currentTargetType
if vartype == "Date" and hasattr(self._valueStack.currentWidget(), "getOffsetDays"):
days = self._valueStack.currentWidget().getOffsetDays()
return f" {target} .SUB. {days}"
if vartype == "Time" and hasattr(self._valueStack.currentWidget(), "getOffsetHours"):
hours = self._valueStack.currentWidget().getOffsetHours()
return f" {target} .SUB. {hours}"
return f" {target} .SUB. {rawVal}"
return ""
def _getValueRaw(
self
) -> str:
if self._valueSrcCombo.currentData() == "variable":
return self.existingVarCombo.currentText().strip()
w = self._valueStack.currentWidget()
if w:
return getValueFromWidget(w)
return ""
def setOpType(
self,
opType: str
):
for i in range(self.opTypeCombo.count()):
if self.opTypeCombo.itemData(i) == opType:
self.opTypeCombo.setCurrentIndex(i)
break
def loadFromScript(
self,
targetVar: str,
valueExpr: str
):
targetUp = targetVar.upper().strip()
for ci in range(self.targetCombo.count()):
d = self.targetCombo.itemData(ci)
if d and d[0] == targetUp:
self.targetCombo.setCurrentIndex(ci)
break
self._setValueFromExpr(valueExpr)
def _setValueFromExpr(
self,
expr: str
):
s = expr.strip()
if not s:
return
up = s.upper()
if isVarReference(s):
self._valueSrcCombo.setCurrentIndex(1)
self._varMgr.populateCombo(self.existingVarCombo)
idx = self._varMgr.findExactNameEntry(self.existingVarCombo, up)
if idx >= 0:
self.existingVarCombo.setCurrentIndex(idx)
else:
self.existingVarCombo.addItem(up, (up, "String"))
self.existingVarCombo.setCurrentIndex(self.existingVarCombo.count() - 1)
else:
self._valueSrcCombo.setCurrentIndex(0)
w = self._valueStack.currentWidget()
if w:
setWidgetValue(w, self._currentTargetType, expr)
def refreshVarCombos(
self
):
currentData = self.targetCombo.currentData()
self.buildTargetCombo()
if currentData:
for i in range(self.targetCombo.count()):
d = self.targetCombo.itemData(i)
if d and d[0] == currentData[0]:
self.targetCombo.setCurrentIndex(i)
break
self._varMgr.populateCombo(self.existingVarCombo)
-226
View File
@@ -1,226 +0,0 @@
# -*- coding: utf-8 -*-
"""
Copyright (c) 2026 KenanZhu.
All rights reserved.
This software is provided "as is", without any warranty of any kind.
You may use, modify, and distribute this file under the terms of the MIT License.
See the LICENSE file for details.
"""
from PySide6.QtCore import Slot
from PySide6.QtGui import (
QSyntaxHighlighter, QTextCharFormat, QColor, QFont, QIcon
)
from PySide6.QtWidgets import (
QDialog, QVBoxLayout, QHBoxLayout, QPlainTextEdit,
QDialogButtonBox, QPushButton, QLabel, QApplication, QStyle
)
class ALScriptHighlighter(QSyntaxHighlighter):
def __init__(
self,
parent=None
):
super().__init__(parent)
self._rules = []
keywordFmt = QTextCharFormat()
keywordFmt.setForeground(QColor("#316BFF"))
keywordFmt.setFontWeight(QFont.Weight.Bold)
for kw in ["IF", "ELSE IF", "ELSE", "ENDIF", "END IF",
"SET", "PASS", "THEN"]:
pattern = r"\b" + kw.replace(" ", r"\s+") + r"\b"
self._rules.append((pattern, keywordFmt))
literalFmt = QTextCharFormat()
literalFmt.setForeground(QColor("#C2185B"))
literalFmt.setFontWeight(QFont.Weight.Bold)
for lit in [".TRUE.", ".FALSE."]:
self._rules.append((r"\b" + lit.replace(".", r"\.") + r"\b", literalFmt))
opFmt = QTextCharFormat()
opFmt.setForeground(QColor("#9C27B0"))
for op in [r"\.EQ\.", r"\.NEQ\.", r"\.BGT\.", r"\.BLT\.",
r"\.BGE\.", r"\.BLE\.", r"\.ADD\.", r"\.SUB\."]:
self._rules.append((op, opFmt))
varFmt = QTextCharFormat()
varFmt.setForeground(QColor("#E65100"))
for var in ["RESERVE_BEGIN_TIME", "RESERVE_END_TIME",
"RESERVE_DATE", "USERNAME", "USER_ENABLE",
"PRIORITY", "CURRENT_TIME", "CURRENT_DATE"]:
self._rules.append((r"\b" + var + r"\b", varFmt))
funcFmt = QTextCharFormat()
funcFmt.setForeground(QColor("#2E7D32"))
self._rules.append((r"\bTIME\([^)]+\)", funcFmt))
self._rules.append((r"\bDATE\([^)]+\)", funcFmt))
strFmt = QTextCharFormat()
strFmt.setForeground(QColor("#388E3C"))
self._rules.append((r"'[^']*'", strFmt))
numFmt = QTextCharFormat()
numFmt.setForeground(QColor("#D32F2F"))
self._rules.append((r"\b\d+\b", numFmt))
commentFmt = QTextCharFormat()
commentFmt.setForeground(QColor("#999999"))
commentFmt.setFontItalic(True)
self._rules.append((r"//[^\n]*", commentFmt))
def highlightBlock(
self,
text
):
import re
for pattern, fmt in self._rules:
for match in re.finditer(pattern, text, re.IGNORECASE):
start = match.start()
length = match.end() - match.start()
self.setFormat(start, length, fmt)
class ALAutoScriptPreviewDialog(QDialog):
def __init__(
self,
parent=None,
script: str = ""
):
super().__init__(parent)
self.__fontSize = 13
self.modifyUi()
self.connectSignals()
self._textEdit.setPlainText(script)
self._highlighter = ALScriptHighlighter(
self._textEdit.document()
)
def modifyUi(
self
):
self.setWindowTitle("AutoScript 预览 - AutoLibrary")
self.setMinimumSize(520, 360)
layout = QVBoxLayout(self)
toolbarLayout = QHBoxLayout()
self._zoomInBtn = QPushButton("")
self._zoomInBtn.setFixedSize(30, 25)
self._zoomOutBtn = QPushButton("")
self._zoomOutBtn.setFixedSize(30, 25)
self._zoomResetBtn = QPushButton(
QApplication.style().standardIcon(
QStyle.StandardPixmap.SP_BrowserReload
), ""
)
self._zoomResetBtn.setFixedSize(30, 25)
self._zoomResetBtn.setToolTip("重置缩放")
self._zoomLabel = QLabel(f"{self.__fontSize}px")
self._zoomLabel.setFixedHeight(25)
toolbarLayout.addWidget(self._zoomInBtn)
toolbarLayout.addWidget(self._zoomOutBtn)
toolbarLayout.addWidget(self._zoomResetBtn)
toolbarLayout.addWidget(self._zoomLabel)
toolbarLayout.addStretch()
self._copyBtn = QPushButton(
QApplication.style().standardIcon(
QStyle.StandardPixmap.SP_FileDialogDetailedView
), ""
)
self._copyBtn.setFixedSize(30, 25)
self._copyBtn.setToolTip("复制脚本")
toolbarLayout.addWidget(self._copyBtn)
layout.addLayout(toolbarLayout)
self._textEdit = QPlainTextEdit(self)
self._textEdit.setReadOnly(True)
self._textEdit.setLineWrapMode(
QPlainTextEdit.LineWrapMode.NoWrap
)
self._textEdit.setStyleSheet(
"QPlainTextEdit {"
" font-family: 'Courier New', 'Consolas', monospace;"
" font-size: 13px;"
"}"
)
layout.addWidget(self._textEdit)
self._btnBox = QDialogButtonBox(
QDialogButtonBox.StandardButton.Close
)
self._btnBox.button(
QDialogButtonBox.StandardButton.Close
).setText("关闭")
layout.addWidget(self._btnBox)
def connectSignals(
self
):
self._btnBox.rejected.connect(self.reject)
self._zoomInBtn.clicked.connect(self.onZoomIn)
self._zoomOutBtn.clicked.connect(self.onZoomOut)
self._zoomResetBtn.clicked.connect(self.onZoomReset)
self._copyBtn.clicked.connect(self.onCopy)
def updateFontSize(
self
):
font = self._textEdit.font()
font.setPointSize(self.__fontSize)
self._textEdit.setFont(font)
self._textEdit.setStyleSheet(
"QPlainTextEdit {"
" font-family: 'Courier New', 'Consolas', monospace;"
f" font-size: {self.__fontSize}px;"
"}"
)
self._zoomLabel.setText(f"{self.__fontSize}px")
@Slot()
def onZoomIn(
self
):
self.__fontSize = min(self.__fontSize + 2, 40)
self.updateFontSize()
@Slot()
def onZoomOut(
self
):
self.__fontSize = max(self.__fontSize - 2, 8)
self.updateFontSize()
@Slot()
def onZoomReset(
self
):
self.__fontSize = 13
self.updateFontSize()
@Slot()
def onCopy(
self
):
clipboard = QApplication.clipboard()
clipboard.setText(self._textEdit.toPlainText())
original = self._copyBtn.text()
self._copyBtn.setText("已复制")
self._copyBtn.setEnabled(False)
from PySide6.QtCore import QTimer
QTimer.singleShot(2000, lambda: (
self._copyBtn.setText(original),
self._copyBtn.setEnabled(True)
))
+3 -2
View File
@@ -18,7 +18,7 @@ from PySide6.QtCore import (
from base.MsgBase import MsgBase
from operators.AutoLib import AutoLib
from utils.JSONReader import JSONReader
from dsl.AutoScriptEngine import AutoScriptEngine
from autoscript import execute, registerDefaultTargetVars
class AutoLibWorker(MsgBase, QThread):
@@ -225,7 +225,8 @@ class TimerTaskWorker(AutoLibWorker):
continue
for user in group.get("users", []):
try:
AutoScriptEngine.execute(auto_script, user)
registerDefaultTargetVars()
execute(auto_script, user)
affected_count += 1
except ValueError as e:
self._showTrace(
+12 -10
View File
@@ -102,10 +102,9 @@ class ALTimerTaskAddDialog(QDialog, Ui_ALTimerTaskAddDialog):
self.AutoScriptSetButton.setMinimumHeight(25)
self.AutoScriptSetButton.setFixedWidth(130)
autoScriptBtnLayout.addWidget(self.AutoScriptSetButton)
self.AutoScriptPreviewButton = QPushButton("预览")
self.AutoScriptPreviewButton = QPushButton("编辑")
self.AutoScriptPreviewButton.setMinimumHeight(25)
self.AutoScriptPreviewButton.setFixedWidth(60)
self.AutoScriptPreviewButton.setEnabled(False)
autoScriptBtnLayout.addWidget(self.AutoScriptPreviewButton)
autoScriptBtnLayout.addStretch()
self.AutoScriptHelpButton = QPushButton("?")
@@ -170,7 +169,6 @@ class ALTimerTaskAddDialog(QDialog, Ui_ALTimerTaskAddDialog):
self.__auto_script = auto_script
self.AutoScriptStatusLabel.setText("已设置")
self.AutoScriptStatusLabel.setStyleSheet("color: #4CAF50;")
self.AutoScriptPreviewButton.setEnabled(True)
self.ConfirmButton.setText("保存")
@@ -299,20 +297,24 @@ class ALTimerTaskAddDialog(QDialog, Ui_ALTimerTaskAddDialog):
if script:
self.AutoScriptStatusLabel.setText("已设置")
self.AutoScriptStatusLabel.setStyleSheet("color: #4CAF50;")
self.AutoScriptPreviewButton.setEnabled(True)
else:
self.AutoScriptStatusLabel.setText("未设置")
self.AutoScriptStatusLabel.setStyleSheet("color: #969696;")
self.AutoScriptPreviewButton.setEnabled(False)
dlg.deleteLater()
@Slot()
def onPreviewAutoScript(self):
if not self.__auto_script:
return
from gui.ALAutoScriptPrevDialog import ALAutoScriptPreviewDialog
dlg = ALAutoScriptPreviewDialog(self, self.__auto_script)
dlg.exec()
from gui.ALAutoScriptEditDialog import ALAutoScriptEditDialog
dlg = ALAutoScriptEditDialog(self, self.__auto_script)
if dlg.exec() == QDialog.DialogCode.Accepted:
script = dlg.getScript()
self.__auto_script = script
if script:
self.AutoScriptStatusLabel.setText("已设置")
self.AutoScriptStatusLabel.setStyleSheet("color: #4CAF50;")
else:
self.AutoScriptStatusLabel.setText("未设置")
self.AutoScriptStatusLabel.setStyleSheet("color: #969696;")
dlg.deleteLater()
@Slot()