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,244 @@
|
|
|
1
|
+
"""
|
|
2
|
+
statable_gui/role_function_dialog.py
|
|
3
|
+
Role function create / edit dialog.
|
|
4
|
+
|
|
5
|
+
Version History
|
|
6
|
+
---------------
|
|
7
|
+
v1.0 - Initial dialog.
|
|
8
|
+
v1.5 - Added namespace field.
|
|
9
|
+
v3.7 - Removed reserved fields from the UI.
|
|
10
|
+
v3.8 - Layout restructured (3 QGroupBox sections).
|
|
11
|
+
v3.9 - Namespace field as editable QComboBox.
|
|
12
|
+
|
|
13
|
+
[R-7]
|
|
14
|
+
- Added "+ New Literal" button below the "Used literals" group.
|
|
15
|
+
Opens NewLiteralDialog, adds the new LiteralDefinition to the
|
|
16
|
+
shared literal_library, and appends it (pre-checked) to the
|
|
17
|
+
list widget.
|
|
18
|
+
- New constructor argument `literal_library=None`. When None, the
|
|
19
|
+
button is hidden and behavior is unchanged (backward compat).
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from PySide6.QtWidgets import (
|
|
23
|
+
QDialog, QVBoxLayout, QHBoxLayout, QFormLayout, QLineEdit, QComboBox,
|
|
24
|
+
QListWidget, QListWidgetItem, QDialogButtonBox, QGroupBox,
|
|
25
|
+
QPushButton, QMessageBox,
|
|
26
|
+
)
|
|
27
|
+
from PySide6.QtCore import Qt
|
|
28
|
+
|
|
29
|
+
from statable.model import RoleFunction
|
|
30
|
+
from .logger import StaTableLogger
|
|
31
|
+
|
|
32
|
+
try:
|
|
33
|
+
from .literal_definition_dialog import NewLiteralDialog
|
|
34
|
+
except ImportError:
|
|
35
|
+
from literal_definition_dialog import NewLiteralDialog
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class RoleFunctionDialog(QDialog):
|
|
39
|
+
"""Dialog for creating / editing role functions."""
|
|
40
|
+
|
|
41
|
+
def __init__(self, parent=None, role_function=None,
|
|
42
|
+
global_vars=None, events=None, literals=None,
|
|
43
|
+
namespace_choices=None,
|
|
44
|
+
literal_library=None):
|
|
45
|
+
super().__init__(parent)
|
|
46
|
+
self.setWindowTitle(self.tr("Edit role function"))
|
|
47
|
+
self.setMinimumWidth(500)
|
|
48
|
+
|
|
49
|
+
self._original = role_function
|
|
50
|
+
self._global_vars = list(global_vars or [])
|
|
51
|
+
self._events = list(events or [])
|
|
52
|
+
self._literals = list(literals or [])
|
|
53
|
+
self._namespace_choices = list(namespace_choices or [])
|
|
54
|
+
# [R-7] shared library for creating new literals
|
|
55
|
+
self._literal_library = literal_library
|
|
56
|
+
|
|
57
|
+
self._setup_ui()
|
|
58
|
+
self._load_data()
|
|
59
|
+
StaTableLogger.debug("RoleFunctionDialog initialized")
|
|
60
|
+
|
|
61
|
+
# ------------------------------------------------------------------
|
|
62
|
+
# UI construction
|
|
63
|
+
# ------------------------------------------------------------------
|
|
64
|
+
def _setup_ui(self):
|
|
65
|
+
main_layout = QVBoxLayout(self)
|
|
66
|
+
|
|
67
|
+
# ---- Top: form fields ----
|
|
68
|
+
form = QFormLayout()
|
|
69
|
+
|
|
70
|
+
self.name_edit = QLineEdit()
|
|
71
|
+
form.addRow("Function name:", self.name_edit)
|
|
72
|
+
|
|
73
|
+
self.namespace_combo = QComboBox()
|
|
74
|
+
self.namespace_combo.setEditable(True)
|
|
75
|
+
self.namespace_combo.setInsertPolicy(QComboBox.NoInsert)
|
|
76
|
+
self.namespace_combo.addItem(self.tr(""))
|
|
77
|
+
for ns in self._namespace_choices:
|
|
78
|
+
if ns and ns != "":
|
|
79
|
+
self.namespace_combo.addItem(ns)
|
|
80
|
+
self.namespace_combo.setToolTip(
|
|
81
|
+
self.tr("Namespace (layer name / feature group name).\n"
|
|
82
|
+
"If specified, it can be referenced as 'Driver.Init'.\n"
|
|
83
|
+
"If empty, it is treated as having no layer ('Init').\n"
|
|
84
|
+
"Select from the list, or type a new value.")
|
|
85
|
+
)
|
|
86
|
+
form.addRow("Namespace:", self.namespace_combo)
|
|
87
|
+
|
|
88
|
+
self.title_edit = QLineEdit()
|
|
89
|
+
self.title_edit.setPlaceholderText(
|
|
90
|
+
self.tr("Label shown in the list (auto-set if empty)"))
|
|
91
|
+
form.addRow("Display name:", self.title_edit)
|
|
92
|
+
|
|
93
|
+
self.desc_edit = QLineEdit()
|
|
94
|
+
form.addRow("Description:", self.desc_edit)
|
|
95
|
+
|
|
96
|
+
main_layout.addLayout(form)
|
|
97
|
+
|
|
98
|
+
# ---- Middle: three checkbox groups ----
|
|
99
|
+
self.global_list = self._make_check_list(
|
|
100
|
+
main_layout, "Used global variables", self._global_vars)
|
|
101
|
+
self.event_list = self._make_check_list(
|
|
102
|
+
main_layout, "Used events", self._events)
|
|
103
|
+
self.literal_list = self._make_check_list(
|
|
104
|
+
main_layout, "Used literals (select from existing)",
|
|
105
|
+
self._literals)
|
|
106
|
+
|
|
107
|
+
# [R-7] "+ New Literal" button (only when library available)
|
|
108
|
+
if self._literal_library is not None:
|
|
109
|
+
btn_row = QHBoxLayout()
|
|
110
|
+
btn_row.addStretch()
|
|
111
|
+
self.new_literal_btn = QPushButton(self.tr("+ New Literal"))
|
|
112
|
+
self.new_literal_btn.setToolTip(
|
|
113
|
+
self.tr("Create a new literal and add it to the list above"))
|
|
114
|
+
self.new_literal_btn.clicked.connect(self._on_new_literal)
|
|
115
|
+
btn_row.addWidget(self.new_literal_btn)
|
|
116
|
+
main_layout.addLayout(btn_row)
|
|
117
|
+
|
|
118
|
+
# ---- Bottom: buttons ----
|
|
119
|
+
buttons = QDialogButtonBox(
|
|
120
|
+
QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
|
|
121
|
+
buttons.accepted.connect(self._on_accept)
|
|
122
|
+
buttons.rejected.connect(self.reject)
|
|
123
|
+
main_layout.addWidget(buttons)
|
|
124
|
+
|
|
125
|
+
@staticmethod
|
|
126
|
+
def _make_check_list(parent_layout, title, items):
|
|
127
|
+
group = QGroupBox(title)
|
|
128
|
+
group_layout = QVBoxLayout(group)
|
|
129
|
+
lst = QListWidget()
|
|
130
|
+
lst.setSelectionMode(QListWidget.NoSelection)
|
|
131
|
+
for text in items:
|
|
132
|
+
item = QListWidgetItem(text)
|
|
133
|
+
item.setFlags(item.flags() | Qt.ItemIsUserCheckable)
|
|
134
|
+
item.setCheckState(Qt.Unchecked)
|
|
135
|
+
lst.addItem(item)
|
|
136
|
+
group_layout.addWidget(lst)
|
|
137
|
+
parent_layout.addWidget(group)
|
|
138
|
+
return lst
|
|
139
|
+
|
|
140
|
+
# ------------------------------------------------------------------
|
|
141
|
+
# Data binding
|
|
142
|
+
# ------------------------------------------------------------------
|
|
143
|
+
def _load_data(self):
|
|
144
|
+
rf = self._original
|
|
145
|
+
if rf is None:
|
|
146
|
+
return
|
|
147
|
+
|
|
148
|
+
self.name_edit.setText(rf.name)
|
|
149
|
+
self.title_edit.setText(rf.title)
|
|
150
|
+
self.desc_edit.setText(rf.description)
|
|
151
|
+
|
|
152
|
+
ns = getattr(rf, 'namespace', '') or ''
|
|
153
|
+
idx = self.namespace_combo.findText(ns)
|
|
154
|
+
if idx >= 0:
|
|
155
|
+
self.namespace_combo.setCurrentIndex(idx)
|
|
156
|
+
else:
|
|
157
|
+
self.namespace_combo.setEditText(ns)
|
|
158
|
+
|
|
159
|
+
self._check_items(
|
|
160
|
+
self.global_list,
|
|
161
|
+
getattr(rf, 'used_global_vars', []) or [])
|
|
162
|
+
self._check_items(
|
|
163
|
+
self.event_list,
|
|
164
|
+
getattr(rf, 'used_events', []) or [])
|
|
165
|
+
self._check_items(
|
|
166
|
+
self.literal_list,
|
|
167
|
+
getattr(rf, 'used_literals', []) or [])
|
|
168
|
+
|
|
169
|
+
@staticmethod
|
|
170
|
+
def _check_items(lst, selected):
|
|
171
|
+
sel = set(selected)
|
|
172
|
+
for i in range(lst.count()):
|
|
173
|
+
item = lst.item(i)
|
|
174
|
+
if item.text() in sel:
|
|
175
|
+
item.setCheckState(Qt.Checked)
|
|
176
|
+
|
|
177
|
+
@staticmethod
|
|
178
|
+
def _collect_checked(lst):
|
|
179
|
+
result = []
|
|
180
|
+
for i in range(lst.count()):
|
|
181
|
+
item = lst.item(i)
|
|
182
|
+
if item.checkState() == Qt.Checked:
|
|
183
|
+
result.append(item.text())
|
|
184
|
+
return result
|
|
185
|
+
|
|
186
|
+
# ------------------------------------------------------------------
|
|
187
|
+
# [R-7] New literal handler
|
|
188
|
+
# ------------------------------------------------------------------
|
|
189
|
+
def _on_new_literal(self):
|
|
190
|
+
if self._literal_library is None:
|
|
191
|
+
return
|
|
192
|
+
|
|
193
|
+
dlg = NewLiteralDialog(self)
|
|
194
|
+
if dlg.exec() != QDialog.Accepted:
|
|
195
|
+
return
|
|
196
|
+
|
|
197
|
+
lit = dlg.get_literal()
|
|
198
|
+
try:
|
|
199
|
+
self._literal_library.add(lit)
|
|
200
|
+
except ValueError:
|
|
201
|
+
QMessageBox.warning(
|
|
202
|
+
self, self.tr("Warning"),
|
|
203
|
+
f"A literal named '{lit.name}' already exists.")
|
|
204
|
+
return
|
|
205
|
+
|
|
206
|
+
# Append to the list widget, pre-checked
|
|
207
|
+
item = QListWidgetItem(lit.name)
|
|
208
|
+
item.setFlags(item.flags() | Qt.ItemIsUserCheckable)
|
|
209
|
+
item.setCheckState(Qt.Checked)
|
|
210
|
+
self.literal_list.addItem(item)
|
|
211
|
+
StaTableLogger.info(f"New literal created: {lit.name}")
|
|
212
|
+
|
|
213
|
+
# ------------------------------------------------------------------
|
|
214
|
+
# OK / Cancel
|
|
215
|
+
# ------------------------------------------------------------------
|
|
216
|
+
def _on_accept(self):
|
|
217
|
+
if not self.title_edit.text().strip():
|
|
218
|
+
auto_title = (
|
|
219
|
+
f"Role function: "
|
|
220
|
+
f"{self.name_edit.text().strip() or '(unnamed)'}")
|
|
221
|
+
self.title_edit.setText(auto_title)
|
|
222
|
+
self.accept()
|
|
223
|
+
|
|
224
|
+
# ------------------------------------------------------------------
|
|
225
|
+
# Result
|
|
226
|
+
# ------------------------------------------------------------------
|
|
227
|
+
def get_role_function(self) -> RoleFunction:
|
|
228
|
+
orig = self._original
|
|
229
|
+
namespace = self.namespace_combo.currentText().strip()
|
|
230
|
+
|
|
231
|
+
return RoleFunction(
|
|
232
|
+
name=self.name_edit.text().strip(),
|
|
233
|
+
namespace=namespace,
|
|
234
|
+
description=self.desc_edit.text().strip(),
|
|
235
|
+
title=self.title_edit.text().strip(),
|
|
236
|
+
return_type=orig.return_type if orig else "void",
|
|
237
|
+
arg1_type=orig.arg1_type if orig else "",
|
|
238
|
+
arg1_name=orig.arg1_name if orig else "",
|
|
239
|
+
arg2_type=orig.arg2_type if orig else "",
|
|
240
|
+
arg2_name=orig.arg2_name if orig else "",
|
|
241
|
+
used_global_vars=self._collect_checked(self.global_list),
|
|
242
|
+
used_events=self._collect_checked(self.event_list),
|
|
243
|
+
used_literals=self._collect_checked(self.literal_list),
|
|
244
|
+
)
|
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
# statable_gui/state_actions_dialog.py
|
|
2
|
+
"""
|
|
3
|
+
State actions (Entry / Exit / Do) editing dialog.
|
|
4
|
+
|
|
5
|
+
Version: 1.0 (2026-09-26 / Phase 4a)
|
|
6
|
+
|
|
7
|
+
Provides StateActionsDialog: a 4-tab editor for per-state actions.
|
|
8
|
+
|
|
9
|
+
Tabs:
|
|
10
|
+
- Entry: actions executed once when entering the state
|
|
11
|
+
- Exit: actions executed once when leaving the state
|
|
12
|
+
- Do: actions executed every loop while in the state
|
|
13
|
+
- Preview: generated C code preview
|
|
14
|
+
|
|
15
|
+
Each action tab contains:
|
|
16
|
+
- Action list (QTableWidget: Type / Target / Condition)
|
|
17
|
+
- Add / Delete / Up / Down buttons
|
|
18
|
+
- Custom code marker hint (read-only)
|
|
19
|
+
|
|
20
|
+
On OK, updates State.entry / State.exit / State.do_actions.
|
|
21
|
+
|
|
22
|
+
Design notes:
|
|
23
|
+
- Uses statable.model.ActionStep.
|
|
24
|
+
- action_type: "role" (default) or "fire_event".
|
|
25
|
+
- Empty Target rows are dropped on OK.
|
|
26
|
+
- Custom code editing is done in the generated file
|
|
27
|
+
(marker: [[STABLE_USER_CODE_START:<Layer>_<Kind>_<State>_custom]]).
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
from typing import List, Optional
|
|
31
|
+
|
|
32
|
+
from PySide6.QtCore import Qt
|
|
33
|
+
from PySide6.QtGui import QFont
|
|
34
|
+
from PySide6.QtWidgets import (
|
|
35
|
+
QDialog, QVBoxLayout, QHBoxLayout, QTableWidget, QTableWidgetItem,
|
|
36
|
+
QPushButton, QDialogButtonBox, QTabWidget, QWidget,
|
|
37
|
+
QLabel, QGroupBox, QHeaderView, QPlainTextEdit,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
from statable.model import State, ActionStep
|
|
41
|
+
from statable.state_machine import StateMachine
|
|
42
|
+
|
|
43
|
+
from .logger import StaTableLogger
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
KIND_ENTRY = "Entry"
|
|
47
|
+
KIND_EXIT = "Exit"
|
|
48
|
+
KIND_DO = "Do"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class _ActionListWidget(QWidget):
|
|
52
|
+
"""Reusable action list editor.
|
|
53
|
+
|
|
54
|
+
Columns:
|
|
55
|
+
0: Type ("role" or "fire_event")
|
|
56
|
+
1: Target (RoleFunc name, or Event name)
|
|
57
|
+
2: Condition (C expression; empty = unconditional)
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
COL_TYPE = 0
|
|
61
|
+
COL_TARGET = 1
|
|
62
|
+
COL_CONDITION = 2
|
|
63
|
+
|
|
64
|
+
VALID_TYPES = ("role", "fire_event")
|
|
65
|
+
|
|
66
|
+
def __init__(self, parent=None):
|
|
67
|
+
super().__init__(parent)
|
|
68
|
+
self._setup_ui()
|
|
69
|
+
|
|
70
|
+
def _setup_ui(self):
|
|
71
|
+
layout = QVBoxLayout(self)
|
|
72
|
+
layout.setContentsMargins(0, 0, 0, 0)
|
|
73
|
+
|
|
74
|
+
self.table = QTableWidget(0, 3)
|
|
75
|
+
self.table.setHorizontalHeaderLabels(["Type", "Target", "Condition"])
|
|
76
|
+
self.table.horizontalHeader().setSectionResizeMode(
|
|
77
|
+
self.COL_TYPE, QHeaderView.ResizeToContents)
|
|
78
|
+
self.table.horizontalHeader().setSectionResizeMode(
|
|
79
|
+
self.COL_TARGET, QHeaderView.Stretch)
|
|
80
|
+
self.table.horizontalHeader().setSectionResizeMode(
|
|
81
|
+
self.COL_CONDITION, QHeaderView.Stretch)
|
|
82
|
+
self.table.setFont(QFont("Consolas", 10))
|
|
83
|
+
layout.addWidget(self.table)
|
|
84
|
+
|
|
85
|
+
btn_row = QHBoxLayout()
|
|
86
|
+
self.add_btn = QPushButton(self.tr("+ Add"))
|
|
87
|
+
self.add_btn.clicked.connect(self._on_add)
|
|
88
|
+
self.del_btn = QPushButton(self.tr("Delete"))
|
|
89
|
+
self.del_btn.clicked.connect(self._on_delete)
|
|
90
|
+
self.up_btn = QPushButton(self.tr("Up"))
|
|
91
|
+
self.up_btn.clicked.connect(self._on_up)
|
|
92
|
+
self.down_btn = QPushButton(self.tr("Down"))
|
|
93
|
+
self.down_btn.clicked.connect(self._on_down)
|
|
94
|
+
btn_row.addWidget(self.add_btn)
|
|
95
|
+
btn_row.addWidget(self.del_btn)
|
|
96
|
+
btn_row.addStretch()
|
|
97
|
+
btn_row.addWidget(self.up_btn)
|
|
98
|
+
btn_row.addWidget(self.down_btn)
|
|
99
|
+
layout.addLayout(btn_row)
|
|
100
|
+
|
|
101
|
+
# ------------------------------------------------------------------
|
|
102
|
+
# Actions
|
|
103
|
+
# ------------------------------------------------------------------
|
|
104
|
+
def _on_add(self):
|
|
105
|
+
row = self.table.rowCount()
|
|
106
|
+
self.table.insertRow(row)
|
|
107
|
+
self.table.setItem(row, self.COL_TYPE,
|
|
108
|
+
QTableWidgetItem("role"))
|
|
109
|
+
self.table.setItem(row, self.COL_TARGET,
|
|
110
|
+
QTableWidgetItem(""))
|
|
111
|
+
self.table.setItem(row, self.COL_CONDITION,
|
|
112
|
+
QTableWidgetItem(""))
|
|
113
|
+
self.table.editItem(self.table.item(row, self.COL_TARGET))
|
|
114
|
+
|
|
115
|
+
def _on_delete(self):
|
|
116
|
+
row = self.table.currentRow()
|
|
117
|
+
if row >= 0:
|
|
118
|
+
self.table.removeRow(row)
|
|
119
|
+
|
|
120
|
+
def _on_up(self):
|
|
121
|
+
row = self.table.currentRow()
|
|
122
|
+
if row > 0:
|
|
123
|
+
self._swap_rows(row, row - 1)
|
|
124
|
+
self.table.selectRow(row - 1)
|
|
125
|
+
|
|
126
|
+
def _on_down(self):
|
|
127
|
+
row = self.table.currentRow()
|
|
128
|
+
if 0 <= row < self.table.rowCount() - 1:
|
|
129
|
+
self._swap_rows(row, row + 1)
|
|
130
|
+
self.table.selectRow(row + 1)
|
|
131
|
+
|
|
132
|
+
def _swap_rows(self, r1: int, r2: int):
|
|
133
|
+
for col in range(self.table.columnCount()):
|
|
134
|
+
i1 = self.table.takeItem(r1, col)
|
|
135
|
+
i2 = self.table.takeItem(r2, col)
|
|
136
|
+
self.table.setItem(r1, col, i2)
|
|
137
|
+
self.table.setItem(r2, col, i1)
|
|
138
|
+
|
|
139
|
+
# ------------------------------------------------------------------
|
|
140
|
+
# Data binding
|
|
141
|
+
# ------------------------------------------------------------------
|
|
142
|
+
def set_actions(self, actions: List[ActionStep]):
|
|
143
|
+
"""Populate the table from a List[ActionStep]."""
|
|
144
|
+
self.table.setRowCount(0)
|
|
145
|
+
for a in (actions or []):
|
|
146
|
+
row = self.table.rowCount()
|
|
147
|
+
self.table.insertRow(row)
|
|
148
|
+
|
|
149
|
+
atype = (getattr(a, "action_type", "role") or "role")
|
|
150
|
+
if atype not in self.VALID_TYPES:
|
|
151
|
+
atype = "role"
|
|
152
|
+
|
|
153
|
+
if atype == "fire_event":
|
|
154
|
+
target = getattr(a, "event_name", "") or ""
|
|
155
|
+
else:
|
|
156
|
+
target = getattr(a, "role_function", "") or ""
|
|
157
|
+
|
|
158
|
+
cond = getattr(a, "condition", "") or ""
|
|
159
|
+
|
|
160
|
+
self.table.setItem(row, self.COL_TYPE,
|
|
161
|
+
QTableWidgetItem(atype))
|
|
162
|
+
self.table.setItem(row, self.COL_TARGET,
|
|
163
|
+
QTableWidgetItem(target))
|
|
164
|
+
self.table.setItem(row, self.COL_CONDITION,
|
|
165
|
+
QTableWidgetItem(cond))
|
|
166
|
+
|
|
167
|
+
def get_actions(self) -> List[ActionStep]:
|
|
168
|
+
"""Return the current table contents as List[ActionStep]."""
|
|
169
|
+
result: List[ActionStep] = []
|
|
170
|
+
for row in range(self.table.rowCount()):
|
|
171
|
+
type_item = self.table.item(row, self.COL_TYPE)
|
|
172
|
+
target_item = self.table.item(row, self.COL_TARGET)
|
|
173
|
+
cond_item = self.table.item(row, self.COL_CONDITION)
|
|
174
|
+
|
|
175
|
+
atype = (type_item.text().strip()
|
|
176
|
+
if type_item else "role") or "role"
|
|
177
|
+
if atype not in self.VALID_TYPES:
|
|
178
|
+
atype = "role"
|
|
179
|
+
target = target_item.text().strip() if target_item else ""
|
|
180
|
+
cond = cond_item.text().strip() if cond_item else ""
|
|
181
|
+
|
|
182
|
+
if not target:
|
|
183
|
+
# Empty target rows are dropped
|
|
184
|
+
continue
|
|
185
|
+
|
|
186
|
+
if atype == "fire_event":
|
|
187
|
+
result.append(ActionStep(
|
|
188
|
+
action_type="fire_event",
|
|
189
|
+
event_name=target,
|
|
190
|
+
condition=cond,
|
|
191
|
+
))
|
|
192
|
+
else:
|
|
193
|
+
result.append(ActionStep(
|
|
194
|
+
action_type="role",
|
|
195
|
+
role_function=target,
|
|
196
|
+
condition=cond,
|
|
197
|
+
))
|
|
198
|
+
return result
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
class StateActionsDialog(QDialog):
|
|
202
|
+
"""State actions (Entry / Exit / Do) editor.
|
|
203
|
+
|
|
204
|
+
[Phase 4a]
|
|
205
|
+
4 tabs: Entry / Exit / Do / Preview.
|
|
206
|
+
Each action tab hosts an _ActionListWidget.
|
|
207
|
+
On OK, updates State.entry / State.exit / State.do_actions.
|
|
208
|
+
"""
|
|
209
|
+
|
|
210
|
+
KIND_TO_ATTR = {
|
|
211
|
+
KIND_ENTRY: "entry",
|
|
212
|
+
KIND_EXIT: "exit",
|
|
213
|
+
KIND_DO: "do_actions",
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
@staticmethod
|
|
217
|
+
def _to_pascal(name: str) -> str:
|
|
218
|
+
"""Convert snake_case / lowerCamelCase to PascalCase."""
|
|
219
|
+
if not name:
|
|
220
|
+
return ""
|
|
221
|
+
# Simple heuristic: capitalize each '_'-separated segment
|
|
222
|
+
parts = [p for p in name.replace("-", "_").split("_") if p]
|
|
223
|
+
return "".join(p[:1].upper() + p[1:] for p in parts)
|
|
224
|
+
|
|
225
|
+
@classmethod
|
|
226
|
+
def _rf_call_preview(cls, layer: str, role_function: str) -> str:
|
|
227
|
+
"""Build a preview-only RoleFunc_<NS>_<Name>(NULL, ctx) string."""
|
|
228
|
+
if not role_function:
|
|
229
|
+
return ""
|
|
230
|
+
rf = role_function.strip()
|
|
231
|
+
if rf.startswith("RoleFunc_"):
|
|
232
|
+
full = rf
|
|
233
|
+
elif "." in rf:
|
|
234
|
+
ns, name = rf.split(".", 1)
|
|
235
|
+
full = (f"RoleFunc_{cls._to_pascal(ns)}_"
|
|
236
|
+
f"{cls._to_pascal(name)}")
|
|
237
|
+
else:
|
|
238
|
+
full = (f"RoleFunc_{layer}_{cls._to_pascal(rf)}"
|
|
239
|
+
if layer else f"RoleFunc_{cls._to_pascal(rf)}")
|
|
240
|
+
return f"(void){full}(NULL, ctx)"
|
|
241
|
+
|
|
242
|
+
def __init__(self, parent=None, state: Optional[State] = None,
|
|
243
|
+
state_machine: Optional[StateMachine] = None):
|
|
244
|
+
super().__init__(parent)
|
|
245
|
+
self.state = state
|
|
246
|
+
self.state_machine = state_machine
|
|
247
|
+
|
|
248
|
+
title_state = state.name if state is not None else "(unknown)"
|
|
249
|
+
self.setWindowTitle(f"State actions - {title_state}")
|
|
250
|
+
self.setMinimumSize(800, 600)
|
|
251
|
+
|
|
252
|
+
self._setup_ui()
|
|
253
|
+
self._load_state()
|
|
254
|
+
StaTableLogger.debug(
|
|
255
|
+
f"StateActionsDialog initialized: state={title_state}")
|
|
256
|
+
|
|
257
|
+
# ------------------------------------------------------------------
|
|
258
|
+
# UI
|
|
259
|
+
# ------------------------------------------------------------------
|
|
260
|
+
def _setup_ui(self):
|
|
261
|
+
layout = QVBoxLayout(self)
|
|
262
|
+
|
|
263
|
+
# Info label
|
|
264
|
+
info = QLabel(
|
|
265
|
+
self.tr("Entry / Exit / Do actions for this state.\n"
|
|
266
|
+
"Custom C code can be edited in the generated file "
|
|
267
|
+
"([[STABLE_USER_CODE_..._custom]] marker).")
|
|
268
|
+
)
|
|
269
|
+
info.setWordWrap(True)
|
|
270
|
+
layout.addWidget(info)
|
|
271
|
+
|
|
272
|
+
self.tabs = QTabWidget()
|
|
273
|
+
layout.addWidget(self.tabs)
|
|
274
|
+
|
|
275
|
+
# Entry / Exit / Do tabs
|
|
276
|
+
self.entry_widget = _ActionListWidget()
|
|
277
|
+
self.exit_widget = _ActionListWidget()
|
|
278
|
+
self.do_widget = _ActionListWidget()
|
|
279
|
+
|
|
280
|
+
self.tabs.addTab(self.entry_widget, KIND_ENTRY)
|
|
281
|
+
self.tabs.addTab(self.exit_widget, KIND_EXIT)
|
|
282
|
+
self.tabs.addTab(self.do_widget, KIND_DO)
|
|
283
|
+
|
|
284
|
+
# Preview tab
|
|
285
|
+
self.preview_widget = QPlainTextEdit()
|
|
286
|
+
self.preview_widget.setReadOnly(True)
|
|
287
|
+
self.preview_widget.setFont(QFont("Consolas", 10))
|
|
288
|
+
self.preview_widget.setPlaceholderText(
|
|
289
|
+
self.tr("Click 'Refresh' to preview the generated C code."))
|
|
290
|
+
preview_container = QWidget()
|
|
291
|
+
preview_layout = QVBoxLayout(preview_container)
|
|
292
|
+
refresh_btn = QPushButton(self.tr("Refresh preview"))
|
|
293
|
+
refresh_btn.clicked.connect(self._refresh_preview)
|
|
294
|
+
preview_layout.addWidget(refresh_btn)
|
|
295
|
+
preview_layout.addWidget(self.preview_widget)
|
|
296
|
+
self.tabs.addTab(preview_container, self.tr("Preview"))
|
|
297
|
+
|
|
298
|
+
# Buttons
|
|
299
|
+
buttons = QDialogButtonBox(
|
|
300
|
+
QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
|
|
301
|
+
buttons.accepted.connect(self._on_accept)
|
|
302
|
+
buttons.rejected.connect(self.reject)
|
|
303
|
+
layout.addWidget(buttons)
|
|
304
|
+
|
|
305
|
+
# ------------------------------------------------------------------
|
|
306
|
+
# Data binding
|
|
307
|
+
# ------------------------------------------------------------------
|
|
308
|
+
def _load_state(self):
|
|
309
|
+
if self.state is None:
|
|
310
|
+
return
|
|
311
|
+
self.entry_widget.set_actions(
|
|
312
|
+
list(getattr(self.state, "entry", []) or []))
|
|
313
|
+
self.exit_widget.set_actions(
|
|
314
|
+
list(getattr(self.state, "exit", []) or []))
|
|
315
|
+
self.do_widget.set_actions(
|
|
316
|
+
list(getattr(self.state, "do_actions", []) or []))
|
|
317
|
+
|
|
318
|
+
def _on_accept(self):
|
|
319
|
+
if self.state is not None:
|
|
320
|
+
self.state.entry = self.entry_widget.get_actions()
|
|
321
|
+
self.state.exit = self.exit_widget.get_actions()
|
|
322
|
+
self.state.do_actions = self.do_widget.get_actions()
|
|
323
|
+
StaTableLogger.debug(
|
|
324
|
+
f"StateActionsDialog OK: state={self.state.name}, "
|
|
325
|
+
f"entry={len(self.state.entry)}, "
|
|
326
|
+
f"exit={len(self.state.exit)}, "
|
|
327
|
+
f"do={len(self.state.do_actions)}")
|
|
328
|
+
self.accept()
|
|
329
|
+
|
|
330
|
+
# ------------------------------------------------------------------
|
|
331
|
+
# Preview
|
|
332
|
+
# ------------------------------------------------------------------
|
|
333
|
+
def _refresh_preview(self):
|
|
334
|
+
"""Generate a small preview of what would be emitted for this state."""
|
|
335
|
+
if self.state is None:
|
|
336
|
+
self.preview_widget.setPlainText("(no state)")
|
|
337
|
+
return
|
|
338
|
+
|
|
339
|
+
state_name = self.state.name
|
|
340
|
+
layer = "Layer"
|
|
341
|
+
if self.state_machine is not None:
|
|
342
|
+
layer = getattr(self.state_machine, "layer_name", "") or "Layer"
|
|
343
|
+
|
|
344
|
+
lines = []
|
|
345
|
+
for kind, actions in (
|
|
346
|
+
(KIND_ENTRY, self.entry_widget.get_actions()),
|
|
347
|
+
(KIND_EXIT, self.exit_widget.get_actions()),
|
|
348
|
+
(KIND_DO, self.do_widget.get_actions()),
|
|
349
|
+
):
|
|
350
|
+
func_name = f"{layer}_{kind}_{state_name}"
|
|
351
|
+
marker = f"{layer}_{kind}_{state_name}_custom"
|
|
352
|
+
lines.append(f"static void {func_name}(SystemContext_t *ctx)")
|
|
353
|
+
lines.append("{")
|
|
354
|
+
if actions:
|
|
355
|
+
lines.append(" /* --- GUI-edited actions (regenerated) --- */")
|
|
356
|
+
for a in actions:
|
|
357
|
+
atype = getattr(a, "action_type", "role") or "role"
|
|
358
|
+
cond = (getattr(a, "condition", "") or "").strip()
|
|
359
|
+
if atype == "fire_event":
|
|
360
|
+
evt = getattr(a, "event_name", "") or ""
|
|
361
|
+
# Mirror the codegen: FIRE_EVENT_<Layer>(ctx,
|
|
362
|
+
# EVENT_<Layer>_<UPPER_SNAKE>)
|
|
363
|
+
if "." in evt:
|
|
364
|
+
evt_layer, evt_name = evt.split(".", 1)
|
|
365
|
+
else:
|
|
366
|
+
evt_layer, evt_name = layer, evt
|
|
367
|
+
evt_upper = evt_name.upper().replace(" ", "_")
|
|
368
|
+
call = (f"FIRE_EVENT_{evt_layer}(ctx, "
|
|
369
|
+
f"EVENT_{evt_layer}_{evt_upper})")
|
|
370
|
+
else:
|
|
371
|
+
rf = getattr(a, "role_function", "") or ""
|
|
372
|
+
call = self._rf_call_preview(layer, rf)
|
|
373
|
+
if cond:
|
|
374
|
+
lines.append(f" if ({cond}) {{")
|
|
375
|
+
lines.append(f" {call};")
|
|
376
|
+
lines.append(" }")
|
|
377
|
+
else:
|
|
378
|
+
lines.append(f" {call};")
|
|
379
|
+
lines.append(" /* --- end GUI-edited actions --- */")
|
|
380
|
+
lines.append("")
|
|
381
|
+
lines.append(" /* --- user custom code (preserved) --- */")
|
|
382
|
+
lines.append(f" /* [[STABLE_USER_CODE_START:{marker}]] */")
|
|
383
|
+
lines.append(" (void)ctx;")
|
|
384
|
+
lines.append(f" /* [[STABLE_USER_CODE_END:{marker}]] */")
|
|
385
|
+
lines.append("}")
|
|
386
|
+
lines.append("")
|
|
387
|
+
|
|
388
|
+
self.preview_widget.setPlainText("\n".join(lines))
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
# ======================================================================
|
|
392
|
+
# Convenience entry point
|
|
393
|
+
# ======================================================================
|
|
394
|
+
def open_state_actions_dialog(parent, state, state_machine):
|
|
395
|
+
"""Open the dialog and return True if the user accepted."""
|
|
396
|
+
dlg = StateActionsDialog(
|
|
397
|
+
parent=parent, state=state, state_machine=state_machine)
|
|
398
|
+
return dlg.exec() == QDialog.Accepted
|