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

refactor(gui): 编排编辑窗口适配 Lua 引擎新接口

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-21 18:22:49 +08:00
parent a0fd03f12f
commit 3cea7df736
5 changed files with 296 additions and 213 deletions
+48 -38
View File
@@ -55,6 +55,9 @@ from autoscript import (
class ALScriptHighlighter(QSyntaxHighlighter): class ALScriptHighlighter(QSyntaxHighlighter):
"""
Syntax highlighter for Lua-based AutoScript.
"""
def __init__( def __init__(
self, self,
@@ -67,26 +70,32 @@ class ALScriptHighlighter(QSyntaxHighlighter):
keywordFmt = QTextCharFormat() keywordFmt = QTextCharFormat()
keywordFmt.setForeground(QColor("#569CD6")) keywordFmt.setForeground(QColor("#569CD6"))
keywordFmt.setFontWeight(QFont.Weight.Bold) keywordFmt.setFontWeight(QFont.Weight.Bold)
for kw in ["IF", "ELSE IF", "ELSE", "ENDIF", "END IF", for kw in [
"SET", "PASS", "THEN"]: "if", "elseif", "else", "end", "then",
pattern = r"\b" + kw.replace(" ", r"\s+") + r"\b" "and", "or", "not",
self._rules.append((pattern, keywordFmt)) "local", "function", "return", "nil",
opFmt = QTextCharFormat() ]:
opFmt.setForeground(QColor("#C586C0")) self._rules.append((r"\b" + kw + r"\b", keywordFmt))
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))
boolFmt = QTextCharFormat() boolFmt = QTextCharFormat()
boolFmt.setForeground(QColor("#4FC1FF")) boolFmt.setForeground(QColor("#4FC1FF"))
boolFmt.setFontWeight(QFont.Weight.Bold) boolFmt.setFontWeight(QFont.Weight.Bold)
self._rules.append((r"\.TRUE\.", boolFmt)) self._rules.append((r"\btrue\b", boolFmt))
self._rules.append((r"\.FALSE\.", boolFmt)) self._rules.append((r"\bfalse\b", boolFmt))
cmpFmt = QTextCharFormat()
cmpFmt.setForeground(QColor("#C586C0"))
cmpFmt.setFontWeight(QFont.Weight.Normal)
for op in [r"==", r"~=", r">=", r"<=", r">", r"<"]:
self._rules.append((op, cmpFmt))
arithFmt = QTextCharFormat()
arithFmt.setForeground(QColor("#C586C0"))
arithFmt.setFontWeight(QFont.Weight.Normal)
for op in [r"\+", r"-", r"\*", r"/", r"\.\."]:
self._rules.append((op, arithFmt))
funcFmt = QTextCharFormat() funcFmt = QTextCharFormat()
funcFmt.setForeground(QColor("#DCDCAA")) funcFmt.setForeground(QColor("#DCDCAA"))
funcFmt.setFontWeight(QFont.Weight.Normal) funcFmt.setFontWeight(QFont.Weight.Normal)
self._rules.append((r"\b(?:DATE|TIME)\b", funcFmt)) for fn in ["CURRENT_DATE", "CURRENT_TIME", "date_add", "time_add"]:
self._rules.append((r"\b" + fn + r"\b", funcFmt))
varFmt = QTextCharFormat() varFmt = QTextCharFormat()
varFmt.setForeground(QColor("#9CDCFE")) varFmt.setForeground(QColor("#9CDCFE"))
varFmt.setFontWeight(QFont.Weight.Normal) varFmt.setFontWeight(QFont.Weight.Normal)
@@ -96,15 +105,16 @@ class ALScriptHighlighter(QSyntaxHighlighter):
strFmt = QTextCharFormat() strFmt = QTextCharFormat()
strFmt.setForeground(QColor("#CE9178")) strFmt.setForeground(QColor("#CE9178"))
strFmt.setFontWeight(QFont.Weight.Normal) strFmt.setFontWeight(QFont.Weight.Normal)
self._rules.append((r'"[^"]*"', strFmt))
self._rules.append((r"'[^']*'", strFmt)) self._rules.append((r"'[^']*'", strFmt))
numFmt = QTextCharFormat() numFmt = QTextCharFormat()
numFmt.setForeground(QColor("#B5CEA8")) numFmt.setForeground(QColor("#B5CEA8"))
numFmt.setFontWeight(QFont.Weight.Normal) numFmt.setFontWeight(QFont.Weight.Normal)
self._rules.append((r"\b\d+\b", numFmt)) self._rules.append((r"\b\d+(?:\.\d+)?\b", numFmt))
commentFmt = QTextCharFormat() commentFmt = QTextCharFormat()
commentFmt.setForeground(QColor("#6A9955")) commentFmt.setForeground(QColor("#6A9955"))
commentFmt.setFontItalic(True) commentFmt.setFontItalic(True)
self._rules.append((r"//[^\n]*", commentFmt)) self._rules.append((r"--[^\n]*", commentFmt))
def highlightBlock( def highlightBlock(
@@ -257,15 +267,15 @@ class ALAutoScriptEditDialog(QDialog):
basicLayout.setContentsMargins(4, 4, 4, 4) basicLayout.setContentsMargins(4, 4, 4, 4)
basicLayout.setAlignment(Qt.AlignLeft | Qt.AlignTop) basicLayout.setAlignment(Qt.AlignLeft | Qt.AlignTop)
controlButtons = [ controlButtons = [
("IF", "IF()\n \nEND IF"), ("if", "if then\n \nend"),
("ELSE IF", "ELSE IF()\n "), ("elseif", "elseif then\n "),
("ELSE", "ELSE"), ("else", "else"),
("END IF", "END IF"), ("end", "end"),
("PASS", "PASS"), ("-- pass", "-- pass"),
] ]
self._addButtonsToGrid(basicLayout, controlButtons, 0, 0, 5) self._addButtonsToGrid(basicLayout, controlButtons, 0, 0, 5)
assignButtons = [ assignButtons = [
("SET", "SET = "), ("=", " = "),
] ]
self._addButtonsToGrid(basicLayout, assignButtons, 0, 5, 1) self._addButtonsToGrid(basicLayout, assignButtons, 0, 5, 1)
tabWidget.addTab(basicWidget, "基本语法") tabWidget.addTab(basicWidget, "基本语法")
@@ -275,22 +285,22 @@ class ALAutoScriptEditDialog(QDialog):
operatorLayout.setContentsMargins(4, 4, 4, 4) operatorLayout.setContentsMargins(4, 4, 4, 4)
operatorLayout.setAlignment(Qt.AlignLeft | Qt.AlignTop) operatorLayout.setAlignment(Qt.AlignLeft | Qt.AlignTop)
arithmeticButtons = [ arithmeticButtons = [
(".ADD.", ".ADD."), ("+", " + "),
(".SUB.", ".SUB."), ("-", " - "),
] ]
self._addButtonsToGrid(operatorLayout, arithmeticButtons, 0, 0, 2) self._addButtonsToGrid(operatorLayout, arithmeticButtons, 0, 0, 2)
compareButtons = [ compareButtons = [
(".EQ.", ".EQ."), ("==", " == "),
(".NEQ.", ".NEQ."), ("~=", " ~= "),
(".BGT.", ".BGT."), (">", " > "),
(".BLT.", ".BLT."), ("<", " < "),
(".BGE.", ".BGE."), (">=", " >= "),
(".BLE.", ".BLE."), ("<=", " <= "),
] ]
self._addButtonsToGrid(operatorLayout, compareButtons, 1, 0, 6) self._addButtonsToGrid(operatorLayout, compareButtons, 1, 0, 6)
logic_buttons = [ logic_buttons = [
(".AND.", ".AND."), ("and", " and "),
(".OR.", ".OR."), ("or", " or "),
] ]
self._addButtonsToGrid(operatorLayout, logic_buttons, 2, 0, 2) self._addButtonsToGrid(operatorLayout, logic_buttons, 2, 0, 2)
tabWidget.addTab(operatorWidget, "运算符") tabWidget.addTab(operatorWidget, "运算符")
@@ -300,19 +310,19 @@ class ALAutoScriptEditDialog(QDialog):
literalLayout.setContentsMargins(4, 4, 4, 4) literalLayout.setContentsMargins(4, 4, 4, 4)
literalLayout.setAlignment(Qt.AlignLeft | Qt.AlignTop) literalLayout.setAlignment(Qt.AlignLeft | Qt.AlignTop)
bool_buttons = [ bool_buttons = [
(".TRUE.", ".TRUE."), ("true", "true"),
(".FALSE.", ".FALSE."), ("false", "false"),
] ]
self._addButtonsToGrid(literalLayout, bool_buttons, 0, 0, 2) self._addButtonsToGrid(literalLayout, bool_buttons, 0, 0, 2)
dateTimeButtons = [ dateTimeButtons = [
("DATE()", "DATE(2025-01-01)"), ("日期", '"2099-01-01"'),
("TIME()", "TIME(00:00)"), ("时间", '"00:00"'),
] ]
self._addButtonsToGrid(literalLayout, dateTimeButtons, 1, 0, 2) self._addButtonsToGrid(literalLayout, dateTimeButtons, 1, 0, 2)
hintButtons = [ hintButtons = [
("字符串", "'请输入文本'"), ("字符串", '"请输入文本"'),
("数字", "123"), ("数字", "123"),
("注释", "// 请输入注释"), ("注释", "-- 请输入注释"),
] ]
self._addButtonsToGrid(literalLayout, hintButtons, 2, 0, 3) self._addButtonsToGrid(literalLayout, hintButtons, 2, 0, 3)
tabWidget.addTab(literalWidget, "字面量") tabWidget.addTab(literalWidget, "字面量")
+18 -10
View File
@@ -1,5 +1,14 @@
# -*- coding: utf-8 -*-
""" """
Conditional block widget for the AutoScript orchestration dialog. 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.
"""
"""
Conditional block widget for the AutoScript orchestration dialog.
""" """
from PySide6.QtCore import Slot from PySide6.QtCore import Slot
from PySide6.QtWidgets import ( from PySide6.QtWidgets import (
@@ -52,7 +61,6 @@ class ConditionalBlock(QGroupBox):
mainLayout = QVBoxLayout(self) mainLayout = QVBoxLayout(self)
mainLayout.setSpacing(6) mainLayout.setSpacing(6)
mainLayout.setContentsMargins(8, 8, 8, 8) mainLayout.setContentsMargins(8, 8, 8, 8)
headerLayout = QHBoxLayout() headerLayout = QHBoxLayout()
headerLayout.setSpacing(8) headerLayout.setSpacing(8)
self.typeCombo = QComboBox(self) self.typeCombo = QComboBox(self)
@@ -70,7 +78,6 @@ class ConditionalBlock(QGroupBox):
self.deleteBlockBtn.setFixedHeight(25) self.deleteBlockBtn.setFixedHeight(25)
headerLayout.addWidget(self.deleteBlockBtn) headerLayout.addWidget(self.deleteBlockBtn)
mainLayout.addLayout(headerLayout) mainLayout.addLayout(headerLayout)
self.conditionWidget = QWidget(self) self.conditionWidget = QWidget(self)
self.conditionWidget.setSizePolicy( self.conditionWidget.setSizePolicy(
QSizePolicy.Preferred, QSizePolicy.Preferred QSizePolicy.Preferred, QSizePolicy.Preferred
@@ -78,7 +85,6 @@ class ConditionalBlock(QGroupBox):
condLayout = QVBoxLayout(self.conditionWidget) condLayout = QVBoxLayout(self.conditionWidget)
condLayout.setContentsMargins(4, 4, 4, 4) condLayout.setContentsMargins(4, 4, 4, 4)
condLayout.setSpacing(6) condLayout.setSpacing(6)
self.condRowsLayout = QVBoxLayout() self.condRowsLayout = QVBoxLayout()
self.condRowsLayout.setSpacing(4) self.condRowsLayout.setSpacing(4)
condLayout.addLayout(self.condRowsLayout) condLayout.addLayout(self.condRowsLayout)
@@ -209,16 +215,18 @@ class ConditionalBlock(QGroupBox):
def toScriptLines( def toScriptLines(
self self
) -> list: ) -> list:
"""
Generate Lua script lines for this conditional block.
"""
blockType = self.getBlockType() blockType = self.getBlockType()
lines = [] lines = []
if blockType in ("IF", "ELSE IF"): if blockType in ("IF", "ELSE IF"):
condTexts = [ condTexts = [
r.toConditionText() for r in self._conditionRows if r.toConditionText() r.toConditionText() for r in self._conditionRows if r.toConditionText()
] ]
if not condTexts: if not condTexts:
condTexts = [".TRUE."] condTexts = ["true"]
if len(condTexts) == 1: if len(condTexts) == 1:
combined = condTexts[0] combined = condTexts[0]
@@ -226,16 +234,16 @@ class ConditionalBlock(QGroupBox):
parts = [] parts = []
for i, ct in enumerate(condTexts): for i, ct in enumerate(condTexts):
if i > 0: if i > 0:
logic = self._conditionRows[i].getLogic() or ".AND." logic = self._conditionRows[i].getLogic() or "and"
parts.append(f" {logic} ") parts.append(f" {logic} ")
parts.append(f"({ct})") parts.append(f"({ct})")
combined = "".join(parts) combined = "".join(parts)
if blockType == "IF": if blockType == "IF":
lines.append(f"IF({combined}) THEN") lines.append(f"if {combined} then")
else: else:
lines.append(f"ELSE IF({combined}) THEN") lines.append(f"elseif {combined} then")
else: else:
lines.append("ELSE") lines.append("else")
for step in self._actionWidgets: for step in self._actionWidgets:
scriptLine = step.toScriptLine() scriptLine = step.toScriptLine()
if scriptLine: if scriptLine:
+15 -3
View File
@@ -1,5 +1,14 @@
# -*- coding: utf-8 -*-
""" """
Orchestration dialog for visually composing AutoScript scripts. 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.
"""
"""
Orchestration dialog for visually composing AutoScript scripts.
""" """
from PySide6.QtCore import Slot from PySide6.QtCore import Slot
from PySide6.QtWidgets import ( from PySide6.QtWidgets import (
@@ -132,18 +141,21 @@ class ALAutoScriptOrchDialog(QDialog):
def getScript( def getScript(
self self
) -> str: ) -> str:
"""
Generate the complete Lua script from all blocks.
"""
parts = [] parts = []
prevType = None prevType = None
for block in self._blocks: for block in self._blocks:
blockType = block.getBlockType() blockType = block.getBlockType()
if blockType == "IF" and prevType is not None: if blockType == "IF" and prevType is not None:
parts.append("END IF") parts.append("end")
lines = block.toScriptLines() lines = block.toScriptLines()
parts.extend(lines) parts.extend(lines)
prevType = blockType prevType = blockType
if self._blocks and self._blocks[0].getBlockType() == "IF": if self._blocks and self._blocks[0].getBlockType() == "IF":
parts.append("END IF") parts.append("end")
return "\n".join(parts) return "\n".join(parts)
@Slot() @Slot()
+183 -143
View File
@@ -1,5 +1,14 @@
# -*- coding: utf-8 -*-
""" """
Helper utilities and constants for the AutoScript orchestration dialog. 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.
"""
"""
Helper utilities and constants for the AutoScript orchestration dialog.
""" """
import re import re
@@ -24,14 +33,10 @@ from PySide6.QtWidgets import (
from autoscript import ( from autoscript import (
ALL_VARIABLES, ALL_VARIABLES,
splitTopLevel
)
from autoscript.ASOperator import (
ARITH_TYPES,
COMPARISON_OPERATORS
) )
# Types that support arithmetic operations (add/sub)
ARITH_TYPES = {"Date", "Time", "Int", "Float"}
VAR_TYPE_ORDER = [ VAR_TYPE_ORDER = [
"String", "String",
"Int", "Int",
@@ -51,22 +56,22 @@ PRESET_VARIABLES = [
PRESET_NAMES = { PRESET_NAMES = {
p["name"] for p in PRESET_VARIABLES p["name"] for p in PRESET_VARIABLES
} }
# Operator display names (UI-specific), symbols derived from engine # Operator display names (UI-specific), using Lua operator symbols
_COMPARE_DISPLAY_MAP = { _COMPARE_DISPLAY_MAP = {
".EQ.": "等于", "==": "等于",
".NEQ.": "不等于", "~=": "不等于",
".BGT.": "大于", ">": "大于",
".BLT.": "小于", "<": "小于",
".BGE.": "大于等于", ">=": "大于等于",
".BLE.": "小于等于", "<=": "小于等于",
} }
COMPARE_OPERATORS = sorted( COMPARE_OPERATORS = sorted(
[(name, op) for op, name in _COMPARE_DISPLAY_MAP.items() if op in COMPARISON_OPERATORS], [(name, op) for op, name in _COMPARE_DISPLAY_MAP.items()],
key=lambda x: len(x[1]), reverse=True key=lambda x: len(x[1]), reverse=True
) )
LOGIC_OPERATORS = [ LOGIC_OPERATORS = [
("并且 (.AND.)", ".AND."), ("并且 (and)", "and"),
("或者 (.OR.)", ".OR."), ("或者 (or)", "or"),
] ]
ACTION_TYPES = [ ACTION_TYPES = [
("设置为", "set"), ("设置为", "set"),
@@ -182,8 +187,8 @@ def makeValueWidget(
return w return w
if var_type == "Boolean": if var_type == "Boolean":
w = QComboBox(parent) w = QComboBox(parent)
w.addItem("是 (.TRUE.)", ".TRUE.") w.addItem("是 (true)", "true")
w.addItem("否 (.FALSE.)", ".FALSE.") w.addItem("否 (false)", "false")
w.setFixedHeight(25) w.setFixedHeight(25)
w.setMinimumWidth(100) w.setMinimumWidth(100)
return w return w
@@ -304,8 +309,8 @@ class _DateInputContainer(QWidget):
layout.addWidget(self._stack) layout.addWidget(self._stack)
layout.addStretch() layout.addStretch()
_RE_CURRENT_DATE_OFFSET = re.compile( _RE_DATE_ADD_CURRENT = re.compile(
r'^CURRENT_DATE\s*([+-])\s*(\d+)$', re.IGNORECASE r'^date_add\(CURRENT_DATE\(\),\s*(-?\d+)\)$', re.IGNORECASE
) )
def getValue( def getValue(
@@ -326,27 +331,24 @@ class _DateInputContainer(QWidget):
expr: str expr: str
): ):
s = expr.strip().upper() s = expr.strip()
_RELATIVE_MAP = { up = s.upper()
"CURRENT_DATE": 2, "TODAY": 2, if up == "CURRENT_DATE()":
"CURRENT_DATE + 1": 3, "TOMORROW": 3,
"CURRENT_DATE + 2": 4,
"CURRENT_DATE - 1": 1,
"CURRENT_DATE - 2": 0,
}
idx = _RELATIVE_MAP.get(s)
if idx is not None:
self._modeCombo.setCurrentIndex(0) self._modeCombo.setCurrentIndex(0)
self._relCombo.setCurrentIndex(idx) self._relCombo.setCurrentIndex(2)
elif self._RE_CURRENT_DATE_OFFSET.match(s): return
m = self._RE_CURRENT_DATE_OFFSET.match(s) m_add = self._RE_DATE_ADD_CURRENT.match(up)
sign = m.group(1) if m_add:
n = int(m.group(2)) n = int(m_add.group(1))
offset = n if sign == "+" else -n _OFFSET_IDX = {-2: 0, -1: 1, 0: 2, 1: 3, 2: 4}
label = f"{n}天后" if offset >= 0 else f"{n}天前" idx = _OFFSET_IDX.get(n)
raw = f"CURRENT_DATE {'+' if sign == '+' else '-'} {n}" if idx is not None:
self._modeCombo.setCurrentIndex(0)
self._relCombo.setCurrentIndex(idx)
return
label = f"{n}天后" if n >= 0 else f"{-n}天前"
raw = f"CURRENT_DATE {'+' if n >= 0 else '-'} {abs(n)}"
self._modeCombo.setCurrentIndex(0) self._modeCombo.setCurrentIndex(0)
# Add dynamic item if not already present
for ci in range(self._relCombo.count()): for ci in range(self._relCombo.count()):
if ci in self._dynamicItems and self._dynamicItems[ci] == raw: if ci in self._dynamicItems and self._dynamicItems[ci] == raw:
self._relCombo.setCurrentIndex(ci) self._relCombo.setCurrentIndex(ci)
@@ -355,12 +357,21 @@ class _DateInputContainer(QWidget):
self._relCombo.addItem(label) self._relCombo.addItem(label)
self._dynamicItems[idx] = raw self._dynamicItems[idx] = raw
self._relCombo.setCurrentIndex(idx) self._relCombo.setCurrentIndex(idx)
elif s.startswith("DATE("): return
m_date_ctor = re.match(r"^DATE\((\d+),\s*(\d+),\s*(\d+)\)$", up)
if m_date_ctor:
self._modeCombo.setCurrentIndex(1) self._modeCombo.setCurrentIndex(1)
m = re.match(r"DATE\((\d{4}-\d{2}-\d{2})\)", s) self._dateEdit.setDate(QDate(
if m: int(m_date_ctor.group(1)),
parts = m.group(1).split("-") int(m_date_ctor.group(2)),
self._dateEdit.setDate(QDate(int(parts[0]), int(parts[1]), int(parts[2]))) int(m_date_ctor.group(3)),
))
return
m_date = re.match(r'^"(\d{4}-\d{2}-\d{2})"$', s)
if m_date:
self._modeCombo.setCurrentIndex(1)
parts = m_date.group(1).split("-")
self._dateEdit.setDate(QDate(int(parts[0]), int(parts[1]), int(parts[2])))
class _TimeInputContainer(QWidget): class _TimeInputContainer(QWidget):
@@ -392,8 +403,16 @@ class _TimeInputContainer(QWidget):
expr: str expr: str
): ):
s = expr.strip().upper() s = expr.strip()
m = re.match(r"TIME\((\d{1,2}:\d{2})\)", s) up = s.upper()
m_time_ctor = re.match(r"^TIME\((\d+),\s*(\d+)\)$", up)
if m_time_ctor:
self._timeEdit.setTime(QTime(
int(m_time_ctor.group(1)),
int(m_time_ctor.group(2)),
))
return
m = re.match(r'^"(\d{1,2}:\d{2})"$', s)
if m: if m:
parts = m.group(1).split(":") parts = m.group(1).split(":")
self._timeEdit.setTime(QTime(int(parts[0]), int(parts[1]))) self._timeEdit.setTime(QTime(int(parts[0]), int(parts[1])))
@@ -541,24 +560,45 @@ def setWidgetValue(
var_type: str, var_type: str,
expr: str expr: str
): ):
"""
Set a widget's value from a Lua script expression.
"""
if hasattr(w, "setValue"): if hasattr(w, "setValue"):
w.setValue(expr) w.setValue(expr)
return return
s = expr.strip().upper() s = expr.strip()
up = s.upper()
if isinstance(w, QTimeEdit): if isinstance(w, QTimeEdit):
m = re.match(r"TIME\((\d{1,2}:\d{2})\)", s) m_time_ctor = re.match(r"^TIME\((\d+),\s*(\d+)\)$", up)
if m: if m_time_ctor:
parts = m.group(1).split(":") w.setTime(QTime(int(m_time_ctor.group(1)), int(m_time_ctor.group(2))))
w.setTime(QTime(int(parts[0]), int(parts[1]))) else:
m = re.match(r'^"(\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): elif isinstance(w, QDateEdit):
m = re.match(r"DATE\((\d{4}-\d{2}-\d{2})\)", s) m_date_ctor = re.match(r"^DATE\((\d+),\s*(\d+),\s*(\d+)\)$", up)
if m: if m_date_ctor:
parts = m.group(1).split("-") w.setDate(QDate(
w.setDate(QDate(int(parts[0]), int(parts[1]), int(parts[2]))) int(m_date_ctor.group(1)),
int(m_date_ctor.group(2)),
int(m_date_ctor.group(3)),
))
else:
m = re.match(r'^"(\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): elif isinstance(w, QComboBox):
for i in range(w.count()): for i in range(w.count()):
if w.itemData(i) == s or w.itemText(i).upper() == s: d = w.itemData(i)
if d is not None:
if str(d).upper() == up:
w.setCurrentIndex(i)
return
if w.itemText(i).upper() == up:
w.setCurrentIndex(i) w.setCurrentIndex(i)
return return
elif isinstance(w, QSpinBox): elif isinstance(w, QSpinBox):
@@ -573,9 +613,8 @@ def setWidgetValue(
pass pass
elif isinstance(w, QLineEdit): elif isinstance(w, QLineEdit):
inner = expr.strip() inner = expr.strip()
if (inner.startswith("'") and inner.endswith("'")) or \ if inner.startswith('"') and inner.endswith('"'):
(inner.startswith('"') and inner.endswith('"')): inner = inner[1:-1].replace('\\"', '"')
inner = inner[1:-1].replace("''", "'")
w.setText(inner) w.setText(inner)
@@ -583,39 +622,86 @@ def encodeValueStr(
raw_value: str, raw_value: str,
var_type: str var_type: str
) -> str: ) -> str:
"""
Encode a raw widget value as a Lua expression.
if isArithExpr(raw_value): Arithmetic expressions (A + B) are passed through for numeric types;
return raw_value Date/Time arithmetic is translated to ``date_add()`` / ``time_add()`` calls.
if var_type == "Time": """
if raw_value.startswith("+") or raw_value.startswith("-"):
return raw_value if var_type in ("Date", "Time"):
if raw_value.upper().startswith("TIME("): return _encodeDateOrTime(str(raw_value), var_type)
return raw_value if isinstance(raw_value, bool):
return f"TIME({raw_value})" return "true" if raw_value else "false"
if var_type == "Date": s = str(raw_value)
if raw_value.upper().startswith("DATE("): if isArithExpr(s):
return raw_value return s
if raw_value.upper().startswith("CURRENT_DATE"):
return raw_value
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": if var_type == "Boolean":
up = raw_value.upper().strip() up = s.upper().strip()
if up in (".TRUE.", ".FALSE."): if up in ("TRUE", "FALSE"):
return up return up.lower()
return ".TRUE." if raw_value else ".FALSE." return "true" if raw_value else "false"
if var_type == "String": if var_type == "String":
escaped = raw_value.replace("'", "''") escaped = s.replace("\\", "\\\\").replace('"', '\\"')
return f"'{escaped}'" return f'"{escaped}"'
return raw_value return s
def _encodeDateOrTime(
raw_value: str,
var_type: str
) -> str:
"""
Translate a date/time widget value into a Lua expression.
"""
s = raw_value.strip()
up = s.upper()
m_arith_spaced = re.match(r'^(.+?)\s+([+-])\s+(.+)$', s)
m_arith_nospace = re.match(r'^([A-Za-z_]\w*)([+-])(\d+|[A-Za-z_]\w*)$', s)
m_arith = m_arith_spaced or m_arith_nospace
if m_arith:
left = m_arith.group(1).strip().upper()
sign = m_arith.group(2)
right = m_arith.group(3).strip()
operand = right if sign == "+" else f"-{right}"
if left == "CURRENT_DATE":
return f"date_add(CURRENT_DATE(), {operand})"
if left == "CURRENT_TIME":
return f"time_add(CURRENT_TIME(), {operand})"
if var_type == "Date":
return f"date_add({left}, {operand})"
if var_type == "Time":
return f"time_add({left}, {operand})"
return f"{left} {sign} {right}"
if up == "CURRENT_DATE":
return "CURRENT_DATE()"
if up == "CURRENT_TIME":
return "CURRENT_TIME()"
_REL_MAP = {
"前天": "date_add(CURRENT_DATE(), -2)",
"昨天": "date_add(CURRENT_DATE(), -1)",
"今天": "CURRENT_DATE()",
"明天": "date_add(CURRENT_DATE(), 1)",
"后天": "date_add(CURRENT_DATE(), 2)",
}
if s in _REL_MAP:
return _REL_MAP[s]
if var_type == "Date":
m_date = re.match(r"^(\d{4})-(\d{2})-(\d{2})$", s)
if m_date:
y, m, d = int(m_date.group(1)), int(m_date.group(2)), int(m_date.group(3))
return f"date({y}, {m}, {d})"
if var_type == "Time":
m_time = re.match(r"^(\d{1,2}):(\d{2})$", s)
if m_time:
h, m = int(m_time.group(1)), int(m_time.group(2))
return f"time({h}, {m})"
if re.match(r"^[+-]?\d+$", s):
return s
if re.match(r"^[A-Za-z_]\w*$", s):
return s
return f'"{s}"'
def stripOuterParens( def stripOuterParens(
@@ -637,8 +723,6 @@ def stripOuterParens(
# Pre-compiled patterns for detecting arithmetic expressions (A + B / A - B) # Pre-compiled patterns for detecting arithmetic expressions (A + B / A - B)
# Must match both spaced form (CURRENT_DATE + 1) and no-space form (RESERVE_DATE+1),
# consistent with ASEngine._resolveArithExpr.
_RE_ARITH_SPACED = re.compile(r'^(.+?)\s+([+-])\s+(.+)$') _RE_ARITH_SPACED = re.compile(r'^(.+?)\s+([+-])\s+(.+)$')
_RE_ARITH_NOSPACE = re.compile(r'^([A-Za-z_]\w*)([+-])(\d+|[A-Za-z_]\w*)$') _RE_ARITH_NOSPACE = re.compile(r'^([A-Za-z_]\w*)([+-])(\d+|[A-Za-z_]\w*)$')
@@ -654,65 +738,21 @@ def isArithExpr(
return bool(_RE_ARITH_SPACED.match(s) or _RE_ARITH_NOSPACE.match(s)) return bool(_RE_ARITH_SPACED.match(s) or _RE_ARITH_NOSPACE.match(s))
# Pre-compiled patterns for SET value whitelist validation,
# matching the priority order of ASEngine._resolveValue.
_RE_VAL_TIME = re.compile(r'^TIME\(\d{1,2}:\d{2}\)$', re.IGNORECASE)
_RE_VAL_DATE = re.compile(r'^DATE\(\d{4}-\d{2}-\d{2}\)$', re.IGNORECASE)
_RE_VAL_ARITH_SPACED = _RE_ARITH_SPACED
_RE_VAL_ARITH_NOSPACE = _RE_ARITH_NOSPACE
_RE_VAL_VAR_REF = re.compile(r'^[A-Z_][A-Z0-9_]*$', re.IGNORECASE)
def _isValidSetValue(
value: str
) -> bool:
"""
Whitelist validation: return True if value matches one of the
legal SET-value patterns recognised by ASEngine._resolveValue.
Order matches _resolveValue priority: TIME → DATE → bool →
quoted string → int → float → arith expr → variable reference.
"""
s = value.strip()
if not s:
return False
if _RE_VAL_TIME.match(s):
return True
if _RE_VAL_DATE.match(s):
return True
if s.upper() in (".TRUE.", ".FALSE."):
return True
if (s.startswith("'") and s.endswith("'")) or (s.startswith('"') and s.endswith('"')):
return True
try:
int(s)
return True
except ValueError:
pass
try:
float(s)
return True
except ValueError:
pass
if _RE_VAL_ARITH_SPACED.match(s) or _RE_VAL_ARITH_NOSPACE.match(s):
return True
if _RE_VAL_VAR_REF.match(s):
return True
return False
def isVarReference( def isVarReference(
expr: str expr: str
) -> bool: ) -> bool:
"""
Return True if *expr* looks like a variable name reference
(as opposed to a literal value or function call).
"""
s = expr.strip() s = expr.strip()
up = s.upper() up = s.upper()
if up in (".TRUE.", ".FALSE."): if up in ("TRUE", "FALSE"):
return False return False
if re.match(r"^TIME\(|^DATE\(|^CURRENT_", up): if re.match(r"^DATE\(|^TIME\(|^DATE_ADD\(|^TIME_ADD\(|^CURRENT_DATE\(|^CURRENT_TIME\(|^CURRENT_", up):
return False return False
if up.startswith("'") or up.startswith('"'): if up.startswith('"') or up.startswith("'"):
return False return False
if re.match(r"^[+-]?\d", s): if re.match(r"^[+-]?\d", s):
return False return False
+32 -19
View File
@@ -1,5 +1,14 @@
# -*- coding: utf-8 -*-
""" """
Widget components for the AutoScript orchestration dialog. 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.
"""
"""
Widget components for the AutoScript orchestration dialog.
""" """
from PySide6.QtCore import Slot from PySide6.QtCore import Slot
from PySide6.QtWidgets import ( from PySide6.QtWidgets import (
@@ -21,7 +30,6 @@ from gui.ALAutoScriptOrchDialog._helpers import (
VAR_TYPE_ORDER, VAR_TYPE_ORDER,
encodeValueStr, encodeValueStr,
getValueFromWidget, getValueFromWidget,
isArithExpr,
makeComboWidget, makeComboWidget,
makeLabel, makeLabel,
makeOffsetWidget, makeOffsetWidget,
@@ -119,8 +127,8 @@ class ConditionRowFrame(QFrame):
self._varMgr.populateCombo(self.leftVarCombo) self._varMgr.populateCombo(self.leftVarCombo)
# Append boolean literal sentinels at the end # Append boolean literal sentinels at the end
self.leftVarCombo.insertSeparator(self.leftVarCombo.count()) self.leftVarCombo.insertSeparator(self.leftVarCombo.count())
self.leftVarCombo.addItem(".TRUE.", (".TRUE.", "Boolean")) self.leftVarCombo.addItem("true", ("true", "Boolean"))
self.leftVarCombo.addItem(".FALSE.", (".FALSE.", "Boolean")) self.leftVarCombo.addItem("false", ("false", "Boolean"))
if wasBool and boolName: if wasBool and boolName:
for ci in range(self.leftVarCombo.count()): for ci in range(self.leftVarCombo.count()):
d = self.leftVarCombo.itemData(ci) d = self.leftVarCombo.itemData(ci)
@@ -156,7 +164,7 @@ class ConditionRowFrame(QFrame):
if not data: if not data:
return return
name, vartype = data name, vartype = data
isBool = name in (".TRUE.", ".FALSE.") isBool = name in ("true", "false")
self._isBoolMode = isBool self._isBoolMode = isBool
self.opCombo.setVisible(not isBool) self.opCombo.setVisible(not isBool)
self._compTypeCombo.setVisible(not isBool) self._compTypeCombo.setVisible(not isBool)
@@ -204,6 +212,9 @@ class ConditionRowFrame(QFrame):
if not data: if not data:
return "" return ""
name, vartype = data name, vartype = data
# CURRENT_DATE / CURRENT_TIME are Lua functions — call them, not reference them
if name in ("CURRENT_DATE", "CURRENT_TIME"):
name = f"{name}()"
opSym = self.opCombo.currentData() opSym = self.opCombo.currentData()
if self._rawRhsExpr: if self._rawRhsExpr:
return f"{name} {opSym} {self._rawRhsExpr}" return f"{name} {opSym} {self._rawRhsExpr}"
@@ -212,6 +223,8 @@ class ConditionRowFrame(QFrame):
rd = self.rhsVarCombo.currentData() rd = self.rhsVarCombo.currentData()
if rd: if rd:
rhsName = rd[0] rhsName = rd[0]
if rhsName in ("CURRENT_DATE", "CURRENT_TIME"):
rhsName = f"{rhsName}()"
return f"{name} {opSym} {rhsName}" return f"{name} {opSym} {rhsName}"
rhsText = self.rhsVarCombo.currentText().strip() rhsText = self.rhsVarCombo.currentText().strip()
if rhsText: if rhsText:
@@ -399,38 +412,37 @@ class ActionStepFrame(QFrame):
def toScriptLine( def toScriptLine(
self self
) -> str: ) -> str:
"""
Generate a single line of Lua script from the current widget state.
"""
target = self.getTargetName() target = self.getTargetName()
op = self.opTypeCombo.currentData() op = self.opTypeCombo.currentData()
if op == "pass": if op == "pass":
return " PASS" return " -- pass"
if not target: if not target:
return "" return ""
rawVal = self._getValueRaw() rawVal = self._getValueRaw()
vartype = self._currentTargetType
if op == "set": if op == "set":
vartype = self._currentTargetType
if isArithExpr(rawVal):
return f" SET {target} = {rawVal}"
encoded = encodeValueStr(rawVal, vartype) encoded = encodeValueStr(rawVal, vartype)
return f" SET {target} = {encoded}" return f" {target} = {encoded}"
elif op == "add": elif op == "add":
vartype = self._currentTargetType
if vartype == "Date" and hasattr(self.valueStack.currentWidget(), "getOffsetDays"): if vartype == "Date" and hasattr(self.valueStack.currentWidget(), "getOffsetDays"):
days = self.valueStack.currentWidget().getOffsetDays() days = self.valueStack.currentWidget().getOffsetDays()
return f" {target} .ADD. {days}" return f" {target} = date_add({target}, {days})"
if vartype == "Time" and hasattr(self.valueStack.currentWidget(), "getOffsetHours"): if vartype == "Time" and hasattr(self.valueStack.currentWidget(), "getOffsetHours"):
hours = self.valueStack.currentWidget().getOffsetHours() hours = self.valueStack.currentWidget().getOffsetHours()
return f" {target} .ADD. {hours}" return f" {target} = time_add({target}, {hours})"
return f" {target} .ADD. {rawVal}" return f" {target} = {target} + {rawVal}"
elif op == "sub": elif op == "sub":
vartype = self._currentTargetType
if vartype == "Date" and hasattr(self.valueStack.currentWidget(), "getOffsetDays"): if vartype == "Date" and hasattr(self.valueStack.currentWidget(), "getOffsetDays"):
days = self.valueStack.currentWidget().getOffsetDays() days = self.valueStack.currentWidget().getOffsetDays()
return f" {target} .SUB. {days}" return f" {target} = date_add({target}, -{days})"
if vartype == "Time" and hasattr(self.valueStack.currentWidget(), "getOffsetHours"): if vartype == "Time" and hasattr(self.valueStack.currentWidget(), "getOffsetHours"):
hours = self.valueStack.currentWidget().getOffsetHours() hours = self.valueStack.currentWidget().getOffsetHours()
return f" {target} .SUB. {hours}" return f" {target} = time_add({target}, -{hours})"
return f" {target} .SUB. {rawVal}" return f" {target} = {target} - {rawVal}"
return "" return ""
@@ -439,7 +451,8 @@ class ActionStepFrame(QFrame):
) -> str: ) -> str:
if self.valueSrcCombo.currentData() == "variable": if self.valueSrcCombo.currentData() == "variable":
return self.existingVarCombo.currentText().strip() data = self.existingVarCombo.currentData()
return data[0] if data else ""
w = self.valueStack.currentWidget() w = self.valueStack.currentWidget()
if w: if w:
return getValueFromWidget(w) return getValueFromWidget(w)