synapse_net.tools.base_widget
1import os 2import sys 3from contextlib import contextmanager 4from pathlib import Path 5 6import napari 7import numpy as np 8import qtpy.QtWidgets as QtWidgets 9 10from napari.utils.notifications import show_info 11from qtpy.QtCore import Qt 12from qtpy.QtWidgets import ( 13 QApplication, QWidget, QVBoxLayout, QHBoxLayout, QLabel, QSpinBox, QComboBox, QCheckBox 14) 15from superqt import QCollapsible 16 17try: 18 from napari_skimage_regionprops import add_table, get_table 19except ImportError: 20 add_table, get_table = None, None 21 22 23class _SilencePrint: 24 def __enter__(self): 25 self._original_stdout = sys.stdout 26 sys.stdout = open(os.devnull, "w") 27 28 def __exit__(self, exc_type, exc_val, exc_tb): 29 sys.stdout.close() 30 sys.stdout = self._original_stdout 31 32 33class BaseWidget(QWidget): 34 def __init__(self): 35 super().__init__() 36 self.viewer = napari.current_viewer() 37 self.attribute_dict = {} 38 39 def _create_layer_selector(self, selector_name, layer_type="Image", prefer_substring=None): 40 """Create a layer selector for an image or labels and store it in a dictionary. 41 42 Args: 43 selector_name (str): The name of the selector, used as a key in the dictionary. 44 layer_type (str): The type of layer to filter for ("Image" or "Labels"). 45 prefer_substring (str, optional): If given, the selector auto-defaults to the first 46 layer whose name contains this substring (case-insensitive); falls back to the first 47 layer otherwise. Applied only when there is no valid current selection (initial 48 population or the selected layer was removed); a user's manual choice is preserved. 49 """ 50 if not hasattr(self, "layer_selectors"): 51 self.layer_selectors = {} 52 53 # Determine the annotation type for the widget 54 if layer_type == "Image": 55 layer_filter = napari.layers.Image 56 elif layer_type == "Labels": 57 layer_filter = napari.layers.Labels 58 elif layer_type == "Shapes": 59 layer_filter = napari.layers.Shapes 60 else: 61 raise ValueError("layer_type must be either 'Image' or 'Labels'.") 62 63 selector_widget = QtWidgets.QWidget() 64 image_selector = QtWidgets.QComboBox() 65 layer_label = QtWidgets.QLabel(f"{selector_name}:") 66 67 # Populate initial options 68 self._update_selector(selector=image_selector, layer_filter=layer_filter, prefer_substring=prefer_substring) 69 70 # Update selector on layer events 71 self.viewer.layers.events.inserted.connect( 72 lambda event: self._update_selector(image_selector, layer_filter, prefer_substring) 73 ) 74 self.viewer.layers.events.removed.connect( 75 lambda event: self._update_selector(image_selector, layer_filter, prefer_substring) 76 ) 77 78 # Store the selector in the dictionary 79 self.layer_selectors[selector_name] = selector_widget 80 81 # Set up layout 82 layout = QVBoxLayout() 83 layout.addWidget(layer_label) 84 layout.addWidget(image_selector) 85 selector_widget.setLayout(layout) 86 return selector_widget 87 88 def _update_selector(self, selector, layer_filter, prefer_substring=None): 89 """Update a single selector with the current image layers in the viewer. 90 91 The user's current selection is preserved if that layer still exists. Only when there is 92 no valid current selection (initial population, or the selected layer was removed) does the 93 default apply: the first layer whose name contains ``prefer_substring`` (case-insensitive) 94 if given, otherwise the first layer (QComboBox default). 95 """ 96 previous = selector.currentText() 97 selector.clear() 98 image_layers = [layer.name for layer in self.viewer.layers if isinstance(layer, layer_filter)] 99 selector.addItems(image_layers) 100 if previous in image_layers: 101 selector.setCurrentText(previous) 102 elif prefer_substring: 103 needle = prefer_substring.lower() 104 match = next((name for name in image_layers if needle in name.lower()), None) 105 if match is not None: 106 selector.setCurrentText(match) 107 108 @contextmanager 109 def _computing(self, button, busy_text, idle_text, message): 110 """Show a busy state around a synchronous, GUI-thread-blocking action, then restore it. 111 112 Disables and relabels ``button``, sets a wait cursor, shows ``message`` and forces one repaint 113 so the busy state is painted *before* the blocking call — otherwise none of it would render 114 until the call returned and the button would just look stuck. The cursor, button label and 115 enabled state are restored on exit (also on error). Disabling the button also blocks a 116 re-entrant second click while the action is in flight. All Qt calls are guarded so they no-op 117 without a running QApplication. 118 """ 119 app = QApplication.instance() 120 button.setEnabled(False) 121 button.setText(busy_text) 122 if app is not None: 123 QApplication.setOverrideCursor(Qt.WaitCursor) 124 show_info(message) 125 if app is not None: 126 app.processEvents() 127 try: 128 yield 129 finally: 130 if app is not None: 131 QApplication.restoreOverrideCursor() 132 button.setEnabled(True) 133 button.setText(idle_text) 134 135 def _get_layer_selector_layer(self, selector_name): 136 """Return the layer currently selected in a given selector.""" 137 if selector_name in self.layer_selectors: 138 selector_widget = self.layer_selectors[selector_name] 139 140 # Retrieve the QComboBox from the QWidget's layout 141 image_selector = selector_widget.layout().itemAt(1).widget() 142 143 if isinstance(image_selector, QComboBox): 144 selected_layer_name = image_selector.currentText() 145 if selected_layer_name in self.viewer.layers: 146 return self.viewer.layers[selected_layer_name] 147 return None # Return None if layer not found 148 149 def _get_layer_selector_data(self, selector_name, return_metadata=False): 150 """Return the data for the layer currently selected in a given selector.""" 151 if selector_name in self.layer_selectors: 152 selector_widget = self.layer_selectors[selector_name] 153 154 # Retrieve the QComboBox from the QWidget's layout 155 image_selector = selector_widget.layout().itemAt(1).widget() 156 157 if isinstance(image_selector, QComboBox): 158 selected_layer_name = image_selector.currentText() 159 if selected_layer_name in self.viewer.layers: 160 if return_metadata: 161 return self.viewer.layers[selected_layer_name].metadata 162 else: 163 return self.viewer.layers[selected_layer_name].data 164 return None # Return None if layer not found 165 166 def _add_string_param(self, name, value, title=None, placeholder=None, layout=None, tooltip=None): 167 if layout is None: 168 layout = QtWidgets.QHBoxLayout() 169 label = QtWidgets.QLabel(title or name) 170 if tooltip: 171 label.setToolTip(tooltip) 172 layout.addWidget(label) 173 param = QtWidgets.QLineEdit() 174 param.setText(value) 175 if placeholder is not None: 176 param.setPlaceholderText(placeholder) 177 param.textChanged.connect(lambda val: setattr(self, name, val)) 178 if tooltip: 179 param.setToolTip(tooltip) 180 layout.addWidget(param) 181 return param, layout 182 183 def _add_float_param(self, name, value, title=None, min_val=0.0, max_val=1.0, decimals=2, 184 step=0.01, layout=None, tooltip=None): 185 if layout is None: 186 layout = QtWidgets.QHBoxLayout() 187 label = QtWidgets.QLabel(title or name) 188 if tooltip: 189 label.setToolTip(tooltip) 190 layout.addWidget(label) 191 param = QtWidgets.QDoubleSpinBox() 192 param.setRange(min_val, max_val) 193 param.setDecimals(decimals) 194 param.setValue(value) 195 param.setSingleStep(step) 196 param.valueChanged.connect(lambda val: setattr(self, name, val)) 197 if tooltip: 198 param.setToolTip(tooltip) 199 layout.addWidget(param) 200 return param, layout 201 202 def _add_int_param(self, name, value, min_val, max_val, title=None, step=1, layout=None, tooltip=None): 203 if layout is None: 204 layout = QHBoxLayout() 205 label = QLabel(title or name) 206 if tooltip: 207 label.setToolTip(tooltip) 208 layout.addWidget(label) 209 param = QSpinBox() 210 param.setRange(min_val, max_val) 211 param.setValue(value) 212 param.setSingleStep(step) 213 param.valueChanged.connect(lambda val: setattr(self, name, val)) 214 if tooltip: 215 param.setToolTip(tooltip) 216 layout.addWidget(param) 217 return param, layout 218 219 def _add_choice_param(self, name, value, options, title=None, layout=None, update=None, tooltip=None): 220 if layout is None: 221 layout = QHBoxLayout() 222 label = QLabel(title or name) 223 if tooltip: 224 label.setToolTip(tooltip) 225 layout.addWidget(label) 226 227 # Create the dropdown menu via QComboBox, set the available values. 228 dropdown = QComboBox() 229 dropdown.addItems(options) 230 if update is None: 231 dropdown.currentIndexChanged.connect(lambda index: setattr(self, name, options[index])) 232 else: 233 dropdown.currentIndexChanged.connect(update) 234 235 # Set the correct value for the value. 236 dropdown.setCurrentIndex(dropdown.findText(value)) 237 238 if tooltip: 239 dropdown.setToolTip(tooltip) 240 241 layout.addWidget(dropdown) 242 return dropdown, layout 243 244 def _add_shape_param(self, names, values, min_val, max_val, step=1, title=None, tooltip=None): 245 layout = QHBoxLayout() 246 247 x_layout = QVBoxLayout() 248 x_param, _ = self._add_int_param( 249 names[0], values[0], min_val=min_val, max_val=max_val, layout=x_layout, step=step, 250 title=title[0] if title is not None else title, tooltip=tooltip 251 ) 252 layout.addLayout(x_layout) 253 254 y_layout = QVBoxLayout() 255 y_param, _ = self._add_int_param( 256 names[1], values[1], min_val=min_val, max_val=max_val, layout=y_layout, step=step, 257 title=title[1] if title is not None else title, tooltip=tooltip 258 ) 259 layout.addLayout(y_layout) 260 261 if len(names) == 3: 262 z_layout = QVBoxLayout() 263 z_param, _ = self._add_int_param( 264 names[2], values[2], min_val=min_val, max_val=max_val, layout=z_layout, step=step, 265 title=title[2] if title is not None else title, tooltip=tooltip 266 ) 267 layout.addLayout(z_layout) 268 return x_param, y_param, z_param, layout 269 270 return x_param, y_param, layout 271 272 def _make_collapsible(self, widget, title): 273 parent_widget = QWidget() 274 parent_widget.setLayout(QVBoxLayout()) 275 collapsible = QCollapsible(title, parent_widget) 276 collapsible.addWidget(widget) 277 parent_widget.layout().addWidget(collapsible) 278 return parent_widget 279 280 def _add_boolean_param(self, name, value, title=None, tooltip=None): 281 checkbox = QCheckBox(name if title is None else title) 282 checkbox.setChecked(value) 283 checkbox.stateChanged.connect(lambda val: setattr(self, name, val)) 284 if tooltip: 285 checkbox.setToolTip(tooltip) 286 return checkbox 287 288 def _add_path_param(self, name, value, select_type, title=None, placeholder=None, tooltip=None): 289 assert select_type in ("directory", "file", "both") 290 291 layout = QtWidgets.QHBoxLayout() 292 label = QtWidgets.QLabel(title or name) 293 if tooltip: 294 label.setToolTip(tooltip) 295 layout.addWidget(label) 296 297 path_textbox = QtWidgets.QLineEdit() 298 path_textbox.setText(str(value)) 299 if placeholder is not None: 300 path_textbox.setPlaceholderText(placeholder) 301 path_textbox.textChanged.connect(lambda val: setattr(self, name, val)) 302 if tooltip: 303 path_textbox.setToolTip(tooltip) 304 305 layout.addWidget(path_textbox) 306 307 def add_path_button(select_type, tooltip=None): 308 # Adjust button text. 309 button_text = f"Select {select_type.capitalize()}" 310 path_button = QtWidgets.QPushButton(button_text) 311 312 # Call appropriate function based on select_type. 313 path_button.clicked.connect(lambda: getattr(self, f"_get_{select_type}_path")(name, path_textbox)) 314 if tooltip: 315 path_button.setToolTip(tooltip) 316 layout.addWidget(path_button) 317 318 if select_type == "both": 319 add_path_button("file") 320 add_path_button("directory") 321 322 else: 323 add_path_button(select_type) 324 325 return path_textbox, layout 326 327 def _get_directory_path(self, name, textbox, tooltip=None): 328 directory = QtWidgets.QFileDialog.getExistingDirectory( 329 self, "Select Directory", "", QtWidgets.QFileDialog.ShowDirsOnly 330 ) 331 if tooltip: 332 directory.setToolTip(tooltip) 333 if directory and Path(directory).is_dir(): 334 textbox.setText(str(directory)) 335 else: 336 # Handle the case where the selected path is not a directory 337 print("Invalid directory selected. Please try again.") 338 339 def _get_file_path(self, name, textbox, tooltip=None): 340 file_path, _ = QtWidgets.QFileDialog.getOpenFileName( 341 self, "Select File", "", "All Files (*)" 342 ) 343 if tooltip: 344 file_path.setToolTip(tooltip) 345 if file_path and Path(file_path).is_file(): 346 textbox.setText(str(file_path)) 347 else: 348 # Handle the case where the selected path is not a file 349 print("Invalid file selected. Please try again.") 350 351 def _handle_resolution(self, metadata, voxel_size_param, ndim, return_as_list=True): 352 # Get the resolution / voxel size from the layer metadata if available. 353 resolution = metadata.get("voxel_size", None) 354 355 # If user input was given then override resolution from metadata. 356 axes = "zyx" if ndim == 3 else "yx" 357 if voxel_size_param.value() != 0.0: # Changed from default. 358 resolution = {ax: voxel_size_param.value() for ax in axes} 359 360 if resolution is not None and return_as_list: 361 resolution = [resolution[ax] for ax in axes] 362 assert len(resolution) == ndim 363 364 return resolution 365 366 def _save_table(self, save_path, data): 367 ext = os.path.splitext(save_path)[1] 368 if ext == "": # No file extension given, By default we save to CSV. 369 file_path = f"{save_path}.csv" 370 data.to_csv(file_path, index=False) 371 elif ext == ".csv": # Extension was specified as csv 372 file_path = save_path 373 data.to_csv(file_path, index=False) 374 elif ext == ".xlsx": # We also support excel. 375 file_path = save_path 376 data.to_excel(file_path, index=False) 377 else: 378 raise ValueError("Invalid extension for table: {ext}. We support .csv or .xlsx.") 379 return file_path 380 381 def _add_properties_and_table(self, layer, table_data, save_path=""): 382 layer.properties = table_data 383 384 if add_table is not None: 385 with _SilencePrint(): 386 add_table(layer, self.viewer) 387 388 # Save table to file if save path is provided. 389 if save_path != "": 390 file_path = self._save_table(self.save_path.text(), table_data) 391 show_info(f"INFO: Added table and saved file to {file_path}.") 392 393 def _add_or_update_layer(self, add_fn, name, data, scale, translate, layer_kwargs): 394 """Add a layer via ``add_fn`` (e.g. ``self.viewer.add_labels``), or refresh it in place if a 395 layer with this name already exists. 396 397 On refresh the ``scale``/``translate`` are reapplied (when provided) because a persisted layer 398 keeps its original transform, which may be stale if the source layer / voxel size changed 399 between runs; any ``layer_kwargs`` (e.g. ``opacity``, ``blending``, ``colormap``) are reapplied 400 too. On first add these go through the layer constructor. Returns the (new or existing) layer. 401 """ 402 if name in self.viewer.layers: 403 layer = self.viewer.layers[name] 404 layer.data = data 405 if scale is not None: 406 layer.scale = scale 407 if translate is not None: 408 layer.translate = translate 409 for key, value in layer_kwargs.items(): 410 setattr(layer, key, value) 411 else: 412 ctor_kwargs = dict(layer_kwargs) 413 if scale is not None: 414 ctor_kwargs["scale"] = scale 415 if translate is not None: 416 ctor_kwargs["translate"] = translate 417 layer = add_fn(data, name=name, **ctor_kwargs) 418 return layer 419 420 def add_or_update_labels(self, name, data, *, scale=None, translate=None, **layer_kwargs): 421 """Add a Labels layer, or refresh it in place if one with this name already exists. 422 423 See :meth:`_add_or_update_layer` for the refresh/transform semantics. Extra keyword arguments 424 (``opacity``, ``blending``, ``colormap``, …) are forwarded to the layer. Returns the layer. 425 """ 426 return self._add_or_update_layer( 427 self.viewer.add_labels, name, data, scale, translate, layer_kwargs 428 ) 429 430 def add_or_update_surface(self, name, vertices, faces, *, scale=None, translate=None, 431 values=None, **layer_kwargs): 432 """Add a Surface layer, or refresh it in place if one with this name already exists. 433 434 Surface layers colour by per-vertex ``values``; a constant array (the default) gives a 435 flat-coloured surface. See :meth:`_add_or_update_layer` for the refresh/transform semantics. 436 Returns the layer. 437 """ 438 if values is None: 439 values = np.ones(len(vertices), dtype="float32") 440 return self._add_or_update_layer( 441 self.viewer.add_surface, name, (vertices, faces, values), scale, translate, layer_kwargs 442 )
class
BaseWidget(PyQt5.QtWidgets.QWidget):
34class BaseWidget(QWidget): 35 def __init__(self): 36 super().__init__() 37 self.viewer = napari.current_viewer() 38 self.attribute_dict = {} 39 40 def _create_layer_selector(self, selector_name, layer_type="Image", prefer_substring=None): 41 """Create a layer selector for an image or labels and store it in a dictionary. 42 43 Args: 44 selector_name (str): The name of the selector, used as a key in the dictionary. 45 layer_type (str): The type of layer to filter for ("Image" or "Labels"). 46 prefer_substring (str, optional): If given, the selector auto-defaults to the first 47 layer whose name contains this substring (case-insensitive); falls back to the first 48 layer otherwise. Applied only when there is no valid current selection (initial 49 population or the selected layer was removed); a user's manual choice is preserved. 50 """ 51 if not hasattr(self, "layer_selectors"): 52 self.layer_selectors = {} 53 54 # Determine the annotation type for the widget 55 if layer_type == "Image": 56 layer_filter = napari.layers.Image 57 elif layer_type == "Labels": 58 layer_filter = napari.layers.Labels 59 elif layer_type == "Shapes": 60 layer_filter = napari.layers.Shapes 61 else: 62 raise ValueError("layer_type must be either 'Image' or 'Labels'.") 63 64 selector_widget = QtWidgets.QWidget() 65 image_selector = QtWidgets.QComboBox() 66 layer_label = QtWidgets.QLabel(f"{selector_name}:") 67 68 # Populate initial options 69 self._update_selector(selector=image_selector, layer_filter=layer_filter, prefer_substring=prefer_substring) 70 71 # Update selector on layer events 72 self.viewer.layers.events.inserted.connect( 73 lambda event: self._update_selector(image_selector, layer_filter, prefer_substring) 74 ) 75 self.viewer.layers.events.removed.connect( 76 lambda event: self._update_selector(image_selector, layer_filter, prefer_substring) 77 ) 78 79 # Store the selector in the dictionary 80 self.layer_selectors[selector_name] = selector_widget 81 82 # Set up layout 83 layout = QVBoxLayout() 84 layout.addWidget(layer_label) 85 layout.addWidget(image_selector) 86 selector_widget.setLayout(layout) 87 return selector_widget 88 89 def _update_selector(self, selector, layer_filter, prefer_substring=None): 90 """Update a single selector with the current image layers in the viewer. 91 92 The user's current selection is preserved if that layer still exists. Only when there is 93 no valid current selection (initial population, or the selected layer was removed) does the 94 default apply: the first layer whose name contains ``prefer_substring`` (case-insensitive) 95 if given, otherwise the first layer (QComboBox default). 96 """ 97 previous = selector.currentText() 98 selector.clear() 99 image_layers = [layer.name for layer in self.viewer.layers if isinstance(layer, layer_filter)] 100 selector.addItems(image_layers) 101 if previous in image_layers: 102 selector.setCurrentText(previous) 103 elif prefer_substring: 104 needle = prefer_substring.lower() 105 match = next((name for name in image_layers if needle in name.lower()), None) 106 if match is not None: 107 selector.setCurrentText(match) 108 109 @contextmanager 110 def _computing(self, button, busy_text, idle_text, message): 111 """Show a busy state around a synchronous, GUI-thread-blocking action, then restore it. 112 113 Disables and relabels ``button``, sets a wait cursor, shows ``message`` and forces one repaint 114 so the busy state is painted *before* the blocking call — otherwise none of it would render 115 until the call returned and the button would just look stuck. The cursor, button label and 116 enabled state are restored on exit (also on error). Disabling the button also blocks a 117 re-entrant second click while the action is in flight. All Qt calls are guarded so they no-op 118 without a running QApplication. 119 """ 120 app = QApplication.instance() 121 button.setEnabled(False) 122 button.setText(busy_text) 123 if app is not None: 124 QApplication.setOverrideCursor(Qt.WaitCursor) 125 show_info(message) 126 if app is not None: 127 app.processEvents() 128 try: 129 yield 130 finally: 131 if app is not None: 132 QApplication.restoreOverrideCursor() 133 button.setEnabled(True) 134 button.setText(idle_text) 135 136 def _get_layer_selector_layer(self, selector_name): 137 """Return the layer currently selected in a given selector.""" 138 if selector_name in self.layer_selectors: 139 selector_widget = self.layer_selectors[selector_name] 140 141 # Retrieve the QComboBox from the QWidget's layout 142 image_selector = selector_widget.layout().itemAt(1).widget() 143 144 if isinstance(image_selector, QComboBox): 145 selected_layer_name = image_selector.currentText() 146 if selected_layer_name in self.viewer.layers: 147 return self.viewer.layers[selected_layer_name] 148 return None # Return None if layer not found 149 150 def _get_layer_selector_data(self, selector_name, return_metadata=False): 151 """Return the data for the layer currently selected in a given selector.""" 152 if selector_name in self.layer_selectors: 153 selector_widget = self.layer_selectors[selector_name] 154 155 # Retrieve the QComboBox from the QWidget's layout 156 image_selector = selector_widget.layout().itemAt(1).widget() 157 158 if isinstance(image_selector, QComboBox): 159 selected_layer_name = image_selector.currentText() 160 if selected_layer_name in self.viewer.layers: 161 if return_metadata: 162 return self.viewer.layers[selected_layer_name].metadata 163 else: 164 return self.viewer.layers[selected_layer_name].data 165 return None # Return None if layer not found 166 167 def _add_string_param(self, name, value, title=None, placeholder=None, layout=None, tooltip=None): 168 if layout is None: 169 layout = QtWidgets.QHBoxLayout() 170 label = QtWidgets.QLabel(title or name) 171 if tooltip: 172 label.setToolTip(tooltip) 173 layout.addWidget(label) 174 param = QtWidgets.QLineEdit() 175 param.setText(value) 176 if placeholder is not None: 177 param.setPlaceholderText(placeholder) 178 param.textChanged.connect(lambda val: setattr(self, name, val)) 179 if tooltip: 180 param.setToolTip(tooltip) 181 layout.addWidget(param) 182 return param, layout 183 184 def _add_float_param(self, name, value, title=None, min_val=0.0, max_val=1.0, decimals=2, 185 step=0.01, layout=None, tooltip=None): 186 if layout is None: 187 layout = QtWidgets.QHBoxLayout() 188 label = QtWidgets.QLabel(title or name) 189 if tooltip: 190 label.setToolTip(tooltip) 191 layout.addWidget(label) 192 param = QtWidgets.QDoubleSpinBox() 193 param.setRange(min_val, max_val) 194 param.setDecimals(decimals) 195 param.setValue(value) 196 param.setSingleStep(step) 197 param.valueChanged.connect(lambda val: setattr(self, name, val)) 198 if tooltip: 199 param.setToolTip(tooltip) 200 layout.addWidget(param) 201 return param, layout 202 203 def _add_int_param(self, name, value, min_val, max_val, title=None, step=1, layout=None, tooltip=None): 204 if layout is None: 205 layout = QHBoxLayout() 206 label = QLabel(title or name) 207 if tooltip: 208 label.setToolTip(tooltip) 209 layout.addWidget(label) 210 param = QSpinBox() 211 param.setRange(min_val, max_val) 212 param.setValue(value) 213 param.setSingleStep(step) 214 param.valueChanged.connect(lambda val: setattr(self, name, val)) 215 if tooltip: 216 param.setToolTip(tooltip) 217 layout.addWidget(param) 218 return param, layout 219 220 def _add_choice_param(self, name, value, options, title=None, layout=None, update=None, tooltip=None): 221 if layout is None: 222 layout = QHBoxLayout() 223 label = QLabel(title or name) 224 if tooltip: 225 label.setToolTip(tooltip) 226 layout.addWidget(label) 227 228 # Create the dropdown menu via QComboBox, set the available values. 229 dropdown = QComboBox() 230 dropdown.addItems(options) 231 if update is None: 232 dropdown.currentIndexChanged.connect(lambda index: setattr(self, name, options[index])) 233 else: 234 dropdown.currentIndexChanged.connect(update) 235 236 # Set the correct value for the value. 237 dropdown.setCurrentIndex(dropdown.findText(value)) 238 239 if tooltip: 240 dropdown.setToolTip(tooltip) 241 242 layout.addWidget(dropdown) 243 return dropdown, layout 244 245 def _add_shape_param(self, names, values, min_val, max_val, step=1, title=None, tooltip=None): 246 layout = QHBoxLayout() 247 248 x_layout = QVBoxLayout() 249 x_param, _ = self._add_int_param( 250 names[0], values[0], min_val=min_val, max_val=max_val, layout=x_layout, step=step, 251 title=title[0] if title is not None else title, tooltip=tooltip 252 ) 253 layout.addLayout(x_layout) 254 255 y_layout = QVBoxLayout() 256 y_param, _ = self._add_int_param( 257 names[1], values[1], min_val=min_val, max_val=max_val, layout=y_layout, step=step, 258 title=title[1] if title is not None else title, tooltip=tooltip 259 ) 260 layout.addLayout(y_layout) 261 262 if len(names) == 3: 263 z_layout = QVBoxLayout() 264 z_param, _ = self._add_int_param( 265 names[2], values[2], min_val=min_val, max_val=max_val, layout=z_layout, step=step, 266 title=title[2] if title is not None else title, tooltip=tooltip 267 ) 268 layout.addLayout(z_layout) 269 return x_param, y_param, z_param, layout 270 271 return x_param, y_param, layout 272 273 def _make_collapsible(self, widget, title): 274 parent_widget = QWidget() 275 parent_widget.setLayout(QVBoxLayout()) 276 collapsible = QCollapsible(title, parent_widget) 277 collapsible.addWidget(widget) 278 parent_widget.layout().addWidget(collapsible) 279 return parent_widget 280 281 def _add_boolean_param(self, name, value, title=None, tooltip=None): 282 checkbox = QCheckBox(name if title is None else title) 283 checkbox.setChecked(value) 284 checkbox.stateChanged.connect(lambda val: setattr(self, name, val)) 285 if tooltip: 286 checkbox.setToolTip(tooltip) 287 return checkbox 288 289 def _add_path_param(self, name, value, select_type, title=None, placeholder=None, tooltip=None): 290 assert select_type in ("directory", "file", "both") 291 292 layout = QtWidgets.QHBoxLayout() 293 label = QtWidgets.QLabel(title or name) 294 if tooltip: 295 label.setToolTip(tooltip) 296 layout.addWidget(label) 297 298 path_textbox = QtWidgets.QLineEdit() 299 path_textbox.setText(str(value)) 300 if placeholder is not None: 301 path_textbox.setPlaceholderText(placeholder) 302 path_textbox.textChanged.connect(lambda val: setattr(self, name, val)) 303 if tooltip: 304 path_textbox.setToolTip(tooltip) 305 306 layout.addWidget(path_textbox) 307 308 def add_path_button(select_type, tooltip=None): 309 # Adjust button text. 310 button_text = f"Select {select_type.capitalize()}" 311 path_button = QtWidgets.QPushButton(button_text) 312 313 # Call appropriate function based on select_type. 314 path_button.clicked.connect(lambda: getattr(self, f"_get_{select_type}_path")(name, path_textbox)) 315 if tooltip: 316 path_button.setToolTip(tooltip) 317 layout.addWidget(path_button) 318 319 if select_type == "both": 320 add_path_button("file") 321 add_path_button("directory") 322 323 else: 324 add_path_button(select_type) 325 326 return path_textbox, layout 327 328 def _get_directory_path(self, name, textbox, tooltip=None): 329 directory = QtWidgets.QFileDialog.getExistingDirectory( 330 self, "Select Directory", "", QtWidgets.QFileDialog.ShowDirsOnly 331 ) 332 if tooltip: 333 directory.setToolTip(tooltip) 334 if directory and Path(directory).is_dir(): 335 textbox.setText(str(directory)) 336 else: 337 # Handle the case where the selected path is not a directory 338 print("Invalid directory selected. Please try again.") 339 340 def _get_file_path(self, name, textbox, tooltip=None): 341 file_path, _ = QtWidgets.QFileDialog.getOpenFileName( 342 self, "Select File", "", "All Files (*)" 343 ) 344 if tooltip: 345 file_path.setToolTip(tooltip) 346 if file_path and Path(file_path).is_file(): 347 textbox.setText(str(file_path)) 348 else: 349 # Handle the case where the selected path is not a file 350 print("Invalid file selected. Please try again.") 351 352 def _handle_resolution(self, metadata, voxel_size_param, ndim, return_as_list=True): 353 # Get the resolution / voxel size from the layer metadata if available. 354 resolution = metadata.get("voxel_size", None) 355 356 # If user input was given then override resolution from metadata. 357 axes = "zyx" if ndim == 3 else "yx" 358 if voxel_size_param.value() != 0.0: # Changed from default. 359 resolution = {ax: voxel_size_param.value() for ax in axes} 360 361 if resolution is not None and return_as_list: 362 resolution = [resolution[ax] for ax in axes] 363 assert len(resolution) == ndim 364 365 return resolution 366 367 def _save_table(self, save_path, data): 368 ext = os.path.splitext(save_path)[1] 369 if ext == "": # No file extension given, By default we save to CSV. 370 file_path = f"{save_path}.csv" 371 data.to_csv(file_path, index=False) 372 elif ext == ".csv": # Extension was specified as csv 373 file_path = save_path 374 data.to_csv(file_path, index=False) 375 elif ext == ".xlsx": # We also support excel. 376 file_path = save_path 377 data.to_excel(file_path, index=False) 378 else: 379 raise ValueError("Invalid extension for table: {ext}. We support .csv or .xlsx.") 380 return file_path 381 382 def _add_properties_and_table(self, layer, table_data, save_path=""): 383 layer.properties = table_data 384 385 if add_table is not None: 386 with _SilencePrint(): 387 add_table(layer, self.viewer) 388 389 # Save table to file if save path is provided. 390 if save_path != "": 391 file_path = self._save_table(self.save_path.text(), table_data) 392 show_info(f"INFO: Added table and saved file to {file_path}.") 393 394 def _add_or_update_layer(self, add_fn, name, data, scale, translate, layer_kwargs): 395 """Add a layer via ``add_fn`` (e.g. ``self.viewer.add_labels``), or refresh it in place if a 396 layer with this name already exists. 397 398 On refresh the ``scale``/``translate`` are reapplied (when provided) because a persisted layer 399 keeps its original transform, which may be stale if the source layer / voxel size changed 400 between runs; any ``layer_kwargs`` (e.g. ``opacity``, ``blending``, ``colormap``) are reapplied 401 too. On first add these go through the layer constructor. Returns the (new or existing) layer. 402 """ 403 if name in self.viewer.layers: 404 layer = self.viewer.layers[name] 405 layer.data = data 406 if scale is not None: 407 layer.scale = scale 408 if translate is not None: 409 layer.translate = translate 410 for key, value in layer_kwargs.items(): 411 setattr(layer, key, value) 412 else: 413 ctor_kwargs = dict(layer_kwargs) 414 if scale is not None: 415 ctor_kwargs["scale"] = scale 416 if translate is not None: 417 ctor_kwargs["translate"] = translate 418 layer = add_fn(data, name=name, **ctor_kwargs) 419 return layer 420 421 def add_or_update_labels(self, name, data, *, scale=None, translate=None, **layer_kwargs): 422 """Add a Labels layer, or refresh it in place if one with this name already exists. 423 424 See :meth:`_add_or_update_layer` for the refresh/transform semantics. Extra keyword arguments 425 (``opacity``, ``blending``, ``colormap``, …) are forwarded to the layer. Returns the layer. 426 """ 427 return self._add_or_update_layer( 428 self.viewer.add_labels, name, data, scale, translate, layer_kwargs 429 ) 430 431 def add_or_update_surface(self, name, vertices, faces, *, scale=None, translate=None, 432 values=None, **layer_kwargs): 433 """Add a Surface layer, or refresh it in place if one with this name already exists. 434 435 Surface layers colour by per-vertex ``values``; a constant array (the default) gives a 436 flat-coloured surface. See :meth:`_add_or_update_layer` for the refresh/transform semantics. 437 Returns the layer. 438 """ 439 if values is None: 440 values = np.ones(len(vertices), dtype="float32") 441 return self._add_or_update_layer( 442 self.viewer.add_surface, name, (vertices, faces, values), scale, translate, layer_kwargs 443 )
QWidget(parent: Optional[QWidget] = None, flags: Union[Qt.WindowFlags, Qt.WindowType] = Qt.WindowFlags())
def
add_or_update_labels(self, name, data, *, scale=None, translate=None, **layer_kwargs):
421 def add_or_update_labels(self, name, data, *, scale=None, translate=None, **layer_kwargs): 422 """Add a Labels layer, or refresh it in place if one with this name already exists. 423 424 See :meth:`_add_or_update_layer` for the refresh/transform semantics. Extra keyword arguments 425 (``opacity``, ``blending``, ``colormap``, …) are forwarded to the layer. Returns the layer. 426 """ 427 return self._add_or_update_layer( 428 self.viewer.add_labels, name, data, scale, translate, layer_kwargs 429 )
Add a Labels layer, or refresh it in place if one with this name already exists.
See _add_or_update_layer() for the refresh/transform semantics. Extra keyword arguments
(opacity, blending, colormap, …) are forwarded to the layer. Returns the layer.
def
add_or_update_surface( self, name, vertices, faces, *, scale=None, translate=None, values=None, **layer_kwargs):
431 def add_or_update_surface(self, name, vertices, faces, *, scale=None, translate=None, 432 values=None, **layer_kwargs): 433 """Add a Surface layer, or refresh it in place if one with this name already exists. 434 435 Surface layers colour by per-vertex ``values``; a constant array (the default) gives a 436 flat-coloured surface. See :meth:`_add_or_update_layer` for the refresh/transform semantics. 437 Returns the layer. 438 """ 439 if values is None: 440 values = np.ones(len(vertices), dtype="float32") 441 return self._add_or_update_layer( 442 self.viewer.add_surface, name, (vertices, faces, values), scale, translate, layer_kwargs 443 )
Add a Surface layer, or refresh it in place if one with this name already exists.
Surface layers colour by per-vertex values; a constant array (the default) gives a
flat-coloured surface. See _add_or_update_layer() for the refresh/transform semantics.
Returns the layer.