Skip to content

Commit da4d3af

Browse files
Merge pull request #1211 from silx-kit/refresh_btn
Refresh button rethink (for hdf5) + auto-refresh (for hdf5)
2 parents 9455b9d + 6ded730 commit da4d3af

6 files changed

Lines changed: 317 additions & 28 deletions

File tree

src/PyMca5/PyMcaCore/NexusDataSource.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -198,15 +198,15 @@ def refresh(self):
198198
try:
199199
phynxInstance = h5open(name)
200200
except IOError:
201-
if 'FAMILY DRIVER' in sys.exc_info()[1].args[0].upper():
201+
if 'FAMILY DRIVER' in str(sys.exc_info()[1]).upper():
202202
FAMILY = True
203203
else:
204204
raise
205205
except TypeError:
206206
try:
207207
phynxInstance = h5open(name)
208208
except IOError:
209-
if 'FAMILY DRIVER' in sys.exc_info()[1].args[0].upper():
209+
if 'FAMILY DRIVER' in str(sys.exc_info()[1]).upper():
210210
FAMILY = True
211211
else:
212212
raise
@@ -627,7 +627,7 @@ def getDataObject(self, key, selection=None):
627627
if output.m:
628628
for mi in range(len(output.m)):
629629
mlength = output.m[mi].size
630-
delta = max(delta, ylength - mlength)
630+
delta = max(delta, abs(ylength - mlength))
631631
length = min(length, mlength)
632632
if delta > 1:
633633
_logger.warning("Stripping last %d points" % delta)

src/PyMca5/PyMcaGui/io/QSourceSelector.py

Lines changed: 58 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ def __init__(self, parent=None, filetypelist=None, pluginsIcon=False):
9797

9898
refreshButton= qt.QPushButton(self.fileWidget)
9999
refreshButton.setIcon(self.reloadIcon)
100-
refreshButton.setToolTip("Refresh data source")
100+
refreshButton.setToolTip("Refresh data source (F5)")
101101

102102
specButton= qt.QPushButton(self.fileWidget)
103103
specButton.setIcon(self.specIcon)
@@ -106,13 +106,29 @@ def __init__(self, parent=None, filetypelist=None, pluginsIcon=False):
106106
else:
107107
specButton.setToolTip("Open new shared memory source")
108108

109+
self.autoRefreshCheckBox = qt.QCheckBox("Auto", self.fileWidget)
110+
self.autoRefreshCheckBox.setToolTip(
111+
"Automatically refresh HDF5 files every second\n"
112+
"to see new appended data.\n" \
113+
"New datasets will not appear until manual refresh (F5).")
114+
115+
self._autoRefreshTimer = qt.QTimer(self)
116+
self._autoRefreshTimer.setInterval(1000)
117+
109118
closeButton.setSizePolicy(qt.QSizePolicy(qt.QSizePolicy.Fixed, qt.QSizePolicy.Minimum))
110119
specButton.setSizePolicy(qt.QSizePolicy(qt.QSizePolicy.Fixed, qt.QSizePolicy.Minimum))
111120
refreshButton.setSizePolicy(qt.QSizePolicy(qt.QSizePolicy.Fixed, qt.QSizePolicy.Minimum))
121+
self.autoRefreshCheckBox.setSizePolicy(qt.QSizePolicy(qt.QSizePolicy.Fixed, qt.QSizePolicy.Minimum))
112122

113123
openButton.clicked.connect(self._openFileSlot)
114124
closeButton.clicked.connect(self.closeFile)
115125
refreshButton.clicked.connect(self._reload)
126+
self.f5Shortcut = qt.QShortcut(qt.QKeySequence(qt.Qt.Key_F5), self, self._reload)
127+
self.autoRefreshCheckBox.toggled.connect(self._autoRefreshToggled)
128+
self._autoRefreshTimer.timeout.connect(self._autoRefreshTimeout)
129+
130+
# For `QDispatcher` to disable during refresh
131+
self.refreshButton = refreshButton
116132

117133
specButton.clicked.connect(self.openBlissOrSpec)
118134
if hasattr(self.fileCombo, "textActivated"):
@@ -127,6 +143,12 @@ def __init__(self, parent=None, filetypelist=None, pluginsIcon=False):
127143
fileWidgetLayout.addWidget(specButton)
128144
if sys.platform == "win32":specButton.hide()
129145
fileWidgetLayout.addWidget(refreshButton)
146+
fileWidgetLayout.addWidget(self.autoRefreshCheckBox)
147+
# if sys.platform == "win32":self.autoRefreshCheckBox.hide()
148+
# Needed because on Windows hdf5 locking system is "broken"
149+
# Commented for testing on Windows
150+
151+
130152
self.specButton = specButton
131153
if pluginsIcon:
132154
self.pluginsButton = qt.QPushButton(self.fileWidget)
@@ -136,19 +158,49 @@ def __init__(self, parent=None, filetypelist=None, pluginsIcon=False):
136158
fileWidgetLayout.addWidget(self.pluginsButton)
137159
self.mainLayout.addWidget(self.fileWidget)
138160

139-
def _reload(self):
161+
def _reload(self, autorefresh=False):
140162
_logger.debug("_reload called")
141163
qstring = self.fileCombo.currentText()
142164
if not len(qstring):
143165
return
144-
145166
key = qt.safe_str(qstring)
146167
ddict = {}
147-
ddict["event"] = "SourceReloaded"
148168
ddict["combokey"] = key
149169
ddict["sourcelist"] = self.mapCombo[key] * 1
170+
if autorefresh:
171+
ddict["event"] = "SourceAutoRefreshed"
172+
else:
173+
ddict["event"] = "SourceReloaded"
150174
self.sigSourceSelectorSignal.emit(ddict)
151175

176+
def _isSourceHDF5(self, sourcelist):
177+
HDF5_EXTENSIONS = (".h5", ".hdf5", ".hdf", ".nxs", ".nx")
178+
if not sourcelist:
179+
return False
180+
return all(source.lower().endswith(HDF5_EXTENSIONS) for source in sourcelist)
181+
182+
def _updateAutoRefreshVisibility(self, sourcelist):
183+
"""Auto checkbox is only for HDF5 sources."""
184+
isHDF5 = self._isSourceHDF5(sourcelist)
185+
self.autoRefreshCheckBox.setEnabled(isHDF5)
186+
if not isHDF5 and self.autoRefreshCheckBox.isChecked():
187+
self.autoRefreshCheckBox.setChecked(False)
188+
189+
def _autoRefreshToggled(self, checked):
190+
if checked:
191+
_logger.info("Auto-refresh started")
192+
self._autoRefreshTimer.start()
193+
else:
194+
_logger.info("Auto-refresh stopped")
195+
self._autoRefreshTimer.stop()
196+
197+
def _autoRefreshTimeout(self):
198+
"""Pause the timer to prevent queuing."""
199+
self._autoRefreshTimer.stop()
200+
self._reload(autorefresh=True)
201+
if self.autoRefreshCheckBox.isChecked():
202+
self._autoRefreshTimer.start()
203+
152204
def _openFileSlot(self):
153205
self.openFile(None, None)
154206

@@ -271,6 +323,7 @@ def _emitSourceSelectedOrReloaded(self, filename, key, filefilter=None):
271323
else:
272324
nitem = self.fileCombo.findText(key)
273325
self.fileCombo.setCurrentIndex(nitem)
326+
self._updateAutoRefreshVisibility(ddict["sourcelist"])
274327
self.sigSourceSelectorSignal.emit(ddict)
275328

276329
def closeFile(self):
@@ -345,6 +398,7 @@ def _fileSelection(self, qstring):
345398
ddict["event"] = "SourceSelected"
346399
ddict["combokey"] = key
347400
ddict["sourcelist"] = self.mapCombo[key]
401+
self._updateAutoRefreshVisibility(ddict["sourcelist"])
348402
self.sigSourceSelectorSignal.emit(ddict)
349403

350404
def test():

src/PyMca5/PyMcaGui/io/hdf5/HDF5Widget.py

Lines changed: 112 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -406,11 +406,17 @@ def raw_keys(self):
406406
return self.file[data_path].keys()
407407
else:
408408
from PyMca5.PyMcaIO import HDF5Utils
409-
return HDF5Utils.safe_hdf5_group_keys(file_path,
409+
result = HDF5Utils.safe_hdf5_group_keys(file_path,
410410
data_path=data_path)
411+
if result:
412+
return result
413+
_logger.debug("Subprocess returned empty.")
414+
# It could be a really empty or a silent failure (file locked by writer).
415+
# becasue `safe_hdf5_group_keys (..., default=list())`.
416+
# If it is empty standard approach will also return empty.
411417
except Exception:
412-
_logger.debug("Using standard approach")
413-
return self.file[data_path].keys()
418+
_logger.debug("Trying standard approach")
419+
return self.file[data_path].keys()
414420
else:
415421
file_path = self.file.filename
416422
data_path = self.name
@@ -666,6 +672,26 @@ def clear(self):
666672
# rootItem.deleteChild(child)
667673
rootItem.children.clear()
668674
self.endResetModel()
675+
676+
def swapFileHandles(self, handleMap):
677+
"""Replace ``_file`` on loaded proxy nodes to point at fresh handles."""
678+
if not handleMap:
679+
return
680+
for child in self.rootItem._children:
681+
try:
682+
oldName = child._file._sourceName
683+
except (ReferenceError, AttributeError, ValueError):
684+
continue
685+
fresh = handleMap.get(oldName)
686+
if fresh is None:
687+
continue
688+
self._swapNodeFile(child, fresh)
689+
690+
def _swapNodeFile(self, node, freshHandle):
691+
"""Replace ``_file`` on node and its children."""
692+
node._file = freshHandle
693+
for child in node._children:
694+
self._swapNodeFile(child, freshHandle)
669695

670696
class FileView(qt.QTreeView):
671697

@@ -707,6 +733,7 @@ def fileUpdated(self, ddict):
707733
class HDF5Widget(FileView):
708734
def __init__(self, model, parent=None, multi_selection=False):
709735
FileView.__init__(self, model, parent)
736+
self._savedTreeState = None
710737
self.setSelectionBehavior(qt.QAbstractItemView.SelectRows)
711738
if multi_selection:
712739
self.setSelectionMode(qt.QAbstractItemView.ExtendedSelection)
@@ -848,9 +875,90 @@ def getSelectedEntries(self):
848875
entry = "/" + path.split("/")[1]
849876
if (entry, filename) not in entryList:
850877
entryList.append((entry, filename))
851-
_logger.info("Returned entryList %s" % entryList)
878+
# `debug` instead of `info` to avoid spam during `auto-refresh`
879+
_logger.debug("Returned entryList %s" % entryList)
852880
return entryList
853881

882+
def saveTreeState(self):
883+
"""Save and return expanded and selected sets in HDF5 tree."""
884+
expanded = set()
885+
selected = set()
886+
model = self.model()
887+
if model is None:
888+
self._savedTreeState = (expanded, selected)
889+
return self._savedTreeState
890+
rootIndex = self.rootIndex()
891+
self._collectExpandedPaths(model, rootIndex, expanded)
892+
for modelIndex in self.selectedIndexes():
893+
if modelIndex.column() != 0:
894+
continue
895+
item = model.getProxyFromIndex(modelIndex)
896+
try:
897+
selected.add((item.file.filename, item.name))
898+
except Exception:
899+
continue
900+
self._savedTreeState = (expanded, selected)
901+
return self._savedTreeState
902+
903+
def _collectExpandedPaths(self, model, parentIndex, expanded):
904+
"""Recursively collect expanded-node."""
905+
for row in range(model.rowCount(parentIndex)):
906+
if not model.hasIndex(row, 0, parentIndex):
907+
continue
908+
index = model.index(row, 0, parentIndex)
909+
if not self.isExpanded(index):
910+
continue
911+
item = model.getProxyFromIndex(index)
912+
try:
913+
expanded.add((item.file.filename, item.name))
914+
except Exception:
915+
continue
916+
self._collectExpandedPaths(model, index, expanded)
917+
918+
def hasSavedTreeState(self):
919+
return self._savedTreeState is not None
920+
921+
def clearSavedTreeState(self):
922+
self._savedTreeState = None
923+
924+
def restoreTreeState(self, state=None):
925+
"""Re-expand and re-select nodes from (saved) state."""
926+
if state is None:
927+
state = self._savedTreeState
928+
self._savedTreeState = None
929+
if state is None:
930+
return
931+
expandedPaths, selectedPaths = state
932+
model = self.model()
933+
if model is None:
934+
return
935+
if expandedPaths or selectedPaths:
936+
rootIndex = self.rootIndex()
937+
selModel = self.selectionModel()
938+
self._restoreTreeNodes(model, rootIndex, expandedPaths,
939+
selectedPaths, selModel)
940+
941+
def _restoreTreeNodes(self, model, parentIndex,
942+
expandPaths, selectPaths, selModel):
943+
for row in range(model.rowCount(parentIndex)):
944+
if not model.hasIndex(row, 0, parentIndex):
945+
continue
946+
index = model.index(row, 0, parentIndex)
947+
item = model.getProxyFromIndex(index)
948+
try:
949+
key = (item.file.filename, item.name)
950+
except Exception:
951+
continue
952+
if key in selectPaths:
953+
selModel.select(
954+
index,
955+
qt.QItemSelectionModel.Select |
956+
qt.QItemSelectionModel.Rows)
957+
if key in expandPaths:
958+
self.setExpanded(index, True)
959+
self._restoreTreeNodes(model, index, expandPaths,
960+
selectPaths, selModel)
961+
854962

855963
class Hdf5SelectionDialog(qt.QDialog):
856964
"""Dialog widget to select a HDF5 item in a file.

0 commit comments

Comments
 (0)