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,500 @@
1
+ # statable_gui/condition_builder_dialog.py
2
+ """Transition condition builder dialog (literal support / R-8 condition template support)"""
3
+
4
+ import re
5
+ import sys
6
+ import os
7
+
8
+ current_dir = os.path.dirname(os.path.abspath(__file__))
9
+ parent_dir = os.path.dirname(current_dir)
10
+ if parent_dir not in sys.path:
11
+ sys.path.insert(0, parent_dir)
12
+
13
+ from PySide6.QtWidgets import (
14
+ QDialog, QVBoxLayout, QHBoxLayout, QPlainTextEdit, QTreeWidget,
15
+ QTreeWidgetItem, QLabel, QLineEdit, QPushButton, QDialogButtonBox,
16
+ QSplitter, QGroupBox, QFrame, QSizePolicy, QMessageBox, QTableWidget,
17
+ QTableWidgetItem, QHeaderView, QComboBox
18
+ )
19
+ from PySide6.QtCore import Qt
20
+ from PySide6.QtGui import QFontMetrics
21
+
22
+ from statable.global_defs import GlobalDefinitions
23
+ from statable.state_machine import StateMachine
24
+
25
+ from statable_gui.libcntrl.literal_library import LiteralLibrary, LiteralDefinition
26
+ from statable_gui.libcntrl.condition_library import ConditionLibrary
27
+
28
+ try:
29
+ from .condition_template_dialog import NewConditionTemplateDialog
30
+ except ImportError:
31
+ from condition_template_dialog import NewConditionTemplateDialog
32
+
33
+
34
+ class ConditionBuilderDialog(QDialog):
35
+ """Dialog to build transition condition via GUI.
36
+
37
+ [R-8]
38
+ - Optional `condition_library` argument.
39
+ - Symbol tree gains a "Condition templates" category that lists
40
+ all templates from the library. Double-clicking one inserts
41
+ its condition text into the expression editor.
42
+ - "+ New Template" button (left pane) opens
43
+ NewConditionTemplateDialog, adds the template to the library,
44
+ and refreshes the symbol tree.
45
+ """
46
+
47
+ def __init__(self, condition: str = "", event_name: str = "",
48
+ global_defs: GlobalDefinitions = None,
49
+ state_machine: StateMachine = None,
50
+ literal_library: LiteralLibrary = None,
51
+ states: list = None,
52
+ target_state: str = "",
53
+ else_target_state: str = "",
54
+ condition_library: ConditionLibrary = None,
55
+ parent=None):
56
+ super().__init__(parent)
57
+ self.setWindowTitle(self.tr("Transition condition builder"))
58
+ self.setMinimumSize(1000, 700)
59
+
60
+ self.global_defs = global_defs if global_defs else GlobalDefinitions()
61
+ self.state_machine = state_machine if state_machine else StateMachine()
62
+ self.literal_library = literal_library if literal_library else LiteralLibrary()
63
+ # [R-8] optional condition template library
64
+ self.condition_library = condition_library
65
+ self.event_name = event_name
66
+
67
+ self.states = states if states is not None else self._get_states_from_state_machine()
68
+ self.target_state = target_state
69
+ self.else_target_state = else_target_state
70
+
71
+ self._setup_ui()
72
+ self.set_current_targets()
73
+ self.condition_edit.setPlainText(condition)
74
+ self.event_name_edit.setText(event_name)
75
+ self._populate_tree()
76
+ self._update_c_code_view()
77
+
78
+ def _get_states_from_state_machine(self):
79
+ if self.state_machine:
80
+ return [s.name for s in self.state_machine.states.values()]
81
+ return []
82
+
83
+ def _setup_ui(self):
84
+ main_layout = QVBoxLayout(self)
85
+ main_layout.setContentsMargins(4, 4, 4, 4)
86
+ main_layout.setSpacing(4)
87
+
88
+ event_layout = QHBoxLayout()
89
+ event_layout.addWidget(QLabel(self.tr("Event name:")))
90
+ self.event_name_edit = QLineEdit()
91
+ event_layout.addWidget(self.event_name_edit)
92
+ main_layout.addLayout(event_layout)
93
+
94
+ target_layout = QHBoxLayout()
95
+ target_layout.addWidget(QLabel(self.tr("Target:")))
96
+ self.target_combo = QComboBox()
97
+ self.target_combo.addItem(self.tr(""))
98
+ self.target_combo.addItems(self.states)
99
+ target_layout.addWidget(self.target_combo)
100
+ main_layout.addLayout(target_layout)
101
+
102
+ else_target_layout = QHBoxLayout()
103
+ else_target_layout.addWidget(QLabel(self.tr("else target:")))
104
+ self.else_target_combo = QComboBox()
105
+ self.else_target_combo.addItem(self.tr(""))
106
+ self.else_target_combo.addItems(self.states)
107
+ else_target_layout.addWidget(self.else_target_combo)
108
+ main_layout.addLayout(else_target_layout)
109
+
110
+ main_splitter = QSplitter(Qt.Horizontal)
111
+
112
+ # Left pane
113
+ left_widget = QGroupBox(self.tr("Symbol to insert"))
114
+ left_layout = QVBoxLayout(left_widget)
115
+ left_layout.setContentsMargins(4, 4, 4, 4)
116
+ left_layout.setSpacing(2)
117
+
118
+ self.symbol_tree = QTreeWidget()
119
+ self.symbol_tree.setHeaderHidden(True)
120
+ self.symbol_tree.itemDoubleClicked.connect(self._insert_symbol)
121
+ left_layout.addWidget(self.symbol_tree)
122
+
123
+ num_layout = QHBoxLayout()
124
+ self.num_input = QLineEdit()
125
+ self.num_input.setPlaceholderText(self.tr("Numeric literal"))
126
+ num_insert_btn = QPushButton(self.tr("Insert"))
127
+ num_insert_btn.clicked.connect(self._insert_number)
128
+ num_layout.addWidget(self.num_input)
129
+ num_layout.addWidget(num_insert_btn)
130
+ left_layout.addLayout(num_layout)
131
+
132
+ # [R-8] "+ New Template" button (only when library available)
133
+ if self.condition_library is not None:
134
+ self.new_template_btn = QPushButton(self.tr("+ New Template"))
135
+ self.new_template_btn.setToolTip(
136
+ self.tr("Create a new condition template and add it to the tree"))
137
+ self.new_template_btn.clicked.connect(self._on_new_template)
138
+ left_layout.addWidget(self.new_template_btn)
139
+
140
+ main_splitter.addWidget(left_widget)
141
+
142
+ # Right pane
143
+ right_widget = QGroupBox(self.tr("Condition expression (symbol names)"))
144
+ right_layout = QVBoxLayout(right_widget)
145
+ right_layout.setContentsMargins(4, 4, 4, 4)
146
+ right_layout.setSpacing(2)
147
+
148
+ literal_btn = QPushButton(self.tr("Literalize"))
149
+ literal_btn.clicked.connect(self._open_literalization)
150
+ right_layout.addWidget(literal_btn, alignment=Qt.AlignLeft)
151
+
152
+ self.condition_edit = QPlainTextEdit()
153
+ self.condition_edit.setPlaceholderText(self.tr("Example: battery_voltage > 3000"))
154
+ self.condition_edit.setFrameStyle(QFrame.NoFrame)
155
+ self.condition_edit.setStyleSheet(
156
+ "QPlainTextEdit { padding: 0px; color: black; background: white; }")
157
+ self.condition_edit.document().setDocumentMargin(0)
158
+ self.condition_edit.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
159
+
160
+ fm = QFontMetrics(self.condition_edit.font())
161
+ row_height = fm.height()
162
+ self.condition_edit.setMinimumHeight(row_height * 10 + 4)
163
+ self.condition_edit.textChanged.connect(self._update_c_code_view)
164
+ right_layout.addWidget(self.condition_edit, 1)
165
+
166
+ clear_btn = QPushButton(self.tr("Clear"))
167
+ clear_btn.clicked.connect(self._clear_condition)
168
+ btn_layout = QHBoxLayout()
169
+ btn_layout.addStretch()
170
+ btn_layout.addWidget(clear_btn)
171
+ right_layout.addLayout(btn_layout)
172
+
173
+ main_splitter.addWidget(right_widget)
174
+ main_splitter.setSizes([300, 700])
175
+ main_layout.addWidget(main_splitter)
176
+
177
+ bottom_widget = QGroupBox(self.tr("Generated C code (ctx-> form)"))
178
+ bottom_layout = QVBoxLayout(bottom_widget)
179
+ bottom_layout.setContentsMargins(4, 4, 4, 4)
180
+ bottom_layout.setSpacing(2)
181
+
182
+ self.c_code_view = QPlainTextEdit()
183
+ self.c_code_view.setReadOnly(True)
184
+ self.c_code_view.setFrameStyle(QFrame.NoFrame)
185
+ self.c_code_view.setStyleSheet(
186
+ "QPlainTextEdit { padding: 0px; color: black; background: #f5f5f5; }")
187
+ self.c_code_view.document().setDocumentMargin(0)
188
+ self.c_code_view.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
189
+ self.c_code_view.setFixedHeight(row_height * 2 + 4)
190
+ bottom_layout.addWidget(self.c_code_view, 1)
191
+ main_layout.addWidget(bottom_widget)
192
+
193
+ buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
194
+ buttons.accepted.connect(self.accept)
195
+ buttons.rejected.connect(self.reject)
196
+ main_layout.addWidget(buttons)
197
+
198
+ def set_current_targets(self):
199
+ if self.target_state in self.states:
200
+ idx = self.target_combo.findText(self.target_state)
201
+ if idx >= 0:
202
+ self.target_combo.setCurrentIndex(idx)
203
+ if self.else_target_state in self.states:
204
+ idx = self.else_target_combo.findText(self.else_target_state)
205
+ if idx >= 0:
206
+ self.else_target_combo.setCurrentIndex(idx)
207
+
208
+ def _populate_tree(self):
209
+ self.symbol_tree.clear()
210
+
211
+ global_vars_item = QTreeWidgetItem(["Global variables"])
212
+ for var in getattr(self.global_defs, 'variables', []):
213
+ child = QTreeWidgetItem([var.name])
214
+ child.setData(0, Qt.UserRole, var.name)
215
+ child.setToolTip(0, getattr(var, 'description', ''))
216
+ global_vars_item.addChild(child)
217
+ self.symbol_tree.addTopLevelItem(global_vars_item)
218
+
219
+ flags_item = QTreeWidgetItem(["Event flags"])
220
+ for flag in getattr(self.global_defs, 'flags', []):
221
+ child = QTreeWidgetItem([flag.name])
222
+ child.setData(0, Qt.UserRole, flag.name)
223
+ child.setToolTip(0, getattr(flag, 'description', ''))
224
+ flags_item.addChild(child)
225
+ self.symbol_tree.addTopLevelItem(flags_item)
226
+
227
+ event_vars_item = QTreeWidgetItem(["Event variable"])
228
+ for event in self.state_machine.events.values():
229
+ data_name = getattr(event, 'data_name', '')
230
+ if data_name:
231
+ symbol = f"event.{data_name}"
232
+ child = QTreeWidgetItem([symbol])
233
+ child.setData(0, Qt.UserRole, symbol)
234
+ child.setToolTip(0, getattr(event, 'description', ''))
235
+ event_vars_item.addChild(child)
236
+ self.symbol_tree.addTopLevelItem(event_vars_item)
237
+
238
+ role_funcs_item = QTreeWidgetItem(["Role function (bool)"])
239
+ for rf in self.state_machine.role_functions.values():
240
+ if getattr(rf, 'return_type', '') == 'bool':
241
+ symbol = f"RoleFunc_{rf.name}(...)"
242
+ child = QTreeWidgetItem([symbol])
243
+ child.setData(0, Qt.UserRole, symbol)
244
+ role_funcs_item.addChild(child)
245
+ self.symbol_tree.addTopLevelItem(role_funcs_item)
246
+
247
+ literal_item = QTreeWidgetItem(["Literal"])
248
+ for lit in self.literal_library.list_all():
249
+ child = QTreeWidgetItem([lit.name])
250
+ child.setData(0, Qt.UserRole, lit.name)
251
+ child.setToolTip(0, f"{lit.name} = {lit.value} ({lit.literal_type})")
252
+ literal_item.addChild(child)
253
+ self.symbol_tree.addTopLevelItem(literal_item)
254
+
255
+ # [R-8] Condition templates
256
+ if self.condition_library is not None:
257
+ tmpl_item = QTreeWidgetItem(["Condition templates"])
258
+ try:
259
+ for tmpl in self.condition_library.list_all():
260
+ name = getattr(tmpl, 'name', '') or ''
261
+ cond = getattr(tmpl, 'condition', '') or ''
262
+ if not name and not cond:
263
+ continue
264
+ # Display name; insert condition text
265
+ display = name if name else cond
266
+ child = QTreeWidgetItem([display])
267
+ child.setData(0, Qt.UserRole, cond)
268
+ child.setToolTip(0, f"{name}: {cond}")
269
+ tmpl_item.addChild(child)
270
+ except Exception:
271
+ pass
272
+ self.symbol_tree.addTopLevelItem(tmpl_item)
273
+
274
+ const_item = QTreeWidgetItem(["Constant symbol"])
275
+ true_child = QTreeWidgetItem(["true"])
276
+ true_child.setData(0, Qt.UserRole, "true")
277
+ false_child = QTreeWidgetItem(["false"])
278
+ false_child.setData(0, Qt.UserRole, "false")
279
+ const_item.addChild(true_child)
280
+ const_item.addChild(false_child)
281
+ self.symbol_tree.addTopLevelItem(const_item)
282
+
283
+ self.symbol_tree.expandAll()
284
+
285
+ def _insert_symbol(self, item, column):
286
+ symbol = item.data(0, Qt.UserRole)
287
+ if symbol:
288
+ self._insert_text(symbol)
289
+
290
+ def _insert_text(self, text: str):
291
+ cursor = self.condition_edit.textCursor()
292
+ cursor.insertText(text)
293
+ self.condition_edit.setTextCursor(cursor)
294
+ self.condition_edit.setFocus()
295
+ self._update_c_code_view()
296
+
297
+ def _insert_number(self):
298
+ num = self.num_input.text().strip()
299
+ if num:
300
+ self._insert_text(num)
301
+
302
+ def _clear_condition(self):
303
+ self.condition_edit.clear()
304
+ self._update_c_code_view()
305
+
306
+ # ------------------------------------------------------------------
307
+ # [R-8] New template handler
308
+ # ------------------------------------------------------------------
309
+ def _on_new_template(self):
310
+ if self.condition_library is None:
311
+ return
312
+
313
+ dlg = NewConditionTemplateDialog(self)
314
+ if dlg.exec() != QDialog.Accepted:
315
+ return
316
+
317
+ tmpl = dlg.get_template()
318
+ try:
319
+ self.condition_library.add(tmpl)
320
+ except ValueError:
321
+ QMessageBox.warning(
322
+ self, self.tr("Warning"),
323
+ f"A condition template named '{tmpl.name}' already exists.")
324
+ return
325
+
326
+ # Refresh tree to include the new template
327
+ self._populate_tree()
328
+
329
+ def _update_c_code_view(self):
330
+ raw_text = self.condition_edit.toPlainText()
331
+ c_code = self._convert_to_c_code(raw_text)
332
+ self.c_code_view.setPlainText(c_code)
333
+
334
+ def _convert_to_c_code(self, text: str) -> str:
335
+ replace_map = {}
336
+ for var in getattr(self.global_defs, 'variables', []):
337
+ replace_map[var.name] = f"ctx->data.{var.name}"
338
+ for flag in getattr(self.global_defs, 'flags', []):
339
+ replace_map[flag.name] = f"ctx->flags.{flag.name}"
340
+ result = text
341
+ for symbol in sorted(replace_map.keys(), key=len, reverse=True):
342
+ result = result.replace(symbol, replace_map[symbol])
343
+ result = self._add_parentheses_to_comparisons(result)
344
+ return result
345
+
346
+ def _add_parentheses_to_comparisons(self, text: str) -> str:
347
+ text = text.replace('\n', ' ')
348
+ parts = []
349
+ depth = 0
350
+ start = 0
351
+ i = 0
352
+ while i < len(text):
353
+ ch = text[i]
354
+ if ch == '(':
355
+ depth += 1
356
+ elif ch == ')':
357
+ depth = max(0, depth - 1)
358
+ elif depth == 0 and i + 1 < len(text) and text[i:i+2] in ('&&', '||'):
359
+ part = text[start:i].strip()
360
+ if part:
361
+ parts.append(part)
362
+ parts.append(text[i:i+2])
363
+ i += 2
364
+ start = i
365
+ continue
366
+ i += 1
367
+ last_part = text[start:].strip()
368
+ if last_part:
369
+ parts.append(last_part)
370
+
371
+ result_parts = []
372
+ for part in parts:
373
+ if part in ('&&', '||'):
374
+ result_parts.append(f" {part} ")
375
+ else:
376
+ if re.search(r'==|!=|>=|<=|>|<', part):
377
+ if not self._is_fully_parenthesized(part):
378
+ part = f"({part})"
379
+ result_parts.append(part)
380
+ return ''.join(result_parts).strip()
381
+
382
+ def _is_fully_parenthesized(self, expr: str) -> bool:
383
+ expr = expr.strip()
384
+ if not (expr.startswith('(') and expr.endswith(')')):
385
+ return False
386
+ depth = 0
387
+ for i, ch in enumerate(expr):
388
+ if ch == '(':
389
+ depth += 1
390
+ elif ch == ')':
391
+ depth -= 1
392
+ if depth == 0:
393
+ return i == len(expr) - 1
394
+ if depth < 0:
395
+ return False
396
+ return False
397
+
398
+ def _open_literalization(self):
399
+ text = self.condition_edit.toPlainText()
400
+ if not text.strip():
401
+ QMessageBox.information(self, self.tr("Info"), self.tr("The condition expression is empty."))
402
+ return
403
+
404
+ dialog = LiteralizationDialog(text, self.literal_library, self)
405
+ if dialog.exec() == QDialog.Accepted:
406
+ new_text = dialog.get_updated_condition_text()
407
+ self.condition_edit.setPlainText(new_text)
408
+ self._populate_tree()
409
+ self._update_c_code_view()
410
+
411
+ def get_condition_text(self) -> str:
412
+ return self.condition_edit.toPlainText().strip()
413
+
414
+ def get_event_name(self) -> str:
415
+ return self.event_name_edit.text().strip()
416
+
417
+ def get_target_state(self) -> str:
418
+ return self.target_combo.currentText().strip()
419
+
420
+ def get_else_target_state(self) -> str:
421
+ return self.else_target_combo.currentText().strip()
422
+
423
+ def get_c_code_text(self) -> str:
424
+ return self.c_code_view.toPlainText().strip()
425
+
426
+
427
+ class LiteralizationDialog(QDialog):
428
+ """Dialog to literalize numeric values in condition expressions"""
429
+
430
+ def __init__(self, condition_text: str, literal_library: LiteralLibrary, parent=None):
431
+ super().__init__(parent)
432
+ self.condition_text = condition_text
433
+ self.literal_library = literal_library
434
+ self.updated_condition_text = condition_text
435
+
436
+ self.setWindowTitle(self.tr("Literalize"))
437
+ self.setMinimumSize(600, 400)
438
+
439
+ self._setup_ui()
440
+ self._scan_numbers()
441
+
442
+ def _setup_ui(self):
443
+ layout = QVBoxLayout(self)
444
+ layout.addWidget(QLabel(
445
+ self.tr("Literalize numeric values in the condition expression. "
446
+ "Give each value a name.")))
447
+
448
+ self.table = QTableWidget(0, 3)
449
+ self.table.setHorizontalHeaderLabels(["Row", "Numeric", "Literal name"])
450
+ self.table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
451
+ layout.addWidget(self.table)
452
+
453
+ buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
454
+ buttons.accepted.connect(self._on_accept)
455
+ buttons.rejected.connect(self.reject)
456
+ layout.addWidget(buttons)
457
+
458
+ def _scan_numbers(self):
459
+ lines = self.condition_text.splitlines()
460
+ for line_no, line in enumerate(lines):
461
+ for match in re.finditer(r'\b\d+(\.\d+)?\b', line):
462
+ value = match.group()
463
+ self.table.insertRow(self.table.rowCount())
464
+ row = self.table.rowCount() - 1
465
+ self.table.setItem(row, 0, QTableWidgetItem(str(line_no + 1)))
466
+ self.table.setItem(row, 1, QTableWidgetItem(value))
467
+ self.table.setItem(row, 2, QTableWidgetItem(f"LITERAL_{row + 1}"))
468
+
469
+ def _on_accept(self):
470
+ replace_map = {}
471
+ for row in range(self.table.rowCount()):
472
+ line_no = self.table.item(row, 0).text().strip()
473
+ value = self.table.item(row, 1).text().strip()
474
+ literal_name = self.table.item(row, 2).text().strip()
475
+
476
+ if not literal_name:
477
+ continue
478
+
479
+ try:
480
+ lit = LiteralDefinition(name=literal_name, value=value, literal_type="int")
481
+ self.literal_library.add(lit)
482
+ except ValueError:
483
+ pass
484
+
485
+ replace_map[(line_no, value)] = literal_name
486
+
487
+ lines = self.condition_text.splitlines()
488
+ updated_lines = []
489
+ for line_no, line in enumerate(lines):
490
+ updated_line = line
491
+ for (target_line, value), literal_name in replace_map.items():
492
+ if int(target_line) == line_no + 1:
493
+ updated_line = updated_line.replace(value, literal_name)
494
+ updated_lines.append(updated_line)
495
+
496
+ self.updated_condition_text = '\n'.join(updated_lines)
497
+ self.accept()
498
+
499
+ def get_updated_condition_text(self) -> str:
500
+ return self.updated_condition_text
@@ -0,0 +1,123 @@
1
+ from PySide6.QtCore import Qt
2
+ from PySide6.QtGui import QFont
3
+ from PySide6.QtWidgets import (
4
+ QDialog, QVBoxLayout, QHBoxLayout, QComboBox,
5
+ QPushButton, QLabel, QDialogButtonBox, QPlainTextEdit, QSplitter,
6
+ QWidget
7
+ )
8
+
9
+ from statable.global_defs import GlobalDefinitions
10
+ from .common_widgets import TitleEditWidget
11
+ from .symbol_picker import SymbolPickerWidget
12
+ from .logger import StaTableLogger
13
+
14
+
15
+ class ConditionEditDialog(QDialog):
16
+ """Dialog to edit the state transition condition"""
17
+
18
+ def __init__(self, parent=None, condition_text="", title="", global_defs=None, role_functions=None):
19
+ super().__init__(parent)
20
+ self.setWindowTitle(self.tr("State transition conditionEdit"))
21
+ self.setMinimumSize(900, 650)
22
+ self.global_defs = global_defs if global_defs else GlobalDefinitions()
23
+ self.role_functions = role_functions if role_functions is not None else {}
24
+
25
+ StaTableLogger.debug(
26
+ f"ConditionEditDialog.__init__: condition_text_len={len(condition_text)}, "
27
+ f"title='{title}', vars={len(self.global_defs.variables)}, flags={len(self.global_defs.flags)}"
28
+ )
29
+
30
+ main_layout = QVBoxLayout(self)
31
+
32
+ # Title input widget
33
+ self.title_widget = TitleEditWidget(self, title=title)
34
+ main_layout.addWidget(self.title_widget)
35
+
36
+ # Role function select / insert bar
37
+ role_bar = QHBoxLayout()
38
+ role_bar.addWidget(QLabel(self.tr("Role function:")))
39
+ self.role_combo = QComboBox()
40
+ self.refresh_role_combo()
41
+ role_bar.addWidget(self.role_combo)
42
+ insert_role_btn = QPushButton(self.tr("Insert"))
43
+ insert_role_btn.clicked.connect(self.insert_role_function)
44
+ role_bar.addWidget(insert_role_btn)
45
+ main_layout.addLayout(role_bar)
46
+
47
+ # Left/right split
48
+ splitter = QSplitter(Qt.Horizontal)
49
+ main_layout.addWidget(splitter)
50
+
51
+ # Left: symbol picker
52
+ self.symbol_picker = SymbolPickerWidget(
53
+ global_defs=self.global_defs,
54
+ role_functions=self.role_functions
55
+ )
56
+ self.symbol_picker.insert_requested.connect(self.insert_symbol)
57
+ splitter.addWidget(self.symbol_picker)
58
+
59
+ # Right pane: condition expression edit
60
+ right_widget = QWidget()
61
+ right_layout = QVBoxLayout(right_widget)
62
+ right_layout.addWidget(QLabel(self.tr("Condition:")))
63
+ self.condition_edit = QPlainTextEdit()
64
+ self.condition_edit.setPlainText(condition_text)
65
+ self.condition_edit.setFont(QFont("Consolas", 10))
66
+ right_layout.addWidget(self.condition_edit, stretch=1)
67
+ splitter.addWidget(right_widget)
68
+
69
+ # Logical operator combo
70
+ op_layout = QHBoxLayout()
71
+ op_layout.addWidget(QLabel(self.tr("Logical operator:")))
72
+ self.op_combo = QComboBox()
73
+ self.op_combo.addItems(["AND", "OR", "XOR", "NAND", "NOR"])
74
+ op_layout.addWidget(self.op_combo)
75
+ op_layout.addStretch()
76
+ main_layout.addLayout(op_layout)
77
+
78
+ # OK/Cancel
79
+ buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
80
+ buttons.accepted.connect(self._on_accept)
81
+ buttons.rejected.connect(self.reject)
82
+ main_layout.addWidget(buttons)
83
+
84
+ StaTableLogger.debug("ConditionEditDialog.__init__ completed")
85
+
86
+ def refresh_role_combo(self):
87
+ self.role_combo.clear()
88
+ for name in self.role_functions.keys():
89
+ self.role_combo.addItem(name)
90
+ StaTableLogger.debug(f"refresh_role_combo: {len(self.role_functions)} roles")
91
+
92
+ def insert_role_function(self):
93
+ func_name = self.role_combo.currentText()
94
+ StaTableLogger.debug(f"insert_role_function: '{func_name}'")
95
+ if not func_name:
96
+ return
97
+ self.condition_edit.insertPlainText(f"{func_name}()")
98
+
99
+ def insert_symbol(self, text: str):
100
+ StaTableLogger.debug(f"ConditionEditDialog.insert_symbol: '{text}'")
101
+ self.condition_edit.insertPlainText(text)
102
+
103
+ def _on_accept(self):
104
+ """OK button: auto-set provisional title if title is empty"""
105
+ condition_text = self.condition_edit.toPlainText().strip()
106
+ if condition_text:
107
+ # Use first 20 chars of condition as provisional title
108
+ first_line = condition_text.split('\n')[0].strip()
109
+ auto_title = first_line[:20] + ("..." if len(first_line) > 20 else "")
110
+ else:
111
+ auto_title = "(untitled condition)"
112
+ self.title_widget.ensure_title(auto_title)
113
+ self.accept()
114
+
115
+ def get_condition_text(self) -> str:
116
+ text = self.condition_edit.toPlainText().strip()
117
+ StaTableLogger.debug(f"get_condition_text: length={len(text)}")
118
+ return text
119
+
120
+ def get_title(self) -> str:
121
+ title = self.title_widget.get_title()
122
+ StaTableLogger.debug(f"get_title: '{title}'")
123
+ return title
@@ -0,0 +1,58 @@
1
+ # statable_gui/condition_template_dialog.py
2
+ """New condition template dialog (R-8 support).
3
+
4
+ Simple dialog for creating a ConditionTemplate, used by
5
+ ConditionBuilderDialog's "+ New Template" button.
6
+ """
7
+
8
+ from PySide6.QtWidgets import (
9
+ QDialog, QVBoxLayout, QFormLayout, QLineEdit,
10
+ QDialogButtonBox, QMessageBox,
11
+ )
12
+
13
+ from statable_gui.libcntrl.condition_library import ConditionTemplate
14
+
15
+
16
+ class NewConditionTemplateDialog(QDialog):
17
+ """Dialog to create a new ConditionTemplate."""
18
+
19
+ def __init__(self, parent=None):
20
+ super().__init__(parent)
21
+ self.setWindowTitle(self.tr("New Condition Template"))
22
+ self.setMinimumWidth(500)
23
+
24
+ layout = QVBoxLayout(self)
25
+ form = QFormLayout()
26
+
27
+ self.name_edit = QLineEdit()
28
+ self.condition_edit = QLineEdit()
29
+ self.condition_edit.setPlaceholderText(
30
+ self.tr("Example: err_code != 0"))
31
+ self.desc_edit = QLineEdit()
32
+
33
+ form.addRow("Name:", self.name_edit)
34
+ form.addRow("Condition:", self.condition_edit)
35
+ form.addRow("Description:", self.desc_edit)
36
+ layout.addLayout(form)
37
+
38
+ buttons = QDialogButtonBox(
39
+ QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
40
+ buttons.accepted.connect(self._on_accept)
41
+ buttons.rejected.connect(self.reject)
42
+ layout.addWidget(buttons)
43
+
44
+ def _on_accept(self):
45
+ if not self.name_edit.text().strip():
46
+ QMessageBox.warning(self, self.tr("Warning"), self.tr("Name is required."))
47
+ return
48
+ if not self.condition_edit.text().strip():
49
+ QMessageBox.warning(self, self.tr("Warning"), self.tr("Condition is required."))
50
+ return
51
+ self.accept()
52
+
53
+ def get_template(self) -> ConditionTemplate:
54
+ return ConditionTemplate(
55
+ name=self.name_edit.text().strip(),
56
+ condition=self.condition_edit.text().strip(),
57
+ description=self.desc_edit.text().strip(),
58
+ )