# -*- 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 utils.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)