From f7bdc2acff3c13a6d632c28c4569690ab106eed7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Picca=20Fr=C3=A9d=C3=A9ric-Emmanuel?= Date: Fri, 18 Aug 2017 14:48:52 +0200 Subject: Import Upstream version 0.5.0+dfsg --- silx/gui/hdf5/Hdf5HeaderView.py | 192 ++++++++++ silx/gui/hdf5/Hdf5Item.py | 421 +++++++++++++++++++++ silx/gui/hdf5/Hdf5LoadingItem.py | 68 ++++ silx/gui/hdf5/Hdf5Node.py | 210 +++++++++++ silx/gui/hdf5/Hdf5TreeModel.py | 581 +++++++++++++++++++++++++++++ silx/gui/hdf5/Hdf5TreeView.py | 204 ++++++++++ silx/gui/hdf5/NexusSortFilterProxyModel.py | 152 ++++++++ silx/gui/hdf5/__init__.py | 44 +++ silx/gui/hdf5/_utils.py | 247 ++++++++++++ silx/gui/hdf5/setup.py | 41 ++ silx/gui/hdf5/test/__init__.py | 39 ++ silx/gui/hdf5/test/_mock.py | 130 +++++++ silx/gui/hdf5/test/test_hdf5.py | 480 ++++++++++++++++++++++++ 13 files changed, 2809 insertions(+) create mode 100644 silx/gui/hdf5/Hdf5HeaderView.py create mode 100644 silx/gui/hdf5/Hdf5Item.py create mode 100644 silx/gui/hdf5/Hdf5LoadingItem.py create mode 100644 silx/gui/hdf5/Hdf5Node.py create mode 100644 silx/gui/hdf5/Hdf5TreeModel.py create mode 100644 silx/gui/hdf5/Hdf5TreeView.py create mode 100644 silx/gui/hdf5/NexusSortFilterProxyModel.py create mode 100644 silx/gui/hdf5/__init__.py create mode 100644 silx/gui/hdf5/_utils.py create mode 100644 silx/gui/hdf5/setup.py create mode 100644 silx/gui/hdf5/test/__init__.py create mode 100644 silx/gui/hdf5/test/_mock.py create mode 100644 silx/gui/hdf5/test/test_hdf5.py (limited to 'silx/gui/hdf5') diff --git a/silx/gui/hdf5/Hdf5HeaderView.py b/silx/gui/hdf5/Hdf5HeaderView.py new file mode 100644 index 0000000..5912230 --- /dev/null +++ b/silx/gui/hdf5/Hdf5HeaderView.py @@ -0,0 +1,192 @@ +# coding: utf-8 +# /*########################################################################## +# +# Copyright (c) 2016 European Synchrotron Radiation Facility +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# +# ###########################################################################*/ + +__authors__ = ["V. Valls"] +__license__ = "MIT" +__date__ = "08/11/2016" + + +from .. import qt + +QTVERSION = qt.qVersion() + + +class Hdf5HeaderView(qt.QHeaderView): + """ + Default HDF5 header + + Manage auto-resize and context menu to display/hide columns + """ + + def __init__(self, orientation, parent=None): + """ + Constructor + + :param orientation qt.Qt.Orientation: Orientation of the header + :param parent qt.QWidget: Parent of the widget + """ + super(Hdf5HeaderView, self).__init__(orientation, parent) + self.setContextMenuPolicy(qt.Qt.CustomContextMenu) + self.customContextMenuRequested.connect(self.__createContextMenu) + + # default initialization done by QTreeView for it's own header + if QTVERSION < "5.0": + self.setClickable(True) + self.setMovable(True) + else: + self.setSectionsClickable(True) + self.setSectionsMovable(True) + self.setDefaultAlignment(qt.Qt.AlignLeft | qt.Qt.AlignVCenter) + self.setStretchLastSection(True) + + self.__auto_resize = True + self.__hide_columns_popup = True + + def setModel(self, model): + """Override model to configure view when a model is expected + + `qt.QHeaderView.setResizeMode` expect already existing columns + to work. + + :param model qt.QAbstractItemModel: A model + """ + super(Hdf5HeaderView, self).setModel(model) + self.__updateAutoResize() + + def __updateAutoResize(self): + """Update the view according to the state of the auto-resize""" + if QTVERSION < "5.0": + setResizeMode = self.setResizeMode + else: + setResizeMode = self.setSectionResizeMode + + if self.__auto_resize: + setResizeMode(0, qt.QHeaderView.ResizeToContents) + setResizeMode(1, qt.QHeaderView.ResizeToContents) + setResizeMode(2, qt.QHeaderView.ResizeToContents) + setResizeMode(3, qt.QHeaderView.Interactive) + setResizeMode(4, qt.QHeaderView.Interactive) + setResizeMode(5, qt.QHeaderView.ResizeToContents) + else: + setResizeMode(0, qt.QHeaderView.Interactive) + setResizeMode(1, qt.QHeaderView.Interactive) + setResizeMode(2, qt.QHeaderView.Interactive) + setResizeMode(3, qt.QHeaderView.Interactive) + setResizeMode(4, qt.QHeaderView.Interactive) + setResizeMode(5, qt.QHeaderView.Interactive) + + def setAutoResizeColumns(self, autoResize): + """Enable/disable auto-resize. When auto-resized, the header take care + of the content of the column to set fixed size of some of them, or to + auto fix the size according to the content. + + :param autoResize bool: Enable/disable auto-resize + """ + if self.__auto_resize == autoResize: + return + self.__auto_resize = autoResize + self.__updateAutoResize() + + def hasAutoResizeColumns(self): + """Is auto-resize enabled. + + :rtype: bool + """ + return self.__auto_resize + + autoResizeColumns = qt.Property(bool, hasAutoResizeColumns, setAutoResizeColumns) + """Property to enable/disable auto-resize.""" + + def setEnableHideColumnsPopup(self, enablePopup): + """Enable/disable a popup to allow to hide/show each column of the + model. + + :param bool enablePopup: Enable/disable popup to hide/show columns + """ + self.__hide_columns_popup = enablePopup + + def hasHideColumnsPopup(self): + """Is popup to hide/show columns is enabled. + + :rtype: bool + """ + return self.__hide_columns_popup + + enableHideColumnsPopup = qt.Property(bool, hasHideColumnsPopup, setAutoResizeColumns) + """Property to enable/disable popup allowing to hide/show columns.""" + + def __genHideSectionEvent(self, column): + """Generate a callback which change the column visibility according to + the event parameter + + :param int column: logical id of the column + :rtype: callable + """ + return lambda checked: self.setSectionHidden(column, not checked) + + def __createContextMenu(self, pos): + """Callback to create and display a context menu + + :param pos qt.QPoint: Requested position for the context menu + """ + if not self.__hide_columns_popup: + return + + model = self.model() + if model.columnCount() > 1: + menu = qt.QMenu(self) + menu.setTitle("Display/hide columns") + + action = qt.QAction("Display/hide column", self) + action.setEnabled(False) + menu.addAction(action) + + for column in range(model.columnCount()): + if column == 0: + # skip the main column + continue + text = model.headerData(column, qt.Qt.Horizontal, qt.Qt.DisplayRole) + action = qt.QAction("%s displayed" % text, self) + action.setCheckable(True) + action.setChecked(not self.isSectionHidden(column)) + action.toggled.connect(self.__genHideSectionEvent(column)) + menu.addAction(action) + + menu.popup(self.viewport().mapToGlobal(pos)) + + def setSections(self, logicalIndexes): + """ + Defines order of visible sections by logical indexes. + + Use `Hdf5TreeModel.NAME_COLUMN` to set the list. + + :param list logicalIndexes: List of logical indexes to display + """ + for pos, column_id in enumerate(logicalIndexes): + current_pos = self.visualIndex(column_id) + self.moveSection(current_pos, pos) + self.setSectionHidden(column_id, False) + for column_id in set(range(self.model().columnCount())) - set(logicalIndexes): + self.setSectionHidden(column_id, True) diff --git a/silx/gui/hdf5/Hdf5Item.py b/silx/gui/hdf5/Hdf5Item.py new file mode 100644 index 0000000..40793a4 --- /dev/null +++ b/silx/gui/hdf5/Hdf5Item.py @@ -0,0 +1,421 @@ +# coding: utf-8 +# /*########################################################################## +# +# Copyright (c) 2016-2017 European Synchrotron Radiation Facility +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# +# ###########################################################################*/ + +__authors__ = ["V. Valls"] +__license__ = "MIT" +__date__ = "20/01/2017" + + +import numpy +import logging +import collections +from .. import qt +from .. import icons +from . import _utils +from .Hdf5Node import Hdf5Node +import silx.io.utils +from silx.gui.data.TextFormatter import TextFormatter + +_logger = logging.getLogger(__name__) + +try: + import h5py +except ImportError as e: + _logger.error("Module %s requires h5py", __name__) + raise e + +_formatter = TextFormatter() + + +class Hdf5Item(Hdf5Node): + """Subclass of :class:`qt.QStandardItem` to represent an HDF5-like + item (dataset, file, group or link) as an element of a HDF5-like + tree structure. + """ + + def __init__(self, text, obj, parent, key=None, h5pyClass=None, isBroken=False, populateAll=False): + """ + :param str text: text displayed + :param object obj: Pointer to h5py data. See the `obj` attribute. + """ + self.__obj = obj + self.__key = key + self.__h5pyClass = h5pyClass + self.__isBroken = isBroken + self.__error = None + self.__text = text + Hdf5Node.__init__(self, parent, populateAll=populateAll) + + @property + def obj(self): + if self.__key: + self.__initH5pyObject() + return self.__obj + + @property + def basename(self): + return self.__text + + @property + def h5pyClass(self): + """Returns the class of the stored object. + + When the object is in lazy loading, this method should be able to + return the type of the futrue loaded object. It allows to delay the + real load of the object. + + :rtype: h5py.File or h5py.Dataset or h5py.Group + """ + if self.__h5pyClass is None: + self.__h5pyClass = silx.io.utils.get_h5py_class(self.obj) + return self.__h5pyClass + + def isGroupObj(self): + """Returns true if the stored HDF5 object is a group (contains sub + groups or datasets). + + :rtype: bool + """ + return issubclass(self.h5pyClass, h5py.Group) + + def isBrokenObj(self): + """Returns true if the stored HDF5 object is broken. + + The stored object is then an h5py link (external or not) which point + to nowhere (tbhe external file is not here, the expected dataset is + still not on the file...) + + :rtype: bool + """ + return self.__isBroken + + def _expectedChildCount(self): + if self.isGroupObj(): + return len(self.obj) + return 0 + + def __initH5pyObject(self): + """Lazy load of the HDF5 node. It is reached from the parent node + with the key of the node.""" + parent_obj = self.parent.obj + + try: + obj = parent_obj.get(self.__key) + except Exception as e: + _logger.debug("Internal h5py error", exc_info=True) + try: + self.__obj = parent_obj.get(self.__key, getlink=True) + except Exception: + self.__obj = None + self.__error = e.args[0] + self.__isBroken = True + else: + if obj is None: + # that's a broken link + self.__obj = parent_obj.get(self.__key, getlink=True) + + # TODO monkey-patch file (ask that in h5py for consistency) + if not hasattr(self.__obj, "name"): + parent_name = parent_obj.name + if parent_name == "/": + self.__obj.name = "/" + self.__key + else: + self.__obj.name = parent_name + "/" + self.__key + # TODO monkey-patch file (ask that in h5py for consistency) + if not hasattr(self.__obj, "file"): + self.__obj.file = parent_obj.file + + if isinstance(self.__obj, h5py.ExternalLink): + message = "External link broken. Path %s::%s does not exist" % (self.__obj.filename, self.__obj.path) + elif isinstance(self.__obj, h5py.SoftLink): + message = "Soft link broken. Path %s does not exist" % (self.__obj.path) + else: + name = self.obj.__class__.__name__.split(".")[-1].capitalize() + message = "%s broken" % (name) + self.__error = message + self.__isBroken = True + else: + self.__obj = obj + + self.__key = None + + def _populateChild(self, populateAll=False): + if self.isGroupObj(): + for name in self.obj: + try: + class_ = self.obj.get(name, getclass=True) + has_error = False + except Exception as e: + _logger.error("Internal h5py error", exc_info=True) + try: + class_ = self.obj.get(name, getclass=True, getlink=True) + except Exception as e: + class_ = h5py.HardLink + has_error = True + item = Hdf5Item(text=name, obj=None, parent=self, key=name, h5pyClass=class_, isBroken=has_error) + self.appendChild(item) + + def hasChildren(self): + """Retuens true of this node have chrild. + + :rtype: bool + """ + if not self.isGroupObj(): + return False + return Hdf5Node.hasChildren(self) + + def _getDefaultIcon(self): + """Returns the icon displayed by the main column. + + :rtype: qt.QIcon + """ + style = qt.QApplication.style() + if self.__isBroken: + icon = style.standardIcon(qt.QStyle.SP_MessageBoxCritical) + return icon + class_ = self.h5pyClass + if issubclass(class_, h5py.File): + return style.standardIcon(qt.QStyle.SP_FileIcon) + elif issubclass(class_, h5py.Group): + return style.standardIcon(qt.QStyle.SP_DirIcon) + elif issubclass(class_, h5py.SoftLink): + return style.standardIcon(qt.QStyle.SP_DirLinkIcon) + elif issubclass(class_, h5py.ExternalLink): + return style.standardIcon(qt.QStyle.SP_FileLinkIcon) + elif issubclass(class_, h5py.Dataset): + if len(self.obj.shape) < 4: + name = "item-%ddim" % len(self.obj.shape) + else: + name = "item-ndim" + if str(self.obj.dtype) == "object": + name = "item-object" + icon = icons.getQIcon(name) + return icon + return None + + def _humanReadableShape(self, dataset): + if dataset.shape == tuple(): + return "scalar" + shape = [str(i) for i in dataset.shape] + text = u" \u00D7 ".join(shape) + return text + + def _humanReadableValue(self, dataset): + if dataset.shape == tuple(): + numpy_object = dataset[()] + text = _formatter.toString(numpy_object) + else: + if dataset.size < 5 and dataset.compression is None: + numpy_object = dataset[0:5] + text = _formatter.toString(numpy_object) + else: + dimension = len(dataset.shape) + if dataset.compression is not None: + text = "Compressed %dD data" % dimension + else: + text = "%dD data" % dimension + return text + + def _humanReadableDType(self, dtype, full=False): + if dtype.type == numpy.string_: + text = "string" + elif dtype.type == numpy.unicode_: + text = "string" + elif dtype.type == numpy.object_: + text = "object" + elif dtype.type == numpy.bool_: + text = "bool" + elif dtype.type == numpy.void: + if dtype.fields is None: + text = "raw" + else: + if not full: + text = "compound" + else: + compound = [d[0] for d in dtype.fields.values()] + compound = [self._humanReadableDType(d) for d in compound] + text = "compound(%s)" % ", ".join(compound) + else: + text = str(dtype) + return text + + def _humanReadableType(self, dataset, full=False): + return self._humanReadableDType(dataset.dtype, full) + + def _setTooltipAttributes(self, attributeDict): + """ + Add key/value attributes that will be displayed in the item tooltip + + :param Dict[str,str] attributeDict: Key/value attributes + """ + if issubclass(self.h5pyClass, h5py.Dataset): + attributeDict["Title"] = "HDF5 Dataset" + attributeDict["Name"] = self.basename + attributeDict["Path"] = self.obj.name + attributeDict["Shape"] = self._humanReadableShape(self.obj) + attributeDict["Value"] = self._humanReadableValue(self.obj) + attributeDict["Data type"] = self._humanReadableType(self.obj, full=True) + elif issubclass(self.h5pyClass, h5py.Group): + attributeDict["Title"] = "HDF5 Group" + attributeDict["Name"] = self.basename + attributeDict["Path"] = self.obj.name + elif issubclass(self.h5pyClass, h5py.File): + attributeDict["Title"] = "HDF5 File" + attributeDict["Name"] = self.basename + attributeDict["Path"] = "/" + elif isinstance(self.obj, h5py.ExternalLink): + attributeDict["Title"] = "HDF5 External Link" + attributeDict["Name"] = self.basename + attributeDict["Path"] = self.obj.name + attributeDict["Linked path"] = self.obj.path + attributeDict["Linked file"] = self.obj.filename + elif isinstance(self.obj, h5py.SoftLink): + attributeDict["Title"] = "HDF5 Soft Link" + attributeDict["Name"] = self.basename + attributeDict["Path"] = self.obj.name + attributeDict["Linked path"] = self.obj.path + else: + pass + + def _getDefaultTooltip(self): + """Returns the default tooltip + + :rtype: str + """ + if self.__error is not None: + self.obj # lazy loading of the object + return self.__error + + attrs = collections.OrderedDict() + self._setTooltipAttributes(attrs) + + title = attrs.pop("Title", None) + if len(attrs) > 0: + tooltip = _utils.htmlFromDict(attrs, title=title) + else: + tooltip = "" + + return tooltip + + def dataName(self, role): + """Data for the name column""" + if role == qt.Qt.TextAlignmentRole: + return qt.Qt.AlignTop | qt.Qt.AlignLeft + if role == qt.Qt.DisplayRole: + return self.__text + if role == qt.Qt.DecorationRole: + return self._getDefaultIcon() + if role == qt.Qt.ToolTipRole: + return self._getDefaultTooltip() + return None + + def dataType(self, role): + """Data for the type column""" + if role == qt.Qt.DecorationRole: + return None + if role == qt.Qt.TextAlignmentRole: + return qt.Qt.AlignTop | qt.Qt.AlignLeft + if role == qt.Qt.DisplayRole: + if self.__error is not None: + return "" + class_ = self.h5pyClass + if issubclass(class_, h5py.Dataset): + text = self._humanReadableType(self.obj) + else: + text = "" + return text + + return None + + def dataShape(self, role): + """Data for the shape column""" + if role == qt.Qt.DecorationRole: + return None + if role == qt.Qt.TextAlignmentRole: + return qt.Qt.AlignTop | qt.Qt.AlignLeft + if role == qt.Qt.DisplayRole: + if self.__error is not None: + return "" + class_ = self.h5pyClass + if not issubclass(class_, h5py.Dataset): + return "" + return self._humanReadableShape(self.obj) + return None + + def dataValue(self, role): + """Data for the value column""" + if role == qt.Qt.DecorationRole: + return None + if role == qt.Qt.TextAlignmentRole: + return qt.Qt.AlignTop | qt.Qt.AlignLeft + if role == qt.Qt.DisplayRole: + if self.__error is not None: + return "" + if not issubclass(self.h5pyClass, h5py.Dataset): + return "" + return self._humanReadableValue(self.obj) + return None + + def dataDescription(self, role): + """Data for the description column""" + if role == qt.Qt.DecorationRole: + return None + if role == qt.Qt.TextAlignmentRole: + return qt.Qt.AlignTop | qt.Qt.AlignLeft + if role == qt.Qt.DisplayRole: + if self.__isBroken: + self.obj # lazy loading of the object + return self.__error + if "desc" in self.obj.attrs: + text = self.obj.attrs["desc"] + else: + return "" + return text + if role == qt.Qt.ToolTipRole: + if self.__error is not None: + self.obj # lazy loading of the object + self.__initH5pyObject() + return self.__error + if "desc" in self.obj.attrs: + text = self.obj.attrs["desc"] + else: + return "" + return "Description: %s" % text + return None + + def dataNode(self, role): + """Data for the node column""" + if role == qt.Qt.DecorationRole: + return None + if role == qt.Qt.TextAlignmentRole: + return qt.Qt.AlignTop | qt.Qt.AlignLeft + if role == qt.Qt.DisplayRole: + class_ = self.h5pyClass + text = class_.__name__.split(".")[-1] + return text + if role == qt.Qt.ToolTipRole: + class_ = self.h5pyClass + return "Class name: %s" % self.__class__ + return None diff --git a/silx/gui/hdf5/Hdf5LoadingItem.py b/silx/gui/hdf5/Hdf5LoadingItem.py new file mode 100644 index 0000000..4467366 --- /dev/null +++ b/silx/gui/hdf5/Hdf5LoadingItem.py @@ -0,0 +1,68 @@ +# coding: utf-8 +# /*########################################################################## +# +# Copyright (c) 2016 European Synchrotron Radiation Facility +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# +# ###########################################################################*/ + +__authors__ = ["V. Valls"] +__license__ = "MIT" +__date__ = "23/09/2016" + + +from .. import qt +from .Hdf5Node import Hdf5Node + + +class Hdf5LoadingItem(Hdf5Node): + """Item displayed when an Hdf5Node is loading. + + At the end of the loading this item is replaced by the loaded one. + """ + + def __init__(self, text, parent, animatedIcon): + """Constructor""" + Hdf5Node.__init__(self, parent) + self.__text = text + self.__animatedIcon = animatedIcon + self.__animatedIcon.register(self) + + @property + def obj(self): + return None + + def dataName(self, role): + if role == qt.Qt.DecorationRole: + return self.__animatedIcon.currentIcon() + if role == qt.Qt.TextAlignmentRole: + return qt.Qt.AlignTop | qt.Qt.AlignLeft + if role == qt.Qt.DisplayRole: + return self.__text + return None + + def dataDescription(self, role): + if role == qt.Qt.DecorationRole: + return None + if role == qt.Qt.TextAlignmentRole: + return qt.Qt.AlignTop | qt.Qt.AlignLeft + if role == qt.Qt.DisplayRole: + return "Loading..." + return None diff --git a/silx/gui/hdf5/Hdf5Node.py b/silx/gui/hdf5/Hdf5Node.py new file mode 100644 index 0000000..31bb097 --- /dev/null +++ b/silx/gui/hdf5/Hdf5Node.py @@ -0,0 +1,210 @@ +# coding: utf-8 +# /*########################################################################## +# +# Copyright (c) 2016 European Synchrotron Radiation Facility +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# +# ###########################################################################*/ + +__authors__ = ["V. Valls"] +__license__ = "MIT" +__date__ = "23/09/2016" + + +class Hdf5Node(object): + """Abstract tree node + + It provides link to the childs and to the parents, and a link to an + external object. + """ + def __init__(self, parent=None, populateAll=False): + """ + Constructor + + :param Hdf5Node parent: Parent of the node, if exists, else None + :param bool populateAll: If true, populate all the tree node. Else + everything is lazy loaded. + """ + self.__child = None + self.__parent = parent + if populateAll: + self.__child = [] + self._populateChild(populateAll=True) + + @property + def parent(self): + """Parent of the node, or None if the node is a root + + :rtype: Hdf5Node + """ + return self.__parent + + def setParent(self, parent): + """Redefine the parent of the node. + + It does not set the node as the children of the new parent. + + :param Hdf5Node parent: The new parent + """ + self.__parent = parent + + def appendChild(self, child): + """Append a child to the node. + + It does not update the parent of the child. + + :param Hdf5Node child: Child to append to the node. + """ + self.__initChild() + self.__child.append(child) + + def removeChildAtIndex(self, index): + """Remove a child at an index of the children list. + + The child is removed and returned. + + :param int index: Index in the child list. + :rtype: Hdf5Node + :raises: IndexError if list is empty or index is out of range. + """ + self.__initChild() + return self.__child.pop(index) + + def insertChild(self, index, child): + """ + Insert a child at a specific index of the child list. + + It does not update the parent of the child. + + :param int index: Index in the child list. + :param Hdf5Node child: Child to insert in the child list. + """ + self.__initChild() + self.__child.insert(index, child) + + def indexOfChild(self, child): + """ + Returns the index of the child in the child list of this node. + + :param Hdf5Node child: Child to find + :raises: ValueError if the value is not present. + """ + self.__initChild() + return self.__child.index(child) + + def hasChildren(self): + """Returns true if the node contains children. + + :rtype: bool + """ + return self.childCount() > 0 + + def childCount(self): + """Returns the number of child in this node. + + :rtype: int + """ + if self.__child is not None: + return len(self.__child) + return self._expectedChildCount() + + def child(self, index): + """Return the child at an expected index. + + :param int index: Index of the child in the child list of the node + :rtype: Hdf5Node + """ + self.__initChild() + return self.__child[index] + + def __initChild(self): + """Init the child of the node in case the list was lazy loaded.""" + if self.__child is None: + self.__child = [] + self._populateChild() + + def _expectedChildCount(self): + """Returns the expected count of children + + :rtype: int + """ + return 0 + + def _populateChild(self, populateAll=False): + """Recurse through an HDF5 structure to append groups an datasets + into the tree model. + + Overwrite it to implement the initialisation of child of the node. + """ + pass + + def dataName(self, role): + """Data for the name column + + Overwrite it to implement the content of the 'name' column. + + :rtype: qt.QVariant + """ + return None + + def dataType(self, role): + """Data for the type column + + Overwrite it to implement the content of the 'type' column. + + :rtype: qt.QVariant + """ + return None + + def dataShape(self, role): + """Data for the shape column + + Overwrite it to implement the content of the 'shape' column. + + :rtype: qt.QVariant + """ + return None + + def dataValue(self, role): + """Data for the value column + + Overwrite it to implement the content of the 'value' column. + + :rtype: qt.QVariant + """ + return None + + def dataDescription(self, role): + """Data for the description column + + Overwrite it to implement the content of the 'description' column. + + :rtype: qt.QVariant + """ + return None + + def dataNode(self, role): + """Data for the node column + + Overwrite it to implement the content of the 'node' column. + + :rtype: qt.QVariant + """ + return None diff --git a/silx/gui/hdf5/Hdf5TreeModel.py b/silx/gui/hdf5/Hdf5TreeModel.py new file mode 100644 index 0000000..fb5de06 --- /dev/null +++ b/silx/gui/hdf5/Hdf5TreeModel.py @@ -0,0 +1,581 @@ +# coding: utf-8 +# /*########################################################################## +# +# Copyright (c) 2016-2017 European Synchrotron Radiation Facility +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# +# ###########################################################################*/ + +__authors__ = ["V. Valls"] +__license__ = "MIT" +__date__ = "19/12/2016" + + +import os +import logging +from .. import qt +from .. import icons +from .Hdf5Node import Hdf5Node +from .Hdf5Item import Hdf5Item +from .Hdf5LoadingItem import Hdf5LoadingItem +from . import _utils +from ... import io as silx_io + +_logger = logging.getLogger(__name__) + +"""Helpers to take care of None objects as signal parameters. +PySide crash if a signal with a None parameter is emitted between threads. +""" +if qt.BINDING == 'PySide': + class _NoneWraper(object): + pass + _NoneWraperInstance = _NoneWraper() + + def _wrapNone(x): + """Wrap x if it is a None value, else returns x""" + if x is None: + return _NoneWraperInstance + else: + return x + + def _unwrapNone(x): + """Unwrap x as a None if a None was stored by `wrapNone`, else returns + x""" + if x is _NoneWraperInstance: + return None + else: + return x +else: + # Allow to fix None event params to avoid PySide crashes + def _wrapNone(x): + return x + + def _unwrapNone(x): + return x + + +class LoadingItemRunnable(qt.QRunnable): + """Runner to process item loading from a file""" + + class __Signals(qt.QObject): + """Signal holder""" + itemReady = qt.Signal(object, object, object) + runnerFinished = qt.Signal(object) + + def __init__(self, filename, item): + """Constructor + + :param LoadingItemWorker worker: Object holding data and signals + """ + super(LoadingItemRunnable, self).__init__() + self.filename = filename + self.oldItem = item + self.signals = self.__Signals() + + def setFile(self, filename, item): + self.filenames.append((filename, item)) + + @property + def itemReady(self): + return self.signals.itemReady + + @property + def runnerFinished(self): + return self.signals.runnerFinished + + def __loadItemTree(self, oldItem, h5obj): + """Create an item tree used by the GUI from an h5py object. + + :param Hdf5Node oldItem: The current item displayed the GUI + :param h5py.File h5obj: The h5py object to display in the GUI + :rtpye: Hdf5Node + """ + if silx_io.is_file(h5obj): + text = os.path.basename(h5obj.filename) + else: + filename = os.path.basename(h5obj.file.filename) + path = h5obj.name + text = "%s::%s" % (filename, path) + item = Hdf5Item(text=text, obj=h5obj, parent=oldItem.parent, populateAll=True) + return item + + @qt.Slot() + def run(self): + """Process the file loading. The worker is used as holder + of the data and the signal. The result is sent as a signal. + """ + try: + h5file = silx_io.open(self.filename) + newItem = self.__loadItemTree(self.oldItem, h5file) + error = None + except IOError as e: + # Should be logged + error = e + newItem = None + + # Take care of None value in case of PySide + newItem = _wrapNone(newItem) + error = _wrapNone(error) + self.itemReady.emit(self.oldItem, newItem, error) + self.runnerFinished.emit(self) + + def autoDelete(self): + return True + + +class Hdf5TreeModel(qt.QAbstractItemModel): + """Tree model storing a list of :class:`h5py.File` like objects. + + The main column display the :class:`h5py.File` list and there hierarchy. + Other columns display information on node hierarchy. + """ + + H5PY_ITEM_ROLE = qt.Qt.UserRole + """Role to reach h5py item from an item index""" + + H5PY_OBJECT_ROLE = qt.Qt.UserRole + 1 + """Role to reach h5py object from an item index""" + + USER_ROLE = qt.Qt.UserRole + 2 + """Start of range of available user role for derivative models""" + + NAME_COLUMN = 0 + """Column id containing HDF5 node names""" + + TYPE_COLUMN = 1 + """Column id containing HDF5 dataset types""" + + SHAPE_COLUMN = 2 + """Column id containing HDF5 dataset shapes""" + + VALUE_COLUMN = 3 + """Column id containing HDF5 dataset values""" + + DESCRIPTION_COLUMN = 4 + """Column id containing HDF5 node description/title/message""" + + NODE_COLUMN = 5 + """Column id containing HDF5 node type""" + + COLUMN_IDS = [ + NAME_COLUMN, + TYPE_COLUMN, + SHAPE_COLUMN, + VALUE_COLUMN, + DESCRIPTION_COLUMN, + NODE_COLUMN, + ] + """List of logical columns available""" + + def __init__(self, parent=None): + super(Hdf5TreeModel, self).__init__(parent) + + self.treeView = parent + self.header_labels = [None] * 6 + self.header_labels[self.NAME_COLUMN] = 'Name' + self.header_labels[self.TYPE_COLUMN] = 'Type' + self.header_labels[self.SHAPE_COLUMN] = 'Shape' + self.header_labels[self.VALUE_COLUMN] = 'Value' + self.header_labels[self.DESCRIPTION_COLUMN] = 'Description' + self.header_labels[self.NODE_COLUMN] = 'Node' + + # Create items + self.__root = Hdf5Node() + self.__fileDropEnabled = True + self.__fileMoveEnabled = True + + self.__animatedIcon = icons.getWaitIcon() + self.__animatedIcon.iconChanged.connect(self.__updateLoadingItems) + self.__runnerSet = set([]) + + # store used icons to avoid to avoid the cache to release it + self.__icons = [] + self.__icons.append(icons.getQIcon("item-0dim")) + self.__icons.append(icons.getQIcon("item-1dim")) + self.__icons.append(icons.getQIcon("item-2dim")) + self.__icons.append(icons.getQIcon("item-3dim")) + self.__icons.append(icons.getQIcon("item-ndim")) + self.__icons.append(icons.getQIcon("item-object")) + + def __updateLoadingItems(self, icon): + for i in range(self.__root.childCount()): + item = self.__root.child(i) + if isinstance(item, Hdf5LoadingItem): + index1 = self.index(i, 0, qt.QModelIndex()) + index2 = self.index(i, self.columnCount() - 1, qt.QModelIndex()) + self.dataChanged.emit(index1, index2) + + def __itemReady(self, oldItem, newItem, error): + """Called at the end of a concurent file loading, when the loading + item is ready. AN error is defined if an exception occured when + loading the newItem . + + :param Hdf5Node oldItem: current displayed item + :param Hdf5Node newItem: item loaded, or None if error is defined + :param Exception error: An exception, or None if newItem is defined + """ + # Take care of None value in case of PySide + newItem = _unwrapNone(newItem) + error = _unwrapNone(error) + row = self.__root.indexOfChild(oldItem) + rootIndex = qt.QModelIndex() + self.beginRemoveRows(rootIndex, row, row) + self.__root.removeChildAtIndex(row) + self.endRemoveRows() + if newItem is not None: + self.beginInsertRows(rootIndex, row, row) + self.__root.insertChild(row, newItem) + self.endInsertRows() + # FIXME the error must be displayed + + def isFileDropEnabled(self): + return self.__fileDropEnabled + + def setFileDropEnabled(self, enabled): + self.__fileDropEnabled = enabled + + fileDropEnabled = qt.Property(bool, isFileDropEnabled, setFileDropEnabled) + """Property to enable/disable file dropping in the model.""" + + def isFileMoveEnabled(self): + return self.__fileMoveEnabled + + def setFileMoveEnabled(self, enabled): + self.__fileMoveEnabled = enabled + + fileMoveEnabled = qt.Property(bool, isFileMoveEnabled, setFileMoveEnabled) + """Property to enable/disable drag-and-drop of files to + change the ordering in the model.""" + + def supportedDropActions(self): + if self.__fileMoveEnabled or self.__fileDropEnabled: + return qt.Qt.CopyAction | qt.Qt.MoveAction + else: + return 0 + + def mimeTypes(self): + if self.__fileMoveEnabled: + return [_utils.Hdf5NodeMimeData.MIME_TYPE] + else: + return [] + + def mimeData(self, indexes): + """ + Returns an object that contains serialized items of data corresponding + to the list of indexes specified. + + :param list(qt.QModelIndex) indexes: List of indexes + :rtype: qt.QMimeData + """ + if not self.__fileMoveEnabled or len(indexes) == 0: + return None + + indexes = [i for i in indexes if i.column() == 0] + if len(indexes) > 1: + raise NotImplementedError("Drag of multi rows is not implemented") + if len(indexes) == 0: + raise NotImplementedError("Drag of cell is not implemented") + + node = self.nodeFromIndex(indexes[0]) + mimeData = _utils.Hdf5NodeMimeData(node) + return mimeData + + def flags(self, index): + defaultFlags = qt.QAbstractItemModel.flags(self, index) + + if index.isValid(): + node = self.nodeFromIndex(index) + if self.__fileMoveEnabled and node.parent is self.__root: + # that's a root + return qt.Qt.ItemIsDragEnabled | defaultFlags + return defaultFlags + elif self.__fileDropEnabled or self.__fileMoveEnabled: + return qt.Qt.ItemIsDropEnabled | defaultFlags + else: + return defaultFlags + + def dropMimeData(self, mimedata, action, row, column, parentIndex): + if action == qt.Qt.IgnoreAction: + return True + + if self.__fileMoveEnabled and mimedata.hasFormat(_utils.Hdf5NodeMimeData.MIME_TYPE): + dragNode = mimedata.node() + parentNode = self.nodeFromIndex(parentIndex) + if parentNode is not dragNode.parent: + return False + + if row == -1: + # append to the parent + row = parentNode.childCount() + else: + # insert at row + pass + + dragNodeParent = dragNode.parent + sourceRow = dragNodeParent.indexOfChild(dragNode) + self.moveRow(parentIndex, sourceRow, parentIndex, row) + return True + + if self.__fileDropEnabled and mimedata.hasFormat("text/uri-list"): + + parentNode = self.nodeFromIndex(parentIndex) + if parentNode is not self.__root: + while(parentNode is not self.__root): + node = parentNode + parentNode = node.parent + row = parentNode.indexOfChild(node) + else: + if row == -1: + row = self.__root.childCount() + + messages = [] + for url in mimedata.urls(): + try: + self.insertFileAsync(url.toLocalFile(), row) + row += 1 + except IOError as e: + messages.append(e.args[0]) + if len(messages) > 0: + title = "Error occurred when loading files" + message = "%s: