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
statable_gui/config.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
# Window / preview size constants
|
|
5
|
+
WINDOW_WIDTH = 1800
|
|
6
|
+
WINDOW_HEIGHT = 1000
|
|
7
|
+
MERMAID_PREVIEW_MIN_HEIGHT = 400
|
|
8
|
+
TABLE_PREVIEW_RATIO = 0.65
|
|
9
|
+
MAX_COLUMN_WIDTH = 400
|
|
10
|
+
MIN_ROW_HEIGHT = 30
|
|
11
|
+
MAX_ROW_HEIGHT = 100
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def get_resource_path(filename: str = "") -> Path:
|
|
15
|
+
if getattr(sys, 'frozen', False):
|
|
16
|
+
base_path = Path(sys._MEIPASS) / "Resources"
|
|
17
|
+
else:
|
|
18
|
+
base_path = Path(__file__).resolve().parent.parent / "Resources"
|
|
19
|
+
if filename:
|
|
20
|
+
return base_path / filename
|
|
21
|
+
return base_path
|
statable_gui/dialogs.py
ADDED
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
# statable_gui/dialogs.py
|
|
2
|
+
"""Transition edit dialog (D&D edit support / v2.2 Mode column)."""
|
|
3
|
+
|
|
4
|
+
from typing import Optional, List
|
|
5
|
+
|
|
6
|
+
from PySide6.QtWidgets import (
|
|
7
|
+
QDialog, QVBoxLayout, QHBoxLayout, QTableWidget, QTableWidgetItem,
|
|
8
|
+
QPushButton, QLabel, QHeaderView, QComboBox, QDialogButtonBox,
|
|
9
|
+
QAbstractItemView, QMessageBox
|
|
10
|
+
)
|
|
11
|
+
from PySide6.QtGui import QFont, QMouseEvent
|
|
12
|
+
from PySide6.QtCore import Qt, Signal
|
|
13
|
+
|
|
14
|
+
from statable.model import Transition
|
|
15
|
+
from .global_defs import GlobalDefinitions
|
|
16
|
+
from .logger import StaTableLogger
|
|
17
|
+
|
|
18
|
+
from .transition_editor_direct.dialog import ActionEditorDialog
|
|
19
|
+
from .transition_editor_direct.draft import ActionDraft, FlowItem
|
|
20
|
+
|
|
21
|
+
from .condition_builder_dialog import ConditionBuilderDialog
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# ======================================================================
|
|
25
|
+
# Column constants (v2.2: Mode column inserted at index 5)
|
|
26
|
+
# ======================================================================
|
|
27
|
+
COL_TITLE = 0
|
|
28
|
+
COL_CONDITION = 1
|
|
29
|
+
COL_ACTION = 2
|
|
30
|
+
COL_TARGET = 3
|
|
31
|
+
COL_DISPLAY = 4
|
|
32
|
+
COL_MODE = 5 # v2.2
|
|
33
|
+
COLUMN_COUNT = 6
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class TransitionTable(QTableWidget):
|
|
37
|
+
"""Table that reliably captures double-click events."""
|
|
38
|
+
cell_double_clicked_any = Signal(int, int)
|
|
39
|
+
|
|
40
|
+
def __init__(self, *args, **kwargs):
|
|
41
|
+
super().__init__(*args, **kwargs)
|
|
42
|
+
self.setEditTriggers(QAbstractItemView.NoEditTriggers)
|
|
43
|
+
|
|
44
|
+
def mouseDoubleClickEvent(self, event: QMouseEvent):
|
|
45
|
+
pos = event.position().toPoint()
|
|
46
|
+
item = self.itemAt(pos)
|
|
47
|
+
if item:
|
|
48
|
+
row, col = item.row(), item.column()
|
|
49
|
+
StaTableLogger.debug(
|
|
50
|
+
f"TransitionTable.mouseDoubleClickEvent: row={row}, col={col}")
|
|
51
|
+
self.cell_double_clicked_any.emit(row, col)
|
|
52
|
+
else:
|
|
53
|
+
StaTableLogger.debug(
|
|
54
|
+
"TransitionTable.mouseDoubleClickEvent: no item at position")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class TransitionListDialog(QDialog):
|
|
58
|
+
"""Dialog to batch-edit multiple transitions in one cell.
|
|
59
|
+
|
|
60
|
+
[v2.2]
|
|
61
|
+
- Mode column (Commit / Tentative) added at index 5.
|
|
62
|
+
- Transition.early_return is preserved through row<->transition conversion.
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
def __init__(self, parent=None, state_names=None, event_name="",
|
|
66
|
+
existing_transitions=None, role_functions=None,
|
|
67
|
+
global_defs=None, state_machine=None):
|
|
68
|
+
super().__init__(parent)
|
|
69
|
+
self.setWindowTitle(self.tr("Transition edit (D&D visual editing)"))
|
|
70
|
+
self.setMinimumSize(1050, 600)
|
|
71
|
+
self.state_names = state_names or []
|
|
72
|
+
self.role_functions = role_functions or {}
|
|
73
|
+
self.global_defs = global_defs if global_defs else GlobalDefinitions()
|
|
74
|
+
self.event_name = event_name
|
|
75
|
+
self.state_machine = state_machine
|
|
76
|
+
|
|
77
|
+
layout = QVBoxLayout(self)
|
|
78
|
+
layout.addWidget(QLabel(
|
|
79
|
+
f"Event: {event_name if event_name else 'Completion transition'}"))
|
|
80
|
+
|
|
81
|
+
self.table = TransitionTable(0, COLUMN_COUNT)
|
|
82
|
+
self.table.setHorizontalHeaderLabels([
|
|
83
|
+
"Title",
|
|
84
|
+
"State transition condition",
|
|
85
|
+
"Action",
|
|
86
|
+
"Target",
|
|
87
|
+
"Display title",
|
|
88
|
+
"Mode", # v2.2
|
|
89
|
+
])
|
|
90
|
+
self.table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
|
|
91
|
+
self.table.setFont(QFont("Consolas", 10))
|
|
92
|
+
layout.addWidget(self.table)
|
|
93
|
+
|
|
94
|
+
self.table.cell_double_clicked_any.connect(self.on_cell_double_clicked)
|
|
95
|
+
self.table.itemChanged.connect(self.on_item_changed)
|
|
96
|
+
|
|
97
|
+
btn_layout = QHBoxLayout()
|
|
98
|
+
add_btn = QPushButton(self.tr("Add row"))
|
|
99
|
+
add_btn.clicked.connect(lambda: self.add_row())
|
|
100
|
+
del_btn = QPushButton(self.tr("Delete row"))
|
|
101
|
+
del_btn.clicked.connect(lambda: self.delete_row())
|
|
102
|
+
up_btn = QPushButton(self.tr("Move up"))
|
|
103
|
+
up_btn.clicked.connect(lambda: self.move_row_up())
|
|
104
|
+
down_btn = QPushButton(self.tr("Move down"))
|
|
105
|
+
down_btn.clicked.connect(lambda: self.move_row_down())
|
|
106
|
+
|
|
107
|
+
dnd_btn = QPushButton(self.tr("D&DEdit"))
|
|
108
|
+
dnd_btn.setToolTip(self.tr("Edit the selected row in the visual editor"))
|
|
109
|
+
dnd_btn.clicked.connect(self.open_dnd_editor)
|
|
110
|
+
|
|
111
|
+
btn_layout.addWidget(add_btn)
|
|
112
|
+
btn_layout.addWidget(del_btn)
|
|
113
|
+
btn_layout.addWidget(up_btn)
|
|
114
|
+
btn_layout.addWidget(down_btn)
|
|
115
|
+
btn_layout.addWidget(dnd_btn)
|
|
116
|
+
btn_layout.addStretch()
|
|
117
|
+
layout.addLayout(btn_layout)
|
|
118
|
+
|
|
119
|
+
buttons = QDialogButtonBox(
|
|
120
|
+
QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
|
|
121
|
+
buttons.accepted.connect(self._on_accept)
|
|
122
|
+
buttons.rejected.connect(self.reject)
|
|
123
|
+
layout.addWidget(buttons)
|
|
124
|
+
|
|
125
|
+
if existing_transitions:
|
|
126
|
+
for trans in existing_transitions:
|
|
127
|
+
self.add_row(trans)
|
|
128
|
+
else:
|
|
129
|
+
self.add_row()
|
|
130
|
+
|
|
131
|
+
StaTableLogger.debug("TransitionListDialog initialization completed")
|
|
132
|
+
|
|
133
|
+
# ------------------------------------------------------------------
|
|
134
|
+
# Double-click handling
|
|
135
|
+
# ------------------------------------------------------------------
|
|
136
|
+
def on_cell_double_clicked(self, row, col):
|
|
137
|
+
if col == COL_CONDITION:
|
|
138
|
+
self.open_condition_builder(row)
|
|
139
|
+
elif col == COL_ACTION or col == COL_DISPLAY:
|
|
140
|
+
self.open_dnd_editor_for_row(row)
|
|
141
|
+
# Mode column: not double-clickable (combo box)
|
|
142
|
+
# else: do nothing
|
|
143
|
+
|
|
144
|
+
def open_condition_builder(self, row):
|
|
145
|
+
item = self.table.item(row, COL_CONDITION)
|
|
146
|
+
if not item:
|
|
147
|
+
item = QTableWidgetItem("")
|
|
148
|
+
self.table.setItem(row, COL_CONDITION, item)
|
|
149
|
+
|
|
150
|
+
current_condition = (
|
|
151
|
+
item.data(Qt.UserRole) if item.data(Qt.UserRole) else "")
|
|
152
|
+
|
|
153
|
+
dlg = ConditionBuilderDialog(
|
|
154
|
+
condition=current_condition,
|
|
155
|
+
global_defs=self.global_defs,
|
|
156
|
+
state_machine=self.state_machine,
|
|
157
|
+
parent=self
|
|
158
|
+
)
|
|
159
|
+
if dlg.exec() == QDialog.Accepted:
|
|
160
|
+
new_condition = dlg.get_condition_text()
|
|
161
|
+
item.setText(new_condition.replace('\n', ' ; '))
|
|
162
|
+
item.setData(Qt.UserRole, new_condition)
|
|
163
|
+
self._update_display_title(row)
|
|
164
|
+
StaTableLogger.info(f"Condition updated for row {row}")
|
|
165
|
+
|
|
166
|
+
def open_dnd_editor_for_row(self, row):
|
|
167
|
+
self.table.setCurrentCell(row, 0)
|
|
168
|
+
self.open_dnd_editor()
|
|
169
|
+
|
|
170
|
+
# ------------------------------------------------------------------
|
|
171
|
+
# D&D Edit
|
|
172
|
+
# ------------------------------------------------------------------
|
|
173
|
+
def open_dnd_editor(self):
|
|
174
|
+
row = self.table.currentRow()
|
|
175
|
+
if row < 0:
|
|
176
|
+
QMessageBox.warning(self, self.tr("Warning"), self.tr("Please select a row to edit."))
|
|
177
|
+
return
|
|
178
|
+
|
|
179
|
+
trans = self._row_to_transition(row)
|
|
180
|
+
if trans is None:
|
|
181
|
+
return
|
|
182
|
+
|
|
183
|
+
draft = self._transition_to_draft(trans)
|
|
184
|
+
|
|
185
|
+
role_func_names = []
|
|
186
|
+
if isinstance(self.role_functions, dict):
|
|
187
|
+
role_func_names = list(self.role_functions.keys())
|
|
188
|
+
elif isinstance(self.role_functions, list):
|
|
189
|
+
role_func_names = self.role_functions
|
|
190
|
+
|
|
191
|
+
transition_events = [trans.event] if trans.event else []
|
|
192
|
+
states = self.state_names
|
|
193
|
+
|
|
194
|
+
dialog = ActionEditorDialog(
|
|
195
|
+
draft,
|
|
196
|
+
role_functions=role_func_names,
|
|
197
|
+
transition_events=transition_events,
|
|
198
|
+
states=states,
|
|
199
|
+
global_defs=self.global_defs,
|
|
200
|
+
state_machine=self.state_machine,
|
|
201
|
+
parent=self
|
|
202
|
+
)
|
|
203
|
+
if dialog.exec() == ActionEditorDialog.Accepted:
|
|
204
|
+
self._draft_to_transition(draft, trans)
|
|
205
|
+
self._update_row_from_transition(row, trans)
|
|
206
|
+
StaTableLogger.info(f"D&D editing completed for row {row}")
|
|
207
|
+
|
|
208
|
+
def _transition_to_draft(self, trans: Transition) -> ActionDraft:
|
|
209
|
+
layer_name = getattr(self.state_machine, 'layer_name', '') or ''
|
|
210
|
+
|
|
211
|
+
draft = ActionDraft(
|
|
212
|
+
source=trans.source,
|
|
213
|
+
event=trans.event,
|
|
214
|
+
layer_name=layer_name,
|
|
215
|
+
)
|
|
216
|
+
flow_item = FlowItem(
|
|
217
|
+
item_type="transition",
|
|
218
|
+
name=trans.event or "Completion",
|
|
219
|
+
edited_text=(
|
|
220
|
+
trans.title
|
|
221
|
+
if trans.title != "(untitled transition)"
|
|
222
|
+
else trans.event or "Completion"
|
|
223
|
+
),
|
|
224
|
+
params={
|
|
225
|
+
"event": trans.event,
|
|
226
|
+
"condition": trans.condition,
|
|
227
|
+
"pre_actions": getattr(trans, 'pre_actions', []),
|
|
228
|
+
"target": trans.target,
|
|
229
|
+
"has_else": getattr(trans, 'has_else', True),
|
|
230
|
+
"else_target": getattr(trans, 'else_target', ''),
|
|
231
|
+
"else_actions": getattr(trans, 'else_actions', []),
|
|
232
|
+
"early_return": getattr(trans, 'early_return', False), # v2.2
|
|
233
|
+
"label": getattr(trans, 'label', ''), # v2.2
|
|
234
|
+
}
|
|
235
|
+
)
|
|
236
|
+
draft.flow_items.append(flow_item)
|
|
237
|
+
return draft
|
|
238
|
+
|
|
239
|
+
def _draft_to_transition(self, draft: ActionDraft, trans: Transition) -> None:
|
|
240
|
+
for item in draft.flow_items:
|
|
241
|
+
if item.item_type == "transition":
|
|
242
|
+
params = item.params
|
|
243
|
+
trans.condition = params.get('condition', '')
|
|
244
|
+
trans.pre_actions = params.get('pre_actions', [])
|
|
245
|
+
trans.target = params.get('target', '')
|
|
246
|
+
trans.has_else = params.get('has_else', True)
|
|
247
|
+
trans.else_target = params.get('else_target', '')
|
|
248
|
+
trans.else_actions = params.get('else_actions', [])
|
|
249
|
+
trans.early_return = params.get('early_return',
|
|
250
|
+
getattr(trans, 'early_return', False))
|
|
251
|
+
if params.get('label'):
|
|
252
|
+
trans.label = params['label']
|
|
253
|
+
if item.edited_text and item.edited_text != item.name:
|
|
254
|
+
trans.title = item.edited_text
|
|
255
|
+
break
|
|
256
|
+
|
|
257
|
+
def _row_to_transition(self, row: int) -> Optional[Transition]:
|
|
258
|
+
if row >= self.table.rowCount():
|
|
259
|
+
return None
|
|
260
|
+
title_item = self.table.item(row, COL_TITLE)
|
|
261
|
+
title = title_item.text().strip() if title_item else "(untitled transition)"
|
|
262
|
+
|
|
263
|
+
condition_item = self.table.item(row, COL_CONDITION)
|
|
264
|
+
condition = (condition_item.data(Qt.UserRole)
|
|
265
|
+
if condition_item else "")
|
|
266
|
+
|
|
267
|
+
target_widget = self.table.cellWidget(row, COL_TARGET)
|
|
268
|
+
target = target_widget.currentText().strip() if target_widget else ""
|
|
269
|
+
|
|
270
|
+
# v2.2: Mode
|
|
271
|
+
mode_widget = self.table.cellWidget(row, COL_MODE)
|
|
272
|
+
early_return = False
|
|
273
|
+
if mode_widget is not None:
|
|
274
|
+
early_return = (mode_widget.currentText().strip() == "Commit")
|
|
275
|
+
|
|
276
|
+
return Transition(
|
|
277
|
+
source="", event=self.event_name, condition=condition, action="",
|
|
278
|
+
target=target, transition_type="external", title=title,
|
|
279
|
+
early_return=early_return,
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
def _update_row_from_transition(self, row: int, trans: Transition):
|
|
283
|
+
title_item = self.table.item(row, COL_TITLE)
|
|
284
|
+
if title_item:
|
|
285
|
+
title_item.setText(trans.title)
|
|
286
|
+
|
|
287
|
+
cond_item = self.table.item(row, COL_CONDITION)
|
|
288
|
+
if cond_item:
|
|
289
|
+
cond_item.setText(trans.condition.replace('\n', ' ; '))
|
|
290
|
+
cond_item.setData(Qt.UserRole, trans.condition)
|
|
291
|
+
|
|
292
|
+
action_item = self.table.item(row, COL_ACTION)
|
|
293
|
+
if action_item:
|
|
294
|
+
action_item.setText(self.tr(""))
|
|
295
|
+
action_item.setData(Qt.UserRole, "")
|
|
296
|
+
|
|
297
|
+
target_widget = self.table.cellWidget(row, COL_TARGET)
|
|
298
|
+
if target_widget:
|
|
299
|
+
idx = target_widget.findText(trans.target)
|
|
300
|
+
if idx >= 0:
|
|
301
|
+
target_widget.setCurrentIndex(idx)
|
|
302
|
+
|
|
303
|
+
# v2.2: Mode
|
|
304
|
+
mode_widget = self.table.cellWidget(row, COL_MODE)
|
|
305
|
+
if mode_widget is not None:
|
|
306
|
+
mode_widget.setCurrentText(
|
|
307
|
+
"Commit" if getattr(trans, 'early_return', False)
|
|
308
|
+
else "Tentative"
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
self._update_display_title(row)
|
|
312
|
+
|
|
313
|
+
# ------------------------------------------------------------------
|
|
314
|
+
# Row management
|
|
315
|
+
# ------------------------------------------------------------------
|
|
316
|
+
def on_item_changed(self, item):
|
|
317
|
+
if item.column() == COL_TITLE:
|
|
318
|
+
row = item.row()
|
|
319
|
+
display_item = self.table.item(row, COL_DISPLAY)
|
|
320
|
+
if display_item:
|
|
321
|
+
display_item.setText(item.text())
|
|
322
|
+
|
|
323
|
+
def _update_display_title(self, row: int):
|
|
324
|
+
title_item = self.table.item(row, COL_TITLE)
|
|
325
|
+
display_item = self.table.item(row, COL_DISPLAY)
|
|
326
|
+
if title_item and display_item:
|
|
327
|
+
display_item.setText(title_item.text())
|
|
328
|
+
|
|
329
|
+
def add_row(self, trans: Optional[Transition] = None):
|
|
330
|
+
row = self.table.rowCount()
|
|
331
|
+
self.table.insertRow(row)
|
|
332
|
+
|
|
333
|
+
# 0: Title
|
|
334
|
+
title_text = trans.title if trans else "(untitled transition)"
|
|
335
|
+
title_item = QTableWidgetItem(title_text)
|
|
336
|
+
title_item.setToolTip(self.tr("Title of this transition. Can be edited directly."))
|
|
337
|
+
self.table.setItem(row, COL_TITLE, title_item)
|
|
338
|
+
|
|
339
|
+
# 1: Condition
|
|
340
|
+
cond_item = QTableWidgetItem(trans.condition if trans else "")
|
|
341
|
+
cond_item.setToolTip(self.tr("Double-click to open condition builder"))
|
|
342
|
+
cond_item.setData(Qt.UserRole, trans.condition if trans else "")
|
|
343
|
+
self.table.setItem(row, COL_CONDITION, cond_item)
|
|
344
|
+
|
|
345
|
+
# 2: Action
|
|
346
|
+
action_text = trans.action if trans else ""
|
|
347
|
+
action_item = QTableWidgetItem(action_text.replace('\n', ' ; '))
|
|
348
|
+
action_item.setToolTip(self.tr("Double-click for D&D editing"))
|
|
349
|
+
action_item.setData(Qt.UserRole, action_text)
|
|
350
|
+
self.table.setItem(row, COL_ACTION, action_item)
|
|
351
|
+
|
|
352
|
+
# 3: Target
|
|
353
|
+
target_combo = QComboBox()
|
|
354
|
+
target_combo.addItem(self.tr(""))
|
|
355
|
+
target_combo.addItems(self.state_names)
|
|
356
|
+
if trans and trans.target:
|
|
357
|
+
idx = target_combo.findText(trans.target)
|
|
358
|
+
if idx >= 0:
|
|
359
|
+
target_combo.setCurrentIndex(idx)
|
|
360
|
+
self.table.setCellWidget(row, COL_TARGET, target_combo)
|
|
361
|
+
|
|
362
|
+
# 4: Display title
|
|
363
|
+
display_title = self._generate_display_title(trans) if trans else ""
|
|
364
|
+
display_item = QTableWidgetItem(display_title)
|
|
365
|
+
display_item.setFlags(display_item.flags() & ~Qt.ItemIsEditable)
|
|
366
|
+
display_item.setToolTip(self.tr("Short label shown in state transition table"))
|
|
367
|
+
self.table.setItem(row, COL_DISPLAY, display_item)
|
|
368
|
+
|
|
369
|
+
# 5: Mode (v2.2)
|
|
370
|
+
mode_combo = QComboBox()
|
|
371
|
+
mode_combo.addItems(["Commit", "Tentative"])
|
|
372
|
+
if trans is not None and getattr(trans, 'early_return', False):
|
|
373
|
+
mode_combo.setCurrentText("Commit")
|
|
374
|
+
else:
|
|
375
|
+
mode_combo.setCurrentText("Tentative")
|
|
376
|
+
mode_combo.setToolTip(
|
|
377
|
+
self.tr("Commit: stop evaluating later transitions (no overwrite)\n"
|
|
378
|
+
"Tentative: later transitions may overwrite the target"))
|
|
379
|
+
self.table.setCellWidget(row, COL_MODE, mode_combo)
|
|
380
|
+
|
|
381
|
+
def delete_row(self):
|
|
382
|
+
row = self.table.currentRow()
|
|
383
|
+
if row >= 0:
|
|
384
|
+
self.table.removeRow(row)
|
|
385
|
+
|
|
386
|
+
def move_row_up(self):
|
|
387
|
+
row = self.table.currentRow()
|
|
388
|
+
if row > 0:
|
|
389
|
+
self._swap_rows(row, row - 1)
|
|
390
|
+
|
|
391
|
+
def move_row_down(self):
|
|
392
|
+
row = self.table.currentRow()
|
|
393
|
+
if row >= 0 and row < self.table.rowCount() - 1:
|
|
394
|
+
self._swap_rows(row, row + 1)
|
|
395
|
+
|
|
396
|
+
def _swap_rows(self, row1: int, row2: int):
|
|
397
|
+
# For widget columns (Target / Mode), swap values, not widgets
|
|
398
|
+
widget_cols = (COL_TARGET, COL_MODE)
|
|
399
|
+
for col in range(self.table.columnCount()):
|
|
400
|
+
if col in widget_cols:
|
|
401
|
+
w1 = self.table.cellWidget(row1, col)
|
|
402
|
+
w2 = self.table.cellWidget(row2, col)
|
|
403
|
+
if w1 is not None and w2 is not None:
|
|
404
|
+
text1 = w1.currentText()
|
|
405
|
+
text2 = w2.currentText()
|
|
406
|
+
w1.setCurrentText(text2)
|
|
407
|
+
w2.setCurrentText(text1)
|
|
408
|
+
else:
|
|
409
|
+
item1 = self.table.takeItem(row1, col)
|
|
410
|
+
item2 = self.table.takeItem(row2, col)
|
|
411
|
+
self.table.setItem(row1, col, item2)
|
|
412
|
+
self.table.setItem(row2, col, item1)
|
|
413
|
+
|
|
414
|
+
def _generate_display_title(self, trans: Optional[Transition]) -> str:
|
|
415
|
+
if not trans:
|
|
416
|
+
return ""
|
|
417
|
+
if trans.title and trans.title != "(untitled transition)":
|
|
418
|
+
return trans.title
|
|
419
|
+
parts = [trans.target] if trans.target else ["(internal)"]
|
|
420
|
+
if trans.condition:
|
|
421
|
+
condition_display = trans.condition.replace('\n', ' ; ')
|
|
422
|
+
parts.append(f"[{condition_display[:30]}]")
|
|
423
|
+
return " ".join(parts)
|
|
424
|
+
|
|
425
|
+
def _on_accept(self):
|
|
426
|
+
for row in range(self.table.rowCount()):
|
|
427
|
+
title_item = self.table.item(row, COL_TITLE)
|
|
428
|
+
if title_item and not title_item.text().strip():
|
|
429
|
+
title_item.setText(self.tr("(untitled transition)"))
|
|
430
|
+
self._update_display_title(row)
|
|
431
|
+
self.accept()
|
|
432
|
+
|
|
433
|
+
def get_transitions(self) -> List[Transition]:
|
|
434
|
+
transitions = []
|
|
435
|
+
for row in range(self.table.rowCount()):
|
|
436
|
+
trans = self._row_to_transition(row)
|
|
437
|
+
if trans:
|
|
438
|
+
transitions.append(trans)
|
|
439
|
+
return transitions
|