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,156 @@
|
|
|
1
|
+
# statable_gui/transition_editor_direct/relations_edit_dialog.py
|
|
2
|
+
"""Dialog to edit one TransitionRelation (v2.2).
|
|
3
|
+
|
|
4
|
+
[v2.2 enhancement]
|
|
5
|
+
- Members list shows each transition as "T1: cond_A -> Active"
|
|
6
|
+
so the user can identify which label belongs to which condition.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
from typing import List, Optional, Dict
|
|
11
|
+
|
|
12
|
+
from PySide6.QtCore import Qt
|
|
13
|
+
from PySide6.QtWidgets import (
|
|
14
|
+
QDialog, QVBoxLayout, QHBoxLayout, QLabel, QComboBox,
|
|
15
|
+
QLineEdit, QListWidget, QListWidgetItem, QDialogButtonBox,
|
|
16
|
+
QGroupBox, QMessageBox,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
from statable.model import TransitionRelation
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger("transition_editor_direct.relations_edit_dialog")
|
|
22
|
+
|
|
23
|
+
KIND_CHOICES = ["sequential", "exclusive", "group"]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _fmt_member(label: str, detail: str) -> str:
|
|
27
|
+
"""Format a member list entry: 'T1: cond_A -> Active'."""
|
|
28
|
+
if detail:
|
|
29
|
+
return f"{label}: {detail}"
|
|
30
|
+
return label
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class RelationsEditDialog(QDialog):
|
|
34
|
+
"""Edit a single relation (kind / members / shared_condition)."""
|
|
35
|
+
|
|
36
|
+
def __init__(self, parent=None,
|
|
37
|
+
relation: Optional[TransitionRelation] = None,
|
|
38
|
+
available_labels: Optional[List[str]] = None,
|
|
39
|
+
member_details: Optional[Dict[str, str]] = None):
|
|
40
|
+
"""
|
|
41
|
+
Args:
|
|
42
|
+
relation: existing relation (None for new)
|
|
43
|
+
available_labels: list of Transition labels in this cell
|
|
44
|
+
member_details: {label: "cond_A -> Active"} for display
|
|
45
|
+
"""
|
|
46
|
+
super().__init__(parent)
|
|
47
|
+
self.setWindowTitle(self.tr("Edit relation"))
|
|
48
|
+
self.setMinimumSize(600, 500)
|
|
49
|
+
|
|
50
|
+
self.available_labels = list(available_labels or [])
|
|
51
|
+
self.member_details = dict(member_details or {})
|
|
52
|
+
|
|
53
|
+
layout = QVBoxLayout(self)
|
|
54
|
+
|
|
55
|
+
# ---- Kind ----
|
|
56
|
+
kind_layout = QHBoxLayout()
|
|
57
|
+
kind_layout.addWidget(QLabel(self.tr("Kind:")))
|
|
58
|
+
self.kind_combo = QComboBox()
|
|
59
|
+
self.kind_combo.addItems(KIND_CHOICES)
|
|
60
|
+
kind_layout.addWidget(self.kind_combo)
|
|
61
|
+
kind_layout.addStretch()
|
|
62
|
+
layout.addLayout(kind_layout)
|
|
63
|
+
|
|
64
|
+
# ---- Members (checkbox list with condition/target) ----
|
|
65
|
+
members_group = QGroupBox(self.tr("Members (Transition labels)"))
|
|
66
|
+
members_layout = QVBoxLayout(members_group)
|
|
67
|
+
self.members_list = QListWidget()
|
|
68
|
+
for lbl in self.available_labels:
|
|
69
|
+
detail = self.member_details.get(lbl, "")
|
|
70
|
+
text = _fmt_member(lbl, detail)
|
|
71
|
+
item = QListWidgetItem(text)
|
|
72
|
+
item.setData(Qt.UserRole, lbl) # store the raw label
|
|
73
|
+
item.setFlags(item.flags() | Qt.ItemIsUserCheckable)
|
|
74
|
+
item.setCheckState(Qt.Unchecked)
|
|
75
|
+
if detail:
|
|
76
|
+
item.setToolTip(f"{lbl}\n{detail}")
|
|
77
|
+
self.members_list.addItem(item)
|
|
78
|
+
members_layout.addWidget(self.members_list)
|
|
79
|
+
|
|
80
|
+
if not self.available_labels:
|
|
81
|
+
hint = QLabel(
|
|
82
|
+
self.tr("No transitions defined in this cell. "
|
|
83
|
+
"Add transitions first."))
|
|
84
|
+
hint.setStyleSheet("color: #a00;")
|
|
85
|
+
members_layout.addWidget(hint)
|
|
86
|
+
layout.addWidget(members_group)
|
|
87
|
+
|
|
88
|
+
# ---- Shared condition ----
|
|
89
|
+
cond_group = QGroupBox(self.tr("Shared condition (used when kind = group)"))
|
|
90
|
+
cond_layout = QVBoxLayout(cond_group)
|
|
91
|
+
self.shared_edit = QLineEdit()
|
|
92
|
+
self.shared_edit.setPlaceholderText(self.tr("e.g. cond_A"))
|
|
93
|
+
cond_layout.addWidget(self.shared_edit)
|
|
94
|
+
layout.addWidget(cond_group)
|
|
95
|
+
|
|
96
|
+
# ---- Note ----
|
|
97
|
+
note_layout = QHBoxLayout()
|
|
98
|
+
note_layout.addWidget(QLabel(self.tr("Note:")))
|
|
99
|
+
self.note_edit = QLineEdit()
|
|
100
|
+
note_layout.addWidget(self.note_edit)
|
|
101
|
+
layout.addLayout(note_layout)
|
|
102
|
+
|
|
103
|
+
# ---- OK / Cancel ----
|
|
104
|
+
buttons = QDialogButtonBox(
|
|
105
|
+
QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
|
|
106
|
+
buttons.accepted.connect(self._on_accept)
|
|
107
|
+
buttons.rejected.connect(self.reject)
|
|
108
|
+
layout.addWidget(buttons)
|
|
109
|
+
|
|
110
|
+
# Load
|
|
111
|
+
if relation is not None:
|
|
112
|
+
self._load(relation)
|
|
113
|
+
|
|
114
|
+
def _load(self, relation: TransitionRelation):
|
|
115
|
+
kind = relation.kind if relation.kind in KIND_CHOICES else "sequential"
|
|
116
|
+
self.kind_combo.setCurrentText(kind)
|
|
117
|
+
self.shared_edit.setText(relation.shared_condition or "")
|
|
118
|
+
self.note_edit.setText(relation.note or "")
|
|
119
|
+
|
|
120
|
+
selected = set(relation.members or [])
|
|
121
|
+
for i in range(self.members_list.count()):
|
|
122
|
+
item = self.members_list.item(i)
|
|
123
|
+
raw_label = item.data(Qt.UserRole) or item.text()
|
|
124
|
+
if raw_label in selected:
|
|
125
|
+
item.setCheckState(Qt.Checked)
|
|
126
|
+
|
|
127
|
+
def _on_accept(self):
|
|
128
|
+
members = self.get_members()
|
|
129
|
+
kind = self.kind_combo.currentText()
|
|
130
|
+
if kind == "group" and not members:
|
|
131
|
+
QMessageBox.warning(
|
|
132
|
+
self, self.tr("Warning"),
|
|
133
|
+
self.tr("A group relation requires at least one member."))
|
|
134
|
+
return
|
|
135
|
+
self.accept()
|
|
136
|
+
|
|
137
|
+
# ------------------------------------------------------------------
|
|
138
|
+
# Public API
|
|
139
|
+
# ------------------------------------------------------------------
|
|
140
|
+
def get_members(self) -> List[str]:
|
|
141
|
+
"""Return only the raw labels (T1, T2, ...)."""
|
|
142
|
+
result = []
|
|
143
|
+
for i in range(self.members_list.count()):
|
|
144
|
+
item = self.members_list.item(i)
|
|
145
|
+
if item.checkState() == Qt.Checked:
|
|
146
|
+
label = item.data(Qt.UserRole) or item.text()
|
|
147
|
+
result.append(label)
|
|
148
|
+
return result
|
|
149
|
+
|
|
150
|
+
def get_relation(self) -> TransitionRelation:
|
|
151
|
+
return TransitionRelation(
|
|
152
|
+
kind=self.kind_combo.currentText(),
|
|
153
|
+
members=self.get_members(),
|
|
154
|
+
shared_condition=self.shared_edit.text().strip(),
|
|
155
|
+
note=self.note_edit.text().strip(),
|
|
156
|
+
)
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
# statable_gui/transition_editor_direct/relations_tab.py
|
|
2
|
+
"""Relations tab widget (v2.2).
|
|
3
|
+
|
|
4
|
+
Sequential / Exclusive / Group relations between transitions.
|
|
5
|
+
Editing is done via RelationsEditDialog.
|
|
6
|
+
|
|
7
|
+
[v2.2 enhancement]
|
|
8
|
+
- Members list shows "T1: cond_A -> Active" so labels are identifiable.
|
|
9
|
+
- set_transitions() lets the dialog pass transition details.
|
|
10
|
+
|
|
11
|
+
[v2.3.1 fix]
|
|
12
|
+
- Add setEditTriggers(NoEditTriggers) to prevent inline edit from
|
|
13
|
+
swallowing subsequent double-clicks. Same fix as transitions_tab.py.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import logging
|
|
17
|
+
from typing import List, Optional, Dict
|
|
18
|
+
|
|
19
|
+
from PySide6.QtCore import Qt, Signal
|
|
20
|
+
from PySide6.QtWidgets import (
|
|
21
|
+
QWidget, QVBoxLayout, QHBoxLayout, QTableWidget, QTableWidgetItem,
|
|
22
|
+
QPushButton, QHeaderView, QAbstractItemView, QLabel, QMessageBox,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
from statable.model import TransitionRelation
|
|
26
|
+
from .draft import ActionDraft
|
|
27
|
+
from .relations_edit_dialog import RelationsEditDialog
|
|
28
|
+
|
|
29
|
+
logger = logging.getLogger("transition_editor_direct.relations_tab")
|
|
30
|
+
|
|
31
|
+
COL_KIND = 0
|
|
32
|
+
COL_MEMBERS = 1
|
|
33
|
+
COL_SHARED = 2
|
|
34
|
+
COL_NOTE = 3
|
|
35
|
+
COLUMN_COUNT = 4
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class RelationsTab(QWidget):
|
|
39
|
+
"""Relations tab."""
|
|
40
|
+
|
|
41
|
+
relations_changed = Signal()
|
|
42
|
+
|
|
43
|
+
def __init__(self, draft: ActionDraft, parent=None):
|
|
44
|
+
super().__init__(parent)
|
|
45
|
+
self.draft = draft
|
|
46
|
+
# v2.2: {label: "cond_A -> Active"} populated by set_transitions()
|
|
47
|
+
self.member_details: Dict[str, str] = {}
|
|
48
|
+
self._build_ui()
|
|
49
|
+
|
|
50
|
+
def _build_ui(self):
|
|
51
|
+
layout = QVBoxLayout(self)
|
|
52
|
+
|
|
53
|
+
info = QLabel(
|
|
54
|
+
self.tr("Relations between transitions in this cell.\n"
|
|
55
|
+
" sequential: evaluate members in order\n"
|
|
56
|
+
" exclusive : at most one fires\n"
|
|
57
|
+
" group : hoists shared_condition as outer if"))
|
|
58
|
+
layout.addWidget(info)
|
|
59
|
+
|
|
60
|
+
self.table = QTableWidget(0, COLUMN_COUNT)
|
|
61
|
+
self.table.setHorizontalHeaderLabels(
|
|
62
|
+
["Kind", "Members", "Shared condition", "Note"])
|
|
63
|
+
self.table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
|
|
64
|
+
self.table.setSelectionBehavior(QAbstractItemView.SelectRows)
|
|
65
|
+
self.table.setSelectionMode(QAbstractItemView.SingleSelection)
|
|
66
|
+
# [v2.3.1 fix] Disable inline editing so that double-click always
|
|
67
|
+
# triggers the edit dialog.
|
|
68
|
+
self.table.setEditTriggers(QAbstractItemView.NoEditTriggers)
|
|
69
|
+
self.table.cellDoubleClicked.connect(self._on_double_click)
|
|
70
|
+
layout.addWidget(self.table)
|
|
71
|
+
|
|
72
|
+
btn_layout = QHBoxLayout()
|
|
73
|
+
add_btn = QPushButton(self.tr("+ Add"))
|
|
74
|
+
add_btn.clicked.connect(lambda: self._add())
|
|
75
|
+
edit_btn = QPushButton(self.tr("Edit..."))
|
|
76
|
+
edit_btn.clicked.connect(lambda: self._edit(self.table.currentRow()))
|
|
77
|
+
del_btn = QPushButton(self.tr("Delete"))
|
|
78
|
+
del_btn.clicked.connect(lambda: self.delete_relation(
|
|
79
|
+
self.table.currentRow()))
|
|
80
|
+
|
|
81
|
+
btn_layout.addWidget(add_btn)
|
|
82
|
+
btn_layout.addWidget(edit_btn)
|
|
83
|
+
btn_layout.addWidget(del_btn)
|
|
84
|
+
btn_layout.addStretch()
|
|
85
|
+
layout.addLayout(btn_layout)
|
|
86
|
+
|
|
87
|
+
# ------------------------------------------------------------------
|
|
88
|
+
# Member details (called by dialog.py)
|
|
89
|
+
# ------------------------------------------------------------------
|
|
90
|
+
def set_member_details(self, details: Dict[str, str]):
|
|
91
|
+
"""Update the {label: 'cond -> target'} map.
|
|
92
|
+
|
|
93
|
+
Called by ActionEditorDialog whenever transitions change.
|
|
94
|
+
"""
|
|
95
|
+
self.member_details = dict(details or {})
|
|
96
|
+
|
|
97
|
+
# ------------------------------------------------------------------
|
|
98
|
+
# Available labels (from current cell's transitions)
|
|
99
|
+
# ------------------------------------------------------------------
|
|
100
|
+
def _get_available_labels(self) -> List[str]:
|
|
101
|
+
labels = []
|
|
102
|
+
for item in self.draft.flow_items:
|
|
103
|
+
if item.item_type == "transition":
|
|
104
|
+
lbl = item.params.get("label", "")
|
|
105
|
+
if lbl:
|
|
106
|
+
labels.append(lbl)
|
|
107
|
+
return labels
|
|
108
|
+
|
|
109
|
+
# ------------------------------------------------------------------
|
|
110
|
+
# Public API
|
|
111
|
+
# ------------------------------------------------------------------
|
|
112
|
+
def row_count(self) -> int:
|
|
113
|
+
return self.table.rowCount()
|
|
114
|
+
|
|
115
|
+
def _make_item(self, text: str) -> QTableWidgetItem:
|
|
116
|
+
"""Create a non-editable table item."""
|
|
117
|
+
item = QTableWidgetItem(text)
|
|
118
|
+
item.setFlags(item.flags() & ~Qt.ItemIsEditable)
|
|
119
|
+
return item
|
|
120
|
+
|
|
121
|
+
def add_relation(self, relation: Optional[TransitionRelation] = None) -> int:
|
|
122
|
+
"""Add a relation. If relation is None, opens the edit dialog first."""
|
|
123
|
+
if relation is None:
|
|
124
|
+
available = self._get_available_labels()
|
|
125
|
+
if not available:
|
|
126
|
+
QMessageBox.warning(
|
|
127
|
+
self, self.tr("Warning"),
|
|
128
|
+
self.tr("No transitions defined in this cell.\n"
|
|
129
|
+
"Add transitions first."))
|
|
130
|
+
return -1
|
|
131
|
+
dlg = RelationsEditDialog(
|
|
132
|
+
parent=self,
|
|
133
|
+
relation=None,
|
|
134
|
+
available_labels=available,
|
|
135
|
+
member_details=self.member_details,
|
|
136
|
+
)
|
|
137
|
+
if dlg.exec() != RelationsEditDialog.Accepted:
|
|
138
|
+
return -1
|
|
139
|
+
relation = dlg.get_relation()
|
|
140
|
+
|
|
141
|
+
row = self.table.rowCount()
|
|
142
|
+
self.table.insertRow(row)
|
|
143
|
+
|
|
144
|
+
self.table.setItem(row, COL_KIND, self._make_item(relation.kind))
|
|
145
|
+
self.table.setItem(row, COL_MEMBERS,
|
|
146
|
+
self._make_item(", ".join(relation.members)))
|
|
147
|
+
self.table.setItem(row, COL_SHARED,
|
|
148
|
+
self._make_item(relation.shared_condition))
|
|
149
|
+
self.table.setItem(row, COL_NOTE, self._make_item(relation.note))
|
|
150
|
+
|
|
151
|
+
self.relations_changed.emit()
|
|
152
|
+
return row
|
|
153
|
+
|
|
154
|
+
def delete_relation(self, row: int) -> None:
|
|
155
|
+
if 0 <= row < self.table.rowCount():
|
|
156
|
+
self.table.removeRow(row)
|
|
157
|
+
self.relations_changed.emit()
|
|
158
|
+
|
|
159
|
+
def get_relations(self) -> List[TransitionRelation]:
|
|
160
|
+
result = []
|
|
161
|
+
for row in range(self.table.rowCount()):
|
|
162
|
+
def _txt(col: int) -> str:
|
|
163
|
+
it = self.table.item(row, col)
|
|
164
|
+
return it.text().strip() if it else ""
|
|
165
|
+
|
|
166
|
+
members = [m.strip() for m in _txt(COL_MEMBERS).split(",")
|
|
167
|
+
if m.strip()]
|
|
168
|
+
result.append(TransitionRelation(
|
|
169
|
+
kind=_txt(COL_KIND) or "sequential",
|
|
170
|
+
members=members,
|
|
171
|
+
shared_condition=_txt(COL_SHARED),
|
|
172
|
+
note=_txt(COL_NOTE),
|
|
173
|
+
))
|
|
174
|
+
return result
|
|
175
|
+
|
|
176
|
+
def set_relations(self, relations: List[TransitionRelation]) -> None:
|
|
177
|
+
self.table.setRowCount(0)
|
|
178
|
+
for r in relations:
|
|
179
|
+
row = self.table.rowCount()
|
|
180
|
+
self.table.insertRow(row)
|
|
181
|
+
self.table.setItem(row, COL_KIND, self._make_item(r.kind))
|
|
182
|
+
self.table.setItem(row, COL_MEMBERS,
|
|
183
|
+
self._make_item(", ".join(r.members)))
|
|
184
|
+
self.table.setItem(row, COL_SHARED,
|
|
185
|
+
self._make_item(r.shared_condition))
|
|
186
|
+
self.table.setItem(row, COL_NOTE, self._make_item(r.note))
|
|
187
|
+
|
|
188
|
+
# ------------------------------------------------------------------
|
|
189
|
+
# Slots
|
|
190
|
+
# ------------------------------------------------------------------
|
|
191
|
+
def _on_double_click(self, row: int, col: int):
|
|
192
|
+
self._edit(row)
|
|
193
|
+
|
|
194
|
+
def _add(self):
|
|
195
|
+
self.add_relation(None)
|
|
196
|
+
|
|
197
|
+
def _edit(self, row: int):
|
|
198
|
+
if row < 0 or row >= self.table.rowCount():
|
|
199
|
+
return
|
|
200
|
+
|
|
201
|
+
def _txt(col: int) -> str:
|
|
202
|
+
it = self.table.item(row, col)
|
|
203
|
+
return it.text().strip() if it else ""
|
|
204
|
+
|
|
205
|
+
current = TransitionRelation(
|
|
206
|
+
kind=_txt(COL_KIND) or "sequential",
|
|
207
|
+
members=[m.strip() for m in _txt(COL_MEMBERS).split(",")
|
|
208
|
+
if m.strip()],
|
|
209
|
+
shared_condition=_txt(COL_SHARED),
|
|
210
|
+
note=_txt(COL_NOTE),
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
available = self._get_available_labels()
|
|
214
|
+
dlg = RelationsEditDialog(
|
|
215
|
+
parent=self,
|
|
216
|
+
relation=current,
|
|
217
|
+
available_labels=available,
|
|
218
|
+
member_details=self.member_details,
|
|
219
|
+
)
|
|
220
|
+
if dlg.exec() != RelationsEditDialog.Accepted:
|
|
221
|
+
return
|
|
222
|
+
|
|
223
|
+
new_r = dlg.get_relation()
|
|
224
|
+
self.table.setItem(row, COL_KIND, self._make_item(new_r.kind))
|
|
225
|
+
self.table.setItem(row, COL_MEMBERS,
|
|
226
|
+
self._make_item(", ".join(new_r.members)))
|
|
227
|
+
self.table.setItem(row, COL_SHARED,
|
|
228
|
+
self._make_item(new_r.shared_condition))
|
|
229
|
+
self.table.setItem(row, COL_NOTE, self._make_item(new_r.note))
|
|
230
|
+
self.relations_changed.emit()
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# statable_gui/transition_editor_direct/system_global_dialog.py
|
|
2
|
+
"""
|
|
3
|
+
System global variables (separate screen)
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from PySide6.QtWidgets import QDialog, QVBoxLayout, QHBoxLayout, QPushButton, QListWidget, QInputDialog
|
|
7
|
+
|
|
8
|
+
from .draft import SystemGlobal
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class SystemGlobalDialog(QDialog):
|
|
12
|
+
def __init__(self, draft, parent=None):
|
|
13
|
+
super().__init__(parent)
|
|
14
|
+
self.draft = draft
|
|
15
|
+
self.setWindowTitle(self.tr("System global variables"))
|
|
16
|
+
self.setMinimumSize(400, 300)
|
|
17
|
+
|
|
18
|
+
layout = QVBoxLayout(self)
|
|
19
|
+
self.list_widget = QListWidget()
|
|
20
|
+
layout.addWidget(self.list_widget)
|
|
21
|
+
|
|
22
|
+
btn_layout = QHBoxLayout()
|
|
23
|
+
add_btn = QPushButton(self.tr("Add"))
|
|
24
|
+
add_btn.clicked.connect(self._add)
|
|
25
|
+
btn_layout.addWidget(add_btn)
|
|
26
|
+
del_btn = QPushButton(self.tr("Delete"))
|
|
27
|
+
del_btn.clicked.connect(self._delete)
|
|
28
|
+
btn_layout.addWidget(del_btn)
|
|
29
|
+
layout.addLayout(btn_layout)
|
|
30
|
+
|
|
31
|
+
close_btn = QPushButton(self.tr("Close"))
|
|
32
|
+
close_btn.clicked.connect(self.accept)
|
|
33
|
+
layout.addWidget(close_btn)
|
|
34
|
+
|
|
35
|
+
self._load()
|
|
36
|
+
|
|
37
|
+
def _load(self):
|
|
38
|
+
self.list_widget.clear()
|
|
39
|
+
for g in self.draft.system_globals:
|
|
40
|
+
self.list_widget.addItem(f"{g.name} : {g.type} = {g.initial_value}")
|
|
41
|
+
|
|
42
|
+
def _add(self):
|
|
43
|
+
name, ok = QInputDialog.getText(self, "Add", "Variable name:")
|
|
44
|
+
if ok and name:
|
|
45
|
+
self.draft.system_globals.append(SystemGlobal(name=name))
|
|
46
|
+
self._load()
|
|
47
|
+
|
|
48
|
+
def _delete(self):
|
|
49
|
+
row = self.list_widget.currentRow()
|
|
50
|
+
if row >= 0:
|
|
51
|
+
del self.draft.system_globals[row]
|
|
52
|
+
self._load()
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# statable_gui/transition_editor_direct/transition_actions_dialog.py
|
|
2
|
+
"""Dialog to edit pre_actions / else_actions for one transition (v2.2)."""
|
|
3
|
+
|
|
4
|
+
import logging
|
|
5
|
+
from typing import List
|
|
6
|
+
|
|
7
|
+
from PySide6.QtCore import Qt
|
|
8
|
+
from PySide6.QtWidgets import (
|
|
9
|
+
QDialog, QVBoxLayout, QHBoxLayout, QLabel, QComboBox, QPushButton,
|
|
10
|
+
QListWidget, QListWidgetItem, QDialogButtonBox, QGroupBox,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger("transition_editor_direct.transition_actions_dialog")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class TransitionActionsDialog(QDialog):
|
|
17
|
+
"""Edit pre_actions and else_actions for a single transition.
|
|
18
|
+
|
|
19
|
+
Provides two list widgets with [+ Add] / [Delete] buttons.
|
|
20
|
+
The add button uses a combo box populated from the role function library.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __init__(self, parent=None,
|
|
24
|
+
label: str = "",
|
|
25
|
+
pre_actions: List[str] = None,
|
|
26
|
+
else_actions: List[str] = None,
|
|
27
|
+
role_functions: List[str] = None):
|
|
28
|
+
super().__init__(parent)
|
|
29
|
+
self.label = label or "(unnamed)"
|
|
30
|
+
self.role_functions = role_functions or []
|
|
31
|
+
|
|
32
|
+
self.setWindowTitle(f"Edit actions: {self.label}")
|
|
33
|
+
self.setMinimumSize(500, 450)
|
|
34
|
+
|
|
35
|
+
layout = QVBoxLayout(self)
|
|
36
|
+
layout.addWidget(QLabel(
|
|
37
|
+
f"Transition: {self.label}\n"
|
|
38
|
+
"Set role functions executed before / after the transition."))
|
|
39
|
+
|
|
40
|
+
# ---- Pre-actions group ----
|
|
41
|
+
pre_group = QGroupBox(self.tr("Pre-actions (executed before next_state assignment)"))
|
|
42
|
+
pre_layout = QVBoxLayout(pre_group)
|
|
43
|
+
self.pre_list = QListWidget()
|
|
44
|
+
for a in (pre_actions or []):
|
|
45
|
+
self.pre_list.addItem(a)
|
|
46
|
+
pre_layout.addWidget(self.pre_list)
|
|
47
|
+
|
|
48
|
+
pre_btn = QHBoxLayout()
|
|
49
|
+
self.pre_combo = QComboBox()
|
|
50
|
+
self.pre_combo.setEditable(True)
|
|
51
|
+
self.pre_combo.addItems(self.role_functions)
|
|
52
|
+
pre_btn.addWidget(self.pre_combo, stretch=1)
|
|
53
|
+
pre_add = QPushButton(self.tr("+ Add"))
|
|
54
|
+
pre_add.clicked.connect(lambda: self._add(self.pre_list, self.pre_combo))
|
|
55
|
+
pre_btn.addWidget(pre_add)
|
|
56
|
+
pre_del = QPushButton(self.tr("Delete"))
|
|
57
|
+
pre_del.clicked.connect(lambda: self._del(self.pre_list))
|
|
58
|
+
pre_btn.addWidget(pre_del)
|
|
59
|
+
pre_layout.addLayout(pre_btn)
|
|
60
|
+
layout.addWidget(pre_group)
|
|
61
|
+
|
|
62
|
+
# ---- Else-actions group ----
|
|
63
|
+
else_group = QGroupBox(self.tr("Else-actions (executed in the else branch)"))
|
|
64
|
+
else_layout = QVBoxLayout(else_group)
|
|
65
|
+
self.else_list = QListWidget()
|
|
66
|
+
for a in (else_actions or []):
|
|
67
|
+
self.else_list.addItem(a)
|
|
68
|
+
else_layout.addWidget(self.else_list)
|
|
69
|
+
|
|
70
|
+
else_btn = QHBoxLayout()
|
|
71
|
+
self.else_combo = QComboBox()
|
|
72
|
+
self.else_combo.setEditable(True)
|
|
73
|
+
self.else_combo.addItems(self.role_functions)
|
|
74
|
+
else_btn.addWidget(self.else_combo, stretch=1)
|
|
75
|
+
else_add = QPushButton(self.tr("+ Add"))
|
|
76
|
+
else_add.clicked.connect(lambda: self._add(self.else_list, self.else_combo))
|
|
77
|
+
else_btn.addWidget(else_add)
|
|
78
|
+
else_del = QPushButton(self.tr("Delete"))
|
|
79
|
+
else_del.clicked.connect(lambda: self._del(self.else_list))
|
|
80
|
+
else_btn.addWidget(else_del)
|
|
81
|
+
else_layout.addLayout(else_btn)
|
|
82
|
+
layout.addWidget(else_group)
|
|
83
|
+
|
|
84
|
+
# ---- OK / Cancel ----
|
|
85
|
+
buttons = QDialogButtonBox(
|
|
86
|
+
QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
|
|
87
|
+
buttons.accepted.connect(self.accept)
|
|
88
|
+
buttons.rejected.connect(self.reject)
|
|
89
|
+
layout.addWidget(buttons)
|
|
90
|
+
|
|
91
|
+
def _add(self, list_widget: QListWidget, combo: QComboBox):
|
|
92
|
+
name = combo.currentText().strip()
|
|
93
|
+
if not name:
|
|
94
|
+
return
|
|
95
|
+
list_widget.addItem(name)
|
|
96
|
+
logger.debug(f"Added: {name}")
|
|
97
|
+
|
|
98
|
+
def _del(self, list_widget: QListWidget):
|
|
99
|
+
row = list_widget.currentRow()
|
|
100
|
+
if row >= 0:
|
|
101
|
+
list_widget.takeItem(row)
|
|
102
|
+
|
|
103
|
+
def get_pre_actions(self) -> List[str]:
|
|
104
|
+
return [self.pre_list.item(i).text()
|
|
105
|
+
for i in range(self.pre_list.count())]
|
|
106
|
+
|
|
107
|
+
def get_else_actions(self) -> List[str]:
|
|
108
|
+
return [self.else_list.item(i).text()
|
|
109
|
+
for i in range(self.else_list.count())]
|