datalab-platform 1.0.1__py3-none-any.whl → 1.0.3__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/adapters_plotpy/converters.py +3 -1
- datalab/adapters_plotpy/coordutils.py +157 -0
- datalab/adapters_plotpy/roi/image.py +35 -6
- datalab/adapters_plotpy/roi/signal.py +8 -1
- datalab/config.py +88 -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 +16 -2
- datalab/gui/h5io.py +25 -0
- datalab/gui/macroeditor.py +37 -6
- datalab/gui/main.py +62 -5
- datalab/gui/newobject.py +7 -0
- datalab/gui/objectview.py +18 -3
- datalab/gui/panel/base.py +89 -16
- datalab/gui/panel/macro.py +26 -0
- datalab/gui/plothandler.py +20 -2
- datalab/gui/processor/base.py +72 -26
- datalab/gui/processor/image.py +6 -2
- datalab/gui/processor/signal.py +10 -0
- datalab/gui/roieditor.py +2 -2
- datalab/locale/fr/LC_MESSAGES/datalab.mo +0 -0
- datalab/locale/fr/LC_MESSAGES/datalab.po +3288 -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 +212 -0
- datalab/tests/features/common/result_deletion_unit_test.py +121 -1
- datalab/tests/features/common/roi_plotitem_unit_test.py +4 -2
- 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 +104 -3
- datalab/tests/features/signal/custom_signal_bug_unit_test.py +96 -0
- 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.1.dist-info → datalab_platform-1.0.3.dist-info}/METADATA +3 -3
- {datalab_platform-1.0.1.dist-info → datalab_platform-1.0.3.dist-info}/RECORD +50 -43
- {datalab_platform-1.0.1.dist-info → datalab_platform-1.0.3.dist-info}/WHEEL +0 -0
- {datalab_platform-1.0.1.dist-info → datalab_platform-1.0.3.dist-info}/entry_points.txt +0 -0
- {datalab_platform-1.0.1.dist-info → datalab_platform-1.0.3.dist-info}/licenses/LICENSE +0 -0
- {datalab_platform-1.0.1.dist-info → datalab_platform-1.0.3.dist-info}/top_level.txt +0 -0
datalab/control/proxy.py
CHANGED
|
@@ -300,6 +300,39 @@ class LocalProxy(BaseProxy):
|
|
|
300
300
|
"""
|
|
301
301
|
self._datalab.add_annotations_from_items(items, refresh_plot, panel)
|
|
302
302
|
|
|
303
|
+
def load_h5_workspace(
|
|
304
|
+
self, h5files: list[str] | str, reset_all: bool = False
|
|
305
|
+
) -> None:
|
|
306
|
+
"""Load HDF5 workspace files without showing file dialog.
|
|
307
|
+
|
|
308
|
+
This method loads one or more DataLab native HDF5 files directly, bypassing
|
|
309
|
+
the file dialog. It is safe to call from the internal console or any context
|
|
310
|
+
where Qt dialogs would cause threading issues.
|
|
311
|
+
|
|
312
|
+
Args:
|
|
313
|
+
h5files: Path(s) to HDF5 file(s). Can be a single path string or a list
|
|
314
|
+
of paths
|
|
315
|
+
reset_all: If True, reset workspace before loading. Defaults to False.
|
|
316
|
+
|
|
317
|
+
Raises:
|
|
318
|
+
ValueError: if file is not a DataLab native HDF5 file
|
|
319
|
+
"""
|
|
320
|
+
if isinstance(h5files, str):
|
|
321
|
+
h5files = [h5files]
|
|
322
|
+
self._datalab.load_h5_workspace(h5files, reset_all)
|
|
323
|
+
|
|
324
|
+
def save_h5_workspace(self, filename: str) -> None:
|
|
325
|
+
"""Save workspace to HDF5 file without showing file dialog.
|
|
326
|
+
|
|
327
|
+
This method saves the current workspace to a DataLab native HDF5 file
|
|
328
|
+
directly, bypassing the file dialog. It is safe to call from the internal
|
|
329
|
+
console or any context where Qt dialogs would cause threading issues.
|
|
330
|
+
|
|
331
|
+
Args:
|
|
332
|
+
filename: Path to the output HDF5 file
|
|
333
|
+
"""
|
|
334
|
+
self._datalab.save_h5_workspace(filename)
|
|
335
|
+
|
|
303
336
|
|
|
304
337
|
@contextmanager
|
|
305
338
|
def proxy_context(what: str) -> Generator[LocalProxy | RemoteProxy, None, None]:
|
datalab/control/remote.py
CHANGED
|
@@ -84,6 +84,8 @@ class RemoteServer(QC.QThread):
|
|
|
84
84
|
SIG_SAVE_TO_H5 = QC.Signal(str)
|
|
85
85
|
SIG_OPEN_H5 = QC.Signal(list, bool, bool)
|
|
86
86
|
SIG_IMPORT_H5 = QC.Signal(str, bool)
|
|
87
|
+
SIG_LOAD_H5_WORKSPACE = QC.Signal(list, bool)
|
|
88
|
+
SIG_SAVE_H5_WORKSPACE = QC.Signal(str)
|
|
87
89
|
SIG_CALC = QC.Signal(str, object)
|
|
88
90
|
SIG_RUN_MACRO = QC.Signal(str)
|
|
89
91
|
SIG_STOP_MACRO = QC.Signal(str)
|
|
@@ -114,6 +116,8 @@ class RemoteServer(QC.QThread):
|
|
|
114
116
|
self.SIG_SAVE_TO_H5.connect(win.save_to_h5_file)
|
|
115
117
|
self.SIG_OPEN_H5.connect(win.open_h5_files)
|
|
116
118
|
self.SIG_IMPORT_H5.connect(win.import_h5_file)
|
|
119
|
+
self.SIG_LOAD_H5_WORKSPACE.connect(win.load_h5_workspace)
|
|
120
|
+
self.SIG_SAVE_H5_WORKSPACE.connect(win.save_h5_workspace)
|
|
117
121
|
self.SIG_CALC.connect(win.calc)
|
|
118
122
|
self.SIG_RUN_MACRO.connect(win.run_macro)
|
|
119
123
|
self.SIG_STOP_MACRO.connect(win.stop_macro)
|
|
@@ -268,6 +272,37 @@ class RemoteServer(QC.QThread):
|
|
|
268
272
|
reset_all = False if reset_all is None else reset_all
|
|
269
273
|
self.SIG_IMPORT_H5.emit(filename, reset_all)
|
|
270
274
|
|
|
275
|
+
@remote_call
|
|
276
|
+
def load_h5_workspace(self, h5files: list[str], reset_all: bool = False) -> None:
|
|
277
|
+
"""Load native DataLab HDF5 workspace files without any GUI elements.
|
|
278
|
+
|
|
279
|
+
This method can be safely called from scripts as it does not create
|
|
280
|
+
any Qt widgets, dialogs, or progress bars.
|
|
281
|
+
|
|
282
|
+
Args:
|
|
283
|
+
h5files: List of native DataLab HDF5 filenames
|
|
284
|
+
reset_all: Reset all application data before importing. Defaults to False.
|
|
285
|
+
|
|
286
|
+
Raises:
|
|
287
|
+
ValueError: If a file is not a valid native DataLab HDF5 file
|
|
288
|
+
"""
|
|
289
|
+
self.SIG_LOAD_H5_WORKSPACE.emit(h5files, reset_all)
|
|
290
|
+
|
|
291
|
+
@remote_call
|
|
292
|
+
def save_h5_workspace(self, filename: str) -> None:
|
|
293
|
+
"""Save current workspace to a native DataLab HDF5 file without GUI elements.
|
|
294
|
+
|
|
295
|
+
This method can be safely called from scripts as it does not create
|
|
296
|
+
any Qt widgets, dialogs, or progress bars.
|
|
297
|
+
|
|
298
|
+
Args:
|
|
299
|
+
filename: HDF5 filename to save to
|
|
300
|
+
|
|
301
|
+
Raises:
|
|
302
|
+
IOError: If file cannot be saved
|
|
303
|
+
"""
|
|
304
|
+
self.SIG_SAVE_H5_WORKSPACE.emit(filename)
|
|
305
|
+
|
|
271
306
|
@remote_call
|
|
272
307
|
def load_from_files(self, filenames: list[str]) -> None:
|
|
273
308
|
"""Open objects from files in current panel (signals/images).
|
datalab/data/doc/DataLab_en.pdf
CHANGED
|
Binary file
|
datalab/data/doc/DataLab_fr.pdf
CHANGED
|
Binary file
|
|
@@ -3,5 +3,5 @@
|
|
|
3
3
|
xmlns="http://www.w3.org/2000/svg">
|
|
4
4
|
<rect style="fill:#696969;fill-opacity:1;stroke-width:0.547874" width="0.53186357" height="12.584304" x="6.7340684" y="0.70784807" />
|
|
5
5
|
<rect style="fill:#696969;fill-opacity:1;stroke-width:0.54642" width="0.53186357" height="12.5176" x="6.7340684" y="-13.258801" transform="rotate(90)" />
|
|
6
|
-
<path style="fill:none;stroke:#009966;stroke-width:0.35;stroke-linejoin:round" d="M 1,7 C 1.5,5.5 2,4.5 2.5,5.5 3,7 3.3,8.5 3.6,7.5 3.9,6 4.2,4 4.5,5 4.8,6.5 5,8.5 5.3,7.5 5.6,6 5.9,3.5 6.3,5 6.7,7 6.9,9.5 7.2,7.5 7.5,5 7.8,2 8.2,5 8.6,8 8.9,11 9.3,8 9.7,5 10.1,2 10.6,5.5 11.1,9 11.6,12 12.2,8.5 12.8,5 13,2" />
|
|
6
|
+
<path style="fill:none;stroke:#009966;stroke-width:0.35;stroke-linejoin:round" d="M 1,7 C 1.5,5.5 2,4.5 2.5,5.5 C 3,7 3.3,8.5 3.6,7.5 C 3.9,6 4.2,4 4.5,5 C 4.8,6.5 5,8.5 5.3,7.5 C 5.6,6 5.9,3.5 6.3,5 C 6.7,7 6.9,9.5 7.2,7.5 C 7.5,5 7.8,2 8.2,5 C 8.6,8 8.9,11 9.3,8 C 9.7,5 10.1,2 10.6,5.5 C 11.1,9 11.6,12 12.2,8.5 C 12.8,5 13,3.5 13,2" />
|
|
7
7
|
</svg>
|
|
@@ -3,5 +3,5 @@
|
|
|
3
3
|
xmlns="http://www.w3.org/2000/svg">
|
|
4
4
|
<rect style="fill:#696969;fill-opacity:1;stroke-width:0.547874" width="0.53186357" height="12.584304" x="6.7340684" y="0.70784807" />
|
|
5
5
|
<rect style="fill:#696969;fill-opacity:1;stroke-width:0.54642" width="0.53186357" height="12.5176" x="6.7340684" y="-13.258801" transform="rotate(90)" />
|
|
6
|
-
<path style="fill:none;stroke:#9900cc;stroke-width:0.35;stroke-linejoin:round" d="M 1.5,12 C 2,12 2.5,12 3,11.9 3.5,11.8 4,11.6 4.5,11.2 5,10.8 5.5,10.2 6,9.5 6.5,8.5 7,7.2 7.5,5.5 8,4 8.5,3 9,2.5 9.5,2.2 10,2.1 10.5,2.05 11,2.02 11.5,2.01 12,2 12.5,2" />
|
|
6
|
+
<path style="fill:none;stroke:#9900cc;stroke-width:0.35;stroke-linejoin:round" d="M 1.5,12 C 2,12 2.5,12 3,11.9 C 3.5,11.8 4,11.6 4.5,11.2 C 5,10.8 5.5,10.2 6,9.5 C 6.5,8.5 7,7.2 7.5,5.5 C 8,4 8.5,3 9,2.5 C 9.5,2.2 10,2.1 10.5,2.05 C 11,2.02 11.5,2.01 12,2 L 12.5,2" />
|
|
7
7
|
</svg>
|
datalab/gui/actionhandler.py
CHANGED
|
@@ -50,6 +50,10 @@ from qtpy import QtWidgets as QW
|
|
|
50
50
|
from datalab.adapters_metadata import GeometryAdapter, TableAdapter, have_results
|
|
51
51
|
from datalab.config import Conf, _
|
|
52
52
|
from datalab.gui import newobject
|
|
53
|
+
from datalab.gui.processor.base import (
|
|
54
|
+
clear_analysis_parameters,
|
|
55
|
+
extract_analysis_parameters,
|
|
56
|
+
)
|
|
53
57
|
from datalab.widgets import fitdialog
|
|
54
58
|
|
|
55
59
|
if TYPE_CHECKING:
|
|
@@ -361,6 +365,15 @@ class BaseActionHandler(metaclass=abc.ABCMeta):
|
|
|
361
365
|
obj: Object containing the result
|
|
362
366
|
adapter: Adapter for the result to delete
|
|
363
367
|
"""
|
|
368
|
+
# Check if this result matches the stored analysis parameters
|
|
369
|
+
# If so, clear them to prevent auto-recompute from attempting to
|
|
370
|
+
# recompute the deleted analysis when ROI changes
|
|
371
|
+
analysis_params = extract_analysis_parameters(obj)
|
|
372
|
+
if (
|
|
373
|
+
analysis_params is not None
|
|
374
|
+
and analysis_params.func_name == adapter.func_name
|
|
375
|
+
):
|
|
376
|
+
clear_analysis_parameters(obj)
|
|
364
377
|
adapter.remove_from(obj)
|
|
365
378
|
# Update properties panel to reflect the removal
|
|
366
379
|
if obj is self.panel.objview.get_current_object():
|
|
@@ -1269,7 +1282,9 @@ class SignalActionHandler(BaseActionHandler):
|
|
|
1269
1282
|
with self.new_menu(_("Axis transformation")):
|
|
1270
1283
|
self.action_for("transpose")
|
|
1271
1284
|
self.action_for("reverse_x")
|
|
1272
|
-
self.action_for("
|
|
1285
|
+
self.action_for("replace_x_by_other_y")
|
|
1286
|
+
self.action_for("xy_mode")
|
|
1287
|
+
self.action_for("to_cartesian", separator=True)
|
|
1273
1288
|
self.action_for("to_polar")
|
|
1274
1289
|
with self.new_menu(_("Frequency filters"), icon_name="highpass.svg"):
|
|
1275
1290
|
self.action_for("lowpass")
|
|
@@ -1364,7 +1379,6 @@ class SignalActionHandler(BaseActionHandler):
|
|
|
1364
1379
|
separator=True,
|
|
1365
1380
|
tip=_("Compute all stability features"),
|
|
1366
1381
|
)
|
|
1367
|
-
self.action_for("xy_mode", separator=True)
|
|
1368
1382
|
|
|
1369
1383
|
# MARK: ANALYSIS
|
|
1370
1384
|
with self.new_category(ActionCategory.ANALYSIS):
|
datalab/gui/h5io.py
CHANGED
|
@@ -54,6 +54,31 @@ class H5InputOutput:
|
|
|
54
54
|
panel.serialize_to_hdf5(writer)
|
|
55
55
|
writer.close()
|
|
56
56
|
|
|
57
|
+
def open_file_headless(self, filename: str, reset_all: bool) -> bool:
|
|
58
|
+
"""Open native DataLab HDF5 file without any GUI elements.
|
|
59
|
+
|
|
60
|
+
This method can be safely called from any thread (e.g., the console thread)
|
|
61
|
+
as it does not create any Qt widgets or dialogs.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
filename: HDF5 filename
|
|
65
|
+
reset_all: Reset all application data before importing
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
True if file was successfully opened as a native DataLab file,
|
|
69
|
+
False if the file format is not compatible (KeyError was raised)
|
|
70
|
+
"""
|
|
71
|
+
try:
|
|
72
|
+
reader = NativeH5Reader(filename)
|
|
73
|
+
if reset_all:
|
|
74
|
+
self.mainwindow.reset_all()
|
|
75
|
+
for panel in self.mainwindow.panels:
|
|
76
|
+
panel.deserialize_from_hdf5(reader, reset_all)
|
|
77
|
+
reader.close()
|
|
78
|
+
return True
|
|
79
|
+
except KeyError:
|
|
80
|
+
return False
|
|
81
|
+
|
|
57
82
|
def open_file(self, filename: str, import_all: bool, reset_all: bool) -> None:
|
|
58
83
|
"""Open HDF5 file"""
|
|
59
84
|
progress = None
|
datalab/gui/macroeditor.py
CHANGED
|
@@ -93,6 +93,7 @@ print("All done!")
|
|
|
93
93
|
self.set_code(self.MACRO_SAMPLE)
|
|
94
94
|
self.editor.modificationChanged.connect(self.modification_changed)
|
|
95
95
|
self.process = None
|
|
96
|
+
self.__last_exit_code = None
|
|
96
97
|
|
|
97
98
|
@property
|
|
98
99
|
def title(self) -> str:
|
|
@@ -185,8 +186,20 @@ print("All done!")
|
|
|
185
186
|
"""
|
|
186
187
|
global UNTITLED_NB # pylint: disable=global-statement
|
|
187
188
|
UNTITLED_NB += 1
|
|
188
|
-
|
|
189
|
-
|
|
189
|
+
return f"macro_{UNTITLED_NB:02d}"
|
|
190
|
+
|
|
191
|
+
@staticmethod
|
|
192
|
+
def set_untitled_number(number: int) -> None:
|
|
193
|
+
"""Set the untitled number counter
|
|
194
|
+
|
|
195
|
+
This is useful when loading macros from HDF5 or files to ensure
|
|
196
|
+
that the next untitled macro has a unique name.
|
|
197
|
+
|
|
198
|
+
Args:
|
|
199
|
+
number: New untitled number
|
|
200
|
+
"""
|
|
201
|
+
global UNTITLED_NB # pylint: disable=global-statement
|
|
202
|
+
UNTITLED_NB = number
|
|
190
203
|
|
|
191
204
|
def modification_changed(self, state: bool) -> None:
|
|
192
205
|
"""Method called when macro's editor modification state changed
|
|
@@ -207,8 +220,10 @@ print("All done!")
|
|
|
207
220
|
Returns:
|
|
208
221
|
Locale str
|
|
209
222
|
"""
|
|
210
|
-
|
|
211
|
-
|
|
223
|
+
# Python 3 outputs UTF-8 by default, so we need to decode as UTF-8
|
|
224
|
+
# instead of using the system locale codec (which might be cp1252 on Windows)
|
|
225
|
+
utf8_codec = QC.QTextCodec.codecForName(b"UTF-8")
|
|
226
|
+
return utf8_codec.toUnicode(bytearr.data())
|
|
212
227
|
|
|
213
228
|
def get_stdout(self) -> str:
|
|
214
229
|
"""Return standard output str
|
|
@@ -257,9 +272,16 @@ print("All done!")
|
|
|
257
272
|
def run(self) -> None:
|
|
258
273
|
"""Run macro"""
|
|
259
274
|
self.process = QC.QProcess()
|
|
260
|
-
code = self.get_code()
|
|
275
|
+
code = self.get_code()
|
|
261
276
|
datalab_path = osp.abspath(osp.join(osp.dirname(datalab.__file__), os.pardir))
|
|
262
|
-
|
|
277
|
+
# Reconfigure stdout/stderr to use UTF-8 encoding to avoid UnicodeEncodeError
|
|
278
|
+
# on Windows with locales that don't support all Unicode characters
|
|
279
|
+
# (e.g., cp1252)
|
|
280
|
+
code = (
|
|
281
|
+
f"import sys; sys.path.append(r'{datalab_path}'); "
|
|
282
|
+
f"sys.stdout.reconfigure(encoding='utf-8'); "
|
|
283
|
+
f"sys.stderr.reconfigure(encoding='utf-8'){os.linesep}{code}"
|
|
284
|
+
)
|
|
263
285
|
env = QC.QProcessEnvironment()
|
|
264
286
|
env.insert(execenv.XMLRPCPORT_ENV, str(execenv.xmlrpcport))
|
|
265
287
|
sysenv = env.systemEnvironment()
|
|
@@ -305,6 +327,15 @@ print("All done!")
|
|
|
305
327
|
exit_code: Exit code
|
|
306
328
|
exit_status: Exit status
|
|
307
329
|
"""
|
|
330
|
+
self.__last_exit_code = exit_code
|
|
308
331
|
self.print(_("# <== '%s' macro has finished") % self.title, eol_before=False)
|
|
309
332
|
self.FINISHED.emit()
|
|
310
333
|
self.process = None
|
|
334
|
+
|
|
335
|
+
def get_exit_code(self) -> int | None:
|
|
336
|
+
"""Return last exit code of the macro process
|
|
337
|
+
|
|
338
|
+
Returns:
|
|
339
|
+
Last exit code or None if process has not finished yet
|
|
340
|
+
"""
|
|
341
|
+
return self.__last_exit_code
|
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(
|
|
@@ -1617,6 +1619,8 @@ class DLMainWindow(QW.QMainWindow, AbstractDLControl, metaclass=DLMainWindowMeta
|
|
|
1617
1619
|
)
|
|
1618
1620
|
if answer == QW.QMessageBox.Yes:
|
|
1619
1621
|
reset_all = True
|
|
1622
|
+
elif answer == QW.QMessageBox.No:
|
|
1623
|
+
reset_all = False
|
|
1620
1624
|
elif answer == QW.QMessageBox.Ignore:
|
|
1621
1625
|
Conf.io.h5_clear_workspace_ask.set(False)
|
|
1622
1626
|
if h5files is None:
|
|
@@ -1665,6 +1669,59 @@ class DLMainWindow(QW.QMainWindow, AbstractDLControl, metaclass=DLMainWindowMeta
|
|
|
1665
1669
|
self.__check_h5file(filename, "load")
|
|
1666
1670
|
self.h5inputoutput.import_files(filenames, False, reset_all)
|
|
1667
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
|
+
|
|
1668
1725
|
@remote_controlled
|
|
1669
1726
|
def import_h5_file(self, filename: str, reset_all: bool | None = None) -> None:
|
|
1670
1727
|
"""Import HDF5 file into DataLab
|
|
@@ -2025,7 +2082,7 @@ class DLMainWindow(QW.QMainWindow, AbstractDLControl, metaclass=DLMainWindowMeta
|
|
|
2025
2082
|
Returns:
|
|
2026
2083
|
True if closed properly, False otherwise
|
|
2027
2084
|
"""
|
|
2028
|
-
if not env.execenv.unattended and self.
|
|
2085
|
+
if not env.execenv.unattended and self.is_modified():
|
|
2029
2086
|
answer = QW.QMessageBox.warning(
|
|
2030
2087
|
self,
|
|
2031
2088
|
_("Quit"),
|
|
@@ -2037,7 +2094,7 @@ class DLMainWindow(QW.QMainWindow, AbstractDLControl, metaclass=DLMainWindowMeta
|
|
|
2037
2094
|
)
|
|
2038
2095
|
if answer == QW.QMessageBox.Yes:
|
|
2039
2096
|
self.save_to_h5_file()
|
|
2040
|
-
if self.
|
|
2097
|
+
if self.is_modified():
|
|
2041
2098
|
return False
|
|
2042
2099
|
elif answer == QW.QMessageBox.Cancel:
|
|
2043
2100
|
return False
|
datalab/gui/newobject.py
CHANGED
|
@@ -122,6 +122,13 @@ def create_signal_gui(
|
|
|
122
122
|
param = NewSignalParam()
|
|
123
123
|
edit = True # Default to editing if no parameters provided
|
|
124
124
|
|
|
125
|
+
# CustomSignalParam requires edit mode to initialize the xyarray.
|
|
126
|
+
# Without this, if edit=False (the default in new_object), the setup_array
|
|
127
|
+
# call would be skipped, leaving xyarray as None, which would cause an
|
|
128
|
+
# AttributeError when trying to access param.xyarray.T later.
|
|
129
|
+
if isinstance(param, OrigCustomSignalParam):
|
|
130
|
+
edit = True
|
|
131
|
+
|
|
125
132
|
if isinstance(param, OrigCustomSignalParam) and edit:
|
|
126
133
|
p_init = NewSignalParam(_("Custom signal"))
|
|
127
134
|
p_init.size = 10 # Set smaller default size for initial input
|
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
|
)
|
|
@@ -216,6 +217,13 @@ class ObjectProp(QW.QWidget):
|
|
|
216
217
|
self.analysis_parameters.setReadOnly(True)
|
|
217
218
|
self.analysis_parameters.setFont(font)
|
|
218
219
|
|
|
220
|
+
# Track newly created objects to show Creation tab only once
|
|
221
|
+
self.newly_created_obj_uuid: str | None = None
|
|
222
|
+
# Track when analysis results were just computed
|
|
223
|
+
self.fresh_analysis_obj_uuid: str | None = None
|
|
224
|
+
# Track when object was just processed (1-to-1)
|
|
225
|
+
self.fresh_processing_obj_uuid: str | None = None
|
|
226
|
+
|
|
219
227
|
self.tabwidget.addTab(
|
|
220
228
|
self.processing_history, get_icon("history.svg"), _("History")
|
|
221
229
|
)
|
|
@@ -370,11 +378,16 @@ class ObjectProp(QW.QWidget):
|
|
|
370
378
|
self.properties.get()
|
|
371
379
|
self.properties.apply_button.setEnabled(False)
|
|
372
380
|
|
|
373
|
-
def update_properties_from(
|
|
381
|
+
def update_properties_from(
|
|
382
|
+
self,
|
|
383
|
+
obj: SignalObj | ImageObj | None = None,
|
|
384
|
+
force_tab: Literal["creation", "processing", "analysis", None] | None = None,
|
|
385
|
+
) -> None:
|
|
374
386
|
"""Update properties panel (properties, creation, processing) from object.
|
|
375
387
|
|
|
376
388
|
Args:
|
|
377
389
|
obj: Signal or Image object
|
|
390
|
+
force_tab: Force a specific tab to be current
|
|
378
391
|
"""
|
|
379
392
|
self.properties.setDisabled(obj is None)
|
|
380
393
|
if obj is None:
|
|
@@ -411,30 +424,56 @@ class ObjectProp(QW.QWidget):
|
|
|
411
424
|
self.processing_scroll = None
|
|
412
425
|
|
|
413
426
|
# Setup Creation and Processing tabs (if applicable)
|
|
414
|
-
has_creation_tab =
|
|
427
|
+
has_creation_tab = False
|
|
428
|
+
has_processing_tab = False
|
|
415
429
|
if obj is not None:
|
|
416
430
|
has_creation_tab = self.setup_creation_tab(obj)
|
|
417
|
-
has_processing_tab = self.setup_processing_tab(obj)
|
|
431
|
+
has_processing_tab = self.setup_processing_tab(obj) # Processing tab setup
|
|
418
432
|
|
|
419
433
|
# Trigger visibility update for History and Analysis parameters tabs
|
|
420
434
|
# (will be called via textChanged signals, but we call explicitly
|
|
421
435
|
# here to ensure initial state is correct)
|
|
422
436
|
self._update_tab_visibility()
|
|
423
437
|
|
|
424
|
-
#
|
|
425
|
-
#
|
|
426
|
-
#
|
|
427
|
-
#
|
|
428
|
-
#
|
|
429
|
-
if
|
|
430
|
-
self.tabwidget.setCurrentWidget(self.analysis_parameters)
|
|
431
|
-
elif has_creation_tab:
|
|
438
|
+
# Determine which tab to show based on force_tab parameter:
|
|
439
|
+
# - If force_tab="creation" and Creation tab exists, show it
|
|
440
|
+
# - If force_tab="processing" and Processing tab exists, show it
|
|
441
|
+
# - If force_tab="analysis" and Analysis tab has content, show it
|
|
442
|
+
# - Otherwise, always show Properties tab (default behavior)
|
|
443
|
+
if force_tab == "creation" and has_creation_tab:
|
|
432
444
|
self.tabwidget.setCurrentWidget(self.creation_scroll)
|
|
433
|
-
elif has_processing_tab:
|
|
445
|
+
elif force_tab == "processing" and has_processing_tab:
|
|
434
446
|
self.tabwidget.setCurrentWidget(self.processing_scroll)
|
|
447
|
+
elif force_tab == "analysis" and has_analysis_parameters:
|
|
448
|
+
self.tabwidget.setCurrentWidget(self.analysis_parameters)
|
|
435
449
|
else:
|
|
450
|
+
# Default: always show Properties tab when switching objects
|
|
436
451
|
self.tabwidget.setCurrentWidget(self.properties)
|
|
437
452
|
|
|
453
|
+
def mark_as_newly_created(self, obj: SignalObj | ImageObj) -> None:
|
|
454
|
+
"""Mark object to show Creation tab on next selection.
|
|
455
|
+
|
|
456
|
+
Args:
|
|
457
|
+
obj: Object to mark
|
|
458
|
+
"""
|
|
459
|
+
self.newly_created_obj_uuid = get_uuid(obj)
|
|
460
|
+
|
|
461
|
+
def mark_as_freshly_processed(self, obj: SignalObj | ImageObj) -> None:
|
|
462
|
+
"""Mark object to show Processing tab on next selection.
|
|
463
|
+
|
|
464
|
+
Args:
|
|
465
|
+
obj: Object to mark
|
|
466
|
+
"""
|
|
467
|
+
self.fresh_processing_obj_uuid = get_uuid(obj)
|
|
468
|
+
|
|
469
|
+
def mark_as_fresh_analysis(self, obj: SignalObj | ImageObj) -> None:
|
|
470
|
+
"""Mark object to show Analysis tab on next selection.
|
|
471
|
+
|
|
472
|
+
Args:
|
|
473
|
+
obj: Object to mark
|
|
474
|
+
"""
|
|
475
|
+
self.fresh_analysis_obj_uuid = get_uuid(obj)
|
|
476
|
+
|
|
438
477
|
def get_changed_properties(self) -> dict[str, Any]:
|
|
439
478
|
"""Get dictionary of properties that have changed from original values.
|
|
440
479
|
|
|
@@ -1490,6 +1529,16 @@ class BaseDataPanel(AbstractPanel, Generic[TypeObj, TypeROI, TypeROIEditor]):
|
|
|
1490
1529
|
obj.check_data()
|
|
1491
1530
|
self.objmodel.add_object(obj, group_id)
|
|
1492
1531
|
|
|
1532
|
+
# Mark this object as newly created to show Creation tab on first selection
|
|
1533
|
+
# BUT: Don't overwrite if this object is already marked as freshly processed
|
|
1534
|
+
# or has fresh analysis results (those take precedence)
|
|
1535
|
+
obj_uuid = get_uuid(obj)
|
|
1536
|
+
if obj_uuid not in (
|
|
1537
|
+
self.objprop.fresh_processing_obj_uuid,
|
|
1538
|
+
self.objprop.fresh_analysis_obj_uuid,
|
|
1539
|
+
):
|
|
1540
|
+
self.objprop.mark_as_newly_created(obj)
|
|
1541
|
+
|
|
1493
1542
|
# Block signals to avoid updating the plot (unnecessary refresh)
|
|
1494
1543
|
self.objview.blockSignals(True)
|
|
1495
1544
|
self.objview.add_object_item(obj, group_id, set_current=set_current)
|
|
@@ -2419,7 +2468,26 @@ class BaseDataPanel(AbstractPanel, Generic[TypeObj, TypeROI, TypeROIEditor]):
|
|
|
2419
2468
|
"""
|
|
2420
2469
|
selected_objects = self.objview.get_sel_objects(include_groups=True)
|
|
2421
2470
|
selected_groups = self.objview.get_sel_groups()
|
|
2422
|
-
|
|
2471
|
+
|
|
2472
|
+
# Determine which tab to show based on object state
|
|
2473
|
+
current_obj = self.objview.get_current_object()
|
|
2474
|
+
force_tab = None
|
|
2475
|
+
if current_obj is not None:
|
|
2476
|
+
obj_uuid = get_uuid(current_obj)
|
|
2477
|
+
# Show Creation tab for newly created objects (only once)
|
|
2478
|
+
if obj_uuid == self.objprop.newly_created_obj_uuid:
|
|
2479
|
+
force_tab = "creation"
|
|
2480
|
+
self.objprop.newly_created_obj_uuid = None
|
|
2481
|
+
# Show Processing tab for freshly processed objects (only once)
|
|
2482
|
+
elif obj_uuid == self.objprop.fresh_processing_obj_uuid:
|
|
2483
|
+
force_tab = "processing"
|
|
2484
|
+
self.objprop.fresh_processing_obj_uuid = None
|
|
2485
|
+
# Show Analysis tab for objects with fresh analysis results
|
|
2486
|
+
elif obj_uuid == self.objprop.fresh_analysis_obj_uuid:
|
|
2487
|
+
force_tab = "analysis"
|
|
2488
|
+
self.objprop.fresh_analysis_obj_uuid = None
|
|
2489
|
+
|
|
2490
|
+
self.objprop.update_properties_from(current_obj, force_tab=force_tab)
|
|
2423
2491
|
self.acthandler.selected_objects_changed(selected_groups, selected_objects)
|
|
2424
2492
|
self.refresh_plot("selected", update_items, False)
|
|
2425
2493
|
|
|
@@ -2854,9 +2922,11 @@ class BaseDataPanel(AbstractPanel, Generic[TypeObj, TypeROI, TypeROIEditor]):
|
|
|
2854
2922
|
dialog was accepted or not.
|
|
2855
2923
|
"""
|
|
2856
2924
|
obj = self.objview.get_sel_objects(include_groups=True)[-1]
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
)
|
|
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)
|
|
2860
2930
|
roi_editor_class = self.get_roieditor_class() # pylint: disable=not-callable
|
|
2861
2931
|
roi_editor = roi_editor_class(
|
|
2862
2932
|
parent=self.parentWidget(),
|
|
@@ -3207,6 +3277,9 @@ class BaseDataPanel(AbstractPanel, Generic[TypeObj, TypeROI, TypeROIEditor]):
|
|
|
3207
3277
|
# Remove all table and geometry results using adapter methods
|
|
3208
3278
|
TableAdapter.remove_all_from(obj)
|
|
3209
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)
|
|
3210
3283
|
if obj is self.objview.get_current_object():
|
|
3211
3284
|
self.objprop.update_properties_from(obj)
|
|
3212
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
|
|