napari-sprout 1.1.0__tar.gz

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 (39) hide show
  1. napari_sprout-1.1.0/LICENSE +21 -0
  2. napari_sprout-1.1.0/PKG-INFO +58 -0
  3. napari_sprout-1.1.0/README.md +28 -0
  4. napari_sprout-1.1.0/napari_sprout/__init__.py +14 -0
  5. napari_sprout-1.1.0/napari_sprout/_widget.py +262 -0
  6. napari_sprout-1.1.0/napari_sprout/layers/__init__.py +1 -0
  7. napari_sprout-1.1.0/napari_sprout/napari.yaml +33 -0
  8. napari_sprout-1.1.0/napari_sprout/utils/__init__.py +1 -0
  9. napari_sprout-1.1.0/napari_sprout/utils/boundary_check.py +53 -0
  10. napari_sprout-1.1.0/napari_sprout/utils/fit_table.py +49 -0
  11. napari_sprout-1.1.0/napari_sprout/utils/histogram_threshold_widget.py +504 -0
  12. napari_sprout-1.1.0/napari_sprout/utils/histogram_widget.py +244 -0
  13. napari_sprout-1.1.0/napari_sprout/utils/jobs.py +270 -0
  14. napari_sprout-1.1.0/napari_sprout/utils/layer_files.py +39 -0
  15. napari_sprout-1.1.0/napari_sprout/utils/layer_lock.py +61 -0
  16. napari_sprout-1.1.0/napari_sprout/utils/roi_layers.py +637 -0
  17. napari_sprout-1.1.0/napari_sprout/utils/roi_widget.py +819 -0
  18. napari_sprout-1.1.0/napari_sprout/utils/slice_preview.py +112 -0
  19. napari_sprout-1.1.0/napari_sprout/utils/status.py +193 -0
  20. napari_sprout-1.1.0/napari_sprout/utils/util_widget.py +1370 -0
  21. napari_sprout-1.1.0/napari_sprout/widgets/__init__.py +1 -0
  22. napari_sprout-1.1.0/napari_sprout/widgets/boundary_widget.py +744 -0
  23. napari_sprout-1.1.0/napari_sprout/widgets/click_owner.py +101 -0
  24. napari_sprout-1.1.0/napari_sprout/widgets/edit_widget.py +1696 -0
  25. napari_sprout-1.1.0/napari_sprout/widgets/grow_widget.py +785 -0
  26. napari_sprout-1.1.0/napari_sprout/widgets/info_widget.py +333 -0
  27. napari_sprout-1.1.0/napari_sprout/widgets/map_widget.py +528 -0
  28. napari_sprout-1.1.0/napari_sprout/widgets/prompt_tab.py +1683 -0
  29. napari_sprout-1.1.0/napari_sprout/widgets/prompt_widget.py +189 -0
  30. napari_sprout-1.1.0/napari_sprout/widgets/seed_widget.py +1042 -0
  31. napari_sprout-1.1.0/napari_sprout/widgets/tools_widget.py +989 -0
  32. napari_sprout-1.1.0/napari_sprout.egg-info/PKG-INFO +58 -0
  33. napari_sprout-1.1.0/napari_sprout.egg-info/SOURCES.txt +37 -0
  34. napari_sprout-1.1.0/napari_sprout.egg-info/dependency_links.txt +1 -0
  35. napari_sprout-1.1.0/napari_sprout.egg-info/entry_points.txt +2 -0
  36. napari_sprout-1.1.0/napari_sprout.egg-info/requires.txt +11 -0
  37. napari_sprout-1.1.0/napari_sprout.egg-info/top_level.txt +1 -0
  38. napari_sprout-1.1.0/pyproject.toml +50 -0
  39. napari_sprout-1.1.0/setup.cfg +4 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Yichen He
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,58 @@
1
+ Metadata-Version: 2.4
2
+ Name: napari-sprout
3
+ Version: 1.1.0
4
+ Summary: napari plugin for SPROUT: seeds, growing, editing and AI prompts for 2D and 3D segmentation
5
+ Author: Yichen He
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/EchanHe/SPROUT
8
+ Project-URL: Documentation, https://github.com/EchanHe/SPROUT#readme
9
+ Project-URL: Issues, https://github.com/EchanHe/SPROUT/issues
10
+ Project-URL: Changelog, https://github.com/EchanHe/SPROUT/blob/main/CHANGELOG.md
11
+ Keywords: napari,segmentation,image analysis,micro-CT,CT,MRI,3D,multi-class
12
+ Classifier: Framework :: napari
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Intended Audience :: Science/Research
16
+ Classifier: Topic :: Scientific/Engineering :: Image Processing
17
+ Requires-Python: >=3.10
18
+ Description-Content-Type: text/markdown
19
+ License-File: LICENSE
20
+ Requires-Dist: sprout-seg~=1.1.0
21
+ Requires-Dist: napari>=0.6.1
22
+ Requires-Dist: qtpy
23
+ Requires-Dist: magicgui
24
+ Requires-Dist: superqt
25
+ Provides-Extra: all
26
+ Requires-Dist: napari[all]; extra == "all"
27
+ Provides-Extra: ai
28
+ Requires-Dist: sprout-seg[ai]~=1.1.0; extra == "ai"
29
+ Dynamic: license-file
30
+
31
+ # napari-sprout
32
+
33
+ The napari plugin of [SPROUT](https://github.com/EchanHe/SPROUT): multi-part segmentation of 2D and 3D greyscale images (e.g. CT, MRI) without training data.
34
+
35
+ - **Seeds**: thresholds and erosion split an image into parts; adaptive and sweep modes combine several settings.
36
+ - **Grow**: seeds grow back to the full shape of each part.
37
+ - **Edit**: split, merge, relabel and clean regions.
38
+ - **Prompt (AI)**: seeds as prompts for nnInteractive.
39
+ - **ROI & Boundary**: work inside a region; make boundary masks.
40
+
41
+ ## Install
42
+
43
+ ```bash
44
+ pip install "napari-sprout[all]" # with napari and Qt
45
+ pip install napari-sprout # napari already installed
46
+ ```
47
+
48
+ For the Prompt (AI) tab also run `pip install "sprout-seg[ai]"` (with an NVIDIA GPU, install PyTorch for your CUDA version first).
49
+
50
+ ## Use
51
+
52
+ Start napari and open **Plugins → SPROUT**. Videos: [SPROUT playlist](https://www.youtube.com/watch?v=4DHI1gs1lzc&list=PL8tsvVKJ9n6FjYYplo-IAwKBLxDh578aj). Docs and the command line (`sprout`): [github.com/EchanHe/SPROUT](https://github.com/EchanHe/SPROUT).
53
+
54
+ ## Cite
55
+
56
+ Y. He, M. Camaiti, L.E. Roberts, J.M. Mulqueeney, E. Ioannou, M. Didziokas, A. Goswami. *SPROUT: A User-friendly, Scalable Toolkit for Multi-class Segmentation of Volumetric Images.* [bioRxiv 2024](https://www.biorxiv.org/content/10.1101/2024.11.22.624847v2.abstract).
57
+
58
+ MIT License.
@@ -0,0 +1,28 @@
1
+ # napari-sprout
2
+
3
+ The napari plugin of [SPROUT](https://github.com/EchanHe/SPROUT): multi-part segmentation of 2D and 3D greyscale images (e.g. CT, MRI) without training data.
4
+
5
+ - **Seeds**: thresholds and erosion split an image into parts; adaptive and sweep modes combine several settings.
6
+ - **Grow**: seeds grow back to the full shape of each part.
7
+ - **Edit**: split, merge, relabel and clean regions.
8
+ - **Prompt (AI)**: seeds as prompts for nnInteractive.
9
+ - **ROI & Boundary**: work inside a region; make boundary masks.
10
+
11
+ ## Install
12
+
13
+ ```bash
14
+ pip install "napari-sprout[all]" # with napari and Qt
15
+ pip install napari-sprout # napari already installed
16
+ ```
17
+
18
+ For the Prompt (AI) tab also run `pip install "sprout-seg[ai]"` (with an NVIDIA GPU, install PyTorch for your CUDA version first).
19
+
20
+ ## Use
21
+
22
+ Start napari and open **Plugins → SPROUT**. Videos: [SPROUT playlist](https://www.youtube.com/watch?v=4DHI1gs1lzc&list=PL8tsvVKJ9n6FjYYplo-IAwKBLxDh578aj). Docs and the command line (`sprout`): [github.com/EchanHe/SPROUT](https://github.com/EchanHe/SPROUT).
23
+
24
+ ## Cite
25
+
26
+ Y. He, M. Camaiti, L.E. Roberts, J.M. Mulqueeney, E. Ioannou, M. Didziokas, A. Goswami. *SPROUT: A User-friendly, Scalable Toolkit for Multi-class Segmentation of Volumetric Images.* [bioRxiv 2024](https://www.biorxiv.org/content/10.1101/2024.11.22.624847v2.abstract).
27
+
28
+ MIT License.
@@ -0,0 +1,14 @@
1
+ """napari-sprout: A napari plugin for SPROUT segmentation."""
2
+
3
+ __version__ = "1.1.0"
4
+
5
+ from ._widget import make_sprout_widget
6
+ from ._widget import make_sprout_widget_edit
7
+ from ._widget import make_sprout_widget_info
8
+ from ._widget import make_sprout_widget_map
9
+
10
+ __all__ = ["make_sprout_widget",
11
+ "make_sprout_widget_edit",
12
+ "make_sprout_widget_info",
13
+ "make_sprout_widget_map"
14
+ ]
@@ -0,0 +1,262 @@
1
+ """Main widget for napari-SPROUT plugin."""
2
+
3
+ from qtpy.QtWidgets import (
4
+ QWidget, QVBoxLayout, QTabWidget, QPushButton,
5
+ QLabel, QHBoxLayout, QFileDialog
6
+ )
7
+ # from qtpy.QtCore import Qt
8
+ import napari
9
+ from napari.utils.notifications import show_info, show_error
10
+ import numpy as np
11
+ import tifffile
12
+
13
+ from .widgets.seed_widget import SeedGenerationWidget
14
+ from .widgets.grow_widget import SeedGrowthWidget
15
+ from .widgets.boundary_widget import BoundaryWidget
16
+ from .widgets.prompt_tab import PromptTab
17
+ from .utils.roi_widget import RoiTools
18
+
19
+ from .widgets.info_widget import LayerInfoWidget
20
+ from .widgets.edit_widget import QtLabelSelector
21
+ from .widgets.map_widget import LabelMapperWidget
22
+ from .widgets.tools_widget import SPROUTToolWidget
23
+
24
+ class SPROUTWidget(QWidget):
25
+ """Main SPROUT widget containing all workflow steps."""
26
+
27
+ def __init__(self, napari_viewer):
28
+ super().__init__()
29
+ self.viewer = napari_viewer
30
+
31
+ self._init_ui()
32
+ self._connect_signals()
33
+
34
+ def _init_ui(self):
35
+ """Initialize the user interface."""
36
+ layout = QVBoxLayout()
37
+
38
+ # Set minimum size to ensure visibility
39
+ self.setMinimumSize(500, 500)
40
+
41
+ # Header
42
+ header_layout = QHBoxLayout()
43
+ title = QLabel("<h4>SPROUT Segmentation</h2>")
44
+ header_layout.addWidget(title)
45
+ header_layout.addStretch()
46
+
47
+ # Load image button
48
+ # self.load_btn = QPushButton("Load Image")
49
+ # header_layout.addWidget(self.load_btn)
50
+
51
+ layout.addLayout(header_layout)
52
+
53
+ # Info label
54
+ # info_text = (
55
+ # "Semi-automated Parcellation of Region Outputs Using "
56
+ # "Thresholding and Transformation"
57
+ # )
58
+ # info_label = QLabel(info_text)
59
+ # info_label.setWordWrap(True)
60
+ # info_label.setStyleSheet("QLabel { color: #666; }")
61
+ # layout.addWidget(info_label)
62
+
63
+ # Tab widget for workflow steps
64
+ self.tabs = QTabWidget()
65
+
66
+ # Create widgets for each step
67
+ self.seed_widget = SeedGenerationWidget(self.viewer)
68
+ self.grow_widget = SeedGrowthWidget(self.viewer)
69
+ self.prompt_tab = PromptTab(self.viewer)
70
+ self.boundary_widget = BoundaryWidget(self.viewer)
71
+
72
+ # ROI tools and the Boundary tab share the last tab (two sub-tabs)
73
+ self.roi_tools = RoiTools(self.viewer)
74
+ self.roi_boundary_tabs = QTabWidget()
75
+ self.roi_boundary_tabs.addTab(self.roi_tools, "ROI")
76
+ self.roi_boundary_tabs.addTab(self.boundary_widget, "Boundary")
77
+
78
+ # Add tabs
79
+ self.tabs.addTab(self.seed_widget, "1. Seeds")
80
+ self.tabs.addTab(self.grow_widget, "2. Grow")
81
+ self.tabs.addTab(self.prompt_tab, "3. Prompt (AI)")
82
+ self.tabs.addTab(self.roi_boundary_tabs, "ROI && Boundary") # "&&": a single "&" is a Qt mnemonic
83
+
84
+ layout.addWidget(self.tabs)
85
+
86
+
87
+ self.setLayout(layout)
88
+
89
+ def _connect_signals(self):
90
+ """Connect widget signals."""
91
+ # self.load_btn.clicked.connect(self.load_image)
92
+ self.seed_widget.seeds_generated.connect(self._on_seeds_generated)
93
+ self.grow_widget.growth_completed.connect(self._on_growth_completed)
94
+ self.boundary_widget.boundary_created.connect(self._on_boundary_created)
95
+
96
+ def _on_boundary_created(self, name):
97
+ """Select a newly created boundary layer in the Seeds and Grow tabs."""
98
+ for w in (self.seed_widget, self.grow_widget):
99
+ w.boundary_combo.setCurrentText(name)
100
+
101
+ def load_image(self):
102
+ """Load an image file."""
103
+ filename, _ = QFileDialog.getOpenFileName(
104
+ self,
105
+ "Load Image",
106
+ "",
107
+ "Image Files (*.tif *.tiff);;All Files (*)"
108
+ )
109
+
110
+ if filename:
111
+ try:
112
+ # Load image
113
+ image = tifffile.imread(filename)
114
+
115
+ # Add to viewer
116
+ self.viewer.add_image(
117
+ image,
118
+ name=filename.split('/')[-1]
119
+ )
120
+
121
+ # Refresh layer lists
122
+ self.seed_widget.refresh_layers()
123
+ self.grow_widget.refresh_layers()
124
+
125
+ show_info(f"Loaded image: {filename}")
126
+
127
+ except Exception as e:
128
+ show_error(f"Error loading image: {str(e)}")
129
+
130
+ def _on_seeds_generated(self, seeds, sizes):
131
+ """Handle seed generation completion."""
132
+ # Switch to grow tab
133
+ self.tabs.setCurrentIndex(1)
134
+
135
+ # Refresh grow widget layers
136
+ self.grow_widget.refresh_layers()
137
+
138
+ show_info("Seeds generated! You can now edit them or proceed to growth.")
139
+
140
+ def _on_growth_completed(self, result):
141
+ """Handle growth completion."""
142
+ show_info("Growth completed! You can save the result or continue editing.")
143
+
144
+
145
+ def make_sprout_widget(viewer: "napari.viewer.Viewer" = None):
146
+ """Create the SPROUT widget.
147
+
148
+ Parameters
149
+ ----------
150
+ viewer : napari.viewer.Viewer, optional
151
+ The napari viewer instance. If not provided, will get current viewer.
152
+
153
+ Returns
154
+ -------
155
+ widget : SPROUTWidget
156
+ The SPROUT widget instance.
157
+ """
158
+ if viewer is None:
159
+ # Get the current viewer from napari
160
+ import napari
161
+ viewer = napari.current_viewer()
162
+ if viewer is None:
163
+ raise RuntimeError("No napari viewer found")
164
+
165
+ widget = SPROUTWidget(viewer)
166
+ # no widget.show() in these factories: napari shows the widget when it docks it
167
+ # (a show() before that flashes it as a separate window)
168
+ return widget
169
+
170
+ def make_sprout_widget_edit(viewer: "napari.viewer.Viewer" = None):
171
+ """Create the SPROUT widget for editing.
172
+
173
+ Parameters
174
+ ----------
175
+ viewer : napari.viewer.Viewer, optional
176
+ The napari viewer instance. If not provided, will get current viewer.
177
+
178
+ Returns
179
+ -------
180
+ widget : SPROUTWidget
181
+ The SPROUT widget instance.
182
+ """
183
+ if viewer is None:
184
+ # Get the current viewer from napari
185
+ import napari
186
+ viewer = napari.current_viewer()
187
+ if viewer is None:
188
+ raise RuntimeError("No napari viewer found")
189
+
190
+ widget = QtLabelSelector(viewer)
191
+ return widget
192
+
193
+ def make_sprout_widget_info(viewer: "napari.viewer.Viewer" = None):
194
+ """Create the SPROUT widget for information.
195
+
196
+ Parameters
197
+ ----------
198
+ viewer : napari.viewer.Viewer, optional
199
+ The napari viewer instance. If not provided, will get current viewer.
200
+
201
+ Returns
202
+ -------
203
+ widget : SPROUTWidget
204
+ The SPROUT widget instance.
205
+ """
206
+ if viewer is None:
207
+ # Get the current viewer from napari
208
+ import napari
209
+ viewer = napari.current_viewer()
210
+ if viewer is None:
211
+ raise RuntimeError("No napari viewer found")
212
+
213
+ widget = LayerInfoWidget(viewer)
214
+ return widget
215
+
216
+
217
+ def make_sprout_widget_map(viewer: "napari.viewer.Viewer" = None):
218
+ """Create the SPROUT widget for mapping.
219
+
220
+ Parameters
221
+ ----------
222
+ viewer : napari.viewer.Viewer, optional
223
+ The napari viewer instance. If not provided, will get current viewer.
224
+
225
+ Returns
226
+ -------
227
+ widget : SPROUTWidget
228
+ The SPROUT widget instance.
229
+ """
230
+ if viewer is None:
231
+ # Get the current viewer from napari
232
+ import napari
233
+ viewer = napari.current_viewer()
234
+ if viewer is None:
235
+ raise RuntimeError("No napari viewer found")
236
+
237
+ widget = LabelMapperWidget(viewer)
238
+ return widget
239
+
240
+
241
+ def make_sprout_widget_tools(viewer: "napari.viewer.Viewer" = None):
242
+ """Create the SPROUT widget for tools.
243
+
244
+ Parameters
245
+ ----------
246
+ viewer : napari.viewer.Viewer, optional
247
+ The napari viewer instance. If not provided, will get current viewer.
248
+
249
+ Returns
250
+ -------
251
+ widget : SPROUTWidget
252
+ The SPROUT widget instance.
253
+ """
254
+ if viewer is None:
255
+ # Get the current viewer from napari
256
+ import napari
257
+ viewer = napari.current_viewer()
258
+ if viewer is None:
259
+ raise RuntimeError("No napari viewer found")
260
+
261
+ widget = SPROUTToolWidget(viewer)
262
+ return widget
@@ -0,0 +1 @@
1
+ """Layer handling for napari-sprout."""
@@ -0,0 +1,33 @@
1
+ name: napari-sprout
2
+ display_name: SPROUT
3
+ contributions:
4
+ commands:
5
+ - id: napari-sprout.make_widget
6
+ python_name: napari_sprout._widget:make_sprout_widget
7
+ title: SPROUT Main Widget
8
+
9
+ - id: napari-sprout.edit_widget
10
+ python_name: napari_sprout._widget:make_sprout_widget_edit
11
+ title: SPROUT Edit Labels
12
+
13
+ - id: napari-sprout.info_widget
14
+ python_name: napari_sprout._widget:make_sprout_widget_info
15
+ title: SPROUT Data Info
16
+
17
+ - id: napari-sprout.map_sprout
18
+ python_name: napari_sprout._widget:make_sprout_widget_map
19
+ title: SPROUT Label Mapping
20
+ - id: napari-sprout.tools_widget
21
+ python_name: napari_sprout._widget:make_sprout_widget_tools
22
+ title: SPROUT Batch Builder
23
+ widgets:
24
+ - command: napari-sprout.make_widget
25
+ display_name: SPROUT
26
+ - command: napari-sprout.edit_widget
27
+ display_name: Edit
28
+ - command: napari-sprout.map_sprout
29
+ display_name: Label Mapping
30
+ - command: napari-sprout.tools_widget
31
+ display_name: Batch Builder
32
+ - command: napari-sprout.info_widget
33
+ display_name: Data Info
@@ -0,0 +1 @@
1
+ """Utility functions for napari-sprout."""
@@ -0,0 +1,53 @@
1
+ """Check the boundary mask when Generate Seeds / Start Growth is clicked.
2
+
3
+ A mask that cannot be used gives a red line under the Boundary combo (no dialog) and the
4
+ run does not start.
5
+
6
+ SPROUT needs a boundary with exactly two values (0 = keep, non-zero = boundary).
7
+ Without this check a wrong layer only fails inside the background worker, with a
8
+ less specific message.
9
+ """
10
+ import numpy as np
11
+
12
+
13
+ def boundary_problem(data):
14
+ """None if `data` works as a SPROUT boundary, else a short description.
15
+
16
+ Mirrors config_core.check_and_cast_boundary: bool, or exactly two values one of
17
+ which is 0. ~1 s on 1e9 voxels (np.unique would take ~20 s).
18
+ """
19
+ data = np.asarray(data)
20
+ if data.dtype == bool:
21
+ return None if data.any() else "it is empty (no boundary voxels)"
22
+ mx, mn = data.max(), data.min()
23
+ if mx == 0 and mn == 0:
24
+ return "it is empty (no boundary voxels)"
25
+ if mn != 0 and mx != 0 and not np.any(data == 0):
26
+ return "it has no 0 voxels (everything would be boundary)"
27
+ if np.any((data != 0) & (data != mx)):
28
+ step = max(1, int(round((data.size / 2e6) ** (1 / data.ndim))))
29
+ values = np.unique(data[(slice(None, None, step),) * data.ndim])
30
+ shown = ", ".join(str(v) for v in values[:6]) + (", ..." if values.size > 6 else "")
31
+ return f"it has more than two values ({shown})"
32
+ return None
33
+
34
+
35
+ def boundary_message(name, problem):
36
+ return (f"The boundary mask '{name}' cannot be used: {problem}.\n"
37
+ "A boundary mask needs exactly two values: 0 = keep, non-zero = boundary.\n"
38
+ "Choose another layer, or set Boundary Mask to None.")
39
+
40
+
41
+ def boundary_ok(parent, name, data, note=None):
42
+ """True if the boundary can be used; otherwise say why in `note` (the red line under
43
+ the Boundary combo, a utils.status.StatusLine) and return False. No dialog."""
44
+ if note is not None:
45
+ note.clear()
46
+ if data is None:
47
+ return True
48
+ problem = boundary_problem(data)
49
+ if problem is None:
50
+ return True
51
+ if note is not None:
52
+ note.set(boundary_message(name, problem), "error")
53
+ return False
@@ -0,0 +1,49 @@
1
+ """A table as high as its rows (no empty filler), capped; it scrolls inside beyond that."""
2
+ from qtpy.QtCore import QTimer
3
+ from qtpy.QtWidgets import QSizePolicy, QTableWidget
4
+
5
+
6
+ class FitTable(QTableWidget):
7
+ """Header + rows high, up to MAX_ROWS rows; an inner scrollbar beyond that.
8
+
9
+ The height follows row inserts / removals by itself (call fit() after anything else
10
+ that changes row heights). Full width: the columns stretch over it.
11
+ """
12
+
13
+ MAX_ROWS = 10
14
+
15
+ def __init__(self, *args, max_rows=None, auto_fit=True, **kwargs):
16
+ super().__init__(*args, **kwargs)
17
+ if max_rows is not None:
18
+ self.MAX_ROWS = max_rows
19
+ if auto_fit:
20
+ m = self.model()
21
+ for sig in (m.rowsInserted, m.rowsRemoved, m.modelReset, m.layoutChanged):
22
+ sig.connect(self._fit_later)
23
+ self.horizontalScrollBar().rangeChanged.connect(self._fit_if_alive)
24
+ self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
25
+ self.fit()
26
+
27
+ def _fit_later(self, *_):
28
+ self.fit()
29
+ QTimer.singleShot(0, self._fit_if_alive) # row heights settle after the insert
30
+
31
+ def _fit_if_alive(self):
32
+ try:
33
+ self.fit()
34
+ except RuntimeError: # deleted meanwhile
35
+ pass
36
+
37
+ def fit(self):
38
+ n = min(self.rowCount(), self.MAX_ROWS)
39
+ h = 2 * self.frameWidth() + self.horizontalHeader().sizeHint().height()
40
+ h += sum(self.rowHeight(i) for i in range(n))
41
+ if self.horizontalScrollBar().maximum() > 0: # columns wider than the table (not the header length:
42
+ # right after the vertical scrollbar appears it is briefly wider than the viewport)
43
+ h += self.horizontalScrollBar().sizeHint().height()
44
+ if self.minimumHeight() != h or self.maximumHeight() != h:
45
+ self.setFixedHeight(h)
46
+
47
+ def resizeEvent(self, event):
48
+ super().resizeEvent(event)
49
+ self.fit() # a narrower table may need its horizontal scrollbar