itasc 0.2.0.dev0__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.
- itasc/__init__.py +16 -0
- itasc/cellpose/__init__.py +31 -0
- itasc/cellpose/cellpose_runner.py +334 -0
- itasc/cellpose/divergence_maps.py +380 -0
- itasc/cellpose/flow_following.py +283 -0
- itasc/cellpose/joint.py +98 -0
- itasc/cellpose/napari.yaml +10 -0
- itasc/cellpose/native_masks.py +313 -0
- itasc/cellpose/retrack.py +232 -0
- itasc/cellpose/shape.py +51 -0
- itasc/cellpose/track_laptrack.py +279 -0
- itasc/contact_analysis/__init__.py +61 -0
- itasc/contact_analysis/_provenance.py +29 -0
- itasc/contact_analysis/catalog.py +393 -0
- itasc/contact_analysis/config.py +155 -0
- itasc/contact_analysis/contacts/__init__.py +24 -0
- itasc/contact_analysis/contacts/batch.py +158 -0
- itasc/contact_analysis/contacts/build.py +837 -0
- itasc/contact_analysis/contacts/contact_labels.py +136 -0
- itasc/contact_analysis/contacts/neighborhood.py +146 -0
- itasc/contact_analysis/contacts/reader.py +58 -0
- itasc/contact_analysis/contacts/signed_contact_length.py +143 -0
- itasc/contact_analysis/curation.py +185 -0
- itasc/contact_analysis/dynamics/__init__.py +43 -0
- itasc/contact_analysis/dynamics/collective.py +206 -0
- itasc/contact_analysis/dynamics/kinematics.py +278 -0
- itasc/contact_analysis/dynamics/msd.py +165 -0
- itasc/contact_analysis/dynamics/store.py +251 -0
- itasc/contact_analysis/dynamics/trajectories.py +100 -0
- itasc/contact_analysis/frame_interval.py +85 -0
- itasc/contact_analysis/napari.yaml +10 -0
- itasc/contact_analysis/pipeline.py +313 -0
- itasc/contact_analysis/pixel_size.py +166 -0
- itasc/contact_analysis/quantifier.py +204 -0
- itasc/contact_analysis/quantifiers/__init__.py +7 -0
- itasc/contact_analysis/quantifiers/_contacts_derived.py +42 -0
- itasc/contact_analysis/quantifiers/cell_density.py +65 -0
- itasc/contact_analysis/quantifiers/cell_dynamics.py +81 -0
- itasc/contact_analysis/quantifiers/cell_shape.py +32 -0
- itasc/contact_analysis/quantifiers/contacts.py +72 -0
- itasc/contact_analysis/quantifiers/neighbor_count.py +26 -0
- itasc/contact_analysis/quantifiers/nucleus_dynamics.py +77 -0
- itasc/contact_analysis/quantifiers/nucleus_shape.py +35 -0
- itasc/contact_analysis/quantifiers/shape_relational.py +34 -0
- itasc/contact_analysis/quantifiers/signed_contact_length.py +44 -0
- itasc/contact_analysis/records.py +129 -0
- itasc/contact_analysis/shape/__init__.py +19 -0
- itasc/contact_analysis/shape/core.py +141 -0
- itasc/contact_analysis/shape/relational.py +134 -0
- itasc/contact_analysis/shape_tables.py +387 -0
- itasc/core/__init__.py +0 -0
- itasc/core/cancellation.py +15 -0
- itasc/core/commit.py +61 -0
- itasc/core/imageops.py +34 -0
- itasc/core/label_store.py +53 -0
- itasc/core/lineage.py +113 -0
- itasc/core/logging.py +51 -0
- itasc/core/paths.py +34 -0
- itasc/core/tiff.py +13 -0
- itasc/correction/__init__.py +25 -0
- itasc/correction/labels.py +983 -0
- itasc/napari/__init__.py +7 -0
- itasc/napari/_experiments_panel.py +1091 -0
- itasc/napari/_flow_layout.py +98 -0
- itasc/napari/_icons.py +126 -0
- itasc/napari/_napari_compat.py +118 -0
- itasc/napari/_paths.py +231 -0
- itasc/napari/_preview_cache.py +60 -0
- itasc/napari/_spotlight.py +46 -0
- itasc/napari/_stage_loader.py +146 -0
- itasc/napari/_stage_status.py +110 -0
- itasc/napari/_standalone_paths.py +89 -0
- itasc/napari/_state.py +146 -0
- itasc/napari/_status_rail.py +128 -0
- itasc/napari/_track_render.py +114 -0
- itasc/napari/_widget_helpers.py +236 -0
- itasc/napari/aggregate_widget.py +339 -0
- itasc/napari/candidate_gallery_controller.py +285 -0
- itasc/napari/cell_workflow_widget.py +1405 -0
- itasc/napari/cellpose_segment_track_widget.py +1253 -0
- itasc/napari/cellpose_widget.py +1034 -0
- itasc/napari/contact_analysis/__init__.py +6 -0
- itasc/napari/contact_analysis/curation_controller.py +94 -0
- itasc/napari/contact_analysis/plugins/__init__.py +102 -0
- itasc/napari/contact_analysis/plugins/catalog_summary.py +56 -0
- itasc/napari/contact_analysis/plugins/curation.py +247 -0
- itasc/napari/contact_analysis/plugins/visualize_contacts.py +64 -0
- itasc/napari/contact_analysis_widget.py +951 -0
- itasc/napari/contact_visualization.py +895 -0
- itasc/napari/correction/__init__.py +8 -0
- itasc/napari/correction/_correction_anchor.py +54 -0
- itasc/napari/correction/_correction_candidate_panel.py +259 -0
- itasc/napari/correction/_correction_candidates.py +169 -0
- itasc/napari/correction/_correction_centroids.py +132 -0
- itasc/napari/correction/_correction_commit.py +71 -0
- itasc/napari/correction/_correction_events.py +39 -0
- itasc/napari/correction/_correction_keymap.py +80 -0
- itasc/napari/correction/_correction_layer_lifecycle.py +137 -0
- itasc/napari/correction/_correction_layer_loader.py +86 -0
- itasc/napari/correction/_correction_navigation.py +97 -0
- itasc/napari/correction/_correction_paint.py +46 -0
- itasc/napari/correction/_correction_playback.py +68 -0
- itasc/napari/correction/_correction_protection.py +44 -0
- itasc/napari/correction/_correction_takeover.py +64 -0
- itasc/napari/correction/_correction_track_accordion.py +566 -0
- itasc/napari/correction/_correction_track_path.py +388 -0
- itasc/napari/correction/_correction_ui.py +367 -0
- itasc/napari/correction/_correction_ui_nucleus.py +393 -0
- itasc/napari/correction/_correction_utils.py +204 -0
- itasc/napari/correction/_correction_validation.py +83 -0
- itasc/napari/correction/cell_correction_widget.py +1190 -0
- itasc/napari/correction/correction_widget.py +1363 -0
- itasc/napari/correction/nucleus_correction_widget.py +2185 -0
- itasc/napari/divergence_maps_widget.py +795 -0
- itasc/napari/lineage_canvas_controller.py +351 -0
- itasc/napari/main_widget.py +1184 -0
- itasc/napari/nucleus_atom_extraction_widget.py +648 -0
- itasc/napari/nucleus_db_browser_widget.py +1442 -0
- itasc/napari/nucleus_pipeline_widget.py +579 -0
- itasc/napari/nucleus_segmentation_inputs_widget.py +21 -0
- itasc/napari/nucleus_tracking_inputs_widget.py +248 -0
- itasc/napari/nucleus_workflow_widget.py +963 -0
- itasc/napari/track_path_controller.py +387 -0
- itasc/napari/ui_gate.py +266 -0
- itasc/napari/ui_style.py +503 -0
- itasc/napari/validated_overlay_controller.py +254 -0
- itasc/napari/widgets.py +616 -0
- itasc/napari.yaml +45 -0
- itasc/segmentation/__init__.py +60 -0
- itasc/segmentation/cell_divergence_segmentation.py +433 -0
- itasc/segmentation/cell_label_icm.py +637 -0
- itasc/segmentation/contour_filtering.py +128 -0
- itasc/segmentation/lineage.py +18 -0
- itasc/segmentation/nucleus_segmentation.py +35 -0
- itasc/tracking_ultrack/__init__.py +12 -0
- itasc/tracking_ultrack/_node_geometry.py +191 -0
- itasc/tracking_ultrack/atoms.py +470 -0
- itasc/tracking_ultrack/config.py +76 -0
- itasc/tracking_ultrack/corrections.py +1213 -0
- itasc/tracking_ultrack/db_build.py +492 -0
- itasc/tracking_ultrack/db_query.py +952 -0
- itasc/tracking_ultrack/export.py +133 -0
- itasc/tracking_ultrack/extend.py +243 -0
- itasc/tracking_ultrack/ingest.py +83 -0
- itasc/tracking_ultrack/linking.py +286 -0
- itasc/tracking_ultrack/multi_threshold.py +46 -0
- itasc/tracking_ultrack/napari.yaml +10 -0
- itasc/tracking_ultrack/reseed.py +456 -0
- itasc/tracking_ultrack/retracker.py +153 -0
- itasc/tracking_ultrack/scoring.py +74 -0
- itasc/tracking_ultrack/seed_prior.py +175 -0
- itasc/tracking_ultrack/solve.py +52 -0
- itasc/tracking_ultrack/swap_candidate.py +171 -0
- itasc/tracking_ultrack/track_quality.py +142 -0
- itasc/tracking_ultrack/validation_nodes.py +315 -0
- itasc/tracking_ultrack/validation_state.py +430 -0
- itasc-0.2.0.dev0.data/data/itasc/napari.yaml +45 -0
- itasc-0.2.0.dev0.dist-info/METADATA +186 -0
- itasc-0.2.0.dev0.dist-info/RECORD +162 -0
- itasc-0.2.0.dev0.dist-info/WHEEL +4 -0
- itasc-0.2.0.dev0.dist-info/entry_points.txt +2 -0
- itasc-0.2.0.dev0.dist-info/licenses/LICENSE +661 -0
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
"""Pure Qt sub-widget builders for the nucleus correction widget.
|
|
2
|
+
|
|
3
|
+
These are the self-contained construction helpers the correction widget used to
|
|
4
|
+
carry inline: each takes the widgets/data it needs and returns a ready ``QWidget``
|
|
5
|
+
(or mutates a passed-in section), with no back-reference to the host widget. The
|
|
6
|
+
host stays responsible for owning the resulting widgets and wiring their signals.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from qtpy.QtCore import Qt, Signal
|
|
11
|
+
from qtpy.QtWidgets import (
|
|
12
|
+
QFrame,
|
|
13
|
+
QGridLayout,
|
|
14
|
+
QGroupBox,
|
|
15
|
+
QHBoxLayout,
|
|
16
|
+
QLabel,
|
|
17
|
+
QMessageBox,
|
|
18
|
+
QSizePolicy,
|
|
19
|
+
QStackedWidget,
|
|
20
|
+
QToolButton,
|
|
21
|
+
QVBoxLayout,
|
|
22
|
+
QWidget,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
from itasc.napari.correction.correction_widget import CorrectionWidget
|
|
26
|
+
from itasc.napari.ui_style import (
|
|
27
|
+
stage_header_action_button,
|
|
28
|
+
stage_header_label,
|
|
29
|
+
)
|
|
30
|
+
from itasc.napari.widgets import CollapsibleSection
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def set_checked_without_signal(button: QWidget, checked: bool) -> None:
|
|
34
|
+
"""Toggle a checkable button's state without emitting its ``toggled`` signal."""
|
|
35
|
+
old = button.blockSignals(True)
|
|
36
|
+
try:
|
|
37
|
+
button.setChecked(checked)
|
|
38
|
+
finally:
|
|
39
|
+
button.blockSignals(old)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def confirm_unsaved_before_deactivate(parent: QWidget, *, save_noun: str) -> str:
|
|
43
|
+
"""Prompt before leaving correction mode with unsaved changes.
|
|
44
|
+
|
|
45
|
+
``save_noun`` names what would be saved (e.g. ``"tracked labels"``). Returns
|
|
46
|
+
``"save"``, ``"discard"``, or ``"cancel"``; the caller performs the actual
|
|
47
|
+
save and clears its dirty flag.
|
|
48
|
+
"""
|
|
49
|
+
choice = QMessageBox.question(
|
|
50
|
+
parent,
|
|
51
|
+
"Save correction changes?",
|
|
52
|
+
(
|
|
53
|
+
"Correction mode has unsaved changes. "
|
|
54
|
+
f"Save {save_noun} before turning correction mode off?"
|
|
55
|
+
),
|
|
56
|
+
QMessageBox.Save | QMessageBox.Discard | QMessageBox.Cancel,
|
|
57
|
+
QMessageBox.Save,
|
|
58
|
+
)
|
|
59
|
+
if choice == QMessageBox.Cancel:
|
|
60
|
+
return "cancel"
|
|
61
|
+
if choice == QMessageBox.Save:
|
|
62
|
+
return "save"
|
|
63
|
+
return "discard"
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
# Width of the slim show-tab a collapsed pane shrinks to.
|
|
67
|
+
_PANE_STRIP_W = 24
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class CollapsiblePane(QWidget):
|
|
71
|
+
"""Wrap a content panel with a titled header whose ✕ collapses it to a tab.
|
|
72
|
+
|
|
73
|
+
Expanded, the pane shows a header row (title + a ✕ hide button) over the
|
|
74
|
+
content. Hiding swaps in a slim full-height ``▸`` show-tab and pins the pane
|
|
75
|
+
narrow; clicking the tab expands it again. ``collapsed_changed`` lets the host
|
|
76
|
+
redistribute the surrounding splitter when the state flips.
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
collapsed_changed = Signal(bool)
|
|
80
|
+
|
|
81
|
+
def __init__(self, content: QWidget, *, title: str, parent: QWidget | None = None) -> None:
|
|
82
|
+
super().__init__(parent)
|
|
83
|
+
self._collapsed = False
|
|
84
|
+
|
|
85
|
+
# Claim the full dock height: without an explicit Expanding vertical
|
|
86
|
+
# policy the pane (and its stack) fall back to their content's size hint,
|
|
87
|
+
# which on some platforms / packaged builds leaves the workspace panels
|
|
88
|
+
# bunched at the top instead of filling the dock.
|
|
89
|
+
self.setSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Expanding)
|
|
90
|
+
|
|
91
|
+
outer = QVBoxLayout(self)
|
|
92
|
+
outer.setContentsMargins(0, 0, 0, 0)
|
|
93
|
+
outer.setSpacing(0)
|
|
94
|
+
self._stack = QStackedWidget(self)
|
|
95
|
+
self._stack.setSizePolicy(
|
|
96
|
+
QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Expanding
|
|
97
|
+
)
|
|
98
|
+
outer.addWidget(self._stack)
|
|
99
|
+
|
|
100
|
+
page = QWidget()
|
|
101
|
+
page_lay = QVBoxLayout(page)
|
|
102
|
+
page_lay.setContentsMargins(0, 0, 0, 0)
|
|
103
|
+
page_lay.setSpacing(2)
|
|
104
|
+
|
|
105
|
+
header = QWidget()
|
|
106
|
+
header_lay = QHBoxLayout(header)
|
|
107
|
+
header_lay.setContentsMargins(6, 2, 2, 2)
|
|
108
|
+
header_lay.setSpacing(4)
|
|
109
|
+
title_lbl = QLabel(title)
|
|
110
|
+
title_lbl.setStyleSheet("font-weight: bold;")
|
|
111
|
+
hide_btn = QToolButton()
|
|
112
|
+
hide_btn.setText("✕")
|
|
113
|
+
hide_btn.setToolTip(f"Hide {title}")
|
|
114
|
+
hide_btn.setAutoRaise(True)
|
|
115
|
+
hide_btn.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed)
|
|
116
|
+
hide_btn.clicked.connect(lambda: self.set_collapsed(True))
|
|
117
|
+
header_lay.addWidget(title_lbl)
|
|
118
|
+
header_lay.addStretch(1)
|
|
119
|
+
header_lay.addWidget(hide_btn)
|
|
120
|
+
|
|
121
|
+
page_lay.addWidget(header)
|
|
122
|
+
page_lay.addWidget(content, stretch=1)
|
|
123
|
+
self._stack.addWidget(page)
|
|
124
|
+
|
|
125
|
+
# The show-tab pins the ✕/▸ buttons to the same top edge: a thin column
|
|
126
|
+
# with the ▸ button at the top and a stretch beneath, rather than a
|
|
127
|
+
# full-height button whose glyph floats to the vertical centre.
|
|
128
|
+
strip = QWidget()
|
|
129
|
+
strip_lay = QVBoxLayout(strip)
|
|
130
|
+
strip_lay.setContentsMargins(2, 2, 2, 2)
|
|
131
|
+
strip_lay.setSpacing(0)
|
|
132
|
+
strip_btn = QToolButton()
|
|
133
|
+
strip_btn.setText("▸")
|
|
134
|
+
strip_btn.setToolTip(f"Show {title}")
|
|
135
|
+
strip_btn.setAutoRaise(True)
|
|
136
|
+
strip_btn.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed)
|
|
137
|
+
strip_btn.clicked.connect(lambda: self.set_collapsed(False))
|
|
138
|
+
strip_lay.addWidget(strip_btn, alignment=Qt.AlignTop)
|
|
139
|
+
strip_lay.addStretch(1)
|
|
140
|
+
self._strip = strip
|
|
141
|
+
self._stack.addWidget(strip)
|
|
142
|
+
|
|
143
|
+
def is_collapsed(self) -> bool:
|
|
144
|
+
return self._collapsed
|
|
145
|
+
|
|
146
|
+
def set_collapsed(self, collapsed: bool) -> None:
|
|
147
|
+
collapsed = bool(collapsed)
|
|
148
|
+
if collapsed == self._collapsed:
|
|
149
|
+
return
|
|
150
|
+
self._collapsed = collapsed
|
|
151
|
+
self._stack.setCurrentIndex(1 if collapsed else 0)
|
|
152
|
+
if collapsed:
|
|
153
|
+
self.setMinimumWidth(_PANE_STRIP_W)
|
|
154
|
+
self.setMaximumWidth(_PANE_STRIP_W)
|
|
155
|
+
else:
|
|
156
|
+
self.setMinimumWidth(0)
|
|
157
|
+
self.setMaximumWidth(16777215)
|
|
158
|
+
self.collapsed_changed.emit(collapsed)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def flatten_embedded_section(section: CollapsibleSection) -> None:
|
|
162
|
+
"""Strip a section's header/margins so it nests flush inside another panel."""
|
|
163
|
+
section.set_header_visible(False)
|
|
164
|
+
section.layout().setContentsMargins(0, 0, 0, 0)
|
|
165
|
+
section._content_frame.layout().setContentsMargins(0, 0, 0, 0)
|
|
166
|
+
section._content_frame.setStyleSheet(
|
|
167
|
+
"QFrame#collapsible_content { border: none; margin: 0px; }"
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def build_correction_header(
|
|
172
|
+
parent: QWidget,
|
|
173
|
+
*,
|
|
174
|
+
shortcuts_btn: QWidget,
|
|
175
|
+
params_btn: QWidget,
|
|
176
|
+
active_btn: QWidget,
|
|
177
|
+
finalize_btn: QWidget | None = None,
|
|
178
|
+
view_toggle_btns: tuple[QWidget, ...] = (),
|
|
179
|
+
status_lbl: QWidget | None = None,
|
|
180
|
+
) -> tuple[QWidget, QLabel]:
|
|
181
|
+
"""Build the full-width correction top bar; returns ``(header, title_label)``.
|
|
182
|
+
|
|
183
|
+
Left→right: the stage title, the activate / shortcuts / params toggles, the
|
|
184
|
+
checkable view-toggle tool-buttons, a stretch, then a single one-line status
|
|
185
|
+
label right-aligned at the end of the bar (the save/action status only — the
|
|
186
|
+
track / validated summary lives in the tracking-overview panel). The bar
|
|
187
|
+
spans the whole workspace dock — over the toolbar, gallery, and accordion.
|
|
188
|
+
"""
|
|
189
|
+
header = QWidget(parent)
|
|
190
|
+
row = QHBoxLayout(header)
|
|
191
|
+
row.setContentsMargins(0, 0, 0, 0)
|
|
192
|
+
row.setSpacing(4)
|
|
193
|
+
|
|
194
|
+
# The title doubles as the inactive plugin-dock entry (a stage "pill" next
|
|
195
|
+
# to the on/off button) and, once correction is active, a proper workspace
|
|
196
|
+
# title. The host toggles between the two looks; it starts as the pill.
|
|
197
|
+
header_lbl = QLabel("Tracking Correction")
|
|
198
|
+
stage_header_label(header_lbl, "nucleus")
|
|
199
|
+
finalize_btns = (finalize_btn,) if finalize_btn is not None else ()
|
|
200
|
+
for button in (
|
|
201
|
+
active_btn, *finalize_btns, shortcuts_btn, params_btn, *view_toggle_btns
|
|
202
|
+
):
|
|
203
|
+
stage_header_action_button(button, "nucleus")
|
|
204
|
+
row.addWidget(header_lbl)
|
|
205
|
+
# Activate first, then Finalize right beside it (both stay visible even in
|
|
206
|
+
# the inactive plugin dock), then the reveal toggles — so correction mode
|
|
207
|
+
# can be entered/exited and the labels finalized even while the dock is hidden.
|
|
208
|
+
row.addWidget(active_btn)
|
|
209
|
+
for button in finalize_btns:
|
|
210
|
+
row.addWidget(button)
|
|
211
|
+
row.addWidget(shortcuts_btn)
|
|
212
|
+
row.addWidget(params_btn)
|
|
213
|
+
if view_toggle_btns:
|
|
214
|
+
row.addSpacing(8)
|
|
215
|
+
for button in view_toggle_btns:
|
|
216
|
+
row.addWidget(button)
|
|
217
|
+
row.addStretch(1)
|
|
218
|
+
if status_lbl is not None:
|
|
219
|
+
row.addWidget(status_lbl)
|
|
220
|
+
return header, header_lbl
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
# Glyphs are scaled up by this factor (relative to each button's default font)
|
|
224
|
+
# so the thin vertical action toolbar reads clearly at its narrow width.
|
|
225
|
+
_TOOLBAR_ICON_SCALE = 1.6
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def _enlarge_glyph(button: QWidget) -> None:
|
|
229
|
+
"""Bump a tool button's glyph font, leaving any stylesheet untouched."""
|
|
230
|
+
font = button.font()
|
|
231
|
+
if font.pointSizeF() > 0:
|
|
232
|
+
font.setPointSizeF(font.pointSizeF() * _TOOLBAR_ICON_SCALE)
|
|
233
|
+
else:
|
|
234
|
+
font.setPixelSize(max(1, round(font.pixelSize() * _TOOLBAR_ICON_SCALE)))
|
|
235
|
+
button.setFont(font)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def build_correction_toolbar(
|
|
239
|
+
parent: QWidget, button_groups: list[tuple[QWidget, ...]]
|
|
240
|
+
) -> QWidget:
|
|
241
|
+
"""Stack groups of action buttons vertically in a thin column, ruled between.
|
|
242
|
+
|
|
243
|
+
The toolbar is the narrow leftmost panel of the workspace body splitter, so
|
|
244
|
+
its buttons run top-to-bottom in a single column with bigger glyphs; group
|
|
245
|
+
separators become horizontal rules. The column hugs its buttons' width so it
|
|
246
|
+
stays thin while the accordion to its right absorbs the spare width.
|
|
247
|
+
"""
|
|
248
|
+
toolbar = QWidget(parent)
|
|
249
|
+
col = QVBoxLayout(toolbar)
|
|
250
|
+
col.setContentsMargins(2, 2, 2, 2)
|
|
251
|
+
col.setSpacing(4)
|
|
252
|
+
|
|
253
|
+
def _sep() -> QFrame:
|
|
254
|
+
line = QFrame()
|
|
255
|
+
line.setFrameShape(QFrame.HLine)
|
|
256
|
+
line.setFrameShadow(QFrame.Sunken)
|
|
257
|
+
return line
|
|
258
|
+
|
|
259
|
+
for i, group in enumerate(button_groups):
|
|
260
|
+
if i > 0:
|
|
261
|
+
col.addWidget(_sep())
|
|
262
|
+
for b in group:
|
|
263
|
+
_enlarge_glyph(b)
|
|
264
|
+
col.addWidget(b)
|
|
265
|
+
col.addStretch(1)
|
|
266
|
+
return toolbar
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
# The shortcut groups, split into side-by-side columns so the reveal area below
|
|
270
|
+
# the top bar reads wide-and-short instead of as one tall stack.
|
|
271
|
+
_SHORTCUT_COLUMNS = (
|
|
272
|
+
(
|
|
273
|
+
(
|
|
274
|
+
"Track Workflow",
|
|
275
|
+
(
|
|
276
|
+
("V", "Validate selected track"),
|
|
277
|
+
("B", "Anchor selected cell at current frame"),
|
|
278
|
+
("A / D", "Extend selected track backward / forward"),
|
|
279
|
+
("Q / E", "Retrack backward / forward"),
|
|
280
|
+
("Z / C", "Swap with smaller / larger candidate fragment"),
|
|
281
|
+
("S", "Save tracked labels"),
|
|
282
|
+
("Space", "Play / stop the movie"),
|
|
283
|
+
),
|
|
284
|
+
),
|
|
285
|
+
),
|
|
286
|
+
(
|
|
287
|
+
(
|
|
288
|
+
"Manual Labels",
|
|
289
|
+
(
|
|
290
|
+
("Middle-click empty space", "Spawn new cell"),
|
|
291
|
+
("Middle-click on cell or Delete", "Erase cell"),
|
|
292
|
+
("Ctrl+Left-click", "Merge with the clicked cell (same frame)"),
|
|
293
|
+
("Ctrl+Middle-click", "Grow / link selected track here"),
|
|
294
|
+
("Ctrl+Right-click", "Swap with the clicked cell, or attach it to the selected track (other frame)"),
|
|
295
|
+
("Shift+Left-drag", "Draw / extend cell path"),
|
|
296
|
+
("Shift+Right-drag", "Split by drawn line"),
|
|
297
|
+
),
|
|
298
|
+
),
|
|
299
|
+
("History", (("Ctrl+Z", "Undo"),)),
|
|
300
|
+
),
|
|
301
|
+
(
|
|
302
|
+
(
|
|
303
|
+
"Selection",
|
|
304
|
+
(
|
|
305
|
+
("Left-click", "Select / highlight cell"),
|
|
306
|
+
("← / →", "Previous / next thumbnail"),
|
|
307
|
+
("↑ / ↓", "Thumbnail row up / down"),
|
|
308
|
+
("Shift+↑ / ↓", "Previous / next track"),
|
|
309
|
+
),
|
|
310
|
+
),
|
|
311
|
+
),
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def _shortcut_column(groups, footer: QWidget | None = None) -> QWidget:
|
|
316
|
+
"""One vertical column of shortcut groups, with an optional footer below."""
|
|
317
|
+
column = QWidget()
|
|
318
|
+
col_lay = QVBoxLayout(column)
|
|
319
|
+
col_lay.setContentsMargins(0, 0, 0, 0)
|
|
320
|
+
col_lay.setSpacing(4)
|
|
321
|
+
|
|
322
|
+
grid_host = QWidget()
|
|
323
|
+
grid = QGridLayout(grid_host)
|
|
324
|
+
grid.setContentsMargins(8, 6, 8, 6)
|
|
325
|
+
grid.setHorizontalSpacing(10)
|
|
326
|
+
grid.setVerticalSpacing(2)
|
|
327
|
+
row = 0
|
|
328
|
+
for i, (title, items) in enumerate(groups):
|
|
329
|
+
row = CorrectionWidget._add_shortcut_group(
|
|
330
|
+
grid, title, list(items), start_row=row, is_first=(i == 0)
|
|
331
|
+
)
|
|
332
|
+
grid.setColumnStretch(1, 1)
|
|
333
|
+
col_lay.addWidget(grid_host)
|
|
334
|
+
if footer is not None:
|
|
335
|
+
col_lay.addWidget(footer)
|
|
336
|
+
col_lay.addStretch(1)
|
|
337
|
+
return column
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def build_shortcuts_widget(attrib_lbl: QWidget | None = None) -> QWidget:
|
|
341
|
+
"""Build the wide multi-column correction-shortcuts reference panel.
|
|
342
|
+
|
|
343
|
+
The shortcut groups are arranged in side-by-side columns (so the panel is
|
|
344
|
+
wide and short under the top bar). The disclaimer / attribution label, when
|
|
345
|
+
supplied, rides at the bottom of the last column (alongside Selection).
|
|
346
|
+
"""
|
|
347
|
+
group = QGroupBox("Correction shortcuts")
|
|
348
|
+
outer = QVBoxLayout(group)
|
|
349
|
+
# A titled group box draws its title inside the top inset, so the top margin
|
|
350
|
+
# must leave room for it — zeroing all four (as the other panels do) clipped
|
|
351
|
+
# the title against the first shortcut row. Reserve a title's height up top.
|
|
352
|
+
title_h = group.fontMetrics().height() + 6
|
|
353
|
+
outer.setContentsMargins(0, title_h, 0, 0)
|
|
354
|
+
outer.setSpacing(2)
|
|
355
|
+
|
|
356
|
+
columns = QHBoxLayout()
|
|
357
|
+
columns.setContentsMargins(0, 0, 0, 0)
|
|
358
|
+
columns.setSpacing(12)
|
|
359
|
+
last = len(_SHORTCUT_COLUMNS) - 1
|
|
360
|
+
for i, col_groups in enumerate(_SHORTCUT_COLUMNS):
|
|
361
|
+
footer = attrib_lbl if i == last else None
|
|
362
|
+
columns.addWidget(
|
|
363
|
+
_shortcut_column(col_groups, footer), alignment=Qt.AlignTop
|
|
364
|
+
)
|
|
365
|
+
columns.addStretch(1)
|
|
366
|
+
outer.addLayout(columns)
|
|
367
|
+
return group
|