diff --git a/src/gui/ALAutoScriptOrchDialog/_dialog.py b/src/gui/ALAutoScriptOrchDialog/_dialog.py index 3d3d751..8cecfed 100644 --- a/src/gui/ALAutoScriptOrchDialog/_dialog.py +++ b/src/gui/ALAutoScriptOrchDialog/_dialog.py @@ -13,27 +13,15 @@ from PySide6.QtWidgets import ( 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._helpers import VariableManager from gui.ALAutoScriptOrchDialog._blocks import ConditionalBlock -from gui.ALAutoScriptOrchDialog._widgets import ConditionRowFrame class ALAutoScriptOrchDialog(QDialog): def __init__( self, - parent = None, - existingScript: str = "" + parent = None ): super().__init__(parent) @@ -42,10 +30,7 @@ class ALAutoScriptOrchDialog(QDialog): self.setupUi() self.connectSignals() - if existingScript and existingScript.strip(): - self.loadFromScript(existingScript) - else: - self.addBlock() + self.addBlock() self.scrollLayout.addStretch() @@ -171,126 +156,3 @@ class ALAutoScriptOrchDialog(QDialog): 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 index dc42004..4cdef03 100644 --- a/src/gui/ALAutoScriptOrchDialog/_helpers.py +++ b/src/gui/ALAutoScriptOrchDialog/_helpers.py @@ -271,6 +271,7 @@ class _DateInputContainer(QWidget): ): super().__init__(parent) + self._dynamicItems = {} # index -> raw expression, for one-way parsed items self.setupUi() @@ -303,6 +304,9 @@ class _DateInputContainer(QWidget): layout.addWidget(self._stack) layout.addStretch() + _RE_CURRENT_DATE_OFFSET = re.compile( + r'^CURRENT_DATE\s*([+-])\s*(\d+)$', re.IGNORECASE + ) def getValue( self @@ -310,6 +314,9 @@ class _DateInputContainer(QWidget): mode = self._modeCombo.currentData() if mode == "relative": + idx = self._relCombo.currentIndex() + if idx in self._dynamicItems: + return self._dynamicItems[idx] return self._relCombo.currentText() return self._dateEdit.date().toString("yyyy-MM-dd") @@ -331,6 +338,23 @@ class _DateInputContainer(QWidget): if idx is not None: 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._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) + return + idx = self._relCombo.count() + self._relCombo.addItem(label) + self._dynamicItems[idx] = raw + self._relCombo.setCurrentIndex(idx) elif s.startswith("DATE("): self._modeCombo.setCurrentIndex(1) m = re.match(r"DATE\((\d{4}-\d{2}-\d{2})\)", s) @@ -565,13 +589,14 @@ def encodeValueStr( 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) + 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", @@ -611,8 +636,11 @@ def stripOuterParens( return s -# Pre-compiled pattern for detecting arithmetic expressions like "A + B" / "C - D" -_RE_ARITH_EXPR = re.compile(r'^.+?\s+[+-]\s+.+$') +# 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*)$') def isArithExpr( @@ -622,7 +650,56 @@ def isArithExpr( Return True if expr looks like a two-operand arithmetic expression (A ± B). """ - return bool(_RE_ARITH_EXPR.match(expr.strip())) + s = expr.strip() + 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( diff --git a/src/gui/ALAutoScriptOrchDialog/_orchestrate.py b/src/gui/ALAutoScriptOrchDialog/_orchestrate.py deleted file mode 100644 index 5bbe941..0000000 --- a/src/gui/ALAutoScriptOrchDialog/_orchestrate.py +++ /dev/null @@ -1,109 +0,0 @@ -""" -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: - prefixed = value if value.startswith("-") else f"+{value}" - self._current_actions.append((target, prefixed, "add")) - else: - prefixed = value if value.startswith("-") else f"-{value}" - self._current_actions.append((target, prefixed, "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 deleted file mode 100644 index 25d424b..0000000 --- a/src/gui/ALAutoScriptOrchDialog/_precheck.py +++ /dev/null @@ -1,163 +0,0 @@ -""" -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 index 1d2fa3d..ecc9f92 100644 --- a/src/gui/ALAutoScriptOrchDialog/_widgets.py +++ b/src/gui/ALAutoScriptOrchDialog/_widgets.py @@ -1,8 +1,6 @@ """ Widget components for the AutoScript orchestration dialog. """ -import re - from PySide6.QtCore import Slot from PySide6.QtWidgets import ( QComboBox, @@ -24,13 +22,11 @@ from gui.ALAutoScriptOrchDialog._helpers import ( encodeValueStr, getValueFromWidget, isArithExpr, - isVarReference, makeComboWidget, makeLabel, makeOffsetWidget, makeValueWidget, makeVarRefCombo, - setWidgetValue, ) @@ -114,7 +110,23 @@ class ConditionRowFrame(QFrame): self ): + wasBool = self._isBoolMode + boolName = None + if wasBool: + data = self.leftVarCombo.currentData() + if data: + boolName = data[0] 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")) + if wasBool and boolName: + for ci in range(self.leftVarCombo.count()): + d = self.leftVarCombo.itemData(ci) + if d and d[0] == boolName: + self.leftVarCombo.setCurrentIndex(ci) + break def populateRhsVarCombo( @@ -143,8 +155,14 @@ class ConditionRowFrame(QFrame): data = self.leftVarCombo.itemData(idx) if not data: return - _, vartype = data - self.updateRhsLiteralWidget(vartype) + name, vartype = data + isBool = name in (".TRUE.", ".FALSE.") + self._isBoolMode = isBool + self.opCombo.setVisible(not isBool) + self._compTypeCombo.setVisible(not isBool) + self.rhsStack.setVisible(not isBool) + if not isBool: + self.updateRhsLiteralWidget(vartype) def updateRhsLiteralWidget( @@ -181,6 +199,8 @@ class ConditionRowFrame(QFrame): ) -> str: data = self.leftVarCombo.currentData() + if self._isBoolMode and data: + return data[0] if not data: return "" name, vartype = data @@ -205,95 +225,6 @@ class ConditionRowFrame(QFrame): return "" - def loadFromParts( - self, - operandName: str, - opSym: str, - valueExpr: str - ): - - self._rawRhsExpr = "" - self.leftVarCombo.blockSignals(True) - self.opCombo.blockSignals(True) - self._compTypeCombo.blockSignals(True) - try: - 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 - finally: - self.leftVarCombo.blockSignals(False) - self.opCombo.blockSignals(False) - self._compTypeCombo.blockSignals(False) - data = self.leftVarCombo.currentData() - vartype = data[1] if data else "String" - self.updateRhsLiteralWidget(vartype) - if not valueExpr: - return - up = valueExpr.strip().upper() - 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) - elif isArithExpr(valueExpr): - self._tryLoadCondArithExpr(valueExpr, vartype) - else: - self._compTypeCombo.setCurrentIndex(0) - w = self.literalWidgets.get(vartype) - if w: - setWidgetValue(w, vartype, valueExpr) - - def _tryLoadCondArithExpr( - self, - expr: str, - vartype: str - ): - """Try to decompose a condition RHS arithmetic expression into UI state.""" - - s = expr.strip() - m = re.match(r'^(.+?)\s+([+-])\s+(.+)$', s) - if not m: - self._rawRhsExpr = s - return - left = m.group(1).strip() - op = m.group(2).strip() - right = m.group(3).strip() - left_up = left.upper() - - if vartype == "Date" and left_up == "CURRENT_DATE": - try: - n = int(right) - offset = n if op == "+" else -n - if offset in (-2, -1, 0, 1, 2): - self._compTypeCombo.setCurrentIndex(0) - w = self.literalWidgets.get("Date") - if w and hasattr(w, "setValue"): - w.setValue(s) - return - except ValueError: - pass - self._rawRhsExpr = s - - - def _isKnownVar( - self, - name: str - ) -> bool: - - return self._varMgr.getInfoByName(name) is not None - - def refreshVarCombos( self ): @@ -301,7 +232,6 @@ class ConditionRowFrame(QFrame): self.populateLeftVarCombo() self.populateRhsVarCombo() - class ActionStepFrame(QFrame): def __init__( @@ -471,9 +401,11 @@ class ActionStepFrame(QFrame): ) -> str: target = self.getTargetName() + op = self.opTypeCombo.currentData() + if op == "pass": + return " PASS" if not target: return "" - op = self.opTypeCombo.currentData() rawVal = self._getValueRaw() if op == "set": vartype = self._currentTargetType @@ -514,132 +446,6 @@ class ActionStepFrame(QFrame): 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 - op = self.opTypeCombo.currentData() - if op in ("add", "sub") and s.startswith("-"): - s = s[1:] - self.opTypeCombo.setCurrentIndex( - 2 if op == "add" else 1 - ) - 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) - elif isArithExpr(s): - self._tryLoadArithExpr(s) - else: - self.valueSrcCombo.setCurrentIndex(0) - w = self.valueStack.currentWidget() - if w: - setWidgetValue(w, self._currentTargetType, expr) - - def _tryLoadArithExpr( - self, - expr: str - ): - """Try to decompose an arithmetic expression into UI state.""" - - s = expr.strip() - m = re.match(r'^(.+?)\s+([+-])\s+(.+)$', s) - if not m: - self._storeAsCustomExpr(s) - return - left = m.group(1).strip() - op = m.group(2).strip() - right = m.group(3).strip() - left_up = left.upper() - - # CURRENT_DATE ± N for Date targets — try relative combo for ±0/1/2, - # otherwise store as custom expression to preserve relative semantics - if self._currentTargetType == "Date" and left_up == "CURRENT_DATE": - try: - n = int(right) - offset = n if op == "+" else -n - if offset in (-2, -1, 0, 1, 2): - w = self._literalWidgets.get("Date") - if w and hasattr(w, "setValue"): - w.setValue(s) - self.valueSrcCombo.setCurrentIndex(0) - return - except ValueError: - pass - self._storeAsCustomExpr(s) - return - - # CURRENT_TIME ± N for Time targets — map to add/sub with offset - if self._currentTargetType == "Time" and left_up == "CURRENT_TIME": - try: - hours = int(right) - if op == "-": - hours = -hours - self.opTypeCombo.setCurrentIndex( - 1 if hours >= 0 else 2 - ) - self.valueSrcCombo.setCurrentIndex(0) - w = self._offsetWidgets.get("Time") - if w and hasattr(w, "setValue"): - w.setValue(str(abs(hours))) - return - except ValueError: - pass - - self._storeAsCustomExpr(s) - - def _storeAsCustomExpr( - self, - expr: str - ): - """Store a raw expression in the variable combo when it can't be decomposed.""" - - self.valueSrcCombo.setCurrentIndex(1) - self._varMgr.populateCombo(self.existingVarCombo) - found = self._varMgr.findExactNameEntry(self.existingVarCombo, expr) - if found < 0: - self.existingVarCombo.addItem(expr, (expr, self._currentTargetType)) - self.existingVarCombo.setCurrentIndex(self.existingVarCombo.count() - 1) - else: - self.existingVarCombo.setCurrentIndex(found) - - def refreshVarCombos( self ):