diff --git a/src/PyMca5/PyMcaCore/NexusDataSource.py b/src/PyMca5/PyMcaCore/NexusDataSource.py index d0835655e..b75025898 100644 --- a/src/PyMca5/PyMcaCore/NexusDataSource.py +++ b/src/PyMca5/PyMcaCore/NexusDataSource.py @@ -227,6 +227,15 @@ def refresh(self): phynxInstance._sourceName = pattern self.__lastKeyInfo = {} + def close(self): + """Close every open HDF5 handle held by this source.""" + for instance in self._sourceObjectList: + try: + instance.close() + except Exception as e: + _logger.debug("Error closing HDF5 source: %s", e) + self._sourceObjectList = [] + def getSourceInfo(self): """ Returns a dictionary with the key "KeyList" (list of all available keys diff --git a/src/PyMca5/PyMcaGui/io/hdf5/HDF5Widget.py b/src/PyMca5/PyMcaGui/io/hdf5/HDF5Widget.py index f327a1143..e738552ef 100644 --- a/src/PyMca5/PyMcaGui/io/hdf5/HDF5Widget.py +++ b/src/PyMca5/PyMcaGui/io/hdf5/HDF5Widget.py @@ -425,11 +425,14 @@ class FileModel(qt.QAbstractItemModel): """ sigFileUpdated = qt.pyqtSignal(object) sigFileAppended = qt.pyqtSignal(object) + sigReadFailed = qt.pyqtSignal(object) def __init__(self, parent=None): qt.QAbstractItemModel.__init__(self, parent) self.rootItem = RootItem(['File/Group/Dataset', 'Description', 'Shape', 'DType']) self._idMap = {qt.QModelIndex().internalId(): self.rootItem} + # to warn only once about read error, can be reset intentionally + self._readErrorReported = False def sort(self, column, order): #print("FileModel sort called with ", column, order) @@ -572,7 +575,14 @@ def parent(self, index): return self.createIndex(parent.row, 0, parent) def rowCount(self, index): - return len(self.getProxyFromIndex(index)) + try: + # the `len(self.children)` can fail. + return len(self.getProxyFromIndex(index)) + except Exception: + if not self._readErrorReported: + self._readErrorReported = True + self.sigReadFailed.emit({"event": "readError", "index": index}) + return 0 def openFile(self, filename, weakreference=False): gc.collect() diff --git a/src/PyMca5/PyMcaGui/io/hdf5/QNexusWidget.py b/src/PyMca5/PyMcaGui/io/hdf5/QNexusWidget.py index a9425e10d..044376925 100644 --- a/src/PyMca5/PyMcaGui/io/hdf5/QNexusWidget.py +++ b/src/PyMca5/PyMcaGui/io/hdf5/QNexusWidget.py @@ -492,6 +492,7 @@ def dataSourceDestroyed(weakrefReference): # stays visible and the tree does not collapse. try: newModel = HDF5Widget.FileModel() + newModel.sigReadFailed.connect(self._hdf5ReadFailed) for source in self.data._sourceObjectList: newModel.appendPhynxFile(source, weakreference=True) self._modelDict[ref] = newModel @@ -515,6 +516,32 @@ def dataSourceDestroyed(weakrefReference): if hasattr(self.hdf5Widget, "expandToDepth"): self.hdf5Widget.expandToDepth(0) + def _hdf5ReadFailed(self, ddict): + self._recoverFromReadError(ddict.get("index")) + self._showReadFailedMessage() + + def _recoverFromReadError(self, index): + model = self.hdf5Widget.model() + # Collapse the node that failed to be read + if index is not None and index.isValid(): + self.hdf5Widget.collapse(index) + # A later failed expansion should warn again + if hasattr(model, "_readErrorReported"): + model._readErrorReported = False + + def _showReadFailedMessage(self): + msg = qt.QMessageBox(self) + msg.setIcon(qt.QMessageBox.Warning) + msg.setWindowTitle("Cannot read HDF5 file") + msg.setText("The selected file could not be read.") + msg.setInformativeText( + "It may be being written now. Try again later." \ + "If it is stuck, try to refresh (F5) or close and open the file again.") + # `open()` (not `exec()`) blocks the window but not the code + # `WA_DeleteOnClose` to delete itself on close (to avoid a leak) + msg.setAttribute(qt.Qt.WA_DeleteOnClose) + msg.open() + def _autoRefreshDatasets(self, source=None, moveToLastSlice=True): """ Auto-refresh: re-read datasets and re-plot without re-building the tree. diff --git a/src/PyMca5/PyMcaGui/pymca/QDispatcher.py b/src/PyMca5/PyMcaGui/pymca/QDispatcher.py index 244693dce..f8ba25168 100644 --- a/src/PyMca5/PyMcaGui/pymca/QDispatcher.py +++ b/src/PyMca5/PyMcaGui/pymca/QDispatcher.py @@ -365,23 +365,30 @@ def _sourceSelectorSlot(self, ddict): if not found: _logger.debug("WARNING: source not found") return - sourceType = source.sourceType - del self.sourceList[self.sourceList.index(source)] - for source in self.sourceList: - if sourceType == source.sourceType: + closedSource = source + sourceType = closedSource.sourceType + del self.sourceList[self.sourceList.index(closedSource)] + try: + for source in self.sourceList: + if sourceType == source.sourceType: + self.selectorWidget[sourceType].setDataSource(source) + self.tabWidget.setCurrentWidget(self.selectorWidget[sourceType]) + return + #there is no other selection of that type + if len(self.sourceList): + source = self.sourceList[0] + sourceType = source.sourceType self.selectorWidget[sourceType].setDataSource(source) - self.tabWidget.setCurrentWidget(self.selectorWidget[sourceType]) - return - #there is no other selection of that type - if len(self.sourceList): - source = self.sourceList[0] - sourceType = source.sourceType - self.selectorWidget[sourceType].setDataSource(source) - else: - self.selectorWidget[sourceType].setDataSource(None) - self.tabWidget.setCurrentWidget(self.selectorWidget[sourceType]) - elif ddict["event"] == "SourceClosed": - _logger.debug("not implemented yet") + else: + self.selectorWidget[sourceType].setDataSource(None) + self.tabWidget.setCurrentWidget(self.selectorWidget[sourceType]) + finally: + # Without this the file stays "open" in the process + # and could not be reopened until PyMca restarted. + try: + closedSource.close() + except Exception as e: + _logger.debug("Error closing source: %s", e) def _selectionUpdatedSlot(self, ddict): _logger.debug("_selectionUpdatedSlot(self, dict=%s)", ddict)