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,413 @@
|
|
|
1
|
+
# statable_gui/matrix_table.py
|
|
2
|
+
"""State transition table widget (D&D editor direct launch support / v2.2 multi-transition).
|
|
3
|
+
|
|
4
|
+
[R-6 change]
|
|
5
|
+
MatrixTableWidget now accepts `layer_names_provider` and forwards
|
|
6
|
+
it to ActionEditorDialog, completing the all-tabs namespace wiring.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from typing import List, Callable
|
|
10
|
+
|
|
11
|
+
from PySide6.QtCore import Qt, Signal
|
|
12
|
+
from PySide6.QtGui import QFont, QKeyEvent
|
|
13
|
+
from PySide6.QtWidgets import (
|
|
14
|
+
QTableWidget, QTableWidgetItem, QHeaderView, QAbstractItemView, QDialog
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
from statable.model import Transition, EventDeliveryType
|
|
18
|
+
from statable.state_machine import StateMachine
|
|
19
|
+
|
|
20
|
+
from .logger import StaTableLogger
|
|
21
|
+
from .config import MAX_COLUMN_WIDTH, MIN_ROW_HEIGHT, MAX_ROW_HEIGHT
|
|
22
|
+
from .global_defs import GlobalDefinitions
|
|
23
|
+
|
|
24
|
+
from .transition_editor_direct.dialog import ActionEditorDialog
|
|
25
|
+
from .state_actions_dialog import StateActionsDialog
|
|
26
|
+
from .transition_editor_direct.draft import (
|
|
27
|
+
ActionDraft, transition_to_flow_item, flow_item_to_transition,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
from statable_gui.libcntrl.role_function_library import RoleFunctionLibrary
|
|
31
|
+
from statable_gui.libcntrl.condition_library import ConditionLibrary
|
|
32
|
+
from statable_gui.libcntrl.literal_library import LiteralLibrary
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# ======================================================================
|
|
36
|
+
# Helpers
|
|
37
|
+
# ======================================================================
|
|
38
|
+
def _truncate_text(text: str, max_chars: int = 40) -> str:
|
|
39
|
+
if not text:
|
|
40
|
+
return ""
|
|
41
|
+
lines = text.split('\n')
|
|
42
|
+
first_line = lines[0].strip() if lines else ""
|
|
43
|
+
if len(first_line) > max_chars:
|
|
44
|
+
return first_line[:max_chars].rstrip() + "..."
|
|
45
|
+
if len(lines) > 1:
|
|
46
|
+
return first_line + " ..."
|
|
47
|
+
return first_line
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _mode_marker(trans: Transition) -> str:
|
|
51
|
+
"""Return the mode marker for a transition.
|
|
52
|
+
|
|
53
|
+
" [C]" -> Commit (early_return=True)
|
|
54
|
+
" [T]" -> Tentative (early_return=False)
|
|
55
|
+
"" -> not specified (backward compat)
|
|
56
|
+
"""
|
|
57
|
+
er = getattr(trans, 'early_return', None)
|
|
58
|
+
if er is True:
|
|
59
|
+
return " [C]"
|
|
60
|
+
if er is False:
|
|
61
|
+
return " [T]"
|
|
62
|
+
return ""
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _mode_label(trans: Transition) -> str:
|
|
66
|
+
"""Human-readable mode label for tooltips."""
|
|
67
|
+
er = getattr(trans, 'early_return', None)
|
|
68
|
+
if er is True:
|
|
69
|
+
return "Commit"
|
|
70
|
+
if er is False:
|
|
71
|
+
return "Tentative"
|
|
72
|
+
return "(unset)"
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _build_transition_tooltip(trans: Transition) -> str:
|
|
76
|
+
parts = []
|
|
77
|
+
parts.append(f"Title: {trans.title}")
|
|
78
|
+
parts.append(f"Target: {trans.target if trans.target else '(internal)'}")
|
|
79
|
+
if trans.event:
|
|
80
|
+
parts.append(f"Event: {trans.event}")
|
|
81
|
+
else:
|
|
82
|
+
parts.append("Event: Completion transition")
|
|
83
|
+
if trans.condition:
|
|
84
|
+
parts.append(f"State transition condition:\n{trans.condition}")
|
|
85
|
+
parts.append(f"Mode: {_mode_label(trans)}")
|
|
86
|
+
if getattr(trans, 'has_else', False):
|
|
87
|
+
else_t = getattr(trans, 'else_target', '') or '(not set)'
|
|
88
|
+
parts.append(f"else target: {else_t}")
|
|
89
|
+
lbl = getattr(trans, 'label', '')
|
|
90
|
+
if lbl:
|
|
91
|
+
parts.append(f"Label: {lbl}")
|
|
92
|
+
return "\n".join(parts)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _event_header_label(event_name: str, delivery_type) -> str:
|
|
96
|
+
if delivery_type == EventDeliveryType.QUEUE:
|
|
97
|
+
return f"[Q] {event_name}"
|
|
98
|
+
elif delivery_type == EventDeliveryType.DOUBLE:
|
|
99
|
+
return f"[D] {event_name}"
|
|
100
|
+
return event_name
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class MatrixTableWidget(QTableWidget):
|
|
104
|
+
transition_changed = Signal()
|
|
105
|
+
|
|
106
|
+
def __init__(self, sm: StateMachine, global_defs: GlobalDefinitions = None,
|
|
107
|
+
role_function_library: RoleFunctionLibrary = None,
|
|
108
|
+
condition_library: ConditionLibrary = None,
|
|
109
|
+
literal_library: LiteralLibrary = None,
|
|
110
|
+
layer_names_provider: Callable[[], List[str]] = None,
|
|
111
|
+
parent=None):
|
|
112
|
+
super().__init__(0, 0, parent)
|
|
113
|
+
self.sm = sm
|
|
114
|
+
self.global_defs = global_defs if global_defs else GlobalDefinitions()
|
|
115
|
+
|
|
116
|
+
self.role_function_library = role_function_library if role_function_library else RoleFunctionLibrary()
|
|
117
|
+
self.condition_library = condition_library if condition_library else ConditionLibrary()
|
|
118
|
+
self.literal_library = literal_library if literal_library else LiteralLibrary()
|
|
119
|
+
# [R-6] All-tabs namespace provider (forwarded to ActionEditorDialog).
|
|
120
|
+
self.layer_names_provider = layer_names_provider
|
|
121
|
+
|
|
122
|
+
StaTableLogger.debug(
|
|
123
|
+
f"MatrixTableWidget.__init__: roles={len(self.role_function_library.list_all())}, "
|
|
124
|
+
f"conditions={len(self.condition_library.list_all())}, "
|
|
125
|
+
f"literals={len(self.literal_library.list_all())}, "
|
|
126
|
+
f"has_lnp={layer_names_provider is not None}"
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
self.horizontalHeader().setSectionResizeMode(QHeaderView.Interactive)
|
|
130
|
+
self.verticalHeader().setSectionResizeMode(QHeaderView.Interactive)
|
|
131
|
+
self.horizontalHeader().setStretchLastSection(True)
|
|
132
|
+
self.verticalHeader().setMinimumWidth(120)
|
|
133
|
+
|
|
134
|
+
self.setSelectionMode(QAbstractItemView.SingleSelection)
|
|
135
|
+
self.setFocusPolicy(Qt.StrongFocus)
|
|
136
|
+
self.setEditTriggers(QAbstractItemView.NoEditTriggers)
|
|
137
|
+
self.cellDoubleClicked.connect(self.open_transition_dialog)
|
|
138
|
+
# [v2.7.1] Horizontal header (state name) double-click
|
|
139
|
+
# opens StateActionsDialog.
|
|
140
|
+
self.horizontalHeader().sectionDoubleClicked.connect(
|
|
141
|
+
self.open_state_actions_for_header)
|
|
142
|
+
self.setFont(QFont("Consolas", 10))
|
|
143
|
+
|
|
144
|
+
self.populate()
|
|
145
|
+
StaTableLogger.debug("MatrixTableWidget initialized")
|
|
146
|
+
|
|
147
|
+
def populate(self):
|
|
148
|
+
states = list(self.sm.states.keys())
|
|
149
|
+
events = list(self.sm.events.keys())
|
|
150
|
+
self.clear()
|
|
151
|
+
self.setRowCount(len(events))
|
|
152
|
+
self.setColumnCount(len(states))
|
|
153
|
+
self.setHorizontalHeaderLabels(states)
|
|
154
|
+
|
|
155
|
+
event_labels = []
|
|
156
|
+
for event_name in events:
|
|
157
|
+
event_obj = self.sm.events.get(event_name)
|
|
158
|
+
delivery = event_obj.delivery_type if event_obj else EventDeliveryType.DIRECT
|
|
159
|
+
event_labels.append(_event_header_label(
|
|
160
|
+
event_name if event_name else "Completion", delivery))
|
|
161
|
+
self.setVerticalHeaderLabels(event_labels)
|
|
162
|
+
|
|
163
|
+
for row, event in enumerate(events):
|
|
164
|
+
for col, state in enumerate(states):
|
|
165
|
+
trans_list = self._find_transitions(state, event)
|
|
166
|
+
if trans_list:
|
|
167
|
+
titles = [self._generate_cell_label(t, event) for t in trans_list]
|
|
168
|
+
display = "\n".join(titles)
|
|
169
|
+
item = QTableWidgetItem(display)
|
|
170
|
+
item.setData(Qt.UserRole, trans_list)
|
|
171
|
+
tooltips = [_build_transition_tooltip(t) for t in trans_list]
|
|
172
|
+
full_tooltip = "\n\n".join(tooltips)
|
|
173
|
+
item.setToolTip(full_tooltip)
|
|
174
|
+
self.setItem(row, col, item)
|
|
175
|
+
else:
|
|
176
|
+
item = QTableWidgetItem("")
|
|
177
|
+
item.setData(Qt.UserRole, [])
|
|
178
|
+
item.setToolTip(self.tr("TransitionNone"))
|
|
179
|
+
self.setItem(row, col, item)
|
|
180
|
+
|
|
181
|
+
self.resizeColumnsToContents()
|
|
182
|
+
self.resizeRowsToContents()
|
|
183
|
+
|
|
184
|
+
for col in range(self.columnCount()):
|
|
185
|
+
current_width = self.columnWidth(col)
|
|
186
|
+
if current_width > MAX_COLUMN_WIDTH:
|
|
187
|
+
self.setColumnWidth(col, MAX_COLUMN_WIDTH)
|
|
188
|
+
|
|
189
|
+
for row in range(self.rowCount()):
|
|
190
|
+
current_height = self.rowHeight(row)
|
|
191
|
+
if current_height < MIN_ROW_HEIGHT:
|
|
192
|
+
self.setRowHeight(row, MIN_ROW_HEIGHT)
|
|
193
|
+
elif current_height > MAX_ROW_HEIGHT:
|
|
194
|
+
self.setRowHeight(row, MAX_ROW_HEIGHT)
|
|
195
|
+
|
|
196
|
+
StaTableLogger.debug(
|
|
197
|
+
f"MatrixTable populated: {len(events)} events, {len(states)} states")
|
|
198
|
+
|
|
199
|
+
def _find_transitions(self, state: str, event: str) -> List[Transition]:
|
|
200
|
+
return self.sm.get_transitions_for_cell(state, event)
|
|
201
|
+
|
|
202
|
+
def _generate_cell_label(self, trans: Transition, event: str) -> str:
|
|
203
|
+
parts = []
|
|
204
|
+
if trans.title and trans.title != "(untitled transition)":
|
|
205
|
+
parts.append(trans.title)
|
|
206
|
+
else:
|
|
207
|
+
if trans.target:
|
|
208
|
+
parts.append(trans.target)
|
|
209
|
+
else:
|
|
210
|
+
parts.append("(internal)")
|
|
211
|
+
if event:
|
|
212
|
+
parts.append(f"({event})")
|
|
213
|
+
if trans.condition:
|
|
214
|
+
condition_display = _truncate_text(trans.condition, 30)
|
|
215
|
+
parts.append(f"[{condition_display}]")
|
|
216
|
+
|
|
217
|
+
marker = _mode_marker(trans)
|
|
218
|
+
if marker:
|
|
219
|
+
parts.append(marker.strip())
|
|
220
|
+
|
|
221
|
+
lbl = getattr(trans, 'label', '')
|
|
222
|
+
if lbl:
|
|
223
|
+
parts.append(f"<{lbl}>")
|
|
224
|
+
|
|
225
|
+
return " ".join(parts)
|
|
226
|
+
|
|
227
|
+
def _generate_title(self, trans: Transition) -> str:
|
|
228
|
+
parts = []
|
|
229
|
+
if trans.target:
|
|
230
|
+
parts.append(trans.target)
|
|
231
|
+
else:
|
|
232
|
+
parts.append("(internal)")
|
|
233
|
+
if trans.condition:
|
|
234
|
+
condition_display = _truncate_text(trans.condition, 30)
|
|
235
|
+
parts.append(f"[{condition_display}]")
|
|
236
|
+
marker = _mode_marker(trans)
|
|
237
|
+
if marker:
|
|
238
|
+
parts.append(marker.strip())
|
|
239
|
+
return " ".join(parts)
|
|
240
|
+
|
|
241
|
+
def open_transition_dialog(self, row: int, col: int):
|
|
242
|
+
"""Open the transition edit dialog."""
|
|
243
|
+
state = self.horizontalHeaderItem(col).text() if self.horizontalHeaderItem(col) else ""
|
|
244
|
+
raw_event = self.verticalHeaderItem(row).text() if self.verticalHeaderItem(row) else ""
|
|
245
|
+
event_name = raw_event
|
|
246
|
+
if event_name.startswith("[Q] "):
|
|
247
|
+
event_name = event_name[4:]
|
|
248
|
+
elif event_name.startswith("[D] "):
|
|
249
|
+
event_name = event_name[4:]
|
|
250
|
+
if event_name == "Completion":
|
|
251
|
+
event_name = ""
|
|
252
|
+
|
|
253
|
+
StaTableLogger.debug(f"=== MatrixTableWidget.open_transition_dialog ===")
|
|
254
|
+
StaTableLogger.debug(f"state={state}, event={event_name}")
|
|
255
|
+
|
|
256
|
+
existing_list = self._find_transitions(state, event_name)
|
|
257
|
+
StaTableLogger.debug(f"existing transitions count = {len(existing_list)}")
|
|
258
|
+
|
|
259
|
+
layer_name = getattr(self.sm, 'layer_name', '') or ''
|
|
260
|
+
|
|
261
|
+
draft = ActionDraft(
|
|
262
|
+
source=state,
|
|
263
|
+
event=event_name,
|
|
264
|
+
layer_name=layer_name,
|
|
265
|
+
)
|
|
266
|
+
for trans in existing_list:
|
|
267
|
+
fi = transition_to_flow_item(trans)
|
|
268
|
+
draft.flow_items.append(fi)
|
|
269
|
+
|
|
270
|
+
# === v2.2: populate cell_actions / cell_relations from SM ===
|
|
271
|
+
draft.cell_actions = list(
|
|
272
|
+
self.sm.get_actions_for_cell(state, event_name)
|
|
273
|
+
)
|
|
274
|
+
draft.cell_relations = list(
|
|
275
|
+
self.sm.get_relations_for_cell(state, event_name)
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
# Merge shared library and current SM role functions
|
|
279
|
+
role_func_names = []
|
|
280
|
+
_seen = set()
|
|
281
|
+
for rf in self.role_function_library.list_all():
|
|
282
|
+
qn = getattr(rf, 'qualified_name', None) or rf.name
|
|
283
|
+
if qn and qn not in _seen:
|
|
284
|
+
_seen.add(qn)
|
|
285
|
+
role_func_names.append(qn)
|
|
286
|
+
for rf in self.sm.role_functions.values():
|
|
287
|
+
qn = getattr(rf, 'qualified_name', None) or rf.name
|
|
288
|
+
if qn and qn not in _seen:
|
|
289
|
+
_seen.add(qn)
|
|
290
|
+
role_func_names.append(qn)
|
|
291
|
+
|
|
292
|
+
states = list(self.sm.states.keys())
|
|
293
|
+
|
|
294
|
+
StaTableLogger.debug(
|
|
295
|
+
f"open_transition_dialog: merged roles={len(role_func_names)} "
|
|
296
|
+
f"(library={len(self.role_function_library.list_all())}, "
|
|
297
|
+
f"sm={len(self.sm.role_functions)}), "
|
|
298
|
+
f"conditions={len(self.condition_library.list_all())}, "
|
|
299
|
+
f"literals={len(self.literal_library.list_all())}, "
|
|
300
|
+
f"has_lnp={self.layer_names_provider is not None}"
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
dialog = ActionEditorDialog(
|
|
304
|
+
draft,
|
|
305
|
+
role_functions=role_func_names,
|
|
306
|
+
transition_events=[event_name] if event_name else [],
|
|
307
|
+
states=states,
|
|
308
|
+
global_defs=self.global_defs,
|
|
309
|
+
state_machine=self.sm,
|
|
310
|
+
role_function_library=self.role_function_library,
|
|
311
|
+
condition_library=self.condition_library,
|
|
312
|
+
literal_library=self.literal_library,
|
|
313
|
+
# [R-6] Forward provider so TransitionsTab / ActionsTab
|
|
314
|
+
# inside the dialog list every layer in the project.
|
|
315
|
+
layer_names_provider=self.layer_names_provider,
|
|
316
|
+
parent=self
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
if dialog.exec() == QDialog.Accepted:
|
|
320
|
+
new_transitions = []
|
|
321
|
+
for item in draft.flow_items:
|
|
322
|
+
if item.item_type == "transition":
|
|
323
|
+
new_transitions.append(
|
|
324
|
+
flow_item_to_transition(item, state, event_name)
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
StaTableLogger.debug(
|
|
328
|
+
f" -> D&D editor accepted, {len(new_transitions)} transitions")
|
|
329
|
+
|
|
330
|
+
# === v2.2: write back cell metadata from draft ===
|
|
331
|
+
self.sm.set_actions_for_cell(state, event_name,
|
|
332
|
+
list(draft.cell_actions))
|
|
333
|
+
self.sm.set_relations_for_cell(state, event_name,
|
|
334
|
+
list(draft.cell_relations))
|
|
335
|
+
|
|
336
|
+
# Replace transitions in the cell
|
|
337
|
+
self.sm.transitions = [
|
|
338
|
+
t for t in self.sm.transitions
|
|
339
|
+
if not (t.source == state and t.event == event_name)
|
|
340
|
+
]
|
|
341
|
+
for trans in new_transitions:
|
|
342
|
+
self.sm.add_transition(trans)
|
|
343
|
+
|
|
344
|
+
self.populate()
|
|
345
|
+
self.transition_changed.emit()
|
|
346
|
+
StaTableLogger.info(
|
|
347
|
+
f"Transition updated: {state} -{event_name or 'Completion'}-> "
|
|
348
|
+
f"{len(new_transitions)} transition(s), "
|
|
349
|
+
f"actions={len(draft.cell_actions)}, "
|
|
350
|
+
f"relations={len(draft.cell_relations)}")
|
|
351
|
+
else:
|
|
352
|
+
StaTableLogger.debug(" -> D&D editor cancelled")
|
|
353
|
+
|
|
354
|
+
def open_state_actions_for_header(self, logical_index: int):
|
|
355
|
+
"""[v2.7.1] Horizontal header (state name) double-click.
|
|
356
|
+
|
|
357
|
+
Opens StateActionsDialog for the state at the given column.
|
|
358
|
+
Cell double-click (existing) still opens ActionEditorDialog.
|
|
359
|
+
"""
|
|
360
|
+
header_item = self.horizontalHeaderItem(logical_index)
|
|
361
|
+
if header_item is None:
|
|
362
|
+
return
|
|
363
|
+
state_name = header_item.text().strip()
|
|
364
|
+
if not state_name or state_name not in self.sm.states:
|
|
365
|
+
StaTableLogger.debug(
|
|
366
|
+
f"open_state_actions_for_header: unknown state "
|
|
367
|
+
f"'{state_name}' (col={logical_index})")
|
|
368
|
+
return
|
|
369
|
+
|
|
370
|
+
state = self.sm.states[state_name]
|
|
371
|
+
dlg = StateActionsDialog(
|
|
372
|
+
parent=self, state=state, state_machine=self.sm)
|
|
373
|
+
if dlg.exec() == QDialog.Accepted:
|
|
374
|
+
self.populate()
|
|
375
|
+
self.transition_changed.emit()
|
|
376
|
+
StaTableLogger.info(
|
|
377
|
+
f"StateActionsDialog applied for state '{state_name}' "
|
|
378
|
+
f"(entry={len(state.entry or [])}, "
|
|
379
|
+
f"exit={len(state.exit or [])}, "
|
|
380
|
+
f"do={len(state.do_actions or [])})")
|
|
381
|
+
|
|
382
|
+
def keyPressEvent(self, event: QKeyEvent):
|
|
383
|
+
if event.key() in (Qt.Key_Return, Qt.Key_Enter, Qt.Key_F2):
|
|
384
|
+
current = self.currentItem()
|
|
385
|
+
if current:
|
|
386
|
+
self.open_transition_dialog(current.row(), current.column())
|
|
387
|
+
return
|
|
388
|
+
elif event.key() in (Qt.Key_Delete, Qt.Key_Backspace):
|
|
389
|
+
current = self.currentItem()
|
|
390
|
+
if current:
|
|
391
|
+
trans_list = current.data(Qt.UserRole)
|
|
392
|
+
if trans_list:
|
|
393
|
+
for trans in trans_list:
|
|
394
|
+
self.sm.remove_transition(trans)
|
|
395
|
+
state = self.horizontalHeaderItem(
|
|
396
|
+
current.column()).text() if self.horizontalHeaderItem(current.column()) else ""
|
|
397
|
+
raw_event = self.verticalHeaderItem(
|
|
398
|
+
current.row()).text() if self.verticalHeaderItem(current.row()) else ""
|
|
399
|
+
event_name = raw_event
|
|
400
|
+
if event_name.startswith("[Q] "):
|
|
401
|
+
event_name = event_name[4:]
|
|
402
|
+
elif event_name.startswith("[D] "):
|
|
403
|
+
event_name = event_name[4:]
|
|
404
|
+
if event_name == "Completion":
|
|
405
|
+
event_name = ""
|
|
406
|
+
self.sm.remove_cell_metadata(state, event_name)
|
|
407
|
+
|
|
408
|
+
self.populate()
|
|
409
|
+
self.transition_changed.emit()
|
|
410
|
+
StaTableLogger.info(
|
|
411
|
+
f"Transition deleted: {len(trans_list)} transition(s)")
|
|
412
|
+
return
|
|
413
|
+
super().keyPressEvent(event)
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""StaTable preference definitions (key names and default values)\n\nTo add or change a setting, just add/edit an entry in\nPREFERENCE_DEFINITIONS in this file.\n"""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
# ----------------------------------------------------------------------
|
|
6
|
+
#Setting item definitions
|
|
7
|
+
# Adding an entry here is enough to enable attribute access from the Preferences class.
|
|
8
|
+
# ----------------------------------------------------------------------
|
|
9
|
+
PREFERENCE_DEFINITIONS = {
|
|
10
|
+
# Last used folder
|
|
11
|
+
"last_project_dir": str(Path.home()), # Project XML
|
|
12
|
+
"last_c_source_dir": str(Path.home()), # C source output dir
|
|
13
|
+
"last_spec_doc_dir": str(Path.home()), # Spec documents location
|
|
14
|
+
"last_export_dir": str(Path.home()), # Export dir
|
|
15
|
+
|
|
16
|
+
# ★ Event delivery settings
|
|
17
|
+
"auto_convert_isr_direct_to_double": True, # Automatically convert ISR-used DIRECT to DOUBLE
|
|
18
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Any, Optional
|
|
4
|
+
|
|
5
|
+
from .preference_keys import PREFERENCE_DEFINITIONS
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Preferences:
|
|
9
|
+
"""Store application settings in a JSON file (key names defined in a separate file)\n\n Keys registered in preference_keys.py's PREFERENCE_DEFINITIONS can be\n read/written via attribute access such as prefs.last_project_dir.\n """
|
|
10
|
+
|
|
11
|
+
DEFAULT_FILE = Path.home() / ".statable" / "preferences.json"
|
|
12
|
+
|
|
13
|
+
def __init__(self, filepath: Optional[Path] = None):
|
|
14
|
+
self.filepath = Path(filepath) if filepath else self.DEFAULT_FILE
|
|
15
|
+
self.data: dict = {}
|
|
16
|
+
self.load()
|
|
17
|
+
self._apply_defaults()
|
|
18
|
+
|
|
19
|
+
def _apply_defaults(self):
|
|
20
|
+
"""Set default values for keys that have not yet been saved"""
|
|
21
|
+
changed = False
|
|
22
|
+
for key, default in PREFERENCE_DEFINITIONS.items():
|
|
23
|
+
if key not in self.data:
|
|
24
|
+
self.data[key] = default
|
|
25
|
+
changed = True
|
|
26
|
+
if changed:
|
|
27
|
+
self.save()
|
|
28
|
+
|
|
29
|
+
def load(self):
|
|
30
|
+
"""Load settings from JSON file"""
|
|
31
|
+
if self.filepath.exists():
|
|
32
|
+
try:
|
|
33
|
+
with open(self.filepath, "r", encoding="utf-8") as f:
|
|
34
|
+
self.data = json.load(f)
|
|
35
|
+
except (json.JSONDecodeError, OSError):
|
|
36
|
+
self.data = {}
|
|
37
|
+
|
|
38
|
+
def save(self):
|
|
39
|
+
"""Save current settings to a JSON file"""
|
|
40
|
+
try:
|
|
41
|
+
self.filepath.parent.mkdir(parents=True, exist_ok=True)
|
|
42
|
+
with open(self.filepath, "w", encoding="utf-8") as f:
|
|
43
|
+
json.dump(self.data, f, indent=2, ensure_ascii=False)
|
|
44
|
+
except OSError:
|
|
45
|
+
#Ignore write failures
|
|
46
|
+
pass
|
|
47
|
+
|
|
48
|
+
def get(self, key: str, default: Any = None) -> Any:
|
|
49
|
+
"""Get setting by key name (legacy style)"""
|
|
50
|
+
return self.data.get(key, default)
|
|
51
|
+
|
|
52
|
+
def set(self, key: str, value: Any):
|
|
53
|
+
"""Update the setting value by key name and save immediately"""
|
|
54
|
+
self.data[key] = value
|
|
55
|
+
self.save()
|
|
56
|
+
|
|
57
|
+
# ------------------------------------------------------------------
|
|
58
|
+
# Attribute access (prefs.last_project_dir etc.)
|
|
59
|
+
# ------------------------------------------------------------------
|
|
60
|
+
def __getattr__(self, name: str) -> Any:
|
|
61
|
+
# If key is defined, return its value
|
|
62
|
+
if name in PREFERENCE_DEFINITIONS:
|
|
63
|
+
return self.data.get(name, PREFERENCE_DEFINITIONS[name])
|
|
64
|
+
# Undefined attributes raise a normal error
|
|
65
|
+
raise AttributeError(f"'Preferences' object has no attribute '{name}'")
|
|
66
|
+
|
|
67
|
+
def __setattr__(self, name: str, value: Any):
|
|
68
|
+
# If key is defined, apply and save the setting
|
|
69
|
+
if name in PREFERENCE_DEFINITIONS:
|
|
70
|
+
self.data[name] = value
|
|
71
|
+
self.save()
|
|
72
|
+
else:
|
|
73
|
+
#Set undefined ones as normal attributes
|
|
74
|
+
super().__setattr__(name, value)
|