statable 3.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- codegen/__init__.py +38 -0
- codegen/c_code_generator.py +1752 -0
- codegen/code_merger.py +371 -0
- codegen/code_templates.py +509 -0
- codegen/config.py +167 -0
- codegen/enum_generator.py +377 -0
- codegen/event_queue_generator.py +303 -0
- codegen/interrupt_generator.py +345 -0
- codegen/naming_convention.py +171 -0
- codegen/osal_generator.py +282 -0
- codegen/role_function_generator.py +1244 -0
- codegen/sample_data.py +269 -0
- codegen/state_actions_generator.py +395 -0
- codegen/struct_generator.py +721 -0
- codegen/timer_generator.py +299 -0
- codegen/transition_generator.py +907 -0
- codegen/type_mapper.py +69 -0
- codegen/validate/__init__.py +38 -0
- codegen/validate/change_actions.py +90 -0
- codegen/validate/change_applier.py +416 -0
- codegen/validate/clipboard_manager.py +47 -0
- codegen/validate/data/__init__.py +30 -0
- codegen/validate/data/action_definitions.py +200 -0
- codegen/validate/data/keywords.py +46 -0
- codegen/validate/data/prompt_templates.py +176 -0
- codegen/validate/data/validation_rules.py +205 -0
- codegen/validate/items/__init__.py +30 -0
- codegen/validate/items/base_validator.py +25 -0
- codegen/validate/items/cell_validator.py +275 -0
- codegen/validate/items/custom_type_validator.py +75 -0
- codegen/validate/items/event_validator.py +78 -0
- codegen/validate/items/flag_validator.py +74 -0
- codegen/validate/items/interrupt_validator.py +75 -0
- codegen/validate/items/queue_validator.py +73 -0
- codegen/validate/items/role_function_validator.py +175 -0
- codegen/validate/items/state_validator.py +88 -0
- codegen/validate/items/timer_validator.py +120 -0
- codegen/validate/items/transition_validator.py +113 -0
- codegen/validate/items/variable_validator.py +132 -0
- codegen/validate/logger.py +52 -0
- codegen/validate/models.py +177 -0
- codegen/validate/prompt_generator.py +724 -0
- codegen/validate/response_parser.py +154 -0
- codegen/validate/response_validator.py +275 -0
- codegen/validate/validation_dialog.py +322 -0
- codegen/validate/validator.py +94 -0
- codegen/variable_generator.py +419 -0
- statable/__init__.py +71 -0
- statable/__main__.py +49 -0
- statable/cli.py +251 -0
- statable/global_defs.py +256 -0
- statable/mermaid_gen.py +144 -0
- statable/model.py +426 -0
- statable/parser.py +64 -0
- statable/sample_data.py +435 -0
- statable/shared/__init__.py +24 -0
- statable/shared/condition_library.py +63 -0
- statable/shared/literal_library.py +66 -0
- statable/shared/role_function_library.py +150 -0
- statable/state_machine.py +121 -0
- statable/xml_io.py +1164 -0
- statable-3.1.0.dist-info/METADATA +151 -0
- statable-3.1.0.dist-info/RECORD +117 -0
- statable-3.1.0.dist-info/WHEEL +5 -0
- statable-3.1.0.dist-info/entry_points.txt +2 -0
- statable-3.1.0.dist-info/top_level.txt +3 -0
- statable_gui/__init__.py +23 -0
- statable_gui/action_edit_dialog.py +214 -0
- statable_gui/code_generation_dialog.py +486 -0
- statable_gui/code_generation_settings_dialog.py +522 -0
- statable_gui/common_widgets.py +537 -0
- statable_gui/condition_builder_dialog.py +500 -0
- statable_gui/condition_edit_dialog.py +123 -0
- statable_gui/condition_template_dialog.py +58 -0
- statable_gui/config.py +21 -0
- statable_gui/dialogs.py +439 -0
- statable_gui/event_definition_dialog.py +605 -0
- statable_gui/event_delivery_settings_dialog.py +191 -0
- statable_gui/event_queue_dialog.py +245 -0
- statable_gui/global_defs.py +167 -0
- statable_gui/global_defs_dialog.py +796 -0
- statable_gui/i18n/__init__.py +118 -0
- statable_gui/interrupt_handler_edit_dialog.py +810 -0
- statable_gui/layer_settings_dialog.py +180 -0
- statable_gui/libcntrl/__init__.py +23 -0
- statable_gui/libcntrl/condition_library.py +13 -0
- statable_gui/libcntrl/literal_library.py +13 -0
- statable_gui/libcntrl/literal_management_dialog.py +185 -0
- statable_gui/libcntrl/role_function_edit_dialog.py +145 -0
- statable_gui/libcntrl/role_function_library.py +13 -0
- statable_gui/literal_definition_dialog.py +62 -0
- statable_gui/logger.py +83 -0
- statable_gui/main_window.py +1380 -0
- statable_gui/matrix_table.py +413 -0
- statable_gui/preference_keys.py +18 -0
- statable_gui/preferences.py +74 -0
- statable_gui/role_function_dialog.py +244 -0
- statable_gui/sample_data.py +11 -0
- statable_gui/state_actions_dialog.py +398 -0
- statable_gui/symbol_picker.py +170 -0
- statable_gui/traceball.py +37 -0
- statable_gui/transition_editor_direct/__init__.py +15 -0
- statable_gui/transition_editor_direct/actions_tab.py +856 -0
- statable_gui/transition_editor_direct/code_widget.py +344 -0
- statable_gui/transition_editor_direct/coverage_analyzer.py +163 -0
- statable_gui/transition_editor_direct/dialog.py +335 -0
- statable_gui/transition_editor_direct/draft.py +270 -0
- statable_gui/transition_editor_direct/edit_dialogs.py +160 -0
- statable_gui/transition_editor_direct/overview_tab.py +173 -0
- statable_gui/transition_editor_direct/palette_widget.py +233 -0
- statable_gui/transition_editor_direct/relations_edit_dialog.py +156 -0
- statable_gui/transition_editor_direct/relations_tab.py +230 -0
- statable_gui/transition_editor_direct/system_global_dialog.py +52 -0
- statable_gui/transition_editor_direct/transition_actions_dialog.py +109 -0
- statable_gui/transition_editor_direct/transitions_tab.py +620 -0
- statable_gui/validation_dialog.py +285 -0
- statable_gui/widgets.py +1315 -0
|
@@ -0,0 +1,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()
|