1
1
mirror of https://github.com/KenanZhu/AutoLibrary.git synced 2026-06-18 23:43:02 +08:00

refactor(gui): 编排窗口迁移至新包并移除旧的预览/编排对话框

This commit is contained in:
2026-05-18 11:15:35 +08:00
parent 33c0f4414c
commit 6cf182c8c8
12 changed files with 2468 additions and 1122 deletions
@@ -0,0 +1,3 @@
from gui.ALAutoScriptOrchDialog._dialog import ALAutoScriptOrchDialog
__all__ = ["ALAutoScriptOrchDialog"]
+272
View File
@@ -0,0 +1,272 @@
"""
Conditional block widget for the AutoScript orchestration dialog.
"""
from PySide6.QtCore import Slot
from PySide6.QtWidgets import (
QComboBox,
QGroupBox,
QHBoxLayout,
QLabel,
QPushButton,
QSizePolicy,
QVBoxLayout,
QWidget,
)
from gui.ALAutoScriptOrchDialog._widgets import (
ActionStepFrame,
ConditionRowFrame,
)
class ConditionalBlock(QGroupBox):
def __init__(
self,
blockIndex: int,
varMgr = None,
parent = None
):
super().__init__(parent)
self.blockIndex = blockIndex
self._varMgr = varMgr
self._actionWidgets = []
self._conditionRows = []
self.setupUi()
self.connectSignals()
self.addInitialConditionRow()
def setupUi(
self
):
self.setUpdatesEnabled(False)
self.setStyleSheet(
"QGroupBox { font-weight: bold; border: 1px solid #ccc; "
"margin-top: 5px; padding-top: 5px; }"
)
self.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed)
mainLayout = QVBoxLayout(self)
mainLayout.setSpacing(6)
mainLayout.setContentsMargins(8, 8, 8, 8)
headerLayout = QHBoxLayout()
headerLayout.setSpacing(8)
self.typeCombo = QComboBox(self)
self.typeCombo.addItem("IF", "IF")
self.typeCombo.addItem("ELSE IF", "ELSE IF")
self.typeCombo.addItem("ELSE", "ELSE")
self.typeCombo.setFixedHeight(25)
if self.blockIndex == 0:
self.typeCombo.setEnabled(False)
headerLayout.addWidget(QLabel("类型:", self))
headerLayout.addWidget(self.typeCombo)
headerLayout.addStretch()
self.deleteBlockBtn = QPushButton("删除此块", self)
self.deleteBlockBtn.setStyleSheet("color: red;")
self.deleteBlockBtn.setFixedHeight(25)
headerLayout.addWidget(self.deleteBlockBtn)
mainLayout.addLayout(headerLayout)
self.conditionWidget = QWidget(self)
self.conditionWidget.setSizePolicy(
QSizePolicy.Preferred, QSizePolicy.Preferred
)
condLayout = QVBoxLayout(self.conditionWidget)
condLayout.setContentsMargins(4, 4, 4, 4)
condLayout.setSpacing(6)
self._condRowsLayout = QVBoxLayout()
self._condRowsLayout.setSpacing(4)
condLayout.addLayout(self._condRowsLayout)
self.addCondBtn = QPushButton("+ 添加条件", self.conditionWidget)
self.addCondBtn.setFixedHeight(25)
condLayout.addWidget(self.addCondBtn)
mainLayout.addWidget(self.conditionWidget)
self.actionLabel = QLabel("执行步骤:", self)
self.actionLabel.setFixedHeight(25)
mainLayout.addWidget(self.actionLabel)
self._actionsLayout = QVBoxLayout()
self._actionsLayout.setSpacing(4)
mainLayout.addLayout(self._actionsLayout)
self.addActionBtn = QPushButton("+ 添加执行步骤", self)
self.addActionBtn.setFixedHeight(25)
mainLayout.addWidget(self.addActionBtn)
self.setUpdatesEnabled(True)
def connectSignals(
self
):
self.typeCombo.currentIndexChanged.connect(self.onTypeChanged)
self.addCondBtn.clicked.connect(self.addConditionRow)
self.addActionBtn.clicked.connect(self.addActionStep)
def addInitialConditionRow(
self
):
row = ConditionRowFrame(
self._varMgr, self.blockIndex,
isFirst=True, parent=self
)
self._conditionRows.append(row)
self._condRowsLayout.addWidget(row)
def addConditionRow(
self
):
row = ConditionRowFrame(
self._varMgr, self.blockIndex,
isFirst=False, parent=self
)
row.deleteBtn.clicked.connect(lambda: self.removeConditionRow(row))
self._conditionRows.append(row)
self._condRowsLayout.addWidget(row)
def removeConditionRow(
self,
row: ConditionRowFrame
):
if row in self._conditionRows and len(self._conditionRows) > 1:
self._conditionRows.remove(row)
self._condRowsLayout.removeWidget(row)
row.hide()
row.deleteLater()
def addActionStep(
self
):
step = ActionStepFrame(self._varMgr, self.blockIndex, parent=self)
step.deleteBtn.clicked.connect(lambda: self.removeActionStep(step))
self._actionWidgets.append(step)
self._actionsLayout.addWidget(step)
def removeActionStep(
self,
step: ActionStepFrame
):
if step in self._actionWidgets:
self._actionWidgets.remove(step)
self._actionsLayout.removeWidget(step)
step.hide()
step.deleteLater()
@Slot(int)
def onTypeChanged(
self,
_idx
):
isCond = self.typeCombo.currentData() in ("IF", "ELSE IF")
self.conditionWidget.setVisible(isCond)
self.actionLabel.setText(
"执行步骤:" if isCond else "ELSE 执行步骤:"
)
def getBlockType(
self
) -> str:
return self.typeCombo.currentData()
def getConditionRows(
self
):
return list(self._conditionRows)
def getActionSteps(
self
):
return list(self._actionWidgets)
def countActionSteps(
self
) -> int:
return len(self._actionWidgets)
def toScriptLines(
self
) -> list:
blockType = self.getBlockType()
lines = []
if blockType in ("IF", "ELSE IF"):
condTexts = [
r.toConditionText() for r in self._conditionRows if r.toConditionText()
]
if not condTexts:
condTexts = [".TRUE."]
if len(condTexts) == 1:
combined = condTexts[0]
else:
parts = []
for i, ct in enumerate(condTexts):
if i > 0:
logic = self._conditionRows[i].getLogic() or ".AND."
parts.append(f" {logic} ")
parts.append(f"({ct})")
combined = "".join(parts)
if blockType == "IF":
lines.append(f"IF({combined}) THEN")
else:
lines.append(f"ELSE IF({combined}) THEN")
else:
lines.append("ELSE")
for step in self._actionWidgets:
scriptLine = step.toScriptLine()
if scriptLine:
lines.append(scriptLine)
return lines
def refreshVarCombos(
self
):
for row in self._conditionRows:
row.refreshVarCombos()
for step in self._actionWidgets:
step.refreshVarCombos()
def setPrevBlockType(
self,
prevType: str | None
):
model = self.typeCombo.model()
if model is None:
return
for data in ("ELSE IF", "ELSE"):
idx = self.typeCombo.findData(data)
if idx < 0:
continue
item = model.item(idx)
shouldEnable = prevType != "ELSE"
item.setEnabled(shouldEnable)
if prevType == "ELSE" and self.typeCombo.currentData() in ("ELSE IF", "ELSE"):
self.typeCombo.setCurrentIndex(0)
+296
View File
@@ -0,0 +1,296 @@
"""
Orchestration dialog for visually composing AutoScript scripts.
"""
from PySide6.QtCore import Slot
from PySide6.QtWidgets import (
QDialog,
QDialogButtonBox,
QFrame,
QMessageBox,
QPushButton,
QScrollArea,
QVBoxLayout,
QWidget,
)
from gui.ALAutoScriptOrchDialog._precheck import precheck
from gui.ALAutoScriptOrchDialog._orchestrate import parseBlocks
from gui.ALAutoScriptOrchDialog._helpers import (
COMPARE_OPERATORS,
PRESET_NAMES,
VariableManager,
findOperatorIn,
splitTopLevel,
stripOuterParens,
)
from gui.ALAutoScriptOrchDialog._blocks import ConditionalBlock
from gui.ALAutoScriptOrchDialog._widgets import ConditionRowFrame
class ALAutoScriptOrchDialog(QDialog):
def __init__(
self,
parent = None,
existingScript: str = ""
):
super().__init__(parent)
self._blocks = []
self._varMgr = VariableManager(self)
self.setupUi()
self.connectSignals()
if existingScript and existingScript.strip():
self.loadFromScript(existingScript)
else:
self.addBlock()
self._scrollLayout.addStretch()
def setupUi(
self
):
self.setWindowTitle("AutoScript 指令编排 - AutoLibrary")
self.setMinimumSize(640, 600)
self.setModal(True)
mainLayout = QVBoxLayout(self)
scroll = QScrollArea()
scroll.setWidgetResizable(True)
scroll.setFrameShape(QFrame.NoFrame)
scrollContent = QWidget()
self._scrollLayout = QVBoxLayout(scrollContent)
self._scrollLayout.setSpacing(5)
scroll.setWidget(scrollContent)
mainLayout.addWidget(scroll)
self.addBlockBtn = QPushButton("+ 添加判断块")
self.addBlockBtn.setFixedHeight(25)
mainLayout.addWidget(self.addBlockBtn)
self.btnBox = QDialogButtonBox(
QDialogButtonBox.StandardButton.Ok |
QDialogButtonBox.StandardButton.Cancel
)
self.btnBox.button(QDialogButtonBox.StandardButton.Ok).setText("确定")
self.btnBox.button(QDialogButtonBox.StandardButton.Cancel).setText("取消")
mainLayout.addWidget(self.btnBox)
def connectSignals(
self
):
self.btnBox.accepted.connect(self.onAccept)
self.btnBox.rejected.connect(self.reject)
self.addBlockBtn.clicked.connect(self.addBlock)
def _updateBlockTypeRestrictions(
self
):
prevType = None
for block in self._blocks:
block.setPrevBlockType(prevType)
prevType = block.getBlockType()
def addBlock(
self
):
block = ConditionalBlock(
len(self._blocks), self._varMgr, parent=self
)
block.deleteBlockBtn.clicked.connect(lambda: self.removeBlock(block))
block.typeCombo.currentIndexChanged.connect(self._updateBlockTypeRestrictions)
block.addActionStep()
self._blocks.append(block)
self._updateBlockTypeRestrictions()
if self._scrollLayout.count() > 0:
lastItem = self._scrollLayout.itemAt(
self._scrollLayout.count() - 1
)
if lastItem and lastItem.spacerItem():
self._scrollLayout.insertWidget(
self._scrollLayout.count() - 1, block
)
return
self._scrollLayout.addWidget(block)
def removeBlock(
self,
block: ConditionalBlock
):
if len(self._blocks) <= 1:
QMessageBox.information(self, "提示", "至少保留一个判断块。")
return
if block in self._blocks:
self._blocks.remove(block)
self._scrollLayout.removeWidget(block)
block.hide()
block.deleteLater()
for i, blk in enumerate(self._blocks):
blk.blockIndex = i
if i == 0:
blk.typeCombo.setEnabled(False)
blk.typeCombo.setCurrentIndex(0)
else:
blk.typeCombo.setEnabled(True)
blk.refreshVarCombos()
self._updateBlockTypeRestrictions()
def getScript(
self
) -> str:
parts = []
prevType = None
for block in self._blocks:
blockType = block.getBlockType()
if blockType == "IF" and prevType is not None:
parts.append("ENDIF")
lines = block.toScriptLines()
parts.extend(lines)
prevType = blockType
if self._blocks and self._blocks[0].getBlockType() == "IF":
parts.append("ENDIF")
return "\n".join(parts)
@Slot()
def onAccept(
self
):
script = self.getScript().strip()
if not script:
QMessageBox.warning(self, "提示", "脚本内容为空,请添加至少一个操作步骤。")
return
self.accept()
@staticmethod
def precheckScriptForOrchestration(
script: str
) -> tuple[bool, str]:
return precheck(script, allowed_vars=PRESET_NAMES)
def loadFromScript(
self,
script: str
):
if not script.strip():
self.addBlock()
return
ok, err = self.precheckScriptForOrchestration(script)
if not ok:
QMessageBox.warning(
self, "无法编排",
f"脚本检查失败:\n{err}\n\n"
"请通过\"编辑\"按钮打开脚本编辑窗口进行修改。"
)
self.addBlock()
return
# Structured block data via observer-based parsing — no duplicate logic
typeIdxMap = {"IF": 0, "ELSE IF": 1, "ELSE": 2}
parsedBlocks = parseBlocks(script)
self._blocks.clear()
while self._scrollLayout.count():
item = self._scrollLayout.takeAt(0)
if item.widget():
item.widget().deleteLater()
try:
for blockType, condition, actions in parsedBlocks:
self.addBlock()
block = self._blocks[-1]
idx = typeIdxMap.get(blockType, 0)
block.typeCombo.setCurrentIndex(idx)
block.onTypeChanged(idx)
for oldStep in list(block._actionWidgets):
block.removeActionStep(oldStep)
for target, valueExpr, opType in actions:
block.addActionStep()
step = block.getActionSteps()[-1]
step.setOpType(opType)
step.loadFromScript(target, valueExpr)
if blockType in ("IF", "ELSE IF") and condition:
self._parseConditions(block, condition)
except Exception:
self._blocks.clear()
while self._scrollLayout.count():
item = self._scrollLayout.takeAt(0)
if item.widget():
item.widget().deleteLater()
self._updateBlockTypeRestrictions()
if not self._blocks:
self.addBlock()
def _parseConditions(
self,
block: ConditionalBlock,
condStr: str
):
s = condStr.strip()
if not s:
return
s = stripOuterParens(s)
orParts = splitTopLevel(s, ".OR.")
allSubConds = []
allLogics = []
for pi, part in enumerate(orParts):
part = part.strip()
if pi > 0:
allLogics.append(".OR.")
andParts = splitTopLevel(part, ".AND.")
for ai, ap in enumerate(andParts):
ap = ap.strip()
if ai > 0:
allLogics.append(".AND.")
allSubConds.append(ap)
for row in list(block._conditionRows):
block._condRowsLayout.removeWidget(row)
row.hide()
row.deleteLater()
block._conditionRows.clear()
for i, subCond in enumerate(allSubConds):
subCond = subCond.strip()
subCond = stripOuterParens(subCond)
isFirst = (i == 0)
row = ConditionRowFrame(
self._varMgr, block.blockIndex,
isFirst=isFirst, parent=block
)
if not isFirst:
row.deleteBtn.clicked.connect(
lambda _checked=False, r=row: block.removeConditionRow(r)
)
if i - 1 < len(allLogics):
logic = allLogics[i - 1]
for li in range(row.logicCombo.count()):
if row.logicCombo.itemData(li) == logic:
row.logicCombo.setCurrentIndex(li)
break
block._conditionRows.append(row)
block._condRowsLayout.addWidget(row)
subUp = subCond.upper()
if subUp in (".TRUE.", ".FALSE."):
row.loadFromParts(subUp, "", "")
else:
opSyms = [op for _, op in COMPARE_OPERATORS]
result = findOperatorIn(subCond, opSyms)
if result:
idx, op = result
leftPart = subCond[:idx].strip()
rightPart = subCond[idx + len(op):].strip()
row.loadFromParts(leftPart, op, rightPart)
else:
row.loadFromParts(subCond, "", "")
if not block._conditionRows:
block.addInitialConditionRow()
+688
View File
@@ -0,0 +1,688 @@
"""
Helper utilities and constants for the AutoScript orchestration dialog.
"""
import re
from PySide6.QtCore import QObject, QDate, QTime
from PySide6.QtWidgets import (
QComboBox,
QDateEdit,
QDoubleSpinBox,
QHBoxLayout,
QLabel,
QLineEdit,
QSizePolicy,
QSpinBox,
QStackedWidget,
QTimeEdit,
QWidget,
)
from autoscript import ALL_VARIABLES
VAR_TYPE_ORDER = [
"String",
"Int",
"Float",
"Boolean",
"Date",
"Time"
]
PRESET_VARIABLES = [
{
"name": name.upper(),
"type": vtype,
"display": display
}
for display, (name, vtype) in ALL_VARIABLES.items()
]
PRESET_NAMES = {
p["name"] for p in PRESET_VARIABLES
}
COMPARE_OPERATORS = sorted([
("等于", ".EQ."),
("不等于", ".NEQ."),
("大于", ".BGT."),
("小于", ".BLT."),
("大于等于", ".BGE."),
("小于等于", ".BLE."),
], key=lambda x: len(x[1]), reverse=True)
LOGIC_OPERATORS = [
("并且 (.AND.)", ".AND."),
("或者 (.OR.)", ".OR."),
]
ACTION_TYPES = [
("设置为", "set"),
("增加", "add"),
("减少", "sub"),
]
ARITH_TYPES = {
"Date",
"Time",
"Int",
"Float"
}
DATE_RELATIVE_OPTIONS = [
("前天", "day_before_yesterday"),
("昨天", "yesterday"),
("今天", "today"),
("明天", "tomorrow"),
("后天", "day_after_tomorrow")
]
DATE_OFFSET_UNITS = [
("", "days"),
("", "weeks"),
("", "months"),
("", "years"),
]
class VariableManager(QObject):
def __init__(
self,
parent = None
):
super().__init__(parent)
self._vars = []
self._nameMap = {}
self._initPresetVars()
def _initPresetVars(
self
):
for p in PRESET_VARIABLES:
entry = {"name": p["name"], "type": p["type"], "display": p["display"]}
self._vars.append(entry)
self._nameMap[p["name"]] = entry
def getInfoByName(
self,
name: str
):
return self._nameMap.get(name.upper().strip())
def populateCombo(
self,
combo: QComboBox
):
currentData = combo.currentData()
combo.blockSignals(True)
combo.clear()
for entry in self._vars:
combo.addItem(
entry["display"],
(entry["name"], entry["type"])
)
if currentData:
for i in range(combo.count()):
d = combo.itemData(i)
if d and d[0] == currentData[0]:
combo.setCurrentIndex(i)
break
combo.blockSignals(False)
def findExactNameEntry(
self,
combo: QComboBox,
name: str
) -> int:
name = name.upper().strip()
for i in range(combo.count()):
d = combo.itemData(i)
if d and len(d) >= 1 and d[0].upper().strip() == name:
return i
return -1
def makeValueWidget(
var_type: str,
parent: QWidget = None
) -> QWidget:
if var_type == "Int":
w = QSpinBox(parent)
w.setRange(-999999, 999999)
w.setFixedHeight(25)
w.setMinimumWidth(100)
return w
if var_type == "Float":
w = QDoubleSpinBox(parent)
w.setRange(-999999.0, 999999.0)
w.setDecimals(2)
w.setFixedHeight(25)
w.setMinimumWidth(100)
return w
if var_type == "String":
w = QLineEdit(parent)
w.setPlaceholderText("输入值")
w.setFixedHeight(25)
w.setMinimumWidth(120)
return w
if var_type == "Boolean":
w = QComboBox(parent)
w.addItem("是 (.TRUE.)", ".TRUE.")
w.addItem("否 (.FALSE.)", ".FALSE.")
w.setFixedHeight(25)
w.setMinimumWidth(100)
return w
if var_type == "Date":
return _DateInputContainer(parent)
if var_type == "Time":
return _TimeInputContainer(parent)
w = QLineEdit(parent)
w.setPlaceholderText("输入值")
w.setFixedHeight(25)
w.setMinimumWidth(120)
return w
def makeOffsetWidget(
var_type: str,
parent: QWidget = None
) -> QWidget:
if var_type == "Int":
w = QSpinBox(parent)
w.setRange(-999999, 999999)
w.setFixedHeight(25)
w.setMinimumWidth(100)
return w
if var_type == "Float":
w = QDoubleSpinBox(parent)
w.setRange(-999999.0, 999999.0)
w.setDecimals(2)
w.setFixedHeight(25)
w.setMinimumWidth(100)
return w
if var_type == "Date":
return _DateOffsetContainer(parent)
if var_type == "Time":
return _TimeOffsetContainer(parent)
w = QLabel("(不支持该操作)", parent)
w.setFixedHeight(25)
return w
def makeVarRefCombo(
parent: QWidget = None
) -> QComboBox:
cb = QComboBox(parent)
cb.setFixedHeight(25)
cb.setMinimumWidth(120)
cb.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
return cb
def makeComboWidget(
items,
min_width: int = 80,
parent: QWidget = None
) -> QComboBox:
cb = QComboBox(parent)
for display, data in items:
cb.addItem(display, data)
cb.setFixedHeight(25)
cb.setMinimumWidth(min_width)
return cb
def makeLabel(
text: str,
parent: QWidget = None,
width: int = None
) -> QLabel:
lbl = QLabel(text, parent)
lbl.setFixedHeight(25)
if width:
lbl.setFixedWidth(width)
return lbl
class _DateInputContainer(QWidget):
def __init__(
self,
parent = None
):
super().__init__(parent)
self.setupUi()
def setupUi(
self
):
layout = QHBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(4)
self._modeCombo = QComboBox(self)
self._modeCombo.addItem("相对日期", "relative")
self._modeCombo.addItem("绝对日期", "absolute")
self._modeCombo.setFixedHeight(25)
self._stack = QStackedWidget(self)
self._relCombo = QComboBox(self)
for display, data in DATE_RELATIVE_OPTIONS:
self._relCombo.addItem(display, data)
self._relCombo.setFixedHeight(25)
self._stack.addWidget(self._relCombo)
self._dateEdit = QDateEdit(self)
self._dateEdit.setDisplayFormat("yyyy-MM-dd")
self._dateEdit.setCalendarPopup(True)
self._dateEdit.setFixedHeight(25)
self._stack.addWidget(self._dateEdit)
self._modeCombo.currentIndexChanged.connect(
lambda i: self._stack.setCurrentIndex(i)
)
layout.addWidget(self._modeCombo)
layout.addWidget(self._stack)
layout.addStretch()
def getValue(
self
) -> str:
mode = self._modeCombo.currentData()
if mode == "relative":
return self._relCombo.currentText()
return self._dateEdit.date().toString("yyyy-MM-dd")
def setValue(
self,
expr: str
):
s = expr.strip().upper()
if s == "CURRENT_DATE - 2":
self._modeCombo.setCurrentIndex(0)
self._relCombo.setCurrentIndex(4)
elif s == "CURRENT_DATE - 1":
self._modeCombo.setCurrentIndex(0)
self._relCombo.setCurrentIndex(3)
elif s in ("CURRENT_DATE", "TODAY"):
self._modeCombo.setCurrentIndex(0)
self._relCombo.setCurrentIndex(0)
elif s == "CURRENT_DATE + 1" or s == "TOMORROW":
self._modeCombo.setCurrentIndex(0)
self._relCombo.setCurrentIndex(1)
elif s == "CURRENT_DATE + 2":
self._modeCombo.setCurrentIndex(0)
self._relCombo.setCurrentIndex(2)
elif s.startswith("DATE("):
self._modeCombo.setCurrentIndex(1)
m = re.match(r"DATE\((\d{4}-\d{2}-\d{2})\)", s)
if m:
parts = m.group(1).split("-")
self._dateEdit.setDate(QDate(int(parts[0]), int(parts[1]), int(parts[2])))
class _TimeInputContainer(QWidget):
def __init__(
self,
parent = None
):
super().__init__(parent)
self._timeEdit = QTimeEdit(self)
self._timeEdit.setDisplayFormat("HH:mm")
self._timeEdit.setFixedHeight(25)
layout = QHBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
layout.addWidget(self._timeEdit)
def getValue(
self
) -> str:
return self._timeEdit.time().toString("HH:mm")
def setValue(
self,
expr: str
):
s = expr.strip().upper()
m = re.match(r"TIME\((\d{1,2}:\d{2})\)", s)
if m:
parts = m.group(1).split(":")
self._timeEdit.setTime(QTime(int(parts[0]), int(parts[1])))
class _DateOffsetContainer(QWidget):
def __init__(
self,
parent = None
):
super().__init__(parent)
self._spinBox = QSpinBox(self)
self._spinBox.setRange(0, 99999)
self._spinBox.setFixedHeight(25)
self._unitCombo = QComboBox(self)
for display, data in DATE_OFFSET_UNITS:
self._unitCombo.addItem(display, data)
self._unitCombo.setFixedHeight(25)
layout = QHBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(4)
layout.addWidget(self._spinBox)
layout.addWidget(self._unitCombo)
layout.addStretch()
def getValue(
self
) -> str:
return str(self.getOffsetDays())
def setValue(
self,
expr: str
):
s = expr.strip().lstrip("+")
try:
self._spinBox.setValue(int(s))
except ValueError:
pass
def getOffsetDays(
self
) -> int:
val = self._spinBox.value()
unit = self._unitCombo.currentData()
if unit == "weeks":
return val * 7
if unit == "months":
return val * 30
if unit == "years":
return val * 365
return val
def getRawValue(
self
) -> str:
return str(self._spinBox.value())
class _TimeOffsetContainer(QWidget):
def __init__(
self,
parent = None
):
super().__init__(parent)
self._spinBox = QSpinBox(self)
self._spinBox.setRange(0, 99999)
self._spinBox.setSuffix(" 小时")
self._spinBox.setFixedHeight(25)
layout = QHBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
layout.addWidget(self._spinBox)
def getValue(
self
) -> str:
return str(self.getOffsetHours())
def setValue(
self,
expr: str
):
s = expr.strip().lstrip("+")
try:
self._spinBox.setValue(int(s))
except ValueError:
pass
def getOffsetHours(
self
) -> int:
return self._spinBox.value()
def getRawValue(
self
) -> str:
return str(self._spinBox.value())
def getValueFromWidget(
w: QWidget
) -> str:
if hasattr(w, "getValue"):
return w.getValue()
if isinstance(w, QTimeEdit):
return w.time().toString("HH:mm")
if isinstance(w, QDateEdit):
return w.date().toString("yyyy-MM-dd")
if isinstance(w, QComboBox):
return w.currentData() or w.currentText()
if isinstance(w, QSpinBox):
return str(w.value())
if isinstance(w, QDoubleSpinBox):
return str(w.value())
if isinstance(w, QLineEdit):
return w.text()
return ""
def setWidgetValue(
w: QWidget,
var_type: str,
expr: str
):
if hasattr(w, "setValue"):
w.setValue(expr)
return
s = expr.strip().upper()
if isinstance(w, QTimeEdit):
m = re.match(r"TIME\((\d{1,2}:\d{2})\)", s)
if m:
parts = m.group(1).split(":")
w.setTime(QTime(int(parts[0]), int(parts[1])))
elif isinstance(w, QDateEdit):
m = re.match(r"DATE\((\d{4}-\d{2}-\d{2})\)", s)
if m:
parts = m.group(1).split("-")
w.setDate(QDate(int(parts[0]), int(parts[1]), int(parts[2])))
elif isinstance(w, QComboBox):
for i in range(w.count()):
if w.itemData(i) == s or w.itemText(i).upper() == s:
w.setCurrentIndex(i)
return
elif isinstance(w, QSpinBox):
try:
w.setValue(int(expr))
except ValueError:
pass
elif isinstance(w, QDoubleSpinBox):
try:
w.setValue(float(expr))
except ValueError:
pass
elif isinstance(w, QLineEdit):
inner = expr.strip()
if (inner.startswith("'") and inner.endswith("'")) or \
(inner.startswith('"') and inner.endswith('"')):
inner = inner[1:-1].replace("''", "'")
w.setText(inner)
def encodeValueStr(
raw_value: str,
var_type: str
) -> str:
if var_type == "Time":
if raw_value.startswith("+") or raw_value.startswith("-"):
return raw_value
if raw_value.startswith("TIME_OFFSET"):
m = re.match(r"TIME_OFFSET\(([+-]\d+),(\w+)\)", raw_value)
if m:
return m.group(1)
return raw_value
return f"TIME({raw_value})"
if var_type == "Date":
relMap = {
"前天": "CURRENT_DATE - 2",
"昨天": "CURRENT_DATE - 1",
"今天": "CURRENT_DATE",
"明天": "CURRENT_DATE + 1",
"后天": "CURRENT_DATE + 2"
}
if raw_value in relMap:
return relMap[raw_value]
return f"DATE({raw_value})"
if var_type == "Boolean":
up = raw_value.upper().strip()
if up in (".TRUE.", ".FALSE."):
return up
return ".TRUE." if raw_value else ".FALSE."
if var_type == "String":
escaped = raw_value.replace("'", "''")
return f"'{escaped}'"
return raw_value
def splitTopLevel(
text: str,
delimiter: str
) -> list:
parts = []
depth = 0
buf = ""
i = 0
textUpper = text.upper()
delimUpper = delimiter.upper()
dlen = len(delimUpper)
while i < len(text):
if text[i] == "(":
depth += 1
buf += text[i]
elif text[i] == ")":
depth -= 1
buf += text[i]
elif depth == 0 and textUpper[i:i + dlen] == delimUpper:
parts.append(buf)
buf = ""
i += dlen
continue
else:
buf += text[i]
i += 1
if buf.strip():
parts.append(buf)
return parts
def stripOuterParens(
s: str
) -> str:
s = s.strip()
if s.startswith("(") and s.endswith(")"):
depth = 0
for i, ch in enumerate(s):
if ch == "(":
depth += 1
elif ch == ")":
depth -= 1
if depth == 0 and i < len(s) - 1:
return s
return s[1:-1].strip()
return s
def isVarReference(
expr: str
) -> bool:
s = expr.strip()
up = s.upper()
if up in (".TRUE.", ".FALSE."):
return False
if re.match(r"^TIME\(|^DATE\(|^CURRENT_", up):
return False
if up.startswith("'") or up.startswith('"'):
return False
if re.match(r"^[+-]?\d", s):
return False
return bool(re.match(r"^[A-Z_][A-Z0-9_]*$", up))
def findOperatorIn(
text: str,
operators: list
) -> tuple[int, str] | None:
for op in operators:
op_upper = op.upper()
start = 0
while True:
idx = text.upper().find(op_upper, start)
if idx < 0:
break
if _isInsideLiteral(text, idx):
start = idx + 1
continue
return (idx, op)
return None
def _isInsideLiteral(
text: str,
pos: int
) -> bool:
in_single = False
in_double = False
for i, ch in enumerate(text):
if i >= pos:
break
if ch == "'" and not in_double:
in_single = not in_single
elif ch == '"' and not in_single:
in_double = not in_double
return in_single or in_double
@@ -0,0 +1,107 @@
"""
Orchestration observer for AutoScript scripts.
Subscribes to ASTokenizer parsing events to produce a structured
block representation for the orchestration dialog UI.
"""
from autoscript.ASObserver import ParsingObserver
from autoscript.ASTokenizer import (
ASTokenizer,
K_IF,
K_ELSE_IF,
K_ELSE,
K_ENDIF,
K_SET,
K_ADD,
K_SUB,
)
__all__ = ["ScriptOrchObserver", "parseBlocks"]
class ScriptOrchObserver(ParsingObserver):
"""
Builds an ordered list of (block_type, condition, actions) tuples
from tokenization events.
Each block:
(type: str, condition: str | None, actions: list[(target, value_expr, op_type)])
"""
def __init__(
self
):
super().__init__()
self._blocks = []
self._current_type = None
self._current_condition = None
self._current_actions = []
def onTokenParsed(
self,
kind: str | None,
data,
line_num: int,
raw_line: str
):
if kind in (K_IF, K_ELSE_IF, K_ELSE):
self._flushCurrentBlock()
self._current_type = kind
self._current_condition = data if kind != K_ELSE else None
self._current_actions = []
elif kind in (K_SET, K_ADD, K_SUB):
target, value = data
if kind == K_SET:
self._current_actions.append((target, value, "set"))
elif kind == K_ADD:
self._current_actions.append((target, f"+{value}", "add"))
else:
self._current_actions.append((target, f"-{value}", "sub"))
elif kind == K_ENDIF:
self._flushCurrentBlock()
self._current_type = None
self._current_condition = None
self._current_actions = []
def onParseComplete(
self,
statements: list
):
self._flushCurrentBlock()
def _flushCurrentBlock(
self
):
if self._current_type is not None:
self._blocks.append((
self._current_type,
self._current_condition,
list(self._current_actions),
))
@property
def blocks(
self
) -> list:
return list(self._blocks)
def parseBlocks(
script: str
) -> list:
"""
Tokenize a script via observer pipeline and return its
structured block representation.
"""
observer = ScriptOrchObserver()
ASTokenizer.tokenizeWithObservers(script, [observer])
return observer.blocks
+163
View File
@@ -0,0 +1,163 @@
"""
Pre-check observer for AutoScript scripts.
Subscribes to ASTokenizer parsing events to validate script syntax
before it reaches the orchestration dialog, eliminating duplicate parsing.
"""
from autoscript.ASObserver import ParsingObserver
from autoscript.ASTokenizer import (
K_IF,
K_ELSE_IF,
K_ELSE,
K_ENDIF,
K_SET,
K_ADD,
K_SUB,
ASTokenizer,
)
__all__ = ["ScriptPrecheckObserver", "precheck"]
class ScriptPrecheckObserver(ParsingObserver):
"""
Validates script syntax and structure during tokenization.
Checks performed:
- IF/ENDIF depth matching
- No nested IF blocks (orchestration limitation)
- ELSE IF / ELSE appear only inside an IF block
- Only allowed variables appear in SET/ADD/SUB targets
- No completely unrecognized syntax lines
"""
def __init__(
self,
allowed_vars: set = None
):
super().__init__()
self._allowed = allowed_vars or set()
self._if_depth = 0
self.errors = []
self._stmts = []
def onTokenParsed(
self,
kind: str | None,
data,
line_num: int,
raw_line: str
):
if kind == K_IF:
self._if_depth += 1
if self._if_depth > 1:
self.errors.append(
f"静态检查:错误(第{line_num}行): 检测到嵌套 IF,编排窗口不支持嵌套条件块。"
)
elif kind == K_ELSE_IF:
if self._if_depth < 1:
self.errors.append(
f"静态检查:错误(第{line_num}行): ELSE IF 前缺少 IF。"
)
elif kind == K_ELSE:
if self._if_depth < 1:
self.errors.append(
f"静态检查:错误(第{line_num}行): ELSE 前缺少 IF。"
)
elif kind == K_ENDIF:
self._if_depth -= 1
if self._if_depth < 0:
self.errors.append(
f"静态检查:错误(第{line_num}行): 多余的 ENDIF。"
)
elif kind is None:
self.errors.append(
f"静态检查:错误(第{line_num}行): 无法识别的语法 '{raw_line}'"
)
elif kind in (K_SET, K_ADD, K_SUB):
target = data[0] if isinstance(data, tuple) else ""
if self._allowed and target.upper() not in self._allowed:
self.errors.append(
f"静态检查:错误(第{line_num}行): 目标变量 '{target}' 不是预设变量,"
f"编排窗口不支持。"
)
def onParseComplete(
self,
statements: list
):
if self._if_depth != 0:
self.errors.append(
f"静态检查:错误(不适用): IF 与 ENDIF 不匹配。")
self._stmts = statements
@property
def valid(
self
) -> bool:
return len(self.errors) == 0
def getErrorMessage(
self
) -> str:
return self.errors[0] if self.errors else ""
def buildSimplifiedScript(
self
) -> str:
"""Replace all non-control-flow statements with PASS for engine validation."""
lines = []
for stmt in self._stmts:
if stmt.kind in (K_IF, K_ELSE_IF, K_ELSE, K_ENDIF):
lines.append(stmt.raw_line)
else:
lines.append("PASS")
return "\n".join(lines)
def precheck(
script: str,
allowed_vars: set = None
) -> tuple[bool, str]:
"""
Run the full precheck pipeline on a script.
Steps:
1. Create a ScriptPrecheckObserver and subscribe it to an ASTokenizer.
2. Tokenize — the observer validates syntax during token events.
3. Replace action lines with PASS and run engine validation
with mock target data.
"""
if not script or not script.strip():
return True, ""
observer = ScriptPrecheckObserver(allowed_vars=allowed_vars)
ASTokenizer.tokenizeWithObservers(script, [observer])
if not observer.valid:
return False, observer.getErrorMessage()
simplified = observer.buildSimplifiedScript()
if not simplified.strip():
return True, ""
try:
from autoscript import (
registerDefaultTargetVars,
buildMockTargetData,
execute
)
registerDefaultTargetVars()
execute(simplified, buildMockTargetData())
except ValueError as e:
return False, f"运行时检查: {e}"
except Exception:
return False, "执行环境异常,请检查 AutoScript 配置。"
return True, ""
+534
View File
@@ -0,0 +1,534 @@
"""
Widget components for the AutoScript orchestration dialog.
"""
from PySide6.QtCore import Slot
from PySide6.QtWidgets import (
QComboBox,
QFrame,
QHBoxLayout,
QLabel,
QPushButton,
QSizePolicy,
QStackedWidget
)
from gui.ALAutoScriptOrchDialog._helpers import (
ACTION_TYPES,
ARITH_TYPES,
COMPARE_OPERATORS,
LOGIC_OPERATORS,
PRESET_VARIABLES,
VAR_TYPE_ORDER,
encodeValueStr,
getValueFromWidget,
isVarReference,
makeComboWidget,
makeLabel,
makeOffsetWidget,
makeValueWidget,
makeVarRefCombo,
setWidgetValue,
)
class ConditionRowFrame(QFrame):
def __init__(
self,
varMgr,
parentBlockIndex: int = 0,
isFirst: bool = False,
parent = None
):
super().__init__(parent)
self._varMgr = varMgr
self._blockIndex = parentBlockIndex
self._isFirst = isFirst
self._isBoolMode = False
self.setupUi()
self.connectSignals()
def setupUi(
self
):
self.setUpdatesEnabled(False)
self.setFrameShape(QFrame.StyledPanel)
self.setFrameShadow(QFrame.Raised)
self.setFixedHeight(32)
layout = QHBoxLayout(self)
layout.setContentsMargins(2, 2, 2, 2)
layout.setSpacing(4)
if self._isFirst:
self.logicCombo = None
else:
self.logicCombo = makeComboWidget(LOGIC_OPERATORS, min_width=110, parent=self)
layout.addWidget(self.logicCombo)
self.leftVarCombo = QComboBox(self)
self.leftVarCombo.setFixedHeight(25)
self.leftVarCombo.setMinimumWidth(120)
self.leftVarCombo.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
self.populateLeftVarCombo()
layout.addWidget(self.leftVarCombo)
self.opCombo = makeComboWidget(COMPARE_OPERATORS, min_width=80, parent=self)
layout.addWidget(self.opCombo)
self._compTypeCombo = makeComboWidget([
("特定值", "literal"),
("变量", "variable"),
], min_width=70, parent=self)
layout.addWidget(self._compTypeCombo)
self._rhsStack = QStackedWidget(self)
self._rhsStack.setFixedHeight(25)
self._literalStack = QStackedWidget(self)
self._literalStack.setFixedHeight(25)
self._literalWidgets = {}
for vt in VAR_TYPE_ORDER:
w = makeValueWidget(vt, self._literalStack)
self._literalWidgets[vt] = w
self._literalStack.addWidget(w)
self._literalStack.setCurrentWidget(self._literalWidgets.get("String"))
self._rhsStack.addWidget(self._literalStack)
self.rhsVarCombo = makeVarRefCombo(self)
self._rhsStack.addWidget(self.rhsVarCombo)
self._rhsStack.setCurrentIndex(0)
layout.addWidget(self._rhsStack)
if not self._isFirst:
self.deleteBtn = QPushButton("×", self)
self.deleteBtn.setFixedSize(25, 25)
self.deleteBtn.setStyleSheet("color: red; font-weight: bold;")
layout.addWidget(self.deleteBtn)
else:
self.deleteBtn = None
layout.addStretch()
self.setUpdatesEnabled(True)
def populateLeftVarCombo(
self
):
self._varMgr.populateCombo(self.leftVarCombo)
def populateRhsVarCombo(
self
):
self._varMgr.populateCombo(self.rhsVarCombo)
def connectSignals(
self
):
self.leftVarCombo.currentIndexChanged.connect(self.onLeftVarChanged)
self._compTypeCombo.currentIndexChanged.connect(self.onCompTypeChanged)
@Slot(int)
def onLeftVarChanged(
self,
idx
):
if idx < 0:
return
data = self.leftVarCombo.itemData(idx)
if not data:
return
_, vartype = data
self.updateRhsLiteralWidget(vartype)
def updateRhsLiteralWidget(
self,
vartype: str
):
if vartype not in self._literalWidgets:
vartype = "String"
self._literalStack.setCurrentWidget(self._literalWidgets[vartype])
@Slot(int)
def onCompTypeChanged(
self,
idx
):
isVar = (self._compTypeCombo.currentData() == "variable")
self._rhsStack.setCurrentIndex(1 if isVar else 0)
if isVar:
self.populateRhsVarCombo()
def getLogic(
self
) -> str:
return self.logicCombo.currentData() if self.logicCombo else ""
def toConditionText(
self
) -> str:
data = self.leftVarCombo.currentData()
if not data:
return ""
name, vartype = data
opSym = self.opCombo.currentData()
isVarRef = (self._compTypeCombo.currentData() == "variable")
if isVarRef:
rd = self.rhsVarCombo.currentData()
if rd:
rhsName = rd[0]
return f"{name} {opSym} {rhsName}"
rhsText = self.rhsVarCombo.currentText().strip()
if rhsText:
return f"{name} {opSym} {rhsText}"
return ""
w = self._literalWidgets.get(vartype)
if w:
rawVal = getValueFromWidget(w)
encoded = encodeValueStr(rawVal, vartype)
return f"{name} {opSym} {encoded}"
return ""
def loadFromParts(
self,
operandName: str,
opSym: str,
valueExpr: str
):
for ci in range(self.leftVarCombo.count()):
d = self.leftVarCombo.itemData(ci)
if d and d[0] == operandName:
self.leftVarCombo.setCurrentIndex(ci)
break
if opSym:
for oi in range(self.opCombo.count()):
if self.opCombo.itemData(oi) == opSym:
self.opCombo.setCurrentIndex(oi)
break
if not valueExpr:
return
up = valueExpr.strip().upper()
data = self.leftVarCombo.currentData()
vartype = data[1] if data else "String"
if isVarReference(valueExpr) or self._isKnownVar(up):
self._compTypeCombo.setCurrentIndex(1)
self.populateRhsVarCombo()
found = self._varMgr.findExactNameEntry(self.rhsVarCombo, up)
if found >= 0:
self.rhsVarCombo.setCurrentIndex(found)
else:
self.rhsVarCombo.addItem(up, (up, "String"))
self.rhsVarCombo.setCurrentIndex(self.rhsVarCombo.count() - 1)
else:
self._compTypeCombo.setCurrentIndex(0)
w = self._literalWidgets.get(vartype)
if w:
setWidgetValue(w, vartype, valueExpr)
def _isKnownVar(
self,
name: str
) -> bool:
return self._varMgr.getInfoByName(name) is not None
def refreshVarCombos(
self
):
self.populateLeftVarCombo()
self.populateRhsVarCombo()
class ActionStepFrame(QFrame):
def __init__(
self,
varMgr,
parentBlockIndex: int = 0,
parent = None
):
super().__init__(parent)
self._varMgr = varMgr
self._blockIndex = parentBlockIndex
self._currentTargetType = "String"
self.setupUi()
self.connectSignals()
def setupUi(
self
):
self.setUpdatesEnabled(False)
self.setFrameShape(QFrame.StyledPanel)
self.setFrameShadow(QFrame.Raised)
self.setFixedHeight(35)
layout = QHBoxLayout(self)
layout.setContentsMargins(2, 2, 2, 2)
layout.setSpacing(4)
self.opTypeCombo = makeComboWidget(ACTION_TYPES, min_width=70, parent=self)
layout.addWidget(self.opTypeCombo)
layout.addWidget(makeLabel("设置", self))
self.targetCombo = QComboBox(self)
self.targetCombo.setFixedHeight(25)
self.targetCombo.setMinimumWidth(120)
self.buildTargetCombo()
layout.addWidget(self.targetCombo)
layout.addWidget(makeLabel("", self))
self._valueSrcCombo = makeComboWidget([
("特定值", "literal"),
("变量", "variable"),
], min_width=70, parent=self)
layout.addWidget(self._valueSrcCombo)
self._valueStack = QStackedWidget(self)
self._valueStack.setFixedHeight(25)
self.initValueStacks()
layout.addWidget(self._valueStack)
self.existingVarCombo = makeVarRefCombo(self)
self.existingVarCombo.setVisible(False)
layout.addWidget(self.existingVarCombo)
self.deleteBtn = QPushButton("×", self)
self.deleteBtn.setFixedSize(25, 25)
self.deleteBtn.setStyleSheet("color: red; font-weight: bold;")
layout.addWidget(self.deleteBtn)
self.setUpdatesEnabled(True)
def buildTargetCombo(
self
):
self.targetCombo.blockSignals(True)
self.targetCombo.clear()
for p in PRESET_VARIABLES:
if p["name"] in ("CURRENT_TIME", "CURRENT_DATE"):
continue
info = self._varMgr.getInfoByName(p["name"])
if info:
self.targetCombo.addItem(
info["display"],
(info["name"], info["type"])
)
self.targetCombo.blockSignals(False)
def initValueStacks(
self
):
self._literalWidgets = {}
self._offsetWidgets = {}
for vt in VAR_TYPE_ORDER:
self._literalWidgets[vt] = makeValueWidget(vt, self._valueStack)
self._valueStack.addWidget(self._literalWidgets[vt])
if vt in ARITH_TYPES:
self._offsetWidgets[vt] = makeOffsetWidget(vt, self._valueStack)
self._valueStack.addWidget(self._offsetWidgets[vt])
else:
lbl = QLabel("(不支持该操作)", self._valueStack)
lbl.setFixedHeight(25)
self._offsetWidgets[vt] = lbl
self._valueStack.addWidget(lbl)
def connectSignals(
self
):
self.opTypeCombo.currentIndexChanged.connect(self.onOpTypeChanged)
self.targetCombo.currentIndexChanged.connect(self.onTargetChanged)
self._valueSrcCombo.currentIndexChanged.connect(self.onValueSrcChanged)
@Slot(int)
def onTargetChanged(
self,
idx
):
if idx < 0:
return
data = self.targetCombo.itemData(idx)
if not data:
return
_, vartype = data
self._currentTargetType = vartype
self.updateRHSWidget()
self.onValueSrcChanged(self._valueSrcCombo.currentIndex())
@Slot(int)
def onOpTypeChanged(
self,
idx
):
self.updateRHSWidget()
def updateRHSWidget(
self
):
op = self.opTypeCombo.currentData()
isArith = (op in ("add", "sub"))
actualType = self._currentTargetType
if isArith and actualType in self._offsetWidgets:
self._valueStack.setCurrentWidget(self._offsetWidgets[actualType])
elif actualType in self._literalWidgets:
self._valueStack.setCurrentWidget(self._literalWidgets[actualType])
else:
self._valueStack.setCurrentWidget(self._literalWidgets.get("String"))
@Slot(int)
def onValueSrcChanged(
self,
idx
):
isVar = (self._valueSrcCombo.currentData() == "variable")
self._valueStack.setVisible(not isVar)
self.existingVarCombo.setVisible(isVar)
if isVar:
self._varMgr.populateCombo(self.existingVarCombo)
else:
self.updateRHSWidget()
def getTargetName(
self
) -> str:
data = self.targetCombo.currentData()
return data[0] if data else ""
def toScriptLine(
self
) -> str:
target = self.getTargetName()
if not target:
return ""
op = self.opTypeCombo.currentData()
rawVal = self._getValueRaw()
if op == "set":
vartype = self._currentTargetType
encoded = encodeValueStr(rawVal, vartype)
if vartype == "Time":
if rawVal.startswith("+"):
return f" {target} .ADD. {rawVal[1:]}"
if rawVal.startswith("-"):
return f" {target} .SUB. {rawVal[1:]}"
return f" SET {target} = {encoded}"
elif op == "add":
vartype = self._currentTargetType
if vartype == "Date" and hasattr(self._valueStack.currentWidget(), "getOffsetDays"):
days = self._valueStack.currentWidget().getOffsetDays()
return f" {target} .ADD. {days}"
if vartype == "Time" and hasattr(self._valueStack.currentWidget(), "getOffsetHours"):
hours = self._valueStack.currentWidget().getOffsetHours()
return f" {target} .ADD. {hours}"
return f" {target} .ADD. {rawVal}"
elif op == "sub":
vartype = self._currentTargetType
if vartype == "Date" and hasattr(self._valueStack.currentWidget(), "getOffsetDays"):
days = self._valueStack.currentWidget().getOffsetDays()
return f" {target} .SUB. {days}"
if vartype == "Time" and hasattr(self._valueStack.currentWidget(), "getOffsetHours"):
hours = self._valueStack.currentWidget().getOffsetHours()
return f" {target} .SUB. {hours}"
return f" {target} .SUB. {rawVal}"
return ""
def _getValueRaw(
self
) -> str:
if self._valueSrcCombo.currentData() == "variable":
return self.existingVarCombo.currentText().strip()
w = self._valueStack.currentWidget()
if w:
return getValueFromWidget(w)
return ""
def setOpType(
self,
opType: str
):
for i in range(self.opTypeCombo.count()):
if self.opTypeCombo.itemData(i) == opType:
self.opTypeCombo.setCurrentIndex(i)
break
def loadFromScript(
self,
targetVar: str,
valueExpr: str
):
targetUp = targetVar.upper().strip()
for ci in range(self.targetCombo.count()):
d = self.targetCombo.itemData(ci)
if d and d[0] == targetUp:
self.targetCombo.setCurrentIndex(ci)
break
self._setValueFromExpr(valueExpr)
def _setValueFromExpr(
self,
expr: str
):
s = expr.strip()
if not s:
return
up = s.upper()
if isVarReference(s):
self._valueSrcCombo.setCurrentIndex(1)
self._varMgr.populateCombo(self.existingVarCombo)
idx = self._varMgr.findExactNameEntry(self.existingVarCombo, up)
if idx >= 0:
self.existingVarCombo.setCurrentIndex(idx)
else:
self.existingVarCombo.addItem(up, (up, "String"))
self.existingVarCombo.setCurrentIndex(self.existingVarCombo.count() - 1)
else:
self._valueSrcCombo.setCurrentIndex(0)
w = self._valueStack.currentWidget()
if w:
setWidgetValue(w, self._currentTargetType, expr)
def refreshVarCombos(
self
):
currentData = self.targetCombo.currentData()
self.buildTargetCombo()
if currentData:
for i in range(self.targetCombo.count()):
d = self.targetCombo.itemData(i)
if d and d[0] == currentData[0]:
self.targetCombo.setCurrentIndex(i)
break
self._varMgr.populateCombo(self.existingVarCombo)