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,477 @@
1
+ """Spots 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
+ SpotsChannelConfig,
22
+ SpotsFeatureData,
23
+ SpotsSegmentationConfig,
24
+ )
25
+ from .rows import SpotsChannelRow, SpotsSegmentationRow
26
+
27
+ if TYPE_CHECKING:
28
+ from .feature import SpotsFeature
29
+
30
+
31
+ class SpotsChannelsDialog(QDialog):
32
+ """Dialog for configuring spots channels."""
33
+
34
+ def __init__(self, feature: "SpotsFeature") -> None:
35
+ """Initialize the spots channels dialog.
36
+
37
+ Parameters
38
+ ----------
39
+ feature : SpotsFeature
40
+ Spots 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, SpotsFeatureData):
47
+ data = SpotsFeatureData()
48
+ feature._state.data = data
49
+ self._data = data
50
+ self._segmentations = data.segmentations
51
+ self._channels = data.channels
52
+ self._rows: list[SpotsChannelRow] = []
53
+ self._segmentation_rows: list[SpotsSegmentationRow] = []
54
+ self._layout_watch_timer: QTimer | None = None
55
+ self._layout_last_sizes: dict[str, tuple[int, int]] = {}
56
+
57
+ self.setWindowTitle("Spots 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
+
94
+ Returns
95
+ -------
96
+ QGroupBox
97
+ Group box containing segmentation rows and the add button.
98
+ """
99
+ section = QGroupBox(
100
+ "Nuclear/cytoplasmic segmentations to exclude background spots"
101
+ )
102
+ section.setFlat(True)
103
+ section.setStyleSheet(self._section_stylesheet())
104
+
105
+ self._segmentations_container = QWidget()
106
+ self._segmentations_container.setSizePolicy(
107
+ QSizePolicy.Expanding, QSizePolicy.Fixed
108
+ )
109
+ self._segmentations_layout = QVBoxLayout()
110
+ self._segmentations_layout.setContentsMargins(0, 0, 0, 0)
111
+ self._segmentations_layout.setSpacing(8)
112
+ self._segmentations_container.setLayout(self._segmentations_layout)
113
+
114
+ self._segmentations_scroll_area = QScrollArea()
115
+ self._segmentations_scroll_area.setWidgetResizable(True)
116
+ self._segmentations_scroll_area.setHorizontalScrollBarPolicy(
117
+ Qt.ScrollBarAlwaysOff
118
+ )
119
+ self._segmentations_scroll_area.setSizePolicy(
120
+ QSizePolicy.Expanding, QSizePolicy.Expanding
121
+ )
122
+ self._segmentations_scroll_area.setWidget(
123
+ self._segmentations_container
124
+ )
125
+
126
+ add_button = QPushButton("Add segmentation")
127
+ add_button.clicked.connect(self._add_segmentation)
128
+
129
+ section_layout = QVBoxLayout()
130
+ section_layout.setContentsMargins(10, 12, 10, 10)
131
+ section_layout.addWidget(self._segmentations_scroll_area)
132
+ section_layout.addWidget(add_button)
133
+ section.setLayout(section_layout)
134
+ self._segmentations_section = section
135
+ return section
136
+
137
+ def _build_channels_section(self) -> QGroupBox:
138
+ """Create the channels section with add/remove controls.
139
+
140
+ Returns
141
+ -------
142
+ QGroupBox
143
+ Group box containing channel rows and the add button.
144
+ """
145
+ self._channels_container = QWidget()
146
+ self._channels_container.setSizePolicy(
147
+ QSizePolicy.Expanding, QSizePolicy.Fixed
148
+ )
149
+ self._channels_layout = QVBoxLayout()
150
+ self._channels_layout.setContentsMargins(0, 0, 0, 0)
151
+ self._channels_layout.setSpacing(8)
152
+ self._channels_container.setLayout(self._channels_layout)
153
+
154
+ section = QGroupBox("Channels")
155
+ section.setFlat(True)
156
+ section.setStyleSheet(self._section_stylesheet())
157
+
158
+ self._channels_scroll_area = QScrollArea()
159
+ self._channels_scroll_area.setWidgetResizable(True)
160
+ self._channels_scroll_area.setHorizontalScrollBarPolicy(
161
+ Qt.ScrollBarAlwaysOff
162
+ )
163
+ self._channels_scroll_area.setSizePolicy(
164
+ QSizePolicy.Expanding, QSizePolicy.Expanding
165
+ )
166
+ self._channels_scroll_area.setWidget(self._channels_container)
167
+
168
+ add_button = QPushButton("Add channel")
169
+ add_button.clicked.connect(self._add_channel)
170
+
171
+ section_layout = QVBoxLayout()
172
+ section_layout.setContentsMargins(10, 12, 10, 10)
173
+ section_layout.addWidget(self._channels_scroll_area)
174
+ section_layout.addWidget(add_button)
175
+ section.setLayout(section_layout)
176
+
177
+ self._channels_section = section
178
+ return section
179
+
180
+ @staticmethod
181
+ def _section_stylesheet() -> str:
182
+ """Return the stylesheet used for dialog sections.
183
+
184
+ Returns
185
+ -------
186
+ str
187
+ Qt stylesheet string for section group boxes.
188
+ """
189
+ return (
190
+ "QGroupBox {"
191
+ " margin-top: 8px;"
192
+ "}"
193
+ "QGroupBox::title {"
194
+ " subcontrol-origin: margin;"
195
+ " subcontrol-position: top left;"
196
+ " padding: 0 6px;"
197
+ "}"
198
+ )
199
+
200
+ def _refresh_labels_combo(self, combo: QComboBox, filter_type: str = "cellular") -> None:
201
+ """Refresh labels layer options for the dialog.
202
+
203
+ Parameters
204
+ ----------
205
+ combo : QComboBox
206
+ Labels combo box to refresh.
207
+ filter_type : str, optional
208
+ Type of labels to show: "cellular" for nuc/cyto labels,
209
+ "spots" for spot labels. Defaults to "cellular".
210
+ """
211
+ current = combo.currentText()
212
+ combo.clear()
213
+ viewer = self._tab._viewer
214
+ if viewer is None:
215
+ combo.addItem("Select labels")
216
+ return
217
+ for layer in viewer.layers:
218
+ if layer.__class__.__name__ == "Labels":
219
+ layer_name = layer.name
220
+ # Filter based on label type
221
+ if filter_type == "cellular" and self._is_cellular_label(layer_name):
222
+ combo.addItem(layer_name)
223
+ elif filter_type == "spots" and self._is_spot_label(layer_name):
224
+ combo.addItem(layer_name)
225
+ if current:
226
+ index = combo.findText(current)
227
+ if index != -1:
228
+ combo.setCurrentIndex(index)
229
+
230
+ def _is_cellular_label(self, layer_name: str) -> bool:
231
+ """Check if a label layer is a cellular segmentation.
232
+
233
+ Parameters
234
+ ----------
235
+ layer_name : str
236
+ Name of the labels layer.
237
+
238
+ Returns
239
+ -------
240
+ bool
241
+ True if the layer is a cellular label (nuclear or cytoplasmic).
242
+ """
243
+ return layer_name.endswith("_nuc_labels") or layer_name.endswith("_cyto_labels")
244
+
245
+ def _is_spot_label(self, layer_name: str) -> bool:
246
+ """Check if a label layer is a spot segmentation.
247
+
248
+ Parameters
249
+ ----------
250
+ layer_name : str
251
+ Name of the labels layer.
252
+
253
+ Returns
254
+ -------
255
+ bool
256
+ True if the layer is a spot label.
257
+ """
258
+ return layer_name.endswith("_spot_labels")
259
+
260
+ def _refresh_image_combo(self, combo: QComboBox) -> None:
261
+ """Refresh image layer options for the dialog.
262
+
263
+ Parameters
264
+ ----------
265
+ combo : QComboBox
266
+ Image combo box to refresh.
267
+ """
268
+ current = combo.currentText()
269
+ combo.clear()
270
+ viewer = self._tab._viewer
271
+ if viewer is None:
272
+ combo.addItem("Select image")
273
+ return
274
+ for layer in viewer.layers:
275
+ if layer.__class__.__name__ == "Image":
276
+ combo.addItem(layer.name)
277
+ if current:
278
+ index = combo.findText(current)
279
+ if index != -1:
280
+ combo.setCurrentIndex(index)
281
+
282
+ def _load_segmentations(self) -> None:
283
+ """Build segmentation rows from stored data."""
284
+ if not self._segmentations:
285
+ return
286
+ for segmentation_data in self._segmentations:
287
+ if not isinstance(segmentation_data, SpotsSegmentationConfig):
288
+ continue
289
+ self._add_segmentation(segmentation_data)
290
+
291
+ def _load_channels(self) -> None:
292
+ """Build channel rows from stored data."""
293
+ if not self._channels:
294
+ return
295
+ for channel_data in self._channels:
296
+ if not isinstance(channel_data, SpotsChannelConfig):
297
+ continue
298
+ self._add_channel(channel_data)
299
+
300
+ def _add_channel(self, channel_data: SpotsChannelConfig | None = None) -> None:
301
+ """Add a channel row to the dialog.
302
+
303
+ Parameters
304
+ ----------
305
+ channel_data : SpotsChannelConfig or None
306
+ Channel configuration data.
307
+ """
308
+ if isinstance(channel_data, bool):
309
+ channel_data = None
310
+ if not isinstance(channel_data, SpotsChannelConfig):
311
+ channel_data = SpotsChannelConfig()
312
+ self._channels.append(channel_data)
313
+ row = SpotsChannelRow(self, channel_data)
314
+ self._rows.append(row)
315
+ self._channels_layout.addWidget(row)
316
+ self._renumber_rows()
317
+ self._schedule_layout_update()
318
+
319
+ def _remove_channel(self, row: SpotsChannelRow) -> None:
320
+ """Remove a channel row and its stored data.
321
+
322
+ Parameters
323
+ ----------
324
+ row : SpotsChannelRow
325
+ Row instance to remove.
326
+ """
327
+ if row not in self._rows:
328
+ return
329
+ self._rows.remove(row)
330
+ if row.data in self._channels:
331
+ self._channels.remove(row.data)
332
+ self._channels_layout.removeWidget(row)
333
+ row.deleteLater()
334
+ self._renumber_rows()
335
+ self._schedule_layout_update()
336
+
337
+ def _renumber_rows(self) -> None:
338
+ """Update channel row titles after changes."""
339
+ for index, row in enumerate(self._rows, start=0):
340
+ row.update_title(index)
341
+
342
+ def _add_segmentation(
343
+ self, segmentation_data: SpotsSegmentationConfig | None = None
344
+ ) -> None:
345
+ """Add a segmentation row to the dialog.
346
+
347
+ Parameters
348
+ ----------
349
+ segmentation_data : SpotsSegmentationConfig or None
350
+ Segmentation configuration data.
351
+ """
352
+ if isinstance(segmentation_data, bool):
353
+ segmentation_data = None
354
+ if not isinstance(segmentation_data, SpotsSegmentationConfig):
355
+ segmentation_data = SpotsSegmentationConfig()
356
+ self._segmentations.append(segmentation_data)
357
+ row = SpotsSegmentationRow(self, segmentation_data)
358
+ self._segmentation_rows.append(row)
359
+ self._segmentations_layout.addWidget(row)
360
+ self._renumber_segmentations()
361
+ self._schedule_layout_update()
362
+
363
+ def _remove_segmentation(self, row: SpotsSegmentationRow) -> None:
364
+ """Remove a segmentation row and its stored data.
365
+
366
+ Parameters
367
+ ----------
368
+ row : SpotsSegmentationRow
369
+ Row instance to remove.
370
+ """
371
+ if row not in self._segmentation_rows:
372
+ return
373
+ self._segmentation_rows.remove(row)
374
+ if row.data in self._segmentations:
375
+ self._segmentations.remove(row.data)
376
+ self._segmentations_layout.removeWidget(row)
377
+ row.deleteLater()
378
+ self._renumber_segmentations()
379
+ self._schedule_layout_update()
380
+
381
+ def _renumber_segmentations(self) -> None:
382
+ """Update segmentation row titles after changes."""
383
+ for index, row in enumerate(self._segmentation_rows, start=0):
384
+ row.update_title(index)
385
+
386
+ def _start_layout_watch(self) -> None:
387
+ """Start a timer to monitor layout changes in the dialog."""
388
+ if self._layout_watch_timer is not None:
389
+ return
390
+ self._layout_watch_timer = QTimer(self)
391
+ self._layout_watch_timer.setInterval(150)
392
+ self._layout_watch_timer.timeout.connect(self._poll_layout)
393
+ self._layout_watch_timer.start()
394
+
395
+ def _schedule_layout_update(self) -> None:
396
+ """Schedule a layout update on the next timer tick."""
397
+ self._layout_last_sizes.clear()
398
+
399
+ def _poll_layout(self) -> None:
400
+ """Recompute layout sizing when content changes."""
401
+ self._apply_scroll_area_layout(
402
+ "segmentations",
403
+ self._segmentations_scroll_area,
404
+ self._segmentations_layout,
405
+ max_ratio=0.2,
406
+ )
407
+ self._apply_scroll_area_layout(
408
+ "channels",
409
+ self._channels_scroll_area,
410
+ self._channels_layout,
411
+ max_ratio=0.8,
412
+ )
413
+
414
+ def _apply_scroll_area_layout(
415
+ self,
416
+ key: str,
417
+ scroll_area: QScrollArea,
418
+ layout: QVBoxLayout,
419
+ max_ratio: float,
420
+ ) -> None:
421
+ """Apply sizing rules for a scroll area section.
422
+
423
+ Parameters
424
+ ----------
425
+ key : str
426
+ Cache key for the section size.
427
+ scroll_area : QScrollArea
428
+ Scroll area to resize.
429
+ layout : QVBoxLayout
430
+ Layout containing section rows.
431
+ max_ratio : float
432
+ Maximum height ratio relative to the screen.
433
+ """
434
+ size = self._layout_content_size(layout)
435
+ if self._layout_last_sizes.get(key) == size:
436
+ return
437
+ self._layout_last_sizes[key] = size
438
+ content = scroll_area.widget()
439
+ if content is not None:
440
+ content.setMinimumWidth(scroll_area.viewport().width())
441
+ scroll_area.updateGeometry()
442
+ scroll_area.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
443
+
444
+ def _layout_content_size(self, layout: QVBoxLayout) -> tuple[int, int]:
445
+ """Return content size for a vertical layout.
446
+
447
+ Parameters
448
+ ----------
449
+ layout : QVBoxLayout
450
+ Layout to measure.
451
+
452
+ Returns
453
+ -------
454
+ tuple of int
455
+ (width, height) of the layout contents.
456
+ """
457
+ layout.activate()
458
+ margins = layout.contentsMargins()
459
+ spacing = layout.spacing()
460
+ count = layout.count()
461
+ total_height = margins.top() + margins.bottom()
462
+ max_width = 0
463
+ for index in range(count):
464
+ item = layout.itemAt(index)
465
+ widget = item.widget()
466
+ if widget is None:
467
+ item_size = item.sizeHint()
468
+ else:
469
+ item_size = widget.sizeHint().expandedTo(
470
+ widget.minimumSizeHint()
471
+ )
472
+ max_width = max(max_width, item_size.width())
473
+ total_height += item_size.height()
474
+ if count > 1:
475
+ total_height += spacing * (count - 1)
476
+ total_width = margins.left() + margins.right() + max_width
477
+ return (total_width, total_height)