diff --git a/src/erlab/interactive/imagetool/__init__.py b/src/erlab/interactive/imagetool/__init__.py index 8830b47b..bf433bdf 100644 --- a/src/erlab/interactive/imagetool/__init__.py +++ b/src/erlab/interactive/imagetool/__init__.py @@ -42,7 +42,7 @@ NormalizeDialog, RotationDialog, ) -from erlab.interactive.utils import DictMenuBar, copy_to_clipboard +from erlab.interactive.utils import DictMenuBar, copy_to_clipboard, wait_dialog from erlab.utils.misc import _convert_to_native if TYPE_CHECKING: @@ -51,6 +51,9 @@ from erlab.interactive.imagetool.slicer import ArraySlicer +_ITOOL_DATA_NAME: str = "" +#: Name to use for the data variable in cached datasets + def _parse_input( data: Collection[xr.DataArray | npt.NDArray] @@ -274,13 +277,16 @@ def array_slicer(self) -> ArraySlicer: return self.slicer_area.array_slicer def to_dataset(self) -> xr.Dataset: + name = self.slicer_area.data.name + name = name if name else "" return self.slicer_area.data.to_dataset( - name="", promote_attrs=False + name=_ITOOL_DATA_NAME, promote_attrs=False ).assign_attrs( { - "state": json.dumps(self.slicer_area.state), - "title": self.windowTitle(), - "rect": self.geometry().getRect(), + "itool_state": json.dumps(self.slicer_area.state), + "itool_title": self.windowTitle(), + "itool_name": name, + "itool_rect": self.geometry().getRect(), } ) @@ -299,7 +305,7 @@ def to_file(self, filename: str | os.PathLike) -> None: self.to_dataset().to_netcdf(filename, engine="h5netcdf", invalid_netcdf=True) @classmethod - def from_dataset(self, ds: xr.Dataset) -> Self: + def from_dataset(cls, ds: xr.Dataset) -> Self: """Restore a window from a dataset saved using :meth:`to_dataset`. Parameters @@ -308,9 +314,13 @@ def from_dataset(self, ds: xr.Dataset) -> Self: The dataset. """ - tool = self(ds[""], state=json.loads(ds.attrs["state"])) - tool.setWindowTitle(ds.attrs["title"]) - tool.setGeometry(*ds.attrs["rect"]) + name = ds.attrs["itool_name"] + name = None if name == "" else name + tool = cls( + ds[_ITOOL_DATA_NAME].rename(name), state=json.loads(ds.attrs["itool_state"]) + ) + tool.setWindowTitle(ds.attrs["itool_title"]) + tool.setGeometry(*ds.attrs["itool_rect"]) return tool @classmethod @@ -405,7 +415,10 @@ class ImageTool(BaseImageTool): def __init__(self, data=None, **kwargs) -> None: super().__init__(data, **kwargs) - self.mnb = ItoolMenuBar(self.slicer_area, self) + self._recent_name_filter: str | None = None + self._recent_directory: str | None = None + + self.mnb = ItoolMenuBar(self) self.slicer_area.sigDataChanged.connect(self._update_title) self._update_title() @@ -419,10 +432,8 @@ def _update_title(self) -> None: # Name contains only whitespace name = None - if name is None and path is None: - title = "" - elif name is None: - title = f"{path}" + if name is None: + title = "" if path is None else path.stem elif path is None or name == path.stem: title = f"{name}" else: @@ -481,7 +492,8 @@ def _open_file( fn, kargs = valid_loaders[self._recent_name_filter] try: - self.slicer_area.set_data(fn(fname, **kargs), file_path=fname) + with wait_dialog(self, "Loading..."): + self.slicer_area.set_data(fn(fname, **kargs), file_path=fname) except Exception as e: QtWidgets.QMessageBox.critical( self, @@ -536,7 +548,8 @@ def _to_hdf5(darr: xr.DataArray, file: str, **kwargs) -> None: if dialog.exec(): files = dialog.selectedFiles() fn, kargs = valid_savers[dialog.selectedNameFilter()] - fn(self.slicer_area._data, files[0], **kargs) + with wait_dialog(self, "Saving..."): + fn(self.slicer_area._data, files[0], **kargs) class ItoolMenuBar(DictMenuBar): @@ -817,6 +830,7 @@ def _normalize(self) -> None: def _reset_filters(self) -> None: self.slicer_area.apply_func(None) + @QtCore.Slot() def _set_colormap_options(self) -> None: self.slicer_area.set_colormap( reverse=self.colorAct[0].isChecked(), @@ -824,11 +838,13 @@ def _set_colormap_options(self) -> None: zero_centered=self.colorAct[2].isChecked(), ) + @QtCore.Slot() def _copy_cursor_val(self) -> None: copy_to_clipboard( str(_convert_to_native(self.slicer_area.array_slicer._values)) ) + @QtCore.Slot() def _copy_cursor_idx(self) -> None: copy_to_clipboard( str(_convert_to_native(self.slicer_area.array_slicer._indices)) diff --git a/src/erlab/interactive/kspace.py b/src/erlab/interactive/kspace.py index c5e713e0..debd9606 100644 --- a/src/erlab/interactive/kspace.py +++ b/src/erlab/interactive/kspace.py @@ -21,7 +21,12 @@ ColorMapComboBox, # noqa: F401 ColorMapGammaWidget, # noqa: F401 ) -from erlab.interactive.utils import copy_to_clipboard, generate_code, xImageItem +from erlab.interactive.utils import ( + copy_to_clipboard, + generate_code, + wait_dialog, + xImageItem, +) from erlab.plotting.bz import get_bz_edge if TYPE_CHECKING: @@ -397,18 +402,12 @@ def show_converted(self) -> None: if self.data.kspace._has_hv: self.data.kspace.inner_potential = self._offset_spins["V0"].value() - wait_dialog = QtWidgets.QDialog(self) - dialog_layout = QtWidgets.QVBoxLayout() - wait_dialog.setLayout(dialog_layout) - dialog_layout.addWidget(QtWidgets.QLabel("Converting...")) - - wait_dialog.open() - from erlab.interactive.imagetool import ImageTool, itool + with wait_dialog(self, "Converting..."): + from erlab.interactive.imagetool import ImageTool, itool - data_kconv = self.data.kspace.convert( - bounds=self.bounds, resolution=self.resolution - ) - wait_dialog.close() + data_kconv = self.data.kspace.convert( + bounds=self.bounds, resolution=self.resolution + ) tool = cast(ImageTool | None, itool(data_kconv, execute=False)) if tool is not None: diff --git a/src/erlab/interactive/utils.py b/src/erlab/interactive/utils.py index 9c63fe54..fb003289 100644 --- a/src/erlab/interactive/utils.py +++ b/src/erlab/interactive/utils.py @@ -2,6 +2,7 @@ from __future__ import annotations +import contextlib import functools import inspect import itertools @@ -43,6 +44,7 @@ "generate_code", "make_crosshairs", "parse_data", + "wait_dialog", "xImageItem", ] @@ -71,6 +73,20 @@ def parse_data(data) -> xr.DataArray: return data # .astype(float, order="C") +@contextlib.contextmanager +def wait_dialog(parent: QtWidgets.QWidget, message: str): + wait_dialog = QtWidgets.QDialog(parent) + dialog_layout = QtWidgets.QVBoxLayout() + wait_dialog.setLayout(dialog_layout) + dialog_layout.addWidget(QtWidgets.QLabel(message)) + + try: + wait_dialog.open() + yield wait_dialog + finally: + wait_dialog.close() + + def array_rect(data): data_coords = tuple(data[dim].values for dim in data.dims) data_incs = tuple(coord[1] - coord[0] for coord in data_coords)