synapse_net.tools.base_widget

  1import os
  2import sys
  3from pathlib import Path
  4
  5import napari
  6import qtpy.QtWidgets as QtWidgets
  7
  8from napari.utils.notifications import show_info
  9from qtpy.QtWidgets import (
 10    QWidget, QVBoxLayout, QHBoxLayout, QLabel, QSpinBox, QComboBox, QCheckBox
 11)
 12from superqt import QCollapsible
 13
 14try:
 15    from napari_skimage_regionprops import add_table, get_table
 16except ImportError:
 17    add_table, get_table = None, None
 18
 19
 20class _SilencePrint:
 21    def __enter__(self):
 22        self._original_stdout = sys.stdout
 23        sys.stdout = open(os.devnull, "w")
 24
 25    def __exit__(self, exc_type, exc_val, exc_tb):
 26        sys.stdout.close()
 27        sys.stdout = self._original_stdout
 28
 29
 30class BaseWidget(QWidget):
 31    def __init__(self):
 32        super().__init__()
 33        self.viewer = napari.current_viewer()
 34        self.attribute_dict = {}
 35
 36    def _create_layer_selector(self, selector_name, layer_type="Image"):
 37        """Create a layer selector for an image or labels and store it in a dictionary.
 38
 39        Args:
 40            selector_name (str): The name of the selector, used as a key in the dictionary.
 41            layer_type (str): The type of layer to filter for ("Image" or "Labels").
 42        """
 43        if not hasattr(self, "layer_selectors"):
 44            self.layer_selectors = {}
 45
 46        # Determine the annotation type for the widget
 47        if layer_type == "Image":
 48            layer_filter = napari.layers.Image
 49        elif layer_type == "Labels":
 50            layer_filter = napari.layers.Labels
 51        elif layer_type == "Shapes":
 52            layer_filter = napari.layers.Shapes
 53        else:
 54            raise ValueError("layer_type must be either 'Image' or 'Labels'.")
 55
 56        selector_widget = QtWidgets.QWidget()
 57        image_selector = QtWidgets.QComboBox()
 58        layer_label = QtWidgets.QLabel(f"{selector_name}:")
 59
 60        # Populate initial options
 61        self._update_selector(selector=image_selector, layer_filter=layer_filter)
 62
 63        # Update selector on layer events
 64        self.viewer.layers.events.inserted.connect(lambda event: self._update_selector(image_selector, layer_filter))
 65        self.viewer.layers.events.removed.connect(lambda event: self._update_selector(image_selector, layer_filter))
 66
 67        # Store the selector in the dictionary
 68        self.layer_selectors[selector_name] = selector_widget
 69
 70        # Set up layout
 71        layout = QVBoxLayout()
 72        layout.addWidget(layer_label)
 73        layout.addWidget(image_selector)
 74        selector_widget.setLayout(layout)
 75        return selector_widget
 76
 77    def _update_selector(self, selector, layer_filter):
 78        """Update a single selector with the current image layers in the viewer."""
 79        selector.clear()
 80        image_layers = [layer.name for layer in self.viewer.layers if isinstance(layer, layer_filter)]
 81        selector.addItems(image_layers)
 82
 83    def _get_layer_selector_layer(self, selector_name):
 84        """Return the layer currently selected in a given selector."""
 85        if selector_name in self.layer_selectors:
 86            selector_widget = self.layer_selectors[selector_name]
 87
 88            # Retrieve the QComboBox from the QWidget's layout
 89            image_selector = selector_widget.layout().itemAt(1).widget()
 90
 91            if isinstance(image_selector, QComboBox):
 92                selected_layer_name = image_selector.currentText()
 93                if selected_layer_name in self.viewer.layers:
 94                    return self.viewer.layers[selected_layer_name]
 95        return None  # Return None if layer not found
 96
 97    def _get_layer_selector_data(self, selector_name, return_metadata=False):
 98        """Return the data for the layer currently selected in a given selector."""
 99        if selector_name in self.layer_selectors:
100            selector_widget = self.layer_selectors[selector_name]
101
102            # Retrieve the QComboBox from the QWidget's layout
103            image_selector = selector_widget.layout().itemAt(1).widget()
104
105            if isinstance(image_selector, QComboBox):
106                selected_layer_name = image_selector.currentText()
107                if selected_layer_name in self.viewer.layers:
108                    if return_metadata:
109                        return self.viewer.layers[selected_layer_name].metadata
110                    else:
111                        return self.viewer.layers[selected_layer_name].data
112        return None  # Return None if layer not found
113
114    def _add_string_param(self, name, value, title=None, placeholder=None, layout=None, tooltip=None):
115        if layout is None:
116            layout = QtWidgets.QHBoxLayout()
117        label = QtWidgets.QLabel(title or name)
118        if tooltip:
119            label.setToolTip(tooltip)
120        layout.addWidget(label)
121        param = QtWidgets.QLineEdit()
122        param.setText(value)
123        if placeholder is not None:
124            param.setPlaceholderText(placeholder)
125        param.textChanged.connect(lambda val: setattr(self, name, val))
126        if tooltip:
127            param.setToolTip(tooltip)
128        layout.addWidget(param)
129        return param, layout
130
131    def _add_float_param(self, name, value, title=None, min_val=0.0, max_val=1.0, decimals=2,
132                         step=0.01, layout=None, tooltip=None):
133        if layout is None:
134            layout = QtWidgets.QHBoxLayout()
135        label = QtWidgets.QLabel(title or name)
136        if tooltip:
137            label.setToolTip(tooltip)
138        layout.addWidget(label)
139        param = QtWidgets.QDoubleSpinBox()
140        param.setRange(min_val, max_val)
141        param.setDecimals(decimals)
142        param.setValue(value)
143        param.setSingleStep(step)
144        param.valueChanged.connect(lambda val: setattr(self, name, val))
145        if tooltip:
146            param.setToolTip(tooltip)
147        layout.addWidget(param)
148        return param, layout
149
150    def _add_int_param(self, name, value, min_val, max_val, title=None, step=1, layout=None, tooltip=None):
151        if layout is None:
152            layout = QHBoxLayout()
153        label = QLabel(title or name)
154        if tooltip:
155            label.setToolTip(tooltip)
156        layout.addWidget(label)
157        param = QSpinBox()
158        param.setRange(min_val, max_val)
159        param.setValue(value)
160        param.setSingleStep(step)
161        param.valueChanged.connect(lambda val: setattr(self, name, val))
162        if tooltip:
163            param.setToolTip(tooltip)
164        layout.addWidget(param)
165        return param, layout
166
167    def _add_choice_param(self, name, value, options, title=None, layout=None, update=None, tooltip=None):
168        if layout is None:
169            layout = QHBoxLayout()
170        label = QLabel(title or name)
171        if tooltip:
172            label.setToolTip(tooltip)
173        layout.addWidget(label)
174
175        # Create the dropdown menu via QComboBox, set the available values.
176        dropdown = QComboBox()
177        dropdown.addItems(options)
178        if update is None:
179            dropdown.currentIndexChanged.connect(lambda index: setattr(self, name, options[index]))
180        else:
181            dropdown.currentIndexChanged.connect(update)
182
183        # Set the correct value for the value.
184        dropdown.setCurrentIndex(dropdown.findText(value))
185
186        if tooltip:
187            dropdown.setToolTip(tooltip)
188
189        layout.addWidget(dropdown)
190        return dropdown, layout
191
192    def _add_shape_param(self, names, values, min_val, max_val, step=1, title=None, tooltip=None):
193        layout = QHBoxLayout()
194
195        x_layout = QVBoxLayout()
196        x_param, _ = self._add_int_param(
197            names[0], values[0], min_val=min_val, max_val=max_val, layout=x_layout, step=step,
198            title=title[0] if title is not None else title, tooltip=tooltip
199        )
200        layout.addLayout(x_layout)
201
202        y_layout = QVBoxLayout()
203        y_param, _ = self._add_int_param(
204            names[1], values[1], min_val=min_val, max_val=max_val, layout=y_layout, step=step,
205            title=title[1] if title is not None else title, tooltip=tooltip
206        )
207        layout.addLayout(y_layout)
208
209        if len(names) == 3:
210            z_layout = QVBoxLayout()
211            z_param, _ = self._add_int_param(
212                names[2], values[2], min_val=min_val, max_val=max_val, layout=z_layout, step=step,
213                title=title[2] if title is not None else title, tooltip=tooltip
214            )
215            layout.addLayout(z_layout)
216            return x_param, y_param, z_param, layout
217
218        return x_param, y_param, layout
219
220    def _make_collapsible(self, widget, title):
221        parent_widget = QWidget()
222        parent_widget.setLayout(QVBoxLayout())
223        collapsible = QCollapsible(title, parent_widget)
224        collapsible.addWidget(widget)
225        parent_widget.layout().addWidget(collapsible)
226        return parent_widget
227
228    def _add_boolean_param(self, name, value, title=None, tooltip=None):
229        checkbox = QCheckBox(name if title is None else title)
230        checkbox.setChecked(value)
231        checkbox.stateChanged.connect(lambda val: setattr(self, name, val))
232        if tooltip:
233            checkbox.setToolTip(tooltip)
234        return checkbox
235
236    def _add_path_param(self, name, value, select_type, title=None, placeholder=None, tooltip=None):
237        assert select_type in ("directory", "file", "both")
238
239        layout = QtWidgets.QHBoxLayout()
240        label = QtWidgets.QLabel(title or name)
241        if tooltip:
242            label.setToolTip(tooltip)
243        layout.addWidget(label)
244
245        path_textbox = QtWidgets.QLineEdit()
246        path_textbox.setText(str(value))
247        if placeholder is not None:
248            path_textbox.setPlaceholderText(placeholder)
249        path_textbox.textChanged.connect(lambda val: setattr(self, name, val))
250        if tooltip:
251            path_textbox.setToolTip(tooltip)
252
253        layout.addWidget(path_textbox)
254
255        def add_path_button(select_type, tooltip=None):
256            # Adjust button text.
257            button_text = f"Select {select_type.capitalize()}"
258            path_button = QtWidgets.QPushButton(button_text)
259
260            # Call appropriate function based on select_type.
261            path_button.clicked.connect(lambda: getattr(self, f"_get_{select_type}_path")(name, path_textbox))
262            if tooltip:
263                path_button.setToolTip(tooltip)
264            layout.addWidget(path_button)
265
266        if select_type == "both":
267            add_path_button("file")
268            add_path_button("directory")
269
270        else:
271            add_path_button(select_type)
272
273        return path_textbox, layout
274
275    def _get_directory_path(self, name, textbox, tooltip=None):
276        directory = QtWidgets.QFileDialog.getExistingDirectory(
277            self, "Select Directory", "", QtWidgets.QFileDialog.ShowDirsOnly
278        )
279        if tooltip:
280            directory.setToolTip(tooltip)
281        if directory and Path(directory).is_dir():
282            textbox.setText(str(directory))
283        else:
284            # Handle the case where the selected path is not a directory
285            print("Invalid directory selected. Please try again.")
286
287    def _get_file_path(self, name, textbox, tooltip=None):
288        file_path, _ = QtWidgets.QFileDialog.getOpenFileName(
289            self, "Select File", "", "All Files (*)"
290        )
291        if tooltip:
292            file_path.setToolTip(tooltip)
293        if file_path and Path(file_path).is_file():
294            textbox.setText(str(file_path))
295        else:
296            # Handle the case where the selected path is not a file
297            print("Invalid file selected. Please try again.")
298
299    def _handle_resolution(self, metadata, voxel_size_param, ndim, return_as_list=True):
300        # Get the resolution / voxel size from the layer metadata if available.
301        resolution = metadata.get("voxel_size", None)
302
303        # If user input was given then override resolution from metadata.
304        axes = "zyx" if ndim == 3 else "yx"
305        if voxel_size_param.value() != 0.0:  # Changed from default.
306            resolution = {ax: voxel_size_param.value() for ax in axes}
307
308        if resolution is not None and return_as_list:
309            resolution = [resolution[ax] for ax in axes]
310            assert len(resolution) == ndim
311
312        return resolution
313
314    def _save_table(self, save_path, data):
315        ext = os.path.splitext(save_path)[1]
316        if ext == "":  # No file extension given, By default we save to CSV.
317            file_path = f"{save_path}.csv"
318            data.to_csv(file_path, index=False)
319        elif ext == ".csv":  # Extension was specified as csv
320            file_path = save_path
321            data.to_csv(file_path, index=False)
322        elif ext == ".xlsx":  # We also support excel.
323            file_path = save_path
324            data.to_excel(file_path, index=False)
325        else:
326            raise ValueError("Invalid extension for table: {ext}. We support .csv or .xlsx.")
327        return file_path
328
329    def _add_properties_and_table(self, layer, table_data, save_path=""):
330        layer.properties = table_data
331
332        if add_table is not None:
333            with _SilencePrint():
334                add_table(layer, self.viewer)
335
336        # Save table to file if save path is provided.
337        if save_path != "":
338            file_path = self._save_table(self.save_path.text(), table_data)
339            show_info(f"INFO: Added table and saved file to {file_path}.")
class BaseWidget(PyQt5.QtWidgets.QWidget):
 31class BaseWidget(QWidget):
 32    def __init__(self):
 33        super().__init__()
 34        self.viewer = napari.current_viewer()
 35        self.attribute_dict = {}
 36
 37    def _create_layer_selector(self, selector_name, layer_type="Image"):
 38        """Create a layer selector for an image or labels and store it in a dictionary.
 39
 40        Args:
 41            selector_name (str): The name of the selector, used as a key in the dictionary.
 42            layer_type (str): The type of layer to filter for ("Image" or "Labels").
 43        """
 44        if not hasattr(self, "layer_selectors"):
 45            self.layer_selectors = {}
 46
 47        # Determine the annotation type for the widget
 48        if layer_type == "Image":
 49            layer_filter = napari.layers.Image
 50        elif layer_type == "Labels":
 51            layer_filter = napari.layers.Labels
 52        elif layer_type == "Shapes":
 53            layer_filter = napari.layers.Shapes
 54        else:
 55            raise ValueError("layer_type must be either 'Image' or 'Labels'.")
 56
 57        selector_widget = QtWidgets.QWidget()
 58        image_selector = QtWidgets.QComboBox()
 59        layer_label = QtWidgets.QLabel(f"{selector_name}:")
 60
 61        # Populate initial options
 62        self._update_selector(selector=image_selector, layer_filter=layer_filter)
 63
 64        # Update selector on layer events
 65        self.viewer.layers.events.inserted.connect(lambda event: self._update_selector(image_selector, layer_filter))
 66        self.viewer.layers.events.removed.connect(lambda event: self._update_selector(image_selector, layer_filter))
 67
 68        # Store the selector in the dictionary
 69        self.layer_selectors[selector_name] = selector_widget
 70
 71        # Set up layout
 72        layout = QVBoxLayout()
 73        layout.addWidget(layer_label)
 74        layout.addWidget(image_selector)
 75        selector_widget.setLayout(layout)
 76        return selector_widget
 77
 78    def _update_selector(self, selector, layer_filter):
 79        """Update a single selector with the current image layers in the viewer."""
 80        selector.clear()
 81        image_layers = [layer.name for layer in self.viewer.layers if isinstance(layer, layer_filter)]
 82        selector.addItems(image_layers)
 83
 84    def _get_layer_selector_layer(self, selector_name):
 85        """Return the layer currently selected in a given selector."""
 86        if selector_name in self.layer_selectors:
 87            selector_widget = self.layer_selectors[selector_name]
 88
 89            # Retrieve the QComboBox from the QWidget's layout
 90            image_selector = selector_widget.layout().itemAt(1).widget()
 91
 92            if isinstance(image_selector, QComboBox):
 93                selected_layer_name = image_selector.currentText()
 94                if selected_layer_name in self.viewer.layers:
 95                    return self.viewer.layers[selected_layer_name]
 96        return None  # Return None if layer not found
 97
 98    def _get_layer_selector_data(self, selector_name, return_metadata=False):
 99        """Return the data for the layer currently selected in a given selector."""
100        if selector_name in self.layer_selectors:
101            selector_widget = self.layer_selectors[selector_name]
102
103            # Retrieve the QComboBox from the QWidget's layout
104            image_selector = selector_widget.layout().itemAt(1).widget()
105
106            if isinstance(image_selector, QComboBox):
107                selected_layer_name = image_selector.currentText()
108                if selected_layer_name in self.viewer.layers:
109                    if return_metadata:
110                        return self.viewer.layers[selected_layer_name].metadata
111                    else:
112                        return self.viewer.layers[selected_layer_name].data
113        return None  # Return None if layer not found
114
115    def _add_string_param(self, name, value, title=None, placeholder=None, layout=None, tooltip=None):
116        if layout is None:
117            layout = QtWidgets.QHBoxLayout()
118        label = QtWidgets.QLabel(title or name)
119        if tooltip:
120            label.setToolTip(tooltip)
121        layout.addWidget(label)
122        param = QtWidgets.QLineEdit()
123        param.setText(value)
124        if placeholder is not None:
125            param.setPlaceholderText(placeholder)
126        param.textChanged.connect(lambda val: setattr(self, name, val))
127        if tooltip:
128            param.setToolTip(tooltip)
129        layout.addWidget(param)
130        return param, layout
131
132    def _add_float_param(self, name, value, title=None, min_val=0.0, max_val=1.0, decimals=2,
133                         step=0.01, layout=None, tooltip=None):
134        if layout is None:
135            layout = QtWidgets.QHBoxLayout()
136        label = QtWidgets.QLabel(title or name)
137        if tooltip:
138            label.setToolTip(tooltip)
139        layout.addWidget(label)
140        param = QtWidgets.QDoubleSpinBox()
141        param.setRange(min_val, max_val)
142        param.setDecimals(decimals)
143        param.setValue(value)
144        param.setSingleStep(step)
145        param.valueChanged.connect(lambda val: setattr(self, name, val))
146        if tooltip:
147            param.setToolTip(tooltip)
148        layout.addWidget(param)
149        return param, layout
150
151    def _add_int_param(self, name, value, min_val, max_val, title=None, step=1, layout=None, tooltip=None):
152        if layout is None:
153            layout = QHBoxLayout()
154        label = QLabel(title or name)
155        if tooltip:
156            label.setToolTip(tooltip)
157        layout.addWidget(label)
158        param = QSpinBox()
159        param.setRange(min_val, max_val)
160        param.setValue(value)
161        param.setSingleStep(step)
162        param.valueChanged.connect(lambda val: setattr(self, name, val))
163        if tooltip:
164            param.setToolTip(tooltip)
165        layout.addWidget(param)
166        return param, layout
167
168    def _add_choice_param(self, name, value, options, title=None, layout=None, update=None, tooltip=None):
169        if layout is None:
170            layout = QHBoxLayout()
171        label = QLabel(title or name)
172        if tooltip:
173            label.setToolTip(tooltip)
174        layout.addWidget(label)
175
176        # Create the dropdown menu via QComboBox, set the available values.
177        dropdown = QComboBox()
178        dropdown.addItems(options)
179        if update is None:
180            dropdown.currentIndexChanged.connect(lambda index: setattr(self, name, options[index]))
181        else:
182            dropdown.currentIndexChanged.connect(update)
183
184        # Set the correct value for the value.
185        dropdown.setCurrentIndex(dropdown.findText(value))
186
187        if tooltip:
188            dropdown.setToolTip(tooltip)
189
190        layout.addWidget(dropdown)
191        return dropdown, layout
192
193    def _add_shape_param(self, names, values, min_val, max_val, step=1, title=None, tooltip=None):
194        layout = QHBoxLayout()
195
196        x_layout = QVBoxLayout()
197        x_param, _ = self._add_int_param(
198            names[0], values[0], min_val=min_val, max_val=max_val, layout=x_layout, step=step,
199            title=title[0] if title is not None else title, tooltip=tooltip
200        )
201        layout.addLayout(x_layout)
202
203        y_layout = QVBoxLayout()
204        y_param, _ = self._add_int_param(
205            names[1], values[1], min_val=min_val, max_val=max_val, layout=y_layout, step=step,
206            title=title[1] if title is not None else title, tooltip=tooltip
207        )
208        layout.addLayout(y_layout)
209
210        if len(names) == 3:
211            z_layout = QVBoxLayout()
212            z_param, _ = self._add_int_param(
213                names[2], values[2], min_val=min_val, max_val=max_val, layout=z_layout, step=step,
214                title=title[2] if title is not None else title, tooltip=tooltip
215            )
216            layout.addLayout(z_layout)
217            return x_param, y_param, z_param, layout
218
219        return x_param, y_param, layout
220
221    def _make_collapsible(self, widget, title):
222        parent_widget = QWidget()
223        parent_widget.setLayout(QVBoxLayout())
224        collapsible = QCollapsible(title, parent_widget)
225        collapsible.addWidget(widget)
226        parent_widget.layout().addWidget(collapsible)
227        return parent_widget
228
229    def _add_boolean_param(self, name, value, title=None, tooltip=None):
230        checkbox = QCheckBox(name if title is None else title)
231        checkbox.setChecked(value)
232        checkbox.stateChanged.connect(lambda val: setattr(self, name, val))
233        if tooltip:
234            checkbox.setToolTip(tooltip)
235        return checkbox
236
237    def _add_path_param(self, name, value, select_type, title=None, placeholder=None, tooltip=None):
238        assert select_type in ("directory", "file", "both")
239
240        layout = QtWidgets.QHBoxLayout()
241        label = QtWidgets.QLabel(title or name)
242        if tooltip:
243            label.setToolTip(tooltip)
244        layout.addWidget(label)
245
246        path_textbox = QtWidgets.QLineEdit()
247        path_textbox.setText(str(value))
248        if placeholder is not None:
249            path_textbox.setPlaceholderText(placeholder)
250        path_textbox.textChanged.connect(lambda val: setattr(self, name, val))
251        if tooltip:
252            path_textbox.setToolTip(tooltip)
253
254        layout.addWidget(path_textbox)
255
256        def add_path_button(select_type, tooltip=None):
257            # Adjust button text.
258            button_text = f"Select {select_type.capitalize()}"
259            path_button = QtWidgets.QPushButton(button_text)
260
261            # Call appropriate function based on select_type.
262            path_button.clicked.connect(lambda: getattr(self, f"_get_{select_type}_path")(name, path_textbox))
263            if tooltip:
264                path_button.setToolTip(tooltip)
265            layout.addWidget(path_button)
266
267        if select_type == "both":
268            add_path_button("file")
269            add_path_button("directory")
270
271        else:
272            add_path_button(select_type)
273
274        return path_textbox, layout
275
276    def _get_directory_path(self, name, textbox, tooltip=None):
277        directory = QtWidgets.QFileDialog.getExistingDirectory(
278            self, "Select Directory", "", QtWidgets.QFileDialog.ShowDirsOnly
279        )
280        if tooltip:
281            directory.setToolTip(tooltip)
282        if directory and Path(directory).is_dir():
283            textbox.setText(str(directory))
284        else:
285            # Handle the case where the selected path is not a directory
286            print("Invalid directory selected. Please try again.")
287
288    def _get_file_path(self, name, textbox, tooltip=None):
289        file_path, _ = QtWidgets.QFileDialog.getOpenFileName(
290            self, "Select File", "", "All Files (*)"
291        )
292        if tooltip:
293            file_path.setToolTip(tooltip)
294        if file_path and Path(file_path).is_file():
295            textbox.setText(str(file_path))
296        else:
297            # Handle the case where the selected path is not a file
298            print("Invalid file selected. Please try again.")
299
300    def _handle_resolution(self, metadata, voxel_size_param, ndim, return_as_list=True):
301        # Get the resolution / voxel size from the layer metadata if available.
302        resolution = metadata.get("voxel_size", None)
303
304        # If user input was given then override resolution from metadata.
305        axes = "zyx" if ndim == 3 else "yx"
306        if voxel_size_param.value() != 0.0:  # Changed from default.
307            resolution = {ax: voxel_size_param.value() for ax in axes}
308
309        if resolution is not None and return_as_list:
310            resolution = [resolution[ax] for ax in axes]
311            assert len(resolution) == ndim
312
313        return resolution
314
315    def _save_table(self, save_path, data):
316        ext = os.path.splitext(save_path)[1]
317        if ext == "":  # No file extension given, By default we save to CSV.
318            file_path = f"{save_path}.csv"
319            data.to_csv(file_path, index=False)
320        elif ext == ".csv":  # Extension was specified as csv
321            file_path = save_path
322            data.to_csv(file_path, index=False)
323        elif ext == ".xlsx":  # We also support excel.
324            file_path = save_path
325            data.to_excel(file_path, index=False)
326        else:
327            raise ValueError("Invalid extension for table: {ext}. We support .csv or .xlsx.")
328        return file_path
329
330    def _add_properties_and_table(self, layer, table_data, save_path=""):
331        layer.properties = table_data
332
333        if add_table is not None:
334            with _SilencePrint():
335                add_table(layer, self.viewer)
336
337        # Save table to file if save path is provided.
338        if save_path != "":
339            file_path = self._save_table(self.save_path.text(), table_data)
340            show_info(f"INFO: Added table and saved file to {file_path}.")

QWidget(parent: typing.Optional[QWidget] = None, flags: Union[Qt.WindowFlags, Qt.WindowType] = Qt.WindowFlags())

viewer
attribute_dict