dynamic-systems-testing-framework 1.0.1__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.
- dstf/__init__.py +41 -0
- dstf/__main__.py +10 -0
- dstf/authoring/__init__.py +13 -0
- dstf/authoring/builtin/__init__.py +32 -0
- dstf/authoring/builtin/accept.py +205 -0
- dstf/authoring/builtin/edit_criteria.py +479 -0
- dstf/authoring/builtin/edit_family.py +228 -0
- dstf/authoring/builtin/edit_on_plot.py +1354 -0
- dstf/authoring/builtin/edit_tree.py +1399 -0
- dstf/authoring/builtin/enqueue.py +144 -0
- dstf/authoring/capabilities.py +463 -0
- dstf/authoring/errors.py +49 -0
- dstf/authoring/family_rows.py +526 -0
- dstf/authoring/forms.py +631 -0
- dstf/authoring/layout.py +104 -0
- dstf/authoring/live_rows.py +337 -0
- dstf/authoring/live_runs.py +385 -0
- dstf/authoring/needle.py +127 -0
- dstf/authoring/picker_rows.py +92 -0
- dstf/authoring/queue_rows.py +354 -0
- dstf/authoring/session.py +3016 -0
- dstf/authoring/shell/__init__.py +9 -0
- dstf/authoring/shell/actions.py +702 -0
- dstf/authoring/shell/app.py +53 -0
- dstf/authoring/shell/drawer_controls.py +190 -0
- dstf/authoring/shell/family_fold.py +468 -0
- dstf/authoring/shell/figure_hook.py +249 -0
- dstf/authoring/shell/grips.js +1000 -0
- dstf/authoring/shell/header.py +783 -0
- dstf/authoring/shell/live_view.py +93 -0
- dstf/authoring/shell/picker.py +267 -0
- dstf/authoring/shell/placing.py +122 -0
- dstf/authoring/shell/plot_card.py +1171 -0
- dstf/authoring/shell/queue_panel.py +433 -0
- dstf/authoring/shell/rail.py +593 -0
- dstf/authoring/shell/shell.py +528 -0
- dstf/authoring/shell/stats_card.py +501 -0
- dstf/authoring/shell/strip.py +204 -0
- dstf/authoring/shell/style.py +721 -0
- dstf/authoring/shell/suite.py +277 -0
- dstf/authoring/shell/suite_grid.js +414 -0
- dstf/authoring/shell/suite_grid.py +373 -0
- dstf/authoring/shell/tab_theme.py +110 -0
- dstf/authoring/shell/tabstate.py +146 -0
- dstf/authoring/shell/test_list.py +179 -0
- dstf/authoring/shell/test_page.py +1404 -0
- dstf/authoring/shell/tree_card.py +1171 -0
- dstf/authoring/shell/variables_drawer.py +589 -0
- dstf/authoring/shell/widgets.py +188 -0
- dstf/authoring/situation.py +287 -0
- dstf/authoring/spec_document.py +215 -0
- dstf/authoring/spec_preview.py +83 -0
- dstf/authoring/suite_rows.py +99 -0
- dstf/authoring/tree_move.py +516 -0
- dstf/authoring/tree_pointer.py +33 -0
- dstf/authoring/tree_text.py +61 -0
- dstf/authoring/typed_number.js +64 -0
- dstf/authoring/typed_number.py +76 -0
- dstf/authoring/unscorable.py +141 -0
- dstf/authoring/variable_rows.py +216 -0
- dstf/backends/__init__.py +283 -0
- dstf/backends/base.py +1117 -0
- dstf/backends/common/__init__.py +1 -0
- dstf/backends/common/json_driver.py +484 -0
- dstf/backends/common/live_mat.py +369 -0
- dstf/backends/common/mat_reader.py +632 -0
- dstf/backends/common/proc_kill.py +238 -0
- dstf/backends/common/proc_output.py +20 -0
- dstf/backends/common/result_files.py +109 -0
- dstf/backends/common/result_variables.py +329 -0
- dstf/backends/common/statistics.py +39 -0
- dstf/backends/cross_backend.py +193 -0
- dstf/backends/dymola/__init__.py +5 -0
- dstf/backends/dymola/backend.py +776 -0
- dstf/backends/dymola/interface_loader.py +241 -0
- dstf/backends/dymola/log_parser.py +396 -0
- dstf/backends/dymola/persistent_backend.py +949 -0
- dstf/backends/fmpy/__init__.py +11 -0
- dstf/backends/fmpy/backend.py +443 -0
- dstf/backends/julia/__init__.py +27 -0
- dstf/backends/julia/backend.py +156 -0
- dstf/backends/julia/persistent_backend.py +541 -0
- dstf/backends/julia/run_persistent.jl +167 -0
- dstf/backends/julia/run_test.jl +146 -0
- dstf/backends/openmodelica/__init__.py +21 -0
- dstf/backends/openmodelica/backend.py +368 -0
- dstf/backends/openmodelica/init_xml.py +76 -0
- dstf/backends/openmodelica/log_parser.py +119 -0
- dstf/backends/openmodelica/mos_generator.py +228 -0
- dstf/backends/openmodelica/persistent_backend.py +1045 -0
- dstf/backends/openmodelica/session_loader.py +63 -0
- dstf/backends/persistent.py +1811 -0
- dstf/backends/pool.py +602 -0
- dstf/backends/progress.py +395 -0
- dstf/backends/python/__init__.py +11 -0
- dstf/backends/python/backend.py +141 -0
- dstf/backends/python/run_test.py +141 -0
- dstf/backends/run_metadata.py +103 -0
- dstf/backends/types.py +310 -0
- dstf/cli/__init__.py +12 -0
- dstf/cli/author_cmd.py +61 -0
- dstf/cli/baseline_cmds.py +341 -0
- dstf/cli/common.py +231 -0
- dstf/cli/diag_cmds.py +350 -0
- dstf/cli/main.py +561 -0
- dstf/cli/review.py +398 -0
- dstf/cli/run_cmds.py +589 -0
- dstf/cli/spec_cmds.py +123 -0
- dstf/comparison/__init__.py +13 -0
- dstf/comparison/algorithms.py +2127 -0
- dstf/comparison/builtins.py +291 -0
- dstf/comparison/comparator.py +1189 -0
- dstf/comparison/geometry.py +164 -0
- dstf/comparison/handles.py +1194 -0
- dstf/comparison/metric_tree.py +332 -0
- dstf/comparison/mode_schema.py +176 -0
- dstf/comparison/modes.py +788 -0
- dstf/comparison/registry.py +230 -0
- dstf/comparison/seeds.py +281 -0
- dstf/comparison/sides.py +158 -0
- dstf/comparison/snap.py +62 -0
- dstf/comparison/strict_keys.py +246 -0
- dstf/comparison/summaries.py +354 -0
- dstf/comparison/tree_eval.py +868 -0
- dstf/comparison/tree_spec.py +678 -0
- dstf/comparison/types.py +194 -0
- dstf/comparison/validator.py +232 -0
- dstf/config.py +815 -0
- dstf/discovery/__init__.py +0 -0
- dstf/discovery/json_recognizer.py +702 -0
- dstf/discovery/library_state.py +248 -0
- dstf/discovery/mo_parser.py +794 -0
- dstf/discovery/patch_apply.py +308 -0
- dstf/discovery/patch_envelope.py +63 -0
- dstf/discovery/plots.py +318 -0
- dstf/discovery/recognizer.py +103 -0
- dstf/discovery/scan_cache.py +154 -0
- dstf/discovery/spec_parser.py +538 -0
- dstf/discovery/spec_shape.py +122 -0
- dstf/discovery/spec_update.py +493 -0
- dstf/discovery/test_registry.py +488 -0
- dstf/discovery/unit_tests.py +95 -0
- dstf/figures/__init__.py +14 -0
- dstf/figures/assemble.py +48 -0
- dstf/figures/decimate.py +150 -0
- dstf/figures/families.py +289 -0
- dstf/figures/palette.py +170 -0
- dstf/figures/plotly.py +1413 -0
- dstf/figures/view.py +233 -0
- dstf/pipeline.py +435 -0
- dstf/plugins.py +104 -0
- dstf/reporting/__init__.py +0 -0
- dstf/reporting/console_report.py +187 -0
- dstf/reporting/context_builder.py +979 -0
- dstf/reporting/dashboard_render.py +534 -0
- dstf/reporting/junit_report.py +192 -0
- dstf/reporting/overlay_loader.py +708 -0
- dstf/reporting/report_suite.py +1059 -0
- dstf/reporting/rows.py +458 -0
- dstf/reporting/schema_export.py +413 -0
- dstf/reporting/stats_rows.py +321 -0
- dstf/reporting/status.py +388 -0
- dstf/reporting/templates/axes.js +61 -0
- dstf/reporting/templates/dashboard.html +284 -0
- dstf/reporting/templates/dashboard.js +604 -0
- dstf/reporting/templates/filters.js +100 -0
- dstf/reporting/templates/interactive.html +420 -0
- dstf/reporting/templates/report.js +116 -0
- dstf/reporting/templates/vendor/plotly.min.js +8 -0
- dstf/reporting/theme.py +324 -0
- dstf/reporting/tree_rows.py +73 -0
- dstf/reporting/verdicts.py +124 -0
- dstf/storage/__init__.py +0 -0
- dstf/storage/accept.py +216 -0
- dstf/storage/baseline.py +43 -0
- dstf/storage/batch_manifest.py +204 -0
- dstf/storage/ref_index.py +295 -0
- dstf/storage/reference_store.py +661 -0
- dstf/storage/roles.py +576 -0
- dstf/storage/run_stats.py +85 -0
- dynamic_systems_testing_framework-1.0.1.dist-info/METADATA +266 -0
- dynamic_systems_testing_framework-1.0.1.dist-info/RECORD +185 -0
- dynamic_systems_testing_framework-1.0.1.dist-info/WHEEL +4 -0
- dynamic_systems_testing_framework-1.0.1.dist-info/entry_points.txt +2 -0
- dynamic_systems_testing_framework-1.0.1.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,702 @@
|
|
|
1
|
+
"""Capabilities at a placement (brief §3.2; DESIGN.md "Buttons", "Disabled
|
|
2
|
+
states explain themselves"). `ActionBar` hosts SUITE and TEST: one button
|
|
3
|
+
per registered capability whose `placements` names this one, labelled by
|
|
4
|
+
the capability's `caption`, disabled with the sentence `available` gave
|
|
5
|
+
beside it; a click opens the bar's dialog hosting the capability's
|
|
6
|
+
`panel`, whose `submit` runs `act` and hands the outcome to the Session.
|
|
7
|
+
The bar knows no capability by name. The dialog is not rebuilt while open
|
|
8
|
+
(decision 4): a confirm list belongs to the moment of the click; an act
|
|
9
|
+
that applied something closes it, a raising one shows the error in it and
|
|
10
|
+
changes nothing, and one that returned nothing leaves it open — it changed
|
|
11
|
+
nothing to close over. There is no plot at either placement: a dialog's
|
|
12
|
+
`panel.figure` raises on a subscription.
|
|
13
|
+
|
|
14
|
+
`InlineHost` hosts the same contract the other way (brief §3.2): no button
|
|
15
|
+
and no dialog — every capability placed here has its panel built straight
|
|
16
|
+
into a slot the page owns, which is how the criterion editor, a tree
|
|
17
|
+
node's controls and a plot's criteria header (VARIABLE) are drawn without
|
|
18
|
+
the shell knowing a capability by name.
|
|
19
|
+
A host given a `RowTarget` lends its panels the page's form row under the
|
|
20
|
+
row it is drawn in (`Panel.row`) and the page's `reveal`; a bar's dialog
|
|
21
|
+
draws a panel's row inside the dialog, below the panel."""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import logging
|
|
26
|
+
from collections.abc import Callable, Hashable, Sequence
|
|
27
|
+
from functools import partial
|
|
28
|
+
from typing import TYPE_CHECKING, Any
|
|
29
|
+
|
|
30
|
+
from nicegui import ui
|
|
31
|
+
|
|
32
|
+
from ..capabilities import Capability, Emphasis, Outcome, Placement
|
|
33
|
+
from ..capabilities import FigureHook as FigureHookProtocol
|
|
34
|
+
from ..errors import USER_ERRORS, user_text
|
|
35
|
+
from ..session import Selection, StaleSpecError
|
|
36
|
+
from .figure_hook import FigureHook
|
|
37
|
+
from .placing import PLACE, PREVIEW, Placing
|
|
38
|
+
from .widgets import Action, IconAction, MenuAction
|
|
39
|
+
|
|
40
|
+
if TYPE_CHECKING:
|
|
41
|
+
from .shell import Shell
|
|
42
|
+
|
|
43
|
+
logger = logging.getLogger(__name__)
|
|
44
|
+
|
|
45
|
+
LOADING = "batch actions unlock when loading finishes"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def log_unless_users(e: BaseException, name: str) -> None:
|
|
49
|
+
"""Log a failed act unless a person's own edit raised it: `user_text`
|
|
50
|
+
shows a ValueError's text alone, and a JSONDecodeError or a numpy shape
|
|
51
|
+
error from a bug is one — it must still reach the log. A value an
|
|
52
|
+
editor REFUSES is a person's own (`unscorable.Refused`)."""
|
|
53
|
+
if not isinstance(e, USER_ERRORS):
|
|
54
|
+
logger.exception("%s failed", name)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
#: A form row's content: ``build(close)`` draws it; ``close`` takes it away.
|
|
58
|
+
RowBuild = Callable[[Callable[[], None]], None]
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class _NoFigure:
|
|
62
|
+
"""`Panel.figure` where there is no plot (SUITE, TEST, a tree row): a
|
|
63
|
+
subscription here is a contract misuse, and loud (decision 3)."""
|
|
64
|
+
|
|
65
|
+
def on(self, kind: str, handler: Callable[[str, dict], None]) -> None:
|
|
66
|
+
raise RuntimeError(f"no figure at this placement to subscribe {kind!r} to")
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class ShellPanel:
|
|
70
|
+
"""The container a capability owns (§4.6 "Panel"): the dialog's slot,
|
|
71
|
+
the figure hook, and `submit` — the one way an outcome reaches the
|
|
72
|
+
Session."""
|
|
73
|
+
|
|
74
|
+
figure: FigureHookProtocol
|
|
75
|
+
|
|
76
|
+
def __init__(
|
|
77
|
+
self,
|
|
78
|
+
submit: Callable[..., bool],
|
|
79
|
+
figure: FigureHookProtocol,
|
|
80
|
+
row: Callable[[str, RowBuild], None] | None = None,
|
|
81
|
+
reveal: Callable[[str], None] | None = None,
|
|
82
|
+
lead: ui.element | None = None,
|
|
83
|
+
target: RowTarget | None = None,
|
|
84
|
+
fail: Callable[[BaseException], None] | None = None,
|
|
85
|
+
kept: dict[Any, Any] | None = None,
|
|
86
|
+
placing: Placing | None = None,
|
|
87
|
+
name: str = "",
|
|
88
|
+
) -> None:
|
|
89
|
+
self._submit, self.figure = submit, figure
|
|
90
|
+
#: The plot's armed field and ⌖ buttons, where the panel is beside
|
|
91
|
+
#: a plot (the criterion editor under one), and the capability the
|
|
92
|
+
#: panel is — a ⌖ is its panel's.
|
|
93
|
+
self._placing, self._name = placing, name
|
|
94
|
+
#: `Panel.kept`: the host's, where it keeps one; else this build's.
|
|
95
|
+
self.kept: dict[Any, Any] = {} if kept is None else kept
|
|
96
|
+
#: Says a refusal the way a failed `submit` does (a drop refused
|
|
97
|
+
#: before it reaches the act); None where nothing is said.
|
|
98
|
+
self._fail = fail
|
|
99
|
+
self._row, self._reveal, self._lead = row, reveal, lead
|
|
100
|
+
#: The row this panel is drawn in, where there is one.
|
|
101
|
+
self._target = target
|
|
102
|
+
|
|
103
|
+
@property
|
|
104
|
+
def can_move(self) -> bool:
|
|
105
|
+
return self._target is not None and self._target.drag
|
|
106
|
+
|
|
107
|
+
def movable(
|
|
108
|
+
self,
|
|
109
|
+
on_drop: Callable[[str, str], None],
|
|
110
|
+
zones: Callable[[], dict[str, dict[str, str | None]]] | None = None,
|
|
111
|
+
) -> bool:
|
|
112
|
+
target = self._target
|
|
113
|
+
if target is None or not target.drag:
|
|
114
|
+
return False
|
|
115
|
+
target.on_drop, target.zones = on_drop, zones
|
|
116
|
+
if self._fail is not None:
|
|
117
|
+
target.refuse = self._fail
|
|
118
|
+
if zones is not None and target.grip is not None:
|
|
119
|
+
target.grip.classes(remove="dstf-grip-off")
|
|
120
|
+
return True
|
|
121
|
+
|
|
122
|
+
def on_click(self, handler: Callable[[], None]) -> bool:
|
|
123
|
+
# The tree card's group rows are clicked (U5, Q6); a criterion's
|
|
124
|
+
# row, on a plot or in the tree, has a click of its own — its
|
|
125
|
+
# editor — and takes no handler.
|
|
126
|
+
target = self._target
|
|
127
|
+
if target is None or not target.clickable:
|
|
128
|
+
return False
|
|
129
|
+
target.click = handler
|
|
130
|
+
return True
|
|
131
|
+
|
|
132
|
+
def moved(self, node_path: str, text: str, paths: dict[str, str]) -> None:
|
|
133
|
+
# Quiet where there is no page (a dialog): the contract's no-op.
|
|
134
|
+
if self._target is not None:
|
|
135
|
+
self._target.page.moved(node_path, text, paths)
|
|
136
|
+
|
|
137
|
+
def edited(self, paths: dict[str, str]) -> None:
|
|
138
|
+
# Quiet where there is no page (a dialog): the contract's no-op.
|
|
139
|
+
if self._target is not None:
|
|
140
|
+
self._target.page.edited(paths)
|
|
141
|
+
|
|
142
|
+
def submit(self, **inputs: Any) -> bool:
|
|
143
|
+
return self._submit(**inputs)
|
|
144
|
+
|
|
145
|
+
# Quiet where there is no plot (a dialog, a tree row, a Statistics
|
|
146
|
+
# row): the contract's no-ops.
|
|
147
|
+
@property
|
|
148
|
+
def can_place(self) -> bool:
|
|
149
|
+
return self._placing is not None
|
|
150
|
+
|
|
151
|
+
def place_button(self, pointer: Hashable, label: str) -> None:
|
|
152
|
+
if self._placing is not None:
|
|
153
|
+
self._placing.button(self._name, pointer, label)
|
|
154
|
+
|
|
155
|
+
def disarm(self) -> None:
|
|
156
|
+
if self._placing is not None:
|
|
157
|
+
self._placing.disarm()
|
|
158
|
+
|
|
159
|
+
def on_place(self, handler: Callable[[Hashable, dict], bool]) -> None:
|
|
160
|
+
placing, name = self._placing, self._name
|
|
161
|
+
if placing is None:
|
|
162
|
+
return
|
|
163
|
+
|
|
164
|
+
def placed(_primitive: str, click: dict) -> None:
|
|
165
|
+
armed = placing.armed
|
|
166
|
+
if armed is None or armed.owner != name:
|
|
167
|
+
return # another panel's field, or none
|
|
168
|
+
if handler(armed.pointer, {**click, "label": armed.label}):
|
|
169
|
+
placing.disarm()
|
|
170
|
+
|
|
171
|
+
self.figure.on(PLACE, placed)
|
|
172
|
+
|
|
173
|
+
def preview(self, leaf: dict | None, revision: int = 0) -> None:
|
|
174
|
+
if self._placing is not None:
|
|
175
|
+
said = {"leaf": leaf, "revision": revision}
|
|
176
|
+
self._placing.hook.emit(PREVIEW, said)
|
|
177
|
+
|
|
178
|
+
def on_preview(self, handler: Callable[[dict | None, int], None]) -> None:
|
|
179
|
+
if self._placing is not None:
|
|
180
|
+
self.figure.on(
|
|
181
|
+
PREVIEW,
|
|
182
|
+
lambda _primitive, said: handler(said["leaf"], said["revision"]),
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
def row(self, title: str, build: RowBuild) -> None:
|
|
186
|
+
# Loud, like `_NoFigure`: a placement that draws no row asked for
|
|
187
|
+
# one is a contract misuse, not a form to drop silently.
|
|
188
|
+
if self._row is None:
|
|
189
|
+
raise RuntimeError("no row at this placement")
|
|
190
|
+
self._row(title, build)
|
|
191
|
+
|
|
192
|
+
def retitle(self, title: str) -> None:
|
|
193
|
+
# Quiet where there is no page's row (a dialog): the contract's no-op.
|
|
194
|
+
if self._target is not None:
|
|
195
|
+
self._target.page.retitle(self._target, title)
|
|
196
|
+
|
|
197
|
+
def lead(self, build: Callable[[], None]) -> None:
|
|
198
|
+
# Loud, like `row`: a placement with no lead place asked for one.
|
|
199
|
+
if self._lead is None:
|
|
200
|
+
raise RuntimeError("no lead place at this placement")
|
|
201
|
+
self._lead.clear()
|
|
202
|
+
with self._lead:
|
|
203
|
+
build()
|
|
204
|
+
|
|
205
|
+
def reveal(self, leaf_path: str) -> None:
|
|
206
|
+
# Quiet where there is no page (a dialog): the contract's no-op.
|
|
207
|
+
if self._reveal is not None:
|
|
208
|
+
self._reveal(leaf_path)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
class RowTarget:
|
|
212
|
+
"""Where an InlineHost's panels open their rows: a slot the page owns
|
|
213
|
+
under one row, and the page's one-open-at-a-time rule. ``key`` names the
|
|
214
|
+
slot page-wide (``tree:<path>``, ``crit:<index>:<path>`` or
|
|
215
|
+
``plot:<index>``); ``node`` is the node path the forms act on — what the
|
|
216
|
+
page watches to rebuild an open row. ``index`` is the plot the row is
|
|
217
|
+
on (None for a tree row): the plot a criterion revealed from this row
|
|
218
|
+
opens on, when that plot holds it."""
|
|
219
|
+
|
|
220
|
+
def __init__(
|
|
221
|
+
self, page: Any, key: str, row: ui.row, node: str, index: int | None
|
|
222
|
+
) -> None:
|
|
223
|
+
self.page, self.key, self.row, self.node = page, key, row, node
|
|
224
|
+
self.index = index
|
|
225
|
+
#: The slot under the row, set by the row's owner once it exists:
|
|
226
|
+
#: the host is built INSIDE the row, before the slot below it is.
|
|
227
|
+
self.slot: ui.column | None = None
|
|
228
|
+
#: Hides the errors the host drawn in this row shows; set by that
|
|
229
|
+
#: host. The page calls it when a form row opens or closes under
|
|
230
|
+
#: THIS row, and for no other row's.
|
|
231
|
+
self.hush: Callable[[], None] = lambda: None
|
|
232
|
+
#: The row's lead place, beside its status, where a panel draws
|
|
233
|
+
#: what the node's kind is read from (`Panel.lead`); None in a row
|
|
234
|
+
#: that has none (a criteria row). Emptied with the host.
|
|
235
|
+
self.lead: ui.element | None = None
|
|
236
|
+
#: Whether a node drawn in this row may be moved from it (U4, V2):
|
|
237
|
+
#: set by the tree card, and by no other row's owner.
|
|
238
|
+
self.drag = False
|
|
239
|
+
#: The row's drag handle (the tree card's, hidden until a panel
|
|
240
|
+
#: makes the row draggable; None on the root's), and what a panel
|
|
241
|
+
#: registered through `Panel.movable`: a drop on the row, the zones
|
|
242
|
+
#: of a drag from it.
|
|
243
|
+
self.grip: ui.element | None = None
|
|
244
|
+
self.on_drop: Callable[[str, str], None] | None = None
|
|
245
|
+
self.zones: Callable[[], dict[str, dict[str, str | None]]] | None = None
|
|
246
|
+
#: Says a drop refused before it reached the panel's act, the way
|
|
247
|
+
#: that panel says its own refusals (`TreeCard.dropped`). Set with
|
|
248
|
+
#: `on_drop`, by `Panel.movable` — every panel in a row has a way
|
|
249
|
+
#: to say a refusal (`InlineHost`) — and read only where `on_drop`
|
|
250
|
+
#: is set: no default, which nothing could reach.
|
|
251
|
+
self.refuse: Callable[[BaseException], None]
|
|
252
|
+
#: Whether a click on this row is the panels' to say (U5, Q6): set
|
|
253
|
+
#: by the tree card on a group's row, and by no other row's owner. What a panel
|
|
254
|
+
#: registered for it (`Panel.on_click`), or None.
|
|
255
|
+
self.clickable = False
|
|
256
|
+
self.click: Callable[[], None] | None = None
|
|
257
|
+
|
|
258
|
+
def open(self, title: str, build: RowBuild) -> None:
|
|
259
|
+
self.page.open_row(self, title, build)
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
class ActionBar:
|
|
263
|
+
"""The bar and its one dialog, at SUITE or TEST."""
|
|
264
|
+
|
|
265
|
+
def __init__(
|
|
266
|
+
self,
|
|
267
|
+
shell: Shell,
|
|
268
|
+
placement: Placement,
|
|
269
|
+
selection_of: Callable[[], Selection],
|
|
270
|
+
*,
|
|
271
|
+
marker: str,
|
|
272
|
+
icons: bool = False,
|
|
273
|
+
emphasis: Callable[[Capability], Emphasis | None] | None = None,
|
|
274
|
+
apply: Callable[[Outcome | Sequence[Outcome]], None] | None = None,
|
|
275
|
+
stale: Callable[[StaleSpecError], None] | None = None,
|
|
276
|
+
) -> None:
|
|
277
|
+
self.shell, self.placement, self.selection_of = shell, placement, selection_of
|
|
278
|
+
self.marker = marker
|
|
279
|
+
#: Where an act refused for a spec changed on disk is said: the
|
|
280
|
+
#: page's header (`Header.stale`), the dialog Save's own refusal
|
|
281
|
+
#: uses — with Reload in it — in place of a line in this bar's.
|
|
282
|
+
self.stale = stale
|
|
283
|
+
#: How an act's outcome is applied: the Session's own, unless the
|
|
284
|
+
#: page the bar is on applies it (`TestPage.apply`, which notes the
|
|
285
|
+
#: `spec` revisions its own acts produced).
|
|
286
|
+
self.apply = apply
|
|
287
|
+
#: Draw each capability that declares an ``icon`` (a Material icon
|
|
288
|
+
#: name) as an icon button; one that declares none keeps its text.
|
|
289
|
+
self.icons = icons
|
|
290
|
+
#: How the page's situation draws each capability: NEXT filled,
|
|
291
|
+
#: MENU in the ⋮ menu, None quiet. Without it every button is
|
|
292
|
+
#: today's outlined one.
|
|
293
|
+
self.emphasis = emphasis
|
|
294
|
+
self.errors: dict[
|
|
295
|
+
str, str
|
|
296
|
+
] = {} # capability name -> the error its dialog shows
|
|
297
|
+
self.actions: dict[str, Action | IconAction | MenuAction] = {}
|
|
298
|
+
#: What the buttons were built from: each placed capability's name
|
|
299
|
+
#: and its emphasis. A move in either rebuilds them.
|
|
300
|
+
self.built: list[tuple[str, Emphasis | None]] = []
|
|
301
|
+
self.dialog = ui.dialog() # one element, rebuilt per open
|
|
302
|
+
self.container = (
|
|
303
|
+
ui.row().classes("dstf-actions items-center gap-3 wrap").mark(marker)
|
|
304
|
+
)
|
|
305
|
+
self.refresh()
|
|
306
|
+
|
|
307
|
+
def clear(self) -> None:
|
|
308
|
+
"""No buttons, and none built from: the next `refresh` draws them
|
|
309
|
+
afresh (a page whose test has gone)."""
|
|
310
|
+
self.dialog.close()
|
|
311
|
+
self.container.clear()
|
|
312
|
+
self.actions, self.built = {}, []
|
|
313
|
+
|
|
314
|
+
def refresh(self) -> None:
|
|
315
|
+
"""Captions and availability from the current selection; the buttons
|
|
316
|
+
are rebuilt only when the capabilities placed here, or the emphasis
|
|
317
|
+
the page's situation gives them, changed."""
|
|
318
|
+
view = self.shell.view_for(self.selection_of())
|
|
319
|
+
placed = self.shell.placed(self.placement, view)
|
|
320
|
+
emphasis = self.emphasis
|
|
321
|
+
built = [
|
|
322
|
+
(cap.name, emphasis(cap) if emphasis is not None else None)
|
|
323
|
+
for cap, _ in placed
|
|
324
|
+
]
|
|
325
|
+
if built != self.built:
|
|
326
|
+
self._build([cap for cap, _ in placed], [e for _, e in built])
|
|
327
|
+
self.built = built
|
|
328
|
+
loading = self.shell.load.in_progress
|
|
329
|
+
for cap, avail in placed:
|
|
330
|
+
if loading:
|
|
331
|
+
reason: str | None = LOADING
|
|
332
|
+
elif avail is True:
|
|
333
|
+
reason = None
|
|
334
|
+
else:
|
|
335
|
+
reason = avail if isinstance(avail, str) else "not available"
|
|
336
|
+
try:
|
|
337
|
+
caption = cap.caption(view)
|
|
338
|
+
except Exception as e: # the label is the fallback; the page draws
|
|
339
|
+
caption = cap.label
|
|
340
|
+
self.shell.report_once(cap.name, "caption", e)
|
|
341
|
+
self.actions[cap.name].set(caption, reason)
|
|
342
|
+
|
|
343
|
+
def _build(self, caps: list[Capability], emphases: list[Emphasis | None]) -> None:
|
|
344
|
+
self.container.clear()
|
|
345
|
+
self.actions = {}
|
|
346
|
+
menu = [cap for cap, e in zip(caps, emphases) if e is Emphasis.MENU]
|
|
347
|
+
with self.container:
|
|
348
|
+
for cap, e in zip(caps, emphases):
|
|
349
|
+
if e is Emphasis.MENU:
|
|
350
|
+
continue
|
|
351
|
+
marker = f"action:{cap.name}"
|
|
352
|
+
icon = getattr(cap, "icon", None) if self.icons else None
|
|
353
|
+
if icon is not None:
|
|
354
|
+
self.actions[cap.name] = IconAction(
|
|
355
|
+
partial(self.open, cap), icon=icon, marker=marker
|
|
356
|
+
)
|
|
357
|
+
continue
|
|
358
|
+
if self.emphasis is None:
|
|
359
|
+
props = "outline dense no-caps"
|
|
360
|
+
elif e is Emphasis.NEXT:
|
|
361
|
+
props = "unelevated dense no-caps color=primary"
|
|
362
|
+
else:
|
|
363
|
+
props = "flat dense no-caps"
|
|
364
|
+
self.actions[cap.name] = Action(
|
|
365
|
+
partial(self.open, cap), props=props, marker=marker
|
|
366
|
+
)
|
|
367
|
+
if menu:
|
|
368
|
+
# Named, with no tooltip: a tooltip covered the open menu's
|
|
369
|
+
# first item. It says whether its menu is open, as the
|
|
370
|
+
# header's ⋮ does.
|
|
371
|
+
more = (
|
|
372
|
+
ui.button(icon="more_vert")
|
|
373
|
+
.props('flat dense round aria-label="More actions"')
|
|
374
|
+
.props('aria-haspopup="menu" aria-expanded="false"')
|
|
375
|
+
.mark("action-more")
|
|
376
|
+
)
|
|
377
|
+
with more, ui.menu() as opened:
|
|
378
|
+
opened.on_value_change(
|
|
379
|
+
lambda e, more=more: more.props(
|
|
380
|
+
f'aria-expanded="{"true" if e.value else "false"}"'
|
|
381
|
+
)
|
|
382
|
+
)
|
|
383
|
+
for cap in menu:
|
|
384
|
+
self.actions[cap.name] = MenuAction(
|
|
385
|
+
partial(self.open, cap),
|
|
386
|
+
marker=f"menu-action:{cap.name}",
|
|
387
|
+
label=cap.label,
|
|
388
|
+
)
|
|
389
|
+
|
|
390
|
+
def open(self, cap: Capability) -> None:
|
|
391
|
+
"""The capability's panel, in the dialog, over the selection as it is
|
|
392
|
+
at this click."""
|
|
393
|
+
view = self.shell.view_for(self.selection_of())
|
|
394
|
+
|
|
395
|
+
def close() -> None:
|
|
396
|
+
self.dialog.close()
|
|
397
|
+
|
|
398
|
+
self.dialog.clear()
|
|
399
|
+
with self.dialog, ui.card().classes("dstf-dialog"):
|
|
400
|
+
ui.label(cap.label).classes("text-h6")
|
|
401
|
+
error = ui.label(self.errors.get(cap.name, "")).classes("text-negative")
|
|
402
|
+
error.mark(f"error:{cap.name}")
|
|
403
|
+
error.set_visibility(bool(error.text))
|
|
404
|
+
slot = ui.column().classes("w-full").mark(f"panel:{cap.name}")
|
|
405
|
+
# The form row's column, below the panel, exists BEFORE the
|
|
406
|
+
# panel runs: a panel may open its row while it is built, and
|
|
407
|
+
# a column made after it raised NameError from `row` (the Task
|
|
408
|
+
# 5 review).
|
|
409
|
+
form = ui.column().classes("w-full gap-2")
|
|
410
|
+
|
|
411
|
+
def submit(**inputs: Any) -> bool:
|
|
412
|
+
try:
|
|
413
|
+
outcome = cap.act(view, **inputs)
|
|
414
|
+
(self.apply or self.shell.session.apply)(outcome)
|
|
415
|
+
except Exception as e: # shown here; a raising act changes nothing
|
|
416
|
+
if isinstance(e, StaleSpecError) and self.stale is not None:
|
|
417
|
+
# Said where Save's own stale-file refusal is, with
|
|
418
|
+
# the way out beside it: this dialog closes for it.
|
|
419
|
+
close()
|
|
420
|
+
self.stale(e)
|
|
421
|
+
return False
|
|
422
|
+
log_unless_users(e, cap.name)
|
|
423
|
+
self.errors[cap.name] = user_text(e)
|
|
424
|
+
error.set_text(self.errors[cap.name])
|
|
425
|
+
error.set_visibility(True)
|
|
426
|
+
return False
|
|
427
|
+
self.errors.pop(cap.name, None)
|
|
428
|
+
if outcome is not None:
|
|
429
|
+
# Like `InlineHost.on_applied`: an act returning None
|
|
430
|
+
# changed nothing. Every plot sends `{"autosize": true}`
|
|
431
|
+
# at mount, and a subscribing panel answering it with
|
|
432
|
+
# None must not close the dialog it sits in.
|
|
433
|
+
close()
|
|
434
|
+
return True
|
|
435
|
+
|
|
436
|
+
def row(title: str, build: RowBuild) -> None:
|
|
437
|
+
"""A dialog has no row to open one under: the form is drawn
|
|
438
|
+
in the dialog, below the panel, one at a time."""
|
|
439
|
+
form.clear()
|
|
440
|
+
|
|
441
|
+
def close_row() -> None:
|
|
442
|
+
form.clear()
|
|
443
|
+
|
|
444
|
+
with form:
|
|
445
|
+
ui.label(title).classes("text-subtitle2")
|
|
446
|
+
build(close_row)
|
|
447
|
+
|
|
448
|
+
with slot:
|
|
449
|
+
try:
|
|
450
|
+
cap.panel(view, ShellPanel(submit, _NoFigure(), row=row))
|
|
451
|
+
except Exception as e: # shown in the slot, never breaks the page
|
|
452
|
+
ui.label(f"{type(e).__name__}: {e}").classes("text-negative")
|
|
453
|
+
ui.button("Close", on_click=close).props("flat dense no-caps").mark(
|
|
454
|
+
f"close:{cap.name}"
|
|
455
|
+
)
|
|
456
|
+
self.dialog.open()
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
class InlineHost:
|
|
460
|
+
"""Every capability placed at `placement`, its panel built inline in
|
|
461
|
+
this host's column — no button, no heading (brief §3.2: Edit criteria
|
|
462
|
+
and Edit on plot read as one editor; a node's controls are a panel).
|
|
463
|
+
An unavailable capability keeps its place and says why (brief §3.3) —
|
|
464
|
+
once per sentence: a second capability refused for the same reason
|
|
465
|
+
draws no line of its own.
|
|
466
|
+
`build()` rebuilds from the selection as it is now; `clear()` empties
|
|
467
|
+
the column and drops this host's figure handlers and the form row its
|
|
468
|
+
panels opened. A raising act shows above that capability's panel — in
|
|
469
|
+
a host drawn in a row, on a line of its own under the row — and
|
|
470
|
+
changes nothing. The error belongs to that row: the next success
|
|
471
|
+
hides it in place, and so does a form row opening or closing under
|
|
472
|
+
THAT row (`hush`), or the host rebuilding; another row's form does
|
|
473
|
+
not. The panel is not rebuilt, so a field being typed in survives.
|
|
474
|
+
`on_applied` is called with that capability's name when its act
|
|
475
|
+
returned an outcome, and only then: an act returning ``None`` changed
|
|
476
|
+
nothing, and the rebuild it would trigger discards a half-typed row
|
|
477
|
+
for nothing. `follow()` is how a host that is never rebuilt keeps up
|
|
478
|
+
with `available`: a capability's answer can move without the tree's
|
|
479
|
+
shape moving (until D150 a job's lock did, on every run) — and with
|
|
480
|
+
``kept``, when what a panel kept there was dropped.
|
|
481
|
+
``kept(name)`` is `Panel.kept` for capability ``name``'s panel in this
|
|
482
|
+
host: the dict its owner keeps across this host's rebuilds. Without
|
|
483
|
+
one, each build of a panel gets a dict of its own.
|
|
484
|
+
``on_said`` hears what this host's error lines say, as they say it:
|
|
485
|
+
the text of a refused act, and ``None`` when an act went through (its
|
|
486
|
+
line hidden). Whoever shows that text somewhere else too — a plot, for
|
|
487
|
+
a drag — shows this one, not a second wording."""
|
|
488
|
+
|
|
489
|
+
def __init__(
|
|
490
|
+
self,
|
|
491
|
+
shell: Shell,
|
|
492
|
+
placement: Placement,
|
|
493
|
+
selection_of: Callable[[], Selection],
|
|
494
|
+
*,
|
|
495
|
+
figure: FigureHook | None,
|
|
496
|
+
marker: str,
|
|
497
|
+
on_applied: Callable[[str], None] | None = None,
|
|
498
|
+
row: RowTarget | None = None,
|
|
499
|
+
apply: Callable[[Outcome | Sequence[Outcome]], None] | None = None,
|
|
500
|
+
kept: Callable[[str], dict[Any, Any]] | None = None,
|
|
501
|
+
on_said: Callable[[str | None], None] | None = None,
|
|
502
|
+
placing: Placing | None = None,
|
|
503
|
+
) -> None:
|
|
504
|
+
self.shell, self.placement, self.selection_of = shell, placement, selection_of
|
|
505
|
+
self.figure, self.marker, self.on_applied = figure, marker, on_applied
|
|
506
|
+
self.on_said = on_said
|
|
507
|
+
#: The plot's ⌖ buttons and armed field (D149), lent to every panel
|
|
508
|
+
#: of this host; None for a host beside no plot.
|
|
509
|
+
self.placing = placing
|
|
510
|
+
self.row = row
|
|
511
|
+
self.kept = kept
|
|
512
|
+
#: capability name -> the `Panel.kept` its panel was built with
|
|
513
|
+
self.handed: dict[str, dict[Any, Any]] = {}
|
|
514
|
+
#: As `ActionBar.apply`: the page's, where the host is on one.
|
|
515
|
+
self.apply = apply
|
|
516
|
+
if row is not None:
|
|
517
|
+
row.hush = self.hush
|
|
518
|
+
self.errors: dict[str, ui.label] = {}
|
|
519
|
+
self.cells: dict[str, ui.column] = {} # capability name -> its place
|
|
520
|
+
#: capability name -> the `available` answer its place was built on
|
|
521
|
+
self.answers: dict[str, bool | str] = {}
|
|
522
|
+
self.column = ui.column().classes("w-full gap-2").mark(marker)
|
|
523
|
+
# A click inside the editor is the editor's: without this, a click on
|
|
524
|
+
# a node's wrap icon also reached the row that hosts it and opened its
|
|
525
|
+
# editor (the review, in Chromium).
|
|
526
|
+
self.column.on("click", js_handler="(e) => e.stopPropagation()")
|
|
527
|
+
|
|
528
|
+
def _owner(self, name: str) -> str:
|
|
529
|
+
return f"{self.marker}:{name}"
|
|
530
|
+
|
|
531
|
+
def hush(self) -> None:
|
|
532
|
+
"""Hide every error this host shows, as a form row opens or closes
|
|
533
|
+
under this host's row: a row's error goes with the row. A refused
|
|
534
|
+
act's error stayed up after its form was reopened (the Task 6
|
|
535
|
+
review)."""
|
|
536
|
+
for label in self.errors.values():
|
|
537
|
+
if not label.is_deleted:
|
|
538
|
+
label.set_visibility(False)
|
|
539
|
+
|
|
540
|
+
def clear(self) -> None:
|
|
541
|
+
if self.figure is not None:
|
|
542
|
+
for name in self.cells:
|
|
543
|
+
self.figure.clear(self._owner(name))
|
|
544
|
+
if self.row is not None:
|
|
545
|
+
# A form row lives only as long as the host whose panel opened
|
|
546
|
+
# it: rebuilt to its why-line (under the single job's lock,
|
|
547
|
+
# before D150), the host said "rerun is running" while its
|
|
548
|
+
# row stayed open with live buttons, and an
|
|
549
|
+
# act from it staged nothing and showed nothing — the error
|
|
550
|
+
# label had gone with the rebuild (the Task 5 review).
|
|
551
|
+
self.row.page.drop_row(self.row)
|
|
552
|
+
# Its error labels live in the row's slot, not in the column.
|
|
553
|
+
for label in self.errors.values():
|
|
554
|
+
if not label.is_deleted:
|
|
555
|
+
label.delete()
|
|
556
|
+
# And what its panels drew in the row's lead place: a host
|
|
557
|
+
# rebuilt to its why-line leaves the kind to the row's text.
|
|
558
|
+
if self.row.lead is not None and not self.row.lead.is_deleted:
|
|
559
|
+
self.row.lead.clear()
|
|
560
|
+
# And what they registered for the drag: a host rebuilt to its
|
|
561
|
+
# why-line takes no drop and offers no handle.
|
|
562
|
+
self.row.on_drop = self.row.zones = None
|
|
563
|
+
# And its click: a host rebuilt to its why-line opens no form.
|
|
564
|
+
self.row.click = None
|
|
565
|
+
if self.row.grip is not None and not self.row.grip.is_deleted:
|
|
566
|
+
self.row.grip.classes(add="dstf-grip-off")
|
|
567
|
+
self.column.clear()
|
|
568
|
+
self.errors, self.cells, self.answers, self.handed = {}, {}, {}, {}
|
|
569
|
+
|
|
570
|
+
def build(self, skip: str | None = None) -> None:
|
|
571
|
+
"""Rebuild every capability's place — but ``skip``'s, left untouched
|
|
572
|
+
(the panel whose own submit this follows: rebuilding it would destroy
|
|
573
|
+
the control being typed in). A changed set of capabilities rebuilds
|
|
574
|
+
them all. ``skip`` misleads in one state: a skipped capability that
|
|
575
|
+
has become UNAVAILABLE keeps its stale panel and gets no why-line
|
|
576
|
+
until the next full ``build()``."""
|
|
577
|
+
view = self.shell.view_for(self.selection_of())
|
|
578
|
+
placed = self.shell.placed(self.placement, view)
|
|
579
|
+
if skip is None or [cap.name for cap, _ in placed] != list(self.cells):
|
|
580
|
+
self.clear()
|
|
581
|
+
skip = None
|
|
582
|
+
# The why-lines drawn so far. Edit criteria and Edit on plot are both
|
|
583
|
+
# refused on a test the Session has no tree for, and the editor
|
|
584
|
+
# read the same sentence twice (the review).
|
|
585
|
+
said: set[str] = set()
|
|
586
|
+
for cap, avail in placed:
|
|
587
|
+
if cap.name == skip:
|
|
588
|
+
continue
|
|
589
|
+
if cap.name not in self.cells:
|
|
590
|
+
with self.column:
|
|
591
|
+
self.cells[cap.name] = ui.column().classes("w-full gap-1")
|
|
592
|
+
elif self.figure is not None:
|
|
593
|
+
self.figure.clear(self._owner(cap.name))
|
|
594
|
+
cell = self.cells[cap.name]
|
|
595
|
+
cell.clear()
|
|
596
|
+
with cell:
|
|
597
|
+
self._build_one(cap, avail, view, said)
|
|
598
|
+
# Not the skipped capability's: its place still shows the answer
|
|
599
|
+
# it was built on, and `follow` is what brings it back in line.
|
|
600
|
+
self.answers[cap.name] = avail
|
|
601
|
+
if self.placing is not None:
|
|
602
|
+
# A field whose ⌖ this build did not draw again is not armed.
|
|
603
|
+
self.placing.settle()
|
|
604
|
+
|
|
605
|
+
def follow(self) -> None:
|
|
606
|
+
"""Rebuild when an `available` answer has moved since this host was
|
|
607
|
+
built, and only then (the Task 4 review): a row that is repainted
|
|
608
|
+
rather than rebuilt otherwise keeps the answer of the moment it was
|
|
609
|
+
drawn — under the single job's lock (before D150) a tree opened
|
|
610
|
+
during a rerun kept "rerun is running on …" after the job ended,
|
|
611
|
+
and one opened before it kept controls the lock refused. A full
|
|
612
|
+
build resets the error labels, which is
|
|
613
|
+
why hiding a stale one is not this method's job.
|
|
614
|
+
|
|
615
|
+
Also when what a panel KEPT (`Panel.kept`) has been dropped under
|
|
616
|
+
it — a Discard, a Save, another tab's edit: its controls still show
|
|
617
|
+
what the dropped dict held (a cell left blank), and nothing would
|
|
618
|
+
bring it back at the next rebuild."""
|
|
619
|
+
view = self.shell.view_for(self.selection_of())
|
|
620
|
+
answers = {
|
|
621
|
+
cap.name: avail for cap, avail in self.shell.placed(self.placement, view)
|
|
622
|
+
}
|
|
623
|
+
kept = self.kept
|
|
624
|
+
dropped = kept is not None and any(
|
|
625
|
+
held and kept(name) is not held for name, held in self.handed.items()
|
|
626
|
+
)
|
|
627
|
+
if answers != self.answers or dropped:
|
|
628
|
+
self.build()
|
|
629
|
+
|
|
630
|
+
def _build_one(
|
|
631
|
+
self, cap: Capability, avail: bool | str, view: Any, said: set[str]
|
|
632
|
+
) -> None:
|
|
633
|
+
slot = self.row.slot if self.row is not None else None
|
|
634
|
+
if slot is not None:
|
|
635
|
+
# A host drawn in a row puts its error on a line of its own in
|
|
636
|
+
# the row's slot, under the icons: in the host, a long refusal
|
|
637
|
+
# widened the host and pushed its icons left of every other
|
|
638
|
+
# row's (the Task 6 review, in Chromium).
|
|
639
|
+
with slot:
|
|
640
|
+
error = ui.label("").classes("text-negative text-xs self-end")
|
|
641
|
+
else:
|
|
642
|
+
error = ui.label("").classes("text-negative text-xs")
|
|
643
|
+
error.mark(f"error:{cap.name}")
|
|
644
|
+
error.set_visibility(False)
|
|
645
|
+
self.errors[cap.name] = error
|
|
646
|
+
if avail is not True:
|
|
647
|
+
sentence = avail if isinstance(avail, str) else "not available"
|
|
648
|
+
if sentence not in said:
|
|
649
|
+
said.add(sentence)
|
|
650
|
+
ui.label(sentence).classes("dstf-why").mark(f"why:{cap.name}")
|
|
651
|
+
return
|
|
652
|
+
|
|
653
|
+
def fail(e: BaseException) -> None:
|
|
654
|
+
log_unless_users(e, cap.name)
|
|
655
|
+
error.set_text(user_text(e))
|
|
656
|
+
error.set_visibility(True)
|
|
657
|
+
if self.on_said is not None:
|
|
658
|
+
self.on_said(error.text)
|
|
659
|
+
if self.row is not None:
|
|
660
|
+
# Said under its row: a tree row inside a folded group is
|
|
661
|
+
# shown (U5, R2).
|
|
662
|
+
self.row.page.unfold_to(self.row)
|
|
663
|
+
|
|
664
|
+
def submit(**inputs: Any) -> bool:
|
|
665
|
+
try:
|
|
666
|
+
outcome = cap.act(view, **inputs)
|
|
667
|
+
(self.apply or self.shell.session.apply)(outcome)
|
|
668
|
+
except Exception as e: # shown, and changes nothing
|
|
669
|
+
fail(e)
|
|
670
|
+
return False
|
|
671
|
+
error.set_visibility(False)
|
|
672
|
+
if self.on_said is not None:
|
|
673
|
+
self.on_said(None)
|
|
674
|
+
if outcome is not None and self.on_applied is not None:
|
|
675
|
+
self.on_applied(cap.name)
|
|
676
|
+
return True
|
|
677
|
+
|
|
678
|
+
figure: FigureHookProtocol = _NoFigure()
|
|
679
|
+
if self.figure is not None:
|
|
680
|
+
figure = self.figure.owned(self._owner(cap.name))
|
|
681
|
+
self.handed[cap.name] = {} if self.kept is None else self.kept(cap.name)
|
|
682
|
+
with ui.column().classes("w-full gap-2").mark(f"panel:{cap.name}"):
|
|
683
|
+
try:
|
|
684
|
+
cap.panel(
|
|
685
|
+
view,
|
|
686
|
+
ShellPanel(
|
|
687
|
+
submit,
|
|
688
|
+
figure,
|
|
689
|
+
row=None if self.row is None else self.row.open,
|
|
690
|
+
reveal=None
|
|
691
|
+
if self.row is None
|
|
692
|
+
else partial(self.row.page.reveal, index=self.row.index),
|
|
693
|
+
lead=None if self.row is None else self.row.lead,
|
|
694
|
+
target=self.row,
|
|
695
|
+
fail=fail,
|
|
696
|
+
kept=self.handed[cap.name],
|
|
697
|
+
placing=self.placing,
|
|
698
|
+
name=cap.name,
|
|
699
|
+
),
|
|
700
|
+
)
|
|
701
|
+
except Exception as e: # shown in the slot, never breaks the page
|
|
702
|
+
ui.label(f"{type(e).__name__}: {e}").classes("text-negative")
|