datalab-platform 1.0.2__py3-none-any.whl → 1.0.4__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 (42) hide show
  1. datalab/__init__.py +1 -1
  2. datalab/adapters_metadata/common.py +2 -2
  3. datalab/config.py +86 -26
  4. datalab/control/baseproxy.py +70 -0
  5. datalab/control/proxy.py +33 -0
  6. datalab/control/remote.py +35 -0
  7. datalab/data/doc/DataLab_en.pdf +0 -0
  8. datalab/data/doc/DataLab_fr.pdf +0 -0
  9. datalab/data/icons/create/linear_chirp.svg +1 -1
  10. datalab/data/icons/create/logistic.svg +1 -1
  11. datalab/gui/actionhandler.py +13 -0
  12. datalab/gui/docks.py +3 -2
  13. datalab/gui/h5io.py +25 -0
  14. datalab/gui/macroeditor.py +19 -5
  15. datalab/gui/main.py +60 -5
  16. datalab/gui/objectview.py +18 -3
  17. datalab/gui/panel/base.py +24 -18
  18. datalab/gui/panel/macro.py +26 -0
  19. datalab/gui/plothandler.py +10 -1
  20. datalab/gui/processor/base.py +43 -10
  21. datalab/gui/processor/image.py +6 -2
  22. datalab/locale/fr/LC_MESSAGES/datalab.mo +0 -0
  23. datalab/locale/fr/LC_MESSAGES/datalab.po +3296 -0
  24. datalab/objectmodel.py +1 -1
  25. datalab/tests/features/common/auto_analysis_recompute_unit_test.py +81 -0
  26. datalab/tests/features/common/coordutils_unit_test.py +1 -1
  27. datalab/tests/features/common/result_deletion_unit_test.py +121 -1
  28. datalab/tests/features/common/update_tree_robustness_test.py +65 -0
  29. datalab/tests/features/control/remoteclient_unit.py +10 -0
  30. datalab/tests/features/hdf5/h5workspace_unit_test.py +133 -0
  31. datalab/tests/features/image/roigrid_unit_test.py +75 -0
  32. datalab/tests/features/macro/macroeditor_unit_test.py +2 -2
  33. datalab/widgets/imagebackground.py +13 -4
  34. datalab/widgets/instconfviewer.py +2 -2
  35. datalab/widgets/signalcursor.py +7 -2
  36. datalab/widgets/signaldeltax.py +4 -1
  37. {datalab_platform-1.0.2.dist-info → datalab_platform-1.0.4.dist-info}/METADATA +7 -7
  38. {datalab_platform-1.0.2.dist-info → datalab_platform-1.0.4.dist-info}/RECORD +42 -38
  39. {datalab_platform-1.0.2.dist-info → datalab_platform-1.0.4.dist-info}/WHEEL +1 -1
  40. {datalab_platform-1.0.2.dist-info → datalab_platform-1.0.4.dist-info}/entry_points.txt +0 -0
  41. {datalab_platform-1.0.2.dist-info → datalab_platform-1.0.4.dist-info}/licenses/LICENSE +0 -0
  42. {datalab_platform-1.0.2.dist-info → datalab_platform-1.0.4.dist-info}/top_level.txt +0 -0
datalab/gui/main.py CHANGED
@@ -1360,6 +1360,10 @@ class DLMainWindow(QW.QMainWindow, AbstractDLControl, metaclass=DLMainWindowMeta
1360
1360
  title += f" [{datalab.__version__}]"
1361
1361
  self.setWindowTitle(title)
1362
1362
 
1363
+ def is_modified(self) -> bool:
1364
+ """Return True if mainwindow is modified"""
1365
+ return self.__is_modified
1366
+
1363
1367
  def __add_dockwidget(self, child, title: str) -> QW.QDockWidget:
1364
1368
  """Add QDockWidget and toggleViewAction"""
1365
1369
  dockwidget, location = child.create_dockwidget(title)
@@ -1559,9 +1563,7 @@ class DLMainWindow(QW.QMainWindow, AbstractDLControl, metaclass=DLMainWindowMeta
1559
1563
  if not filename:
1560
1564
  return
1561
1565
  with qth.qt_try_loadsave_file(self, filename, "save"):
1562
- filename = self.__check_h5file(filename, "save")
1563
- self.h5inputoutput.save_file(filename)
1564
- self.set_modified(False)
1566
+ self.save_h5_workspace(filename)
1565
1567
 
1566
1568
  @remote_controlled
1567
1569
  def open_h5_files(
@@ -1667,6 +1669,59 @@ class DLMainWindow(QW.QMainWindow, AbstractDLControl, metaclass=DLMainWindowMeta
1667
1669
  self.__check_h5file(filename, "load")
1668
1670
  self.h5inputoutput.import_files(filenames, False, reset_all)
1669
1671
 
1672
+ @remote_controlled
1673
+ def load_h5_workspace(self, h5files: list[str], reset_all: bool = False) -> None:
1674
+ """Load native DataLab HDF5 workspace files without any GUI elements.
1675
+
1676
+ This method can be safely called from the internal console as it does not
1677
+ create any Qt widgets, dialogs, or progress bars. It is designed for
1678
+ programmatic use when loading DataLab workspace files.
1679
+
1680
+ .. warning::
1681
+
1682
+ This method only supports native DataLab HDF5 files. For importing
1683
+ arbitrary HDF5 files (non-native), use the GUI menu or macros with
1684
+ :class:`datalab.control.proxy.RemoteProxy`.
1685
+
1686
+ Args:
1687
+ h5files: List of native DataLab HDF5 filenames
1688
+ reset_all: Reset all application data before importing. Defaults to False.
1689
+
1690
+ Raises:
1691
+ ValueError: If a file is not a valid native DataLab HDF5 file
1692
+ """
1693
+ for idx, filename in enumerate(h5files):
1694
+ filename = self.__check_h5file(filename, "load")
1695
+ success = self.h5inputoutput.open_file_headless(
1696
+ filename, reset_all=(reset_all and idx == 0)
1697
+ )
1698
+ if not success:
1699
+ raise ValueError(
1700
+ f"File '{filename}' is not a native DataLab HDF5 file. "
1701
+ f"Use the GUI menu or a macro with RemoteProxy to import "
1702
+ f"arbitrary HDF5 files."
1703
+ )
1704
+ # Refresh panel trees after loading
1705
+ self.repopulate_panel_trees()
1706
+
1707
+ @remote_controlled
1708
+ def save_h5_workspace(self, filename: str) -> None:
1709
+ """Save current workspace to a native DataLab HDF5 file without GUI elements.
1710
+
1711
+ This method can be safely called from the internal console as it does not
1712
+ create any Qt widgets, dialogs, or progress bars. It is designed for
1713
+ programmatic use when saving DataLab workspace files.
1714
+
1715
+ Args:
1716
+ filename: HDF5 filename to save to
1717
+
1718
+ Raises:
1719
+ IOError: If file cannot be saved
1720
+ """
1721
+ filename = self.__check_h5file(filename, "save")
1722
+ self.h5inputoutput.save_file(filename)
1723
+ self.set_modified(False)
1724
+
1670
1725
  @remote_controlled
1671
1726
  def import_h5_file(self, filename: str, reset_all: bool | None = None) -> None:
1672
1727
  """Import HDF5 file into DataLab
@@ -2027,7 +2082,7 @@ class DLMainWindow(QW.QMainWindow, AbstractDLControl, metaclass=DLMainWindowMeta
2027
2082
  Returns:
2028
2083
  True if closed properly, False otherwise
2029
2084
  """
2030
- if not env.execenv.unattended and self.__is_modified:
2085
+ if not env.execenv.unattended and self.is_modified():
2031
2086
  answer = QW.QMessageBox.warning(
2032
2087
  self,
2033
2088
  _("Quit"),
@@ -2039,7 +2094,7 @@ class DLMainWindow(QW.QMainWindow, AbstractDLControl, metaclass=DLMainWindowMeta
2039
2094
  )
2040
2095
  if answer == QW.QMessageBox.Yes:
2041
2096
  self.save_to_h5_file()
2042
- if self.__is_modified:
2097
+ if self.is_modified():
2043
2098
  return False
2044
2099
  elif answer == QW.QMessageBox.Cancel:
2045
2100
  return False
datalab/gui/objectview.py CHANGED
@@ -222,11 +222,26 @@ class SimpleObjectTree(QW.QTreeWidget):
222
222
  self.set_current_item_id(uuid)
223
223
 
224
224
  def update_tree(self) -> None:
225
- """Update tree"""
225
+ """Update tree
226
+
227
+ Note: If an item is not found in the tree, the tree is repopulated to ensure
228
+ consistency between the tree and the model. This can happen in rare cases when
229
+ objects are added to the model but the tree was not properly updated.
230
+ """
226
231
  for group in self.objmodel.get_groups():
227
- self.__update_item(self.get_item_from_id(get_uuid(group)), group)
232
+ group_item = self.get_item_from_id(get_uuid(group))
233
+ if group_item is None:
234
+ # Group item not found, repopulate tree to fix inconsistency
235
+ self.populate_tree()
236
+ return
237
+ self.__update_item(group_item, group)
228
238
  for obj in group:
229
- self.__update_item(self.get_item_from_id(get_uuid(obj)), obj)
239
+ obj_item = self.get_item_from_id(get_uuid(obj))
240
+ if obj_item is None:
241
+ # Object item not found, repopulate tree to fix inconsistency
242
+ self.populate_tree()
243
+ return
244
+ self.__update_item(obj_item, obj)
230
245
 
231
246
  def __add_to_group_item(
232
247
  self, obj: SignalObj | ImageObj, group_item: QW.QTreeWidgetItem
datalab/gui/panel/base.py CHANGED
@@ -79,6 +79,7 @@ from datalab.gui.newobject import (
79
79
  from datalab.gui.processor.base import (
80
80
  PROCESSING_PARAMETERS_OPTION,
81
81
  ProcessingParameters,
82
+ clear_analysis_parameters,
82
83
  extract_processing_parameters,
83
84
  insert_processing_parameters,
84
85
  )
@@ -217,11 +218,11 @@ class ObjectProp(QW.QWidget):
217
218
  self.analysis_parameters.setFont(font)
218
219
 
219
220
  # Track newly created objects to show Creation tab only once
220
- self._newly_created_obj_uuid: str | None = None
221
+ self.newly_created_obj_uuid: str | None = None
221
222
  # Track when analysis results were just computed
222
- self._fresh_analysis_obj_uuid: str | None = None
223
+ self.fresh_analysis_obj_uuid: str | None = None
223
224
  # Track when object was just processed (1-to-1)
224
- self._fresh_processing_obj_uuid: str | None = None
225
+ self.fresh_processing_obj_uuid: str | None = None
225
226
 
226
227
  self.tabwidget.addTab(
227
228
  self.processing_history, get_icon("history.svg"), _("History")
@@ -455,7 +456,7 @@ class ObjectProp(QW.QWidget):
455
456
  Args:
456
457
  obj: Object to mark
457
458
  """
458
- self._newly_created_obj_uuid = get_uuid(obj)
459
+ self.newly_created_obj_uuid = get_uuid(obj)
459
460
 
460
461
  def mark_as_freshly_processed(self, obj: SignalObj | ImageObj) -> None:
461
462
  """Mark object to show Processing tab on next selection.
@@ -463,7 +464,7 @@ class ObjectProp(QW.QWidget):
463
464
  Args:
464
465
  obj: Object to mark
465
466
  """
466
- self._fresh_processing_obj_uuid = get_uuid(obj)
467
+ self.fresh_processing_obj_uuid = get_uuid(obj)
467
468
 
468
469
  def mark_as_fresh_analysis(self, obj: SignalObj | ImageObj) -> None:
469
470
  """Mark object to show Analysis tab on next selection.
@@ -471,7 +472,7 @@ class ObjectProp(QW.QWidget):
471
472
  Args:
472
473
  obj: Object to mark
473
474
  """
474
- self._fresh_analysis_obj_uuid = get_uuid(obj)
475
+ self.fresh_analysis_obj_uuid = get_uuid(obj)
475
476
 
476
477
  def get_changed_properties(self) -> dict[str, Any]:
477
478
  """Get dictionary of properties that have changed from original values.
@@ -1532,9 +1533,9 @@ class BaseDataPanel(AbstractPanel, Generic[TypeObj, TypeROI, TypeROIEditor]):
1532
1533
  # BUT: Don't overwrite if this object is already marked as freshly processed
1533
1534
  # or has fresh analysis results (those take precedence)
1534
1535
  obj_uuid = get_uuid(obj)
1535
- if (
1536
- obj_uuid != self.objprop._fresh_processing_obj_uuid
1537
- and obj_uuid != self.objprop._fresh_analysis_obj_uuid
1536
+ if obj_uuid not in (
1537
+ self.objprop.fresh_processing_obj_uuid,
1538
+ self.objprop.fresh_analysis_obj_uuid,
1538
1539
  ):
1539
1540
  self.objprop.mark_as_newly_created(obj)
1540
1541
 
@@ -2474,17 +2475,17 @@ class BaseDataPanel(AbstractPanel, Generic[TypeObj, TypeROI, TypeROIEditor]):
2474
2475
  if current_obj is not None:
2475
2476
  obj_uuid = get_uuid(current_obj)
2476
2477
  # Show Creation tab for newly created objects (only once)
2477
- if obj_uuid == self.objprop._newly_created_obj_uuid:
2478
+ if obj_uuid == self.objprop.newly_created_obj_uuid:
2478
2479
  force_tab = "creation"
2479
- self.objprop._newly_created_obj_uuid = None
2480
+ self.objprop.newly_created_obj_uuid = None
2480
2481
  # Show Processing tab for freshly processed objects (only once)
2481
- elif obj_uuid == self.objprop._fresh_processing_obj_uuid:
2482
+ elif obj_uuid == self.objprop.fresh_processing_obj_uuid:
2482
2483
  force_tab = "processing"
2483
- self.objprop._fresh_processing_obj_uuid = None
2484
+ self.objprop.fresh_processing_obj_uuid = None
2484
2485
  # Show Analysis tab for objects with fresh analysis results
2485
- elif obj_uuid == self.objprop._fresh_analysis_obj_uuid:
2486
+ elif obj_uuid == self.objprop.fresh_analysis_obj_uuid:
2486
2487
  force_tab = "analysis"
2487
- self.objprop._fresh_analysis_obj_uuid = None
2488
+ self.objprop.fresh_analysis_obj_uuid = None
2488
2489
 
2489
2490
  self.objprop.update_properties_from(current_obj, force_tab=force_tab)
2490
2491
  self.acthandler.selected_objects_changed(selected_groups, selected_objects)
@@ -2921,9 +2922,11 @@ class BaseDataPanel(AbstractPanel, Generic[TypeObj, TypeROI, TypeROIEditor]):
2921
2922
  dialog was accepted or not.
2922
2923
  """
2923
2924
  obj = self.objview.get_sel_objects(include_groups=True)[-1]
2924
- item = create_adapter_from_object(obj).make_item(
2925
- update_from=self.plothandler[get_uuid(obj)]
2926
- )
2925
+ # Use get() instead of [] to avoid KeyError if the plot item doesn't exist
2926
+ # (can happen when "auto refresh" is disabled or in "show first only" mode
2927
+ # where not all objects have plot items created yet)
2928
+ existing_item = self.plothandler.get(get_uuid(obj))
2929
+ item = create_adapter_from_object(obj).make_item(update_from=existing_item)
2927
2930
  roi_editor_class = self.get_roieditor_class() # pylint: disable=not-callable
2928
2931
  roi_editor = roi_editor_class(
2929
2932
  parent=self.parentWidget(),
@@ -3274,6 +3277,9 @@ class BaseDataPanel(AbstractPanel, Generic[TypeObj, TypeROI, TypeROIEditor]):
3274
3277
  # Remove all table and geometry results using adapter methods
3275
3278
  TableAdapter.remove_all_from(obj)
3276
3279
  GeometryAdapter.remove_all_from(obj)
3280
+ # Clear analysis parameters to prevent auto-recompute from
3281
+ # attempting to recompute deleted analyses when ROI changes
3282
+ clear_analysis_parameters(obj)
3277
3283
  if obj is self.objview.get_current_object():
3278
3284
  self.objprop.update_properties_from(obj)
3279
3285
  # Update action states to reflect the removal
@@ -204,6 +204,10 @@ class MacroPanel(AbstractPanel, DockableWidgetMixin):
204
204
  self.addWidget(widget)
205
205
  self.setStretchFactor(0, 2)
206
206
  self.setStretchFactor(1, 1)
207
+ # Set initial sizes: give more space to editor (70%) than console (30%)
208
+ # This ensures proper layout on first open
209
+ total_height = 600 # Default reasonable height
210
+ self.setSizes([int(total_height * 0.7), int(total_height * 0.3)])
207
211
 
208
212
  self.run_action = None
209
213
  self.stop_action = None
@@ -252,6 +256,8 @@ class MacroPanel(AbstractPanel, DockableWidgetMixin):
252
256
  # in a group but directly in the root of the HDF5 file
253
257
  obj = self.deserialize_object_from_hdf5(reader, name, reset_all)
254
258
  self.add_object(obj)
259
+ # Update untitled number counter to prevent duplicate names
260
+ self.update_untitled_counter()
255
261
 
256
262
  def __len__(self) -> int:
257
263
  """Return number of objects"""
@@ -298,6 +304,8 @@ class MacroPanel(AbstractPanel, DockableWidgetMixin):
298
304
  self.tabwidget.clear()
299
305
  self.__macros.clear()
300
306
  super().remove_all_objects()
307
+ # Reset untitled counter when clearing all macros
308
+ Macro.set_untitled_number(0)
301
309
 
302
310
  # ---- Macro panel API -------------------------------------------------------------
303
311
  def setup_actions(self) -> None:
@@ -572,9 +580,27 @@ class MacroPanel(AbstractPanel, DockableWidgetMixin):
572
580
  Conf.main.base_dir.set(filename)
573
581
  macro = self.add_macro()
574
582
  macro.from_file(filename)
583
+ # Update untitled number counter to prevent duplicate names
584
+ self.update_untitled_counter()
575
585
  return self.get_number_from_macro(macro)
576
586
  return -1
577
587
 
588
+ def update_untitled_counter(self) -> None:
589
+ """Update the untitled counter based on existing macro titles
590
+
591
+ This scans all macro titles to find the highest "macro_XX" number
592
+ and updates the global counter to prevent duplicate names.
593
+ """
594
+ max_untitled = 0
595
+ for macro in self.__macros:
596
+ # Match titles like "macro_01", "macro_02", etc.
597
+ match = re.match(r"macro_(\d+)", macro.title)
598
+ if match:
599
+ number = int(match.group(1))
600
+ max_untitled = max(max_untitled, number)
601
+ # Set the counter to the highest found number
602
+ Macro.set_untitled_number(max_untitled)
603
+
578
604
  def remove_macro(self, number_or_title: int | str | None = None) -> None:
579
605
  """Remove macro
580
606
 
@@ -162,6 +162,7 @@ class BasePlotHandler(Generic[TypeObj, TypePlotItem]): # type: ignore
162
162
  self.__merged_result_adapters = {}
163
163
  self.cleanup_dataview()
164
164
  self.remove_all_shape_items()
165
+ self.plot.replot()
165
166
 
166
167
  def add_shapes(self, oid: str, do_autoscale: bool = False) -> None:
167
168
  """Add geometric shape items associated to computed results and annotations,
@@ -391,7 +392,10 @@ class BasePlotHandler(Generic[TypeObj, TypePlotItem]): # type: ignore
391
392
  if what == "selected":
392
393
  # Refresh selected objects
393
394
  oids = self.panel.objview.get_sel_object_uuids(include_groups=True)
394
- if len(oids) == 1:
395
+ if len(oids) <= 1:
396
+ # Cleanup data view when there is 0 or 1 selected object.
397
+ # This removes stray plot items (like XRangeSelection, DataInfoLabel)
398
+ # that were created by PlotPy tools but are not managed by DataLab.
395
399
  self.cleanup_dataview()
396
400
  self.remove_all_shape_items()
397
401
  for item in self:
@@ -806,4 +810,9 @@ class ImagePlotHandler(BasePlotHandler[ImageObj, MaskedXYImageItem]):
806
810
  options = super().get_plot_options()
807
811
  options.zlabel = self.plot.get_axis_title("right")
808
812
  options.zunit = self.plot.get_axis_unit("right")
813
+ # Include aspect ratio configuration so that separate plot dialogs
814
+ # (e.g. "View in a new window", ROI editors, profile dialogs) use the same
815
+ # settings as the integrated plot handler:
816
+ options.aspect_ratio = self.plot.get_aspect_ratio()
817
+ options.lock_aspect_ratio = self.plot.lock_aspect_ratio
809
818
  return options
@@ -195,6 +195,21 @@ def insert_processing_parameters(
195
195
  obj.set_metadata_option(PROCESSING_PARAMETERS_OPTION, pp.to_dict())
196
196
 
197
197
 
198
+ def clear_analysis_parameters(obj: SignalObj | ImageObj) -> None:
199
+ """Clear analysis parameters from object metadata.
200
+
201
+ This removes the stored analysis parameters (1-to-0 operations) from the object.
202
+ Should be called when all analysis results are deleted to prevent the
203
+ auto_recompute_analysis function from attempting to recompute deleted analyses.
204
+
205
+ Args:
206
+ obj: Signal or Image object
207
+ """
208
+ key = f"__{ANALYSIS_PARAMETERS_OPTION}"
209
+ if key in obj.metadata:
210
+ del obj.metadata[key]
211
+
212
+
198
213
  def run_with_env(func: Callable, args: tuple, env_json: str) -> CompOut:
199
214
  """Wrapper to apply environment config before calling func
200
215
 
@@ -945,7 +960,9 @@ class BaseProcessor(QC.QObject, Generic[TypeROI, TypeROIParam]):
945
960
  TableAdapter.remove_all_from(result_obj)
946
961
  GeometryAdapter.remove_all_from(result_obj)
947
962
 
948
- def auto_recompute_analysis(self, obj: SignalObj | ImageObj) -> None:
963
+ def auto_recompute_analysis(
964
+ self, obj: SignalObj | ImageObj, refresh_plot: bool = True
965
+ ) -> None:
949
966
  """Automatically recompute analysis (1-to-0) operations after data changes.
950
967
 
951
968
  This method checks if the object has 1-to-0 analysis parameters (analysis
@@ -962,6 +979,7 @@ class BaseProcessor(QC.QObject, Generic[TypeROI, TypeROIParam]):
962
979
 
963
980
  Args:
964
981
  obj: The object whose data was modified
982
+ refresh_plot: Whether to refresh the plot after recomputation
965
983
  """
966
984
  # Check if object has 1-to-0 analysis parameters (analysis operations)
967
985
  proc_params = extract_analysis_parameters(obj)
@@ -974,14 +992,16 @@ class BaseProcessor(QC.QObject, Generic[TypeROI, TypeROIParam]):
974
992
  # Get the actual function from the function name
975
993
  feature = self.get_feature(proc_params.func_name)
976
994
 
977
- # Recompute the analysis operation silently
995
+ # Recompute the analysis operation silently, only for this specific object
996
+ # (not all selected objects, to avoid O(n²) behavior when called in a loop)
978
997
  with Conf.proc.show_result_dialog.temp(False):
979
- self.compute_1_to_0(feature.function, param, edit=False)
998
+ self.compute_1_to_0(feature.function, param, edit=False, target_objs=[obj])
980
999
 
981
1000
  # Update the view
982
1001
  obj_uuid = get_uuid(obj)
983
1002
  self.panel.objview.update_item(obj_uuid)
984
- self.panel.refresh_plot(obj_uuid, update_items=True, force=True)
1003
+ if refresh_plot:
1004
+ self.panel.refresh_plot(obj_uuid, update_items=True, force=True)
985
1005
 
986
1006
  def __exec_func(
987
1007
  self,
@@ -1332,12 +1352,14 @@ class BaseProcessor(QC.QObject, Generic[TypeROI, TypeROIParam]):
1332
1352
  title: str | None = None,
1333
1353
  comment: str | None = None,
1334
1354
  edit: bool | None = None,
1355
+ target_objs: list[SignalObj | ImageObj] | None = None,
1335
1356
  ) -> ResultData:
1336
1357
  """Generic processing method: 1 object in → no object out.
1337
1358
 
1338
- Applies a function to each selected object, returning metadata or measurement
1339
- results (e.g. peak coordinates, statistical properties) without generating
1340
- new objects. Results are stored in the object's metadata and returned as a
1359
+ Applies a function to each selected object (or specified target objects),
1360
+ returning metadata or measurement results (e.g. peak coordinates, statistical
1361
+ properties) without generating new objects. Results are stored in the object's
1362
+ metadata and returned as a
1341
1363
  ResultData instance.
1342
1364
 
1343
1365
  Args:
@@ -1349,6 +1371,8 @@ class BaseProcessor(QC.QObject, Generic[TypeROI, TypeROIParam]):
1349
1371
  title: Optional progress bar title.
1350
1372
  comment: Optional comment for parameter dialog.
1351
1373
  edit: Whether to open the parameter editor before execution.
1374
+ target_objs: Optional list of specific objects to process. If None,
1375
+ processes all currently selected objects.
1352
1376
 
1353
1377
  Returns:
1354
1378
  ResultData instance containing the results for all processed objects.
@@ -1365,7 +1389,11 @@ class BaseProcessor(QC.QObject, Generic[TypeROI, TypeROIParam]):
1365
1389
  if param is not None:
1366
1390
  if edit and not param.edit(parent=self.mainwindow):
1367
1391
  return None
1368
- objs = self.panel.objview.get_sel_objects(include_groups=True)
1392
+ objs = (
1393
+ target_objs
1394
+ if target_objs is not None
1395
+ else self.panel.objview.get_sel_objects(include_groups=True)
1396
+ )
1369
1397
  current_obj = self.panel.objview.get_current_object()
1370
1398
  title = func.__name__ if title is None else title
1371
1399
  refresh_needed = False
@@ -2381,8 +2409,13 @@ class BaseProcessor(QC.QObject, Generic[TypeROI, TypeROIParam]):
2381
2409
  )
2382
2410
  # Auto-recompute analysis operations for objects with modified ROIs
2383
2411
  if mode == "apply":
2384
- for obj_i in objs:
2385
- self.auto_recompute_analysis(obj_i)
2412
+ with create_progress_bar(
2413
+ self.panel, _("Recomputing..."), max_=len(objs)
2414
+ ) as progress:
2415
+ for idx, obj_i in enumerate(objs):
2416
+ progress.setValue(idx)
2417
+ self.auto_recompute_analysis(obj_i, refresh_plot=False)
2418
+ self.panel.manual_refresh()
2386
2419
  return edited_roi
2387
2420
 
2388
2421
  def edit_roi_numerically(self) -> TypeROI:
@@ -779,7 +779,8 @@ class ImageProcessor(BaseProcessor[ImageROI, ROI2DParam]):
779
779
  == QW.QMessageBox.No
780
780
  ):
781
781
  return
782
- editor = ImageGridROIEditor(parent=self.parent(), obj=obj0)
782
+ options = self.panel.plothandler.get_plot_options()
783
+ editor = ImageGridROIEditor(parent=self.parent(), obj=obj0, options=options)
783
784
  if exec_dialog(editor):
784
785
  for obj in self.panel.objview.get_sel_objects():
785
786
  obj.roi = editor.get_roi()
@@ -1014,7 +1015,10 @@ class ImageProcessor(BaseProcessor[ImageROI, ROI2DParam]):
1014
1015
  with :py:func:`sigima.proc.image.offset_correction`"""
1015
1016
  obj = self.panel.objview.get_sel_objects(include_groups=True)[0]
1016
1017
  if param is None:
1017
- dlg = imagebackground.ImageBackgroundDialog(obj, parent=self.mainwindow)
1018
+ options = self.panel.plothandler.get_plot_options()
1019
+ dlg = imagebackground.ImageBackgroundDialog(
1020
+ obj, parent=self.mainwindow, options=options
1021
+ )
1018
1022
  if exec_dialog(dlg):
1019
1023
  x0, y0, x1, y1 = dlg.get_rect_coords()
1020
1024
  param = ROI2DParam.create(
Binary file