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
|
2016-04-05 17:17:02 +08:00
|
|
|
from PyQt5 import QtCore, QtWidgets, QtGui
|
2016-04-04 23:37:51 +08:00
|
|
|
|
2016-05-26 00:46:21 +08:00
|
|
|
from artiq.protocols import pyon
|
|
|
|
|
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")
|
2016-06-15 22:54:06 +08:00
|
|
|
except OSError: # e.g. file being written (see #470)
|
2016-06-16 17:50:09 +08:00
|
|
|
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.value)
|
|
|
|
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
|
2016-04-21 01:16:19 +08:00
|
|
|
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.IconMode)
|
|
|
|
w = self._char_width*self.default_size
|
|
|
|
self.setIconSize(QtCore.QSize(w, w*self.aspect))
|
|
|
|
self.setFlow(self.LeftToRight)
|
|
|
|
self.setResizeMode(self.Adjust)
|
|
|
|
self.setWrapping(True)
|
|
|
|
|
|
|
|
def wheelEvent(self, ev):
|
|
|
|
if ev.modifiers() & QtCore.Qt.ControlModifier:
|
|
|
|
a = self._char_width*self.min_size
|
|
|
|
b = self._char_width*self.max_size
|
2016-04-21 01:16:19 +08:00
|
|
|
w = self.iconSize().width()*self.zoom_step**(
|
|
|
|
ev.angleDelta().y()/120.)
|
2016-04-21 00:11:04 +08:00
|
|
|
if a <= w <= b:
|
|
|
|
self.setIconSize(QtCore.QSize(w, w*self.aspect))
|
|
|
|
else:
|
|
|
|
QtWidgets.QListView.wheelEvent(self, ev)
|
|
|
|
|
|
|
|
|
2016-05-26 00:46:21 +08:00
|
|
|
class Hdf5FileSystemModel(QtWidgets.QFileSystemModel):
|
|
|
|
def __init__(self):
|
|
|
|
QtWidgets.QFileSystemModel.__init__(self)
|
|
|
|
self.setFilter(QtCore.QDir.Drives | QtCore.QDir.NoDotAndDotDot |
|
|
|
|
QtCore.QDir.AllDirs | QtCore.QDir.Files)
|
|
|
|
self.setNameFilterDisables(False)
|
|
|
|
self.setIconProvider(ThumbnailIconProvider())
|
|
|
|
|
|
|
|
def data(self, idx, role):
|
|
|
|
if role == QtCore.Qt.ToolTipRole:
|
|
|
|
info = self.fileInfo(idx)
|
|
|
|
h5 = open_h5(info)
|
|
|
|
if h5 is not None:
|
|
|
|
try:
|
|
|
|
expid = pyon.decode(h5["expid"].value)
|
|
|
|
start_time = datetime.fromtimestamp(h5["start_time"].value)
|
2016-05-26 04:21:07 +08:00
|
|
|
v = ("artiq_version: {}\nrepo_rev: {}\nfile: {}\n"
|
|
|
|
"class_name: {}\nrid: {}\nstart_time: {}").format(
|
2016-05-26 00:55:39 +08:00
|
|
|
h5["artiq_version"].value, expid["repo_rev"],
|
|
|
|
expid["file"], expid["class_name"],
|
|
|
|
h5["rid"].value, 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 QtWidgets.QFileSystemModel.data(self, idx, role)
|
|
|
|
|
|
|
|
|
2016-04-14 17:55:44 +08:00
|
|
|
class FilesDock(QtWidgets.QDockWidget):
|
2016-05-05 06:44:42 +08:00
|
|
|
def __init__(self, datasets, browse_root="", select=None):
|
2016-04-14 17:55:44 +08:00
|
|
|
QtWidgets.QDockWidget.__init__(self, "Files")
|
|
|
|
self.setObjectName("Files")
|
2016-04-20 03:59:02 +08:00
|
|
|
self.setFeatures(self.DockWidgetMovable | self.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)
|
2016-04-21 17:54:57 +08:00
|
|
|
if browse_root != "":
|
|
|
|
browse_root = os.path.abspath(browse_root)
|
2016-04-17 16:34:10 +08:00
|
|
|
self.rt.setRootIndex(rt_model.mapFromSource(
|
2016-04-21 17:54:57 +08:00
|
|
|
self.model.setRootPath(browse_root)))
|
|
|
|
self.rt.setHeaderHidden(True)
|
2016-04-17 16:34:10 +08:00
|
|
|
self.rt.setSelectionBehavior(self.rt.SelectRows)
|
|
|
|
self.rt.setSelectionMode(self.rt.SingleSelection)
|
2016-04-06 15:45:13 +08:00
|
|
|
self.rt.selectionModel().currentChanged.connect(
|
2016-04-06 18:16:40 +08:00
|
|
|
self.tree_current_changed)
|
2016-04-20 19:20:33 +08:00
|
|
|
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-05 17:17:02 +08:00
|
|
|
|
2016-04-21 01:16:19 +08:00
|
|
|
self.restore_selected = select is None
|
|
|
|
if select is not None:
|
|
|
|
f = os.path.abspath(select)
|
|
|
|
if os.path.isdir(f):
|
|
|
|
self.select_dir(f)
|
|
|
|
else:
|
|
|
|
self.select_file(f)
|
|
|
|
|
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:
|
2016-04-20 16:59:45 +08:00
|
|
|
if "datasets" not in f:
|
2016-04-06 18:16:40 +08:00
|
|
|
return
|
2016-04-27 19:21:15 +08:00
|
|
|
rd = {k: (True, v.value) for k, v in f["datasets"].items()}
|
2016-04-06 18:16:40 +08:00
|
|
|
self.datasets.init(rd)
|
2016-04-04 23:37:51 +08:00
|
|
|
|
2016-05-16 17:45:13 +08:00
|
|
|
def list_activated(self, idx):
|
|
|
|
if not self.model.fileInfo(idx).isDir():
|
|
|
|
return
|
|
|
|
self.rl.setRootIndex(idx)
|
|
|
|
idx = self.rt.model().mapFromSource(idx)
|
|
|
|
self.rt.expand(idx)
|
|
|
|
self.rt.setCurrentIndex(idx)
|
|
|
|
|
2016-04-17 16:34:10 +08:00
|
|
|
def select_dir(self, path):
|
|
|
|
if not os.path.exists(path):
|
|
|
|
return
|
|
|
|
idx = self.model.index(path)
|
2016-04-20 16:37:02 +08:00
|
|
|
if not idx.isValid():
|
|
|
|
return
|
2016-04-17 16:34:10 +08:00
|
|
|
self.rl.setRootIndex(idx)
|
|
|
|
|
2016-04-21 01:16:19 +08:00
|
|
|
# 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(
|
2016-04-21 01:16:19 +08:00
|
|
|
self.rt.model().mapFromSource(self.model.index(path)),
|
2016-04-27 19:21:15 +08:00
|
|
|
self.rt.PositionAtCenter)
|
|
|
|
)
|
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)
|
2016-04-20 19:20:33 +08:00
|
|
|
self.rt.setCurrentIndex(idx)
|
2016-04-17 16:34:10 +08:00
|
|
|
|
|
|
|
def select_file(self, path):
|
|
|
|
if not os.path.exists(path):
|
|
|
|
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():
|
|
|
|
return
|
|
|
|
self.rl.setCurrentIndex(idx)
|
2016-04-04 23:37:51 +08:00
|
|
|
|
|
|
|
def save_state(self):
|
|
|
|
return {
|
2016-04-20 16:37:02 +08:00
|
|
|
"dir": self.model.filePath(self.rl.rootIndex()),
|
2016-04-17 16:34:10 +08:00
|
|
|
"file": self.model.filePath(self.rl.currentIndex()),
|
2016-04-10 16:22:24 +08:00
|
|
|
"splitter": bytes(self.splitter.saveState()),
|
2016-04-04 23:37:51 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
def restore_state(self, state):
|
2016-04-21 01:16:19 +08:00
|
|
|
if self.restore_selected:
|
2016-04-20 19:57:12 +08:00
|
|
|
self.select_dir(state["dir"])
|
|
|
|
self.select_file(state["file"])
|
|
|
|
self.splitter.restoreState(QtCore.QByteArray(state["splitter"]))
|