senoquant 1.0.0b1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (148) hide show
  1. senoquant/__init__.py +6 -0
  2. senoquant/_reader.py +7 -0
  3. senoquant/_widget.py +33 -0
  4. senoquant/napari.yaml +83 -0
  5. senoquant/reader/__init__.py +5 -0
  6. senoquant/reader/core.py +369 -0
  7. senoquant/tabs/__init__.py +15 -0
  8. senoquant/tabs/batch/__init__.py +10 -0
  9. senoquant/tabs/batch/backend.py +641 -0
  10. senoquant/tabs/batch/config.py +270 -0
  11. senoquant/tabs/batch/frontend.py +1283 -0
  12. senoquant/tabs/batch/io.py +326 -0
  13. senoquant/tabs/batch/layers.py +86 -0
  14. senoquant/tabs/quantification/__init__.py +1 -0
  15. senoquant/tabs/quantification/backend.py +228 -0
  16. senoquant/tabs/quantification/features/__init__.py +80 -0
  17. senoquant/tabs/quantification/features/base.py +142 -0
  18. senoquant/tabs/quantification/features/marker/__init__.py +5 -0
  19. senoquant/tabs/quantification/features/marker/config.py +69 -0
  20. senoquant/tabs/quantification/features/marker/dialog.py +437 -0
  21. senoquant/tabs/quantification/features/marker/export.py +879 -0
  22. senoquant/tabs/quantification/features/marker/feature.py +119 -0
  23. senoquant/tabs/quantification/features/marker/morphology.py +285 -0
  24. senoquant/tabs/quantification/features/marker/rows.py +654 -0
  25. senoquant/tabs/quantification/features/marker/thresholding.py +46 -0
  26. senoquant/tabs/quantification/features/roi.py +346 -0
  27. senoquant/tabs/quantification/features/spots/__init__.py +5 -0
  28. senoquant/tabs/quantification/features/spots/config.py +62 -0
  29. senoquant/tabs/quantification/features/spots/dialog.py +477 -0
  30. senoquant/tabs/quantification/features/spots/export.py +1292 -0
  31. senoquant/tabs/quantification/features/spots/feature.py +112 -0
  32. senoquant/tabs/quantification/features/spots/morphology.py +279 -0
  33. senoquant/tabs/quantification/features/spots/rows.py +241 -0
  34. senoquant/tabs/quantification/frontend.py +815 -0
  35. senoquant/tabs/segmentation/__init__.py +1 -0
  36. senoquant/tabs/segmentation/backend.py +131 -0
  37. senoquant/tabs/segmentation/frontend.py +1009 -0
  38. senoquant/tabs/segmentation/models/__init__.py +5 -0
  39. senoquant/tabs/segmentation/models/base.py +146 -0
  40. senoquant/tabs/segmentation/models/cpsam/details.json +65 -0
  41. senoquant/tabs/segmentation/models/cpsam/model.py +150 -0
  42. senoquant/tabs/segmentation/models/default_2d/details.json +69 -0
  43. senoquant/tabs/segmentation/models/default_2d/model.py +664 -0
  44. senoquant/tabs/segmentation/models/default_3d/details.json +69 -0
  45. senoquant/tabs/segmentation/models/default_3d/model.py +682 -0
  46. senoquant/tabs/segmentation/models/hf.py +71 -0
  47. senoquant/tabs/segmentation/models/nuclear_dilation/__init__.py +1 -0
  48. senoquant/tabs/segmentation/models/nuclear_dilation/details.json +26 -0
  49. senoquant/tabs/segmentation/models/nuclear_dilation/model.py +96 -0
  50. senoquant/tabs/segmentation/models/perinuclear_rings/__init__.py +1 -0
  51. senoquant/tabs/segmentation/models/perinuclear_rings/details.json +34 -0
  52. senoquant/tabs/segmentation/models/perinuclear_rings/model.py +132 -0
  53. senoquant/tabs/segmentation/stardist_onnx_utils/__init__.py +2 -0
  54. senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/csbdeep/__init__.py +3 -0
  55. senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/csbdeep/data/__init__.py +6 -0
  56. senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/csbdeep/data/generate.py +470 -0
  57. senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/csbdeep/data/prepare.py +273 -0
  58. senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/csbdeep/data/rawdata.py +112 -0
  59. senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/csbdeep/data/transform.py +384 -0
  60. senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/csbdeep/internals/__init__.py +0 -0
  61. senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/csbdeep/internals/blocks.py +184 -0
  62. senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/csbdeep/internals/losses.py +79 -0
  63. senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/csbdeep/internals/nets.py +165 -0
  64. senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/csbdeep/internals/predict.py +467 -0
  65. senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/csbdeep/internals/probability.py +67 -0
  66. senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/csbdeep/internals/train.py +148 -0
  67. senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/csbdeep/io/__init__.py +163 -0
  68. senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/csbdeep/models/__init__.py +52 -0
  69. senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/csbdeep/models/base_model.py +329 -0
  70. senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/csbdeep/models/care_isotropic.py +160 -0
  71. senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/csbdeep/models/care_projection.py +178 -0
  72. senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/csbdeep/models/care_standard.py +446 -0
  73. senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/csbdeep/models/care_upsampling.py +54 -0
  74. senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/csbdeep/models/config.py +254 -0
  75. senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/csbdeep/models/pretrained.py +119 -0
  76. senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/csbdeep/scripts/__init__.py +0 -0
  77. senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/csbdeep/scripts/care_predict.py +180 -0
  78. senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/csbdeep/utils/__init__.py +5 -0
  79. senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/csbdeep/utils/plot_utils.py +159 -0
  80. senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/csbdeep/utils/six.py +18 -0
  81. senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/csbdeep/utils/tf.py +644 -0
  82. senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/csbdeep/utils/utils.py +272 -0
  83. senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/csbdeep/version.py +1 -0
  84. senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/docs/source/conf.py +368 -0
  85. senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/setup.py +68 -0
  86. senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/tests/test_datagen.py +169 -0
  87. senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/tests/test_models.py +462 -0
  88. senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/tests/test_utils.py +166 -0
  89. senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/tools/create_zip_contents.py +34 -0
  90. senoquant/tabs/segmentation/stardist_onnx_utils/_stardist/__init__.py +30 -0
  91. senoquant/tabs/segmentation/stardist_onnx_utils/_stardist/big.py +624 -0
  92. senoquant/tabs/segmentation/stardist_onnx_utils/_stardist/bioimageio_utils.py +494 -0
  93. senoquant/tabs/segmentation/stardist_onnx_utils/_stardist/data/__init__.py +39 -0
  94. senoquant/tabs/segmentation/stardist_onnx_utils/_stardist/geometry/__init__.py +10 -0
  95. senoquant/tabs/segmentation/stardist_onnx_utils/_stardist/geometry/geom2d.py +215 -0
  96. senoquant/tabs/segmentation/stardist_onnx_utils/_stardist/geometry/geom3d.py +349 -0
  97. senoquant/tabs/segmentation/stardist_onnx_utils/_stardist/matching.py +483 -0
  98. senoquant/tabs/segmentation/stardist_onnx_utils/_stardist/models/__init__.py +28 -0
  99. senoquant/tabs/segmentation/stardist_onnx_utils/_stardist/models/base.py +1217 -0
  100. senoquant/tabs/segmentation/stardist_onnx_utils/_stardist/models/model2d.py +594 -0
  101. senoquant/tabs/segmentation/stardist_onnx_utils/_stardist/models/model3d.py +696 -0
  102. senoquant/tabs/segmentation/stardist_onnx_utils/_stardist/nms.py +384 -0
  103. senoquant/tabs/segmentation/stardist_onnx_utils/_stardist/plot/__init__.py +2 -0
  104. senoquant/tabs/segmentation/stardist_onnx_utils/_stardist/plot/plot.py +74 -0
  105. senoquant/tabs/segmentation/stardist_onnx_utils/_stardist/plot/render.py +298 -0
  106. senoquant/tabs/segmentation/stardist_onnx_utils/_stardist/rays3d.py +373 -0
  107. senoquant/tabs/segmentation/stardist_onnx_utils/_stardist/sample_patches.py +65 -0
  108. senoquant/tabs/segmentation/stardist_onnx_utils/_stardist/scripts/__init__.py +0 -0
  109. senoquant/tabs/segmentation/stardist_onnx_utils/_stardist/scripts/predict2d.py +90 -0
  110. senoquant/tabs/segmentation/stardist_onnx_utils/_stardist/scripts/predict3d.py +93 -0
  111. senoquant/tabs/segmentation/stardist_onnx_utils/_stardist/utils.py +408 -0
  112. senoquant/tabs/segmentation/stardist_onnx_utils/_stardist/version.py +1 -0
  113. senoquant/tabs/segmentation/stardist_onnx_utils/onnx_framework/__init__.py +45 -0
  114. senoquant/tabs/segmentation/stardist_onnx_utils/onnx_framework/convert/__init__.py +17 -0
  115. senoquant/tabs/segmentation/stardist_onnx_utils/onnx_framework/convert/cli.py +55 -0
  116. senoquant/tabs/segmentation/stardist_onnx_utils/onnx_framework/convert/core.py +285 -0
  117. senoquant/tabs/segmentation/stardist_onnx_utils/onnx_framework/inspect/__init__.py +15 -0
  118. senoquant/tabs/segmentation/stardist_onnx_utils/onnx_framework/inspect/cli.py +36 -0
  119. senoquant/tabs/segmentation/stardist_onnx_utils/onnx_framework/inspect/divisibility.py +193 -0
  120. senoquant/tabs/segmentation/stardist_onnx_utils/onnx_framework/inspect/probe.py +100 -0
  121. senoquant/tabs/segmentation/stardist_onnx_utils/onnx_framework/inspect/receptive_field.py +182 -0
  122. senoquant/tabs/segmentation/stardist_onnx_utils/onnx_framework/inspect/rf_cli.py +48 -0
  123. senoquant/tabs/segmentation/stardist_onnx_utils/onnx_framework/inspect/valid_sizes.py +278 -0
  124. senoquant/tabs/segmentation/stardist_onnx_utils/onnx_framework/post/__init__.py +8 -0
  125. senoquant/tabs/segmentation/stardist_onnx_utils/onnx_framework/post/core.py +157 -0
  126. senoquant/tabs/segmentation/stardist_onnx_utils/onnx_framework/pre/__init__.py +17 -0
  127. senoquant/tabs/segmentation/stardist_onnx_utils/onnx_framework/pre/core.py +226 -0
  128. senoquant/tabs/segmentation/stardist_onnx_utils/onnx_framework/predict/__init__.py +5 -0
  129. senoquant/tabs/segmentation/stardist_onnx_utils/onnx_framework/predict/core.py +401 -0
  130. senoquant/tabs/settings/__init__.py +1 -0
  131. senoquant/tabs/settings/backend.py +29 -0
  132. senoquant/tabs/settings/frontend.py +19 -0
  133. senoquant/tabs/spots/__init__.py +1 -0
  134. senoquant/tabs/spots/backend.py +139 -0
  135. senoquant/tabs/spots/frontend.py +800 -0
  136. senoquant/tabs/spots/models/__init__.py +5 -0
  137. senoquant/tabs/spots/models/base.py +94 -0
  138. senoquant/tabs/spots/models/rmp/details.json +61 -0
  139. senoquant/tabs/spots/models/rmp/model.py +499 -0
  140. senoquant/tabs/spots/models/udwt/details.json +103 -0
  141. senoquant/tabs/spots/models/udwt/model.py +482 -0
  142. senoquant/utils.py +25 -0
  143. senoquant-1.0.0b1.dist-info/METADATA +193 -0
  144. senoquant-1.0.0b1.dist-info/RECORD +148 -0
  145. senoquant-1.0.0b1.dist-info/WHEEL +5 -0
  146. senoquant-1.0.0b1.dist-info/entry_points.txt +2 -0
  147. senoquant-1.0.0b1.dist-info/licenses/LICENSE +28 -0
  148. senoquant-1.0.0b1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,437 @@
1
+ """Marker channels dialog."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING
6
+
7
+ from qtpy.QtCore import Qt, QTimer
8
+ from qtpy.QtWidgets import (
9
+ QComboBox,
10
+ QDialog,
11
+ QGroupBox,
12
+ QPushButton,
13
+ QScrollArea,
14
+ QSplitter,
15
+ QSizePolicy,
16
+ QVBoxLayout,
17
+ QWidget,
18
+ )
19
+
20
+ from .config import (
21
+ MarkerChannelConfig,
22
+ MarkerFeatureData,
23
+ MarkerSegmentationConfig,
24
+ )
25
+ from .rows import MarkerChannelRow, MarkerSegmentationRow
26
+
27
+ if TYPE_CHECKING:
28
+ from .feature import MarkerFeature
29
+
30
+
31
+ class MarkerChannelsDialog(QDialog):
32
+ """Dialog for configuring multiple marker channels."""
33
+
34
+ def __init__(self, feature: "MarkerFeature") -> None:
35
+ """Initialize the marker channels dialog.
36
+
37
+ Parameters
38
+ ----------
39
+ feature : MarkerFeature
40
+ Marker feature instance owning the dialog.
41
+ """
42
+ super().__init__(feature._tab)
43
+ self._feature = feature
44
+ self._tab = feature._tab
45
+ data = feature._state.data
46
+ if not isinstance(data, MarkerFeatureData):
47
+ data = MarkerFeatureData()
48
+ feature._state.data = data
49
+ self._data = data
50
+ self._segmentations = data.segmentations
51
+ self._channels = data.channels
52
+ self._rows: list[MarkerChannelRow] = []
53
+ self._segmentation_rows: list[MarkerSegmentationRow] = []
54
+ self._layout_watch_timer: QTimer | None = None
55
+ self._layout_last_sizes: dict[str, tuple[int, int]] = {}
56
+
57
+ self.setWindowTitle("Marker channels")
58
+ self.setMinimumSize(600, 800)
59
+ layout = QVBoxLayout()
60
+
61
+ segmentations_section = self._build_segmentations_section()
62
+ channels_section = self._build_channels_section()
63
+ splitter = QSplitter(Qt.Vertical)
64
+ splitter.setChildrenCollapsible(False)
65
+ splitter.addWidget(segmentations_section)
66
+ splitter.addWidget(channels_section)
67
+ splitter.setStretchFactor(0, 2)
68
+ splitter.setStretchFactor(1, 3)
69
+ layout.addWidget(splitter, 1)
70
+
71
+ close_button = QPushButton("Save")
72
+ close_button.clicked.connect(self.accept)
73
+ layout.addWidget(close_button)
74
+
75
+ self.setLayout(layout)
76
+ self._load_segmentations()
77
+ self._load_channels()
78
+ self._start_layout_watch()
79
+
80
+ def closeEvent(self, event) -> None:
81
+ """Handle window close as a save action.
82
+
83
+ Parameters
84
+ ----------
85
+ event : QCloseEvent
86
+ Close event from Qt.
87
+ """
88
+ self.accept()
89
+ event.accept()
90
+
91
+ def _build_segmentations_section(self) -> QGroupBox:
92
+ """Create the segmentations section with add/remove controls."""
93
+ section = QGroupBox("Segmentations")
94
+ section.setFlat(True)
95
+ section.setStyleSheet(self._section_stylesheet())
96
+
97
+ self._segmentations_container = QWidget()
98
+ self._segmentations_container.setSizePolicy(
99
+ QSizePolicy.Expanding, QSizePolicy.Fixed
100
+ )
101
+ self._segmentations_layout = QVBoxLayout()
102
+ self._segmentations_layout.setContentsMargins(0, 0, 0, 0)
103
+ self._segmentations_layout.setSpacing(8)
104
+ self._segmentations_container.setLayout(self._segmentations_layout)
105
+
106
+ self._segmentations_scroll_area = QScrollArea()
107
+ self._segmentations_scroll_area.setWidgetResizable(True)
108
+ self._segmentations_scroll_area.setHorizontalScrollBarPolicy(
109
+ Qt.ScrollBarAlwaysOff
110
+ )
111
+ self._segmentations_scroll_area.setSizePolicy(
112
+ QSizePolicy.Expanding, QSizePolicy.Expanding
113
+ )
114
+ self._segmentations_scroll_area.setWidget(
115
+ self._segmentations_container
116
+ )
117
+
118
+ add_button = QPushButton("Add segmentation")
119
+ add_button.clicked.connect(self._add_segmentation)
120
+
121
+ section_layout = QVBoxLayout()
122
+ section_layout.setContentsMargins(10, 12, 10, 10)
123
+ section_layout.addWidget(self._segmentations_scroll_area)
124
+ section_layout.addWidget(add_button)
125
+ section.setLayout(section_layout)
126
+ self._segmentations_section = section
127
+ return section
128
+
129
+ def _build_channels_section(self) -> QGroupBox:
130
+ """Create the channels section with add/remove controls."""
131
+ self._channels_container = QWidget()
132
+ self._channels_container.setSizePolicy(
133
+ QSizePolicy.Expanding, QSizePolicy.Fixed
134
+ )
135
+ self._channels_layout = QVBoxLayout()
136
+ self._channels_layout.setContentsMargins(0, 0, 0, 0)
137
+ self._channels_layout.setSpacing(8)
138
+ self._channels_container.setLayout(self._channels_layout)
139
+
140
+ section = QGroupBox("Channels")
141
+ section.setFlat(True)
142
+ section.setStyleSheet(self._section_stylesheet())
143
+
144
+ self._channels_scroll_area = QScrollArea()
145
+ self._channels_scroll_area.setWidgetResizable(True)
146
+ self._channels_scroll_area.setHorizontalScrollBarPolicy(
147
+ Qt.ScrollBarAlwaysOff
148
+ )
149
+ self._channels_scroll_area.setSizePolicy(
150
+ QSizePolicy.Expanding, QSizePolicy.Expanding
151
+ )
152
+ self._channels_scroll_area.setWidget(self._channels_container)
153
+
154
+ add_button = QPushButton("Add channel")
155
+ add_button.clicked.connect(self._add_channel)
156
+
157
+ section_layout = QVBoxLayout()
158
+ section_layout.setContentsMargins(10, 12, 10, 10)
159
+ section_layout.addWidget(self._channels_scroll_area)
160
+ section_layout.addWidget(add_button)
161
+ section.setLayout(section_layout)
162
+
163
+ self._channels_section = section
164
+ return section
165
+
166
+ @staticmethod
167
+ def _section_stylesheet() -> str:
168
+ """Return the stylesheet used for dialog sections."""
169
+ return (
170
+ "QGroupBox {"
171
+ " margin-top: 8px;"
172
+ "}"
173
+ "QGroupBox::title {"
174
+ " subcontrol-origin: margin;"
175
+ " subcontrol-position: top left;"
176
+ " padding: 0 6px;"
177
+ "}"
178
+ )
179
+
180
+ def _refresh_labels_combo(self, combo: QComboBox) -> None:
181
+ """Refresh labels layer options for the dialog.
182
+
183
+ Parameters
184
+ ----------
185
+ combo : QComboBox
186
+ Labels combo box to refresh.
187
+ """
188
+ current = combo.currentText()
189
+ combo.clear()
190
+ viewer = self._tab._viewer
191
+ if viewer is None:
192
+ combo.addItem("Select labels")
193
+ return
194
+ for layer in viewer.layers:
195
+ if layer.__class__.__name__ == "Labels":
196
+ layer_name = layer.name
197
+ # Only show cellular labels (nuclear/cytoplasmic), exclude spot labels
198
+ if self._is_cellular_label(layer_name):
199
+ combo.addItem(layer_name)
200
+ if current:
201
+ index = combo.findText(current)
202
+ if index != -1:
203
+ combo.setCurrentIndex(index)
204
+
205
+ def _is_cellular_label(self, layer_name: str) -> bool:
206
+ """Check if a label layer is a cellular segmentation.
207
+
208
+ Parameters
209
+ ----------
210
+ layer_name : str
211
+ Name of the labels layer.
212
+
213
+ Returns
214
+ -------
215
+ bool
216
+ True if the layer is a cellular label (nuclear or cytoplasmic).
217
+ """
218
+ return layer_name.endswith("_nuc_labels") or layer_name.endswith("_cyto_labels")
219
+
220
+ def _refresh_image_combo(self, combo: QComboBox) -> None:
221
+ """Refresh image layer options for the dialog.
222
+
223
+ Parameters
224
+ ----------
225
+ combo : QComboBox
226
+ Image combo box to refresh.
227
+ """
228
+ current = combo.currentText()
229
+ combo.clear()
230
+ viewer = self._tab._viewer
231
+ if viewer is None:
232
+ combo.addItem("Select image")
233
+ return
234
+ for layer in viewer.layers:
235
+ if layer.__class__.__name__ == "Image":
236
+ combo.addItem(layer.name)
237
+ if current:
238
+ index = combo.findText(current)
239
+ if index != -1:
240
+ combo.setCurrentIndex(index)
241
+
242
+ def _load_segmentations(self) -> None:
243
+ """Build segmentation rows from stored data."""
244
+ if not self._segmentations:
245
+ return
246
+ for segmentation_data in self._segmentations:
247
+ if not isinstance(segmentation_data, MarkerSegmentationConfig):
248
+ continue
249
+ self._add_segmentation(segmentation_data)
250
+
251
+ def _load_channels(self) -> None:
252
+ """Build channel rows from stored data."""
253
+ if not self._channels:
254
+ return
255
+ for channel_data in self._channels:
256
+ if not isinstance(channel_data, MarkerChannelConfig):
257
+ continue
258
+ self._add_channel(channel_data)
259
+
260
+ def _add_channel(self, channel_data: MarkerChannelConfig | None = None) -> None:
261
+ """Add a channel row to the dialog.
262
+
263
+ Parameters
264
+ ----------
265
+ channel_data : MarkerChannelConfig or None
266
+ Channel configuration data.
267
+ """
268
+ if isinstance(channel_data, bool):
269
+ channel_data = None
270
+ if not isinstance(channel_data, MarkerChannelConfig):
271
+ channel_data = MarkerChannelConfig()
272
+ self._channels.append(channel_data)
273
+ row = MarkerChannelRow(self, channel_data)
274
+ self._rows.append(row)
275
+ self._channels_layout.addWidget(row)
276
+ self._renumber_rows()
277
+ self._schedule_layout_update()
278
+
279
+ def _remove_channel(self, row: MarkerChannelRow) -> None:
280
+ """Remove a channel row and its stored data.
281
+
282
+ Parameters
283
+ ----------
284
+ row : MarkerChannelRow
285
+ Row instance to remove.
286
+ """
287
+ if row not in self._rows:
288
+ return
289
+ self._rows.remove(row)
290
+ if row.data in self._channels:
291
+ self._channels.remove(row.data)
292
+ self._channels_layout.removeWidget(row)
293
+ row.deleteLater()
294
+ self._renumber_rows()
295
+ self._schedule_layout_update()
296
+
297
+ def _renumber_rows(self) -> None:
298
+ """Update channel row titles after changes."""
299
+ for index, row in enumerate(self._rows, start=0):
300
+ row.update_title(index)
301
+
302
+ def _add_segmentation(
303
+ self, segmentation_data: MarkerSegmentationConfig | None = None
304
+ ) -> None:
305
+ """Add a segmentation row to the dialog.
306
+
307
+ Parameters
308
+ ----------
309
+ segmentation_data : MarkerSegmentationConfig or None
310
+ Segmentation configuration data.
311
+ """
312
+ if isinstance(segmentation_data, bool):
313
+ segmentation_data = None
314
+ if not isinstance(segmentation_data, MarkerSegmentationConfig):
315
+ segmentation_data = MarkerSegmentationConfig()
316
+ self._segmentations.append(segmentation_data)
317
+ row = MarkerSegmentationRow(self, segmentation_data)
318
+ self._segmentation_rows.append(row)
319
+ self._segmentations_layout.addWidget(row)
320
+ self._renumber_segmentations()
321
+ self._schedule_layout_update()
322
+
323
+ def _remove_segmentation(self, row: MarkerSegmentationRow) -> None:
324
+ """Remove a segmentation row and its stored data.
325
+
326
+ Parameters
327
+ ----------
328
+ row : MarkerSegmentationRow
329
+ Row instance to remove.
330
+ """
331
+ if row not in self._segmentation_rows:
332
+ return
333
+ self._segmentation_rows.remove(row)
334
+ if row.data in self._segmentations:
335
+ self._segmentations.remove(row.data)
336
+ self._segmentations_layout.removeWidget(row)
337
+ row.deleteLater()
338
+ self._renumber_segmentations()
339
+ self._schedule_layout_update()
340
+
341
+ def _renumber_segmentations(self) -> None:
342
+ """Update segmentation row titles after changes."""
343
+ for index, row in enumerate(self._segmentation_rows, start=0):
344
+ row.update_title(index)
345
+
346
+ def _start_layout_watch(self) -> None:
347
+ """Start a timer to monitor layout changes in the dialog."""
348
+ if self._layout_watch_timer is not None:
349
+ return
350
+ self._layout_watch_timer = QTimer(self)
351
+ self._layout_watch_timer.setInterval(150)
352
+ self._layout_watch_timer.timeout.connect(self._poll_layout)
353
+ self._layout_watch_timer.start()
354
+
355
+ def _schedule_layout_update(self) -> None:
356
+ """Schedule a layout update on the next timer tick."""
357
+ self._layout_last_sizes.clear()
358
+
359
+ def _poll_layout(self) -> None:
360
+ """Recompute layout sizing when content changes."""
361
+ self._apply_scroll_area_layout(
362
+ "segmentations",
363
+ self._segmentations_scroll_area,
364
+ self._segmentations_layout,
365
+ max_ratio=0.2,
366
+ )
367
+ self._apply_scroll_area_layout(
368
+ "channels",
369
+ self._channels_scroll_area,
370
+ self._channels_layout,
371
+ max_ratio=0.8,
372
+ )
373
+
374
+ def _apply_scroll_area_layout(
375
+ self,
376
+ key: str,
377
+ scroll_area: QScrollArea,
378
+ layout: QVBoxLayout,
379
+ max_ratio: float,
380
+ ) -> None:
381
+ """Apply sizing rules for a scroll area section.
382
+
383
+ Parameters
384
+ ----------
385
+ key : str
386
+ Cache key for the section size.
387
+ scroll_area : QScrollArea
388
+ Scroll area to resize.
389
+ layout : QVBoxLayout
390
+ Layout containing section rows.
391
+ max_ratio : float
392
+ Maximum height ratio relative to the screen.
393
+ """
394
+ size = self._layout_content_size(layout)
395
+ if self._layout_last_sizes.get(key) == size:
396
+ return
397
+ self._layout_last_sizes[key] = size
398
+ content = scroll_area.widget()
399
+ if content is not None:
400
+ content.setMinimumWidth(scroll_area.viewport().width())
401
+ scroll_area.updateGeometry()
402
+ scroll_area.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
403
+
404
+ def _layout_content_size(self, layout: QVBoxLayout) -> tuple[int, int]:
405
+ """Return content size for a vertical layout.
406
+
407
+ Parameters
408
+ ----------
409
+ layout : QVBoxLayout
410
+ Layout to measure.
411
+
412
+ Returns
413
+ -------
414
+ tuple of int
415
+ (width, height) of the layout contents.
416
+ """
417
+ layout.activate()
418
+ margins = layout.contentsMargins()
419
+ spacing = layout.spacing()
420
+ count = layout.count()
421
+ total_height = margins.top() + margins.bottom()
422
+ max_width = 0
423
+ for index in range(count):
424
+ item = layout.itemAt(index)
425
+ widget = item.widget()
426
+ if widget is None:
427
+ item_size = item.sizeHint()
428
+ else:
429
+ item_size = widget.sizeHint().expandedTo(
430
+ widget.minimumSizeHint()
431
+ )
432
+ max_width = max(max_width, item_size.width())
433
+ total_height += item_size.height()
434
+ if count > 1:
435
+ total_height += spacing * (count - 1)
436
+ total_width = margins.left() + margins.right() + max_width
437
+ return (total_width, total_height)