statable 3.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. codegen/__init__.py +38 -0
  2. codegen/c_code_generator.py +1752 -0
  3. codegen/code_merger.py +371 -0
  4. codegen/code_templates.py +509 -0
  5. codegen/config.py +167 -0
  6. codegen/enum_generator.py +377 -0
  7. codegen/event_queue_generator.py +303 -0
  8. codegen/interrupt_generator.py +345 -0
  9. codegen/naming_convention.py +171 -0
  10. codegen/osal_generator.py +282 -0
  11. codegen/role_function_generator.py +1244 -0
  12. codegen/sample_data.py +269 -0
  13. codegen/state_actions_generator.py +395 -0
  14. codegen/struct_generator.py +721 -0
  15. codegen/timer_generator.py +299 -0
  16. codegen/transition_generator.py +907 -0
  17. codegen/type_mapper.py +69 -0
  18. codegen/validate/__init__.py +38 -0
  19. codegen/validate/change_actions.py +90 -0
  20. codegen/validate/change_applier.py +416 -0
  21. codegen/validate/clipboard_manager.py +47 -0
  22. codegen/validate/data/__init__.py +30 -0
  23. codegen/validate/data/action_definitions.py +200 -0
  24. codegen/validate/data/keywords.py +46 -0
  25. codegen/validate/data/prompt_templates.py +176 -0
  26. codegen/validate/data/validation_rules.py +205 -0
  27. codegen/validate/items/__init__.py +30 -0
  28. codegen/validate/items/base_validator.py +25 -0
  29. codegen/validate/items/cell_validator.py +275 -0
  30. codegen/validate/items/custom_type_validator.py +75 -0
  31. codegen/validate/items/event_validator.py +78 -0
  32. codegen/validate/items/flag_validator.py +74 -0
  33. codegen/validate/items/interrupt_validator.py +75 -0
  34. codegen/validate/items/queue_validator.py +73 -0
  35. codegen/validate/items/role_function_validator.py +175 -0
  36. codegen/validate/items/state_validator.py +88 -0
  37. codegen/validate/items/timer_validator.py +120 -0
  38. codegen/validate/items/transition_validator.py +113 -0
  39. codegen/validate/items/variable_validator.py +132 -0
  40. codegen/validate/logger.py +52 -0
  41. codegen/validate/models.py +177 -0
  42. codegen/validate/prompt_generator.py +724 -0
  43. codegen/validate/response_parser.py +154 -0
  44. codegen/validate/response_validator.py +275 -0
  45. codegen/validate/validation_dialog.py +322 -0
  46. codegen/validate/validator.py +94 -0
  47. codegen/variable_generator.py +419 -0
  48. statable/__init__.py +71 -0
  49. statable/__main__.py +49 -0
  50. statable/cli.py +251 -0
  51. statable/global_defs.py +256 -0
  52. statable/mermaid_gen.py +144 -0
  53. statable/model.py +426 -0
  54. statable/parser.py +64 -0
  55. statable/sample_data.py +435 -0
  56. statable/shared/__init__.py +24 -0
  57. statable/shared/condition_library.py +63 -0
  58. statable/shared/literal_library.py +66 -0
  59. statable/shared/role_function_library.py +150 -0
  60. statable/state_machine.py +121 -0
  61. statable/xml_io.py +1164 -0
  62. statable-3.1.0.dist-info/METADATA +151 -0
  63. statable-3.1.0.dist-info/RECORD +117 -0
  64. statable-3.1.0.dist-info/WHEEL +5 -0
  65. statable-3.1.0.dist-info/entry_points.txt +2 -0
  66. statable-3.1.0.dist-info/top_level.txt +3 -0
  67. statable_gui/__init__.py +23 -0
  68. statable_gui/action_edit_dialog.py +214 -0
  69. statable_gui/code_generation_dialog.py +486 -0
  70. statable_gui/code_generation_settings_dialog.py +522 -0
  71. statable_gui/common_widgets.py +537 -0
  72. statable_gui/condition_builder_dialog.py +500 -0
  73. statable_gui/condition_edit_dialog.py +123 -0
  74. statable_gui/condition_template_dialog.py +58 -0
  75. statable_gui/config.py +21 -0
  76. statable_gui/dialogs.py +439 -0
  77. statable_gui/event_definition_dialog.py +605 -0
  78. statable_gui/event_delivery_settings_dialog.py +191 -0
  79. statable_gui/event_queue_dialog.py +245 -0
  80. statable_gui/global_defs.py +167 -0
  81. statable_gui/global_defs_dialog.py +796 -0
  82. statable_gui/i18n/__init__.py +118 -0
  83. statable_gui/interrupt_handler_edit_dialog.py +810 -0
  84. statable_gui/layer_settings_dialog.py +180 -0
  85. statable_gui/libcntrl/__init__.py +23 -0
  86. statable_gui/libcntrl/condition_library.py +13 -0
  87. statable_gui/libcntrl/literal_library.py +13 -0
  88. statable_gui/libcntrl/literal_management_dialog.py +185 -0
  89. statable_gui/libcntrl/role_function_edit_dialog.py +145 -0
  90. statable_gui/libcntrl/role_function_library.py +13 -0
  91. statable_gui/literal_definition_dialog.py +62 -0
  92. statable_gui/logger.py +83 -0
  93. statable_gui/main_window.py +1380 -0
  94. statable_gui/matrix_table.py +413 -0
  95. statable_gui/preference_keys.py +18 -0
  96. statable_gui/preferences.py +74 -0
  97. statable_gui/role_function_dialog.py +244 -0
  98. statable_gui/sample_data.py +11 -0
  99. statable_gui/state_actions_dialog.py +398 -0
  100. statable_gui/symbol_picker.py +170 -0
  101. statable_gui/traceball.py +37 -0
  102. statable_gui/transition_editor_direct/__init__.py +15 -0
  103. statable_gui/transition_editor_direct/actions_tab.py +856 -0
  104. statable_gui/transition_editor_direct/code_widget.py +344 -0
  105. statable_gui/transition_editor_direct/coverage_analyzer.py +163 -0
  106. statable_gui/transition_editor_direct/dialog.py +335 -0
  107. statable_gui/transition_editor_direct/draft.py +270 -0
  108. statable_gui/transition_editor_direct/edit_dialogs.py +160 -0
  109. statable_gui/transition_editor_direct/overview_tab.py +173 -0
  110. statable_gui/transition_editor_direct/palette_widget.py +233 -0
  111. statable_gui/transition_editor_direct/relations_edit_dialog.py +156 -0
  112. statable_gui/transition_editor_direct/relations_tab.py +230 -0
  113. statable_gui/transition_editor_direct/system_global_dialog.py +52 -0
  114. statable_gui/transition_editor_direct/transition_actions_dialog.py +109 -0
  115. statable_gui/transition_editor_direct/transitions_tab.py +620 -0
  116. statable_gui/validation_dialog.py +285 -0
  117. statable_gui/widgets.py +1315 -0
@@ -0,0 +1,810 @@
1
+ """Management dialog for interrupt handlers, device resources, timer settings (multi-timer support / ISR support)"""
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, QTabWidget, QWidget,
9
+ QTableWidget, QTableWidgetItem, QPushButton, QLabel, QHeaderView,
10
+ QComboBox, QLineEdit, QDialogButtonBox, QMessageBox,
11
+ QAbstractItemView, QSpinBox, QCheckBox, QFormLayout,
12
+ QInputDialog
13
+ )
14
+
15
+ from statable.global_defs import (
16
+ GlobalDefinitions,
17
+ InterruptHandlerDef,
18
+ InterruptAction,
19
+ DevicePlaceholderDef,
20
+ TimerBaseDef,
21
+ TimerDerivedDef,
22
+ )
23
+
24
+ from .condition_edit_dialog import ConditionEditDialog
25
+ from .action_edit_dialog import ActionEditDialog
26
+ from .logger import StaTableLogger
27
+
28
+
29
+ class DoubleClickTable(QTableWidget):
30
+ def __init__(self, *args, **kwargs):
31
+ super().__init__(*args, **kwargs)
32
+ self.setEditTriggers(QAbstractItemView.NoEditTriggers)
33
+ self.setSelectionBehavior(QAbstractItemView.SelectRows)
34
+ self.setSelectionMode(QAbstractItemView.SingleSelection)
35
+
36
+ def mouseDoubleClickEvent(self, event: QMouseEvent):
37
+ pos = event.position().toPoint()
38
+ item = self.itemAt(pos)
39
+ if item:
40
+ row = item.row()
41
+ StaTableLogger.debug(f"DoubleClickTable.mouseDoubleClickEvent: row={row}")
42
+ self.cellDoubleClicked.emit(row, item.column())
43
+ else:
44
+ StaTableLogger.debug("DoubleClickTable.mouseDoubleClickEvent: no item")
45
+
46
+
47
+ class InterruptEditDialog(QDialog):
48
+ def __init__(
49
+ self,
50
+ parent=None,
51
+ event_names=None,
52
+ interrupt: Optional[InterruptHandlerDef] = None,
53
+ global_defs: Optional[GlobalDefinitions] = None,
54
+ role_functions: Optional[dict] = None,
55
+ ):
56
+ super().__init__(parent)
57
+ self.setWindowTitle(self.tr("Edit interrupt handler"))
58
+ self.setMinimumSize(1100, 800)
59
+ self.event_names = event_names or []
60
+ self.global_defs = global_defs if global_defs else GlobalDefinitions()
61
+ self.role_functions = role_functions if role_functions is not None else {}
62
+
63
+ layout = QVBoxLayout(self)
64
+
65
+ title = QLabel(self.tr("Edit interrupt handler"))
66
+ title.setFont(QFont("sans-serif", 14, QFont.Bold))
67
+ title.setAlignment(Qt.AlignCenter)
68
+ layout.addWidget(title)
69
+
70
+ form = QFormLayout()
71
+ layout.addLayout(form)
72
+
73
+ self.title_edit = QLineEdit()
74
+ if interrupt:
75
+ self.title_edit.setText(interrupt.title)
76
+ self.title_edit.setPlaceholderText(self.tr("Label shown in the list (auto-set if empty)"))
77
+ self.title_edit.setToolTip(self.tr("Enter the title of this interrupt handler. If empty, a provisional title is set automatically."))
78
+ form.addRow("Title *", self.title_edit)
79
+
80
+ self.name_edit = QLineEdit()
81
+ if interrupt:
82
+ self.name_edit.setText(interrupt.name)
83
+ self.name_edit.setToolTip(self.tr("Please enter an interrupt name (e.g., TIMER0_IRQHandler)"))
84
+ form.addRow("Interrupt name", self.name_edit)
85
+
86
+ self.desc_edit = QLineEdit()
87
+ if interrupt:
88
+ self.desc_edit.setText(interrupt.description)
89
+ self.desc_edit.setToolTip(self.tr("Enter the description of this interrupt handler"))
90
+ form.addRow("Description", self.desc_edit)
91
+
92
+ self.event_combo = QComboBox()
93
+ self.event_combo.setEditable(True)
94
+ self.event_combo.setInsertPolicy(QComboBox.NoInsert)
95
+ self.event_combo.addItem(self.tr(""))
96
+ self.event_combo.addItems(self.event_names)
97
+ if interrupt and interrupt.event_names:
98
+ self.event_combo.setCurrentText(interrupt.event_names[0])
99
+ self.event_combo.setToolTip(
100
+ self.tr("Select or enter the state transition event name to notify from the ISR.\n"
101
+ "Entering a new event name registers it as-is.")
102
+ )
103
+ form.addRow("Event name", self.event_combo)
104
+
105
+ self.timer_check = QCheckBox()
106
+ self.timer_check.setChecked(interrupt.is_timer if interrupt else False)
107
+ self.timer_check.setToolTip(self.tr("Check this if it is a timer interrupt"))
108
+ form.addRow("Timer interrupt", self.timer_check)
109
+
110
+ layout.addWidget(QLabel(self.tr("Conditional action list:")))
111
+ help_label = QLabel(
112
+ self.tr("Describe \"state transition condition\" and \"action code\" in each row.\n"
113
+ "If the state transition condition is empty, the action is executed unconditionally.\n"
114
+ "Double-click to edit each cell.")
115
+ )
116
+ help_label.setStyleSheet("color: gray; font-size: 11px;")
117
+ layout.addWidget(help_label)
118
+
119
+ self.action_table = DoubleClickTable(0, 2)
120
+ self.action_table.setHorizontalHeaderLabels(["State transition condition", "Action code"])
121
+ self.action_table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
122
+ self.action_table.cellDoubleClicked.connect(self.on_action_double_clicked)
123
+ layout.addWidget(self.action_table, stretch=1)
124
+
125
+ btn_layout = QHBoxLayout()
126
+ add_btn = QPushButton(self.tr("Add row"))
127
+ add_btn.clicked.connect(lambda: self.add_action_row())
128
+ del_btn = QPushButton(self.tr("Delete row"))
129
+ del_btn.clicked.connect(lambda: self.delete_action_row())
130
+ btn_layout.addWidget(add_btn)
131
+ btn_layout.addWidget(del_btn)
132
+ btn_layout.addStretch()
133
+ layout.addLayout(btn_layout)
134
+
135
+ buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
136
+ buttons.accepted.connect(self._on_accept)
137
+ buttons.rejected.connect(self.reject)
138
+ layout.addWidget(buttons)
139
+
140
+ if interrupt and interrupt.actions:
141
+ for act in interrupt.actions:
142
+ self.add_action_row(act.condition, act.action)
143
+ else:
144
+ self.add_action_row()
145
+
146
+ StaTableLogger.debug("InterruptEditDialog initialized")
147
+
148
+ def add_action_row(self, condition: str = "", action: str = ""):
149
+ row = self.action_table.rowCount()
150
+ self.action_table.insertRow(row)
151
+
152
+ condition_item = QTableWidgetItem(condition.replace('\n', ' ; ') if condition else "")
153
+ condition_item.setToolTip(self.tr("Double-click to edit the state transition condition"))
154
+ condition_item.setData(Qt.UserRole, condition)
155
+ self.action_table.setItem(row, 0, condition_item)
156
+
157
+ action_item = QTableWidgetItem(action.replace('\n', ' ; ') if action else "")
158
+ action_item.setToolTip(self.tr("Double-click to edit the action code"))
159
+ action_item.setData(Qt.UserRole, action)
160
+ self.action_table.setItem(row, 1, action_item)
161
+
162
+ def delete_action_row(self):
163
+ row = self.action_table.currentRow()
164
+ if row >= 0:
165
+ self.action_table.removeRow(row)
166
+
167
+ def on_action_double_clicked(self, row, col):
168
+ StaTableLogger.debug(f"on_action_double_clicked: row={row}, col={col}")
169
+
170
+ if col == 0:
171
+ item = self.action_table.item(row, 0)
172
+ if not item:
173
+ return
174
+ current_condition = item.data(Qt.UserRole) if item.data(Qt.UserRole) else ""
175
+ dlg = ConditionEditDialog(
176
+ self,
177
+ condition_text=current_condition,
178
+ global_defs=self.global_defs,
179
+ role_functions=self.role_functions
180
+ )
181
+ if dlg.exec() == QDialog.Accepted:
182
+ new_condition = dlg.get_condition_text()
183
+ item.setText(new_condition.replace('\n', ' ; '))
184
+ item.setData(Qt.UserRole, new_condition)
185
+
186
+ elif col == 1:
187
+ item = self.action_table.item(row, 1)
188
+ if not item:
189
+ return
190
+ current_action = item.data(Qt.UserRole) if item.data(Qt.UserRole) else ""
191
+ dlg = ActionEditDialog(
192
+ self,
193
+ action_text=current_action,
194
+ role_functions=self.role_functions,
195
+ global_defs=self.global_defs
196
+ )
197
+ if dlg.exec() == QDialog.Accepted:
198
+ new_action = dlg.get_action_text()
199
+ item.setText(new_action.replace('\n', ' ; '))
200
+ item.setData(Qt.UserRole, new_action)
201
+
202
+ def _on_accept(self):
203
+ if not self.title_edit.text().strip():
204
+ auto_title = f"Interrupt: {self.name_edit.text().strip() or '(unnamed)'}"
205
+ self.title_edit.setText(auto_title)
206
+ StaTableLogger.debug(f"Auto title generated: '{auto_title}'")
207
+ self.accept()
208
+
209
+ # Stage 3: auto-extract used_role_functions / used_variables
210
+ def get_interrupt(self) -> InterruptHandlerDef:
211
+ actions = []
212
+ for row in range(self.action_table.rowCount()):
213
+ condition_item = self.action_table.item(row, 0)
214
+ action_item = self.action_table.item(row, 1)
215
+ if not condition_item or not action_item:
216
+ continue
217
+ condition = condition_item.data(Qt.UserRole) if condition_item.data(Qt.UserRole) else ""
218
+ action = action_item.data(Qt.UserRole) if action_item.data(Qt.UserRole) else ""
219
+ if not condition and not action:
220
+ continue
221
+ actions.append(InterruptAction(condition=condition, action=action))
222
+
223
+ event_names = []
224
+ if self.event_combo.currentText().strip():
225
+ event_names.append(self.event_combo.currentText().strip())
226
+
227
+ handler = InterruptHandlerDef(
228
+ name=self.name_edit.text().strip(),
229
+ description=self.desc_edit.text().strip(),
230
+ event_names=event_names,
231
+ is_timer=self.timer_check.isChecked(),
232
+ actions=actions,
233
+ title=self.title_edit.text().strip(),
234
+ )
235
+
236
+ # Auto-extract used role functions and variables
237
+ try:
238
+ from codegen.interrupt_generator import InterruptGenerator
239
+ gen = InterruptGenerator()
240
+ gen.update_handler_symbols(handler)
241
+ StaTableLogger.debug(
242
+ f"InterruptEditDialog.get_interrupt: "
243
+ f"used_rfs={handler.used_role_functions}, "
244
+ f"used_vars={handler.used_variables}"
245
+ )
246
+ except Exception as e:
247
+ StaTableLogger.debug(
248
+ f"InterruptEditDialog.get_interrupt: "
249
+ f"symbol extraction skipped: {e}"
250
+ )
251
+
252
+ return handler
253
+
254
+
255
+ class DevicePlaceholderEditDialog(QDialog):
256
+ def __init__(self, parent=None, placeholder: Optional[DevicePlaceholderDef] = None):
257
+ super().__init__(parent)
258
+ self.setWindowTitle(self.tr("Device resource placeholder definitionEdit"))
259
+ self.setMinimumWidth(450)
260
+
261
+ form = QFormLayout(self)
262
+
263
+ self.title_edit = QLineEdit()
264
+ if placeholder:
265
+ self.title_edit.setText(placeholder.title)
266
+ self.title_edit.setPlaceholderText(self.tr("Label shown in the list (auto-set if empty)"))
267
+ form.addRow("Title *", self.title_edit)
268
+
269
+ self.name_edit = QLineEdit()
270
+ if placeholder:
271
+ self.name_edit.setText(placeholder.name)
272
+ form.addRow("Placeholder name", self.name_edit)
273
+
274
+ self.desc_edit = QLineEdit()
275
+ if placeholder:
276
+ self.desc_edit.setText(placeholder.description)
277
+ form.addRow("Description", self.desc_edit)
278
+
279
+ buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
280
+ buttons.accepted.connect(self._on_accept)
281
+ buttons.rejected.connect(self.reject)
282
+ form.addRow(buttons)
283
+
284
+ def _on_accept(self):
285
+ if not self.title_edit.text().strip():
286
+ auto_title = f"Device: {self.name_edit.text().strip() or '(unnamed)'}"
287
+ self.title_edit.setText(auto_title)
288
+ self.accept()
289
+
290
+ def get_placeholder(self) -> DevicePlaceholderDef:
291
+ return DevicePlaceholderDef(
292
+ name=self.name_edit.text().strip(),
293
+ description=self.desc_edit.text().strip(),
294
+ title=self.title_edit.text().strip(),
295
+ )
296
+
297
+
298
+ class TimerBaseEditDialog(QDialog):
299
+ def __init__(self, parent=None, timer_base: TimerBaseDef = None):
300
+ super().__init__(parent)
301
+ self.setWindowTitle(self.tr("Timer base variableEdit"))
302
+ self.setMinimumWidth(450)
303
+
304
+ form = QFormLayout(self)
305
+
306
+ self.title_edit = QLineEdit()
307
+ if timer_base:
308
+ self.title_edit.setText(timer_base.title)
309
+ self.title_edit.setPlaceholderText(self.tr("Label shown in the list (auto-set if empty)"))
310
+ form.addRow("Title *", self.title_edit)
311
+
312
+ self.var_edit = QLineEdit()
313
+ if timer_base:
314
+ self.var_edit.setText(timer_base.variable_name)
315
+ form.addRow("Base variable name", self.var_edit)
316
+
317
+ self.unit_edit = QLineEdit()
318
+ if timer_base:
319
+ self.unit_edit.setText(timer_base.unit)
320
+ form.addRow("Unit", self.unit_edit)
321
+
322
+ self.type_edit = QLineEdit()
323
+ if timer_base:
324
+ self.type_edit.setText(timer_base.data_type)
325
+ form.addRow("Type", self.type_edit)
326
+
327
+ self.interrupt_edit = QLineEdit()
328
+ if timer_base:
329
+ self.interrupt_edit.setText(timer_base.interrupt_name)
330
+ self.interrupt_edit.setPlaceholderText(self.tr("Interrupt name driving this timer (optional)"))
331
+ form.addRow("Interrupt name", self.interrupt_edit)
332
+
333
+ buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
334
+ buttons.accepted.connect(self._on_accept)
335
+ buttons.rejected.connect(self.reject)
336
+ form.addRow(buttons)
337
+
338
+ def _on_accept(self):
339
+ if not self.title_edit.text().strip():
340
+ auto_title = f"Timer base: {self.var_edit.text().strip() or '(unnamed)'}"
341
+ self.title_edit.setText(auto_title)
342
+ self.accept()
343
+
344
+ def get_timer_base(self) -> TimerBaseDef:
345
+ return TimerBaseDef(
346
+ variable_name=self.var_edit.text().strip(),
347
+ unit=self.unit_edit.text().strip(),
348
+ data_type=self.type_edit.text().strip(),
349
+ title=self.title_edit.text().strip(),
350
+ interrupt_name=self.interrupt_edit.text().strip(),
351
+ )
352
+
353
+
354
+ class TimerDerivedEditDialog(QDialog):
355
+ def __init__(self, parent=None, derived: Optional[TimerDerivedDef] = None):
356
+ super().__init__(parent)
357
+ self.setWindowTitle(self.tr("Edit derived timer variable"))
358
+ self.setMinimumWidth(450)
359
+
360
+ form = QFormLayout(self)
361
+
362
+ self.title_edit = QLineEdit()
363
+ if derived:
364
+ self.title_edit.setText(derived.title)
365
+ self.title_edit.setPlaceholderText(self.tr("Label shown in the list (auto-set if empty)"))
366
+ form.addRow("Title *", self.title_edit)
367
+
368
+ self.period_edit = QLineEdit()
369
+ if derived:
370
+ self.period_edit.setText(derived.period_name)
371
+ form.addRow("Period name", self.period_edit)
372
+
373
+ self.mult_spin = QSpinBox()
374
+ self.mult_spin.setRange(1, 1000000)
375
+ if derived:
376
+ self.mult_spin.setValue(derived.multiplier)
377
+ form.addRow("Multiplier", self.mult_spin)
378
+
379
+ self.var_edit = QLineEdit()
380
+ if derived:
381
+ self.var_edit.setText(derived.variable_name)
382
+ form.addRow("Variable name", self.var_edit)
383
+
384
+ self.type_edit = QLineEdit()
385
+ if derived:
386
+ self.type_edit.setText(derived.data_type)
387
+ form.addRow("Type", self.type_edit)
388
+
389
+ buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
390
+ buttons.accepted.connect(self._on_accept)
391
+ buttons.rejected.connect(self.reject)
392
+ form.addRow(buttons)
393
+
394
+ def _on_accept(self):
395
+ if not self.title_edit.text().strip():
396
+ auto_title = f"Timer: {self.var_edit.text().strip() or '(unnamed)'}"
397
+ self.title_edit.setText(auto_title)
398
+ self.accept()
399
+
400
+ def get_derived(self) -> TimerDerivedDef:
401
+ return TimerDerivedDef(
402
+ period_name=self.period_edit.text().strip(),
403
+ multiplier=self.mult_spin.value(),
404
+ variable_name=self.var_edit.text().strip(),
405
+ data_type=self.type_edit.text().strip(),
406
+ title=self.title_edit.text().strip(),
407
+ )
408
+
409
+
410
+ class InterruptHandlerEditDialog(QDialog):
411
+ """Dialog to manage interrupt handlers, device resources, and timer settings together"""
412
+
413
+ def __init__(
414
+ self,
415
+ global_defs: GlobalDefinitions,
416
+ event_names: Optional[List[str]] = None,
417
+ role_functions: Optional[dict] = None,
418
+ parent=None
419
+ ):
420
+ super().__init__(parent)
421
+ self.global_defs = global_defs
422
+ self.event_names = event_names or []
423
+ self.role_functions = role_functions if role_functions is not None else {}
424
+
425
+ self.setWindowTitle(self.tr("Interrupt handlers, device resources, timer settings"))
426
+ self.setMinimumSize(1200, 800)
427
+
428
+ layout = QVBoxLayout(self)
429
+
430
+ self.tabs = QTabWidget()
431
+ layout.addWidget(self.tabs)
432
+
433
+ self.tabs.addTab(self._create_interrupt_tab(), self.tr("Interrupt handler"))
434
+ self.tabs.addTab(self._create_placeholder_tab(), self.tr("Device resource"))
435
+ self.tabs.addTab(self._create_timer_tab(), self.tr("Timer settings"))
436
+
437
+ close_btn = QPushButton(self.tr("Close"))
438
+ close_btn.clicked.connect(self.accept)
439
+ layout.addWidget(close_btn, alignment=Qt.AlignRight)
440
+
441
+ self.refresh_interrupt_table()
442
+ self.refresh_placeholder_table()
443
+ self.refresh_timer_tabs()
444
+
445
+ StaTableLogger.debug("InterruptHandlerEditDialog initialized")
446
+
447
+ # ------------------------------------------------------------------
448
+ # Interrupt handler tab
449
+ # ------------------------------------------------------------------
450
+ def _create_interrupt_tab(self) -> QWidget:
451
+ widget = QWidget()
452
+ layout = QVBoxLayout(widget)
453
+
454
+ self.interrupt_table = DoubleClickTable(0, 6)
455
+ self.interrupt_table.setHorizontalHeaderLabels([
456
+ "Title", "Interrupt name", "Description", "Event name", "Timer", "Number of conditional actions"
457
+ ])
458
+ self.interrupt_table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
459
+ self.interrupt_table.cellDoubleClicked.connect(self.on_interrupt_double_clicked)
460
+ layout.addWidget(self.interrupt_table)
461
+
462
+ btn_layout = QHBoxLayout()
463
+ add_btn = QPushButton(self.tr("Add"))
464
+ add_btn.clicked.connect(self.add_interrupt)
465
+ del_btn = QPushButton(self.tr("Delete"))
466
+ del_btn.clicked.connect(self.delete_interrupt)
467
+ btn_layout.addWidget(add_btn)
468
+ btn_layout.addWidget(del_btn)
469
+ btn_layout.addStretch()
470
+ layout.addLayout(btn_layout)
471
+
472
+ return widget
473
+
474
+ def refresh_interrupt_table(self):
475
+ self.interrupt_table.setRowCount(0)
476
+ for intr in self.global_defs.interrupts:
477
+ row = self.interrupt_table.rowCount()
478
+ self.interrupt_table.insertRow(row)
479
+ self.interrupt_table.setItem(row, 0, QTableWidgetItem(intr.title))
480
+ self.interrupt_table.setItem(row, 1, QTableWidgetItem(intr.name))
481
+ self.interrupt_table.setItem(row, 2, QTableWidgetItem(intr.description))
482
+ self.interrupt_table.setItem(row, 3, QTableWidgetItem(", ".join(intr.event_names)))
483
+ self.interrupt_table.setItem(row, 4, QTableWidgetItem("✔" if intr.is_timer else ""))
484
+ self.interrupt_table.setItem(row, 5, QTableWidgetItem(str(len(intr.actions))))
485
+
486
+ def on_interrupt_double_clicked(self, row, col):
487
+ StaTableLogger.debug(f"on_interrupt_double_clicked: row={row}, col={col}")
488
+ if row < 0 or row >= len(self.global_defs.interrupts):
489
+ return
490
+ target = self.global_defs.interrupts[row]
491
+ dlg = InterruptEditDialog(
492
+ self,
493
+ event_names=self.event_names,
494
+ interrupt=target,
495
+ global_defs=self.global_defs,
496
+ role_functions=self.role_functions
497
+ )
498
+ if dlg.exec() == QDialog.Accepted:
499
+ new_intr = dlg.get_interrupt()
500
+ if not new_intr.name:
501
+ QMessageBox.warning(self, self.tr("Warning"), self.tr("Please enter an interrupt name."))
502
+ return
503
+ self.global_defs.interrupts[row] = new_intr
504
+ self.refresh_interrupt_table()
505
+
506
+ def add_interrupt(self):
507
+ dlg = InterruptEditDialog(
508
+ self,
509
+ event_names=self.event_names,
510
+ global_defs=self.global_defs,
511
+ role_functions=self.role_functions
512
+ )
513
+ if dlg.exec() == QDialog.Accepted:
514
+ intr = dlg.get_interrupt()
515
+ if not intr.name:
516
+ QMessageBox.warning(self, self.tr("Warning"), self.tr("Please enter an interrupt name."))
517
+ return
518
+ self.global_defs.interrupts.append(intr)
519
+ self.refresh_interrupt_table()
520
+
521
+ def delete_interrupt(self):
522
+ row = self.interrupt_table.currentRow()
523
+ if 0 <= row < len(self.global_defs.interrupts):
524
+ self.global_defs.interrupts.pop(row)
525
+ self.refresh_interrupt_table()
526
+
527
+ # ------------------------------------------------------------------
528
+ # Device resource tab
529
+ # ------------------------------------------------------------------
530
+ def _create_placeholder_tab(self) -> QWidget:
531
+ widget = QWidget()
532
+ layout = QVBoxLayout(widget)
533
+
534
+ self.placeholder_table = DoubleClickTable(0, 3)
535
+ self.placeholder_table.setHorizontalHeaderLabels(["Title", "Placeholder name", "Description"])
536
+ self.placeholder_table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
537
+ self.placeholder_table.cellDoubleClicked.connect(self.on_placeholder_double_clicked)
538
+ layout.addWidget(self.placeholder_table)
539
+
540
+ btn_layout = QHBoxLayout()
541
+ add_btn = QPushButton(self.tr("Add"))
542
+ add_btn.clicked.connect(self.add_placeholder)
543
+ del_btn = QPushButton(self.tr("Delete"))
544
+ del_btn.clicked.connect(self.delete_placeholder)
545
+ btn_layout.addWidget(add_btn)
546
+ btn_layout.addWidget(del_btn)
547
+ btn_layout.addStretch()
548
+ layout.addLayout(btn_layout)
549
+
550
+ return widget
551
+
552
+ def refresh_placeholder_table(self):
553
+ self.placeholder_table.setRowCount(0)
554
+ for ph in self.global_defs.placeholders:
555
+ row = self.placeholder_table.rowCount()
556
+ self.placeholder_table.insertRow(row)
557
+ self.placeholder_table.setItem(row, 0, QTableWidgetItem(ph.title))
558
+ self.placeholder_table.setItem(row, 1, QTableWidgetItem(ph.name))
559
+ self.placeholder_table.setItem(row, 2, QTableWidgetItem(ph.description))
560
+
561
+ def on_placeholder_double_clicked(self, row, col):
562
+ StaTableLogger.debug(f"on_placeholder_double_clicked: row={row}, col={col}")
563
+ if row < 0 or row >= len(self.global_defs.placeholders):
564
+ return
565
+ target = self.global_defs.placeholders[row]
566
+ dlg = DevicePlaceholderEditDialog(self, placeholder=target)
567
+ if dlg.exec() == QDialog.Accepted:
568
+ new_ph = dlg.get_placeholder()
569
+ if not new_ph.name:
570
+ QMessageBox.warning(self, self.tr("Warning"), self.tr("Please enter a placeholder name."))
571
+ return
572
+ self.global_defs.placeholders[row] = new_ph
573
+ self.refresh_placeholder_table()
574
+
575
+ def add_placeholder(self):
576
+ dlg = DevicePlaceholderEditDialog(self)
577
+ if dlg.exec() == QDialog.Accepted:
578
+ ph = dlg.get_placeholder()
579
+ if not ph.name:
580
+ QMessageBox.warning(self, self.tr("Warning"), self.tr("Please enter a placeholder name."))
581
+ return
582
+ self.global_defs.placeholders.append(ph)
583
+ self.refresh_placeholder_table()
584
+
585
+ def delete_placeholder(self):
586
+ row = self.placeholder_table.currentRow()
587
+ if 0 <= row < len(self.global_defs.placeholders):
588
+ self.global_defs.placeholders.pop(row)
589
+ self.refresh_placeholder_table()
590
+
591
+ # ------------------------------------------------------------------
592
+ # Timer settings tab (multi-base-timer support)
593
+ # ------------------------------------------------------------------
594
+ def _create_timer_tab(self) -> QWidget:
595
+ widget = QWidget()
596
+ layout = QVBoxLayout(widget)
597
+
598
+ # Base timer tab
599
+ self.timer_tabs = QTabWidget()
600
+ self.timer_tabs.setTabsClosable(True)
601
+ self.timer_tabs.tabCloseRequested.connect(self.close_timer_tab)
602
+ self.timer_tabs.tabBarDoubleClicked.connect(self.rename_timer_tab)
603
+ layout.addWidget(self.timer_tabs)
604
+
605
+ # \"+\" button
606
+ add_timer_btn = QPushButton(self.tr("+"))
607
+ add_timer_btn.setFixedWidth(30)
608
+ add_timer_btn.setToolTip(self.tr("Add base timer"))
609
+ add_timer_btn.clicked.connect(self.add_timer_base)
610
+ layout.addWidget(add_timer_btn, alignment=Qt.AlignRight)
611
+
612
+ return widget
613
+
614
+ def refresh_timer_tabs(self):
615
+ """Display all base timers as tabs"""
616
+ self.timer_tabs.clear()
617
+
618
+ all_timers = [self.global_defs.timer_base] + self.global_defs.extra_timers
619
+ for timer in all_timers:
620
+ tab = self._create_timer_base_tab(timer)
621
+ self.timer_tabs.addTab(tab, timer.title)
622
+
623
+ def _create_timer_base_tab(self, timer: TimerBaseDef) -> QWidget:
624
+ """Create an edit tab for one base timer"""
625
+ widget = QWidget()
626
+ layout = QVBoxLayout(widget)
627
+
628
+ form = QFormLayout()
629
+ layout.addLayout(form)
630
+
631
+ # Title (editable, linked to tab name)
632
+ title_edit = QLineEdit(timer.title)
633
+ title_edit.textChanged.connect(lambda text, t=timer: self._on_timer_title_changed(t, text))
634
+ form.addRow("Title *", title_edit)
635
+
636
+ var_edit = QLineEdit(timer.variable_name)
637
+ var_edit.textChanged.connect(lambda text, t=timer: self._on_timer_var_changed(t, text))
638
+ form.addRow("Base variable name", var_edit)
639
+
640
+ unit_edit = QLineEdit(timer.unit)
641
+ unit_edit.textChanged.connect(lambda text, t=timer: self._on_timer_unit_changed(t, text))
642
+ form.addRow("Unit", unit_edit)
643
+
644
+ type_edit = QLineEdit(timer.data_type)
645
+ type_edit.textChanged.connect(lambda text, t=timer: self._on_timer_type_changed(t, text))
646
+ form.addRow("Type", type_edit)
647
+
648
+ interrupt_edit = QLineEdit(timer.interrupt_name)
649
+ interrupt_edit.textChanged.connect(lambda text, t=timer: self._on_timer_interrupt_changed(t, text))
650
+ interrupt_edit.setPlaceholderText(self.tr("Interrupt name (optional)"))
651
+ form.addRow("Interrupt name", interrupt_edit)
652
+
653
+ # Edit button
654
+ edit_btn = QPushButton(self.tr("Edit base timer..."))
655
+ edit_btn.clicked.connect(lambda _checked, t=timer: self.edit_timer_base(t))
656
+ form.addRow("", edit_btn)
657
+
658
+ layout.addWidget(QLabel(self.tr("Derived timer variable:")))
659
+ derived_table = DoubleClickTable(0, 5)
660
+ derived_table.setHorizontalHeaderLabels(["Title", "Period name", "Multiplier", "Variable name", "Type"])
661
+ derived_table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
662
+ derived_table.cellDoubleClicked.connect(lambda row, col, t=timer: self.on_derived_double_clicked(t, row, col))
663
+ layout.addWidget(derived_table)
664
+
665
+ # Derived timer button
666
+ btn_layout = QHBoxLayout()
667
+ add_btn = QPushButton(self.tr("Add"))
668
+ add_btn.clicked.connect(lambda _checked, t=timer: self.add_derived(t))
669
+ del_btn = QPushButton(self.tr("Delete"))
670
+ del_btn.clicked.connect(lambda _checked, t=timer: self.delete_derived(t))
671
+ btn_layout.addWidget(add_btn)
672
+ btn_layout.addWidget(del_btn)
673
+ btn_layout.addStretch()
674
+ layout.addLayout(btn_layout)
675
+
676
+ self._refresh_derived_table(derived_table, timer)
677
+
678
+ return widget
679
+
680
+ def _refresh_derived_table(self, table: DoubleClickTable, timer: TimerBaseDef):
681
+ """Display derived timers of the specified timer in the table"""
682
+ table.setRowCount(0)
683
+ for d in timer.derived:
684
+ row = table.rowCount()
685
+ table.insertRow(row)
686
+ table.setItem(row, 0, QTableWidgetItem(d.title))
687
+ table.setItem(row, 1, QTableWidgetItem(d.period_name))
688
+ table.setItem(row, 2, QTableWidgetItem(str(d.multiplier)))
689
+ table.setItem(row, 3, QTableWidgetItem(d.variable_name))
690
+ table.setItem(row, 4, QTableWidgetItem(d.data_type))
691
+
692
+ def _on_timer_title_changed(self, timer: TimerBaseDef, text: str):
693
+ timer.title = text.strip() or f"Timer base: {timer.variable_name}"
694
+ all_timers = [self.global_defs.timer_base] + self.global_defs.extra_timers
695
+ for i, t in enumerate(all_timers):
696
+ if t is timer:
697
+ self.timer_tabs.setTabText(i, timer.title)
698
+ break
699
+
700
+ def _on_timer_var_changed(self, timer: TimerBaseDef, text: str):
701
+ timer.variable_name = text.strip()
702
+ self.global_defs.add_timer_variables()
703
+
704
+ def _on_timer_unit_changed(self, timer: TimerBaseDef, text: str):
705
+ timer.unit = text.strip()
706
+ self.global_defs.add_timer_variables()
707
+
708
+ def _on_timer_type_changed(self, timer: TimerBaseDef, text: str):
709
+ timer.data_type = text.strip()
710
+ self.global_defs.add_timer_variables()
711
+
712
+ def _on_timer_interrupt_changed(self, timer: TimerBaseDef, text: str):
713
+ timer.interrupt_name = text.strip()
714
+
715
+ def add_timer_base(self):
716
+ dlg = TimerBaseEditDialog(self)
717
+ if dlg.exec() == QDialog.Accepted:
718
+ new_timer = dlg.get_timer_base()
719
+ if not new_timer.variable_name:
720
+ QMessageBox.warning(self, self.tr("Warning"), self.tr("Please enter a base variable name."))
721
+ return
722
+ self.global_defs.extra_timers.append(new_timer)
723
+ self.global_defs.add_timer_variables()
724
+ self.refresh_timer_tabs()
725
+ StaTableLogger.info(f"Timer base added: {new_timer.title}")
726
+
727
+ def edit_timer_base(self, timer: TimerBaseDef):
728
+ dlg = TimerBaseEditDialog(self, timer_base=timer)
729
+ if dlg.exec() == QDialog.Accepted:
730
+ new_timer = dlg.get_timer_base()
731
+ if not new_timer.variable_name:
732
+ QMessageBox.warning(self, self.tr("Warning"), self.tr("Please enter a base variable name."))
733
+ return
734
+ timer.variable_name = new_timer.variable_name
735
+ timer.unit = new_timer.unit
736
+ timer.data_type = new_timer.data_type
737
+ timer.title = new_timer.title
738
+ timer.interrupt_name = new_timer.interrupt_name
739
+ self.global_defs.add_timer_variables()
740
+ self.refresh_timer_tabs()
741
+ StaTableLogger.info(f"Timer base updated: {timer.title}")
742
+
743
+ def close_timer_tab(self, index: int):
744
+ if index == 0:
745
+ QMessageBox.warning(self, self.tr("Warning"), self.tr("The main timer cannot be deleted."))
746
+ return
747
+ all_timers = [self.global_defs.timer_base] + self.global_defs.extra_timers
748
+ if 0 <= index < len(all_timers):
749
+ timer = all_timers[index]
750
+ reply = QMessageBox.question(
751
+ self, self.tr("Confirm"),
752
+ f"Base timer '{timer.title}': confirm delete?",
753
+ QMessageBox.Yes | QMessageBox.No
754
+ )
755
+ if reply == QMessageBox.Yes:
756
+ self.global_defs.extra_timers.pop(index - 1)
757
+ self.global_defs.add_timer_variables()
758
+ self.refresh_timer_tabs()
759
+ StaTableLogger.info(f"Timer base deleted: {timer.title}")
760
+
761
+ def rename_timer_tab(self, index: int):
762
+ all_timers = [self.global_defs.timer_base] + self.global_defs.extra_timers
763
+ if 0 <= index < len(all_timers):
764
+ current = all_timers[index]
765
+ new_name, ok = QInputDialog.getText(
766
+ self, "Rename timer", "Enter a new timer name:",
767
+ text=current.title
768
+ )
769
+ if ok and new_name.strip():
770
+ current.title = new_name.strip()
771
+ self.timer_tabs.setTabText(index, current.title)
772
+ self.global_defs.add_timer_variables()
773
+ StaTableLogger.info(f"Timer tab renamed: {current.title}")
774
+
775
+ def on_derived_double_clicked(self, timer: TimerBaseDef, row: int, col: int):
776
+ StaTableLogger.debug(f"on_derived_double_clicked: timer='{timer.title}', row={row}, col={col}")
777
+ if row < 0 or row >= len(timer.derived):
778
+ return
779
+ target = timer.derived[row]
780
+ dlg = TimerDerivedEditDialog(self, derived=target)
781
+ if dlg.exec() == QDialog.Accepted:
782
+ new_d = dlg.get_derived()
783
+ if not new_d.period_name:
784
+ QMessageBox.warning(self, self.tr("Warning"), self.tr("Please enter a period name."))
785
+ return
786
+ timer.derived[row] = new_d
787
+ self.global_defs.add_timer_variables()
788
+ self.refresh_timer_tabs()
789
+
790
+ def add_derived(self, timer: TimerBaseDef):
791
+ dlg = TimerDerivedEditDialog(self)
792
+ if dlg.exec() == QDialog.Accepted:
793
+ d = dlg.get_derived()
794
+ if not d.period_name:
795
+ QMessageBox.warning(self, self.tr("Warning"), self.tr("Please enter a period name."))
796
+ return
797
+ timer.derived.append(d)
798
+ self.global_defs.add_timer_variables()
799
+ self.refresh_timer_tabs()
800
+
801
+ def delete_derived(self, timer: TimerBaseDef):
802
+ current_widget = self.timer_tabs.currentWidget()
803
+ if current_widget:
804
+ table = current_widget.findChild(DoubleClickTable)
805
+ if table:
806
+ row = table.currentRow()
807
+ if 0 <= row < len(timer.derived):
808
+ timer.derived.pop(row)
809
+ self.global_defs.add_timer_variables()
810
+ self.refresh_timer_tabs()