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