diff --git a/src/gui/ALAutoScriptEditDialog.py b/src/gui/ALAutoScriptEditDialog.py new file mode 100644 index 0000000..2cbfdef --- /dev/null +++ b/src/gui/ALAutoScriptEditDialog.py @@ -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) + )) diff --git a/src/gui/ALAutoScriptOrchDialog.py b/src/gui/ALAutoScriptOrchDialog.py deleted file mode 100644 index e244763..0000000 --- a/src/gui/ALAutoScriptOrchDialog.py +++ /dev/null @@ -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) \ No newline at end of file diff --git a/src/gui/ALAutoScriptOrchDialog/__init__.py b/src/gui/ALAutoScriptOrchDialog/__init__.py new file mode 100644 index 0000000..38ca871 --- /dev/null +++ b/src/gui/ALAutoScriptOrchDialog/__init__.py @@ -0,0 +1,3 @@ +from gui.ALAutoScriptOrchDialog._dialog import ALAutoScriptOrchDialog + +__all__ = ["ALAutoScriptOrchDialog"] diff --git a/src/gui/ALAutoScriptOrchDialog/_blocks.py b/src/gui/ALAutoScriptOrchDialog/_blocks.py new file mode 100644 index 0000000..4422e80 --- /dev/null +++ b/src/gui/ALAutoScriptOrchDialog/_blocks.py @@ -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) diff --git a/src/gui/ALAutoScriptOrchDialog/_dialog.py b/src/gui/ALAutoScriptOrchDialog/_dialog.py new file mode 100644 index 0000000..21f838a --- /dev/null +++ b/src/gui/ALAutoScriptOrchDialog/_dialog.py @@ -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() diff --git a/src/gui/ALAutoScriptOrchDialog/_helpers.py b/src/gui/ALAutoScriptOrchDialog/_helpers.py new file mode 100644 index 0000000..cd8261f --- /dev/null +++ b/src/gui/ALAutoScriptOrchDialog/_helpers.py @@ -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 diff --git a/src/gui/ALAutoScriptOrchDialog/_orchestrate.py b/src/gui/ALAutoScriptOrchDialog/_orchestrate.py new file mode 100644 index 0000000..b518d38 --- /dev/null +++ b/src/gui/ALAutoScriptOrchDialog/_orchestrate.py @@ -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 diff --git a/src/gui/ALAutoScriptOrchDialog/_precheck.py b/src/gui/ALAutoScriptOrchDialog/_precheck.py new file mode 100644 index 0000000..25d424b --- /dev/null +++ b/src/gui/ALAutoScriptOrchDialog/_precheck.py @@ -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, "" diff --git a/src/gui/ALAutoScriptOrchDialog/_widgets.py b/src/gui/ALAutoScriptOrchDialog/_widgets.py new file mode 100644 index 0000000..d0fced6 --- /dev/null +++ b/src/gui/ALAutoScriptOrchDialog/_widgets.py @@ -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) diff --git a/src/gui/ALAutoScriptPrevDialog.py b/src/gui/ALAutoScriptPrevDialog.py deleted file mode 100644 index 4becb08..0000000 --- a/src/gui/ALAutoScriptPrevDialog.py +++ /dev/null @@ -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) - )) diff --git a/src/gui/ALMainWorkers.py b/src/gui/ALMainWorkers.py index f6003d3..f1680b0 100644 --- a/src/gui/ALMainWorkers.py +++ b/src/gui/ALMainWorkers.py @@ -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( diff --git a/src/gui/ALTimerTaskAddDialog.py b/src/gui/ALTimerTaskAddDialog.py index dab9139..21d13fa 100644 --- a/src/gui/ALTimerTaskAddDialog.py +++ b/src/gui/ALTimerTaskAddDialog.py @@ -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()