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.
Files changed (117) hide show
  1. codegen/__init__.py +38 -0
  2. codegen/c_code_generator.py +1752 -0
  3. codegen/code_merger.py +371 -0
  4. codegen/code_templates.py +509 -0
  5. codegen/config.py +167 -0
  6. codegen/enum_generator.py +377 -0
  7. codegen/event_queue_generator.py +303 -0
  8. codegen/interrupt_generator.py +345 -0
  9. codegen/naming_convention.py +171 -0
  10. codegen/osal_generator.py +282 -0
  11. codegen/role_function_generator.py +1244 -0
  12. codegen/sample_data.py +269 -0
  13. codegen/state_actions_generator.py +395 -0
  14. codegen/struct_generator.py +721 -0
  15. codegen/timer_generator.py +299 -0
  16. codegen/transition_generator.py +907 -0
  17. codegen/type_mapper.py +69 -0
  18. codegen/validate/__init__.py +38 -0
  19. codegen/validate/change_actions.py +90 -0
  20. codegen/validate/change_applier.py +416 -0
  21. codegen/validate/clipboard_manager.py +47 -0
  22. codegen/validate/data/__init__.py +30 -0
  23. codegen/validate/data/action_definitions.py +200 -0
  24. codegen/validate/data/keywords.py +46 -0
  25. codegen/validate/data/prompt_templates.py +176 -0
  26. codegen/validate/data/validation_rules.py +205 -0
  27. codegen/validate/items/__init__.py +30 -0
  28. codegen/validate/items/base_validator.py +25 -0
  29. codegen/validate/items/cell_validator.py +275 -0
  30. codegen/validate/items/custom_type_validator.py +75 -0
  31. codegen/validate/items/event_validator.py +78 -0
  32. codegen/validate/items/flag_validator.py +74 -0
  33. codegen/validate/items/interrupt_validator.py +75 -0
  34. codegen/validate/items/queue_validator.py +73 -0
  35. codegen/validate/items/role_function_validator.py +175 -0
  36. codegen/validate/items/state_validator.py +88 -0
  37. codegen/validate/items/timer_validator.py +120 -0
  38. codegen/validate/items/transition_validator.py +113 -0
  39. codegen/validate/items/variable_validator.py +132 -0
  40. codegen/validate/logger.py +52 -0
  41. codegen/validate/models.py +177 -0
  42. codegen/validate/prompt_generator.py +724 -0
  43. codegen/validate/response_parser.py +154 -0
  44. codegen/validate/response_validator.py +275 -0
  45. codegen/validate/validation_dialog.py +322 -0
  46. codegen/validate/validator.py +94 -0
  47. codegen/variable_generator.py +419 -0
  48. statable/__init__.py +71 -0
  49. statable/__main__.py +49 -0
  50. statable/cli.py +251 -0
  51. statable/global_defs.py +256 -0
  52. statable/mermaid_gen.py +144 -0
  53. statable/model.py +426 -0
  54. statable/parser.py +64 -0
  55. statable/sample_data.py +435 -0
  56. statable/shared/__init__.py +24 -0
  57. statable/shared/condition_library.py +63 -0
  58. statable/shared/literal_library.py +66 -0
  59. statable/shared/role_function_library.py +150 -0
  60. statable/state_machine.py +121 -0
  61. statable/xml_io.py +1164 -0
  62. statable-3.1.0.dist-info/METADATA +151 -0
  63. statable-3.1.0.dist-info/RECORD +117 -0
  64. statable-3.1.0.dist-info/WHEEL +5 -0
  65. statable-3.1.0.dist-info/entry_points.txt +2 -0
  66. statable-3.1.0.dist-info/top_level.txt +3 -0
  67. statable_gui/__init__.py +23 -0
  68. statable_gui/action_edit_dialog.py +214 -0
  69. statable_gui/code_generation_dialog.py +486 -0
  70. statable_gui/code_generation_settings_dialog.py +522 -0
  71. statable_gui/common_widgets.py +537 -0
  72. statable_gui/condition_builder_dialog.py +500 -0
  73. statable_gui/condition_edit_dialog.py +123 -0
  74. statable_gui/condition_template_dialog.py +58 -0
  75. statable_gui/config.py +21 -0
  76. statable_gui/dialogs.py +439 -0
  77. statable_gui/event_definition_dialog.py +605 -0
  78. statable_gui/event_delivery_settings_dialog.py +191 -0
  79. statable_gui/event_queue_dialog.py +245 -0
  80. statable_gui/global_defs.py +167 -0
  81. statable_gui/global_defs_dialog.py +796 -0
  82. statable_gui/i18n/__init__.py +118 -0
  83. statable_gui/interrupt_handler_edit_dialog.py +810 -0
  84. statable_gui/layer_settings_dialog.py +180 -0
  85. statable_gui/libcntrl/__init__.py +23 -0
  86. statable_gui/libcntrl/condition_library.py +13 -0
  87. statable_gui/libcntrl/literal_library.py +13 -0
  88. statable_gui/libcntrl/literal_management_dialog.py +185 -0
  89. statable_gui/libcntrl/role_function_edit_dialog.py +145 -0
  90. statable_gui/libcntrl/role_function_library.py +13 -0
  91. statable_gui/literal_definition_dialog.py +62 -0
  92. statable_gui/logger.py +83 -0
  93. statable_gui/main_window.py +1380 -0
  94. statable_gui/matrix_table.py +413 -0
  95. statable_gui/preference_keys.py +18 -0
  96. statable_gui/preferences.py +74 -0
  97. statable_gui/role_function_dialog.py +244 -0
  98. statable_gui/sample_data.py +11 -0
  99. statable_gui/state_actions_dialog.py +398 -0
  100. statable_gui/symbol_picker.py +170 -0
  101. statable_gui/traceball.py +37 -0
  102. statable_gui/transition_editor_direct/__init__.py +15 -0
  103. statable_gui/transition_editor_direct/actions_tab.py +856 -0
  104. statable_gui/transition_editor_direct/code_widget.py +344 -0
  105. statable_gui/transition_editor_direct/coverage_analyzer.py +163 -0
  106. statable_gui/transition_editor_direct/dialog.py +335 -0
  107. statable_gui/transition_editor_direct/draft.py +270 -0
  108. statable_gui/transition_editor_direct/edit_dialogs.py +160 -0
  109. statable_gui/transition_editor_direct/overview_tab.py +173 -0
  110. statable_gui/transition_editor_direct/palette_widget.py +233 -0
  111. statable_gui/transition_editor_direct/relations_edit_dialog.py +156 -0
  112. statable_gui/transition_editor_direct/relations_tab.py +230 -0
  113. statable_gui/transition_editor_direct/system_global_dialog.py +52 -0
  114. statable_gui/transition_editor_direct/transition_actions_dialog.py +109 -0
  115. statable_gui/transition_editor_direct/transitions_tab.py +620 -0
  116. statable_gui/validation_dialog.py +285 -0
  117. 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
@@ -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