statable 3.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- codegen/__init__.py +38 -0
- codegen/c_code_generator.py +1752 -0
- codegen/code_merger.py +371 -0
- codegen/code_templates.py +509 -0
- codegen/config.py +167 -0
- codegen/enum_generator.py +377 -0
- codegen/event_queue_generator.py +303 -0
- codegen/interrupt_generator.py +345 -0
- codegen/naming_convention.py +171 -0
- codegen/osal_generator.py +282 -0
- codegen/role_function_generator.py +1244 -0
- codegen/sample_data.py +269 -0
- codegen/state_actions_generator.py +395 -0
- codegen/struct_generator.py +721 -0
- codegen/timer_generator.py +299 -0
- codegen/transition_generator.py +907 -0
- codegen/type_mapper.py +69 -0
- codegen/validate/__init__.py +38 -0
- codegen/validate/change_actions.py +90 -0
- codegen/validate/change_applier.py +416 -0
- codegen/validate/clipboard_manager.py +47 -0
- codegen/validate/data/__init__.py +30 -0
- codegen/validate/data/action_definitions.py +200 -0
- codegen/validate/data/keywords.py +46 -0
- codegen/validate/data/prompt_templates.py +176 -0
- codegen/validate/data/validation_rules.py +205 -0
- codegen/validate/items/__init__.py +30 -0
- codegen/validate/items/base_validator.py +25 -0
- codegen/validate/items/cell_validator.py +275 -0
- codegen/validate/items/custom_type_validator.py +75 -0
- codegen/validate/items/event_validator.py +78 -0
- codegen/validate/items/flag_validator.py +74 -0
- codegen/validate/items/interrupt_validator.py +75 -0
- codegen/validate/items/queue_validator.py +73 -0
- codegen/validate/items/role_function_validator.py +175 -0
- codegen/validate/items/state_validator.py +88 -0
- codegen/validate/items/timer_validator.py +120 -0
- codegen/validate/items/transition_validator.py +113 -0
- codegen/validate/items/variable_validator.py +132 -0
- codegen/validate/logger.py +52 -0
- codegen/validate/models.py +177 -0
- codegen/validate/prompt_generator.py +724 -0
- codegen/validate/response_parser.py +154 -0
- codegen/validate/response_validator.py +275 -0
- codegen/validate/validation_dialog.py +322 -0
- codegen/validate/validator.py +94 -0
- codegen/variable_generator.py +419 -0
- statable/__init__.py +71 -0
- statable/__main__.py +49 -0
- statable/cli.py +251 -0
- statable/global_defs.py +256 -0
- statable/mermaid_gen.py +144 -0
- statable/model.py +426 -0
- statable/parser.py +64 -0
- statable/sample_data.py +435 -0
- statable/shared/__init__.py +24 -0
- statable/shared/condition_library.py +63 -0
- statable/shared/literal_library.py +66 -0
- statable/shared/role_function_library.py +150 -0
- statable/state_machine.py +121 -0
- statable/xml_io.py +1164 -0
- statable-3.1.0.dist-info/METADATA +151 -0
- statable-3.1.0.dist-info/RECORD +117 -0
- statable-3.1.0.dist-info/WHEEL +5 -0
- statable-3.1.0.dist-info/entry_points.txt +2 -0
- statable-3.1.0.dist-info/top_level.txt +3 -0
- statable_gui/__init__.py +23 -0
- statable_gui/action_edit_dialog.py +214 -0
- statable_gui/code_generation_dialog.py +486 -0
- statable_gui/code_generation_settings_dialog.py +522 -0
- statable_gui/common_widgets.py +537 -0
- statable_gui/condition_builder_dialog.py +500 -0
- statable_gui/condition_edit_dialog.py +123 -0
- statable_gui/condition_template_dialog.py +58 -0
- statable_gui/config.py +21 -0
- statable_gui/dialogs.py +439 -0
- statable_gui/event_definition_dialog.py +605 -0
- statable_gui/event_delivery_settings_dialog.py +191 -0
- statable_gui/event_queue_dialog.py +245 -0
- statable_gui/global_defs.py +167 -0
- statable_gui/global_defs_dialog.py +796 -0
- statable_gui/i18n/__init__.py +118 -0
- statable_gui/interrupt_handler_edit_dialog.py +810 -0
- statable_gui/layer_settings_dialog.py +180 -0
- statable_gui/libcntrl/__init__.py +23 -0
- statable_gui/libcntrl/condition_library.py +13 -0
- statable_gui/libcntrl/literal_library.py +13 -0
- statable_gui/libcntrl/literal_management_dialog.py +185 -0
- statable_gui/libcntrl/role_function_edit_dialog.py +145 -0
- statable_gui/libcntrl/role_function_library.py +13 -0
- statable_gui/literal_definition_dialog.py +62 -0
- statable_gui/logger.py +83 -0
- statable_gui/main_window.py +1380 -0
- statable_gui/matrix_table.py +413 -0
- statable_gui/preference_keys.py +18 -0
- statable_gui/preferences.py +74 -0
- statable_gui/role_function_dialog.py +244 -0
- statable_gui/sample_data.py +11 -0
- statable_gui/state_actions_dialog.py +398 -0
- statable_gui/symbol_picker.py +170 -0
- statable_gui/traceball.py +37 -0
- statable_gui/transition_editor_direct/__init__.py +15 -0
- statable_gui/transition_editor_direct/actions_tab.py +856 -0
- statable_gui/transition_editor_direct/code_widget.py +344 -0
- statable_gui/transition_editor_direct/coverage_analyzer.py +163 -0
- statable_gui/transition_editor_direct/dialog.py +335 -0
- statable_gui/transition_editor_direct/draft.py +270 -0
- statable_gui/transition_editor_direct/edit_dialogs.py +160 -0
- statable_gui/transition_editor_direct/overview_tab.py +173 -0
- statable_gui/transition_editor_direct/palette_widget.py +233 -0
- statable_gui/transition_editor_direct/relations_edit_dialog.py +156 -0
- statable_gui/transition_editor_direct/relations_tab.py +230 -0
- statable_gui/transition_editor_direct/system_global_dialog.py +52 -0
- statable_gui/transition_editor_direct/transition_actions_dialog.py +109 -0
- statable_gui/transition_editor_direct/transitions_tab.py +620 -0
- statable_gui/validation_dialog.py +285 -0
- statable_gui/widgets.py +1315 -0
|
@@ -0,0 +1,856 @@
|
|
|
1
|
+
# statable_gui/transition_editor_direct/actions_tab.py
|
|
2
|
+
"""Pre / Post Actions tab widget (v2.2, v2.5 role-func management).
|
|
3
|
+
|
|
4
|
+
Two groups:
|
|
5
|
+
- Pre : before_transitions (executed before the transition chain)
|
|
6
|
+
- Post : after_transitions (executed after the transition chain)
|
|
7
|
+
|
|
8
|
+
[v2.2 change]
|
|
9
|
+
- `always` trigger removed; old data migrated to `before_transitions`.
|
|
10
|
+
- Role function input is an editable ComboBox.
|
|
11
|
+
|
|
12
|
+
[v2.5 change]
|
|
13
|
+
- Each _ActionGroup gains three buttons:
|
|
14
|
+
+ New Role Function -> RoleFunctionDialog, registers into
|
|
15
|
+
state_machine.role_functions and
|
|
16
|
+
appends a row to the group table.
|
|
17
|
+
Edit Role Function -> edits the role function bound to the
|
|
18
|
+
currently selected row.
|
|
19
|
+
Delete Role Function -> removes it from state_machine and
|
|
20
|
+
deletes all rows referencing it.
|
|
21
|
+
- New constructor args: role_function_library, literal_library,
|
|
22
|
+
layer_names_provider (all optional / keyword).
|
|
23
|
+
|
|
24
|
+
[v2.5 fix]
|
|
25
|
+
- StateMachine.role_functions is keyed by *bare* name (rf.name),
|
|
26
|
+
but this tab displays the *qualified* name (rf.namespace + '.' +
|
|
27
|
+
rf.name). Edit / Delete previously looked up the dict with the
|
|
28
|
+
qualified name and therefore always failed with "not registered".
|
|
29
|
+
The new helper _find_rf_by_display() resolves qualified_name ->
|
|
30
|
+
RoleFunction object, and the caller uses rf.name for the dict
|
|
31
|
+
operations.
|
|
32
|
+
- Extensive debug logging added to _on_new / _on_edit / _on_delete
|
|
33
|
+
so that failures can be diagnosed from the log alone.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
import logging
|
|
37
|
+
from typing import List, Optional
|
|
38
|
+
|
|
39
|
+
from PySide6.QtCore import Qt, Signal
|
|
40
|
+
from PySide6.QtWidgets import (
|
|
41
|
+
QWidget, QVBoxLayout, QHBoxLayout, QTableWidget, QTableWidgetItem,
|
|
42
|
+
QPushButton, QComboBox, QHeaderView, QAbstractItemView, QLabel,
|
|
43
|
+
QGroupBox, QMessageBox, QDialog,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
from statable.model import ActionStep
|
|
47
|
+
from statable_gui.role_function_dialog import RoleFunctionDialog
|
|
48
|
+
from .draft import ActionDraft
|
|
49
|
+
|
|
50
|
+
logger = logging.getLogger("transition_editor_direct.actions_tab")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
# Supported triggers (always is deprecated)
|
|
54
|
+
TRIGGER_PRE = "before_transitions"
|
|
55
|
+
TRIGGER_POST = "after_transitions"
|
|
56
|
+
|
|
57
|
+
# Migration map
|
|
58
|
+
_LEGACY_TRIGGER_MAP = {
|
|
59
|
+
"always": TRIGGER_PRE, # v2.2: always -> before_transitions
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _migrate_trigger(trigger: str) -> str:
|
|
64
|
+
"""Migrate legacy trigger names to the new ones."""
|
|
65
|
+
t = (trigger or "").strip()
|
|
66
|
+
if t in _LEGACY_TRIGGER_MAP:
|
|
67
|
+
return _LEGACY_TRIGGER_MAP[t]
|
|
68
|
+
if t in (TRIGGER_PRE, TRIGGER_POST):
|
|
69
|
+
return t
|
|
70
|
+
return TRIGGER_PRE
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _qualified_name(rf) -> str:
|
|
74
|
+
"""Return qualified_name if available, else fall back to name."""
|
|
75
|
+
qn = getattr(rf, 'qualified_name', None)
|
|
76
|
+
if qn:
|
|
77
|
+
return qn
|
|
78
|
+
return getattr(rf, 'name', '') or ''
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class _ActionGroup(QGroupBox):
|
|
82
|
+
"""A QGroupBox containing a role function list for one trigger."""
|
|
83
|
+
|
|
84
|
+
changed = Signal()
|
|
85
|
+
|
|
86
|
+
def __init__(self, title: str, trigger: str,
|
|
87
|
+
role_functions: List[str],
|
|
88
|
+
role_function_library=None,
|
|
89
|
+
literal_library=None,
|
|
90
|
+
layer_names_provider=None,
|
|
91
|
+
global_defs=None,
|
|
92
|
+
state_machine=None,
|
|
93
|
+
parent=None):
|
|
94
|
+
super().__init__(title, parent)
|
|
95
|
+
self.trigger = trigger
|
|
96
|
+
self.role_functions = list(role_functions or [])
|
|
97
|
+
|
|
98
|
+
# v2.5: context for RoleFunctionDialog
|
|
99
|
+
self.role_function_library = role_function_library
|
|
100
|
+
self.literal_library = literal_library
|
|
101
|
+
self.layer_names_provider = layer_names_provider
|
|
102
|
+
self.global_defs = global_defs
|
|
103
|
+
self.state_machine = state_machine
|
|
104
|
+
|
|
105
|
+
logger.debug(
|
|
106
|
+
f"_ActionGroup.__init__: title='{title}', trigger='{trigger}', "
|
|
107
|
+
f"role_functions={len(self.role_functions)}, "
|
|
108
|
+
f"has_sm={state_machine is not None}, "
|
|
109
|
+
f"has_rflib={role_function_library is not None}, "
|
|
110
|
+
f"has_litlib={literal_library is not None}, "
|
|
111
|
+
f"has_lnp={layer_names_provider is not None}")
|
|
112
|
+
|
|
113
|
+
layout = QVBoxLayout(self)
|
|
114
|
+
|
|
115
|
+
self.table = QTableWidget(0, 1)
|
|
116
|
+
self.table.setHorizontalHeaderLabels(["Role function"])
|
|
117
|
+
self.table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
|
|
118
|
+
self.table.setSelectionBehavior(QAbstractItemView.SelectRows)
|
|
119
|
+
self.table.setSelectionMode(QAbstractItemView.SingleSelection)
|
|
120
|
+
layout.addWidget(self.table)
|
|
121
|
+
|
|
122
|
+
# ---- Row 1: combo + row operations ----
|
|
123
|
+
btn_layout = QHBoxLayout()
|
|
124
|
+
self.combo = QComboBox()
|
|
125
|
+
self.combo.setEditable(True)
|
|
126
|
+
self.combo.addItems(self.role_functions)
|
|
127
|
+
btn_layout.addWidget(self.combo, stretch=1)
|
|
128
|
+
|
|
129
|
+
add_btn = QPushButton(self.tr("+ Add"))
|
|
130
|
+
add_btn.clicked.connect(self._on_add)
|
|
131
|
+
btn_layout.addWidget(add_btn)
|
|
132
|
+
|
|
133
|
+
del_btn = QPushButton(self.tr("Delete"))
|
|
134
|
+
del_btn.setToolTip(self.tr("Remove the selected row from this list "
|
|
135
|
+
"(does not delete the role function itself)"))
|
|
136
|
+
del_btn.clicked.connect(self._on_delete)
|
|
137
|
+
btn_layout.addWidget(del_btn)
|
|
138
|
+
|
|
139
|
+
up_btn = QPushButton(self.tr("Up"))
|
|
140
|
+
up_btn.clicked.connect(self._on_move_up)
|
|
141
|
+
btn_layout.addWidget(up_btn)
|
|
142
|
+
|
|
143
|
+
down_btn = QPushButton(self.tr("Down"))
|
|
144
|
+
down_btn.clicked.connect(self._on_move_down)
|
|
145
|
+
btn_layout.addWidget(down_btn)
|
|
146
|
+
|
|
147
|
+
layout.addLayout(btn_layout)
|
|
148
|
+
|
|
149
|
+
# ---- Row 2 (v2.5): role function management ----
|
|
150
|
+
role_btn_layout = QHBoxLayout()
|
|
151
|
+
|
|
152
|
+
self.new_role_btn = QPushButton(self.tr("+ New Role Function"))
|
|
153
|
+
self.new_role_btn.setToolTip(
|
|
154
|
+
self.tr("Create a new role function and add it to this list"))
|
|
155
|
+
self.new_role_btn.clicked.connect(self._on_new_role_function)
|
|
156
|
+
role_btn_layout.addWidget(self.new_role_btn)
|
|
157
|
+
|
|
158
|
+
self.edit_role_btn = QPushButton(self.tr("Edit Role Function"))
|
|
159
|
+
self.edit_role_btn.setToolTip(
|
|
160
|
+
self.tr("Edit the role function bound to the selected row"))
|
|
161
|
+
self.edit_role_btn.clicked.connect(self._on_edit_role_function)
|
|
162
|
+
role_btn_layout.addWidget(self.edit_role_btn)
|
|
163
|
+
|
|
164
|
+
self.delete_role_btn = QPushButton(self.tr("Delete Role Function"))
|
|
165
|
+
self.delete_role_btn.setToolTip(
|
|
166
|
+
self.tr("Delete the selected role function from the state machine"))
|
|
167
|
+
self.delete_role_btn.clicked.connect(self._on_delete_role_function)
|
|
168
|
+
role_btn_layout.addWidget(self.delete_role_btn)
|
|
169
|
+
|
|
170
|
+
role_btn_layout.addStretch()
|
|
171
|
+
layout.addLayout(role_btn_layout)
|
|
172
|
+
|
|
173
|
+
# ------------------------------------------------------------------
|
|
174
|
+
# Public setters
|
|
175
|
+
# ------------------------------------------------------------------
|
|
176
|
+
def set_role_functions(self, names: List[str]):
|
|
177
|
+
self.role_functions = list(names or [])
|
|
178
|
+
current = self.combo.currentText()
|
|
179
|
+
self.combo.clear()
|
|
180
|
+
self.combo.addItems(self.role_functions)
|
|
181
|
+
if current:
|
|
182
|
+
self.combo.setCurrentText(current)
|
|
183
|
+
logger.debug(
|
|
184
|
+
f"set_role_functions: {len(self.role_functions)} candidates")
|
|
185
|
+
|
|
186
|
+
def set_actions(self, actions: List[ActionStep]):
|
|
187
|
+
"""Replace the list (filtered by trigger)."""
|
|
188
|
+
self.table.setRowCount(0)
|
|
189
|
+
for a in actions:
|
|
190
|
+
if _migrate_trigger(a.trigger) != self.trigger:
|
|
191
|
+
continue
|
|
192
|
+
self._append_row(a.role_function)
|
|
193
|
+
logger.debug(
|
|
194
|
+
f"set_actions: table rows={self.table.rowCount()}, "
|
|
195
|
+
f"trigger={self.trigger}")
|
|
196
|
+
|
|
197
|
+
def get_actions(self) -> List[ActionStep]:
|
|
198
|
+
result = []
|
|
199
|
+
for row in range(self.table.rowCount()):
|
|
200
|
+
item = self.table.item(row, 0)
|
|
201
|
+
name = item.text().strip() if item else ""
|
|
202
|
+
if name:
|
|
203
|
+
result.append(ActionStep(
|
|
204
|
+
role_function=name,
|
|
205
|
+
trigger=self.trigger,
|
|
206
|
+
title=name,
|
|
207
|
+
))
|
|
208
|
+
return result
|
|
209
|
+
|
|
210
|
+
def row_count(self) -> int:
|
|
211
|
+
return self.table.rowCount()
|
|
212
|
+
|
|
213
|
+
# ------------------------------------------------------------------
|
|
214
|
+
# Slots (row operations - existing)
|
|
215
|
+
# ------------------------------------------------------------------
|
|
216
|
+
def _append_row(self, role_function: str):
|
|
217
|
+
row = self.table.rowCount()
|
|
218
|
+
self.table.insertRow(row)
|
|
219
|
+
self.table.setItem(row, 0, QTableWidgetItem(role_function))
|
|
220
|
+
logger.debug(
|
|
221
|
+
f"_append_row: '{role_function}' -> row {row}")
|
|
222
|
+
|
|
223
|
+
def _on_add(self):
|
|
224
|
+
name = self.combo.currentText().strip()
|
|
225
|
+
if not name:
|
|
226
|
+
return
|
|
227
|
+
self._append_row(name)
|
|
228
|
+
self.changed.emit()
|
|
229
|
+
|
|
230
|
+
def _on_delete(self):
|
|
231
|
+
row = self.table.currentRow()
|
|
232
|
+
if row >= 0:
|
|
233
|
+
self.table.removeRow(row)
|
|
234
|
+
self.changed.emit()
|
|
235
|
+
|
|
236
|
+
def _on_move_up(self):
|
|
237
|
+
row = self.table.currentRow()
|
|
238
|
+
if row <= 0:
|
|
239
|
+
return
|
|
240
|
+
self._swap(row, row - 1)
|
|
241
|
+
self.table.setCurrentCell(row - 1, 0)
|
|
242
|
+
self.changed.emit()
|
|
243
|
+
|
|
244
|
+
def _on_move_down(self):
|
|
245
|
+
row = self.table.currentRow()
|
|
246
|
+
if row < 0 or row >= self.table.rowCount() - 1:
|
|
247
|
+
return
|
|
248
|
+
self._swap(row, row + 1)
|
|
249
|
+
self.table.setCurrentCell(row + 1, 0)
|
|
250
|
+
self.changed.emit()
|
|
251
|
+
|
|
252
|
+
def _swap(self, r1: int, r2: int):
|
|
253
|
+
i1 = self.table.takeItem(r1, 0)
|
|
254
|
+
i2 = self.table.takeItem(r2, 0)
|
|
255
|
+
self.table.setItem(r1, 0, i2)
|
|
256
|
+
self.table.setItem(r2, 0, i1)
|
|
257
|
+
|
|
258
|
+
# ------------------------------------------------------------------
|
|
259
|
+
# v2.5: debug helper
|
|
260
|
+
# ------------------------------------------------------------------
|
|
261
|
+
def _dump_sm_roles(self, label: str):
|
|
262
|
+
"""Log the current StateMachine.role_functions contents.
|
|
263
|
+
|
|
264
|
+
Useful for diagnosing qualified-vs-bare name mismatches.
|
|
265
|
+
"""
|
|
266
|
+
if self.state_machine is None:
|
|
267
|
+
logger.debug(f"[{label}] state_machine is None")
|
|
268
|
+
return
|
|
269
|
+
try:
|
|
270
|
+
items = list(self.state_machine.role_functions.items())
|
|
271
|
+
except Exception as e:
|
|
272
|
+
logger.warning(f"[{label}] cannot list role_functions: {e}")
|
|
273
|
+
return
|
|
274
|
+
logger.debug(f"[{label}] state_machine.role_functions: "
|
|
275
|
+
f"{len(items)} entries")
|
|
276
|
+
for key, rf in items:
|
|
277
|
+
logger.debug(
|
|
278
|
+
f"[{label}] key='{key}' "
|
|
279
|
+
f"name='{getattr(rf, 'name', '?')}' "
|
|
280
|
+
f"namespace='{getattr(rf, 'namespace', '?')}' "
|
|
281
|
+
f"qualified='{_qualified_name(rf)}'")
|
|
282
|
+
|
|
283
|
+
# ------------------------------------------------------------------
|
|
284
|
+
# v2.5: role-function lookup
|
|
285
|
+
# ------------------------------------------------------------------
|
|
286
|
+
def _find_rf_by_display(self, display_name: str):
|
|
287
|
+
"""Find a StateMachine role function whose qualified_name
|
|
288
|
+
(or bare name) matches `display_name`.
|
|
289
|
+
|
|
290
|
+
The StateMachine keys role functions by *bare* name
|
|
291
|
+
(`rf.name`), but this tab displays the qualified name
|
|
292
|
+
(`rf.namespace + '.' + rf.name`). This helper bridges the
|
|
293
|
+
two so that Edit / Delete can locate the correct entry.
|
|
294
|
+
|
|
295
|
+
Returns the RoleFunction object, or None if not found.
|
|
296
|
+
"""
|
|
297
|
+
if self.state_machine is None:
|
|
298
|
+
logger.warning(
|
|
299
|
+
f"_find_rf_by_display('{display_name}'): "
|
|
300
|
+
"state_machine is None")
|
|
301
|
+
return None
|
|
302
|
+
if not display_name:
|
|
303
|
+
logger.debug("_find_rf_by_display: empty display_name")
|
|
304
|
+
return None
|
|
305
|
+
|
|
306
|
+
entries = list(self.state_machine.role_functions.values())
|
|
307
|
+
logger.debug(
|
|
308
|
+
f"_find_rf_by_display('{display_name}'): "
|
|
309
|
+
f"scanning {len(entries)} entries")
|
|
310
|
+
|
|
311
|
+
for rf in entries:
|
|
312
|
+
qn = _qualified_name(rf)
|
|
313
|
+
bare = getattr(rf, 'name', '') or ''
|
|
314
|
+
logger.debug(
|
|
315
|
+
f" candidate: bare='{bare}', qualified='{qn}'")
|
|
316
|
+
if qn == display_name:
|
|
317
|
+
logger.debug(
|
|
318
|
+
f" -> matched by qualified_name: '{qn}'")
|
|
319
|
+
return rf
|
|
320
|
+
if bare == display_name:
|
|
321
|
+
logger.debug(
|
|
322
|
+
f" -> matched by bare name: '{bare}'")
|
|
323
|
+
return rf
|
|
324
|
+
|
|
325
|
+
logger.warning(
|
|
326
|
+
f"_find_rf_by_display('{display_name}'): NOT FOUND "
|
|
327
|
+
f"among {len(entries)} entries")
|
|
328
|
+
return None
|
|
329
|
+
|
|
330
|
+
# ------------------------------------------------------------------
|
|
331
|
+
# v2.5: namespace / dialog helpers
|
|
332
|
+
# ------------------------------------------------------------------
|
|
333
|
+
def _get_namespace_choices(self) -> List[str]:
|
|
334
|
+
"""Build namespace candidates for the RoleFunctionDialog.
|
|
335
|
+
|
|
336
|
+
Order:
|
|
337
|
+
1. layer_names_provider (all tabs in the project)
|
|
338
|
+
2. state_machine.layer_name
|
|
339
|
+
3. state_machine.role_functions[].namespace
|
|
340
|
+
4. role_function_library entries
|
|
341
|
+
"""
|
|
342
|
+
ordered: List[str] = []
|
|
343
|
+
|
|
344
|
+
def _add(ns):
|
|
345
|
+
ns = (ns or "").strip()
|
|
346
|
+
if ns and ns not in ordered:
|
|
347
|
+
ordered.append(ns)
|
|
348
|
+
|
|
349
|
+
if self.layer_names_provider is not None:
|
|
350
|
+
try:
|
|
351
|
+
for name in self.layer_names_provider():
|
|
352
|
+
_add(name)
|
|
353
|
+
except Exception as e:
|
|
354
|
+
logger.warning(f"layer_names_provider failed: {e}")
|
|
355
|
+
|
|
356
|
+
if self.state_machine is not None:
|
|
357
|
+
_add(getattr(self.state_machine, 'layer_name', ''))
|
|
358
|
+
try:
|
|
359
|
+
for rf in self.state_machine.role_functions.values():
|
|
360
|
+
_add(getattr(rf, 'namespace', ''))
|
|
361
|
+
except Exception as e:
|
|
362
|
+
logger.warning(
|
|
363
|
+
f"state_machine.role_functions iteration failed: {e}")
|
|
364
|
+
|
|
365
|
+
if self.role_function_library is not None:
|
|
366
|
+
try:
|
|
367
|
+
for rf in self.role_function_library.list_all():
|
|
368
|
+
_add(getattr(rf, 'namespace', ''))
|
|
369
|
+
except Exception as e:
|
|
370
|
+
logger.warning(
|
|
371
|
+
f"role_function_library.list_all() failed: {e}")
|
|
372
|
+
|
|
373
|
+
logger.debug(
|
|
374
|
+
f"_get_namespace_choices: {len(ordered)} candidates "
|
|
375
|
+
f"{ordered}")
|
|
376
|
+
return ordered
|
|
377
|
+
|
|
378
|
+
def _dialog_kwargs(self) -> dict:
|
|
379
|
+
"""Build kwargs for RoleFunctionDialog (mirrors SettingsPanel)."""
|
|
380
|
+
global_vars = []
|
|
381
|
+
if self.global_defs is not None:
|
|
382
|
+
global_vars = [
|
|
383
|
+
getattr(v, 'name', '')
|
|
384
|
+
for v in (getattr(self.global_defs, 'variables', []) or [])
|
|
385
|
+
if getattr(v, 'name', '')
|
|
386
|
+
]
|
|
387
|
+
|
|
388
|
+
events = []
|
|
389
|
+
if self.state_machine is not None:
|
|
390
|
+
events = [
|
|
391
|
+
getattr(e, 'name', '')
|
|
392
|
+
for e in self.state_machine.events.values()
|
|
393
|
+
if getattr(e, 'name', '')
|
|
394
|
+
]
|
|
395
|
+
|
|
396
|
+
literals = []
|
|
397
|
+
if self.literal_library is not None:
|
|
398
|
+
try:
|
|
399
|
+
literals = [
|
|
400
|
+
getattr(lit, 'name', '')
|
|
401
|
+
for lit in self.literal_library.list_all()
|
|
402
|
+
if getattr(lit, 'name', '')
|
|
403
|
+
]
|
|
404
|
+
except Exception as e:
|
|
405
|
+
logger.warning(
|
|
406
|
+
f"literal_library.list_all() failed: {e}")
|
|
407
|
+
|
|
408
|
+
kwargs = dict(
|
|
409
|
+
global_vars=global_vars,
|
|
410
|
+
events=events,
|
|
411
|
+
literals=literals,
|
|
412
|
+
namespace_choices=self._get_namespace_choices(),
|
|
413
|
+
)
|
|
414
|
+
logger.debug(
|
|
415
|
+
f"_dialog_kwargs: global_vars={len(global_vars)}, "
|
|
416
|
+
f"events={len(events)}, literals={len(literals)}, "
|
|
417
|
+
f"namespace_choices={len(kwargs['namespace_choices'])}")
|
|
418
|
+
return kwargs
|
|
419
|
+
|
|
420
|
+
def _sync_combo(self):
|
|
421
|
+
"""Rebuild the editable combo from self.role_functions."""
|
|
422
|
+
current = self.combo.currentText()
|
|
423
|
+
self.combo.clear()
|
|
424
|
+
self.combo.addItems(self.role_functions)
|
|
425
|
+
if current:
|
|
426
|
+
self.combo.setCurrentText(current)
|
|
427
|
+
logger.debug(
|
|
428
|
+
f"_sync_combo: {len(self.role_functions)} items, "
|
|
429
|
+
f"current='{current}'")
|
|
430
|
+
|
|
431
|
+
# ------------------------------------------------------------------
|
|
432
|
+
# v2.5: slot implementations
|
|
433
|
+
# ------------------------------------------------------------------
|
|
434
|
+
def _on_new_role_function(self):
|
|
435
|
+
"""v2.5: create a new role function and append it to the list."""
|
|
436
|
+
logger.debug("=== _on_new_role_function: ENTER ===")
|
|
437
|
+
if self.state_machine is None:
|
|
438
|
+
logger.warning(
|
|
439
|
+
"_on_new_role_function: state_machine is None -> abort")
|
|
440
|
+
QMessageBox.information(
|
|
441
|
+
self, self.tr("Information"),
|
|
442
|
+
self.tr("StateMachine is not available; cannot create role "
|
|
443
|
+
"functions from here."))
|
|
444
|
+
return
|
|
445
|
+
|
|
446
|
+
self._dump_sm_roles("_on_new_role_function/before")
|
|
447
|
+
|
|
448
|
+
dlg = RoleFunctionDialog(self, **self._dialog_kwargs())
|
|
449
|
+
logger.debug("_on_new_role_function: dialog opened")
|
|
450
|
+
if dlg.exec() != QDialog.Accepted:
|
|
451
|
+
logger.debug("_on_new_role_function: dialog cancelled")
|
|
452
|
+
return
|
|
453
|
+
|
|
454
|
+
rf = dlg.get_role_function()
|
|
455
|
+
logger.debug(
|
|
456
|
+
f"_on_new_role_function: got rf "
|
|
457
|
+
f"name='{rf.name}', namespace='{rf.namespace}', "
|
|
458
|
+
f"title='{rf.title}', qualified='{_qualified_name(rf)}'")
|
|
459
|
+
|
|
460
|
+
if not rf.name:
|
|
461
|
+
logger.warning(
|
|
462
|
+
"_on_new_role_function: empty name -> abort")
|
|
463
|
+
QMessageBox.warning(
|
|
464
|
+
self, self.tr("Warning"),
|
|
465
|
+
self.tr("Role function name is required."))
|
|
466
|
+
return
|
|
467
|
+
if rf.name in self.state_machine.role_functions:
|
|
468
|
+
logger.warning(
|
|
469
|
+
f"_on_new_role_function: duplicate bare name "
|
|
470
|
+
f"'{rf.name}' in state_machine")
|
|
471
|
+
QMessageBox.warning(
|
|
472
|
+
self, self.tr("Warning"),
|
|
473
|
+
self.tr("A role function with the same name already exists."))
|
|
474
|
+
return
|
|
475
|
+
|
|
476
|
+
try:
|
|
477
|
+
self.state_machine.add_role_function(rf)
|
|
478
|
+
logger.debug(
|
|
479
|
+
f"_on_new_role_function: added to sm key='{rf.name}'")
|
|
480
|
+
except ValueError as e:
|
|
481
|
+
logger.warning(
|
|
482
|
+
f"_on_new_role_function: add_role_function failed: {e}")
|
|
483
|
+
QMessageBox.warning(self, self.tr("Warning"), str(e))
|
|
484
|
+
return
|
|
485
|
+
|
|
486
|
+
self._dump_sm_roles("_on_new_role_function/after_add")
|
|
487
|
+
|
|
488
|
+
qn = _qualified_name(rf)
|
|
489
|
+
if qn and qn not in self.role_functions:
|
|
490
|
+
self.role_functions.append(qn)
|
|
491
|
+
self._sync_combo()
|
|
492
|
+
logger.debug(
|
|
493
|
+
f"_on_new_role_function: appended '{qn}' to local "
|
|
494
|
+
f"role_functions (now {len(self.role_functions)})")
|
|
495
|
+
|
|
496
|
+
self._append_row(qn)
|
|
497
|
+
last_row = self.table.rowCount() - 1
|
|
498
|
+
self.table.setCurrentCell(last_row, 0)
|
|
499
|
+
self.changed.emit()
|
|
500
|
+
logger.info(f"Role function created: {qn}")
|
|
501
|
+
logger.debug("=== _on_new_role_function: EXIT (ok) ===")
|
|
502
|
+
|
|
503
|
+
def _on_edit_role_function(self):
|
|
504
|
+
"""v2.5: edit the role function bound to the selected row."""
|
|
505
|
+
logger.debug("=== _on_edit_role_function: ENTER ===")
|
|
506
|
+
row = self.table.currentRow()
|
|
507
|
+
logger.debug(f"_on_edit_role_function: currentRow={row}")
|
|
508
|
+
if row < 0:
|
|
509
|
+
QMessageBox.information(
|
|
510
|
+
self, self.tr("Information"), self.tr("Please select a row first."))
|
|
511
|
+
return
|
|
512
|
+
item = self.table.item(row, 0)
|
|
513
|
+
if item is None:
|
|
514
|
+
logger.warning(
|
|
515
|
+
f"_on_edit_role_function: no item at row {row}")
|
|
516
|
+
return
|
|
517
|
+
display_name = item.text().strip()
|
|
518
|
+
logger.debug(
|
|
519
|
+
f"_on_edit_role_function: display_name='{display_name}'")
|
|
520
|
+
if not display_name:
|
|
521
|
+
logger.warning(
|
|
522
|
+
"_on_edit_role_function: empty display_name")
|
|
523
|
+
return
|
|
524
|
+
|
|
525
|
+
if self.state_machine is None:
|
|
526
|
+
logger.warning(
|
|
527
|
+
"_on_edit_role_function: state_machine is None")
|
|
528
|
+
QMessageBox.information(
|
|
529
|
+
self, self.tr("Information"),
|
|
530
|
+
self.tr("StateMachine is not available."))
|
|
531
|
+
return
|
|
532
|
+
|
|
533
|
+
self._dump_sm_roles("_on_edit_role_function/before_lookup")
|
|
534
|
+
|
|
535
|
+
# [fix] Resolve via qualified_name -> RoleFunction object,
|
|
536
|
+
# then use its bare name as the StateMachine dict key.
|
|
537
|
+
rf = self._find_rf_by_display(display_name)
|
|
538
|
+
if rf is None:
|
|
539
|
+
logger.warning(
|
|
540
|
+
f"_on_edit_role_function: could not resolve "
|
|
541
|
+
f"'{display_name}'")
|
|
542
|
+
QMessageBox.information(
|
|
543
|
+
self, self.tr("Information"),
|
|
544
|
+
f"Role function '{display_name}' is not registered in "
|
|
545
|
+
"this state machine.\nOnly state-machine-owned role "
|
|
546
|
+
"functions can be edited here.")
|
|
547
|
+
return
|
|
548
|
+
pure_name = rf.name
|
|
549
|
+
logger.debug(
|
|
550
|
+
f"_on_edit_role_function: resolved '{display_name}' -> "
|
|
551
|
+
f"pure_name='{pure_name}'")
|
|
552
|
+
|
|
553
|
+
dlg = RoleFunctionDialog(
|
|
554
|
+
self, role_function=rf, **self._dialog_kwargs())
|
|
555
|
+
logger.debug("_on_edit_role_function: dialog opened")
|
|
556
|
+
if dlg.exec() != QDialog.Accepted:
|
|
557
|
+
logger.debug("_on_edit_role_function: dialog cancelled")
|
|
558
|
+
return
|
|
559
|
+
|
|
560
|
+
updated = dlg.get_role_function()
|
|
561
|
+
logger.debug(
|
|
562
|
+
f"_on_edit_role_function: updated "
|
|
563
|
+
f"name='{updated.name}', namespace='{updated.namespace}', "
|
|
564
|
+
f"qualified='{_qualified_name(updated)}'")
|
|
565
|
+
|
|
566
|
+
if (updated.name != pure_name
|
|
567
|
+
and updated.name in self.state_machine.role_functions):
|
|
568
|
+
logger.warning(
|
|
569
|
+
f"_on_edit_role_function: rename to '{updated.name}' "
|
|
570
|
+
f"collides with existing entry")
|
|
571
|
+
QMessageBox.warning(
|
|
572
|
+
self, self.tr("Warning"),
|
|
573
|
+
self.tr("A role function with the same name already exists."))
|
|
574
|
+
return
|
|
575
|
+
|
|
576
|
+
# [fix] remove_role_function takes the BARE name (dict key).
|
|
577
|
+
logger.debug(
|
|
578
|
+
f"_on_edit_role_function: removing old key '{pure_name}'")
|
|
579
|
+
self.state_machine.remove_role_function(pure_name)
|
|
580
|
+
try:
|
|
581
|
+
self.state_machine.add_role_function(updated)
|
|
582
|
+
logger.debug(
|
|
583
|
+
f"_on_edit_role_function: added new key "
|
|
584
|
+
f"'{updated.name}'")
|
|
585
|
+
except ValueError as e:
|
|
586
|
+
logger.warning(
|
|
587
|
+
f"_on_edit_role_function: add_role_function failed: {e} "
|
|
588
|
+
f"-> rollback")
|
|
589
|
+
# Roll back: re-add the original to avoid losing it
|
|
590
|
+
try:
|
|
591
|
+
self.state_machine.add_role_function(rf)
|
|
592
|
+
logger.debug(
|
|
593
|
+
"_on_edit_role_function: rollback succeeded")
|
|
594
|
+
except Exception as e2:
|
|
595
|
+
logger.error(
|
|
596
|
+
f"_on_edit_role_function: rollback failed: {e2}")
|
|
597
|
+
QMessageBox.warning(self, self.tr("Warning"), str(e))
|
|
598
|
+
return
|
|
599
|
+
|
|
600
|
+
self._dump_sm_roles("_on_edit_role_function/after_update")
|
|
601
|
+
|
|
602
|
+
new_qn = _qualified_name(updated)
|
|
603
|
+
item.setText(new_qn)
|
|
604
|
+
logger.debug(
|
|
605
|
+
f"_on_edit_role_function: row {row} text -> '{new_qn}'")
|
|
606
|
+
|
|
607
|
+
# Keep the combo / role_functions list in sync
|
|
608
|
+
for i, n in enumerate(self.role_functions):
|
|
609
|
+
if n == display_name:
|
|
610
|
+
self.role_functions[i] = new_qn
|
|
611
|
+
logger.debug(
|
|
612
|
+
f"_on_edit_role_function: role_functions[{i}] "
|
|
613
|
+
f"'{n}' -> '{new_qn}'")
|
|
614
|
+
break
|
|
615
|
+
else:
|
|
616
|
+
if new_qn and new_qn not in self.role_functions:
|
|
617
|
+
self.role_functions.append(new_qn)
|
|
618
|
+
logger.debug(
|
|
619
|
+
f"_on_edit_role_function: appended '{new_qn}' "
|
|
620
|
+
f"to role_functions")
|
|
621
|
+
self._sync_combo()
|
|
622
|
+
|
|
623
|
+
# Also rewrite any other rows referencing the old name
|
|
624
|
+
for r in range(self.table.rowCount()):
|
|
625
|
+
if r == row:
|
|
626
|
+
continue
|
|
627
|
+
it = self.table.item(r, 0)
|
|
628
|
+
if it is not None and it.text().strip() == display_name:
|
|
629
|
+
it.setText(new_qn)
|
|
630
|
+
logger.debug(
|
|
631
|
+
f"_on_edit_role_function: row {r} also updated "
|
|
632
|
+
f"-> '{new_qn}'")
|
|
633
|
+
|
|
634
|
+
self.changed.emit()
|
|
635
|
+
logger.info(
|
|
636
|
+
f"Role function updated: '{display_name}' -> '{new_qn}' "
|
|
637
|
+
f"(pure name: '{pure_name}' -> '{updated.name}')")
|
|
638
|
+
logger.debug("=== _on_edit_role_function: EXIT (ok) ===")
|
|
639
|
+
|
|
640
|
+
def _on_delete_role_function(self):
|
|
641
|
+
"""v2.5: delete the selected role function from the state machine."""
|
|
642
|
+
logger.debug("=== _on_delete_role_function: ENTER ===")
|
|
643
|
+
row = self.table.currentRow()
|
|
644
|
+
logger.debug(f"_on_delete_role_function: currentRow={row}")
|
|
645
|
+
if row < 0:
|
|
646
|
+
QMessageBox.information(
|
|
647
|
+
self, self.tr("Information"), self.tr("Please select a row first."))
|
|
648
|
+
return
|
|
649
|
+
item = self.table.item(row, 0)
|
|
650
|
+
if item is None:
|
|
651
|
+
logger.warning(
|
|
652
|
+
f"_on_delete_role_function: no item at row {row}")
|
|
653
|
+
return
|
|
654
|
+
display_name = item.text().strip()
|
|
655
|
+
logger.debug(
|
|
656
|
+
f"_on_delete_role_function: display_name='{display_name}'")
|
|
657
|
+
if not display_name:
|
|
658
|
+
logger.warning(
|
|
659
|
+
"_on_delete_role_function: empty display_name")
|
|
660
|
+
return
|
|
661
|
+
|
|
662
|
+
if self.state_machine is None:
|
|
663
|
+
logger.warning(
|
|
664
|
+
"_on_delete_role_function: state_machine is None")
|
|
665
|
+
QMessageBox.information(
|
|
666
|
+
self, self.tr("Information"),
|
|
667
|
+
self.tr("StateMachine is not available."))
|
|
668
|
+
return
|
|
669
|
+
|
|
670
|
+
self._dump_sm_roles("_on_delete_role_function/before_lookup")
|
|
671
|
+
|
|
672
|
+
# [fix] Resolve via qualified_name -> RoleFunction object.
|
|
673
|
+
rf = self._find_rf_by_display(display_name)
|
|
674
|
+
if rf is None:
|
|
675
|
+
logger.warning(
|
|
676
|
+
f"_on_delete_role_function: could not resolve "
|
|
677
|
+
f"'{display_name}'")
|
|
678
|
+
QMessageBox.information(
|
|
679
|
+
self, self.tr("Information"),
|
|
680
|
+
f"Role function '{display_name}' is not registered in "
|
|
681
|
+
"this state machine.")
|
|
682
|
+
return
|
|
683
|
+
pure_name = rf.name
|
|
684
|
+
logger.debug(
|
|
685
|
+
f"_on_delete_role_function: resolved '{display_name}' -> "
|
|
686
|
+
f"pure_name='{pure_name}'")
|
|
687
|
+
|
|
688
|
+
reply = QMessageBox.question(
|
|
689
|
+
self, self.tr("Confirm delete"),
|
|
690
|
+
f"Delete role function '{display_name}' from the state "
|
|
691
|
+
"machine?\nThis removes it from all cells in this layer.",
|
|
692
|
+
QMessageBox.Yes | QMessageBox.No,
|
|
693
|
+
QMessageBox.No)
|
|
694
|
+
if reply != QMessageBox.Yes:
|
|
695
|
+
logger.debug(
|
|
696
|
+
"_on_delete_role_function: user declined -> abort")
|
|
697
|
+
return
|
|
698
|
+
|
|
699
|
+
# [fix] remove_role_function takes the BARE name.
|
|
700
|
+
logger.debug(
|
|
701
|
+
f"_on_delete_role_function: removing key '{pure_name}'")
|
|
702
|
+
self.state_machine.remove_role_function(pure_name)
|
|
703
|
+
|
|
704
|
+
self._dump_sm_roles("_on_delete_role_function/after_remove")
|
|
705
|
+
|
|
706
|
+
# Remove all rows referencing this display name
|
|
707
|
+
removed_rows = 0
|
|
708
|
+
for r in range(self.table.rowCount() - 1, -1, -1):
|
|
709
|
+
it = self.table.item(r, 0)
|
|
710
|
+
if it is not None and it.text().strip() == display_name:
|
|
711
|
+
self.table.removeRow(r)
|
|
712
|
+
removed_rows += 1
|
|
713
|
+
logger.debug(
|
|
714
|
+
f"_on_delete_role_function: removed {removed_rows} row(s) "
|
|
715
|
+
f"from table")
|
|
716
|
+
|
|
717
|
+
if display_name in self.role_functions:
|
|
718
|
+
self.role_functions.remove(display_name)
|
|
719
|
+
logger.debug(
|
|
720
|
+
f"_on_delete_role_function: removed '{display_name}' "
|
|
721
|
+
f"from role_functions (now {len(self.role_functions)})")
|
|
722
|
+
self._sync_combo()
|
|
723
|
+
|
|
724
|
+
self.changed.emit()
|
|
725
|
+
logger.info(
|
|
726
|
+
f"Role function deleted: '{display_name}' "
|
|
727
|
+
f"(pure name: '{pure_name}')")
|
|
728
|
+
logger.debug("=== _on_delete_role_function: EXIT (ok) ===")
|
|
729
|
+
|
|
730
|
+
|
|
731
|
+
class ActionsTab(QWidget):
|
|
732
|
+
"""Pre / Post Actions tab."""
|
|
733
|
+
|
|
734
|
+
actions_changed = Signal()
|
|
735
|
+
|
|
736
|
+
def __init__(self, draft: ActionDraft,
|
|
737
|
+
role_functions: Optional[List[str]] = None,
|
|
738
|
+
global_defs=None, state_machine=None,
|
|
739
|
+
role_function_library=None,
|
|
740
|
+
literal_library=None,
|
|
741
|
+
layer_names_provider=None,
|
|
742
|
+
parent=None):
|
|
743
|
+
super().__init__(parent)
|
|
744
|
+
self.draft = draft
|
|
745
|
+
self.role_functions = list(role_functions or [])
|
|
746
|
+
self.global_defs = global_defs
|
|
747
|
+
self.state_machine = state_machine
|
|
748
|
+
# v2.5: context for RoleFunctionDialog
|
|
749
|
+
self.role_function_library = role_function_library
|
|
750
|
+
self.literal_library = literal_library
|
|
751
|
+
self.layer_names_provider = layer_names_provider
|
|
752
|
+
|
|
753
|
+
logger.debug(
|
|
754
|
+
f"ActionsTab.__init__: role_functions={len(self.role_functions)}, "
|
|
755
|
+
f"has_sm={state_machine is not None}, "
|
|
756
|
+
f"has_rflib={role_function_library is not None}, "
|
|
757
|
+
f"has_litlib={literal_library is not None}, "
|
|
758
|
+
f"has_lnp={layer_names_provider is not None}")
|
|
759
|
+
|
|
760
|
+
self._build_ui()
|
|
761
|
+
|
|
762
|
+
def set_role_functions(self, names: List[str]):
|
|
763
|
+
self.role_functions = list(names or [])
|
|
764
|
+
self.pre_group.set_role_functions(self.role_functions)
|
|
765
|
+
self.post_group.set_role_functions(self.role_functions)
|
|
766
|
+
logger.debug(
|
|
767
|
+
f"ActionsTab.set_role_functions: {len(self.role_functions)} "
|
|
768
|
+
f"candidates")
|
|
769
|
+
|
|
770
|
+
def _build_ui(self):
|
|
771
|
+
layout = QVBoxLayout(self)
|
|
772
|
+
layout.addWidget(QLabel(
|
|
773
|
+
self.tr("Cell-level actions:\n"
|
|
774
|
+
" Pre = before the transition chain (runs even if no transition fires)\n"
|
|
775
|
+
" Post = after the transition chain (runs even after early return)")))
|
|
776
|
+
|
|
777
|
+
self.pre_group = _ActionGroup(
|
|
778
|
+
"Pre (before transitions)", TRIGGER_PRE, self.role_functions,
|
|
779
|
+
role_function_library=self.role_function_library,
|
|
780
|
+
literal_library=self.literal_library,
|
|
781
|
+
layer_names_provider=self.layer_names_provider,
|
|
782
|
+
global_defs=self.global_defs,
|
|
783
|
+
state_machine=self.state_machine,
|
|
784
|
+
)
|
|
785
|
+
self.pre_group.changed.connect(self._on_changed)
|
|
786
|
+
layout.addWidget(self.pre_group)
|
|
787
|
+
|
|
788
|
+
self.post_group = _ActionGroup(
|
|
789
|
+
"Post (after transitions)", TRIGGER_POST, self.role_functions,
|
|
790
|
+
role_function_library=self.role_function_library,
|
|
791
|
+
literal_library=self.literal_library,
|
|
792
|
+
layer_names_provider=self.layer_names_provider,
|
|
793
|
+
global_defs=self.global_defs,
|
|
794
|
+
state_machine=self.state_machine,
|
|
795
|
+
)
|
|
796
|
+
self.post_group.changed.connect(self._on_changed)
|
|
797
|
+
layout.addWidget(self.post_group)
|
|
798
|
+
|
|
799
|
+
# ------------------------------------------------------------------
|
|
800
|
+
# Public API
|
|
801
|
+
# ------------------------------------------------------------------
|
|
802
|
+
def row_count(self) -> int:
|
|
803
|
+
"""Total rows across both groups (for compatibility)."""
|
|
804
|
+
return self.pre_group.row_count() + self.post_group.row_count()
|
|
805
|
+
|
|
806
|
+
def add_action(self, action: Optional[ActionStep] = None) -> int:
|
|
807
|
+
"""Add a row to the appropriate group. Returns the row index within that group."""
|
|
808
|
+
if action is None:
|
|
809
|
+
action = ActionStep(role_function="", trigger=TRIGGER_PRE)
|
|
810
|
+
trigger = _migrate_trigger(action.trigger)
|
|
811
|
+
target = self.pre_group if trigger == TRIGGER_PRE else self.post_group
|
|
812
|
+
target._append_row(action.role_function)
|
|
813
|
+
self._on_changed()
|
|
814
|
+
return target.row_count() - 1
|
|
815
|
+
|
|
816
|
+
def get_actions(self) -> List[ActionStep]:
|
|
817
|
+
result = []
|
|
818
|
+
result.extend(self.pre_group.get_actions())
|
|
819
|
+
result.extend(self.post_group.get_actions())
|
|
820
|
+
return result
|
|
821
|
+
|
|
822
|
+
def set_actions(self, actions: List[ActionStep]) -> None:
|
|
823
|
+
self.pre_group.set_actions(actions or [])
|
|
824
|
+
self.post_group.set_actions(actions or [])
|
|
825
|
+
|
|
826
|
+
# Legacy helpers (kept for test compatibility)
|
|
827
|
+
def delete_action(self, row: int) -> None:
|
|
828
|
+
# Interpret row as combined index; delete from the first group first
|
|
829
|
+
pre_count = self.pre_group.row_count()
|
|
830
|
+
if 0 <= row < pre_count:
|
|
831
|
+
self.pre_group.table.setCurrentCell(row, 0)
|
|
832
|
+
self.pre_group._on_delete()
|
|
833
|
+
else:
|
|
834
|
+
post_row = row - pre_count
|
|
835
|
+
if 0 <= post_row < self.post_group.row_count():
|
|
836
|
+
self.post_group.table.setCurrentCell(post_row, 0)
|
|
837
|
+
self.post_group._on_delete()
|
|
838
|
+
self._on_changed()
|
|
839
|
+
|
|
840
|
+
def move_up(self, row: int) -> int:
|
|
841
|
+
pre_count = self.pre_group.row_count()
|
|
842
|
+
if 0 <= row < pre_count:
|
|
843
|
+
self.pre_group.table.setCurrentCell(row, 0)
|
|
844
|
+
self.pre_group._on_move_up()
|
|
845
|
+
self._on_changed()
|
|
846
|
+
return max(0, row - 1)
|
|
847
|
+
post_row = row - pre_count
|
|
848
|
+
if 0 <= post_row < self.post_group.row_count():
|
|
849
|
+
self.post_group.table.setCurrentCell(post_row, 0)
|
|
850
|
+
self.post_group._on_move_up()
|
|
851
|
+
self._on_changed()
|
|
852
|
+
return row - 1 if post_row > 0 else row
|
|
853
|
+
return row
|
|
854
|
+
|
|
855
|
+
def _on_changed(self):
|
|
856
|
+
self.actions_changed.emit()
|