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.
- datalab/__init__.py +1 -1
- datalab/adapters_metadata/common.py +2 -2
- datalab/config.py +86 -26
- datalab/control/baseproxy.py +70 -0
- datalab/control/proxy.py +33 -0
- datalab/control/remote.py +35 -0
- datalab/data/doc/DataLab_en.pdf +0 -0
- datalab/data/doc/DataLab_fr.pdf +0 -0
- datalab/data/icons/create/linear_chirp.svg +1 -1
- datalab/data/icons/create/logistic.svg +1 -1
- datalab/gui/actionhandler.py +13 -0
- datalab/gui/docks.py +3 -2
- datalab/gui/h5io.py +25 -0
- datalab/gui/macroeditor.py +19 -5
- datalab/gui/main.py +60 -5
- datalab/gui/objectview.py +18 -3
- datalab/gui/panel/base.py +24 -18
- datalab/gui/panel/macro.py +26 -0
- datalab/gui/plothandler.py +10 -1
- datalab/gui/processor/base.py +43 -10
- datalab/gui/processor/image.py +6 -2
- datalab/locale/fr/LC_MESSAGES/datalab.mo +0 -0
- datalab/locale/fr/LC_MESSAGES/datalab.po +3296 -0
- datalab/objectmodel.py +1 -1
- datalab/tests/features/common/auto_analysis_recompute_unit_test.py +81 -0
- datalab/tests/features/common/coordutils_unit_test.py +1 -1
- datalab/tests/features/common/result_deletion_unit_test.py +121 -1
- datalab/tests/features/common/update_tree_robustness_test.py +65 -0
- datalab/tests/features/control/remoteclient_unit.py +10 -0
- datalab/tests/features/hdf5/h5workspace_unit_test.py +133 -0
- datalab/tests/features/image/roigrid_unit_test.py +75 -0
- datalab/tests/features/macro/macroeditor_unit_test.py +2 -2
- datalab/widgets/imagebackground.py +13 -4
- datalab/widgets/instconfviewer.py +2 -2
- datalab/widgets/signalcursor.py +7 -2
- datalab/widgets/signaldeltax.py +4 -1
- {datalab_platform-1.0.2.dist-info → datalab_platform-1.0.4.dist-info}/METADATA +7 -7
- {datalab_platform-1.0.2.dist-info → datalab_platform-1.0.4.dist-info}/RECORD +42 -38
- {datalab_platform-1.0.2.dist-info → datalab_platform-1.0.4.dist-info}/WHEEL +1 -1
- {datalab_platform-1.0.2.dist-info → datalab_platform-1.0.4.dist-info}/entry_points.txt +0 -0
- {datalab_platform-1.0.2.dist-info → datalab_platform-1.0.4.dist-info}/licenses/LICENSE +0 -0
- {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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
221
|
+
self.newly_created_obj_uuid: str | None = None
|
|
221
222
|
# Track when analysis results were just computed
|
|
222
|
-
self.
|
|
223
|
+
self.fresh_analysis_obj_uuid: str | None = None
|
|
223
224
|
# Track when object was just processed (1-to-1)
|
|
224
|
-
self.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
1537
|
-
|
|
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.
|
|
2478
|
+
if obj_uuid == self.objprop.newly_created_obj_uuid:
|
|
2478
2479
|
force_tab = "creation"
|
|
2479
|
-
self.objprop.
|
|
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.
|
|
2482
|
+
elif obj_uuid == self.objprop.fresh_processing_obj_uuid:
|
|
2482
2483
|
force_tab = "processing"
|
|
2483
|
-
self.objprop.
|
|
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.
|
|
2486
|
+
elif obj_uuid == self.objprop.fresh_analysis_obj_uuid:
|
|
2486
2487
|
force_tab = "analysis"
|
|
2487
|
-
self.objprop.
|
|
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
|
-
|
|
2925
|
-
|
|
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
|
datalab/gui/panel/macro.py
CHANGED
|
@@ -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
|
|
datalab/gui/plothandler.py
CHANGED
|
@@ -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)
|
|
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
|
datalab/gui/processor/base.py
CHANGED
|
@@ -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(
|
|
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
|
-
|
|
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
|
|
1339
|
-
results (e.g. peak coordinates, statistical
|
|
1340
|
-
new objects. Results are stored in the object's
|
|
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 =
|
|
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
|
-
|
|
2385
|
-
self.
|
|
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:
|
datalab/gui/processor/image.py
CHANGED
|
@@ -779,7 +779,8 @@ class ImageProcessor(BaseProcessor[ImageROI, ROI2DParam]):
|
|
|
779
779
|
== QW.QMessageBox.No
|
|
780
780
|
):
|
|
781
781
|
return
|
|
782
|
-
|
|
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
|
-
|
|
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
|