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
statable_gui/widgets.py
ADDED
|
@@ -0,0 +1,1315 @@
|
|
|
1
|
+
# statable_gui/widgets.py
|
|
2
|
+
"""StaTable main widget (shared library support / v2.2 entry-exit list)
|
|
3
|
+
|
|
4
|
+
Version: 3.11 (2026-09-22)
|
|
5
|
+
- [C-38 fix] SettingsPanel.add_state() now emits settings_changed
|
|
6
|
+
so that the window's modified flag ([*]) updates when a state
|
|
7
|
+
is added.
|
|
8
|
+
|
|
9
|
+
- [v3.11] Namespace combo box: full project-wide candidates.
|
|
10
|
+
* SettingsPanel now accepts `layer_names_provider`, a
|
|
11
|
+
zero-argument callable returning every tab name (= every
|
|
12
|
+
layer name) in the project.
|
|
13
|
+
* StateMachineTab forwards `layer_names_provider` to
|
|
14
|
+
SettingsPanel.
|
|
15
|
+
* _get_namespace_choices() merges candidates from:
|
|
16
|
+
1. layer_names_provider (all tabs: Application, Driver, ...)
|
|
17
|
+
2. sm.layer_name (current tab, in case provider
|
|
18
|
+
is not supplied)
|
|
19
|
+
3. role_function_library namespaces
|
|
20
|
+
4. sm.role_functions namespaces
|
|
21
|
+
Deduplicated while preserving order, so the inline combo
|
|
22
|
+
box and RoleFunctionDialog show the same complete list.
|
|
23
|
+
|
|
24
|
+
[v3.10 behavior retained]
|
|
25
|
+
* Role function table: inline editing restricted to the
|
|
26
|
+
Namespace column (editable QComboBox delegate).
|
|
27
|
+
Title / Function name / Description are read-only there.
|
|
28
|
+
* _NamespaceDelegate uses _get_namespace_choices() on every
|
|
29
|
+
editor creation, so the candidate list stays fresh.
|
|
30
|
+
|
|
31
|
+
[v3.9 behavior retained]
|
|
32
|
+
* SettingsPanel receives role_function_library and
|
|
33
|
+
literal_library from StateMachineTab.
|
|
34
|
+
* RoleFunctionDialog renders namespace as an editable
|
|
35
|
+
QComboBox populated with candidates.
|
|
36
|
+
|
|
37
|
+
[v3.8 behavior retained]
|
|
38
|
+
* "Edit" button + row double-click for role functions.
|
|
39
|
+
* used_global_vars / used_events / used_literals persisted.
|
|
40
|
+
* apply_changes() uses getattr(...) for defensive access.
|
|
41
|
+
|
|
42
|
+
[v3.7 behavior retained]
|
|
43
|
+
* State list: 6 -> 5 columns (removed "do function").
|
|
44
|
+
* Role function: 9 -> 4 columns (removed "Return type",
|
|
45
|
+
"Arg 1 type", "Arg 1 name", "Arg 2 type", "Arg 2 name").
|
|
46
|
+
* StateMachineTab: QSplitter(Qt.Horizontal).
|
|
47
|
+
* apply_changes() carries reserved values forward.
|
|
48
|
+
|
|
49
|
+
[v3.6 behavior retained]
|
|
50
|
+
* SettingsPanel: entry / exit columns are non-editable
|
|
51
|
+
inline (edited via ActionEditDialog).
|
|
52
|
+
[v3.5 behavior retained]
|
|
53
|
+
* MermaidWidget: QScrollArea alignment VCenter.
|
|
54
|
+
[v3.4 behavior retained]
|
|
55
|
+
* getSvgSize() returns a JSON string.
|
|
56
|
+
[v3.3 behavior retained]
|
|
57
|
+
* QScrollArea viewport background #fafafa.
|
|
58
|
+
[v3.2 behavior retained]
|
|
59
|
+
* Async render; JS console forwarded to StaTableLogger.
|
|
60
|
+
[v3.0] MAX_SVG_WIDTH 1200 -> 3000.
|
|
61
|
+
[v2.8] Persistent debug copy retained.
|
|
62
|
+
[v2.2] entry / exit are List[str]; "; " separator.
|
|
63
|
+
|
|
64
|
+
[v2.3 change]
|
|
65
|
+
- StateMachineTab: dataModified signal relays child modifications
|
|
66
|
+
to MainWindow (for the [*] window-modified marker).
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
import os
|
|
70
|
+
import json
|
|
71
|
+
import shutil
|
|
72
|
+
import tempfile
|
|
73
|
+
from typing import Optional, List, Callable
|
|
74
|
+
|
|
75
|
+
from PySide6.QtCore import Qt, Signal, QUrl, QTimer
|
|
76
|
+
from PySide6.QtGui import QFont, QKeyEvent, QPalette, QColor
|
|
77
|
+
from PySide6.QtWidgets import (
|
|
78
|
+
QWidget, QVBoxLayout, QHBoxLayout, QTableWidget, QTableWidgetItem,
|
|
79
|
+
QPushButton, QPlainTextEdit, QSplitter, QLabel, QHeaderView,
|
|
80
|
+
QTabWidget, QAbstractItemView, QMessageBox, QDialog, QScrollArea,
|
|
81
|
+
QComboBox, QStyledItemDelegate
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
# ============================================================
|
|
85
|
+
# A1: branch WebEngine import itself by environment variable
|
|
86
|
+
# ============================================================
|
|
87
|
+
_DISABLE_MERMAID = os.environ.get("STATABLE_DISABLE_MERMAID") == "1"
|
|
88
|
+
|
|
89
|
+
if not _DISABLE_MERMAID:
|
|
90
|
+
try:
|
|
91
|
+
from PySide6.QtWebEngineWidgets import QWebEngineView
|
|
92
|
+
from PySide6.QtWebEngineCore import (
|
|
93
|
+
QWebEngineSettings, QWebEnginePage)
|
|
94
|
+
WEBENGINE_AVAILABLE = True
|
|
95
|
+
except ImportError:
|
|
96
|
+
WEBENGINE_AVAILABLE = False
|
|
97
|
+
QWebEngineView = None
|
|
98
|
+
QWebEngineSettings = None
|
|
99
|
+
QWebEnginePage = None
|
|
100
|
+
else:
|
|
101
|
+
WEBENGINE_AVAILABLE = False
|
|
102
|
+
QWebEngineView = None
|
|
103
|
+
QWebEngineSettings = None
|
|
104
|
+
QWebEnginePage = None
|
|
105
|
+
|
|
106
|
+
from statable.model import State, Event, Transition, StateType, EventKind, RoleFunction, ActionStep
|
|
107
|
+
from statable.state_machine import StateMachine
|
|
108
|
+
from statable.mermaid_gen import generate_mermaid
|
|
109
|
+
|
|
110
|
+
from .logger import StaTableLogger
|
|
111
|
+
from .config import (
|
|
112
|
+
get_resource_path, TABLE_PREVIEW_RATIO, WINDOW_HEIGHT,
|
|
113
|
+
MERMAID_PREVIEW_MIN_HEIGHT, MAX_COLUMN_WIDTH, MIN_ROW_HEIGHT, MAX_ROW_HEIGHT
|
|
114
|
+
)
|
|
115
|
+
from .matrix_table import MatrixTableWidget
|
|
116
|
+
from .role_function_dialog import RoleFunctionDialog
|
|
117
|
+
from .action_edit_dialog import ActionEditDialog
|
|
118
|
+
from .state_actions_dialog import StateActionsDialog
|
|
119
|
+
from .global_defs import GlobalDefinitions
|
|
120
|
+
from .event_definition_dialog import EventDefinitionDialog
|
|
121
|
+
|
|
122
|
+
from statable_gui.libcntrl.role_function_library import RoleFunctionLibrary
|
|
123
|
+
from statable_gui.libcntrl.condition_library import ConditionLibrary
|
|
124
|
+
from statable_gui.libcntrl.literal_library import LiteralLibrary
|
|
125
|
+
|
|
126
|
+
# ======================================================================
|
|
127
|
+
# v2.2: List[str] <-> display helpers for entry / exit
|
|
128
|
+
# ======================================================================
|
|
129
|
+
_ENTRY_EXIT_SEP = "; "
|
|
130
|
+
_MERMAID_BG = "#fafafa"
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _extract_action_name(action) -> str:
|
|
134
|
+
"""[v2.7.0] Extract display name from ActionStep or str.
|
|
135
|
+
|
|
136
|
+
For ActionStep:
|
|
137
|
+
- action_type="role" -> role_function
|
|
138
|
+
- action_type="fire_event" -> event_name
|
|
139
|
+
- custom -> ""
|
|
140
|
+
"""
|
|
141
|
+
if action is None:
|
|
142
|
+
return ""
|
|
143
|
+
if isinstance(action, str):
|
|
144
|
+
return action.strip()
|
|
145
|
+
atype = getattr(action, "action_type", "role") or "role"
|
|
146
|
+
if atype == "fire_event":
|
|
147
|
+
return (getattr(action, "event_name", "") or "").strip()
|
|
148
|
+
if atype == "custom":
|
|
149
|
+
return ""
|
|
150
|
+
return (getattr(action, "role_function", "") or "").strip()
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _list_to_display(items) -> str:
|
|
154
|
+
"""[v2.7.0] Convert List[ActionStep] (or List[str]) to display string."""
|
|
155
|
+
if items is None:
|
|
156
|
+
return ""
|
|
157
|
+
if isinstance(items, str):
|
|
158
|
+
return items
|
|
159
|
+
if isinstance(items, list):
|
|
160
|
+
names = [_extract_action_name(x) for x in items]
|
|
161
|
+
return _ENTRY_EXIT_SEP.join(n for n in names if n)
|
|
162
|
+
return str(items)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _display_to_list(text) -> List[ActionStep]:
|
|
166
|
+
"""[v2.7.0] Parse a display string ('A; B; C') into List[ActionStep].
|
|
167
|
+
|
|
168
|
+
Legacy List[str] input is also accepted and converted.
|
|
169
|
+
"""
|
|
170
|
+
if not text:
|
|
171
|
+
return []
|
|
172
|
+
if isinstance(text, list):
|
|
173
|
+
result = []
|
|
174
|
+
for x in text:
|
|
175
|
+
if isinstance(x, ActionStep):
|
|
176
|
+
result.append(x)
|
|
177
|
+
elif isinstance(x, str) and x.strip():
|
|
178
|
+
result.append(ActionStep(role_function=x.strip()))
|
|
179
|
+
return result
|
|
180
|
+
parts = str(text).split(';')
|
|
181
|
+
return [ActionStep(role_function=p.strip())
|
|
182
|
+
for p in parts if p.strip()]
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
# ======================================================================
|
|
186
|
+
# [v3.2-debug] Custom page to forward JS console to StaTableLogger
|
|
187
|
+
# ======================================================================
|
|
188
|
+
if WEBENGINE_AVAILABLE:
|
|
189
|
+
class _DebugWebEnginePage(QWebEnginePage):
|
|
190
|
+
"""Forward JS console messages to StaTableLogger."""
|
|
191
|
+
|
|
192
|
+
def javaScriptConsoleMessage(self, level, message, line, source):
|
|
193
|
+
try:
|
|
194
|
+
lvl_name = {
|
|
195
|
+
0: "INFO",
|
|
196
|
+
1: "WARN",
|
|
197
|
+
2: "ERROR",
|
|
198
|
+
}.get(int(level), str(level))
|
|
199
|
+
except (TypeError, ValueError):
|
|
200
|
+
lvl_name = str(level)
|
|
201
|
+
StaTableLogger.debug(
|
|
202
|
+
f"[JSConsole:{lvl_name}] {message} "
|
|
203
|
+
f"(line {line}, {os.path.basename(source)})")
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
# ======================================================================
|
|
207
|
+
# [v3.10] Namespace column delegate (editable QComboBox)
|
|
208
|
+
# ======================================================================
|
|
209
|
+
class _NamespaceDelegate(QStyledItemDelegate):
|
|
210
|
+
"""Inline editor for the Role-function Namespace column.
|
|
211
|
+
|
|
212
|
+
[v3.10]
|
|
213
|
+
Renders an editable QComboBox populated with the current
|
|
214
|
+
namespace candidates (supplied by `choices_provider`). The
|
|
215
|
+
user can either pick a candidate or type a custom value.
|
|
216
|
+
|
|
217
|
+
[v3.11]
|
|
218
|
+
`choices_provider` is re-evaluated on every editor creation,
|
|
219
|
+
so newly added layers / namespaces appear immediately.
|
|
220
|
+
"""
|
|
221
|
+
|
|
222
|
+
def __init__(self, choices_provider, parent=None):
|
|
223
|
+
super().__init__(parent)
|
|
224
|
+
self._choices_provider = choices_provider
|
|
225
|
+
|
|
226
|
+
def createEditor(self, parent, option, index):
|
|
227
|
+
combo = QComboBox(parent)
|
|
228
|
+
combo.setEditable(True)
|
|
229
|
+
combo.setInsertPolicy(QComboBox.NoInsert)
|
|
230
|
+
# First entry: empty namespace (no layer)
|
|
231
|
+
combo.addItem(self.tr(""))
|
|
232
|
+
try:
|
|
233
|
+
for ns in (self._choices_provider() or []):
|
|
234
|
+
ns = (ns or "").strip()
|
|
235
|
+
if ns and combo.findText(ns) < 0:
|
|
236
|
+
combo.addItem(ns)
|
|
237
|
+
except Exception as e:
|
|
238
|
+
StaTableLogger.warning(
|
|
239
|
+
f"_NamespaceDelegate.choices_provider failed: {e}")
|
|
240
|
+
return combo
|
|
241
|
+
|
|
242
|
+
def setEditorData(self, editor, index):
|
|
243
|
+
value = index.data(Qt.EditRole) or ""
|
|
244
|
+
value = str(value)
|
|
245
|
+
idx = editor.findText(value)
|
|
246
|
+
if idx >= 0:
|
|
247
|
+
editor.setCurrentIndex(idx)
|
|
248
|
+
else:
|
|
249
|
+
editor.setEditText(value)
|
|
250
|
+
|
|
251
|
+
def setModelData(self, editor, model, index):
|
|
252
|
+
model.setData(index, editor.currentText().strip(), Qt.EditRole)
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
class MermaidWidget(QWidget):
|
|
256
|
+
"""Mermaid diagram preview widget.
|
|
257
|
+
|
|
258
|
+
[v3.5] Diagram vertically centered in the viewport.
|
|
259
|
+
QScrollArea alignment = Qt.AlignLeft | Qt.AlignVCenter.
|
|
260
|
+
|
|
261
|
+
[v3.4] getSvgSize() returns a JSON string.
|
|
262
|
+
|
|
263
|
+
[v3.3] QScrollArea viewport background set to _MERMAID_BG.
|
|
264
|
+
|
|
265
|
+
[v3.2] Async render (mermaid.init/run + polling) and
|
|
266
|
+
retry-based sizing; JS console forwarded to StaTableLogger.
|
|
267
|
+
|
|
268
|
+
[v3.0] MAX_SVG_WIDTH raised 1200 -> 3000.
|
|
269
|
+
|
|
270
|
+
[v2.8] Persistent debug copy of the generated HTML is written to
|
|
271
|
+
%USERPROFILE%\\statable_mermaid_debug.html
|
|
272
|
+
"""
|
|
273
|
+
|
|
274
|
+
MAX_SVG_WIDTH = 3000
|
|
275
|
+
MAX_CONTENT_W = 20000
|
|
276
|
+
MAX_CONTENT_H = 20000
|
|
277
|
+
SIZE_RETRY_MAX = 5
|
|
278
|
+
SIZE_RETRY_DELAY_MS = 200
|
|
279
|
+
|
|
280
|
+
def __init__(self, parent=None):
|
|
281
|
+
super().__init__(parent)
|
|
282
|
+
layout = QVBoxLayout(self)
|
|
283
|
+
layout.setContentsMargins(0, 0, 0, 0)
|
|
284
|
+
layout.setSpacing(0)
|
|
285
|
+
|
|
286
|
+
self.web_view = None
|
|
287
|
+
self.text_view = None
|
|
288
|
+
self.scroll_area = None
|
|
289
|
+
self._disabled = (
|
|
290
|
+
os.environ.get("STATABLE_DISABLE_MERMAID") == "1"
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
StaTableLogger.debug(
|
|
294
|
+
f"MermaidWidget.__init__: MAX_SVG_WIDTH={self.MAX_SVG_WIDTH}, "
|
|
295
|
+
f"disabled={self._disabled}, webengine_available={WEBENGINE_AVAILABLE}"
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
if self._disabled:
|
|
299
|
+
placeholder = QLabel(self.tr("Mermaid rendering disabled (test mode)"))
|
|
300
|
+
placeholder.setAlignment(Qt.AlignCenter)
|
|
301
|
+
layout.addWidget(placeholder)
|
|
302
|
+
StaTableLogger.debug(
|
|
303
|
+
"MermaidWidget: disabled via STATABLE_DISABLE_MERMAID")
|
|
304
|
+
return
|
|
305
|
+
|
|
306
|
+
if WEBENGINE_AVAILABLE:
|
|
307
|
+
self.scroll_area = QScrollArea()
|
|
308
|
+
self.scroll_area.setWidgetResizable(False)
|
|
309
|
+
self.scroll_area.setAlignment(Qt.AlignLeft | Qt.AlignVCenter)
|
|
310
|
+
self.scroll_area.setHorizontalScrollBarPolicy(
|
|
311
|
+
Qt.ScrollBarAsNeeded)
|
|
312
|
+
self.scroll_area.setVerticalScrollBarPolicy(
|
|
313
|
+
Qt.ScrollBarAsNeeded)
|
|
314
|
+
|
|
315
|
+
self.scroll_area.viewport().setAutoFillBackground(True)
|
|
316
|
+
pal = self.scroll_area.viewport().palette()
|
|
317
|
+
pal.setColor(QPalette.Window, QColor(_MERMAID_BG))
|
|
318
|
+
self.scroll_area.viewport().setPalette(pal)
|
|
319
|
+
|
|
320
|
+
self.web_view = QWebEngineView()
|
|
321
|
+
|
|
322
|
+
try:
|
|
323
|
+
self.web_view.setPage(_DebugWebEnginePage(self.web_view))
|
|
324
|
+
StaTableLogger.debug(
|
|
325
|
+
"MermaidWidget: _DebugWebEnginePage attached")
|
|
326
|
+
except Exception as e:
|
|
327
|
+
StaTableLogger.warning(
|
|
328
|
+
f"Failed to attach debug page: {e}")
|
|
329
|
+
|
|
330
|
+
self.web_view.setFixedSize(100, 100)
|
|
331
|
+
|
|
332
|
+
try:
|
|
333
|
+
self.web_view.setZoomFactor(1.0)
|
|
334
|
+
except Exception as e:
|
|
335
|
+
StaTableLogger.warning(f"setZoomFactor failed: {e}")
|
|
336
|
+
|
|
337
|
+
settings = self.web_view.settings()
|
|
338
|
+
settings.setAttribute(
|
|
339
|
+
QWebEngineSettings.LocalContentCanAccessFileUrls, True)
|
|
340
|
+
settings.setAttribute(
|
|
341
|
+
QWebEngineSettings.LocalContentCanAccessRemoteUrls, True)
|
|
342
|
+
self.web_view.loadFinished.connect(self._on_load_finished)
|
|
343
|
+
|
|
344
|
+
self.scroll_area.setWidget(self.web_view)
|
|
345
|
+
layout.addWidget(self.scroll_area)
|
|
346
|
+
else:
|
|
347
|
+
self.text_view = QPlainTextEdit()
|
|
348
|
+
self.text_view.setReadOnly(True)
|
|
349
|
+
layout.addWidget(self.text_view)
|
|
350
|
+
self.text_view.setPlainText(
|
|
351
|
+
"QWebEngineView is not available.\n"
|
|
352
|
+
"Please install PySide6-Addons.\n"
|
|
353
|
+
"pip install PySide6-Addons"
|
|
354
|
+
)
|
|
355
|
+
StaTableLogger.warning("MermaidWidget: WebEngine NOT available")
|
|
356
|
+
|
|
357
|
+
def set_mermaid_code(self, code: str):
|
|
358
|
+
StaTableLogger.debug("MermaidWidget.set_mermaid_code called")
|
|
359
|
+
if self._disabled:
|
|
360
|
+
return
|
|
361
|
+
|
|
362
|
+
if self.web_view:
|
|
363
|
+
mermaid_js_path = get_resource_path("mermaidwin.js")
|
|
364
|
+
if not mermaid_js_path.exists():
|
|
365
|
+
StaTableLogger.error(
|
|
366
|
+
f"mermaidwin.js NOT found: {mermaid_js_path}")
|
|
367
|
+
return
|
|
368
|
+
|
|
369
|
+
js_abs_url = QUrl.fromLocalFile(str(mermaid_js_path)).toString()
|
|
370
|
+
|
|
371
|
+
html = f"""<!DOCTYPE html>
|
|
372
|
+
<html>
|
|
373
|
+
<head>
|
|
374
|
+
<meta charset="utf-8">
|
|
375
|
+
<meta name="viewport"
|
|
376
|
+
content="width=device-width, initial-scale=1.0">
|
|
377
|
+
<style>
|
|
378
|
+
html, body {{
|
|
379
|
+
margin: 0;
|
|
380
|
+
padding: 0;
|
|
381
|
+
overflow: hidden;
|
|
382
|
+
background-color: {_MERMAID_BG};
|
|
383
|
+
}}
|
|
384
|
+
.mermaid {{
|
|
385
|
+
padding: 20px;
|
|
386
|
+
margin: 0;
|
|
387
|
+
background-color: transparent;
|
|
388
|
+
display: inline-block;
|
|
389
|
+
width: max-content;
|
|
390
|
+
}}
|
|
391
|
+
</style>
|
|
392
|
+
<script src="{js_abs_url}"></script>
|
|
393
|
+
<script>
|
|
394
|
+
var MAX_SVG_WIDTH = {self.MAX_SVG_WIDTH};
|
|
395
|
+
console.log('[MermaidDebug] script loaded');
|
|
396
|
+
console.log('[MermaidDebug] MAX_SVG_WIDTH =', MAX_SVG_WIDTH);
|
|
397
|
+
console.log('[MermaidDebug] typeof mermaid =', typeof mermaid);
|
|
398
|
+
if (typeof mermaid !== 'undefined') {{
|
|
399
|
+
console.log('[MermaidDebug] mermaid.run type =',
|
|
400
|
+
typeof mermaid.run);
|
|
401
|
+
console.log('[MermaidDebug] mermaid.init type =',
|
|
402
|
+
typeof mermaid.init);
|
|
403
|
+
console.log('[MermaidDebug] mermaid.initialize type =',
|
|
404
|
+
typeof mermaid.initialize);
|
|
405
|
+
}}
|
|
406
|
+
|
|
407
|
+
try {{
|
|
408
|
+
mermaid.initialize({{
|
|
409
|
+
startOnLoad: false,
|
|
410
|
+
theme: 'default',
|
|
411
|
+
themeVariables: {{
|
|
412
|
+
fontSize: '16px',
|
|
413
|
+
fontFamily: 'Segoe UI, Yu Gothic UI, sans-serif',
|
|
414
|
+
}},
|
|
415
|
+
'stateDiagram-v2': {{
|
|
416
|
+
nodeSpacing: 50,
|
|
417
|
+
rankSpacing: 25,
|
|
418
|
+
padding: 8,
|
|
419
|
+
useMaxWidth: false,
|
|
420
|
+
}},
|
|
421
|
+
stateDiagram: {{
|
|
422
|
+
nodeSpacing: 50,
|
|
423
|
+
rankSpacing: 25,
|
|
424
|
+
padding: 8,
|
|
425
|
+
useMaxWidth: false,
|
|
426
|
+
}},
|
|
427
|
+
}});
|
|
428
|
+
console.log('[MermaidDebug] mermaid.initialize done');
|
|
429
|
+
}} catch (e) {{
|
|
430
|
+
console.error('[MermaidDebug] mermaid.initialize failed:', e);
|
|
431
|
+
}}
|
|
432
|
+
|
|
433
|
+
function waitForSvg(timeoutMs) {{
|
|
434
|
+
return new Promise(function(resolve) {{
|
|
435
|
+
var start = Date.now();
|
|
436
|
+
(function poll() {{
|
|
437
|
+
var svg = document.querySelector('.mermaid svg');
|
|
438
|
+
if (svg && svg.getAttribute('viewBox')) {{
|
|
439
|
+
resolve(true);
|
|
440
|
+
return;
|
|
441
|
+
}}
|
|
442
|
+
if (Date.now() - start > timeoutMs) {{
|
|
443
|
+
resolve(false);
|
|
444
|
+
return;
|
|
445
|
+
}}
|
|
446
|
+
setTimeout(poll, 50);
|
|
447
|
+
}})();
|
|
448
|
+
}});
|
|
449
|
+
}}
|
|
450
|
+
|
|
451
|
+
async function renderMermaid() {{
|
|
452
|
+
console.log('[MermaidDebug] renderMermaid: ENTER');
|
|
453
|
+
|
|
454
|
+
var nodes = document.querySelectorAll('.mermaid');
|
|
455
|
+
console.log('[MermaidDebug] .mermaid node count =', nodes.length);
|
|
456
|
+
if (!nodes.length) {{
|
|
457
|
+
console.warn('[MermaidDebug] no .mermaid nodes; abort');
|
|
458
|
+
return 'no-nodes';
|
|
459
|
+
}}
|
|
460
|
+
|
|
461
|
+
try {{
|
|
462
|
+
var promise = null;
|
|
463
|
+
if (typeof mermaid.run === 'function') {{
|
|
464
|
+
console.log('[MermaidDebug] branch: mermaid.run');
|
|
465
|
+
promise = mermaid.run({{ nodes: nodes }});
|
|
466
|
+
}} else if (typeof mermaid.init === 'function') {{
|
|
467
|
+
console.log('[MermaidDebug] branch: mermaid.init');
|
|
468
|
+
promise = mermaid.init(undefined, nodes);
|
|
469
|
+
}} else {{
|
|
470
|
+
console.warn('[MermaidDebug] no run/init available');
|
|
471
|
+
}}
|
|
472
|
+
|
|
473
|
+
if (promise && typeof promise.then === 'function') {{
|
|
474
|
+
console.log('[MermaidDebug] awaiting promise');
|
|
475
|
+
await promise;
|
|
476
|
+
console.log('[MermaidDebug] promise resolved');
|
|
477
|
+
}} else {{
|
|
478
|
+
console.log(
|
|
479
|
+
'[MermaidDebug] no promise; polling for SVG');
|
|
480
|
+
var ok = await waitForSvg(3000);
|
|
481
|
+
console.log('[MermaidDebug] waitForSvg =', ok);
|
|
482
|
+
}}
|
|
483
|
+
}} catch (e) {{
|
|
484
|
+
console.error('[MermaidDebug] render failed:', e);
|
|
485
|
+
console.error('[MermaidDebug] stack:', e && e.stack);
|
|
486
|
+
return 'render-failed';
|
|
487
|
+
}}
|
|
488
|
+
|
|
489
|
+
var svgs = document.querySelectorAll('.mermaid svg');
|
|
490
|
+
console.log(
|
|
491
|
+
'[MermaidDebug] SVG count after render =', svgs.length);
|
|
492
|
+
|
|
493
|
+
svgs.forEach(function(svg, i) {{
|
|
494
|
+
var vb = svg.getAttribute('viewBox');
|
|
495
|
+
console.log('[MermaidDebug] svg[' + i + '] viewBox =', vb);
|
|
496
|
+
if (!vb) return;
|
|
497
|
+
var parts = vb.split(/\\s+/);
|
|
498
|
+
if (parts.length !== 4) return;
|
|
499
|
+
var w = parseFloat(parts[2]);
|
|
500
|
+
var h = parseFloat(parts[3]);
|
|
501
|
+
if (!w || !h) return;
|
|
502
|
+
var scale = 1.0;
|
|
503
|
+
if (w > MAX_SVG_WIDTH) scale = MAX_SVG_WIDTH / w;
|
|
504
|
+
var newW = Math.round(w * scale);
|
|
505
|
+
var newH = Math.round(h * scale);
|
|
506
|
+
svg.removeAttribute('style');
|
|
507
|
+
svg.setAttribute('width', newW);
|
|
508
|
+
svg.setAttribute('height', newH);
|
|
509
|
+
console.log('[MermaidDebug] svg[' + i +
|
|
510
|
+
'] forced ' + newW + 'x' + newH +
|
|
511
|
+
' (natural ' + w + 'x' + h + ')');
|
|
512
|
+
}});
|
|
513
|
+
|
|
514
|
+
console.log('[MermaidDebug] renderMermaid: EXIT');
|
|
515
|
+
return 'ok';
|
|
516
|
+
}}
|
|
517
|
+
|
|
518
|
+
/* [v3.4] Returns a JSON string instead of a bare array.
|
|
519
|
+
PySide6's runJavaScript callback sometimes converts JS
|
|
520
|
+
arrays to '' at the Qt boundary; a string is reliable. */
|
|
521
|
+
function getSvgSize() {{
|
|
522
|
+
console.log('[MermaidDebug] getSvgSize: ENTER');
|
|
523
|
+
var svg = document.querySelector('.mermaid svg');
|
|
524
|
+
console.log('[MermaidDebug] getSvgSize: svg exists =', !!svg);
|
|
525
|
+
if (!svg) return JSON.stringify([0, 0]);
|
|
526
|
+
|
|
527
|
+
var w = parseFloat(svg.getAttribute('width'));
|
|
528
|
+
var h = parseFloat(svg.getAttribute('height'));
|
|
529
|
+
console.log('[MermaidDebug] getSvgSize: raw width/height =',
|
|
530
|
+
w, h);
|
|
531
|
+
|
|
532
|
+
if (!w || !h) {{
|
|
533
|
+
var vb = svg.getAttribute('viewBox');
|
|
534
|
+
console.log('[MermaidDebug] getSvgSize: viewBox =', vb);
|
|
535
|
+
if (vb) {{
|
|
536
|
+
var parts = vb.split(/\\s+/);
|
|
537
|
+
if (parts.length === 4) {{
|
|
538
|
+
var nw = parseFloat(parts[2]);
|
|
539
|
+
var nh = parseFloat(parts[3]);
|
|
540
|
+
var sc = 1.0;
|
|
541
|
+
if (nw > MAX_SVG_WIDTH) sc = MAX_SVG_WIDTH / nw;
|
|
542
|
+
w = Math.round(nw * sc);
|
|
543
|
+
h = Math.round(nh * sc);
|
|
544
|
+
}}
|
|
545
|
+
}}
|
|
546
|
+
}}
|
|
547
|
+
|
|
548
|
+
if (!w || !h) {{
|
|
549
|
+
console.log('[MermaidDebug] getSvgSize: returning [0,0]');
|
|
550
|
+
return JSON.stringify([0, 0]);
|
|
551
|
+
}}
|
|
552
|
+
|
|
553
|
+
var pad = 40;
|
|
554
|
+
var result = [Math.round(w + pad), Math.round(h + pad)];
|
|
555
|
+
var jsonStr = JSON.stringify(result);
|
|
556
|
+
console.log('[MermaidDebug] getSvgSize: returning ' + jsonStr);
|
|
557
|
+
return jsonStr;
|
|
558
|
+
}}
|
|
559
|
+
</script>
|
|
560
|
+
</head>
|
|
561
|
+
<body>
|
|
562
|
+
<pre class="mermaid">
|
|
563
|
+
{code}
|
|
564
|
+
</pre>
|
|
565
|
+
<script>
|
|
566
|
+
console.log('[MermaidDebug] body loaded; .mermaid count =',
|
|
567
|
+
document.querySelectorAll('.mermaid').length);
|
|
568
|
+
</script>
|
|
569
|
+
</body>
|
|
570
|
+
</html>"""
|
|
571
|
+
|
|
572
|
+
try:
|
|
573
|
+
with tempfile.NamedTemporaryFile(
|
|
574
|
+
mode='w', suffix='.html',
|
|
575
|
+
delete=False, encoding='utf-8') as f:
|
|
576
|
+
f.write(html)
|
|
577
|
+
temp_path = f.name
|
|
578
|
+
StaTableLogger.debug(
|
|
579
|
+
f"MermaidWidget: temp HTML written: {temp_path}")
|
|
580
|
+
|
|
581
|
+
try:
|
|
582
|
+
debug_path = os.path.join(
|
|
583
|
+
os.path.expanduser("~"),
|
|
584
|
+
"statable_mermaid_debug.html")
|
|
585
|
+
shutil.copy(temp_path, debug_path)
|
|
586
|
+
StaTableLogger.info(
|
|
587
|
+
f"[DEBUG] Mermaid HTML saved to: {debug_path}")
|
|
588
|
+
except Exception as e:
|
|
589
|
+
StaTableLogger.warning(
|
|
590
|
+
f"[DEBUG] failed to save debug HTML: {e}")
|
|
591
|
+
|
|
592
|
+
self.web_view.load(QUrl.fromLocalFile(temp_path))
|
|
593
|
+
except Exception as e:
|
|
594
|
+
StaTableLogger.error(
|
|
595
|
+
f"Failed to create temporary HTML: {e}")
|
|
596
|
+
elif self.text_view:
|
|
597
|
+
self.text_view.setPlainText(code)
|
|
598
|
+
|
|
599
|
+
def _on_load_finished(self, ok: bool):
|
|
600
|
+
StaTableLogger.debug(
|
|
601
|
+
f"MermaidWidget._on_load_finished: ok={ok}")
|
|
602
|
+
if ok and self.web_view:
|
|
603
|
+
self.web_view.page().runJavaScript(
|
|
604
|
+
"renderMermaid();",
|
|
605
|
+
lambda r: StaTableLogger.debug(
|
|
606
|
+
f"renderMermaid() returned: {r!r}"),
|
|
607
|
+
)
|
|
608
|
+
QTimer.singleShot(
|
|
609
|
+
self.SIZE_RETRY_DELAY_MS,
|
|
610
|
+
lambda: self._request_svg_size(retry=0),
|
|
611
|
+
)
|
|
612
|
+
elif not ok:
|
|
613
|
+
StaTableLogger.warning(
|
|
614
|
+
"MermaidWidget: WebEngine load FAILED")
|
|
615
|
+
|
|
616
|
+
def _request_svg_size(self, retry: int = 0):
|
|
617
|
+
StaTableLogger.debug(
|
|
618
|
+
f"MermaidWidget._request_svg_size: retry={retry}")
|
|
619
|
+
if not self.web_view:
|
|
620
|
+
return
|
|
621
|
+
self.web_view.page().runJavaScript(
|
|
622
|
+
"getSvgSize();",
|
|
623
|
+
lambda result: self._apply_svg_size(result, retry),
|
|
624
|
+
)
|
|
625
|
+
|
|
626
|
+
def _apply_svg_size(self, result, retry: int = 0):
|
|
627
|
+
StaTableLogger.debug(
|
|
628
|
+
f"MermaidWidget._apply_svg_size: retry={retry}, "
|
|
629
|
+
f"result={result!r}, type={type(result).__name__}")
|
|
630
|
+
|
|
631
|
+
values = None
|
|
632
|
+
if isinstance(result, (list, tuple)) and len(result) >= 2:
|
|
633
|
+
values = result
|
|
634
|
+
elif isinstance(result, str):
|
|
635
|
+
s = result.strip()
|
|
636
|
+
if s:
|
|
637
|
+
try:
|
|
638
|
+
parsed = json.loads(s)
|
|
639
|
+
if isinstance(parsed, (list, tuple)) and len(parsed) >= 2:
|
|
640
|
+
values = parsed
|
|
641
|
+
except (json.JSONDecodeError, ValueError):
|
|
642
|
+
pass
|
|
643
|
+
|
|
644
|
+
valid = False
|
|
645
|
+
w = h = 0
|
|
646
|
+
if values is not None:
|
|
647
|
+
try:
|
|
648
|
+
w = int(values[0])
|
|
649
|
+
h = int(values[1])
|
|
650
|
+
if w > 0 and h > 0:
|
|
651
|
+
valid = True
|
|
652
|
+
except (ValueError, TypeError):
|
|
653
|
+
pass
|
|
654
|
+
|
|
655
|
+
if not valid:
|
|
656
|
+
if retry < self.SIZE_RETRY_MAX:
|
|
657
|
+
next_retry = retry + 1
|
|
658
|
+
QTimer.singleShot(
|
|
659
|
+
self.SIZE_RETRY_DELAY_MS,
|
|
660
|
+
lambda: self._request_svg_size(retry=next_retry),
|
|
661
|
+
)
|
|
662
|
+
else:
|
|
663
|
+
StaTableLogger.warning(
|
|
664
|
+
f"MermaidWidget: size query failed after "
|
|
665
|
+
f"{self.SIZE_RETRY_MAX} retries; keeping current size")
|
|
666
|
+
return
|
|
667
|
+
|
|
668
|
+
w = min(w, self.MAX_CONTENT_W)
|
|
669
|
+
h = min(h, self.MAX_CONTENT_H)
|
|
670
|
+
self.web_view.setFixedSize(w, h)
|
|
671
|
+
|
|
672
|
+
|
|
673
|
+
class SettingsPanel(QWidget):
|
|
674
|
+
"""State / role function settings panel.
|
|
675
|
+
|
|
676
|
+
[C-38 fix]
|
|
677
|
+
add_state() now emits settings_changed so that the parent
|
|
678
|
+
StateMachineTab / MainWindow updated the window's modified
|
|
679
|
+
flag when a state is added.
|
|
680
|
+
|
|
681
|
+
[v3.11]
|
|
682
|
+
Accepts `layer_names_provider`, a zero-argument callable that
|
|
683
|
+
returns all tab names (all layer names in the project). It is
|
|
684
|
+
used by _get_namespace_choices() to populate the namespace
|
|
685
|
+
combo box with every layer, not just the current one.
|
|
686
|
+
|
|
687
|
+
[v3.10]
|
|
688
|
+
Role function table: only the Namespace column is editable
|
|
689
|
+
inline (via _NamespaceDelegate).
|
|
690
|
+
|
|
691
|
+
[v3.9]
|
|
692
|
+
Receives role_function_library and literal_library.
|
|
693
|
+
|
|
694
|
+
[v3.8]
|
|
695
|
+
"Edit" button + row double-click for role functions.
|
|
696
|
+
|
|
697
|
+
[v3.7]
|
|
698
|
+
State list: 5 columns. Role function: 4 columns.
|
|
699
|
+
Reserved fields carried forward in apply_changes().
|
|
700
|
+
"""
|
|
701
|
+
settings_changed = Signal()
|
|
702
|
+
|
|
703
|
+
# [v2.7.1] State list is now 3 columns: 0=Name, 1=Description, 2=Type.
|
|
704
|
+
# Double-click on Name opens StateActionsDialog; Description / Type
|
|
705
|
+
# are edited inline.
|
|
706
|
+
STATE_COL_NAME = 0
|
|
707
|
+
STATE_COL_DESC = 1
|
|
708
|
+
STATE_COL_TYPE = 2
|
|
709
|
+
|
|
710
|
+
# [v3.10] Role-function table layout: 0=Title, 1=Function name,
|
|
711
|
+
# 2=Namespace, 3=Description. Only column 2 is editable.
|
|
712
|
+
ROLE_NAMESPACE_COL = 2
|
|
713
|
+
|
|
714
|
+
def __init__(self, sm: StateMachine,
|
|
715
|
+
global_defs: GlobalDefinitions = None,
|
|
716
|
+
literal_library: LiteralLibrary = None,
|
|
717
|
+
role_function_library: RoleFunctionLibrary = None,
|
|
718
|
+
layer_names_provider: Callable[[], List[str]] = None,
|
|
719
|
+
parent=None):
|
|
720
|
+
super().__init__(parent)
|
|
721
|
+
self.sm = sm
|
|
722
|
+
self.global_defs = global_defs if global_defs else GlobalDefinitions()
|
|
723
|
+
self.literal_library = literal_library
|
|
724
|
+
self.role_function_library = role_function_library
|
|
725
|
+
# [v3.11] Callable returning all tab names (= all layer names)
|
|
726
|
+
self.layer_names_provider = layer_names_provider
|
|
727
|
+
self._updating = False
|
|
728
|
+
|
|
729
|
+
self._debounce_timer = QTimer()
|
|
730
|
+
self._debounce_timer.setSingleShot(True)
|
|
731
|
+
self._debounce_timer.setInterval(300)
|
|
732
|
+
self._debounce_timer.timeout.connect(self._emit_settings_changed)
|
|
733
|
+
|
|
734
|
+
layout = QVBoxLayout(self)
|
|
735
|
+
self.tab = QTabWidget()
|
|
736
|
+
layout.addWidget(self.tab)
|
|
737
|
+
|
|
738
|
+
# ---- State tab ----
|
|
739
|
+
state_tab = QWidget()
|
|
740
|
+
state_layout = QVBoxLayout(state_tab)
|
|
741
|
+
# [v2.7.1] 3 columns: Name / Description / Type.
|
|
742
|
+
# entry / exit are edited only via StateActionsDialog.
|
|
743
|
+
self.state_table = QTableWidget(0, 3)
|
|
744
|
+
self.state_table.setHorizontalHeaderLabels([
|
|
745
|
+
"Name", "Description", "Type"
|
|
746
|
+
])
|
|
747
|
+
self.state_table.horizontalHeader().setSectionResizeMode(
|
|
748
|
+
QHeaderView.Stretch)
|
|
749
|
+
self.state_table.setFont(QFont("Consolas", 10))
|
|
750
|
+
state_layout.addWidget(self.state_table)
|
|
751
|
+
btn_state = QHBoxLayout()
|
|
752
|
+
add_state_btn = QPushButton(self.tr("Add"))
|
|
753
|
+
add_state_btn.clicked.connect(self.add_state)
|
|
754
|
+
del_state_btn = QPushButton(self.tr("Delete"))
|
|
755
|
+
del_state_btn.clicked.connect(self.delete_state)
|
|
756
|
+
btn_state.addWidget(add_state_btn)
|
|
757
|
+
btn_state.addWidget(del_state_btn)
|
|
758
|
+
state_layout.addLayout(btn_state)
|
|
759
|
+
self.tab.addTab(state_tab, self.tr("State list"))
|
|
760
|
+
|
|
761
|
+
# ---- Role function tab ----
|
|
762
|
+
role_tab = QWidget()
|
|
763
|
+
role_layout = QVBoxLayout(role_tab)
|
|
764
|
+
self.role_table = QTableWidget(0, 4)
|
|
765
|
+
self.role_table.setHorizontalHeaderLabels([
|
|
766
|
+
"Title", "Function name", "Namespace", "Description"
|
|
767
|
+
])
|
|
768
|
+
self.role_table.horizontalHeader().setSectionResizeMode(
|
|
769
|
+
QHeaderView.Stretch)
|
|
770
|
+
self.role_table.setFont(QFont("Consolas", 10))
|
|
771
|
+
|
|
772
|
+
# [v3.10] Namespace column: editable QComboBox delegate.
|
|
773
|
+
self._namespace_delegate = _NamespaceDelegate(
|
|
774
|
+
self._get_namespace_choices, self.role_table)
|
|
775
|
+
self.role_table.setItemDelegateForColumn(
|
|
776
|
+
self.ROLE_NAMESPACE_COL, self._namespace_delegate)
|
|
777
|
+
|
|
778
|
+
role_layout.addWidget(self.role_table)
|
|
779
|
+
btn_role = QHBoxLayout()
|
|
780
|
+
add_role_btn = QPushButton(self.tr("Add"))
|
|
781
|
+
add_role_btn.clicked.connect(self.add_role_function)
|
|
782
|
+
edit_role_btn = QPushButton(self.tr("Edit"))
|
|
783
|
+
edit_role_btn.clicked.connect(self.edit_role_function)
|
|
784
|
+
del_role_btn = QPushButton(self.tr("Delete"))
|
|
785
|
+
del_role_btn.clicked.connect(self.delete_role_function)
|
|
786
|
+
btn_role.addWidget(add_role_btn)
|
|
787
|
+
btn_role.addWidget(edit_role_btn)
|
|
788
|
+
btn_role.addWidget(del_role_btn)
|
|
789
|
+
role_layout.addLayout(btn_role)
|
|
790
|
+
|
|
791
|
+
event_btn = QPushButton(self.tr("Event definitions..."))
|
|
792
|
+
event_btn.clicked.connect(self.open_event_definition)
|
|
793
|
+
role_layout.addWidget(event_btn)
|
|
794
|
+
|
|
795
|
+
self.tab.addTab(role_tab, self.tr("Role function"))
|
|
796
|
+
|
|
797
|
+
self.state_table.itemChanged.connect(self.on_state_table_item_changed)
|
|
798
|
+
self.role_table.itemChanged.connect(self.on_role_table_item_changed)
|
|
799
|
+
self.state_table.cellDoubleClicked.connect(
|
|
800
|
+
self.on_state_table_cell_double_clicked)
|
|
801
|
+
self.role_table.cellDoubleClicked.connect(
|
|
802
|
+
self.on_role_table_cell_double_clicked)
|
|
803
|
+
|
|
804
|
+
self.populate()
|
|
805
|
+
StaTableLogger.debug("SettingsPanel initialized")
|
|
806
|
+
|
|
807
|
+
def _emit_settings_changed(self):
|
|
808
|
+
if not self._updating:
|
|
809
|
+
self.settings_changed.emit()
|
|
810
|
+
|
|
811
|
+
@staticmethod
|
|
812
|
+
def _make_state_item(text: str, col: int) -> QTableWidgetItem:
|
|
813
|
+
# [v2.7.1] All 3 columns are inline-editable; Name is also
|
|
814
|
+
# opened by double-click (see on_state_table_cell_double_clicked).
|
|
815
|
+
return QTableWidgetItem(text)
|
|
816
|
+
|
|
817
|
+
# ------------------------------------------------------------------
|
|
818
|
+
# Namespace choices provider (shared by delegate + dialog)
|
|
819
|
+
# ------------------------------------------------------------------
|
|
820
|
+
def _get_namespace_choices(self) -> List[str]:
|
|
821
|
+
"""Return namespace candidates (deduped, order preserved).
|
|
822
|
+
|
|
823
|
+
[v3.11]
|
|
824
|
+
Candidates come from (in order):
|
|
825
|
+
1. layer_names_provider (all tabs: Application, Driver,
|
|
826
|
+
Middleware, ...)
|
|
827
|
+
2. self.sm.layer_name (current tab, in case provider
|
|
828
|
+
is unavailable)
|
|
829
|
+
3. role_function_library (shared library)
|
|
830
|
+
4. sm.role_functions (this StateMachine)
|
|
831
|
+
|
|
832
|
+
Used both by the inline namespace combo-box delegate and
|
|
833
|
+
by _role_function_dialog_kwargs() for RoleFunctionDialog,
|
|
834
|
+
so both show the same complete list.
|
|
835
|
+
"""
|
|
836
|
+
ordered: List[str] = []
|
|
837
|
+
|
|
838
|
+
def _add(ns):
|
|
839
|
+
ns = (ns or "").strip()
|
|
840
|
+
if ns and ns not in ordered:
|
|
841
|
+
ordered.append(ns)
|
|
842
|
+
|
|
843
|
+
# 1. All tab names (= all layer names in the project)
|
|
844
|
+
if self.layer_names_provider is not None:
|
|
845
|
+
try:
|
|
846
|
+
for name in self.layer_names_provider():
|
|
847
|
+
_add(name)
|
|
848
|
+
except Exception as e:
|
|
849
|
+
StaTableLogger.warning(
|
|
850
|
+
f"layer_names_provider failed: {e}")
|
|
851
|
+
|
|
852
|
+
# 2. Current tab's layer name (fallback)
|
|
853
|
+
_add(getattr(self.sm, 'layer_name', ''))
|
|
854
|
+
|
|
855
|
+
# 3. Shared library namespaces
|
|
856
|
+
if self.role_function_library is not None:
|
|
857
|
+
try:
|
|
858
|
+
for rf in self.role_function_library.list_all():
|
|
859
|
+
_add(getattr(rf, 'namespace', ''))
|
|
860
|
+
except Exception as e:
|
|
861
|
+
StaTableLogger.warning(
|
|
862
|
+
f"role_function_library.list_all() failed: {e}")
|
|
863
|
+
|
|
864
|
+
# 4. Namespaces used in this StateMachine
|
|
865
|
+
for rf in self.sm.role_functions.values():
|
|
866
|
+
_add(getattr(rf, 'namespace', ''))
|
|
867
|
+
|
|
868
|
+
return ordered
|
|
869
|
+
|
|
870
|
+
# ------------------------------------------------------------------
|
|
871
|
+
# Populate
|
|
872
|
+
# ------------------------------------------------------------------
|
|
873
|
+
def populate(self):
|
|
874
|
+
self._updating = True
|
|
875
|
+
|
|
876
|
+
states = list(self.sm.states.values())
|
|
877
|
+
self.state_table.setRowCount(len(states))
|
|
878
|
+
for row, state in enumerate(states):
|
|
879
|
+
name_item = QTableWidgetItem(state.name)
|
|
880
|
+
name_item.setToolTip(
|
|
881
|
+
self.tr("Double-click to edit actions (Entry / Exit / Do)"))
|
|
882
|
+
self.state_table.setItem(row, self.STATE_COL_NAME, name_item)
|
|
883
|
+
self.state_table.setItem(
|
|
884
|
+
row, self.STATE_COL_DESC,
|
|
885
|
+
QTableWidgetItem(state.description))
|
|
886
|
+
self.state_table.setItem(
|
|
887
|
+
row, self.STATE_COL_TYPE,
|
|
888
|
+
QTableWidgetItem(state.type.value))
|
|
889
|
+
|
|
890
|
+
self.populate_role_table()
|
|
891
|
+
|
|
892
|
+
self._updating = False
|
|
893
|
+
StaTableLogger.debug(
|
|
894
|
+
f"Settings populated: {len(states)} states, "
|
|
895
|
+
f"{len(self.sm.role_functions)} roles")
|
|
896
|
+
|
|
897
|
+
def populate_role_table(self):
|
|
898
|
+
"""Refresh the role function table.
|
|
899
|
+
|
|
900
|
+
[v3.10]
|
|
901
|
+
Only the Namespace column is editable inline.
|
|
902
|
+
"""
|
|
903
|
+
roles = list(self.sm.role_functions.values())
|
|
904
|
+
self.role_table.setRowCount(len(roles))
|
|
905
|
+
for row, rf in enumerate(roles):
|
|
906
|
+
values = [rf.title, rf.name, rf.namespace, rf.description]
|
|
907
|
+
for col, text in enumerate(values):
|
|
908
|
+
item = QTableWidgetItem(text)
|
|
909
|
+
if col != self.ROLE_NAMESPACE_COL:
|
|
910
|
+
item.setFlags(item.flags() & ~Qt.ItemIsEditable)
|
|
911
|
+
self.role_table.setItem(row, col, item)
|
|
912
|
+
|
|
913
|
+
# ------------------------------------------------------------------
|
|
914
|
+
# State editing
|
|
915
|
+
# ------------------------------------------------------------------
|
|
916
|
+
def on_state_table_cell_double_clicked(self, row, col):
|
|
917
|
+
"""[v2.7.1] Name column double-click opens StateActionsDialog.
|
|
918
|
+
|
|
919
|
+
Name (0): StateActionsDialog
|
|
920
|
+
Description (1): inline edit (default behavior)
|
|
921
|
+
Type (2): inline edit (default behavior)
|
|
922
|
+
"""
|
|
923
|
+
StaTableLogger.debug(
|
|
924
|
+
f"SettingsPanel.on_state_table_cell_double_clicked: "
|
|
925
|
+
f"row={row}, col={col}")
|
|
926
|
+
if col == self.STATE_COL_NAME:
|
|
927
|
+
self._open_state_actions(row)
|
|
928
|
+
|
|
929
|
+
def _open_state_actions(self, row: int):
|
|
930
|
+
"""[v2.7.1] Open the StateActionsDialog for the state in this row."""
|
|
931
|
+
name_item = self.state_table.item(row, self.STATE_COL_NAME)
|
|
932
|
+
name = name_item.text().strip() if name_item else ""
|
|
933
|
+
if not name or name not in self.sm.states:
|
|
934
|
+
StaTableLogger.debug(
|
|
935
|
+
f"_open_state_actions: unknown state '{name}'")
|
|
936
|
+
return
|
|
937
|
+
|
|
938
|
+
state = self.sm.states[name]
|
|
939
|
+
dlg = StateActionsDialog(
|
|
940
|
+
parent=self, state=state, state_machine=self.sm)
|
|
941
|
+
if dlg.exec() == QDialog.Accepted:
|
|
942
|
+
self.settings_changed.emit()
|
|
943
|
+
StaTableLogger.info(
|
|
944
|
+
f"StateActionsDialog applied for state '{name}' "
|
|
945
|
+
f"(entry={len(state.entry or [])}, "
|
|
946
|
+
f"exit={len(state.exit or [])}, "
|
|
947
|
+
f"do={len(state.do_actions or [])})")
|
|
948
|
+
|
|
949
|
+
def add_state(self):
|
|
950
|
+
row = self.state_table.rowCount()
|
|
951
|
+
self.state_table.insertRow(row)
|
|
952
|
+
defaults = ["", "", "normal"] # Name / Description / Type
|
|
953
|
+
for col, default in enumerate(defaults):
|
|
954
|
+
self.state_table.setItem(
|
|
955
|
+
row, col, self._make_state_item(default, col))
|
|
956
|
+
self.state_table.editItem(self.state_table.item(row, 0))
|
|
957
|
+
# [C-38 fix] Notify listeners that a state was added
|
|
958
|
+
self.settings_changed.emit()
|
|
959
|
+
StaTableLogger.debug("Add state row")
|
|
960
|
+
|
|
961
|
+
def delete_state(self):
|
|
962
|
+
row = self.state_table.currentRow()
|
|
963
|
+
if row >= 0:
|
|
964
|
+
name = (self.state_table.item(row, 0).text().strip()
|
|
965
|
+
if self.state_table.item(row, 0) else "")
|
|
966
|
+
if name and name in self.sm.states:
|
|
967
|
+
self.sm.transitions = [
|
|
968
|
+
t for t in self.sm.transitions
|
|
969
|
+
if t.source != name and t.target != name
|
|
970
|
+
]
|
|
971
|
+
for (src, evt) in list(self.sm.get_cell_keys()):
|
|
972
|
+
if src == name:
|
|
973
|
+
self.sm.remove_cell_metadata(src, evt)
|
|
974
|
+
del self.sm.states[name]
|
|
975
|
+
self.populate()
|
|
976
|
+
self.settings_changed.emit()
|
|
977
|
+
StaTableLogger.info(f"State deleted: {name}")
|
|
978
|
+
else:
|
|
979
|
+
self.state_table.removeRow(row)
|
|
980
|
+
|
|
981
|
+
def open_event_definition(self):
|
|
982
|
+
StaTableLogger.debug("SettingsPanel.open_event_definition called")
|
|
983
|
+
dlg = EventDefinitionDialog(self.sm, self.global_defs, self)
|
|
984
|
+
if dlg.exec() == QDialog.Accepted:
|
|
985
|
+
self.settings_changed.emit()
|
|
986
|
+
StaTableLogger.info("Event definitions updated")
|
|
987
|
+
|
|
988
|
+
# ------------------------------------------------------------------
|
|
989
|
+
# Role function dialog helpers
|
|
990
|
+
# ------------------------------------------------------------------
|
|
991
|
+
def _role_function_dialog_kwargs(self):
|
|
992
|
+
"""Build kwargs for RoleFunctionDialog."""
|
|
993
|
+
global_vars = [
|
|
994
|
+
getattr(v, 'name', '') for v in
|
|
995
|
+
(getattr(self.global_defs, 'variables', []) or [])
|
|
996
|
+
if getattr(v, 'name', '')
|
|
997
|
+
]
|
|
998
|
+
|
|
999
|
+
events = [
|
|
1000
|
+
getattr(e, 'name', '') for e in self.sm.events.values()
|
|
1001
|
+
if getattr(e, 'name', '')
|
|
1002
|
+
]
|
|
1003
|
+
|
|
1004
|
+
literals = []
|
|
1005
|
+
if self.literal_library is not None:
|
|
1006
|
+
try:
|
|
1007
|
+
literals = [
|
|
1008
|
+
getattr(lit, 'name', '')
|
|
1009
|
+
for lit in self.literal_library.list_all()
|
|
1010
|
+
if getattr(lit, 'name', '')
|
|
1011
|
+
]
|
|
1012
|
+
except Exception as e:
|
|
1013
|
+
StaTableLogger.warning(
|
|
1014
|
+
f"literal_library.list_all() failed: {e}")
|
|
1015
|
+
|
|
1016
|
+
return dict(
|
|
1017
|
+
global_vars=global_vars,
|
|
1018
|
+
events=events,
|
|
1019
|
+
literals=literals,
|
|
1020
|
+
namespace_choices=self._get_namespace_choices(),
|
|
1021
|
+
# [R-7] pass library so "+ New Literal" works
|
|
1022
|
+
literal_library=self.literal_library,
|
|
1023
|
+
)
|
|
1024
|
+
|
|
1025
|
+
def add_role_function(self):
|
|
1026
|
+
StaTableLogger.debug("SettingsPanel.add_role_function called")
|
|
1027
|
+
dlg = RoleFunctionDialog(
|
|
1028
|
+
self, **self._role_function_dialog_kwargs())
|
|
1029
|
+
if dlg.exec() == QDialog.Accepted:
|
|
1030
|
+
rf = dlg.get_role_function()
|
|
1031
|
+
if rf.name in self.sm.role_functions:
|
|
1032
|
+
QMessageBox.warning(
|
|
1033
|
+
self, self.tr("Warning"),
|
|
1034
|
+
self.tr("A role function with the same name already exists."))
|
|
1035
|
+
return
|
|
1036
|
+
self.sm.add_role_function(rf)
|
|
1037
|
+
self.populate_role_table()
|
|
1038
|
+
self.settings_changed.emit()
|
|
1039
|
+
|
|
1040
|
+
def edit_role_function(self):
|
|
1041
|
+
row = self.role_table.currentRow()
|
|
1042
|
+
if row < 0:
|
|
1043
|
+
StaTableLogger.debug("edit_role_function: no row selected")
|
|
1044
|
+
return
|
|
1045
|
+
|
|
1046
|
+
name = (self.role_table.item(row, 1).text().strip()
|
|
1047
|
+
if self.role_table.item(row, 1) else "")
|
|
1048
|
+
if not name or name not in self.sm.role_functions:
|
|
1049
|
+
StaTableLogger.debug(
|
|
1050
|
+
f"edit_role_function: unknown name '{name}'")
|
|
1051
|
+
return
|
|
1052
|
+
|
|
1053
|
+
existing = self.sm.role_functions[name]
|
|
1054
|
+
dlg = RoleFunctionDialog(
|
|
1055
|
+
self,
|
|
1056
|
+
role_function=existing,
|
|
1057
|
+
**self._role_function_dialog_kwargs(),
|
|
1058
|
+
)
|
|
1059
|
+
if dlg.exec() != QDialog.Accepted:
|
|
1060
|
+
return
|
|
1061
|
+
|
|
1062
|
+
updated = dlg.get_role_function()
|
|
1063
|
+
|
|
1064
|
+
if updated.name != name and updated.name in self.sm.role_functions:
|
|
1065
|
+
QMessageBox.warning(
|
|
1066
|
+
self, self.tr("Warning"),
|
|
1067
|
+
self.tr("A role function with the same name already exists."))
|
|
1068
|
+
return
|
|
1069
|
+
|
|
1070
|
+
self.sm.remove_role_function(name)
|
|
1071
|
+
self.sm.add_role_function(updated)
|
|
1072
|
+
self.populate_role_table()
|
|
1073
|
+
self.settings_changed.emit()
|
|
1074
|
+
StaTableLogger.info(
|
|
1075
|
+
f"Role function updated: '{name}' -> '{updated.name}'")
|
|
1076
|
+
|
|
1077
|
+
def on_role_table_cell_double_clicked(self, row, col):
|
|
1078
|
+
"""Row double-click handler.
|
|
1079
|
+
|
|
1080
|
+
[v3.10]
|
|
1081
|
+
If the Namespace column was double-clicked, the inline
|
|
1082
|
+
combo-box delegate handles it; do not open the dialog.
|
|
1083
|
+
"""
|
|
1084
|
+
StaTableLogger.debug(
|
|
1085
|
+
f"SettingsPanel.on_role_table_cell_double_clicked: "
|
|
1086
|
+
f"row={row}, col={col}")
|
|
1087
|
+
|
|
1088
|
+
if col == self.ROLE_NAMESPACE_COL:
|
|
1089
|
+
return
|
|
1090
|
+
|
|
1091
|
+
self.role_table.selectRow(row)
|
|
1092
|
+
self.edit_role_function()
|
|
1093
|
+
|
|
1094
|
+
def delete_role_function(self):
|
|
1095
|
+
row = self.role_table.currentRow()
|
|
1096
|
+
if row >= 0:
|
|
1097
|
+
name = (self.role_table.item(row, 1).text().strip()
|
|
1098
|
+
if self.role_table.item(row, 1) else "")
|
|
1099
|
+
if name and name in self.sm.role_functions:
|
|
1100
|
+
self.sm.remove_role_function(name)
|
|
1101
|
+
self.populate_role_table()
|
|
1102
|
+
self.settings_changed.emit()
|
|
1103
|
+
StaTableLogger.info(f"Role function deleted: {name}")
|
|
1104
|
+
else:
|
|
1105
|
+
self.role_table.removeRow(row)
|
|
1106
|
+
|
|
1107
|
+
def on_state_table_item_changed(self, item):
|
|
1108
|
+
self._debounce_timer.start()
|
|
1109
|
+
|
|
1110
|
+
def on_role_table_item_changed(self, item):
|
|
1111
|
+
self._debounce_timer.start()
|
|
1112
|
+
|
|
1113
|
+
def apply_changes(self):
|
|
1114
|
+
"""Apply the UI edits back into the StateMachine.
|
|
1115
|
+
|
|
1116
|
+
[v3.10]
|
|
1117
|
+
The Namespace column may have been edited inline via the
|
|
1118
|
+
combo-box delegate. This method picks up the change and
|
|
1119
|
+
rebuilds the role functions accordingly.
|
|
1120
|
+
"""
|
|
1121
|
+
# ---- States ----
|
|
1122
|
+
# [v2.7.1] State list is 3 columns: Name / Description / Type.
|
|
1123
|
+
# entry / exit are preserved on the state object (they are
|
|
1124
|
+
# edited only via StateActionsDialog).
|
|
1125
|
+
for row in range(self.state_table.rowCount()):
|
|
1126
|
+
name = (self.state_table.item(row, self.STATE_COL_NAME).text().strip()
|
|
1127
|
+
if self.state_table.item(row, self.STATE_COL_NAME) else "")
|
|
1128
|
+
desc = (self.state_table.item(row, self.STATE_COL_DESC).text().strip()
|
|
1129
|
+
if self.state_table.item(row, self.STATE_COL_DESC) else "")
|
|
1130
|
+
type_str = (self.state_table.item(row, self.STATE_COL_TYPE).text().strip()
|
|
1131
|
+
if self.state_table.item(row, self.STATE_COL_TYPE) else "normal")
|
|
1132
|
+
|
|
1133
|
+
if name:
|
|
1134
|
+
if name in self.sm.states:
|
|
1135
|
+
st = self.sm.states[name]
|
|
1136
|
+
st.description = desc
|
|
1137
|
+
try:
|
|
1138
|
+
st.type = StateType(type_str)
|
|
1139
|
+
except ValueError:
|
|
1140
|
+
st.type = StateType.NORMAL
|
|
1141
|
+
else:
|
|
1142
|
+
self.sm.add_state(State(
|
|
1143
|
+
name, type=StateType(type_str), description=desc,
|
|
1144
|
+
))
|
|
1145
|
+
|
|
1146
|
+
# ---- Role functions ----
|
|
1147
|
+
existing_roles = dict(self.sm.role_functions)
|
|
1148
|
+
self.sm.role_functions.clear()
|
|
1149
|
+
|
|
1150
|
+
for row in range(self.role_table.rowCount()):
|
|
1151
|
+
title = (self.role_table.item(row, 0).text().strip()
|
|
1152
|
+
if self.role_table.item(row, 0) else "")
|
|
1153
|
+
name = (self.role_table.item(row, 1).text().strip()
|
|
1154
|
+
if self.role_table.item(row, 1) else "")
|
|
1155
|
+
if name:
|
|
1156
|
+
namespace = (self.role_table.item(row, 2).text().strip()
|
|
1157
|
+
if self.role_table.item(row, 2) else "")
|
|
1158
|
+
desc = (self.role_table.item(row, 3).text().strip()
|
|
1159
|
+
if self.role_table.item(row, 3) else "")
|
|
1160
|
+
|
|
1161
|
+
prev = existing_roles.get(name)
|
|
1162
|
+
self.sm.add_role_function(RoleFunction(
|
|
1163
|
+
name=name,
|
|
1164
|
+
namespace=namespace,
|
|
1165
|
+
description=desc,
|
|
1166
|
+
title=title,
|
|
1167
|
+
# [Reserved] carry forward previous values
|
|
1168
|
+
return_type=(
|
|
1169
|
+
getattr(prev, 'return_type', 'void')
|
|
1170
|
+
if prev else "void"),
|
|
1171
|
+
arg1_type=(
|
|
1172
|
+
getattr(prev, 'arg1_type', '') if prev else ""),
|
|
1173
|
+
arg1_name=(
|
|
1174
|
+
getattr(prev, 'arg1_name', '') if prev else ""),
|
|
1175
|
+
arg2_type=(
|
|
1176
|
+
getattr(prev, 'arg2_type', '') if prev else ""),
|
|
1177
|
+
arg2_name=(
|
|
1178
|
+
getattr(prev, 'arg2_name', '') if prev else ""),
|
|
1179
|
+
# [v3.8] carry forward symbol references
|
|
1180
|
+
used_global_vars=list(
|
|
1181
|
+
getattr(prev, 'used_global_vars', []) or []
|
|
1182
|
+
) if prev else [],
|
|
1183
|
+
used_events=list(
|
|
1184
|
+
getattr(prev, 'used_events', []) or []
|
|
1185
|
+
) if prev else [],
|
|
1186
|
+
used_literals=list(
|
|
1187
|
+
getattr(prev, 'used_literals', []) or []
|
|
1188
|
+
) if prev else [],
|
|
1189
|
+
))
|
|
1190
|
+
StaTableLogger.debug("Settings changes applied")
|
|
1191
|
+
|
|
1192
|
+
|
|
1193
|
+
class StateMachineTab(QWidget):
|
|
1194
|
+
"""Tab hosting one state machine (matrix + mermaid + settings).
|
|
1195
|
+
|
|
1196
|
+
[v3.11]
|
|
1197
|
+
Accepts and forwards `layer_names_provider` to SettingsPanel.
|
|
1198
|
+
|
|
1199
|
+
[v3.9]
|
|
1200
|
+
Passes role_function_library and literal_library to SettingsPanel.
|
|
1201
|
+
|
|
1202
|
+
[v3.7 change]
|
|
1203
|
+
Layout: QSplitter(Qt.Horizontal).
|
|
1204
|
+
|
|
1205
|
+
[v2.3 change]
|
|
1206
|
+
dataModified signal relays child modifications to MainWindow.
|
|
1207
|
+
"""
|
|
1208
|
+
|
|
1209
|
+
dataModified = Signal()
|
|
1210
|
+
|
|
1211
|
+
SETTINGS_MIN_WIDTH = 260
|
|
1212
|
+
SETTINGS_MAX_WIDTH = 560
|
|
1213
|
+
|
|
1214
|
+
def __init__(self, sm: StateMachine,
|
|
1215
|
+
global_defs: GlobalDefinitions = None,
|
|
1216
|
+
role_function_library: RoleFunctionLibrary = None,
|
|
1217
|
+
condition_library: ConditionLibrary = None,
|
|
1218
|
+
literal_library: LiteralLibrary = None,
|
|
1219
|
+
layer_names_provider: Callable[[], List[str]] = None,
|
|
1220
|
+
parent=None):
|
|
1221
|
+
super().__init__(parent)
|
|
1222
|
+
self.sm = sm
|
|
1223
|
+
self.global_defs = global_defs if global_defs else GlobalDefinitions()
|
|
1224
|
+
|
|
1225
|
+
self.role_function_library = (
|
|
1226
|
+
role_function_library if role_function_library
|
|
1227
|
+
else RoleFunctionLibrary())
|
|
1228
|
+
self.condition_library = (
|
|
1229
|
+
condition_library if condition_library
|
|
1230
|
+
else ConditionLibrary())
|
|
1231
|
+
self.literal_library = (
|
|
1232
|
+
literal_library if literal_library
|
|
1233
|
+
else LiteralLibrary())
|
|
1234
|
+
# [v3.11] forwarded to SettingsPanel
|
|
1235
|
+
self.layer_names_provider = layer_names_provider
|
|
1236
|
+
|
|
1237
|
+
StaTableLogger.debug(
|
|
1238
|
+
f"StateMachineTab.__init__: global_defs id={id(self.global_defs)}, "
|
|
1239
|
+
f"vars={len(self.global_defs.variables)}, "
|
|
1240
|
+
f"flags={len(self.global_defs.flags)}"
|
|
1241
|
+
)
|
|
1242
|
+
StaTableLogger.debug(
|
|
1243
|
+
f"StateMachineTab shared libraries: "
|
|
1244
|
+
f"roles={len(self.role_function_library.list_all())}, "
|
|
1245
|
+
f"conditions={len(self.condition_library.list_all())}, "
|
|
1246
|
+
f"literals={len(self.literal_library.list_all())}"
|
|
1247
|
+
)
|
|
1248
|
+
|
|
1249
|
+
outer = QVBoxLayout(self)
|
|
1250
|
+
outer.setContentsMargins(0, 0, 0, 0)
|
|
1251
|
+
outer.setSpacing(0)
|
|
1252
|
+
|
|
1253
|
+
main_split = QSplitter(Qt.Horizontal)
|
|
1254
|
+
main_split.setChildrenCollapsible(False)
|
|
1255
|
+
|
|
1256
|
+
left_split = QSplitter(Qt.Vertical)
|
|
1257
|
+
# [R-6] Forward layer_names_provider to MatrixTableWidget so
|
|
1258
|
+
# that ActionEditorDialog / TransitionsTab / ActionsTab
|
|
1259
|
+
# can list every layer in the project.
|
|
1260
|
+
self.table = MatrixTableWidget(
|
|
1261
|
+
sm,
|
|
1262
|
+
global_defs=self.global_defs,
|
|
1263
|
+
role_function_library=self.role_function_library,
|
|
1264
|
+
condition_library=self.condition_library,
|
|
1265
|
+
literal_library=self.literal_library,
|
|
1266
|
+
layer_names_provider=self.layer_names_provider,
|
|
1267
|
+
)
|
|
1268
|
+
self.mermaid = MermaidWidget()
|
|
1269
|
+
left_split.addWidget(self.table)
|
|
1270
|
+
left_split.addWidget(self.mermaid)
|
|
1271
|
+
|
|
1272
|
+
total_height = WINDOW_HEIGHT
|
|
1273
|
+
table_height = int(total_height * TABLE_PREVIEW_RATIO)
|
|
1274
|
+
mermaid_height = total_height - table_height
|
|
1275
|
+
left_split.setSizes([table_height, mermaid_height])
|
|
1276
|
+
|
|
1277
|
+
self.table.setMinimumHeight(300)
|
|
1278
|
+
self.mermaid.setMinimumHeight(MERMAID_PREVIEW_MIN_HEIGHT)
|
|
1279
|
+
|
|
1280
|
+
main_split.addWidget(left_split)
|
|
1281
|
+
|
|
1282
|
+
# [v3.11] Forward layer_names_provider to SettingsPanel.
|
|
1283
|
+
self.settings = SettingsPanel(
|
|
1284
|
+
sm,
|
|
1285
|
+
global_defs=self.global_defs,
|
|
1286
|
+
literal_library=self.literal_library,
|
|
1287
|
+
role_function_library=self.role_function_library,
|
|
1288
|
+
layer_names_provider=self.layer_names_provider,
|
|
1289
|
+
)
|
|
1290
|
+
self.settings.setMinimumWidth(self.SETTINGS_MIN_WIDTH)
|
|
1291
|
+
self.settings.setMaximumWidth(self.SETTINGS_MAX_WIDTH)
|
|
1292
|
+
|
|
1293
|
+
main_split.addWidget(self.settings)
|
|
1294
|
+
|
|
1295
|
+
main_split.setStretchFactor(0, 4)
|
|
1296
|
+
main_split.setStretchFactor(1, 1)
|
|
1297
|
+
main_split.setSizes([WINDOW_HEIGHT, self.SETTINGS_MIN_WIDTH])
|
|
1298
|
+
|
|
1299
|
+
outer.addWidget(main_split)
|
|
1300
|
+
|
|
1301
|
+
self.table.transition_changed.connect(self.update_mermaid)
|
|
1302
|
+
self.settings.settings_changed.connect(self.update_mermaid)
|
|
1303
|
+
|
|
1304
|
+
self.table.transition_changed.connect(self.dataModified)
|
|
1305
|
+
self.settings.settings_changed.connect(self.dataModified)
|
|
1306
|
+
|
|
1307
|
+
self.update_mermaid()
|
|
1308
|
+
StaTableLogger.debug("StateMachineTab created")
|
|
1309
|
+
|
|
1310
|
+
def update_mermaid(self):
|
|
1311
|
+
self.settings.apply_changes()
|
|
1312
|
+
self.table.populate()
|
|
1313
|
+
code = generate_mermaid(self.sm)
|
|
1314
|
+
self.mermaid.set_mermaid_code(code)
|
|
1315
|
+
StaTableLogger.info("Mermaid updated for current tab")
|