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,1380 @@
1
+ # statable_gui/main_window.py
2
+ """
3
+ StaTable main window
4
+ Integrates code generation, validation/AI integration, and shared library
5
+ management (multi-layer support).
6
+
7
+ Version History
8
+ ---------------
9
+ v1.5 - Pass namespace in RoleFunction registration in __init__ (bug #86)
10
+
11
+ v2.3 - Add New Project feature (F-15).
12
+ * new_project(): reset to an empty Application layer.
13
+ * _maybe_save(): unified unsaved-changes confirmation.
14
+ * closeEvent(): prompt on window close.
15
+ * _update_window_title(): 'Untitled[*] - StaTable' format.
16
+ * _on_tab_data_modified(): slot for StateMachineTab.dataModified.
17
+ * save_project() now returns bool (success / cancel / failure).
18
+ * open_project() prompts via _maybe_save() at the beginning.
19
+ * add_state_machine_tab() connects dataModified.
20
+ * Tab add / rename / close set windowModified(True).
21
+
22
+ v2.4 - Provide all layer names to StateMachineTab (v3.11).
23
+ * _get_all_layer_names(): new helper returning every tab
24
+ name (= every layer name in the project).
25
+ * add_state_machine_tab(): pass `layer_names_provider` to
26
+ StateMachineTab, which forwards it to SettingsPanel so
27
+ the namespace combo box (inline editor + RoleFunctionDialog)
28
+ lists all layers, not just the current tab's layer.
29
+ * [C-19 fix] Add missing '\\n' in the output-directory
30
+ warning message so "settings.Please specify." is split
31
+ correctly across two lines.
32
+ """
33
+
34
+ import sys
35
+ import os
36
+ import logging
37
+ from pathlib import Path
38
+
39
+ from PySide6.QtCore import Qt
40
+ from PySide6.QtGui import QAction
41
+ from PySide6.QtWidgets import (
42
+ QMainWindow, QTabWidget, QToolButton, QMessageBox,
43
+ QInputDialog, QFileDialog, QDialog, QToolBar
44
+ )
45
+
46
+ from statable.state_machine import StateMachine
47
+ from statable.xml_io import (
48
+ project_to_xml, project_from_xml)
49
+ from statable.global_defs import GlobalDefinitions
50
+ from statable.sample_data import (
51
+ create_sample_state_machine,
52
+ create_sample_global_defs)
53
+
54
+ from .logger import StaTableLogger
55
+ from .traceball import TraceBallWidget
56
+ from .config import WINDOW_WIDTH, WINDOW_HEIGHT
57
+ from .widgets import StateMachineTab
58
+ from .preferences import Preferences
59
+ from .global_defs_dialog import GlobalDefinitionsDialog
60
+ from .interrupt_handler_edit_dialog import (
61
+ InterruptHandlerEditDialog)
62
+ from .event_definition_dialog import EventDefinitionDialog
63
+ from .event_delivery_settings_dialog import (
64
+ EventDeliverySettingsDialog)
65
+ from .common_widgets import TypeManagerDialog
66
+ from .layer_settings_dialog import LayerSettingsDialog
67
+
68
+ # Shared library (v1.5: integrated into statable_gui/libcntrl)
69
+ from statable_gui.libcntrl.role_function_library import (
70
+ RoleFunctionLibrary, RoleFunction)
71
+ from statable_gui.libcntrl.condition_library import (
72
+ ConditionLibrary, ConditionTemplate)
73
+ from statable_gui.libcntrl.literal_library import (
74
+ LiteralLibrary, LiteralDefinition)
75
+ # Code generation module
76
+ sys.path.append(
77
+ os.path.dirname(
78
+ os.path.dirname(os.path.abspath(__file__))))
79
+ sys.path.append(os.path.join(
80
+ os.path.dirname(
81
+ os.path.dirname(os.path.abspath(__file__))),
82
+ 'codegen'))
83
+
84
+ try:
85
+ from codegen.c_code_generator import CCodeGenerator
86
+ from codegen.sample_data import SampleDataGenerator
87
+ from codegen.config import ConfigManager
88
+ from .code_generation_dialog import (
89
+ CodeGenerationDialog, WarningCollector)
90
+ from .code_generation_settings_dialog import (
91
+ CodeGenerationSettingsDialog)
92
+ except ImportError:
93
+ from c_code_generator import CCodeGenerator
94
+ from sample_data import SampleDataGenerator
95
+ from config import ConfigManager
96
+ from .code_generation_dialog import (
97
+ CodeGenerationDialog, WarningCollector)
98
+ from .code_generation_settings_dialog import (
99
+ CodeGenerationSettingsDialog)
100
+
101
+ # Validation / AI integration module
102
+ from .validation_dialog import ValidationDialog
103
+
104
+
105
+ class MainWindow(QMainWindow):
106
+ def __init__(self):
107
+ super().__init__()
108
+ self.setWindowTitle(
109
+ self.tr("StaTable - State Transition Editor[*]"))
110
+ self.resize(WINDOW_WIDTH, WINDOW_HEIGHT)
111
+
112
+ self.logger = StaTableLogger()
113
+ self.logger.debug(
114
+ "MainWindow initialization started")
115
+
116
+ # Preferences
117
+ self.prefs = Preferences()
118
+
119
+ # Code generation settings manager
120
+ self.config_manager = ConfigManager()
121
+
122
+ # Global variables, event flags, interrupts,
123
+ # Device / timer settings
124
+ self.global_defs = create_sample_global_defs()
125
+ self.global_defs.add_timer_variables()
126
+ StaTableLogger.debug(
127
+ f"MainWindow.global_defs: "
128
+ f"id={id(self.global_defs)}, "
129
+ f"vars={len(self.global_defs.variables)}, "
130
+ f"flags={len(self.global_defs.flags)}, "
131
+ f"interrupts={len(self.global_defs.interrupts)}, "
132
+ f"placeholders={len(self.global_defs.placeholders)}"
133
+ )
134
+
135
+ #Shared library (across project)
136
+ self.role_function_library = RoleFunctionLibrary()
137
+ self.condition_library = ConditionLibrary()
138
+ self.literal_library = LiteralLibrary()
139
+
140
+ # Register data from the sample state machine into the shared library
141
+ sample_sm = create_sample_state_machine()
142
+
143
+ # ==========================================================
144
+ # [v1.5 fix] register role function into shared library
145
+ # Also pass namespace (v1.4 section 9.6 #69 / v1.5 #86)
146
+ # ==========================================================
147
+ for rf in sample_sm.role_functions.values():
148
+ StaTableLogger.debug(
149
+ f"Attempting to register role function: "
150
+ f"name={rf.name}, "
151
+ f"namespace={getattr(rf, 'namespace', '')}, "
152
+ f"title={rf.title}")
153
+ try:
154
+ lib_rf = RoleFunction(
155
+ name=rf.name,
156
+ namespace=getattr(rf, 'namespace', '') or '', # ★ v1.5 Add
157
+ title=rf.title,
158
+ description=getattr(rf, 'description', ''),
159
+ )
160
+ self.role_function_library.add(lib_rf)
161
+ StaTableLogger.debug(
162
+ f"Registered role function to "
163
+ f"shared library: {lib_rf.qualified_name}")
164
+ except Exception as e:
165
+ StaTableLogger.error(
166
+ f"Failed to register role function "
167
+ f"{rf.name}: {e}", exc_info=True)
168
+
169
+ StaTableLogger.debug(
170
+ f"After role registration: "
171
+ f"roles={len(self.role_function_library.list_all())}")
172
+
173
+ # Add condition template
174
+ try:
175
+ self.condition_library.add(ConditionTemplate(
176
+ name="ERROR",
177
+ condition="err_code != 0"))
178
+ self.condition_library.add(ConditionTemplate(
179
+ name="RETRY",
180
+ condition="retry_count < RETRY_THRESHOLD"))
181
+ StaTableLogger.debug(
182
+ "Registered condition templates to "
183
+ "shared library")
184
+ except Exception as e:
185
+ StaTableLogger.warning(
186
+ f"Failed to register condition templates: {e}")
187
+
188
+ # Add literal
189
+ try:
190
+ self.literal_library.add(LiteralDefinition(
191
+ name="RETRY_THRESHOLD", value="3",
192
+ literal_type="int",
193
+ description="Retry count threshold"))
194
+ self.literal_library.add(LiteralDefinition(
195
+ name="VOLTAGE_MIN", value="2.5",
196
+ literal_type="float",
197
+ description="Min voltage"))
198
+ StaTableLogger.debug(
199
+ "Registered literals to shared library")
200
+ except Exception as e:
201
+ StaTableLogger.warning(
202
+ f"Failed to register literals: {e}")
203
+
204
+ #Debug log: output shared library content
205
+ StaTableLogger.debug(
206
+ "MainWindow shared libraries initialized: "
207
+ f"roles={len(self.role_function_library.list_all())}, "
208
+ f"conditions={len(self.condition_library.list_all())}, "
209
+ f"literals={len(self.literal_library.list_all())}"
210
+ )
211
+
212
+ self.tab_widget = QTabWidget()
213
+ self.tab_widget.setTabsClosable(True)
214
+ self.tab_widget.tabCloseRequested.connect(
215
+ self.close_tab)
216
+ self.setCentralWidget(self.tab_widget)
217
+
218
+ # "+" button
219
+ self.add_tab_button = QToolButton()
220
+ self.add_tab_button.setText(self.tr("+"))
221
+ self.add_tab_button.setToolTip(
222
+ self.tr("Add new state machine"))
223
+ self.add_tab_button.clicked.connect(
224
+ self.add_new_tab)
225
+ self.tab_widget.setCornerWidget(
226
+ self.add_tab_button, Qt.TopRightCorner)
227
+
228
+ #Double-click to rename tab
229
+ self.tab_widget.tabBarDoubleClicked.connect(
230
+ self.rename_tab_at)
231
+
232
+ self.create_menus()
233
+ self.create_toolbar()
234
+
235
+ self.traceball = TraceBallWidget(self)
236
+ self.addDockWidget(
237
+ Qt.BottomDockWidgetArea, self.traceball)
238
+ self.traceball.hide()
239
+
240
+ # Initial tab
241
+ self.add_state_machine_tab(
242
+ "Application", sample_sm)
243
+
244
+ self.logger.debug(
245
+ "MainWindow initialization completed")
246
+
247
+ # ------------------------------------------------------------------
248
+ # Toolbar
249
+ # ------------------------------------------------------------------
250
+ def create_toolbar(self):
251
+ toolbar = QToolBar("Main toolbar", self)
252
+ toolbar.setMovable(False)
253
+ toolbar.setToolButtonStyle(Qt.ToolButtonTextOnly)
254
+ self.addToolBar(Qt.TopToolBarArea, toolbar)
255
+
256
+ global_defs_btn = QAction(self.tr("Global definitions"), self)
257
+ global_defs_btn.setToolTip(
258
+ self.tr("Open global variables / event flag definitions"))
259
+ global_defs_btn.triggered.connect(
260
+ self.open_global_defs_dialog)
261
+ toolbar.addAction(global_defs_btn)
262
+
263
+ type_defs_btn = QAction(self.tr("Type definitions"), self)
264
+ type_defs_btn.setToolTip(
265
+ self.tr("Manage user-defined types (structs)"))
266
+ type_defs_btn.triggered.connect(
267
+ self.open_type_manager)
268
+ toolbar.addAction(type_defs_btn)
269
+
270
+ event_defs_btn = QAction(self.tr("Event definitions"), self)
271
+ event_defs_btn.setToolTip(
272
+ self.tr("Open state transition event definitions"))
273
+ event_defs_btn.triggered.connect(
274
+ self.open_event_definition_dialog)
275
+ toolbar.addAction(event_defs_btn)
276
+
277
+ delivery_btn = QAction(self.tr("Event delivery settings"), self)
278
+ delivery_btn.setToolTip(
279
+ self.tr("Open event delivery type settings"))
280
+ delivery_btn.triggered.connect(
281
+ self.open_event_delivery_settings)
282
+ toolbar.addAction(delivery_btn)
283
+
284
+ interrupt_btn = QAction(self.tr("Interrupt settings"), self)
285
+ interrupt_btn.setToolTip(
286
+ self.tr("Interrupt handler, device resources,"
287
+ "Open timer settings"))
288
+ interrupt_btn.triggered.connect(
289
+ self.open_interrupt_settings)
290
+ toolbar.addAction(interrupt_btn)
291
+
292
+ # Layer settings
293
+ layer_btn = QAction(self.tr("Layer settings"), self)
294
+ layer_btn.setToolTip(
295
+ self.tr("Set layer execution priority and initialization order"))
296
+ layer_btn.triggered.connect(
297
+ self.open_layer_settings)
298
+ toolbar.addAction(layer_btn)
299
+
300
+ toolbar.addSeparator()
301
+
302
+ validate_btn = QAction(self.tr("Validation / AI diagnosis"), self)
303
+ validate_btn.setToolTip(
304
+ self.tr("Open pre-generation validation / AI diagnosis"))
305
+ validate_btn.triggered.connect(
306
+ self.open_validation_dialog)
307
+ toolbar.addAction(validate_btn)
308
+
309
+ toolbar.addSeparator()
310
+
311
+ generate_btn = QAction(self.tr("Code generation"), self)
312
+ generate_btn.setToolTip(self.tr("Generate C code"))
313
+ generate_btn.triggered.connect(
314
+ self.open_code_generation_dialog)
315
+ toolbar.addAction(generate_btn)
316
+
317
+ gen_settings_btn = QAction(self.tr("Generation settings"), self)
318
+ gen_settings_btn.setToolTip(
319
+ self.tr("Change code generation settings"))
320
+ gen_settings_btn.triggered.connect(
321
+ self.open_code_generation_settings)
322
+ toolbar.addAction(gen_settings_btn)
323
+
324
+ gen_save_btn = QAction(self.tr("Save generated code"), self)
325
+ gen_save_btn.setToolTip(
326
+ self.tr("Directly save generated code"))
327
+ gen_save_btn.triggered.connect(
328
+ self.save_generated_code_direct)
329
+ toolbar.addAction(gen_save_btn)
330
+
331
+ toolbar.addSeparator()
332
+
333
+ open_btn = QAction(self.tr("Open"), self)
334
+ open_btn.setToolTip(self.tr("Open project"))
335
+ open_btn.triggered.connect(self.open_project)
336
+ toolbar.addAction(open_btn)
337
+
338
+ save_btn = QAction(self.tr("Save"), self)
339
+ save_btn.setToolTip(self.tr("Save project"))
340
+ save_btn.triggered.connect(self.save_project)
341
+ toolbar.addAction(save_btn)
342
+
343
+ toolbar.addSeparator()
344
+
345
+ new_tab_btn = QAction(self.tr("New tab"), self)
346
+ new_tab_btn.setToolTip(
347
+ self.tr("Add a new state transition tab"))
348
+ new_tab_btn.triggered.connect(self.add_new_tab)
349
+ toolbar.addAction(new_tab_btn)
350
+
351
+ rename_btn = QAction(self.tr("Rename tab"), self)
352
+ rename_btn.setToolTip(self.tr("Rename current tab"))
353
+ rename_btn.triggered.connect(
354
+ self.rename_current_tab)
355
+ toolbar.addAction(rename_btn)
356
+
357
+ toolbar.addSeparator()
358
+
359
+ traceball_btn = QAction(self.tr("Show log"), self)
360
+ traceball_btn.setCheckable(True)
361
+ traceball_btn.setChecked(False)
362
+ traceball_btn.setToolTip(
363
+ self.tr("Show/hide TraceBall log"))
364
+ traceball_btn.toggled.connect(
365
+ self.toggle_traceball)
366
+ toolbar.addAction(traceball_btn)
367
+
368
+ StaTableLogger.debug("Toolbar created")
369
+
370
+ # ------------------------------------------------------------------
371
+ # Menu
372
+ # ------------------------------------------------------------------
373
+ def create_menus(self):
374
+ menubar = self.menuBar()
375
+
376
+ file_menu = menubar.addMenu(self.tr("File"))
377
+
378
+ # [v2.3] New Project (Ctrl+N)
379
+ new_project_action = QAction(self.tr("New Project..."), self)
380
+ new_project_action.setShortcut("Ctrl+N")
381
+ new_project_action.triggered.connect(self.new_project)
382
+ file_menu.addAction(new_project_action)
383
+
384
+ file_menu.addSeparator()
385
+
386
+ open_action = QAction(self.tr("Open Project..."), self)
387
+ open_action.triggered.connect(self.open_project)
388
+ file_menu.addAction(open_action)
389
+
390
+ save_action = QAction(self.tr("Save Project..."), self)
391
+ save_action.triggered.connect(self.save_project)
392
+ file_menu.addAction(save_action)
393
+
394
+ rename_action = QAction(self.tr("Rename Tab..."), self)
395
+ rename_action.triggered.connect(
396
+ self.rename_current_tab)
397
+ file_menu.addAction(rename_action)
398
+
399
+ file_menu.addSeparator()
400
+ new_tab_action = QAction(
401
+ self.tr("New State Machine"), self)
402
+ new_tab_action.triggered.connect(self.add_new_tab)
403
+ file_menu.addAction(new_tab_action)
404
+
405
+ edit_menu = menubar.addMenu(self.tr("Edit"))
406
+ global_defs_action = QAction(
407
+ self.tr("Global Definitions..."), self)
408
+ global_defs_action.triggered.connect(
409
+ self.open_global_defs_dialog)
410
+ edit_menu.addAction(global_defs_action)
411
+
412
+ type_defs_action = QAction(
413
+ self.tr("Type Definitions..."), self)
414
+ type_defs_action.triggered.connect(
415
+ self.open_type_manager)
416
+ edit_menu.addAction(type_defs_action)
417
+
418
+ event_defs_action = QAction(
419
+ self.tr("Event Definitions..."), self)
420
+ event_defs_action.triggered.connect(
421
+ self.open_event_definition_dialog)
422
+ edit_menu.addAction(event_defs_action)
423
+
424
+ delivery_settings_action = QAction(
425
+ self.tr("Event Delivery Settings..."), self)
426
+ delivery_settings_action.triggered.connect(
427
+ self.open_event_delivery_settings)
428
+ edit_menu.addAction(delivery_settings_action)
429
+
430
+ interrupt_action = QAction(
431
+ self.tr("Interrupt Settings..."), self)
432
+ interrupt_action.triggered.connect(
433
+ self.open_interrupt_settings)
434
+ edit_menu.addAction(interrupt_action)
435
+
436
+ # Layer settings
437
+ layer_settings_action = QAction(
438
+ self.tr("Layer Settings..."), self)
439
+ layer_settings_action.triggered.connect(
440
+ self.open_layer_settings)
441
+ edit_menu.addAction(layer_settings_action)
442
+
443
+ validation_menu = menubar.addMenu(self.tr("Validate(&V)"))
444
+ validate_action = QAction(
445
+ self.tr("Validation / AI diagnosis..."), self)
446
+ validate_action.setShortcut("Ctrl+Shift+V")
447
+ validate_action.triggered.connect(
448
+ self.open_validation_dialog)
449
+ validation_menu.addAction(validate_action)
450
+
451
+ code_gen_menu = menubar.addMenu(self.tr("Code generation(&G)"))
452
+ generate_action = QAction(
453
+ self.tr("Code generation..."), self)
454
+ generate_action.setShortcut("Ctrl+G")
455
+ generate_action.triggered.connect(
456
+ self.open_code_generation_dialog)
457
+ code_gen_menu.addAction(generate_action)
458
+
459
+ gen_settings_action = QAction(
460
+ self.tr("Generation settings..."), self)
461
+ gen_settings_action.setShortcut("Ctrl+Shift+G")
462
+ gen_settings_action.triggered.connect(
463
+ self.open_code_generation_settings)
464
+ code_gen_menu.addAction(gen_settings_action)
465
+
466
+ code_gen_menu.addSeparator()
467
+ gen_save_action = QAction(
468
+ self.tr("Save generated code..."), self)
469
+ gen_save_action.setShortcut("Ctrl+Shift+S")
470
+ gen_save_action.triggered.connect(
471
+ self.save_generated_code_direct)
472
+ code_gen_menu.addAction(gen_save_action)
473
+
474
+ # [v3.1] Language menu (English / Chinese) with auto-restart
475
+ lang_menu = menubar.addMenu("Language / \u8bed\u8a00")
476
+
477
+ def _switch_language(code):
478
+ from statable_gui.i18n import (
479
+ save_preferred_language,
480
+ load_preferred_language,
481
+ )
482
+ current = load_preferred_language()
483
+ if code == current:
484
+ return
485
+ save_preferred_language(code)
486
+
487
+ from PySide6.QtWidgets import QMessageBox
488
+ msg = QMessageBox(self)
489
+ msg.setIcon(QMessageBox.Question)
490
+ msg.setWindowTitle("Language / \u8bed\u8a00")
491
+ msg.setText(
492
+ "Restart StaTable to apply the new language?\n"
493
+ "\u662f\u5426\u91cd\u65b0\u542f\u52a8 StaTable \u4ee5\u5e94\u7528\u65b0\u7684\u8bed\u8a00\uff1f"
494
+ )
495
+ btn_restart = msg.addButton(
496
+ "Restart now / \u7acb\u5373\u91cd\u542f",
497
+ QMessageBox.AcceptRole)
498
+ msg.addButton(
499
+ "Later / \u7a0d\u540e",
500
+ QMessageBox.RejectRole)
501
+ msg.exec()
502
+ if msg.clickedButton() is not btn_restart:
503
+ return
504
+
505
+ _relaunch_application()
506
+
507
+ def _relaunch_application():
508
+ """Start a new instance detached, then quit (guaranteed)."""
509
+ import os
510
+ import sys as _sys
511
+ from PySide6.QtCore import QProcess
512
+
513
+ # Guard: prevent concurrent restarts
514
+ if getattr(self, "_restart_in_progress", False):
515
+ return
516
+ self._restart_in_progress = True
517
+
518
+ exe = _sys.executable
519
+ args = [exe, "-m", "statable"]
520
+ cwd = os.getcwd()
521
+
522
+ # Disable UI to avoid double-clicks
523
+ self.setEnabled(False)
524
+
525
+ # Fully detach: redirect I/O to null, new process group
526
+ ok = False
527
+ try:
528
+ import subprocess
529
+ kwargs = {
530
+ "cwd": cwd,
531
+ "stdin": subprocess.DEVNULL,
532
+ "stdout": subprocess.DEVNULL,
533
+ "stderr": subprocess.DEVNULL,
534
+ "close_fds": True,
535
+ }
536
+ if os.name == "nt":
537
+ kwargs["creationflags"] = (
538
+ 0x00000008 # DETACHED_PROCESS
539
+ | 0x00000200 # CREATE_NEW_PROCESS_GROUP
540
+ )
541
+ else:
542
+ kwargs["start_new_session"] = True
543
+ subprocess.Popen(args, **kwargs)
544
+ ok = True
545
+ except Exception:
546
+ ok = False
547
+
548
+ if not ok:
549
+ self._restart_in_progress = False
550
+ self.setEnabled(True)
551
+ QMessageBox.warning(
552
+ self,
553
+ "Restart failed",
554
+ "Could not restart automatically.\n"
555
+ "Please start StaTable manually.",
556
+ )
557
+ return
558
+
559
+ # Exit immediately (no cleanup needed)
560
+ os._exit(0)
561
+
562
+ act_en = QAction("English", self)
563
+ act_en.triggered.connect(
564
+ lambda checked=False: _switch_language("en"))
565
+ lang_menu.addAction(act_en)
566
+
567
+ act_zh = QAction("\u7b80\u4f53\u4e2d\u6587", self)
568
+ act_zh.triggered.connect(
569
+ lambda checked=False: _switch_language("zh_CN"))
570
+ lang_menu.addAction(act_zh)
571
+
572
+ view_menu = menubar.addMenu(self.tr("View"))
573
+ toggle_traceball = QAction(self.tr("TraceBall"), self)
574
+ toggle_traceball.setCheckable(True)
575
+ toggle_traceball.setChecked(False)
576
+ toggle_traceball.toggled.connect(
577
+ self.toggle_traceball)
578
+ view_menu.addAction(toggle_traceball)
579
+
580
+ # ------------------------------------------------------------------
581
+ # Layer settings
582
+ # ------------------------------------------------------------------
583
+ def open_layer_settings(self):
584
+ """Open the layer settings dialog"""
585
+ StaTableLogger.debug(
586
+ "MainWindow.open_layer_settings called")
587
+
588
+ layers = []
589
+ for index in range(self.tab_widget.count()):
590
+ tab = self.tab_widget.widget(index)
591
+ if hasattr(tab, 'sm'):
592
+ name = self.tab_widget.tabText(index)
593
+ layers.append((name, tab.sm))
594
+ StaTableLogger.debug(
595
+ f" layer[{index}]: name='{name}', "
596
+ f"priority={getattr(tab.sm, 'layer_priority', 5)}, "
597
+ f"layer_name='{getattr(tab.sm, 'layer_name', '')}'"
598
+ )
599
+
600
+ if not layers:
601
+ QMessageBox.warning(
602
+ self, self.tr("Warning"),
603
+ self.tr("No layers are defined."))
604
+ return
605
+
606
+ StaTableLogger.debug(
607
+ f"Opening LayerSettingsDialog with "
608
+ f"{len(layers)} layers")
609
+ dlg = LayerSettingsDialog(layers, self)
610
+ if dlg.exec() == QDialog.Accepted:
611
+ dlg.apply_settings()
612
+ StaTableLogger.info("Layer settings updated")
613
+ for name, sm in layers:
614
+ StaTableLogger.debug(
615
+ f" after apply: '{name}' -> "
616
+ f"priority={sm.layer_priority}, "
617
+ f"layer_name='{sm.layer_name}', "
618
+ f"description='{sm.layer_description}'"
619
+ )
620
+ else:
621
+ StaTableLogger.debug(
622
+ "LayerSettingsDialog cancelled")
623
+
624
+ # ------------------------------------------------------------------
625
+ # Dialog launch methods
626
+ # ------------------------------------------------------------------
627
+ def open_type_manager(self):
628
+ StaTableLogger.debug(
629
+ "MainWindow.open_type_manager called")
630
+ dlg = TypeManagerDialog(self, self.global_defs)
631
+ dlg.exec()
632
+ StaTableLogger.debug(
633
+ "TypeManagerDialog closed")
634
+
635
+ def open_global_defs_dialog(self):
636
+ StaTableLogger.debug(
637
+ "MainWindow.open_global_defs_dialog called")
638
+ self.global_defs.add_timer_variables()
639
+ dlg = GlobalDefinitionsDialog(
640
+ self.global_defs, self)
641
+ dlg.exec()
642
+ StaTableLogger.debug(
643
+ "GlobalDefinitionsDialog closed")
644
+
645
+ def open_event_definition_dialog(self):
646
+ StaTableLogger.debug(
647
+ "MainWindow.open_event_definition_dialog called")
648
+
649
+ current_tab = self.tab_widget.currentWidget()
650
+ if current_tab is None or \
651
+ not hasattr(current_tab, 'sm'):
652
+ QMessageBox.warning(
653
+ self, self.tr("Warning"),
654
+ self.tr("There is no state transition tab."))
655
+ return
656
+ dlg = EventDefinitionDialog(
657
+ current_tab.sm, self.global_defs, self)
658
+ if dlg.exec() == QDialog.Accepted:
659
+ current_tab.update_mermaid()
660
+ StaTableLogger.info(
661
+ "Event definitions updated")
662
+
663
+ def open_event_delivery_settings(self):
664
+ StaTableLogger.debug(
665
+ "MainWindow.open_event_delivery_settings called")
666
+ current_tab = self.tab_widget.currentWidget()
667
+ if current_tab is None or \
668
+ not hasattr(current_tab, 'sm'):
669
+ QMessageBox.warning(
670
+ self, self.tr("Warning"),
671
+ self.tr("There is no state transition tab."))
672
+ return
673
+
674
+ auto_convert = \
675
+ self.prefs.auto_convert_isr_direct_to_double
676
+ dlg = EventDeliverySettingsDialog(
677
+ current_tab.sm, self.global_defs,
678
+ auto_convert=auto_convert, parent=self)
679
+ if dlg.exec() == QDialog.Accepted:
680
+ self.prefs.auto_convert_isr_direct_to_double = \
681
+ dlg.get_auto_convert()
682
+ current_tab.update_mermaid()
683
+ StaTableLogger.info(
684
+ "Event delivery settings updated")
685
+
686
+ def open_interrupt_settings(self):
687
+ StaTableLogger.debug(
688
+ "MainWindow.open_interrupt_settings called")
689
+
690
+ event_names = []
691
+ role_functions = {}
692
+ for index in range(self.tab_widget.count()):
693
+ tab = self.tab_widget.widget(index)
694
+ if hasattr(tab, 'sm'):
695
+ event_names.extend(tab.sm.events.keys())
696
+ role_functions.update(
697
+ tab.sm.role_functions)
698
+
699
+ event_names = list(set(event_names))
700
+ dlg = InterruptHandlerEditDialog(
701
+ global_defs=self.global_defs,
702
+ event_names=event_names,
703
+ role_functions=role_functions,
704
+ parent=self)
705
+ dlg.exec()
706
+ StaTableLogger.debug(
707
+ "InterruptHandlerEditDialog closed")
708
+
709
+ # ------------------------------------------------------------------
710
+ # Project save / load
711
+ # ------------------------------------------------------------------
712
+ def save_project(self) -> bool:
713
+ """Save all tabs, global definitions, shared libraries, and
714
+ project settings.
715
+
716
+ [v2.3] Returns:
717
+ True on success, False on cancel or failure.
718
+ """
719
+ StaTableLogger.debug(
720
+ "MainWindow.save_project called")
721
+
722
+ tabs = []
723
+ for index in range(self.tab_widget.count()):
724
+ tab = self.tab_widget.widget(index)
725
+ name = self.tab_widget.tabText(index)
726
+ tabs.append((name, tab.sm))
727
+
728
+ last_dir = self.prefs.last_project_dir
729
+ default_path = str(
730
+ Path(last_dir) / "project.xml") \
731
+ if last_dir else "project.xml"
732
+
733
+ filepath, _ = QFileDialog.getSaveFileName(
734
+ self, "Save Project", default_path,
735
+ "XML files (*.xml)")
736
+ if not filepath:
737
+ StaTableLogger.debug("Save cancelled")
738
+ return False
739
+ try:
740
+ # Collect project settings
741
+ config = self.config_manager.get_config()
742
+ project_settings = {
743
+ 'project_name': config.project_name,
744
+ 'table_type': config.table_type,
745
+ 'generation_style':
746
+ config.generation_style,
747
+ 'os_type': config.os_type,
748
+ 'folder_structure':
749
+ config.folder_structure,
750
+ 'include_dir_name':
751
+ config.include_dir_name,
752
+ 'source_dir_name':
753
+ config.source_dir_name,
754
+ 'common_dir_name':
755
+ config.common_dir_name,
756
+ 'project_dir_name':
757
+ config.project_dir_name,
758
+ 'generate_super_include':
759
+ config.generate_super_include,
760
+ 'super_include_file':
761
+ config.super_include_file,
762
+ 'external_includes':
763
+ list(config.external_includes),
764
+ 'external_includes_in_super':
765
+ config.external_includes_in_super,
766
+ 'external_includes_in_role':
767
+ config.external_includes_in_role,
768
+ 'external_includes_in_transitions':
769
+ config.external_includes_in_transitions,
770
+ 'external_includes_in_common':
771
+ config.external_includes_in_common,
772
+ 'max_consecutive_pending_events':
773
+ config.max_consecutive_pending_events,
774
+ }
775
+ StaTableLogger.debug(
776
+ f"Project settings: {project_settings}")
777
+
778
+ project_to_xml(
779
+ tabs,
780
+ self.global_defs,
781
+ filepath,
782
+ role_function_library=(
783
+ self.role_function_library),
784
+ condition_library=self.condition_library,
785
+ literal_library=self.literal_library,
786
+ project_settings=project_settings,
787
+ )
788
+ self.prefs.last_project_dir = str(
789
+ Path(filepath).parent)
790
+ self.logger.info(
791
+ f"Project saved to {filepath}")
792
+
793
+ # [v2.3] Clear modified flag on success
794
+ self.setWindowModified(False)
795
+ self._update_window_title()
796
+ return True
797
+ except Exception as e:
798
+ import traceback
799
+ self.logger.error(
800
+ f"Failed to save project: "
801
+ f"{filepath}, error: {e}\n"
802
+ f"{traceback.format_exc()}")
803
+ QMessageBox.critical(
804
+ self, self.tr("Error"),
805
+ f"Failed to save project:\n{e}")
806
+ return False
807
+
808
+ def open_project(self):
809
+ """Load the whole project"""
810
+ # [v2.3] Prompt to save before loading
811
+ if not self._maybe_save():
812
+ return
813
+
814
+ StaTableLogger.debug(
815
+ "MainWindow.open_project called")
816
+
817
+ last_dir = self.prefs.last_project_dir
818
+ filepath, _ = QFileDialog.getOpenFileName(
819
+ self, "Open Project", last_dir,
820
+ "XML files (*.xml)")
821
+ if not filepath:
822
+ StaTableLogger.debug("Open cancelled")
823
+ return
824
+ try:
825
+ StaTableLogger.debug(
826
+ f"Calling project_from_xml: {filepath}")
827
+ (tabs, global_defs, role_lib,
828
+ cond_lib, lit_lib,
829
+ project_settings) = project_from_xml(filepath)
830
+
831
+ StaTableLogger.debug(
832
+ f"Loaded: tabs={len(tabs)}, "
833
+ f"project_settings={project_settings}"
834
+ )
835
+
836
+ self.close_all_tabs()
837
+ for name, sm in tabs:
838
+ self.add_state_machine_tab(name, sm)
839
+
840
+ # [v2.3] Clear modified flag AFTER tabs are restored,
841
+ # so dataModified emissions from add_state_machine_tab
842
+ # do not leave the window marked as modified.
843
+ self.setWindowModified(False)
844
+ self._update_window_title()
845
+
846
+ self.global_defs = global_defs
847
+ self.global_defs.add_timer_variables()
848
+
849
+ # Replace shared library too
850
+ if role_lib is not None:
851
+ self.role_function_library = role_lib
852
+ if cond_lib is not None:
853
+ self.condition_library = cond_lib
854
+ if lit_lib is not None:
855
+ self.literal_library = lit_lib
856
+
857
+ # ============================================================
858
+ # Auto-complete 1: register each tab's SM role functions into the shared library
859
+ # Even if <SharedLibraries> in the XML is empty, present candidates in the palette
860
+ # ============================================================
861
+ from statable_gui.libcntrl.role_function_library import (
862
+ RoleFunction as LibRoleFunction)
863
+ registered_rfs = 0
864
+ for _tab_name, _sm in tabs:
865
+ for _rf in _sm.role_functions.values():
866
+ _qn = (getattr(_rf, 'qualified_name', None)
867
+ or getattr(_rf, 'name', ''))
868
+ if not _qn:
869
+ continue
870
+ #Skip if already present in library
871
+ if self.role_function_library.get(_qn) is not None:
872
+ continue
873
+ try:
874
+ self.role_function_library.add(LibRoleFunction(
875
+ name=getattr(_rf, 'name', '') or '',
876
+ namespace=getattr(_rf, 'namespace', '') or '',
877
+ title=getattr(_rf, 'title', '') or '',
878
+ description=getattr(_rf, 'description', '') or '',
879
+ ))
880
+ registered_rfs += 1
881
+ except ValueError:
882
+ pass # Duplicates ignored
883
+ except Exception as e:
884
+ StaTableLogger.warning(
885
+ f"Failed to register role function {_qn}: {e}")
886
+
887
+ if registered_rfs:
888
+ StaTableLogger.info(
889
+ f"Registered {registered_rfs} role functions "
890
+ f"from tabs to shared library "
891
+ f"(total={len(self.role_function_library.list_all())})"
892
+ )
893
+
894
+ # ============================================================
895
+ # Auto-complete 2: register each transition's condition into ConditionLibrary
896
+ # ============================================================
897
+ from statable_gui.libcntrl.condition_library import (
898
+ ConditionTemplate)
899
+ existing_cond_exprs = set()
900
+ for _ct in self.condition_library.list_all():
901
+ _c = (getattr(_ct, 'condition', '') or '').strip()
902
+ if _c:
903
+ existing_cond_exprs.add(_c)
904
+ existing_cond_names = {
905
+ ct.name for ct in self.condition_library.list_all()
906
+ }
907
+
908
+ registered_conds = 0
909
+ for _tab_name, _sm in tabs:
910
+ for _trans in _sm.transitions:
911
+ _cond = (getattr(_trans, 'condition', '') or '').strip()
912
+ if not _cond:
913
+ continue
914
+ if _cond in existing_cond_exprs:
915
+ continue
916
+
917
+ _base = _cond[:40]
918
+ _name = _base
919
+ _idx = 2
920
+ while _name in existing_cond_names:
921
+ _name = f"{_base}_{_idx}"
922
+ _idx += 1
923
+
924
+ try:
925
+ self.condition_library.add(ConditionTemplate(
926
+ name=_name,
927
+ condition=_cond,
928
+ description=f"Auto-collected ({_tab_name})",
929
+ ))
930
+ existing_cond_exprs.add(_cond)
931
+ existing_cond_names.add(_name)
932
+ registered_conds += 1
933
+ except ValueError:
934
+ pass
935
+ except Exception as e:
936
+ StaTableLogger.warning(
937
+ f"Failed to register condition '{_name}': {e}")
938
+
939
+ if registered_conds:
940
+ StaTableLogger.info(
941
+ f"Registered {registered_conds} conditions "
942
+ f"from transitions "
943
+ f"(total={len(self.condition_library.list_all())})"
944
+ )
945
+
946
+ # ============================================================
947
+ # Apply project settings
948
+ # ============================================================
949
+ if project_settings:
950
+ config = self.config_manager.get_config()
951
+ for key, value in project_settings.items():
952
+ if hasattr(config, key):
953
+ setattr(config, key, value)
954
+ StaTableLogger.debug(
955
+ f" config.{key} = {value}")
956
+ self.config_manager.set_config(config)
957
+
958
+ # ============================================================
959
+ # Update library references of existing tabs
960
+ # ============================================================
961
+ for index in range(self.tab_widget.count()):
962
+ tab = self.tab_widget.widget(index)
963
+ if hasattr(tab, 'role_function_library'):
964
+ tab.role_function_library = \
965
+ self.role_function_library
966
+ if hasattr(tab, 'condition_library'):
967
+ tab.condition_library = \
968
+ self.condition_library
969
+ if hasattr(tab, 'literal_library'):
970
+ tab.literal_library = \
971
+ self.literal_library
972
+ if hasattr(tab, 'table'):
973
+ tab.table.role_function_library = \
974
+ self.role_function_library
975
+ tab.table.condition_library = \
976
+ self.condition_library
977
+ tab.table.literal_library = \
978
+ self.literal_library
979
+ if hasattr(tab.table, 'populate'):
980
+ try:
981
+ tab.table.populate()
982
+ except Exception:
983
+ pass
984
+
985
+ self.prefs.last_project_dir = str(
986
+ Path(filepath).parent)
987
+ self.logger.info(
988
+ f"Project loaded from {filepath}")
989
+ except Exception as e:
990
+ import traceback
991
+ self.logger.error(
992
+ f"Failed to open project: "
993
+ f"{filepath}, error: {e}\n"
994
+ f"{traceback.format_exc()}")
995
+ QMessageBox.critical(
996
+ self, self.tr("Error"),
997
+ f"Failed to open project:\n{e}")
998
+
999
+ def close_all_tabs(self):
1000
+ StaTableLogger.debug("close_all_tabs called")
1001
+ while self.tab_widget.count() > 0:
1002
+ widget = self.tab_widget.widget(0)
1003
+ self.tab_widget.removeTab(0)
1004
+ widget.deleteLater()
1005
+
1006
+ # ------------------------------------------------------------------
1007
+ # Tab management
1008
+ # ------------------------------------------------------------------
1009
+ def rename_current_tab(self):
1010
+ index = self.tab_widget.currentIndex()
1011
+ if index >= 0:
1012
+ self.rename_tab_at(index)
1013
+
1014
+ def rename_tab_at(self, index: int):
1015
+ if index < 0:
1016
+ return
1017
+ current_name = self.tab_widget.tabText(index)
1018
+ new_name, ok = QInputDialog.getText(
1019
+ self, "Rename Tab",
1020
+ "Enter new tab name:",
1021
+ text=current_name)
1022
+ if ok and new_name.strip():
1023
+ self.tab_widget.setTabText(
1024
+ index, new_name.strip())
1025
+ self.logger.info(
1026
+ f"Tab renamed: {current_name} -> "
1027
+ f"{new_name.strip()}")
1028
+ # [v2.3] Mark modified
1029
+ self.setWindowModified(True)
1030
+ self._update_window_title()
1031
+
1032
+ def add_new_tab(self):
1033
+ name, ok = QInputDialog.getText(
1034
+ self, "New State Machine",
1035
+ "Enter tab name:")
1036
+ if ok and name.strip():
1037
+ sm = StateMachine()
1038
+ self.add_state_machine_tab(name.strip(), sm)
1039
+ self.logger.info(
1040
+ f"New tab added: {name.strip()}")
1041
+ # [v2.3] Mark modified
1042
+ self.setWindowModified(True)
1043
+ self._update_window_title()
1044
+
1045
+ def add_state_machine_tab(self, name: str,
1046
+ sm: StateMachine):
1047
+ StaTableLogger.debug(
1048
+ f"add_state_machine_tab: name={name}, "
1049
+ f"layer_name='{getattr(sm, 'layer_name', '')}', "
1050
+ f"layer_priority={getattr(sm, 'layer_priority', 5)}, "
1051
+ f"roles={len(self.role_function_library.list_all())}"
1052
+ )
1053
+
1054
+ #If layer name unset, use tab name
1055
+ if not getattr(sm, 'layer_name', ''):
1056
+ sm.layer_name = name
1057
+
1058
+ tab = StateMachineTab(
1059
+ sm,
1060
+ global_defs=self.global_defs,
1061
+ role_function_library=self.role_function_library,
1062
+ condition_library=self.condition_library,
1063
+ literal_library=self.literal_library,
1064
+ # [v2.4] Provide all tab names as namespace candidates.
1065
+ # Forwarded to SettingsPanel so the inline combo
1066
+ # box and RoleFunctionDialog list every layer.
1067
+ layer_names_provider=self._get_all_layer_names)
1068
+
1069
+ # [v2.3] Relay tab modification signal to MainWindow
1070
+ tab.dataModified.connect(self._on_tab_data_modified)
1071
+
1072
+ idx = self.tab_widget.addTab(tab, name)
1073
+ self.tab_widget.setCurrentIndex(idx)
1074
+ self.logger.debug(
1075
+ f"Tab '{name}' added at index {idx} "
1076
+ f"(layer_name='{sm.layer_name}')")
1077
+
1078
+ def close_tab(self, index: int):
1079
+ if self.tab_widget.count() <= 1:
1080
+ QMessageBox.warning(
1081
+ self, self.tr("Warning"),
1082
+ self.tr("At least one tab is required."))
1083
+ return
1084
+ widget = self.tab_widget.widget(index)
1085
+ self.tab_widget.removeTab(index)
1086
+ widget.deleteLater()
1087
+ self.logger.info(
1088
+ f"Tab closed at index {index}")
1089
+ # [v2.3] Mark modified
1090
+ self.setWindowModified(True)
1091
+ self._update_window_title()
1092
+
1093
+ # ------------------------------------------------------------------
1094
+ # Log / validation / code generation
1095
+ # ------------------------------------------------------------------
1096
+ def toggle_traceball(self, checked: bool):
1097
+ if checked:
1098
+ self.traceball.show()
1099
+ self.logger.debug("TraceBall shown")
1100
+ else:
1101
+ self.traceball.hide()
1102
+ self.logger.debug("TraceBall hidden")
1103
+
1104
+ def _get_current_state_machine(self):
1105
+ current_tab = self.tab_widget.currentWidget()
1106
+ if current_tab is not None and \
1107
+ hasattr(current_tab, 'sm'):
1108
+ return current_tab.sm
1109
+ return None
1110
+
1111
+ def _get_current_data(self):
1112
+ sm = self._get_current_state_machine()
1113
+ if sm is not None and self.global_defs is not None:
1114
+ return sm, self.global_defs
1115
+
1116
+ sample_gen = SampleDataGenerator()
1117
+ return sample_gen.get_sample_data()
1118
+
1119
+ # Multi-layer support helper
1120
+ def _get_all_layers(self):
1121
+ """Return the (name, sm) list of all tabs sorted by priority ascending"""
1122
+ layers = []
1123
+ for index in range(self.tab_widget.count()):
1124
+ tab = self.tab_widget.widget(index)
1125
+ if hasattr(tab, 'sm'):
1126
+ name = self.tab_widget.tabText(index)
1127
+ layers.append((name, tab.sm))
1128
+ layers.sort(
1129
+ key=lambda x: getattr(x[1], 'layer_priority', 5)
1130
+ )
1131
+ return layers
1132
+
1133
+ def _get_all_layer_names(self) -> list:
1134
+ """Return all tab names (= all layer names in the project).
1135
+
1136
+ [v2.4]
1137
+ Used by StateMachineTab / SettingsPanel to populate the
1138
+ namespace combo box with every layer in the project, not
1139
+ just the current tab's layer. Called via the
1140
+ `layer_names_provider` callable on every editor creation,
1141
+ so newly added / renamed tabs are reflected immediately.
1142
+ """
1143
+ names = []
1144
+ try:
1145
+ for i in range(self.tab_widget.count()):
1146
+ text = self.tab_widget.tabText(i)
1147
+ if text and text not in names:
1148
+ names.append(text)
1149
+ except Exception as e:
1150
+ StaTableLogger.warning(
1151
+ f"_get_all_layer_names failed: {e}")
1152
+ return names
1153
+
1154
+ def open_validation_dialog(self):
1155
+ StaTableLogger.debug(
1156
+ "MainWindow.open_validation_dialog called")
1157
+ sm, gd = self._get_current_data()
1158
+ dialog = ValidationDialog(sm, gd, self)
1159
+ dialog.exec()
1160
+ StaTableLogger.debug(
1161
+ "ValidationDialog closed")
1162
+
1163
+ def open_code_generation_dialog(self):
1164
+ StaTableLogger.debug(
1165
+ "MainWindow.open_code_generation_dialog called")
1166
+
1167
+ # Collect all tabs as layers
1168
+ layers = self._get_all_layers()
1169
+ if layers:
1170
+ primary_sm = layers[0][1]
1171
+ gd = self.global_defs
1172
+ else:
1173
+ sample_gen = SampleDataGenerator()
1174
+ primary_sm, gd = sample_gen.get_sample_data()
1175
+ layers = [(getattr(primary_sm, 'layer_name', ''),
1176
+ primary_sm)]
1177
+
1178
+ dialog = CodeGenerationDialog(
1179
+ state_machine=primary_sm,
1180
+ global_defs=gd,
1181
+ parent=self,
1182
+ role_function_library=(
1183
+ self.role_function_library),
1184
+ )
1185
+ # Pass all layers
1186
+ dialog.all_layers = layers
1187
+ dialog.config_manager = self.config_manager
1188
+ dialog._load_config_to_ui()
1189
+ dialog.exec()
1190
+ StaTableLogger.debug(
1191
+ "CodeGenerationDialog closed")
1192
+
1193
+ def open_code_generation_settings(self):
1194
+ StaTableLogger.debug(
1195
+ "MainWindow.open_code_generation_settings called")
1196
+ dialog = CodeGenerationSettingsDialog(
1197
+ config_manager=self.config_manager,
1198
+ parent=self)
1199
+ dialog.exec()
1200
+ StaTableLogger.debug(
1201
+ "CodeGenerationSettingsDialog closed")
1202
+
1203
+ # ------------------------------------------------------------------
1204
+ # Direct save of generated code (multi-layer + warning collection)
1205
+ # ------------------------------------------------------------------
1206
+ def save_generated_code_direct(self):
1207
+ StaTableLogger.debug(
1208
+ "MainWindow.save_generated_code_direct called")
1209
+ config = self.config_manager.get_config()
1210
+ output_dir = config.output_directory
1211
+
1212
+ if not output_dir:
1213
+ QMessageBox.warning(
1214
+ self, self.tr("Warning"),
1215
+ self.tr("Output directory is not set.\n"
1216
+ # [C-19 fix] Add the missing '.' and '\n' so the
1217
+ # message reads "... in settings.\nPlease specify."
1218
+ "First set the output directory in settings.\n"
1219
+ "Please specify."))
1220
+ self.open_code_generation_settings()
1221
+ config = self.config_manager.get_config()
1222
+ output_dir = config.output_directory
1223
+ if not output_dir:
1224
+ return
1225
+
1226
+ # Collect all tabs as layers
1227
+ layers = self._get_all_layers()
1228
+ if not layers:
1229
+ QMessageBox.warning(
1230
+ self, self.tr("Warning"), self.tr("There are no tabs."))
1231
+ return
1232
+
1233
+ global_defs = self.global_defs
1234
+ if global_defs is None:
1235
+ sample_gen = SampleDataGenerator()
1236
+ _, global_defs = sample_gen.get_sample_data()
1237
+
1238
+ collector = WarningCollector()
1239
+ root_logger = logging.getLogger()
1240
+ root_logger.addHandler(collector)
1241
+
1242
+ saved_files = []
1243
+ try:
1244
+ generator = CCodeGenerator(config=config)
1245
+ # Multi-layer generation
1246
+ generated_files = generator.generate_all_layers(
1247
+ layers,
1248
+ global_defs,
1249
+ role_function_library=(
1250
+ self.role_function_library),
1251
+ )
1252
+ os.makedirs(output_dir, exist_ok=True)
1253
+
1254
+ if config.save_with_merge:
1255
+ saved_files = (
1256
+ generator
1257
+ .save_generated_code_with_merge(
1258
+ generated_files, output_dir))
1259
+ else:
1260
+ saved_files = (
1261
+ generator.save_generated_code(
1262
+ generated_files, output_dir))
1263
+
1264
+ StaTableLogger.info(
1265
+ f"{len(saved_files)} files saved to "
1266
+ f"{output_dir} ({len(layers)} layers)")
1267
+
1268
+ except Exception as e:
1269
+ QMessageBox.critical(
1270
+ self, self.tr("Error"),
1271
+ f"Code generation failed:\n{e}")
1272
+ StaTableLogger.error(
1273
+ f"Code generation failed: {e}")
1274
+ return
1275
+ finally:
1276
+ root_logger.removeHandler(collector)
1277
+
1278
+ QMessageBox.information(
1279
+ self, self.tr("Save complete"),
1280
+ f"{len(saved_files)} files saved.\n"
1281
+ f"Layers: {len(layers)}\n\n"
1282
+ f"Output: {output_dir}")
1283
+
1284
+ if collector.records:
1285
+ seen = set()
1286
+ unique = []
1287
+ for r in collector.records:
1288
+ if r not in seen:
1289
+ seen.add(r)
1290
+ unique.append(r)
1291
+ QMessageBox.warning(
1292
+ self, self.tr("Warnings during generation"),
1293
+ "The following warnings occurred:\n\n"
1294
+ + "\n".join(f"- {m}" for m in unique))
1295
+
1296
+ # ==================================================================
1297
+ # New Project feature (v2.3 / F-15)
1298
+ # ==================================================================
1299
+
1300
+ def new_project(self) -> None:
1301
+ """Start a new empty project.
1302
+
1303
+ Preserves: Preferences, TraceBall log.
1304
+ Clears: tabs, global definitions, shared libraries,
1305
+ ConfigManager, windowModified flag.
1306
+ """
1307
+ if not self._maybe_save():
1308
+ return
1309
+
1310
+ StaTableLogger.debug("MainWindow.new_project: start")
1311
+
1312
+ # 1. Remove all tabs (close_all_tabs uses removeTab directly)
1313
+ self.close_all_tabs()
1314
+
1315
+ # 2. Reset project-scoped global definitions
1316
+ self.global_defs = GlobalDefinitions()
1317
+ self.global_defs.add_timer_variables()
1318
+
1319
+ # 3. Clear shared libraries (decision #5, revised)
1320
+ self.role_function_library = RoleFunctionLibrary()
1321
+ self.condition_library = ConditionLibrary()
1322
+ self.literal_library = LiteralLibrary()
1323
+
1324
+ # 4. Reset code generation config to defaults
1325
+ self.config_manager.reset()
1326
+
1327
+ # 5. One empty Application layer
1328
+ empty_sm = StateMachine()
1329
+ empty_sm.layer_name = "Application"
1330
+ empty_sm.layer_priority = 5
1331
+ self.add_state_machine_tab("Application", empty_sm)
1332
+
1333
+ # 6. Reset modified flag and window title
1334
+ self.setWindowModified(False)
1335
+ self._update_window_title()
1336
+
1337
+ # 7. Status bar notification
1338
+ self.statusBar().showMessage("New project created", 3000)
1339
+
1340
+ StaTableLogger.debug("MainWindow.new_project: done")
1341
+
1342
+ def _maybe_save(self) -> bool:
1343
+ """Prompt to save if there are unsaved changes.
1344
+
1345
+ Returns True to proceed, False to abort.
1346
+ Called from new_project / open_project / closeEvent.
1347
+ """
1348
+ if not self.isWindowModified():
1349
+ return True
1350
+
1351
+ ret = QMessageBox.warning(
1352
+ self,
1353
+ self.tr("Unsaved Changes"),
1354
+ self.tr("The current project has unsaved changes.\n"
1355
+ "Do you want to save them before continuing?"),
1356
+ QMessageBox.Save | QMessageBox.Discard | QMessageBox.Cancel,
1357
+ QMessageBox.Save,
1358
+ )
1359
+ if ret == QMessageBox.Save:
1360
+ return self.save_project()
1361
+ return ret == QMessageBox.Discard
1362
+
1363
+ def closeEvent(self, event) -> None:
1364
+ """Prompt on window close if there are unsaved changes."""
1365
+ if self._maybe_save():
1366
+ event.accept()
1367
+ else:
1368
+ event.ignore()
1369
+
1370
+ def _update_window_title(self) -> None:
1371
+ """Update window title with modified marker.
1372
+
1373
+ Qt replaces [*] with '*' when setWindowModified(True).
1374
+ """
1375
+ self.setWindowTitle(self.tr("Untitled[*] - StaTable"))
1376
+
1377
+ def _on_tab_data_modified(self) -> None:
1378
+ """Slot connected to each StateMachineTab.dataModified signal."""
1379
+ self.setWindowModified(True)
1380
+ self._update_window_title()