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,335 @@
|
|
|
1
|
+
# statable_gui/transition_editor_direct/dialog.py
|
|
2
|
+
"""Action edit main dialog (v2.2: 5-tab structure).
|
|
3
|
+
|
|
4
|
+
Tabs:
|
|
5
|
+
- Transitions : ordered list of transitions
|
|
6
|
+
- Pre / Post Actions: cell-level actions (before / after transitions)
|
|
7
|
+
- Relations : relations between transitions
|
|
8
|
+
- Overview : coverage / reachability report
|
|
9
|
+
- Preview : generated C code
|
|
10
|
+
|
|
11
|
+
[v2.2 fix]
|
|
12
|
+
- _load_draft() order: Actions -> Relations -> Transitions (last)
|
|
13
|
+
to prevent intermediate signals from wiping cell_actions /
|
|
14
|
+
cell_relations before they are loaded.
|
|
15
|
+
- _loading flag suppresses _on_content_changed during initial load.
|
|
16
|
+
- _sync_member_details() propagates {label: "cond -> target"} to
|
|
17
|
+
RelationsTab so the member list shows identifiable entries.
|
|
18
|
+
|
|
19
|
+
[v2.5 change]
|
|
20
|
+
- TransitionsTab and ActionsTab now receive `role_function_library`
|
|
21
|
+
and `literal_library` from this dialog.
|
|
22
|
+
|
|
23
|
+
[R-6 change]
|
|
24
|
+
- ActionEditorDialog now accepts `layer_names_provider` and forwards
|
|
25
|
+
it to TransitionsTab and ActionsTab. This completes the 4-hop
|
|
26
|
+
wiring:
|
|
27
|
+
MainWindow → StateMachineTab → MatrixTableWidget
|
|
28
|
+
→ ActionEditorDialog → TransitionsTab / ActionsTab
|
|
29
|
+
so the namespace combo box on those tabs lists every layer in
|
|
30
|
+
the project, not just the current tab's layer.
|
|
31
|
+
- Previously this dialog intentionally did NOT forward the
|
|
32
|
+
provider (v2.5 scope). R-6 completes the wiring.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
import logging
|
|
36
|
+
import sys
|
|
37
|
+
import os
|
|
38
|
+
from typing import List, Callable
|
|
39
|
+
|
|
40
|
+
current_dir = os.path.dirname(os.path.abspath(__file__))
|
|
41
|
+
parent_dir = os.path.dirname(current_dir)
|
|
42
|
+
if parent_dir not in sys.path:
|
|
43
|
+
sys.path.insert(0, parent_dir)
|
|
44
|
+
|
|
45
|
+
from PySide6.QtWidgets import (
|
|
46
|
+
QDialog, QVBoxLayout, QHBoxLayout, QPushButton, QTabWidget,
|
|
47
|
+
QWidget, QMessageBox, QToolBar, QLabel,
|
|
48
|
+
)
|
|
49
|
+
from PySide6.QtCore import Qt
|
|
50
|
+
|
|
51
|
+
from .draft import (
|
|
52
|
+
ActionDraft, FlowItem, transition_to_flow_item, flow_item_to_transition,
|
|
53
|
+
)
|
|
54
|
+
from .transitions_tab import TransitionsTab
|
|
55
|
+
from .actions_tab import ActionsTab
|
|
56
|
+
from .relations_tab import RelationsTab
|
|
57
|
+
from .overview_tab import OverviewTab
|
|
58
|
+
from .code_widget import CodeWidget
|
|
59
|
+
|
|
60
|
+
from statable_gui.libcntrl.role_function_library import (
|
|
61
|
+
RoleFunctionLibrary, RoleFunction,
|
|
62
|
+
)
|
|
63
|
+
from statable_gui.libcntrl.condition_library import (
|
|
64
|
+
ConditionLibrary, ConditionTemplate,
|
|
65
|
+
)
|
|
66
|
+
from statable_gui.libcntrl.literal_library import LiteralLibrary
|
|
67
|
+
|
|
68
|
+
logger = logging.getLogger("transition_editor_direct.dialog")
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class ActionEditorDialog(QDialog):
|
|
72
|
+
"""Main dialog for editing a transition cell (v2.2 / 5 tabs)."""
|
|
73
|
+
|
|
74
|
+
TAB_NAMES = ("Transitions", "Pre / Post Actions",
|
|
75
|
+
"Relations", "Overview", "Preview")
|
|
76
|
+
|
|
77
|
+
def __init__(self, draft: ActionDraft,
|
|
78
|
+
role_functions=None, transition_events=None,
|
|
79
|
+
states=None, global_defs=None, state_machine=None,
|
|
80
|
+
role_function_library=None, condition_library=None,
|
|
81
|
+
literal_library=None,
|
|
82
|
+
layer_names_provider: Callable[[], List[str]] = None,
|
|
83
|
+
parent=None):
|
|
84
|
+
super().__init__(parent)
|
|
85
|
+
self.draft = draft
|
|
86
|
+
self.role_functions = role_functions or []
|
|
87
|
+
self.states = states or []
|
|
88
|
+
self.global_defs = global_defs
|
|
89
|
+
self.state_machine = state_machine
|
|
90
|
+
|
|
91
|
+
self.role_function_library = (
|
|
92
|
+
role_function_library if role_function_library
|
|
93
|
+
else RoleFunctionLibrary()
|
|
94
|
+
)
|
|
95
|
+
self.condition_library = (
|
|
96
|
+
condition_library if condition_library
|
|
97
|
+
else ConditionLibrary()
|
|
98
|
+
)
|
|
99
|
+
self.literal_library = (
|
|
100
|
+
literal_library if literal_library
|
|
101
|
+
else LiteralLibrary()
|
|
102
|
+
)
|
|
103
|
+
# [R-6] All-tabs namespace provider (forwarded to tabs).
|
|
104
|
+
self.layer_names_provider = layer_names_provider
|
|
105
|
+
|
|
106
|
+
# v2.2 fix: guard flag for _load_draft
|
|
107
|
+
self._loading = False
|
|
108
|
+
|
|
109
|
+
logger.debug("=== ActionEditorDialog init ===")
|
|
110
|
+
logger.debug(f"source={draft.source}, event={draft.event}")
|
|
111
|
+
logger.debug(
|
|
112
|
+
f"has_layer_names_provider="
|
|
113
|
+
f"{layer_names_provider is not None}")
|
|
114
|
+
|
|
115
|
+
self.setWindowTitle(
|
|
116
|
+
f"Action edit: {draft.source} --[{draft.event}]--> ?")
|
|
117
|
+
self.setMinimumSize(1000, 700)
|
|
118
|
+
|
|
119
|
+
self._setup_ui()
|
|
120
|
+
self._load_draft()
|
|
121
|
+
|
|
122
|
+
logger.debug("=== ActionEditorDialog init end ===")
|
|
123
|
+
|
|
124
|
+
# ------------------------------------------------------------------
|
|
125
|
+
# UI
|
|
126
|
+
# ------------------------------------------------------------------
|
|
127
|
+
def _setup_ui(self):
|
|
128
|
+
main_layout = QVBoxLayout(self)
|
|
129
|
+
|
|
130
|
+
toolbar = QToolBar()
|
|
131
|
+
refresh_btn = QPushButton(self.tr("Refresh preview"))
|
|
132
|
+
refresh_btn.clicked.connect(self._on_refresh_preview)
|
|
133
|
+
toolbar.addWidget(refresh_btn)
|
|
134
|
+
main_layout.addWidget(toolbar)
|
|
135
|
+
|
|
136
|
+
self.tabs = QTabWidget()
|
|
137
|
+
main_layout.addWidget(self.tabs)
|
|
138
|
+
|
|
139
|
+
# ==============================================================
|
|
140
|
+
# Tab 1: Transitions
|
|
141
|
+
# ==============================================================
|
|
142
|
+
# [v2.5] Pass shared libraries so "+ New Role Function" on this
|
|
143
|
+
# tab can build the same RoleFunctionDialog kwargs as
|
|
144
|
+
# SettingsPanel (global_vars / events / literals /
|
|
145
|
+
# namespace_choices).
|
|
146
|
+
# [R-6] Forward layer_names_provider so namespace_choices
|
|
147
|
+
# include every layer in the project.
|
|
148
|
+
self.transitions_tab = TransitionsTab(
|
|
149
|
+
self.draft,
|
|
150
|
+
states=self.states,
|
|
151
|
+
global_defs=self.global_defs,
|
|
152
|
+
state_machine=self.state_machine,
|
|
153
|
+
role_function_library=self.role_function_library,
|
|
154
|
+
literal_library=self.literal_library,
|
|
155
|
+
condition_library=self.condition_library,
|
|
156
|
+
layer_names_provider=self.layer_names_provider,
|
|
157
|
+
)
|
|
158
|
+
# v2.2: provide role function candidates to the TransitionsTab
|
|
159
|
+
self.transitions_tab.set_role_functions(self.role_functions)
|
|
160
|
+
self.tabs.addTab(self.transitions_tab, self.TAB_NAMES[0])
|
|
161
|
+
|
|
162
|
+
# ==============================================================
|
|
163
|
+
# Tab 2: Pre / Post Actions
|
|
164
|
+
# ==============================================================
|
|
165
|
+
# [v2.5] Same context propagation as Tab 1.
|
|
166
|
+
# [R-6] Forward layer_names_provider.
|
|
167
|
+
self.actions_tab = ActionsTab(
|
|
168
|
+
self.draft,
|
|
169
|
+
role_functions=self.role_functions,
|
|
170
|
+
global_defs=self.global_defs,
|
|
171
|
+
state_machine=self.state_machine,
|
|
172
|
+
role_function_library=self.role_function_library,
|
|
173
|
+
literal_library=self.literal_library,
|
|
174
|
+
layer_names_provider=self.layer_names_provider,
|
|
175
|
+
)
|
|
176
|
+
self.tabs.addTab(self.actions_tab, self.TAB_NAMES[1])
|
|
177
|
+
|
|
178
|
+
# ==============================================================
|
|
179
|
+
# Tab 3: Relations
|
|
180
|
+
# ==============================================================
|
|
181
|
+
self.relations_tab = RelationsTab(self.draft)
|
|
182
|
+
self.tabs.addTab(self.relations_tab, self.TAB_NAMES[2])
|
|
183
|
+
|
|
184
|
+
# ==============================================================
|
|
185
|
+
# Tab 4: Overview
|
|
186
|
+
# ==============================================================
|
|
187
|
+
self.overview_tab = OverviewTab(
|
|
188
|
+
self.draft,
|
|
189
|
+
state_machine=self.state_machine,
|
|
190
|
+
)
|
|
191
|
+
self.tabs.addTab(self.overview_tab, self.TAB_NAMES[3])
|
|
192
|
+
|
|
193
|
+
# ==============================================================
|
|
194
|
+
# Tab 5: Preview
|
|
195
|
+
# ==============================================================
|
|
196
|
+
self.code_widget = CodeWidget(self.draft)
|
|
197
|
+
self.tabs.addTab(self.code_widget, self.TAB_NAMES[4])
|
|
198
|
+
|
|
199
|
+
# ==============================================================
|
|
200
|
+
# Signal connections
|
|
201
|
+
# ==============================================================
|
|
202
|
+
self.transitions_tab.transitions_changed.connect(
|
|
203
|
+
self._on_content_changed)
|
|
204
|
+
self.actions_tab.actions_changed.connect(
|
|
205
|
+
self._on_content_changed)
|
|
206
|
+
self.relations_tab.relations_changed.connect(
|
|
207
|
+
self._on_content_changed)
|
|
208
|
+
|
|
209
|
+
# ==============================================================
|
|
210
|
+
# OK / Cancel
|
|
211
|
+
# ==============================================================
|
|
212
|
+
btn_layout = QHBoxLayout()
|
|
213
|
+
btn_layout.addStretch()
|
|
214
|
+
cancel_btn = QPushButton(self.tr("Cancel"))
|
|
215
|
+
cancel_btn.clicked.connect(self.reject)
|
|
216
|
+
btn_layout.addWidget(cancel_btn)
|
|
217
|
+
ok_btn = QPushButton(self.tr("OK"))
|
|
218
|
+
ok_btn.clicked.connect(self._on_accept)
|
|
219
|
+
btn_layout.addWidget(ok_btn)
|
|
220
|
+
main_layout.addLayout(btn_layout)
|
|
221
|
+
|
|
222
|
+
# ------------------------------------------------------------------
|
|
223
|
+
# Load / save
|
|
224
|
+
# ------------------------------------------------------------------
|
|
225
|
+
def _load_draft(self):
|
|
226
|
+
"""Populate tabs from the current draft state.
|
|
227
|
+
|
|
228
|
+
[v2.2 fix]
|
|
229
|
+
Order: Actions -> Relations -> Transitions (last).
|
|
230
|
+
"""
|
|
231
|
+
self._loading = True
|
|
232
|
+
try:
|
|
233
|
+
# 1. Actions first
|
|
234
|
+
actions = list(self.draft.cell_actions)
|
|
235
|
+
logger.debug(
|
|
236
|
+
f"_load_draft: actions={len(actions)}, "
|
|
237
|
+
f"relations={len(self.draft.cell_relations)}")
|
|
238
|
+
self.actions_tab.set_actions(actions)
|
|
239
|
+
|
|
240
|
+
# 2. Relations
|
|
241
|
+
relations = list(self.draft.cell_relations)
|
|
242
|
+
self.relations_tab.set_relations(relations)
|
|
243
|
+
|
|
244
|
+
# 3. Transitions last (emits signals)
|
|
245
|
+
transitions = []
|
|
246
|
+
for item in self.draft.flow_items:
|
|
247
|
+
if item.item_type == "transition":
|
|
248
|
+
t = flow_item_to_transition(
|
|
249
|
+
item, self.draft.source, self.draft.event)
|
|
250
|
+
transitions.append(t)
|
|
251
|
+
self.transitions_tab.set_transitions(transitions)
|
|
252
|
+
finally:
|
|
253
|
+
self._loading = False
|
|
254
|
+
|
|
255
|
+
# v2.2: sync member details before refreshing preview
|
|
256
|
+
self._sync_member_details()
|
|
257
|
+
|
|
258
|
+
# Refresh preview / overview once after load
|
|
259
|
+
try:
|
|
260
|
+
self.code_widget.update_code()
|
|
261
|
+
except Exception as e:
|
|
262
|
+
logger.warning(f"code update failed: {e}")
|
|
263
|
+
try:
|
|
264
|
+
self.overview_tab.refresh()
|
|
265
|
+
except Exception as e:
|
|
266
|
+
logger.warning(f"overview refresh failed: {e}")
|
|
267
|
+
|
|
268
|
+
def _save_draft(self):
|
|
269
|
+
"""Sync tab contents back to the draft."""
|
|
270
|
+
transitions = self.transitions_tab.get_transitions()
|
|
271
|
+
self.draft.flow_items = [
|
|
272
|
+
transition_to_flow_item(t) for t in transitions
|
|
273
|
+
]
|
|
274
|
+
self.draft.cell_actions = list(self.actions_tab.get_actions())
|
|
275
|
+
self.draft.cell_relations = list(self.relations_tab.get_relations())
|
|
276
|
+
|
|
277
|
+
# ------------------------------------------------------------------
|
|
278
|
+
# v2.2: member details propagation
|
|
279
|
+
# ------------------------------------------------------------------
|
|
280
|
+
def _sync_member_details(self):
|
|
281
|
+
"""Build {label: 'cond -> target [mode]'} from the TransitionsTab
|
|
282
|
+
and pass it to RelationsTab so members are identifiable.
|
|
283
|
+
"""
|
|
284
|
+
details = {}
|
|
285
|
+
try:
|
|
286
|
+
transitions = self.transitions_tab.get_transitions()
|
|
287
|
+
except Exception as e:
|
|
288
|
+
logger.warning(f"_sync_member_details failed: {e}")
|
|
289
|
+
return
|
|
290
|
+
|
|
291
|
+
for t in transitions:
|
|
292
|
+
label = getattr(t, 'label', '') or ''
|
|
293
|
+
if not label:
|
|
294
|
+
continue
|
|
295
|
+
cond = (getattr(t, 'condition', '') or '').strip() or "(no cond)"
|
|
296
|
+
target = (getattr(t, 'target', '') or '').strip() or "(none)"
|
|
297
|
+
mode = "Commit" if getattr(t, 'early_return', False) else "Tentative"
|
|
298
|
+
details[label] = f"{cond} -> {target} [{mode}]"
|
|
299
|
+
|
|
300
|
+
self.relations_tab.set_member_details(details)
|
|
301
|
+
|
|
302
|
+
# ------------------------------------------------------------------
|
|
303
|
+
# Slots
|
|
304
|
+
# ------------------------------------------------------------------
|
|
305
|
+
def _on_content_changed(self):
|
|
306
|
+
# v2.2 fix: ignore signals emitted during initial load
|
|
307
|
+
if getattr(self, "_loading", False):
|
|
308
|
+
return
|
|
309
|
+
|
|
310
|
+
self._save_draft()
|
|
311
|
+
self._sync_member_details()
|
|
312
|
+
self.code_widget.update_code()
|
|
313
|
+
try:
|
|
314
|
+
self.overview_tab.refresh()
|
|
315
|
+
except Exception as e:
|
|
316
|
+
logger.warning(f"overview refresh failed: {e}")
|
|
317
|
+
|
|
318
|
+
def _on_refresh_preview(self):
|
|
319
|
+
self._save_draft()
|
|
320
|
+
self._sync_member_details()
|
|
321
|
+
self.code_widget.update_code()
|
|
322
|
+
try:
|
|
323
|
+
self.overview_tab.refresh()
|
|
324
|
+
except Exception as e:
|
|
325
|
+
logger.warning(f"overview refresh failed: {e}")
|
|
326
|
+
|
|
327
|
+
def _on_accept(self):
|
|
328
|
+
self._save_draft()
|
|
329
|
+
self.accept()
|
|
330
|
+
|
|
331
|
+
# ------------------------------------------------------------------
|
|
332
|
+
# Public API (test-friendly)
|
|
333
|
+
# ------------------------------------------------------------------
|
|
334
|
+
def get_tab_names(self) -> List[str]:
|
|
335
|
+
return [self.tabs.tabText(i) for i in range(self.tabs.count())]
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
# statable_gui/transition_editor_direct/draft.py
|
|
2
|
+
"""Action edit draft model.
|
|
3
|
+
|
|
4
|
+
[v2.2 additions]
|
|
5
|
+
- ActionDraft.cell_actions / cell_relations
|
|
6
|
+
- FlowItem.params['early_return'] / ['label']
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from typing import List, Dict, Any, Optional
|
|
12
|
+
|
|
13
|
+
from statable.model import (
|
|
14
|
+
ActionStep, TransitionRelation, Transition as ModelTransition,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger("transition_editor_direct.draft")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def ensure_list(value) -> List[str]:
|
|
21
|
+
if isinstance(value, list):
|
|
22
|
+
return value
|
|
23
|
+
if value is None:
|
|
24
|
+
return []
|
|
25
|
+
if isinstance(value, str):
|
|
26
|
+
stripped = value.strip()
|
|
27
|
+
if not stripped:
|
|
28
|
+
return []
|
|
29
|
+
return [stripped]
|
|
30
|
+
logger.warning(
|
|
31
|
+
f"Unexpected type for list: {type(value)}. Returning empty list.")
|
|
32
|
+
return []
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass
|
|
36
|
+
class SystemGlobal:
|
|
37
|
+
name: str
|
|
38
|
+
type: str = "uint16_t"
|
|
39
|
+
initial_value: str = "0"
|
|
40
|
+
description: str = ""
|
|
41
|
+
|
|
42
|
+
def to_dict(self) -> dict:
|
|
43
|
+
return {
|
|
44
|
+
'name': self.name,
|
|
45
|
+
'type': self.type,
|
|
46
|
+
'initial_value': self.initial_value,
|
|
47
|
+
'description': self.description,
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
@classmethod
|
|
51
|
+
def from_dict(cls, data: dict) -> 'SystemGlobal':
|
|
52
|
+
return cls(
|
|
53
|
+
name=data.get('name', ''),
|
|
54
|
+
type=data.get('type', 'uint16_t'),
|
|
55
|
+
initial_value=data.get('initial_value', '0'),
|
|
56
|
+
description=data.get('description', ''),
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@dataclass
|
|
61
|
+
class TransitionParams:
|
|
62
|
+
event: str = "NewEvent"
|
|
63
|
+
condition: str = ""
|
|
64
|
+
pre_actions: List[str] = field(default_factory=list)
|
|
65
|
+
target: str = ""
|
|
66
|
+
has_else: bool = True
|
|
67
|
+
else_target: str = ""
|
|
68
|
+
else_actions: List[str] = field(default_factory=list)
|
|
69
|
+
early_return: bool = False # v2.2
|
|
70
|
+
label: str = "" # v2.2
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@dataclass
|
|
74
|
+
class FlowItem:
|
|
75
|
+
item_type: str = ""
|
|
76
|
+
name: str = ""
|
|
77
|
+
edited_text: str = ""
|
|
78
|
+
params: Dict[str, Any] = field(default_factory=dict)
|
|
79
|
+
pos_x: Optional[float] = None
|
|
80
|
+
pos_y: Optional[float] = None
|
|
81
|
+
|
|
82
|
+
def display_text(self) -> str:
|
|
83
|
+
return self.edited_text or self.name
|
|
84
|
+
|
|
85
|
+
def to_dict(self) -> dict:
|
|
86
|
+
return {
|
|
87
|
+
'item_type': self.item_type,
|
|
88
|
+
'name': self.name,
|
|
89
|
+
'edited_text': self.edited_text,
|
|
90
|
+
'params': self.params,
|
|
91
|
+
'pos_x': self.pos_x,
|
|
92
|
+
'pos_y': self.pos_y,
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
@classmethod
|
|
96
|
+
def from_dict(cls, data: dict) -> 'FlowItem':
|
|
97
|
+
return cls(
|
|
98
|
+
item_type=data.get('item_type', ''),
|
|
99
|
+
name=data.get('name', ''),
|
|
100
|
+
edited_text=data.get('edited_text', ''),
|
|
101
|
+
params=data.get('params', {}),
|
|
102
|
+
pos_x=data.get('pos_x'),
|
|
103
|
+
pos_y=data.get('pos_y'),
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@dataclass
|
|
108
|
+
class ActionDraft:
|
|
109
|
+
source: str = ""
|
|
110
|
+
event: str = ""
|
|
111
|
+
layer_name: str = ""
|
|
112
|
+
|
|
113
|
+
flow_items: List[FlowItem] = field(default_factory=list)
|
|
114
|
+
default_target: str = ""
|
|
115
|
+
|
|
116
|
+
system_globals: List[SystemGlobal] = field(default_factory=list)
|
|
117
|
+
generated_code: str = ""
|
|
118
|
+
|
|
119
|
+
role_func_map: Dict[str, str] = field(default_factory=dict)
|
|
120
|
+
user_code: Dict[str, str] = field(default_factory=dict)
|
|
121
|
+
|
|
122
|
+
# === v2.2: cell-level metadata ===
|
|
123
|
+
cell_actions: List[ActionStep] = field(default_factory=list)
|
|
124
|
+
cell_relations: List[TransitionRelation] = field(default_factory=list)
|
|
125
|
+
|
|
126
|
+
def clear(self):
|
|
127
|
+
self.flow_items = []
|
|
128
|
+
self.default_target = ""
|
|
129
|
+
self.system_globals = []
|
|
130
|
+
self.generated_code = ""
|
|
131
|
+
self.role_func_map = {}
|
|
132
|
+
self.user_code = {}
|
|
133
|
+
self.cell_actions = []
|
|
134
|
+
self.cell_relations = []
|
|
135
|
+
|
|
136
|
+
def get_role_func_name(self, base_name: str, phase: str) -> str:
|
|
137
|
+
key = f"{self.source}|{self.event}|{phase}|{base_name}"
|
|
138
|
+
if key not in self.role_func_map:
|
|
139
|
+
func_name = f"RoleFunc_{base_name}_{self.source}_{self.event}_{phase}"
|
|
140
|
+
self.role_func_map[key] = func_name
|
|
141
|
+
return self.role_func_map[key]
|
|
142
|
+
|
|
143
|
+
def to_dict(self) -> dict:
|
|
144
|
+
return {
|
|
145
|
+
'source': self.source,
|
|
146
|
+
'event': self.event,
|
|
147
|
+
'layer_name': self.layer_name,
|
|
148
|
+
'flow_items': [i.to_dict() for i in self.flow_items],
|
|
149
|
+
'default_target': self.default_target,
|
|
150
|
+
'system_globals': [g.to_dict() for g in self.system_globals],
|
|
151
|
+
'generated_code': self.generated_code,
|
|
152
|
+
'role_func_map': self.role_func_map,
|
|
153
|
+
'user_code': self.user_code,
|
|
154
|
+
# v2.2
|
|
155
|
+
'cell_actions': [
|
|
156
|
+
{'role_function': a.role_function, 'trigger': a.trigger,
|
|
157
|
+
'title': a.title}
|
|
158
|
+
for a in self.cell_actions
|
|
159
|
+
],
|
|
160
|
+
'cell_relations': [
|
|
161
|
+
{'kind': r.kind, 'members': list(r.members),
|
|
162
|
+
'shared_condition': r.shared_condition, 'note': r.note}
|
|
163
|
+
for r in self.cell_relations
|
|
164
|
+
],
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
@classmethod
|
|
168
|
+
def from_dict(cls, data: dict) -> 'ActionDraft':
|
|
169
|
+
d = cls(
|
|
170
|
+
source=data.get('source', ''),
|
|
171
|
+
event=data.get('event', ''),
|
|
172
|
+
layer_name=data.get('layer_name', ''),
|
|
173
|
+
flow_items=[FlowItem.from_dict(i)
|
|
174
|
+
for i in data.get('flow_items', [])],
|
|
175
|
+
default_target=data.get('default_target', ''),
|
|
176
|
+
system_globals=[SystemGlobal.from_dict(g)
|
|
177
|
+
for g in data.get('system_globals', [])],
|
|
178
|
+
generated_code=data.get('generated_code', ''),
|
|
179
|
+
role_func_map=data.get('role_func_map', {}),
|
|
180
|
+
user_code=data.get('user_code', {}),
|
|
181
|
+
)
|
|
182
|
+
d.cell_actions = [
|
|
183
|
+
ActionStep(
|
|
184
|
+
role_function=a.get('role_function', ''),
|
|
185
|
+
trigger=a.get('trigger', 'always'),
|
|
186
|
+
title=a.get('title', ''),
|
|
187
|
+
)
|
|
188
|
+
for a in data.get('cell_actions', [])
|
|
189
|
+
]
|
|
190
|
+
d.cell_relations = [
|
|
191
|
+
TransitionRelation(
|
|
192
|
+
kind=r.get('kind', 'sequential'),
|
|
193
|
+
members=list(r.get('members', [])),
|
|
194
|
+
shared_condition=r.get('shared_condition', ''),
|
|
195
|
+
note=r.get('note', ''),
|
|
196
|
+
)
|
|
197
|
+
for r in data.get('cell_relations', [])
|
|
198
|
+
]
|
|
199
|
+
return d
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def transition_to_flow_item(trans) -> FlowItem:
|
|
203
|
+
"""Transition -> FlowItem (v2.2: preserves early_return / label)."""
|
|
204
|
+
pre_actions = ensure_list(getattr(trans, 'pre_actions', []))
|
|
205
|
+
else_actions = ensure_list(getattr(trans, 'else_actions', []))
|
|
206
|
+
condition = getattr(trans, 'condition', '')
|
|
207
|
+
if not isinstance(condition, str):
|
|
208
|
+
logger.error(f"Condition is not str: {type(condition)}. Using empty string.")
|
|
209
|
+
condition = ""
|
|
210
|
+
|
|
211
|
+
event_name = trans.event if trans.event else "NewEvent"
|
|
212
|
+
|
|
213
|
+
logger.debug(
|
|
214
|
+
f"transition_to_flow_item: event='{event_name}', "
|
|
215
|
+
f"condition='{condition}', pre_actions={pre_actions}, "
|
|
216
|
+
f"else_actions={else_actions}")
|
|
217
|
+
|
|
218
|
+
return FlowItem(
|
|
219
|
+
item_type="transition",
|
|
220
|
+
name=event_name,
|
|
221
|
+
edited_text=(
|
|
222
|
+
trans.title if trans.title != "(untitled transition)"
|
|
223
|
+
else event_name
|
|
224
|
+
),
|
|
225
|
+
params={
|
|
226
|
+
"event": event_name,
|
|
227
|
+
"condition": condition,
|
|
228
|
+
"pre_actions": pre_actions,
|
|
229
|
+
"target": trans.target,
|
|
230
|
+
"has_else": getattr(trans, 'has_else', True),
|
|
231
|
+
"else_target": getattr(trans, 'else_target', ''),
|
|
232
|
+
"else_actions": else_actions,
|
|
233
|
+
# v2.2
|
|
234
|
+
"early_return": getattr(trans, 'early_return', False),
|
|
235
|
+
"label": getattr(trans, 'label', ''),
|
|
236
|
+
}
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def flow_item_to_transition(item: FlowItem, source: str, event: str):
|
|
241
|
+
"""FlowItem -> Transition (v2.2: preserves early_return / label)."""
|
|
242
|
+
from statable.model import Transition
|
|
243
|
+
params = item.params
|
|
244
|
+
pre_actions = ensure_list(params.get('pre_actions', []))
|
|
245
|
+
else_actions = ensure_list(params.get('else_actions', []))
|
|
246
|
+
condition = params.get('condition', '')
|
|
247
|
+
if not isinstance(condition, str):
|
|
248
|
+
logger.error(f"Condition is not str: {type(condition)}. Using empty string.")
|
|
249
|
+
condition = ""
|
|
250
|
+
|
|
251
|
+
logger.debug(
|
|
252
|
+
f"flow_item_to_transition: condition='{condition}', "
|
|
253
|
+
f"pre_actions={pre_actions}, else_actions={else_actions}")
|
|
254
|
+
|
|
255
|
+
return Transition(
|
|
256
|
+
source=source,
|
|
257
|
+
event=event,
|
|
258
|
+
condition=condition,
|
|
259
|
+
pre_actions=pre_actions,
|
|
260
|
+
target=params.get('target', ''),
|
|
261
|
+
has_else=params.get('has_else', True),
|
|
262
|
+
else_target=params.get('else_target', ''),
|
|
263
|
+
else_actions=else_actions,
|
|
264
|
+
early_return=params.get('early_return', False),
|
|
265
|
+
label=params.get('label', ''),
|
|
266
|
+
action="",
|
|
267
|
+
transition_type="external",
|
|
268
|
+
title=item.edited_text if item.edited_text and item.edited_text != item.name
|
|
269
|
+
else "(untitled transition)",
|
|
270
|
+
)
|