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,160 @@
|
|
|
1
|
+
# statable_gui/transition_editor_direct/edit_dialogs.py
|
|
2
|
+
"""\nNode edit dialog (else checkbox support, condition builder support)\n"""
|
|
3
|
+
|
|
4
|
+
from PySide6.QtWidgets import (
|
|
5
|
+
QDialog, QVBoxLayout, QHBoxLayout, QPushButton,
|
|
6
|
+
QLabel, QComboBox, QLineEdit, QListWidget, QAbstractItemView, QCheckBox
|
|
7
|
+
)
|
|
8
|
+
from PySide6.QtCore import Qt
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class BaseEditDialog(QDialog):
|
|
12
|
+
def __init__(self, item, parent=None):
|
|
13
|
+
super().__init__(parent)
|
|
14
|
+
self.item = item
|
|
15
|
+
self.setMinimumWidth(400)
|
|
16
|
+
|
|
17
|
+
def _add_buttons(self, layout):
|
|
18
|
+
btn_layout = QHBoxLayout()
|
|
19
|
+
btn_layout.addStretch()
|
|
20
|
+
cancel_btn = QPushButton(self.tr("Cancel"))
|
|
21
|
+
cancel_btn.clicked.connect(self.reject)
|
|
22
|
+
btn_layout.addWidget(cancel_btn)
|
|
23
|
+
ok_btn = QPushButton(self.tr("OK"))
|
|
24
|
+
ok_btn.clicked.connect(self.accept)
|
|
25
|
+
btn_layout.addWidget(ok_btn)
|
|
26
|
+
layout.addLayout(btn_layout)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class FunctionEditDialog(BaseEditDialog):
|
|
30
|
+
def __init__(self, item, role_functions=None, parent=None):
|
|
31
|
+
super().__init__(item, parent)
|
|
32
|
+
self.setWindowTitle(self.tr("Edit role function"))
|
|
33
|
+
self.role_functions = role_functions or []
|
|
34
|
+
|
|
35
|
+
layout = QVBoxLayout(self)
|
|
36
|
+
h1 = QHBoxLayout()
|
|
37
|
+
h1.addWidget(QLabel(self.tr("Function name:")))
|
|
38
|
+
self.func_combo = QComboBox()
|
|
39
|
+
self.func_combo.setEditable(True)
|
|
40
|
+
self.func_combo.addItems(self.role_functions)
|
|
41
|
+
self.func_combo.setCurrentText(item.name)
|
|
42
|
+
h1.addWidget(self.func_combo)
|
|
43
|
+
layout.addLayout(h1)
|
|
44
|
+
self._add_buttons(layout)
|
|
45
|
+
|
|
46
|
+
def get_result(self):
|
|
47
|
+
return self.func_combo.currentText(), {}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class TransitionEditDialog(BaseEditDialog):
|
|
51
|
+
def __init__(self, item, states=None, role_functions=None, global_defs=None, state_machine=None, parent=None):
|
|
52
|
+
super().__init__(item, parent)
|
|
53
|
+
self.setWindowTitle(self.tr("Edit state transition event"))
|
|
54
|
+
self.states = states or []
|
|
55
|
+
self.role_functions = role_functions or []
|
|
56
|
+
self.global_defs = global_defs
|
|
57
|
+
self.state_machine = state_machine
|
|
58
|
+
|
|
59
|
+
layout = QVBoxLayout(self)
|
|
60
|
+
|
|
61
|
+
h0 = QHBoxLayout()
|
|
62
|
+
h0.addWidget(QLabel(self.tr("Event:")))
|
|
63
|
+
self.event_label = QLabel(item.params.get('event', item.name))
|
|
64
|
+
h0.addWidget(self.event_label)
|
|
65
|
+
layout.addLayout(h0)
|
|
66
|
+
|
|
67
|
+
h1 = QHBoxLayout()
|
|
68
|
+
h1.addWidget(QLabel(self.tr("Condition:")))
|
|
69
|
+
self.cond_edit = QLineEdit(item.params.get('condition', ''))
|
|
70
|
+
h1.addWidget(self.cond_edit)
|
|
71
|
+
|
|
72
|
+
cond_builder_btn = QPushButton(self.tr("Edit condition..."))
|
|
73
|
+
cond_builder_btn.clicked.connect(self._open_condition_builder)
|
|
74
|
+
h1.addWidget(cond_builder_btn)
|
|
75
|
+
layout.addLayout(h1)
|
|
76
|
+
|
|
77
|
+
layout.addWidget(QLabel(self.tr("Pre-transition processing:")))
|
|
78
|
+
self.pre_list = QListWidget()
|
|
79
|
+
self.pre_list.setDragDropMode(QAbstractItemView.InternalMove)
|
|
80
|
+
self.pre_list.setDefaultDropAction(Qt.MoveAction)
|
|
81
|
+
for a in item.params.get('pre_actions', []):
|
|
82
|
+
self.pre_list.addItem(a)
|
|
83
|
+
layout.addWidget(self.pre_list)
|
|
84
|
+
|
|
85
|
+
pre_btn = QHBoxLayout()
|
|
86
|
+
add_pre_btn = QPushButton(self.tr("Add"))
|
|
87
|
+
add_pre_btn.clicked.connect(self._add_pre)
|
|
88
|
+
pre_btn.addWidget(add_pre_btn)
|
|
89
|
+
del_pre_btn = QPushButton(self.tr("Delete"))
|
|
90
|
+
del_pre_btn.clicked.connect(self._del_pre)
|
|
91
|
+
pre_btn.addWidget(del_pre_btn)
|
|
92
|
+
layout.addLayout(pre_btn)
|
|
93
|
+
|
|
94
|
+
self.has_else_check = QCheckBox(self.tr("Use else condition"))
|
|
95
|
+
self.has_else_check.setChecked(item.params.get('has_else', True))
|
|
96
|
+
layout.addWidget(self.has_else_check)
|
|
97
|
+
|
|
98
|
+
h3 = QHBoxLayout()
|
|
99
|
+
h3.addWidget(QLabel(self.tr("else target:")))
|
|
100
|
+
self.else_target_combo = QComboBox()
|
|
101
|
+
self.else_target_combo.setEditable(True)
|
|
102
|
+
self.else_target_combo.addItems(self.states)
|
|
103
|
+
self.else_target_combo.setCurrentText(item.params.get('else_target', ''))
|
|
104
|
+
h3.addWidget(self.else_target_combo)
|
|
105
|
+
layout.addLayout(h3)
|
|
106
|
+
|
|
107
|
+
h2 = QHBoxLayout()
|
|
108
|
+
h2.addWidget(QLabel(self.tr("Target:")))
|
|
109
|
+
self.target_combo = QComboBox()
|
|
110
|
+
self.target_combo.setEditable(True)
|
|
111
|
+
self.target_combo.addItems(self.states)
|
|
112
|
+
self.target_combo.setCurrentText(item.params.get('target', ''))
|
|
113
|
+
h2.addWidget(self.target_combo)
|
|
114
|
+
layout.addLayout(h2)
|
|
115
|
+
|
|
116
|
+
self._add_buttons(layout)
|
|
117
|
+
|
|
118
|
+
def _add_pre(self):
|
|
119
|
+
if self.role_functions:
|
|
120
|
+
self.pre_list.addItem(self.role_functions[0])
|
|
121
|
+
|
|
122
|
+
def _del_pre(self):
|
|
123
|
+
row = self.pre_list.currentRow()
|
|
124
|
+
if row >= 0:
|
|
125
|
+
self.pre_list.takeItem(row)
|
|
126
|
+
|
|
127
|
+
def _open_condition_builder(self):
|
|
128
|
+
from statable_gui.condition_builder_dialog import ConditionBuilderDialog
|
|
129
|
+
dlg = ConditionBuilderDialog(
|
|
130
|
+
condition=self.cond_edit.text(),
|
|
131
|
+
global_defs=self.global_defs,
|
|
132
|
+
state_machine=self.state_machine,
|
|
133
|
+
parent=self
|
|
134
|
+
)
|
|
135
|
+
if dlg.exec() == QDialog.Accepted:
|
|
136
|
+
self.cond_edit.setText(dlg.get_condition_text())
|
|
137
|
+
|
|
138
|
+
def get_result(self):
|
|
139
|
+
condition = self.cond_edit.text()
|
|
140
|
+
pre_actions = [self.pre_list.item(i).text() for i in range(self.pre_list.count())]
|
|
141
|
+
target = self.target_combo.currentText()
|
|
142
|
+
has_else = self.has_else_check.isChecked()
|
|
143
|
+
else_target = self.else_target_combo.currentText() if has_else else ""
|
|
144
|
+
|
|
145
|
+
edited = f"{self.event_label.text()}: {condition} → {target}"
|
|
146
|
+
if pre_actions:
|
|
147
|
+
edited += f" (pre: {', '.join(pre_actions)})"
|
|
148
|
+
if has_else:
|
|
149
|
+
edited += f" [else→{else_target}]" if else_target else " [else]"
|
|
150
|
+
|
|
151
|
+
params = {
|
|
152
|
+
'event': self.event_label.text(),
|
|
153
|
+
'condition': condition,
|
|
154
|
+
'pre_actions': pre_actions,
|
|
155
|
+
'target': target,
|
|
156
|
+
'has_else': has_else,
|
|
157
|
+
'else_target': else_target,
|
|
158
|
+
'else_actions': [], # Future support
|
|
159
|
+
}
|
|
160
|
+
return edited, params
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
# statable_gui/transition_editor_direct/overview_tab.py
|
|
2
|
+
"""Overview tab widget (v2.2).
|
|
3
|
+
|
|
4
|
+
Shows coverage analysis for the current cell and the whole state machine.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
from typing import Optional
|
|
9
|
+
|
|
10
|
+
from PySide6.QtCore import Qt
|
|
11
|
+
from PySide6.QtGui import QFont
|
|
12
|
+
from PySide6.QtWidgets import (
|
|
13
|
+
QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QPlainTextEdit,
|
|
14
|
+
QLabel, QSplitter,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
from .draft import ActionDraft
|
|
18
|
+
from .coverage_analyzer import CoverageAnalyzer
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger("transition_editor_direct.overview_tab")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class OverviewTab(QWidget):
|
|
24
|
+
"""Overview tab: coverage / reachability report."""
|
|
25
|
+
|
|
26
|
+
def __init__(self, draft: ActionDraft,
|
|
27
|
+
state_machine=None, parent=None):
|
|
28
|
+
super().__init__(parent)
|
|
29
|
+
self.draft = draft
|
|
30
|
+
self.state_machine = state_machine
|
|
31
|
+
self.analyzer = CoverageAnalyzer()
|
|
32
|
+
|
|
33
|
+
self._build_ui()
|
|
34
|
+
self.refresh()
|
|
35
|
+
|
|
36
|
+
def _build_ui(self):
|
|
37
|
+
layout = QVBoxLayout(self)
|
|
38
|
+
|
|
39
|
+
top = QHBoxLayout()
|
|
40
|
+
top.addWidget(QLabel(self.tr("Coverage / reachability report")))
|
|
41
|
+
top.addStretch()
|
|
42
|
+
refresh_btn = QPushButton(self.tr("Refresh"))
|
|
43
|
+
refresh_btn.clicked.connect(self.refresh)
|
|
44
|
+
top.addWidget(refresh_btn)
|
|
45
|
+
layout.addLayout(top)
|
|
46
|
+
|
|
47
|
+
splitter = QSplitter(Qt.Vertical)
|
|
48
|
+
|
|
49
|
+
# Cell-internal
|
|
50
|
+
cell_box = QWidget()
|
|
51
|
+
cell_layout = QVBoxLayout(cell_box)
|
|
52
|
+
cell_layout.setContentsMargins(0, 0, 0, 0)
|
|
53
|
+
cell_layout.addWidget(QLabel(self.tr("Cell-level analysis")))
|
|
54
|
+
self.cell_view = QPlainTextEdit()
|
|
55
|
+
self.cell_view.setReadOnly(True)
|
|
56
|
+
self.cell_view.setFont(QFont("Consolas", 10))
|
|
57
|
+
cell_layout.addWidget(self.cell_view)
|
|
58
|
+
splitter.addWidget(cell_box)
|
|
59
|
+
|
|
60
|
+
# State graph
|
|
61
|
+
graph_box = QWidget()
|
|
62
|
+
graph_layout = QVBoxLayout(graph_box)
|
|
63
|
+
graph_layout.setContentsMargins(0, 0, 0, 0)
|
|
64
|
+
graph_layout.addWidget(QLabel(self.tr("State graph analysis")))
|
|
65
|
+
self.graph_view = QPlainTextEdit()
|
|
66
|
+
self.graph_view.setReadOnly(True)
|
|
67
|
+
self.graph_view.setFont(QFont("Consolas", 10))
|
|
68
|
+
graph_layout.addWidget(self.graph_view)
|
|
69
|
+
splitter.addWidget(graph_box)
|
|
70
|
+
|
|
71
|
+
layout.addWidget(splitter)
|
|
72
|
+
|
|
73
|
+
# ------------------------------------------------------------------
|
|
74
|
+
# Public API
|
|
75
|
+
# ------------------------------------------------------------------
|
|
76
|
+
def refresh(self):
|
|
77
|
+
"""Recompute analysis and update views."""
|
|
78
|
+
self._refresh_cell_view()
|
|
79
|
+
self._refresh_graph_view()
|
|
80
|
+
|
|
81
|
+
def get_summary_text(self) -> str:
|
|
82
|
+
"""Return a plain-text summary (test-friendly)."""
|
|
83
|
+
return (self.cell_view.toPlainText() + "\n\n"
|
|
84
|
+
+ self.graph_view.toPlainText())
|
|
85
|
+
|
|
86
|
+
# ------------------------------------------------------------------
|
|
87
|
+
# Internals
|
|
88
|
+
# ------------------------------------------------------------------
|
|
89
|
+
def _refresh_cell_view(self):
|
|
90
|
+
sm = self.state_machine
|
|
91
|
+
source = self.draft.source
|
|
92
|
+
event = self.draft.event
|
|
93
|
+
|
|
94
|
+
lines = []
|
|
95
|
+
lines.append(f"Cell: {source} -[{event or 'Completion'}]->")
|
|
96
|
+
|
|
97
|
+
if sm is None:
|
|
98
|
+
lines.append("(state machine not available)")
|
|
99
|
+
self.cell_view.setPlainText("\n".join(lines))
|
|
100
|
+
return
|
|
101
|
+
|
|
102
|
+
report = self.analyzer.analyze_cell(sm, source, event)
|
|
103
|
+
lines.append(f"Transitions: {report.transitions_count}")
|
|
104
|
+
lines.append("")
|
|
105
|
+
|
|
106
|
+
if report.unreachable_labels:
|
|
107
|
+
lines.append("Unreachable transitions:")
|
|
108
|
+
for lbl in report.unreachable_labels:
|
|
109
|
+
lines.append(f" - {lbl}")
|
|
110
|
+
else:
|
|
111
|
+
lines.append("Unreachable transitions: none")
|
|
112
|
+
|
|
113
|
+
lines.append("")
|
|
114
|
+
|
|
115
|
+
if report.duplicate_targets:
|
|
116
|
+
lines.append("Duplicate targets:")
|
|
117
|
+
for tgt, labels in report.duplicate_targets.items():
|
|
118
|
+
lines.append(f" - {tgt}: {', '.join(labels)}")
|
|
119
|
+
else:
|
|
120
|
+
lines.append("Duplicate targets: none")
|
|
121
|
+
|
|
122
|
+
lines.append("")
|
|
123
|
+
|
|
124
|
+
if report.overlap_pairs:
|
|
125
|
+
lines.append("Possible condition overlaps:")
|
|
126
|
+
for a, b in report.overlap_pairs:
|
|
127
|
+
lines.append(f" - {a} and {b}")
|
|
128
|
+
else:
|
|
129
|
+
lines.append("Possible condition overlaps: none")
|
|
130
|
+
|
|
131
|
+
self.cell_view.setPlainText("\n".join(lines))
|
|
132
|
+
|
|
133
|
+
def _refresh_graph_view(self):
|
|
134
|
+
sm = self.state_machine
|
|
135
|
+
lines = []
|
|
136
|
+
|
|
137
|
+
if sm is None:
|
|
138
|
+
lines.append("(state machine not available)")
|
|
139
|
+
self.graph_view.setPlainText("\n".join(lines))
|
|
140
|
+
return
|
|
141
|
+
|
|
142
|
+
report = self.analyzer.analyze_state_graph(sm)
|
|
143
|
+
|
|
144
|
+
lines.append(f"States: {len(sm.states)}")
|
|
145
|
+
lines.append(f"Initial: {getattr(sm, 'initial_state', None) or '(unset)'}")
|
|
146
|
+
lines.append("")
|
|
147
|
+
|
|
148
|
+
if report.unreachable_states:
|
|
149
|
+
lines.append("Unreachable states:")
|
|
150
|
+
for s in report.unreachable_states:
|
|
151
|
+
lines.append(f" - {s}")
|
|
152
|
+
else:
|
|
153
|
+
lines.append("Unreachable states: none")
|
|
154
|
+
|
|
155
|
+
lines.append("")
|
|
156
|
+
|
|
157
|
+
if report.terminal_states:
|
|
158
|
+
lines.append("Terminal states (no outgoing):")
|
|
159
|
+
for s in report.terminal_states:
|
|
160
|
+
lines.append(f" - {s}")
|
|
161
|
+
else:
|
|
162
|
+
lines.append("Terminal states: none")
|
|
163
|
+
|
|
164
|
+
lines.append("")
|
|
165
|
+
|
|
166
|
+
if report.self_loops:
|
|
167
|
+
lines.append("Self loops:")
|
|
168
|
+
for src, evt in report.self_loops:
|
|
169
|
+
lines.append(f" - {src} -[{evt or 'Completion'}]-> {src}")
|
|
170
|
+
else:
|
|
171
|
+
lines.append("Self loops: none")
|
|
172
|
+
|
|
173
|
+
self.graph_view.setPlainText("\n".join(lines))
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
# statable_gui/transition_editor_direct/palette_widget.py
|
|
2
|
+
"""\nCollapsible palette by category\n(shared library support / double-click edit / drag start support)\n\n[v1.5 fix]\n - refresh_lists: rf.name -> rf.qualified_name (bug #88)\n Driver.Init and App.Init were both shown as \"Init\",\n causing wrong edit target; fixed\n - _generate_unique_name: added namespace collision check\n"""
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
import logging
|
|
6
|
+
|
|
7
|
+
from PySide6.QtCore import Qt, QMimeData, Signal
|
|
8
|
+
from PySide6.QtGui import QDrag
|
|
9
|
+
from PySide6.QtWidgets import (
|
|
10
|
+
QWidget, QVBoxLayout, QListWidget, QListWidgetItem,
|
|
11
|
+
QAbstractItemView, QPushButton, QLabel, QGroupBox,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger("transition_editor_direct.palette")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class PaletteListWidget(QListWidget):
|
|
18
|
+
MIME_TYPE = "application/x-flow-item"
|
|
19
|
+
item_edit_requested = Signal(str)
|
|
20
|
+
|
|
21
|
+
def __init__(self, item_type: str, parent=None):
|
|
22
|
+
super().__init__(parent)
|
|
23
|
+
self.item_type = item_type
|
|
24
|
+
self.setDragEnabled(True)
|
|
25
|
+
self.setDragDropMode(QAbstractItemView.DragOnly)
|
|
26
|
+
self.setSelectionMode(QAbstractItemView.SingleSelection)
|
|
27
|
+
|
|
28
|
+
def mimeData(self, items):
|
|
29
|
+
mime = QMimeData()
|
|
30
|
+
if items:
|
|
31
|
+
data = {
|
|
32
|
+
"item_type": self.item_type,
|
|
33
|
+
"name": items[0].text(),
|
|
34
|
+
}
|
|
35
|
+
mime.setData(self.MIME_TYPE, json.dumps(data).encode("utf-8"))
|
|
36
|
+
mime.setText(items[0].text())
|
|
37
|
+
logger.debug(
|
|
38
|
+
f"PaletteListWidget.mimeData: type={self.item_type}, "
|
|
39
|
+
f"name='{items[0].text()}'"
|
|
40
|
+
)
|
|
41
|
+
return mime
|
|
42
|
+
|
|
43
|
+
def startDrag(self, supported_actions):
|
|
44
|
+
"""Override drag start to reliably execute QDrag"""
|
|
45
|
+
item = self.currentItem()
|
|
46
|
+
if item is None:
|
|
47
|
+
logger.debug("PaletteListWidget.startDrag: no current item")
|
|
48
|
+
return
|
|
49
|
+
|
|
50
|
+
logger.debug(
|
|
51
|
+
f"PaletteListWidget.startDrag: type={self.item_type}, "
|
|
52
|
+
f"name='{item.text()}'"
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
mime_data = QMimeData()
|
|
56
|
+
data = {
|
|
57
|
+
"item_type": self.item_type,
|
|
58
|
+
"name": item.text(),
|
|
59
|
+
}
|
|
60
|
+
mime_data.setData(self.MIME_TYPE, json.dumps(data).encode("utf-8"))
|
|
61
|
+
mime_data.setText(item.text())
|
|
62
|
+
|
|
63
|
+
drag = QDrag(self)
|
|
64
|
+
drag.setMimeData(mime_data)
|
|
65
|
+
result = drag.exec_(Qt.CopyAction)
|
|
66
|
+
logger.debug(f"PaletteListWidget.startDrag: drag result={result}")
|
|
67
|
+
|
|
68
|
+
def mouseDoubleClickEvent(self, event):
|
|
69
|
+
logger.debug(
|
|
70
|
+
f"PaletteListWidget.mouseDoubleClickEvent called: "
|
|
71
|
+
f"type={self.item_type}"
|
|
72
|
+
)
|
|
73
|
+
item = self.itemAt(event.position().toPoint())
|
|
74
|
+
if item:
|
|
75
|
+
text = item.text()
|
|
76
|
+
logger.debug(f" item text = '{text}'")
|
|
77
|
+
logger.debug(f" emitting item_edit_requested('{text}')")
|
|
78
|
+
self.item_edit_requested.emit(text)
|
|
79
|
+
event.accept()
|
|
80
|
+
return
|
|
81
|
+
logger.debug(" no item at click position")
|
|
82
|
+
super().mouseDoubleClickEvent(event)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class PaletteWidget(QWidget):
|
|
86
|
+
"""Collapsible palette by category (event selection screen)"""
|
|
87
|
+
|
|
88
|
+
edit_function_requested = Signal(str)
|
|
89
|
+
edit_transition_requested = Signal(str)
|
|
90
|
+
|
|
91
|
+
def __init__(self, role_function_library=None,
|
|
92
|
+
condition_library=None, parent=None):
|
|
93
|
+
super().__init__(parent)
|
|
94
|
+
self.role_function_library = role_function_library
|
|
95
|
+
self.condition_library = condition_library
|
|
96
|
+
self._setup_ui()
|
|
97
|
+
self.refresh_lists()
|
|
98
|
+
logger.debug("PaletteWidget initialized")
|
|
99
|
+
|
|
100
|
+
def _setup_ui(self):
|
|
101
|
+
layout = QVBoxLayout(self)
|
|
102
|
+
layout.setContentsMargins(4, 4, 4, 4)
|
|
103
|
+
layout.setSpacing(4)
|
|
104
|
+
|
|
105
|
+
title_label = QLabel(self.tr("Event selection screen"))
|
|
106
|
+
title_label.setStyleSheet("font-weight: bold; font-size: 14px;")
|
|
107
|
+
title_label.setAlignment(Qt.AlignCenter)
|
|
108
|
+
layout.addWidget(title_label)
|
|
109
|
+
|
|
110
|
+
# Role function section
|
|
111
|
+
func_group = QGroupBox(self.tr("Role function"))
|
|
112
|
+
v1 = QVBoxLayout(func_group)
|
|
113
|
+
v1.setContentsMargins(4, 4, 4, 4)
|
|
114
|
+
v1.setSpacing(2)
|
|
115
|
+
|
|
116
|
+
self.function_list = PaletteListWidget("function")
|
|
117
|
+
self.function_list.item_edit_requested.connect(
|
|
118
|
+
self._on_function_item_edit_requested
|
|
119
|
+
)
|
|
120
|
+
v1.addWidget(self.function_list)
|
|
121
|
+
|
|
122
|
+
add_func_btn = QPushButton(self.tr("+ Role functionAdd"))
|
|
123
|
+
add_func_btn.clicked.connect(self._add_function)
|
|
124
|
+
v1.addWidget(add_func_btn)
|
|
125
|
+
|
|
126
|
+
# Transition condition section
|
|
127
|
+
transition_group = QGroupBox(self.tr("Transition condition"))
|
|
128
|
+
v2 = QVBoxLayout(transition_group)
|
|
129
|
+
v2.setContentsMargins(4, 4, 4, 4)
|
|
130
|
+
v2.setSpacing(2)
|
|
131
|
+
|
|
132
|
+
self.transition_list = PaletteListWidget("transition")
|
|
133
|
+
self.transition_list.item_edit_requested.connect(
|
|
134
|
+
self._on_transition_item_edit_requested
|
|
135
|
+
)
|
|
136
|
+
v2.addWidget(self.transition_list)
|
|
137
|
+
|
|
138
|
+
add_transition_btn = QPushButton(self.tr("+ Transition conditionAdd"))
|
|
139
|
+
add_transition_btn.clicked.connect(self._add_transition)
|
|
140
|
+
v2.addWidget(add_transition_btn)
|
|
141
|
+
|
|
142
|
+
layout.addWidget(func_group)
|
|
143
|
+
layout.addWidget(transition_group)
|
|
144
|
+
layout.addStretch()
|
|
145
|
+
|
|
146
|
+
def _generate_unique_name(self, base_name: str, library) -> str:
|
|
147
|
+
"""\n Generate a unique name\n\n [v1.5 improvement]\n RoleFunctionLibrary.role_functions is keyed by qualified_name,\n so collision checks against existing keys must be done with both\n base_name (bare name) and qualified_name.\n """
|
|
148
|
+
existing_names = set()
|
|
149
|
+
if hasattr(library, 'role_functions'):
|
|
150
|
+
existing_names = set(library.role_functions.keys())
|
|
151
|
+
elif hasattr(library, 'condition_templates'):
|
|
152
|
+
existing_names = set(library.condition_templates.keys())
|
|
153
|
+
|
|
154
|
+
def _collides(name: str) -> bool:
|
|
155
|
+
if name in existing_names:
|
|
156
|
+
return True
|
|
157
|
+
# Also detect suffix match of 'Namespace.Name'
|
|
158
|
+
return any(k.endswith('.' + name) for k in existing_names)
|
|
159
|
+
|
|
160
|
+
if not _collides(base_name):
|
|
161
|
+
return base_name
|
|
162
|
+
|
|
163
|
+
index = 2
|
|
164
|
+
while _collides(f"{base_name}_{index}"):
|
|
165
|
+
index += 1
|
|
166
|
+
return f"{base_name}_{index}"
|
|
167
|
+
|
|
168
|
+
def _add_function(self):
|
|
169
|
+
from statable_gui.libcntrl.role_function_library import RoleFunction
|
|
170
|
+
base_name = "NewFunction"
|
|
171
|
+
name = self._generate_unique_name(
|
|
172
|
+
base_name, self.role_function_library
|
|
173
|
+
)
|
|
174
|
+
try:
|
|
175
|
+
self.role_function_library.add(
|
|
176
|
+
RoleFunction(name=name, title=name)
|
|
177
|
+
)
|
|
178
|
+
logger.debug(f"Added new role function to library: {name}")
|
|
179
|
+
except ValueError as e:
|
|
180
|
+
logger.warning(f"Failed to add role function: {e}")
|
|
181
|
+
self.refresh_lists()
|
|
182
|
+
|
|
183
|
+
def _add_transition(self):
|
|
184
|
+
from statable_gui.libcntrl.condition_library import ConditionTemplate
|
|
185
|
+
base_name = "NewEvent"
|
|
186
|
+
name = self._generate_unique_name(
|
|
187
|
+
base_name, self.condition_library
|
|
188
|
+
)
|
|
189
|
+
try:
|
|
190
|
+
self.condition_library.add(
|
|
191
|
+
ConditionTemplate(name=name, condition="")
|
|
192
|
+
)
|
|
193
|
+
logger.debug(f"Added new condition template to library: {name}")
|
|
194
|
+
except ValueError as e:
|
|
195
|
+
logger.warning(f"Failed to add condition template: {e}")
|
|
196
|
+
self.refresh_lists()
|
|
197
|
+
|
|
198
|
+
def _on_function_item_edit_requested(self, name: str):
|
|
199
|
+
"""\n Role function edit request\n\n [v1.5 fix]\n name can be qualified_name ('Driver.Init') or\n bare name ('Init'). ActionEditorDialog downstream resolves\n via RoleFunctionLibrary.get(name), so both work correctly.\n """
|
|
200
|
+
logger.debug(
|
|
201
|
+
f"PaletteWidget._on_function_item_edit_requested: "
|
|
202
|
+
f"name='{name}'"
|
|
203
|
+
)
|
|
204
|
+
self.edit_function_requested.emit(name)
|
|
205
|
+
|
|
206
|
+
def _on_transition_item_edit_requested(self, name: str):
|
|
207
|
+
logger.debug(
|
|
208
|
+
f"PaletteWidget._on_transition_item_edit_requested: "
|
|
209
|
+
f"name='{name}'"
|
|
210
|
+
)
|
|
211
|
+
self.edit_transition_requested.emit(name)
|
|
212
|
+
|
|
213
|
+
def refresh_lists(self):
|
|
214
|
+
"""\n Rebuild the role function / transition condition list\n\n [v1.5 fix]\n Role functions shown by qualified_name ('Driver.Init').\n With bare names only, namespace collisions cause wrong edit target.\n """
|
|
215
|
+
logger.debug("PaletteWidget.refresh_lists called")
|
|
216
|
+
if self.role_function_library:
|
|
217
|
+
self.function_list.clear()
|
|
218
|
+
for rf in self.role_function_library.list_all():
|
|
219
|
+
display_name = getattr(rf, 'qualified_name', None) or rf.name
|
|
220
|
+
self.function_list.addItem(display_name)
|
|
221
|
+
logger.debug(
|
|
222
|
+
f"Function list refreshed: "
|
|
223
|
+
f"{self.function_list.count()} items"
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
if self.condition_library:
|
|
227
|
+
self.transition_list.clear()
|
|
228
|
+
for ct in self.condition_library.list_all():
|
|
229
|
+
self.transition_list.addItem(ct.name)
|
|
230
|
+
logger.debug(
|
|
231
|
+
f"Transition list refreshed: "
|
|
232
|
+
f"{self.transition_list.count()} items"
|
|
233
|
+
)
|