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):
"""
Syntax highlighter for Lua-based AutoScript.
"""
def __init__(
self,
@@ -67,26 +70,32 @@ class ALScriptHighlighter(QSyntaxHighlighter):
keywordFmt = QTextCharFormat()
keywordFmt.setForeground(QColor("#569CD6"))
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("#C586C0"))
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))
for kw in [
"if", "elseif", "else", "end", "then",
"and", "or", "not",
"local", "function", "return", "nil",
]:
self._rules.append((r"\b" + kw + r"\b", keywordFmt))
boolFmt = QTextCharFormat()
boolFmt.setForeground(QColor("#4FC1FF"))
boolFmt.setFontWeight(QFont.Weight.Bold)
self._rules.append((r"\.TRUE\.", boolFmt))
self._rules.append((r"\.FALSE\.", boolFmt))
self._rules.append((r"\btrue\b", 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.setForeground(QColor("#DCDCAA"))
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.setForeground(QColor("#9CDCFE"))
varFmt.setFontWeight(QFont.Weight.Normal)
@@ -96,15 +105,16 @@ class ALScriptHighlighter(QSyntaxHighlighter):
strFmt = QTextCharFormat()
strFmt.setForeground(QColor("#CE9178"))
strFmt.setFontWeight(QFont.Weight.Normal)
self._rules.append((r'"[^"]*"', strFmt))
self._rules.append((r"'[^']*'", strFmt))
numFmt = QTextCharFormat()
numFmt.setForeground(QColor("#B5CEA8"))
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.setForeground(QColor("#6A9955"))
commentFmt.setFontItalic(True)
self._rules.append((r"//[^\n]*", commentFmt))
self._rules.append((r"--[^\n]*", commentFmt))
def highlightBlock(
@@ -257,15 +267,15 @@ class ALAutoScriptEditDialog(QDialog):
basicLayout.setContentsMargins(4, 4, 4, 4)
basicLayout.setAlignment(Qt.AlignLeft | Qt.AlignTop)
controlButtons = [
("IF", "IF()\n \nEND IF"),
("ELSE IF", "ELSE IF()\n "),
("ELSE", "ELSE"),
("END IF", "END IF"),
("PASS", "PASS"),
("if", "if then\n \nend"),
("elseif", "elseif then\n "),
("else", "else"),
("end", "end"),
("-- pass", "-- pass"),
]
self._addButtonsToGrid(basicLayout, controlButtons, 0, 0, 5)
assignButtons = [
("SET", "SET = "),
("=", " = "),
]
self._addButtonsToGrid(basicLayout, assignButtons, 0, 5, 1)
tabWidget.addTab(basicWidget, "基本语法")
@@ -275,22 +285,22 @@ class ALAutoScriptEditDialog(QDialog):
operatorLayout.setContentsMargins(4, 4, 4, 4)
operatorLayout.setAlignment(Qt.AlignLeft | Qt.AlignTop)
arithmeticButtons = [
(".ADD.", ".ADD."),
(".SUB.", ".SUB."),
("+", " + "),
("-", " - "),
]
self._addButtonsToGrid(operatorLayout, arithmeticButtons, 0, 0, 2)
compareButtons = [
(".EQ.", ".EQ."),
(".NEQ.", ".NEQ."),
(".BGT.", ".BGT."),
(".BLT.", ".BLT."),
(".BGE.", ".BGE."),
(".BLE.", ".BLE."),
("==", " == "),
("~=", " ~= "),
(">", " > "),
("<", " < "),
(">=", " >= "),
("<=", " <= "),
]
self._addButtonsToGrid(operatorLayout, compareButtons, 1, 0, 6)
logic_buttons = [
(".AND.", ".AND."),
(".OR.", ".OR."),
("and", " and "),
("or", " or "),
]
self._addButtonsToGrid(operatorLayout, logic_buttons, 2, 0, 2)
tabWidget.addTab(operatorWidget, "运算符")
@@ -300,19 +310,19 @@ class ALAutoScriptEditDialog(QDialog):
literalLayout.setContentsMargins(4, 4, 4, 4)
literalLayout.setAlignment(Qt.AlignLeft | Qt.AlignTop)
bool_buttons = [
(".TRUE.", ".TRUE."),
(".FALSE.", ".FALSE."),
("true", "true"),
("false", "false"),
]
self._addButtonsToGrid(literalLayout, bool_buttons, 0, 0, 2)
dateTimeButtons = [
("DATE()", "DATE(2025-01-01)"),
("TIME()", "TIME(00:00)"),
("日期", '"2099-01-01"'),
("时间", '"00:00"'),
]
self._addButtonsToGrid(literalLayout, dateTimeButtons, 1, 0, 2)
hintButtons = [
("字符串", "'请输入文本'"),
("字符串", '"请输入文本"'),
("数字", "123"),
("注释", "// 请输入注释"),
("注释", "-- 请输入注释"),
]
self._addButtonsToGrid(literalLayout, hintButtons, 2, 0, 3)
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.QtWidgets import (
@@ -52,7 +61,6 @@ class ConditionalBlock(QGroupBox):
mainLayout = QVBoxLayout(self)
mainLayout.setSpacing(6)
mainLayout.setContentsMargins(8, 8, 8, 8)
headerLayout = QHBoxLayout()
headerLayout.setSpacing(8)
self.typeCombo = QComboBox(self)
@@ -70,7 +78,6 @@ class ConditionalBlock(QGroupBox):
self.deleteBlockBtn.setFixedHeight(25)
headerLayout.addWidget(self.deleteBlockBtn)
mainLayout.addLayout(headerLayout)
self.conditionWidget = QWidget(self)
self.conditionWidget.setSizePolicy(
QSizePolicy.Preferred, QSizePolicy.Preferred
@@ -78,7 +85,6 @@ class ConditionalBlock(QGroupBox):
condLayout = QVBoxLayout(self.conditionWidget)
condLayout.setContentsMargins(4, 4, 4, 4)
condLayout.setSpacing(6)
self.condRowsLayout = QVBoxLayout()
self.condRowsLayout.setSpacing(4)
condLayout.addLayout(self.condRowsLayout)
@@ -209,16 +215,18 @@ class ConditionalBlock(QGroupBox):
def toScriptLines(
self
) -> list:
"""
Generate Lua script lines for this conditional block.
"""
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."]
condTexts = ["true"]
if len(condTexts) == 1:
combined = condTexts[0]
@@ -226,16 +234,16 @@ class ConditionalBlock(QGroupBox):
parts = []
for i, ct in enumerate(condTexts):
if i > 0:
logic = self._conditionRows[i].getLogic() or ".AND."
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")
lines.append(f"if {combined} then")
else:
lines.append(f"ELSE IF({combined}) THEN")
lines.append(f"elseif {combined} then")
else:
lines.append("ELSE")
lines.append("else")
for step in self._actionWidgets:
scriptLine = step.toScriptLine()
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.QtWidgets import (
@@ -132,18 +141,21 @@ class ALAutoScriptOrchDialog(QDialog):
def getScript(
self
) -> str:
"""
Generate the complete Lua script from all blocks.
"""
parts = []
prevType = None
for block in self._blocks:
blockType = block.getBlockType()
if blockType == "IF" and prevType is not None:
parts.append("END IF")
parts.append("end")
lines = block.toScriptLines()
parts.extend(lines)
prevType = blockType
if self._blocks and self._blocks[0].getBlockType() == "IF":
parts.append("END IF")
parts.append("end")
return "\n".join(parts)
@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
@@ -24,14 +33,10 @@ from PySide6.QtWidgets import (
from autoscript import (
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 = [
"String",
"Int",
@@ -51,22 +56,22 @@ PRESET_VARIABLES = [
PRESET_NAMES = {
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 = {
".EQ.": "等于",
".NEQ.": "不等于",
".BGT.": "大于",
".BLT.": "小于",
".BGE.": "大于等于",
".BLE.": "小于等于",
"==": "等于",
"~=": "不等于",
">": "大于",
"<": "小于",
">=": "大于等于",
"<=": "小于等于",
}
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
)
LOGIC_OPERATORS = [
("并且 (.AND.)", ".AND."),
("或者 (.OR.)", ".OR."),
("并且 (and)", "and"),
("或者 (or)", "or"),
]
ACTION_TYPES = [
("设置为", "set"),
@@ -182,8 +187,8 @@ def makeValueWidget(
return w
if var_type == "Boolean":
w = QComboBox(parent)
w.addItem("是 (.TRUE.)", ".TRUE.")
w.addItem("否 (.FALSE.)", ".FALSE.")
w.addItem("是 (true)", "true")
w.addItem("否 (false)", "false")
w.setFixedHeight(25)
w.setMinimumWidth(100)
return w
@@ -304,8 +309,8 @@ class _DateInputContainer(QWidget):
layout.addWidget(self._stack)
layout.addStretch()
_RE_CURRENT_DATE_OFFSET = re.compile(
r'^CURRENT_DATE\s*([+-])\s*(\d+)$', re.IGNORECASE
_RE_DATE_ADD_CURRENT = re.compile(
r'^date_add\(CURRENT_DATE\(\),\s*(-?\d+)\)$', re.IGNORECASE
)
def getValue(
@@ -326,27 +331,24 @@ class _DateInputContainer(QWidget):
expr: str
):
s = expr.strip().upper()
_RELATIVE_MAP = {
"CURRENT_DATE": 2, "TODAY": 2,
"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:
s = expr.strip()
up = s.upper()
if up == "CURRENT_DATE()":
self._modeCombo.setCurrentIndex(0)
self._relCombo.setCurrentIndex(idx)
elif self._RE_CURRENT_DATE_OFFSET.match(s):
m = self._RE_CURRENT_DATE_OFFSET.match(s)
sign = m.group(1)
n = int(m.group(2))
offset = n if sign == "+" else -n
label = f"{n}天后" if offset >= 0 else f"{n}天前"
raw = f"CURRENT_DATE {'+' if sign == '+' else '-'} {n}"
self._relCombo.setCurrentIndex(2)
return
m_add = self._RE_DATE_ADD_CURRENT.match(up)
if m_add:
n = int(m_add.group(1))
_OFFSET_IDX = {-2: 0, -1: 1, 0: 2, 1: 3, 2: 4}
idx = _OFFSET_IDX.get(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)
# Add dynamic item if not already present
for ci in range(self._relCombo.count()):
if ci in self._dynamicItems and self._dynamicItems[ci] == raw:
self._relCombo.setCurrentIndex(ci)
@@ -355,12 +357,21 @@ class _DateInputContainer(QWidget):
self._relCombo.addItem(label)
self._dynamicItems[idx] = raw
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)
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])))
self._dateEdit.setDate(QDate(
int(m_date_ctor.group(1)),
int(m_date_ctor.group(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):
@@ -392,8 +403,16 @@ class _TimeInputContainer(QWidget):
expr: str
):
s = expr.strip().upper()
m = re.match(r"TIME\((\d{1,2}:\d{2})\)", s)
s = expr.strip()
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:
parts = m.group(1).split(":")
self._timeEdit.setTime(QTime(int(parts[0]), int(parts[1])))
@@ -541,24 +560,45 @@ def setWidgetValue(
var_type: str,
expr: str
):
"""
Set a widget's value from a Lua script expression.
"""
if hasattr(w, "setValue"):
w.setValue(expr)
return
s = expr.strip().upper()
s = expr.strip()
up = s.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])))
m_time_ctor = re.match(r"^TIME\((\d+),\s*(\d+)\)$", up)
if m_time_ctor:
w.setTime(QTime(int(m_time_ctor.group(1)), int(m_time_ctor.group(2))))
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):
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])))
m_date_ctor = re.match(r"^DATE\((\d+),\s*(\d+),\s*(\d+)\)$", up)
if m_date_ctor:
w.setDate(QDate(
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):
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)
return
elif isinstance(w, QSpinBox):
@@ -573,9 +613,8 @@ def setWidgetValue(
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("''", "'")
if inner.startswith('"') and inner.endswith('"'):
inner = inner[1:-1].replace('\\"', '"')
w.setText(inner)
@@ -583,39 +622,86 @@ def encodeValueStr(
raw_value: str,
var_type: str
) -> str:
"""
Encode a raw widget value as a Lua expression.
if isArithExpr(raw_value):
return raw_value
if var_type == "Time":
if raw_value.startswith("+") or raw_value.startswith("-"):
return raw_value
if raw_value.upper().startswith("TIME("):
return raw_value
return f"TIME({raw_value})"
if var_type == "Date":
if raw_value.upper().startswith("DATE("):
return raw_value
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})"
Arithmetic expressions (A + B) are passed through for numeric types;
Date/Time arithmetic is translated to ``date_add()`` / ``time_add()`` calls.
"""
if var_type in ("Date", "Time"):
return _encodeDateOrTime(str(raw_value), var_type)
if isinstance(raw_value, bool):
return "true" if raw_value else "false"
s = str(raw_value)
if isArithExpr(s):
return s
if var_type == "Boolean":
up = raw_value.upper().strip()
if up in (".TRUE.", ".FALSE."):
return up
return ".TRUE." if raw_value else ".FALSE."
up = s.upper().strip()
if up in ("TRUE", "FALSE"):
return up.lower()
return "true" if raw_value else "false"
if var_type == "String":
escaped = raw_value.replace("'", "''")
return f"'{escaped}'"
return raw_value
escaped = s.replace("\\", "\\\\").replace('"', '\\"')
return f'"{escaped}"'
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(
@@ -637,8 +723,6 @@ def stripOuterParens(
# 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_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))
# 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(
expr: str
) -> bool:
"""
Return True if *expr* looks like a variable name reference
(as opposed to a literal value or function call).
"""
s = expr.strip()
up = s.upper()
if up in (".TRUE.", ".FALSE."):
if up in ("TRUE", "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
if up.startswith("'") or up.startswith('"'):
if up.startswith('"') or up.startswith("'"):
return False
if re.match(r"^[+-]?\d", s):
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.QtWidgets import (
@@ -21,7 +30,6 @@ from gui.ALAutoScriptOrchDialog._helpers import (
VAR_TYPE_ORDER,
encodeValueStr,
getValueFromWidget,
isArithExpr,
makeComboWidget,
makeLabel,
makeOffsetWidget,
@@ -119,8 +127,8 @@ class ConditionRowFrame(QFrame):
self._varMgr.populateCombo(self.leftVarCombo)
# Append boolean literal sentinels at the end
self.leftVarCombo.insertSeparator(self.leftVarCombo.count())
self.leftVarCombo.addItem(".TRUE.", (".TRUE.", "Boolean"))
self.leftVarCombo.addItem(".FALSE.", (".FALSE.", "Boolean"))
self.leftVarCombo.addItem("true", ("true", "Boolean"))
self.leftVarCombo.addItem("false", ("false", "Boolean"))
if wasBool and boolName:
for ci in range(self.leftVarCombo.count()):
d = self.leftVarCombo.itemData(ci)
@@ -156,7 +164,7 @@ class ConditionRowFrame(QFrame):
if not data:
return
name, vartype = data
isBool = name in (".TRUE.", ".FALSE.")
isBool = name in ("true", "false")
self._isBoolMode = isBool
self.opCombo.setVisible(not isBool)
self._compTypeCombo.setVisible(not isBool)
@@ -204,6 +212,9 @@ class ConditionRowFrame(QFrame):
if not data:
return ""
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()
if self._rawRhsExpr:
return f"{name} {opSym} {self._rawRhsExpr}"
@@ -212,6 +223,8 @@ class ConditionRowFrame(QFrame):
rd = self.rhsVarCombo.currentData()
if rd:
rhsName = rd[0]
if rhsName in ("CURRENT_DATE", "CURRENT_TIME"):
rhsName = f"{rhsName}()"
return f"{name} {opSym} {rhsName}"
rhsText = self.rhsVarCombo.currentText().strip()
if rhsText:
@@ -399,38 +412,37 @@ class ActionStepFrame(QFrame):
def toScriptLine(
self
) -> str:
"""
Generate a single line of Lua script from the current widget state.
"""
target = self.getTargetName()
op = self.opTypeCombo.currentData()
if op == "pass":
return " PASS"
return " -- pass"
if not target:
return ""
rawVal = self._getValueRaw()
vartype = self._currentTargetType
if op == "set":
vartype = self._currentTargetType
if isArithExpr(rawVal):
return f" SET {target} = {rawVal}"
encoded = encodeValueStr(rawVal, vartype)
return f" SET {target} = {encoded}"
return f" {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}"
return f" {target} = date_add({target}, {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}"
return f" {target} = time_add({target}, {hours})"
return f" {target} = {target} + {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}"
return f" {target} = date_add({target}, -{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 f" {target} = time_add({target}, -{hours})"
return f" {target} = {target} - {rawVal}"
return ""
@@ -439,7 +451,8 @@ class ActionStepFrame(QFrame):
) -> str:
if self.valueSrcCombo.currentData() == "variable":
return self.existingVarCombo.currentText().strip()
data = self.existingVarCombo.currentData()
return data[0] if data else ""
w = self.valueStack.currentWidget()
if w:
return getValueFromWidget(w)