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,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
|
+
)
|