artiq/artiq/browser/files.py

290 lines
11 KiB
Python
Raw Normal View History

2016-04-04 23:37:51 +08:00
import logging
2016-04-06 22:15:51 +08:00
import os
2016-05-26 00:46:21 +08:00
from datetime import datetime
2016-04-04 23:37:51 +08:00
2016-04-05 15:51:04 +08:00
import h5py
from PyQt6 import QtCore, QtWidgets, QtGui
2016-04-04 23:37:51 +08:00
2019-11-10 15:55:17 +08:00
from sipyco import pyon
2016-05-26 00:46:21 +08:00
2016-04-04 23:37:51 +08:00
logger = logging.getLogger(__name__)
2016-04-18 23:10:40 +08:00
def open_h5(info):
if not (info.isFile() and info.isReadable() and
info.suffix() == "h5"):
return
try:
2016-05-04 00:07:43 +08:00
return h5py.File(info.filePath(), "r")
except OSError: # e.g. file being written (see #470)
logger.debug("OSError when opening HDF5 file %s", info.filePath(),
exc_info=True)
2016-04-18 23:10:40 +08:00
except:
logger.warning("unable to read HDF5 file %s", info.filePath(),
exc_info=True)
2016-04-17 16:34:10 +08:00
class ThumbnailIconProvider(QtWidgets.QFileIconProvider):
2016-04-06 02:01:25 +08:00
def icon(self, info):
2016-04-06 18:16:40 +08:00
icon = self.hdf5_thumbnail(info)
if icon is None:
icon = QtWidgets.QFileIconProvider.icon(self, info)
return icon
def hdf5_thumbnail(self, info):
2016-04-18 23:10:40 +08:00
f = open_h5(info)
if not f:
2016-04-08 11:44:37 +08:00
return
with f:
try:
t = f["datasets/thumbnail"]
except KeyError:
return
try:
img = QtGui.QImage.fromData(t[()])
2016-04-08 11:44:37 +08:00
except:
2016-04-17 16:34:10 +08:00
logger.warning("unable to read thumbnail from %s",
info.filePath(), exc_info=True)
2016-04-06 18:16:40 +08:00
return
pix = QtGui.QPixmap.fromImage(img)
return QtGui.QIcon(pix)
2016-04-17 16:34:10 +08:00
class DirsOnlyProxy(QtCore.QSortFilterProxyModel):
def filterAcceptsRow(self, row, parent):
idx = self.sourceModel().index(row, 0, parent)
if not self.sourceModel().fileInfo(idx).isDir():
return False
return QtCore.QSortFilterProxyModel.filterAcceptsRow(self, row, parent)
2016-04-21 00:11:04 +08:00
class ZoomIconView(QtWidgets.QListView):
zoom_step = 2**.25
aspect = 2/3
default_size = 25
min_size = 10
max_size = 1000
2016-04-21 00:11:04 +08:00
def __init__(self):
QtWidgets.QListView.__init__(self)
self._char_width = QtGui.QFontMetrics(self.font()).averageCharWidth()
self.setViewMode(self.ViewMode.IconMode)
2016-04-21 00:11:04 +08:00
w = self._char_width*self.default_size
self.setIconSize(QtCore.QSize(w, int(w*self.aspect)))
self.setFlow(self.Flow.LeftToRight)
self.setResizeMode(self.ResizeMode.Adjust)
2016-04-21 00:11:04 +08:00
self.setWrapping(True)
def wheelEvent(self, ev):
if ev.modifiers() & QtCore.Qt.KeyboardModifier.ControlModifier:
2016-04-21 00:11:04 +08:00
a = self._char_width*self.min_size
b = self._char_width*self.max_size
w = self.iconSize().width()*self.zoom_step**(
ev.angleDelta().y()/120.)
2016-04-21 00:11:04 +08:00
if a <= w <= b:
2024-07-10 15:47:32 +08:00
self.setIconSize(QtCore.QSize(int(w), int(w*self.aspect)))
2016-04-21 00:11:04 +08:00
else:
QtWidgets.QListView.wheelEvent(self, ev)
class Hdf5FileSystemModel(QtGui.QFileSystemModel):
2016-05-26 00:46:21 +08:00
def __init__(self):
QtGui.QFileSystemModel.__init__(self)
self.setFilter(QtCore.QDir.Filter.Drives | QtCore.QDir.Filter.NoDotAndDotDot |
QtCore.QDir.Filter.AllDirs | QtCore.QDir.Filter.Files)
2016-05-26 00:46:21 +08:00
self.setNameFilterDisables(False)
self.setIconProvider(ThumbnailIconProvider())
def data(self, idx, role):
if role == QtCore.Qt.ItemDataRole.ToolTipRole:
2016-05-26 00:46:21 +08:00
info = self.fileInfo(idx)
h5 = open_h5(info)
if h5 is not None:
try:
expid = pyon.decode(h5["expid"][()]) if "expid" in h5 else dict()
start_time = datetime.fromtimestamp(h5["start_time"][()]) if "start_time" in h5 else "<none>"
v = ("artiq_version: {}\nrepo_rev: {}\nfile: {}\n"
"class_name: {}\nrid: {}\nstart_time: {}").format(
h5["artiq_version"].asstr()[()] if "artiq_version" in h5 else "<none>",
expid.get("repo_rev", "<none>"),
expid.get("file", "<none>"), expid.get("class_name", "<none>"),
h5["rid"][()] if "rid" in h5 else "<none>", start_time)
2016-05-26 00:46:21 +08:00
return v
except:
logger.warning("unable to read metadata from %s",
info.filePath(), exc_info=True)
return QtGui.QFileSystemModel.data(self, idx, role)
2016-05-26 00:46:21 +08:00
2016-04-14 17:55:44 +08:00
class FilesDock(QtWidgets.QDockWidget):
dataset_activated = QtCore.pyqtSignal(str)
dataset_changed = QtCore.pyqtSignal(str)
metadata_changed = QtCore.pyqtSignal(dict)
def __init__(self, datasets, browse_root=""):
2016-04-14 17:55:44 +08:00
QtWidgets.QDockWidget.__init__(self, "Files")
self.setObjectName("Files")
self.setFeatures(self.DockWidgetFeature.DockWidgetMovable | self.DockWidgetFeature.DockWidgetFloatable)
2016-04-10 16:22:24 +08:00
self.splitter = QtWidgets.QSplitter()
self.setWidget(self.splitter)
2016-04-05 15:51:04 +08:00
self.datasets = datasets
2016-05-26 00:46:21 +08:00
self.model = Hdf5FileSystemModel()
2016-04-04 23:37:51 +08:00
self.rt = QtWidgets.QTreeView()
2016-04-17 16:34:10 +08:00
rt_model = DirsOnlyProxy()
rt_model.setDynamicSortFilter(True)
rt_model.setSourceModel(self.model)
self.rt.setModel(rt_model)
self.model.directoryLoaded.connect(
lambda: self.rt.resizeColumnToContents(0))
self.rt.setAnimated(False)
if browse_root != "":
browse_root = os.path.abspath(browse_root)
2016-04-17 16:34:10 +08:00
self.rt.setRootIndex(rt_model.mapFromSource(
self.model.setRootPath(browse_root)))
self.rt.setHeaderHidden(True)
self.rt.setSelectionBehavior(self.rt.SelectionBehavior.SelectRows)
self.rt.setSelectionMode(self.rt.SelectionMode.SingleSelection)
self.rt.selectionModel().currentChanged.connect(
2016-04-06 18:16:40 +08:00
self.tree_current_changed)
self.rt.setRootIsDecorated(False)
2016-04-17 16:34:10 +08:00
for i in range(1, 4):
self.rt.hideColumn(i)
2016-04-10 16:22:24 +08:00
self.splitter.addWidget(self.rt)
2016-04-05 16:05:53 +08:00
2016-04-21 00:11:04 +08:00
self.rl = ZoomIconView()
2016-04-17 16:34:10 +08:00
self.rl.setModel(self.model)
self.rl.selectionModel().currentChanged.connect(
self.list_current_changed)
2016-05-16 17:45:13 +08:00
self.rl.activated.connect(self.list_activated)
2016-04-10 16:22:24 +08:00
self.splitter.addWidget(self.rl)
2016-04-06 18:16:40 +08:00
def tree_current_changed(self, current, previous):
2016-04-17 16:34:10 +08:00
idx = self.rt.model().mapToSource(current)
self.rl.setRootIndex(idx)
2016-04-06 18:16:40 +08:00
def list_current_changed(self, current, previous):
2016-04-17 16:34:10 +08:00
info = self.model.fileInfo(current)
2016-04-18 23:10:40 +08:00
f = open_h5(info)
if not f:
2016-04-08 11:54:49 +08:00
return
2016-06-12 13:11:36 +08:00
logger.debug("loading datasets from %s", info.filePath())
2016-04-08 11:44:37 +08:00
with f:
try:
expid = pyon.decode(f["expid"][()]) if "expid" in f else dict()
start_time = datetime.fromtimestamp(f["start_time"][()]) if "start_time" in f else "<none>"
v = {
"artiq_version": f["artiq_version"].asstr()[()] if "artiq_version" in f else "<none>",
"repo_rev": expid.get("repo_rev", "<none>"),
"file": expid.get("file", "<none>"),
"class_name": expid.get("class_name", "<none>"),
"rid": f["rid"][()] if "rid" in f else "<none>",
"start_time": start_time,
}
self.metadata_changed.emit(v)
except:
logger.warning("unable to read metadata from %s",
info.filePath(), exc_info=True)
rd = {}
if "archive" in f:
def visitor(k, v):
if isinstance(v, h5py.Dataset):
2023-07-10 10:52:47 +08:00
# v.attrs is a non-serializable h5py.AttributeManager, need to convert to dict
# See https://docs.h5py.org/en/stable/high/attr.html#h5py.AttributeManager
rd[k] = (True, v[()], dict(v.attrs))
f["archive"].visititems(visitor)
if "datasets" in f:
def visitor(k, v):
if isinstance(v, h5py.Dataset):
if k in rd:
logger.warning("dataset '%s' is both in archive "
"and outputs", k)
2023-07-10 10:52:47 +08:00
# v.attrs is a non-serializable h5py.AttributeManager, need to convert to dict
# See https://docs.h5py.org/en/stable/high/attr.html#h5py.AttributeManager
rd[k] = (True, v[()], dict(v.attrs))
f["datasets"].visititems(visitor)
self.datasets.init(rd)
self.dataset_changed.emit(info.filePath())
2016-04-04 23:37:51 +08:00
2016-05-16 17:45:13 +08:00
def list_activated(self, idx):
info = self.model.fileInfo(idx)
if not info.isDir():
self.dataset_activated.emit(info.filePath())
2016-05-16 17:45:13 +08:00
return
self.rl.setRootIndex(idx)
idx = self.rt.model().mapFromSource(idx)
self.rt.expand(idx)
self.rt.setCurrentIndex(idx)
def select(self, path):
f = os.path.abspath(path)
if os.path.isdir(f):
self.select_dir(f)
else:
self.select_file(f)
2016-04-17 16:34:10 +08:00
def select_dir(self, path):
if not os.path.exists(path):
logger.warning("directory does not exist %s", path)
2016-04-17 16:34:10 +08:00
return
idx = self.model.index(path)
2016-04-20 16:37:02 +08:00
if not idx.isValid():
logger.warning("directory invalid %s", path)
2016-04-20 16:37:02 +08:00
return
2016-04-17 16:34:10 +08:00
self.rl.setRootIndex(idx)
# ugly, see Spyder: late indexing, late scroll
2016-04-17 16:34:10 +08:00
def scroll_when_loaded(p):
if p != path:
return
self.model.directoryLoaded.disconnect(scroll_when_loaded)
QtCore.QTimer.singleShot(
2016-04-27 19:21:15 +08:00
100,
lambda: self.rt.scrollTo(
self.rt.model().mapFromSource(self.model.index(path)),
self.rt.ScrollHint.PositionAtCenter)
2016-04-27 19:21:15 +08:00
)
2016-04-17 16:34:10 +08:00
self.model.directoryLoaded.connect(scroll_when_loaded)
2016-04-20 03:59:02 +08:00
idx = self.rt.model().mapFromSource(idx)
self.rt.expand(idx)
self.rt.setCurrentIndex(idx)
2016-04-17 16:34:10 +08:00
def select_file(self, path):
if not os.path.exists(path):
logger.warning("file does not exist %s", path)
2016-04-17 16:34:10 +08:00
return
self.select_dir(os.path.dirname(path))
2016-04-20 16:37:02 +08:00
idx = self.model.index(path)
if not idx.isValid():
logger.warning("file invalid %s", path)
2016-04-20 16:37:02 +08:00
return
self.rl.setCurrentIndex(idx)
2016-04-04 23:37:51 +08:00
def save_state(self):
state = {
2016-04-20 16:37:02 +08:00
"dir": self.model.filePath(self.rl.rootIndex()),
2016-04-10 16:22:24 +08:00
"splitter": bytes(self.splitter.saveState()),
2016-04-04 23:37:51 +08:00
}
idx = self.rl.currentIndex()
if idx.isValid():
state["file"] = self.model.filePath(idx)
else:
state["file"] = None
return state
2016-04-04 23:37:51 +08:00
def restore_state(self, state):
self.splitter.restoreState(QtCore.QByteArray(state["splitter"]))
self.select_dir(state["dir"])
if state["file"] is not None:
self.select_file(state["file"])