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,605 @@
|
|
|
1
|
+
"""State transition event definition dialog"""
|
|
2
|
+
|
|
3
|
+
from typing import Optional, List
|
|
4
|
+
|
|
5
|
+
from PySide6.QtCore import Qt
|
|
6
|
+
from PySide6.QtGui import QFont, QMouseEvent
|
|
7
|
+
from PySide6.QtWidgets import (
|
|
8
|
+
QDialog, QVBoxLayout, QHBoxLayout, QTableWidget, QTableWidgetItem,
|
|
9
|
+
QPushButton, QLabel, QHeaderView, QComboBox, QLineEdit,
|
|
10
|
+
QFormLayout, QDialogButtonBox, QMessageBox, QAbstractItemView,
|
|
11
|
+
QWidget, QRadioButton, QButtonGroup, QCheckBox, QSpinBox,
|
|
12
|
+
QGroupBox
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
from statable.state_machine import StateMachine
|
|
16
|
+
from statable.model import (Event, EventDeliveryType, EventSourceLayer,
|
|
17
|
+
EventKind, EventTrigger)
|
|
18
|
+
from statable.global_defs import GlobalDefinitions
|
|
19
|
+
from .common_widgets import TitleEditWidget, TypeComboBox
|
|
20
|
+
from .logger import StaTableLogger
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class DoubleClickTable(QTableWidget):
|
|
24
|
+
"""Table that reliably captures double-click events"""
|
|
25
|
+
def __init__(self, *args, **kwargs):
|
|
26
|
+
super().__init__(*args, **kwargs)
|
|
27
|
+
self.setEditTriggers(QAbstractItemView.NoEditTriggers)
|
|
28
|
+
self.setSelectionBehavior(QAbstractItemView.SelectRows)
|
|
29
|
+
self.setSelectionMode(QAbstractItemView.SingleSelection)
|
|
30
|
+
|
|
31
|
+
def mouseDoubleClickEvent(self, event: QMouseEvent):
|
|
32
|
+
pos = event.position().toPoint()
|
|
33
|
+
item = self.itemAt(pos)
|
|
34
|
+
if item:
|
|
35
|
+
row = item.row()
|
|
36
|
+
self.cellDoubleClicked.emit(row, item.column())
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class EventEditDialog(QDialog):
|
|
40
|
+
"""Edit state transition event dialog"""
|
|
41
|
+
|
|
42
|
+
def __init__(self, parent=None, event: Optional[Event] = None,
|
|
43
|
+
global_defs=None, state_machine=None):
|
|
44
|
+
super().__init__(parent)
|
|
45
|
+
self.global_defs = global_defs if global_defs else GlobalDefinitions()
|
|
46
|
+
self.state_machine = state_machine
|
|
47
|
+
self.setWindowTitle(self.tr("Edit state transition event"))
|
|
48
|
+
self.setMinimumWidth(500)
|
|
49
|
+
|
|
50
|
+
layout = QVBoxLayout(self)
|
|
51
|
+
form = QFormLayout()
|
|
52
|
+
layout.addLayout(form)
|
|
53
|
+
|
|
54
|
+
# Title input field (required / provisional title auto-set)
|
|
55
|
+
self.title_widget = TitleEditWidget(self, title=event.title if event else "")
|
|
56
|
+
form.addRow("", self.title_widget)
|
|
57
|
+
|
|
58
|
+
# Event name
|
|
59
|
+
self.name_edit = QLineEdit(event.name if event else "")
|
|
60
|
+
form.addRow("Event name", self.name_edit)
|
|
61
|
+
|
|
62
|
+
# Event ID
|
|
63
|
+
self.id_spin = QSpinBox()
|
|
64
|
+
self.id_spin.setRange(0, 65535)
|
|
65
|
+
if event and event.id is not None:
|
|
66
|
+
self.id_spin.setValue(event.id)
|
|
67
|
+
form.addRow("Event ID", self.id_spin)
|
|
68
|
+
|
|
69
|
+
# description
|
|
70
|
+
self.desc_edit = QLineEdit(event.description if event else "")
|
|
71
|
+
form.addRow("Description", self.desc_edit)
|
|
72
|
+
|
|
73
|
+
# Event kind
|
|
74
|
+
self.kind_combo = QComboBox()
|
|
75
|
+
for kind in EventKind:
|
|
76
|
+
self.kind_combo.addItem(kind.value, kind)
|
|
77
|
+
if event:
|
|
78
|
+
idx = self.kind_combo.findData(event.kind)
|
|
79
|
+
if idx >= 0:
|
|
80
|
+
self.kind_combo.setCurrentIndex(idx)
|
|
81
|
+
form.addRow("Event kind", self.kind_combo)
|
|
82
|
+
|
|
83
|
+
# Source layer
|
|
84
|
+
self.layer_driver = QRadioButton(self.tr("Driver layer"))
|
|
85
|
+
self.layer_middleware = QRadioButton(self.tr("Middle layer"))
|
|
86
|
+
group = QButtonGroup(self)
|
|
87
|
+
group.addButton(self.layer_driver)
|
|
88
|
+
group.addButton(self.layer_middleware)
|
|
89
|
+
if event and event.source_layer == EventSourceLayer.MIDDLEWARE:
|
|
90
|
+
self.layer_middleware.setChecked(True)
|
|
91
|
+
else:
|
|
92
|
+
self.layer_driver.setChecked(True)
|
|
93
|
+
layer_layout = QHBoxLayout()
|
|
94
|
+
layer_layout.addWidget(self.layer_driver)
|
|
95
|
+
layer_layout.addWidget(self.layer_middleware)
|
|
96
|
+
layer_layout.addStretch()
|
|
97
|
+
form.addRow("Source layer", layer_layout)
|
|
98
|
+
|
|
99
|
+
# Delivery type
|
|
100
|
+
self.delivery_combo = QComboBox()
|
|
101
|
+
self.delivery_combo.addItem(self.tr("DIRECT"), EventDeliveryType.DIRECT)
|
|
102
|
+
self.delivery_combo.addItem(self.tr("QUEUE"), EventDeliveryType.QUEUE)
|
|
103
|
+
self.delivery_combo.addItem(self.tr("DOUBLE"), EventDeliveryType.DOUBLE)
|
|
104
|
+
if event:
|
|
105
|
+
idx = self.delivery_combo.findData(event.delivery_type)
|
|
106
|
+
if idx >= 0:
|
|
107
|
+
self.delivery_combo.setCurrentIndex(idx)
|
|
108
|
+
form.addRow("Delivery type", self.delivery_combo)
|
|
109
|
+
|
|
110
|
+
# Attached data
|
|
111
|
+
self.data_check = QCheckBox(self.tr("Use attached data"))
|
|
112
|
+
self.data_check.setChecked(bool(event.data_type if event else False))
|
|
113
|
+
form.addRow("", self.data_check)
|
|
114
|
+
|
|
115
|
+
self.data_type_combo = TypeComboBox(self, global_defs=self.global_defs)
|
|
116
|
+
if event:
|
|
117
|
+
self.data_type_combo.set_current_text(event.data_type)
|
|
118
|
+
form.addRow("Data type", self.data_type_combo)
|
|
119
|
+
|
|
120
|
+
self.data_name_edit = QLineEdit(event.data_name if event else "")
|
|
121
|
+
form.addRow("Data variable name", self.data_name_edit)
|
|
122
|
+
|
|
123
|
+
self.data_check.toggled.connect(self._on_data_check_toggled)
|
|
124
|
+
self._on_data_check_toggled(self.data_check.isChecked())
|
|
125
|
+
|
|
126
|
+
# [C-51 Step 2] Trigger free-text field.
|
|
127
|
+
# Enabled only for kind in {signal, call, time};
|
|
128
|
+
# kind=change is handled inside transition conditions.
|
|
129
|
+
self.trigger_edit = QLineEdit(
|
|
130
|
+
getattr(event, 'trigger', '') if event else "")
|
|
131
|
+
self.trigger_edit.setPlaceholderText(
|
|
132
|
+
self.tr("e.g. GPIO edge (ISR), debounce 5ms / "
|
|
133
|
+
"periodic timer 10ms / direct call from Application"))
|
|
134
|
+
self.trigger_edit.setToolTip(
|
|
135
|
+
self.tr("Free-text description of when / from where this event fires.\n"
|
|
136
|
+
"Examples:\n"
|
|
137
|
+
" signal: GPIO edge (ISR), debounce 5ms\n"
|
|
138
|
+
" signal: from Application layer via OSAL queue\n"
|
|
139
|
+
" call: direct call from Middleware\n"
|
|
140
|
+
" time: periodic timer (10ms)\n"
|
|
141
|
+
"Not used for kind=change (use transition condition)."))
|
|
142
|
+
form.addRow("Trigger", self.trigger_edit)
|
|
143
|
+
# [C-51 Step 3] Structured trigger detail section
|
|
144
|
+
self._build_trigger_detail_section(layout, event)
|
|
145
|
+
self.kind_combo.currentIndexChanged.connect(
|
|
146
|
+
self._on_kind_changed)
|
|
147
|
+
self._on_kind_changed()
|
|
148
|
+
|
|
149
|
+
# OK/Cancel
|
|
150
|
+
buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
|
|
151
|
+
buttons.accepted.connect(self._on_accept)
|
|
152
|
+
buttons.rejected.connect(self.reject)
|
|
153
|
+
layout.addWidget(buttons)
|
|
154
|
+
|
|
155
|
+
StaTableLogger.debug("EventEditDialog initialized")
|
|
156
|
+
|
|
157
|
+
def _on_data_check_toggled(self, checked: bool):
|
|
158
|
+
self.data_type_combo.setEnabled(checked)
|
|
159
|
+
self.data_name_edit.setEnabled(checked)
|
|
160
|
+
|
|
161
|
+
def _on_kind_changed(self):
|
|
162
|
+
# [C-51 Step 2] trigger is meaningful only for
|
|
163
|
+
# signal / call / time. kind=change uses the
|
|
164
|
+
# transition condition instead.
|
|
165
|
+
kind = self.kind_combo.currentData()
|
|
166
|
+
enabled = kind in (
|
|
167
|
+
EventKind.SIGNAL, EventKind.CALL, EventKind.TIME)
|
|
168
|
+
self.trigger_edit.setEnabled(enabled)
|
|
169
|
+
if not enabled:
|
|
170
|
+
self.trigger_edit.setToolTip(
|
|
171
|
+
self.tr("kind=change fires via the transition condition; "
|
|
172
|
+
"trigger is not used."))
|
|
173
|
+
# [C-51 Step 3] keep the structured trigger section in sync
|
|
174
|
+
trig_group = getattr(self, 'trig_group', None)
|
|
175
|
+
if trig_group is not None:
|
|
176
|
+
trig_group.setEnabled(enabled)
|
|
177
|
+
|
|
178
|
+
# ------------------------------------------------------------------
|
|
179
|
+
# [C-51 Step 3] Structured trigger detail
|
|
180
|
+
# ------------------------------------------------------------------
|
|
181
|
+
def _build_trigger_detail_section(self, parent_layout, event):
|
|
182
|
+
"""Add collapsible Trigger detail QGroupBox."""
|
|
183
|
+
group = QGroupBox(self.tr("Trigger detail (structured)"))
|
|
184
|
+
group.setCheckable(True)
|
|
185
|
+
group_layout = QFormLayout(group)
|
|
186
|
+
|
|
187
|
+
td = getattr(event, 'trigger_detail', None) if event else None
|
|
188
|
+
group.setChecked(td is not None)
|
|
189
|
+
|
|
190
|
+
# Type
|
|
191
|
+
self.trig_type_combo = QComboBox()
|
|
192
|
+
for t in ("manual", "edge", "polling", "timer",
|
|
193
|
+
"call", "comparison"):
|
|
194
|
+
self.trig_type_combo.addItem(t, t)
|
|
195
|
+
if td is not None:
|
|
196
|
+
idx = self.trig_type_combo.findData(td.type)
|
|
197
|
+
if idx >= 0:
|
|
198
|
+
self.trig_type_combo.setCurrentIndex(idx)
|
|
199
|
+
group_layout.addRow("Type", self.trig_type_combo)
|
|
200
|
+
|
|
201
|
+
# Source (editable: allows custom entry)
|
|
202
|
+
self.trig_source_combo = QComboBox()
|
|
203
|
+
self.trig_source_combo.setEditable(True)
|
|
204
|
+
self.trig_source_combo.setInsertPolicy(QComboBox.NoInsert)
|
|
205
|
+
if td is not None and td.source:
|
|
206
|
+
self.trig_source_combo.setCurrentText(td.source)
|
|
207
|
+
group_layout.addRow("Source", self.trig_source_combo)
|
|
208
|
+
|
|
209
|
+
# Edge (edge type)
|
|
210
|
+
self.trig_edge_combo = QComboBox()
|
|
211
|
+
for e in ("rising", "falling", "both"):
|
|
212
|
+
self.trig_edge_combo.addItem(e, e)
|
|
213
|
+
if td is not None and td.edge:
|
|
214
|
+
idx = self.trig_edge_combo.findData(td.edge)
|
|
215
|
+
if idx >= 0:
|
|
216
|
+
self.trig_edge_combo.setCurrentIndex(idx)
|
|
217
|
+
self.trig_edge_row = self._register_row(
|
|
218
|
+
group_layout, "Edge", self.trig_edge_combo)
|
|
219
|
+
|
|
220
|
+
# Debounce (edge type)
|
|
221
|
+
self.trig_debounce_spin = QSpinBox()
|
|
222
|
+
self.trig_debounce_spin.setRange(0, 100000)
|
|
223
|
+
self.trig_debounce_spin.setSuffix(" ms")
|
|
224
|
+
if td is not None:
|
|
225
|
+
self.trig_debounce_spin.setValue(td.debounce_ms)
|
|
226
|
+
self.trig_debounce_row = self._register_row(
|
|
227
|
+
group_layout, "Debounce", self.trig_debounce_spin)
|
|
228
|
+
|
|
229
|
+
# Period (polling / timer)
|
|
230
|
+
self.trig_period_spin = QSpinBox()
|
|
231
|
+
self.trig_period_spin.setRange(0, 10000000)
|
|
232
|
+
self.trig_period_spin.setSuffix(" ms")
|
|
233
|
+
if td is not None:
|
|
234
|
+
self.trig_period_spin.setValue(td.period_ms)
|
|
235
|
+
self.trig_period_row = self._register_row(
|
|
236
|
+
group_layout, "Period", self.trig_period_spin)
|
|
237
|
+
|
|
238
|
+
# Auto reload (timer)
|
|
239
|
+
self.trig_autoreload_check = QCheckBox(self.tr("Auto reload"))
|
|
240
|
+
self.trig_autoreload_check.setChecked(
|
|
241
|
+
td.auto_reload if td is not None else True)
|
|
242
|
+
self.trig_autoreload_row = self._register_row(
|
|
243
|
+
group_layout, "Timer", self.trig_autoreload_check)
|
|
244
|
+
|
|
245
|
+
# Caller (call)
|
|
246
|
+
self.trig_caller_combo = QComboBox()
|
|
247
|
+
self.trig_caller_combo.setEditable(True)
|
|
248
|
+
self.trig_caller_combo.setInsertPolicy(QComboBox.NoInsert)
|
|
249
|
+
if td is not None and td.caller:
|
|
250
|
+
self.trig_caller_combo.setCurrentText(td.caller)
|
|
251
|
+
self.trig_caller_row = self._register_row(
|
|
252
|
+
group_layout, "Caller", self.trig_caller_combo)
|
|
253
|
+
|
|
254
|
+
# Condition + Build (comparison)
|
|
255
|
+
self.trig_condition_edit = QLineEdit()
|
|
256
|
+
self.trig_condition_build_btn = QPushButton(self.tr("Build..."))
|
|
257
|
+
self.trig_condition_build_btn.clicked.connect(
|
|
258
|
+
self._open_trigger_condition_builder)
|
|
259
|
+
cond_widget = QWidget()
|
|
260
|
+
cond_layout = QHBoxLayout(cond_widget)
|
|
261
|
+
cond_layout.setContentsMargins(0, 0, 0, 0)
|
|
262
|
+
cond_layout.addWidget(self.trig_condition_edit, 1)
|
|
263
|
+
cond_layout.addWidget(self.trig_condition_build_btn)
|
|
264
|
+
if td is not None:
|
|
265
|
+
self.trig_condition_edit.setText(td.condition)
|
|
266
|
+
self.trig_condition_row = self._register_row(
|
|
267
|
+
group_layout, "Condition", cond_widget)
|
|
268
|
+
|
|
269
|
+
# Poll period (comparison)
|
|
270
|
+
self.trig_pollperiod_spin = QSpinBox()
|
|
271
|
+
self.trig_pollperiod_spin.setRange(0, 10000000)
|
|
272
|
+
self.trig_pollperiod_spin.setSuffix(" ms")
|
|
273
|
+
if td is not None:
|
|
274
|
+
self.trig_pollperiod_spin.setValue(td.poll_period_ms)
|
|
275
|
+
self.trig_pollperiod_row = self._register_row(
|
|
276
|
+
group_layout, "Poll period", self.trig_pollperiod_spin)
|
|
277
|
+
|
|
278
|
+
# Description (optional)
|
|
279
|
+
self.trig_desc_edit = QLineEdit()
|
|
280
|
+
if td is not None:
|
|
281
|
+
self.trig_desc_edit.setText(td.description)
|
|
282
|
+
group_layout.addRow("Description", self.trig_desc_edit)
|
|
283
|
+
|
|
284
|
+
# Wire up
|
|
285
|
+
self.trig_type_combo.currentIndexChanged.connect(
|
|
286
|
+
self._on_trigger_type_changed)
|
|
287
|
+
group.toggled.connect(lambda _c: self._on_trigger_type_changed())
|
|
288
|
+
|
|
289
|
+
parent_layout.addWidget(group)
|
|
290
|
+
self.trig_group = group
|
|
291
|
+
|
|
292
|
+
# Initial state
|
|
293
|
+
self._on_trigger_type_changed()
|
|
294
|
+
|
|
295
|
+
def _register_row(self, layout, label_text, widget):
|
|
296
|
+
"""Add a QFormLayout row; return (label, widget) for visibility."""
|
|
297
|
+
layout.addRow(label_text, widget)
|
|
298
|
+
label = layout.labelForField(widget)
|
|
299
|
+
return (label, widget)
|
|
300
|
+
|
|
301
|
+
def _set_row_visible(self, row, visible):
|
|
302
|
+
lbl, w = row
|
|
303
|
+
if lbl is not None:
|
|
304
|
+
lbl.setVisible(visible)
|
|
305
|
+
w.setVisible(visible)
|
|
306
|
+
|
|
307
|
+
def _on_trigger_type_changed(self):
|
|
308
|
+
"""Show/hide dynamic fields based on Type; refresh Source choices."""
|
|
309
|
+
t = self.trig_type_combo.currentData()
|
|
310
|
+
|
|
311
|
+
self._set_row_visible(self.trig_edge_row, t == "edge")
|
|
312
|
+
self._set_row_visible(self.trig_debounce_row, t == "edge")
|
|
313
|
+
self._set_row_visible(self.trig_period_row,
|
|
314
|
+
t in ("polling", "timer"))
|
|
315
|
+
self._set_row_visible(self.trig_autoreload_row, t == "timer")
|
|
316
|
+
self._set_row_visible(self.trig_caller_row, t == "call")
|
|
317
|
+
self._set_row_visible(self.trig_condition_row, t == "comparison")
|
|
318
|
+
self._set_row_visible(self.trig_pollperiod_row, t == "comparison")
|
|
319
|
+
|
|
320
|
+
self._populate_source_choices(t)
|
|
321
|
+
|
|
322
|
+
def _populate_source_choices(self, trigger_type):
|
|
323
|
+
"""Populate Source dropdown from GlobalDefinitions / role_functions."""
|
|
324
|
+
current = self.trig_source_combo.currentText().strip()
|
|
325
|
+
self.trig_source_combo.blockSignals(True)
|
|
326
|
+
self.trig_source_combo.clear()
|
|
327
|
+
self.trig_source_combo.addItem(self.tr(""))
|
|
328
|
+
|
|
329
|
+
candidates = []
|
|
330
|
+
if trigger_type == "edge":
|
|
331
|
+
for intr in getattr(self.global_defs, 'interrupts', []) or []:
|
|
332
|
+
if getattr(intr, 'name', ''):
|
|
333
|
+
candidates.append(intr.name)
|
|
334
|
+
elif trigger_type in ("polling", "timer"):
|
|
335
|
+
tb = getattr(self.global_defs, 'timer_base', None)
|
|
336
|
+
if tb is not None and getattr(tb, 'variable_name', ''):
|
|
337
|
+
candidates.append(tb.variable_name)
|
|
338
|
+
for t in getattr(self.global_defs, 'extra_timers', []) or []:
|
|
339
|
+
if getattr(t, 'variable_name', ''):
|
|
340
|
+
candidates.append(t.variable_name)
|
|
341
|
+
elif trigger_type == "call":
|
|
342
|
+
if self.state_machine is not None:
|
|
343
|
+
for rf in self.state_machine.role_functions.values():
|
|
344
|
+
ns = getattr(rf, 'namespace', '')
|
|
345
|
+
n = getattr(rf, 'name', '')
|
|
346
|
+
qn = f"{ns}.{n}" if ns else n
|
|
347
|
+
if qn:
|
|
348
|
+
candidates.append(qn)
|
|
349
|
+
# comparison: no candidates (use Build button)
|
|
350
|
+
|
|
351
|
+
for c in candidates:
|
|
352
|
+
self.trig_source_combo.addItem(c)
|
|
353
|
+
|
|
354
|
+
if current:
|
|
355
|
+
self.trig_source_combo.setCurrentText(current)
|
|
356
|
+
self.trig_source_combo.blockSignals(False)
|
|
357
|
+
|
|
358
|
+
def _open_trigger_condition_builder(self):
|
|
359
|
+
"""Open ConditionBuilderDialog for comparison-type condition."""
|
|
360
|
+
try:
|
|
361
|
+
from .condition_builder_dialog import ConditionBuilderDialog
|
|
362
|
+
except ImportError:
|
|
363
|
+
return
|
|
364
|
+
dlg = ConditionBuilderDialog(
|
|
365
|
+
condition=self.trig_condition_edit.text(),
|
|
366
|
+
event_name=self.name_edit.text().strip(),
|
|
367
|
+
global_defs=self.global_defs,
|
|
368
|
+
state_machine=self.state_machine,
|
|
369
|
+
parent=self,
|
|
370
|
+
)
|
|
371
|
+
if dlg.exec() == QDialog.Accepted:
|
|
372
|
+
self.trig_condition_edit.setText(dlg.get_condition_text())
|
|
373
|
+
|
|
374
|
+
def _get_trigger_detail(self):
|
|
375
|
+
"""Build EventTrigger from UI, or None if section unchecked."""
|
|
376
|
+
if not self.trig_group.isChecked():
|
|
377
|
+
return None
|
|
378
|
+
t = self.trig_type_combo.currentData()
|
|
379
|
+
kwargs = {
|
|
380
|
+
"type": t,
|
|
381
|
+
"source": self.trig_source_combo.currentText().strip(),
|
|
382
|
+
"description": self.trig_desc_edit.text().strip(),
|
|
383
|
+
}
|
|
384
|
+
if t == "edge":
|
|
385
|
+
kwargs["edge"] = self.trig_edge_combo.currentData()
|
|
386
|
+
kwargs["debounce_ms"] = self.trig_debounce_spin.value()
|
|
387
|
+
elif t == "polling":
|
|
388
|
+
kwargs["period_ms"] = self.trig_period_spin.value()
|
|
389
|
+
elif t == "timer":
|
|
390
|
+
kwargs["period_ms"] = self.trig_period_spin.value()
|
|
391
|
+
kwargs["auto_reload"] = self.trig_autoreload_check.isChecked()
|
|
392
|
+
elif t == "call":
|
|
393
|
+
kwargs["caller"] = self.trig_caller_combo.currentText().strip()
|
|
394
|
+
elif t == "comparison":
|
|
395
|
+
kwargs["condition"] = self.trig_condition_edit.text().strip()
|
|
396
|
+
kwargs["poll_period_ms"] = self.trig_pollperiod_spin.value()
|
|
397
|
+
return EventTrigger(**kwargs)
|
|
398
|
+
|
|
399
|
+
def _on_accept(self):
|
|
400
|
+
"""OK button: auto-set provisional title if title is empty"""
|
|
401
|
+
auto_title = f"Event: {self.name_edit.text().strip() or '(unnamed)'}"
|
|
402
|
+
self.title_widget.ensure_title(auto_title)
|
|
403
|
+
self.accept()
|
|
404
|
+
|
|
405
|
+
def get_event(self) -> Event:
|
|
406
|
+
source_layer = EventSourceLayer.MIDDLEWARE if self.layer_middleware.isChecked() else EventSourceLayer.DRIVER
|
|
407
|
+
data_type = self.data_type_combo.current_text() if self.data_check.isChecked() else ""
|
|
408
|
+
data_name = self.data_name_edit.text().strip() if self.data_check.isChecked() else ""
|
|
409
|
+
return Event(
|
|
410
|
+
name=self.name_edit.text().strip(),
|
|
411
|
+
id=self.id_spin.value(),
|
|
412
|
+
kind=self.kind_combo.currentData(),
|
|
413
|
+
description=self.desc_edit.text().strip(),
|
|
414
|
+
delivery_type=self.delivery_combo.currentData(),
|
|
415
|
+
source_layer=source_layer,
|
|
416
|
+
data_type=data_type,
|
|
417
|
+
data_name=data_name,
|
|
418
|
+
title=self.title_widget.get_title(),
|
|
419
|
+
# [C-51 Step 2] Only meaningful for signal/call/time.
|
|
420
|
+
# For other kinds, store "" regardless of UI state.
|
|
421
|
+
trigger=(self.trigger_edit.text().strip()
|
|
422
|
+
if self.kind_combo.currentData() in (
|
|
423
|
+
EventKind.SIGNAL, EventKind.CALL,
|
|
424
|
+
EventKind.TIME)
|
|
425
|
+
else ""),
|
|
426
|
+
# [C-51 Step 3] Structured trigger detail
|
|
427
|
+
trigger_detail=self._get_trigger_detail(),
|
|
428
|
+
)
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
class EventDefinitionDialog(QDialog):
|
|
432
|
+
"""State transition event definition list dialog"""
|
|
433
|
+
|
|
434
|
+
def __init__(self, sm: StateMachine, global_defs: Optional[GlobalDefinitions] = None, parent=None):
|
|
435
|
+
super().__init__(parent)
|
|
436
|
+
self.sm = sm
|
|
437
|
+
self.global_defs = global_defs if global_defs else GlobalDefinitions()
|
|
438
|
+
self.setWindowTitle(self.tr("StateTransitionEvent definitions"))
|
|
439
|
+
self.setMinimumSize(1100, 650)
|
|
440
|
+
|
|
441
|
+
layout = QVBoxLayout(self)
|
|
442
|
+
|
|
443
|
+
# Search
|
|
444
|
+
search_layout = QHBoxLayout()
|
|
445
|
+
search_layout.addWidget(QLabel(self.tr("Search (prefix match):")))
|
|
446
|
+
self.search_edit = QLineEdit()
|
|
447
|
+
self.search_edit.setPlaceholderText(self.tr("Title, event name, description"))
|
|
448
|
+
self.search_edit.textChanged.connect(self.refresh_table)
|
|
449
|
+
search_layout.addWidget(self.search_edit)
|
|
450
|
+
layout.addLayout(search_layout)
|
|
451
|
+
|
|
452
|
+
# Event list table (title column added / direct edit)
|
|
453
|
+
self.table = DoubleClickTable(0, 7)
|
|
454
|
+
self.table.setHorizontalHeaderLabels(["Title", "Event name", "ID", "Source", "Delivery type", "Attached data", "Description"])
|
|
455
|
+
self.table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
|
|
456
|
+
self.table.setFont(QFont("Consolas", 10))
|
|
457
|
+
self.table.cellDoubleClicked.connect(self.on_double_clicked)
|
|
458
|
+
self.table.itemChanged.connect(self.on_item_changed)
|
|
459
|
+
layout.addWidget(self.table, stretch=1)
|
|
460
|
+
|
|
461
|
+
# Buttons
|
|
462
|
+
btn_layout = QHBoxLayout()
|
|
463
|
+
add_btn = QPushButton(self.tr("Add"))
|
|
464
|
+
add_btn.clicked.connect(self.add_event)
|
|
465
|
+
del_btn = QPushButton(self.tr("Delete"))
|
|
466
|
+
del_btn.clicked.connect(self.delete_event)
|
|
467
|
+
btn_layout.addWidget(add_btn)
|
|
468
|
+
btn_layout.addWidget(del_btn)
|
|
469
|
+
btn_layout.addStretch()
|
|
470
|
+
layout.addLayout(btn_layout)
|
|
471
|
+
|
|
472
|
+
# Close
|
|
473
|
+
close_btn = QPushButton(self.tr("Close"))
|
|
474
|
+
close_btn.clicked.connect(self.accept)
|
|
475
|
+
layout.addWidget(close_btn, alignment=Qt.AlignRight)
|
|
476
|
+
|
|
477
|
+
self.refresh_table()
|
|
478
|
+
StaTableLogger.debug("EventDefinitionDialog initialized")
|
|
479
|
+
|
|
480
|
+
def refresh_table(self):
|
|
481
|
+
query = self.search_edit.text().strip().lower() if hasattr(self, 'search_edit') else ""
|
|
482
|
+
self.table.setRowCount(0)
|
|
483
|
+
for event in self.sm.events.values():
|
|
484
|
+
name = event.name if event.name else "(completion)"
|
|
485
|
+
desc = event.description
|
|
486
|
+
title = event.title
|
|
487
|
+
if query and not (title.lower().startswith(query) or name.lower().startswith(query) or desc.lower().startswith(query)):
|
|
488
|
+
continue
|
|
489
|
+
row = self.table.rowCount()
|
|
490
|
+
self.table.insertRow(row)
|
|
491
|
+
|
|
492
|
+
# Title (directly editable)
|
|
493
|
+
title_item = QTableWidgetItem(title)
|
|
494
|
+
title_item.setToolTip(self.tr("Title of this event. Can be edited directly."))
|
|
495
|
+
self.table.setItem(row, 0, title_item)
|
|
496
|
+
|
|
497
|
+
self.table.setItem(row, 1, QTableWidgetItem(name))
|
|
498
|
+
self.table.setItem(row, 2, QTableWidgetItem(str(event.id) if event.id is not None else ""))
|
|
499
|
+
self.table.setItem(row, 3, QTableWidgetItem(event.source_layer.value))
|
|
500
|
+
self.table.setItem(row, 4, QTableWidgetItem(event.delivery_type.value))
|
|
501
|
+
data_text = f"{event.data_type} {event.data_name}" if event.data_type else ""
|
|
502
|
+
self.table.setItem(row, 5, QTableWidgetItem(data_text))
|
|
503
|
+
self.table.setItem(row, 6, QTableWidgetItem(event.description))
|
|
504
|
+
|
|
505
|
+
def on_item_changed(self, item):
|
|
506
|
+
"""Handler when the title column is edited directly"""
|
|
507
|
+
if item.column() == 0:
|
|
508
|
+
row = item.row()
|
|
509
|
+
name_item = self.table.item(row, 1)
|
|
510
|
+
if name_item:
|
|
511
|
+
name = name_item.text()
|
|
512
|
+
if name == "(completion)":
|
|
513
|
+
name = ""
|
|
514
|
+
if name in self.sm.events:
|
|
515
|
+
new_title = item.text().strip() or f"Event: {name or '(Completion)'}"
|
|
516
|
+
self.sm.events[name].title = new_title
|
|
517
|
+
|
|
518
|
+
def _find_event_by_row(self, row: int) -> Optional[Event]:
|
|
519
|
+
name_item = self.table.item(row, 1)
|
|
520
|
+
if not name_item:
|
|
521
|
+
return None
|
|
522
|
+
name = name_item.text()
|
|
523
|
+
if name == "(completion)":
|
|
524
|
+
name = ""
|
|
525
|
+
return self.sm.events.get(name)
|
|
526
|
+
|
|
527
|
+
def on_double_clicked(self, row, col):
|
|
528
|
+
if col == 0:
|
|
529
|
+
return
|
|
530
|
+
event = self._find_event_by_row(row)
|
|
531
|
+
if not event:
|
|
532
|
+
return
|
|
533
|
+
dlg = EventEditDialog(self, event=event,
|
|
534
|
+
global_defs=self.global_defs,
|
|
535
|
+
state_machine=self.sm)
|
|
536
|
+
if dlg.exec() == QDialog.Accepted:
|
|
537
|
+
new_event = dlg.get_event()
|
|
538
|
+
if not new_event.name:
|
|
539
|
+
QMessageBox.warning(self, self.tr("Warning"), self.tr("Please enter an event name."))
|
|
540
|
+
return
|
|
541
|
+
# Handler when the name changes
|
|
542
|
+
old_name = event.name
|
|
543
|
+
if old_name != new_event.name:
|
|
544
|
+
if new_event.name in self.sm.events:
|
|
545
|
+
QMessageBox.warning(self, self.tr("Warning"), f"Event '{new_event.name}' already exists.")
|
|
546
|
+
return
|
|
547
|
+
#Rebuild dict preserving original positions
|
|
548
|
+
new_events = {}
|
|
549
|
+
for key, value in self.sm.events.items():
|
|
550
|
+
if key == old_name:
|
|
551
|
+
new_events[new_event.name] = new_event
|
|
552
|
+
else:
|
|
553
|
+
new_events[key] = value
|
|
554
|
+
self.sm.events = new_events
|
|
555
|
+
for trans in self.sm.transitions:
|
|
556
|
+
if trans.event == old_name:
|
|
557
|
+
trans.event = new_event.name
|
|
558
|
+
else:
|
|
559
|
+
self.sm.events[old_name] = new_event
|
|
560
|
+
|
|
561
|
+
self.refresh_table()
|
|
562
|
+
StaTableLogger.info(f"Event updated: {new_event.name}")
|
|
563
|
+
|
|
564
|
+
def add_event(self):
|
|
565
|
+
dlg = EventEditDialog(self, global_defs=self.global_defs)
|
|
566
|
+
if dlg.exec() == QDialog.Accepted:
|
|
567
|
+
event = dlg.get_event()
|
|
568
|
+
if not event.name:
|
|
569
|
+
QMessageBox.warning(self, self.tr("Warning"), self.tr("Please enter an event name."))
|
|
570
|
+
return
|
|
571
|
+
if event.name in self.sm.events:
|
|
572
|
+
QMessageBox.warning(self, self.tr("Warning"), f"Event '{event.name}' already exists.")
|
|
573
|
+
return
|
|
574
|
+
self.sm.add_event(event)
|
|
575
|
+
self.refresh_table()
|
|
576
|
+
StaTableLogger.info(f"Event added: {event.name}")
|
|
577
|
+
|
|
578
|
+
def delete_event(self):
|
|
579
|
+
row = self.table.currentRow()
|
|
580
|
+
if row < 0:
|
|
581
|
+
return
|
|
582
|
+
event = self._find_event_by_row(row)
|
|
583
|
+
if not event:
|
|
584
|
+
return
|
|
585
|
+
|
|
586
|
+
# Dependency check
|
|
587
|
+
transitions = self.sm.get_transitions_for_event(event.name)
|
|
588
|
+
if transitions:
|
|
589
|
+
msg = f"Event '{event.title}' cannot be deleted.\n\nUsed by the following transitions:\n\n"
|
|
590
|
+
for trans in transitions:
|
|
591
|
+
msg += f" - {trans.source} → {trans.target}\n"
|
|
592
|
+
msg += "\nPlease delete these transitions first."
|
|
593
|
+
QMessageBox.warning(self, self.tr("Warning"), msg)
|
|
594
|
+
return
|
|
595
|
+
|
|
596
|
+
# Interrupt handler check
|
|
597
|
+
for intr in self.global_defs.interrupts:
|
|
598
|
+
if event.name in intr.event_names:
|
|
599
|
+
QMessageBox.warning(self, self.tr("Warning"), f"Event '{event.title}' is used by interrupt handler '{intr.title}'.")
|
|
600
|
+
return
|
|
601
|
+
reply = QMessageBox.question(self, self.tr("Confirm"), f"Event '{event.title}': confirm delete?", QMessageBox.Yes | QMessageBox.No)
|
|
602
|
+
if reply == QMessageBox.Yes:
|
|
603
|
+
self.sm.remove_event(event.name)
|
|
604
|
+
self.refresh_table()
|
|
605
|
+
StaTableLogger.info(f"Event deleted: {event.name}")
|