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,486 @@
1
+ # statable_gui/code_generation_dialog.py
2
+ """\nC code generation dialog (PySide6 compatible / multi-layer support)\n"""
3
+
4
+ import os
5
+ import sys
6
+ import json
7
+ import logging
8
+ from typing import Dict, Optional, List
9
+
10
+ from PySide6.QtWidgets import (
11
+ QDialog, QVBoxLayout, QHBoxLayout, QPushButton,
12
+ QLabel, QTextEdit, QFileDialog, QMessageBox,
13
+ QComboBox, QGroupBox, QFormLayout,
14
+ QProgressBar, QLineEdit
15
+ )
16
+ from PySide6.QtCore import Qt
17
+ from PySide6.QtGui import QFont
18
+
19
+ # Path settings
20
+ sys.path.append(
21
+ os.path.dirname(
22
+ os.path.dirname(os.path.abspath(__file__))
23
+ )
24
+ )
25
+
26
+ try:
27
+ from codegen.c_code_generator import CCodeGenerator
28
+ from codegen.sample_data import SampleDataGenerator
29
+ from codegen.config import (
30
+ CodeGenerationConfig, ConfigManager,
31
+ )
32
+ except ImportError:
33
+ sys.path.append(os.path.join(
34
+ os.path.dirname(
35
+ os.path.dirname(os.path.abspath(__file__))
36
+ ),
37
+ 'codegen',
38
+ ))
39
+ from c_code_generator import CCodeGenerator
40
+ from sample_data import SampleDataGenerator
41
+ from config import (
42
+ CodeGenerationConfig, ConfigManager,
43
+ )
44
+
45
+ #Import the settings dialog
46
+ try:
47
+ from .code_generation_settings_dialog import (
48
+ CodeGenerationSettingsDialog,
49
+ )
50
+ except ImportError:
51
+ from code_generation_settings_dialog import (
52
+ CodeGenerationSettingsDialog,
53
+ )
54
+
55
+ logger = logging.getLogger(__name__)
56
+
57
+ #Default settings file path
58
+ DEFAULT_SETTINGS_FILE = os.path.join(
59
+ os.path.dirname(os.path.abspath(__file__)),
60
+ "codegen_settings.json",
61
+ )
62
+
63
+
64
+ # ============================================================
65
+ # Warning collector class
66
+ # ============================================================
67
+ class WarningCollector(logging.Handler):
68
+ """\n Handler that collects logs at WARNING level or higher\n\n usage:\n collector = WarningCollector()\n root = logging.getLogger()\n root.addHandler(collector)\n try:\n ... processing ...\n finally:\n root.removeHandler(collector)\n\n if collector.records:\n show_message_box(collector.records)\n """
69
+
70
+ def __init__(self):
71
+ super().__init__(level=logging.WARNING)
72
+ self.records: List[str] = []
73
+
74
+ def emit(self, record):
75
+ if record.levelno >= logging.WARNING:
76
+ try:
77
+ msg = record.getMessage()
78
+ except Exception:
79
+ msg = str(record.msg)
80
+ self.records.append(msg)
81
+
82
+
83
+ # ============================================================
84
+ # CodeGenerationDialog
85
+ # ============================================================
86
+ class CodeGenerationDialog(QDialog):
87
+ """C code generation dialog (multi-layer support)"""
88
+
89
+ def __init__(self, state_machine=None,
90
+ global_defs=None,
91
+ parent=None,
92
+ settings_file=None,
93
+ role_function_library=None):
94
+ super().__init__(parent)
95
+ self.state_machine = state_machine
96
+ self.global_defs = global_defs
97
+ self.generated_files: Dict[str, str] = {}
98
+ self.config_manager = ConfigManager()
99
+ self.settings_file = (
100
+ settings_file or DEFAULT_SETTINGS_FILE
101
+ )
102
+ self.role_function_library = role_function_library
103
+ # Multi-layer list (set from outside)
104
+ self.all_layers = None
105
+
106
+ self._load_saved_settings()
107
+
108
+ self.setWindowTitle(self.tr("CCode generation"))
109
+ self.setMinimumSize(800, 600)
110
+
111
+ self._setup_ui()
112
+ self._load_sample_data_if_needed()
113
+ self._load_config_to_ui()
114
+
115
+ #---- Setting read/write ----
116
+ def _load_saved_settings(self):
117
+ try:
118
+ if os.path.exists(self.settings_file):
119
+ with open(self.settings_file, 'r',
120
+ encoding='utf-8') as f:
121
+ data = json.load(f)
122
+ if 'output_directory' in data:
123
+ self.config_manager.update(
124
+ output_directory=data['output_directory'])
125
+ if 'generation_style' in data:
126
+ self.config_manager.update(
127
+ generation_style=data['generation_style'])
128
+ if 'os_type' in data:
129
+ self.config_manager.update(
130
+ os_type=data['os_type'])
131
+ if 'save_with_merge' in data:
132
+ self.config_manager.update(
133
+ save_with_merge=data['save_with_merge'])
134
+ logger.info(
135
+ f"Loaded previous settings: {data}")
136
+ except Exception as e:
137
+ logger.warning(f"Failed to load settings: {e}")
138
+
139
+ def _save_settings(self):
140
+ """Save current settings"""
141
+ try:
142
+ config = self.config_manager.get_config()
143
+ data = {
144
+ 'output_directory': config.output_directory,
145
+ 'generation_style': config.generation_style,
146
+ 'os_type': config.os_type,
147
+ 'save_with_merge': config.save_with_merge,
148
+ }
149
+ settings_dir = os.path.dirname(
150
+ self.settings_file)
151
+ if settings_dir and not os.path.exists(
152
+ settings_dir):
153
+ os.makedirs(settings_dir, exist_ok=True)
154
+ with open(self.settings_file, 'w',
155
+ encoding='utf-8') as f:
156
+ json.dump(data, f, ensure_ascii=False,
157
+ indent=2)
158
+ logger.info(f"Settings saved: {data}")
159
+ except Exception as e:
160
+ logger.warning(f"Failed to save settings: {e}")
161
+
162
+ # ---- UI ----
163
+ def _setup_ui(self):
164
+ """Build UI"""
165
+ main_layout = QVBoxLayout(self)
166
+
167
+ # Settings info group
168
+ info_group = QGroupBox(self.tr("Generation settingsInfo"))
169
+ info_layout = QFormLayout(info_group)
170
+
171
+ # Output destination
172
+ self.output_dir_edit = QLineEdit()
173
+ self.output_dir_edit.setPlaceholderText(
174
+ self.tr("Select output directory"))
175
+ self.output_dir_edit.textChanged.connect(
176
+ self._on_output_dir_changed)
177
+ self.output_dir_btn = QPushButton(self.tr("Browse..."))
178
+ self.output_dir_btn.clicked.connect(
179
+ self._select_output_dir)
180
+
181
+ output_dir_layout = QHBoxLayout()
182
+ output_dir_layout.addWidget(self.output_dir_edit)
183
+ output_dir_layout.addWidget(self.output_dir_btn)
184
+ info_layout.addRow("Output directory:", output_dir_layout)
185
+
186
+ # Generation style
187
+ self.style_combo = QComboBox()
188
+ self.style_combo.addItem(
189
+ self.tr("Table-driven style"), "table_driven")
190
+ self.style_combo.addItem(
191
+ self.tr("switch-case style"), "switch_case")
192
+ self.style_combo.currentIndexChanged.connect(
193
+ self._on_style_changed)
194
+ info_layout.addRow("Generation style:", self.style_combo)
195
+
196
+ # OS type label
197
+ self.os_label = QLabel(self.tr("NonRTOS"))
198
+ info_layout.addRow("OS type:", self.os_label)
199
+
200
+ # Merge settings label
201
+ self.merge_label = QLabel(self.tr("Enabled"))
202
+ info_layout.addRow("Merge:", self.merge_label)
203
+
204
+ # Advanced settings button
205
+ self.settings_btn = QPushButton(self.tr("Advanced settings..."))
206
+ self.settings_btn.clicked.connect(
207
+ self._open_settings_dialog)
208
+ info_layout.addRow("", self.settings_btn)
209
+
210
+ main_layout.addWidget(info_group)
211
+
212
+ # Action buttons
213
+ button_layout = QHBoxLayout()
214
+
215
+ self.generate_btn = QPushButton(self.tr("Code generation"))
216
+ self.generate_btn.clicked.connect(
217
+ self._generate_code)
218
+ button_layout.addWidget(self.generate_btn)
219
+
220
+ self.save_btn = QPushButton(self.tr("Save"))
221
+ self.save_btn.setEnabled(False)
222
+ self.save_btn.clicked.connect(self._save_code)
223
+ button_layout.addWidget(self.save_btn)
224
+
225
+ self.close_btn = QPushButton(self.tr("Close"))
226
+ self.close_btn.clicked.connect(self._on_close)
227
+ button_layout.addWidget(self.close_btn)
228
+
229
+ main_layout.addLayout(button_layout)
230
+
231
+ # Preview area
232
+ preview_label = QLabel(self.tr("Generated code preview:"))
233
+ main_layout.addWidget(preview_label)
234
+
235
+ self.preview_tabs = QComboBox()
236
+ self.preview_tabs.currentIndexChanged.connect(
237
+ self._update_preview)
238
+ main_layout.addWidget(self.preview_tabs)
239
+
240
+ self.preview_text = QTextEdit()
241
+ self.preview_text.setReadOnly(True)
242
+ self.preview_text.setFont(QFont("Consolas", 10))
243
+ main_layout.addWidget(self.preview_text)
244
+
245
+ # Progress bar
246
+ self.progress_bar = QProgressBar()
247
+ self.progress_bar.setVisible(False)
248
+ main_layout.addWidget(self.progress_bar)
249
+
250
+ #---- Data load ----
251
+ def _load_sample_data_if_needed(self):
252
+ """Load when sample data not set"""
253
+ if self.state_machine is None or \
254
+ self.global_defs is None:
255
+ sample_gen = SampleDataGenerator()
256
+ self.state_machine, self.global_defs = \
257
+ sample_gen.get_sample_data()
258
+ logger.info("Sample data loaded")
259
+
260
+ def _load_config_to_ui(self):
261
+ """Reflect settings to UI"""
262
+ config = self.config_manager.get_config()
263
+
264
+ if config.output_directory:
265
+ self.output_dir_edit.setText(
266
+ config.output_directory)
267
+ idx = self.style_combo.findData(
268
+ config.generation_style)
269
+ if idx >= 0:
270
+ self.style_combo.setCurrentIndex(idx)
271
+ self._update_info_labels(config)
272
+
273
+ def _update_info_labels(self, config):
274
+ """Update the settings info label"""
275
+ os_names = {
276
+ 'non_rtos': 'NonRTOS (bare metal)',
277
+ 'freertos': 'FreeRTOS',
278
+ 'threadx': 'ThreadX',
279
+ }
280
+ self.os_label.setText(
281
+ os_names.get(config.os_type, config.os_type))
282
+ self.merge_label.setText(
283
+ "Enabled" if config.save_with_merge else "Disabled")
284
+
285
+ #---- Event handler ----
286
+ def _on_output_dir_changed(self, text):
287
+ """Handler when output dir changes"""
288
+ self.config_manager.update(
289
+ output_directory=text if text else "")
290
+
291
+ def _select_output_dir(self):
292
+ """Select output directory"""
293
+ current = self.output_dir_edit.text() or os.getcwd()
294
+ dir_path = QFileDialog.getExistingDirectory(
295
+ self, "Select output directory", current)
296
+ if dir_path:
297
+ self.output_dir_edit.setText(dir_path)
298
+ self.config_manager.update(
299
+ output_directory=dir_path)
300
+ self._save_settings()
301
+
302
+ def _on_style_changed(self, index):
303
+ """Handler when style changes"""
304
+ style = self.style_combo.currentData()
305
+ self.config_manager.update(
306
+ generation_style=style)
307
+ self._save_settings()
308
+
309
+ def _open_settings_dialog(self):
310
+ """Open advanced settings dialog"""
311
+ dialog = CodeGenerationSettingsDialog(
312
+ config_manager=self.config_manager,
313
+ parent=self)
314
+ if dialog.exec() == QDialog.DialogCode.Accepted:
315
+ config = dialog.get_config()
316
+ self.config_manager.set_config(config)
317
+ self._load_config_to_ui()
318
+ self._save_settings()
319
+
320
+ # ---- Warning display ----
321
+ def _show_warnings(self, records):
322
+ if not records:
323
+ return
324
+ # Deduplication (order-preserving)
325
+ seen = set()
326
+ unique = []
327
+ for r in records:
328
+ if r not in seen:
329
+ seen.add(r)
330
+ unique.append(r)
331
+ QMessageBox.warning(
332
+ self,
333
+ self.tr("Warnings during generation"),
334
+ "The following warnings occurred:\n\n"
335
+ + "\n".join(f"- {m}" for m in unique),
336
+ )
337
+
338
+ # ---- ★ Code generation ----
339
+ def _generate_code(self):
340
+ """Execute code generation"""
341
+ if self.state_machine is None or \
342
+ self.global_defs is None:
343
+ QMessageBox.warning(
344
+ self, self.tr("Warning"),
345
+ self.tr("State machine and global definitions"
346
+ "is not set."))
347
+ return
348
+
349
+ output_dir = self.output_dir_edit.text().strip()
350
+ if output_dir:
351
+ self.config_manager.update(
352
+ output_directory=output_dir)
353
+
354
+ if not output_dir:
355
+ QMessageBox.warning(
356
+ self, self.tr("Warning"),
357
+ self.tr("Please set the output directory."))
358
+ self._select_output_dir()
359
+ output_dir = self.output_dir_edit.text().strip()
360
+ if not output_dir:
361
+ return
362
+
363
+ config = self.config_manager.get_config()
364
+
365
+ self.generate_btn.setEnabled(False)
366
+ self.progress_bar.setVisible(True)
367
+ self.progress_bar.setRange(0, 0)
368
+
369
+ # Attach warning collection handler
370
+ collector = WarningCollector()
371
+ root_logger = logging.getLogger()
372
+ root_logger.addHandler(collector)
373
+ try:
374
+ generator = CCodeGenerator(config=config)
375
+ # Automatic multi-layer / single detection
376
+ if self.all_layers and len(self.all_layers) > 0:
377
+ logger.info(
378
+ f"Multi-layer generation: "
379
+ f"{len(self.all_layers)} layers"
380
+ )
381
+ self.generated_files = (
382
+ generator.generate_all_layers(
383
+ self.all_layers,
384
+ self.global_defs,
385
+ role_function_library=(
386
+ self.role_function_library),
387
+ )
388
+ )
389
+ else:
390
+ logger.info("Single-layer generation")
391
+ self.generated_files = generator.generate_all(
392
+ self.state_machine,
393
+ self.global_defs,
394
+ role_function_library=(
395
+ self.role_function_library),
396
+ )
397
+ except Exception as e:
398
+ self.generate_btn.setEnabled(True)
399
+ self.progress_bar.setVisible(False)
400
+ QMessageBox.critical(
401
+ self, self.tr("Error"),
402
+ f"Code generation failed:\n{e}")
403
+ return
404
+ finally:
405
+ root_logger.removeHandler(collector)
406
+
407
+ # Preview apply
408
+ self.preview_tabs.clear()
409
+ for filename in self.generated_files.keys():
410
+ self.preview_tabs.addItem(filename)
411
+ self._update_preview()
412
+
413
+ self.generate_btn.setEnabled(True)
414
+ self.save_btn.setEnabled(True)
415
+ self.progress_bar.setVisible(False)
416
+ self._save_settings()
417
+
418
+ # CompletionMessage
419
+ QMessageBox.information(
420
+ self, self.tr("Completion"),
421
+ f"{len(self.generated_files)} files "
422
+ f"generated.\nOutput: {output_dir}")
423
+
424
+ # Show warnings last if any
425
+ self._show_warnings(collector.records)
426
+
427
+ def _update_preview(self):
428
+ """Update preview"""
429
+ filename = self.preview_tabs.currentText()
430
+ if filename in self.generated_files:
431
+ self.preview_text.setPlainText(
432
+ self.generated_files[filename])
433
+ else:
434
+ self.preview_text.clear()
435
+
436
+ def _save_code(self):
437
+ """Save generated code"""
438
+ if not self.generated_files:
439
+ QMessageBox.warning(
440
+ self, self.tr("Warning"),
441
+ self.tr("No code was generated."))
442
+ return
443
+
444
+ output_dir = self.output_dir_edit.text().strip()
445
+ if not output_dir:
446
+ QMessageBox.warning(
447
+ self, self.tr("Warning"),
448
+ self.tr("Please set the output directory."))
449
+ return
450
+
451
+ try:
452
+ os.makedirs(output_dir, exist_ok=True)
453
+ config = self.config_manager.get_config()
454
+ generator = CCodeGenerator(config=config)
455
+ if config.save_with_merge:
456
+ saved_files = \
457
+ generator.save_generated_code_with_merge(
458
+ self.generated_files, output_dir)
459
+ else:
460
+ saved_files = \
461
+ generator.save_generated_code(
462
+ self.generated_files, output_dir)
463
+ self._save_settings()
464
+ QMessageBox.information(
465
+ self, self.tr("Save complete"),
466
+ f"{len(saved_files)} files "
467
+ f"saved.\n\n"
468
+ f"Output: {output_dir}")
469
+ except Exception as e:
470
+ QMessageBox.critical(
471
+ self, self.tr("Error"),
472
+ f"Save failed:\n{e}")
473
+
474
+ def _on_close(self):
475
+ """Handler on close"""
476
+ self._save_settings()
477
+ self.reject()
478
+
479
+ # ---- getter ----
480
+ def get_generated_files(self) -> Dict[str, str]:
481
+ """Get generated files"""
482
+ return self.generated_files
483
+
484
+ def get_config(self):
485
+ """Get current settings"""
486
+ return self.config_manager.get_config()