summaryrefslogtreecommitdiff
path: root/silx/gui/hdf5
diff options
context:
space:
mode:
authorPicca Frédéric-Emmanuel <picca@synchrotron-soleil.fr>2017-08-18 14:48:52 +0200
committerPicca Frédéric-Emmanuel <picca@synchrotron-soleil.fr>2017-08-18 14:48:52 +0200
commitf7bdc2acff3c13a6d632c28c4569690ab106eed7 (patch)
tree9d67cdb7152ee4e711379e03fe0546c7c3b97303 /silx/gui/hdf5
Import Upstream version 0.5.0+dfsg
Diffstat (limited to 'silx/gui/hdf5')
-rw-r--r--silx/gui/hdf5/Hdf5HeaderView.py192
-rw-r--r--silx/gui/hdf5/Hdf5Item.py421
-rw-r--r--silx/gui/hdf5/Hdf5LoadingItem.py68
-rw-r--r--silx/gui/hdf5/Hdf5Node.py210
-rw-r--r--silx/gui/hdf5/Hdf5TreeModel.py581
-rw-r--r--silx/gui/hdf5/Hdf5TreeView.py204
-rw-r--r--silx/gui/hdf5/NexusSortFilterProxyModel.py152
-rw-r--r--silx/gui/hdf5/__init__.py44
-rw-r--r--silx/gui/hdf5/_utils.py247
-rw-r--r--silx/gui/hdf5/setup.py41
-rw-r--r--silx/gui/hdf5/test/__init__.py39
-rw-r--r--silx/gui/hdf5/test/_mock.py130
-rw-r--r--silx/gui/hdf5/test/test_hdf5.py480
13 files changed, 2809 insertions, 0 deletions
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 = "<html>%s:<ul><li>%s</li><ul></html>" % (title, "</li><li>".join(messages))
+ qt.QMessageBox.critical(None, title, message)
+ return True
+
+ return False
+
+ def headerData(self, section, orientation, role=qt.Qt.DisplayRole):
+ if orientation == qt.Qt.Horizontal:
+ if role in [qt.Qt.DisplayRole, qt.Qt.EditRole]:
+ return self.header_labels[section]
+ return None
+
+ def insertNode(self, row, node):
+ if row == -1:
+ row = self.__root.childCount()
+ self.beginInsertRows(qt.QModelIndex(), row, row)
+ self.__root.insertChild(row, node)
+ self.endInsertRows()
+
+ def moveRow(self, sourceParentIndex, sourceRow, destinationParentIndex, destinationRow):
+ if sourceRow == destinationRow or sourceRow == destinationRow - 1:
+ # abort move, same place
+ return
+ return self.moveRows(sourceParentIndex, sourceRow, 1, destinationParentIndex, destinationRow)
+
+ def moveRows(self, sourceParentIndex, sourceRow, count, destinationParentIndex, destinationRow):
+ self.beginMoveRows(sourceParentIndex, sourceRow, sourceRow, destinationParentIndex, destinationRow)
+ sourceNode = self.nodeFromIndex(sourceParentIndex)
+ destinationNode = self.nodeFromIndex(destinationParentIndex)
+
+ if sourceNode is destinationNode and sourceRow < destinationRow:
+ item = sourceNode.child(sourceRow)
+ destinationNode.insertChild(destinationRow, item)
+ sourceNode.removeChildAtIndex(sourceRow)
+ else:
+ item = sourceNode.removeChildAtIndex(sourceRow)
+ destinationNode.insertChild(destinationRow, item)
+
+ self.endMoveRows()
+ return True
+
+ def index(self, row, column, parent=qt.QModelIndex()):
+ try:
+ node = self.nodeFromIndex(parent)
+ return self.createIndex(row, column, node.child(row))
+ except IndexError:
+ return qt.QModelIndex()
+
+ def data(self, index, role=qt.Qt.DisplayRole):
+ node = self.nodeFromIndex(index)
+
+ if role == self.H5PY_ITEM_ROLE:
+ return node
+
+ if role == self.H5PY_OBJECT_ROLE:
+ return node.obj
+
+ if index.column() == self.NAME_COLUMN:
+ return node.dataName(role)
+ elif index.column() == self.TYPE_COLUMN:
+ return node.dataType(role)
+ elif index.column() == self.SHAPE_COLUMN:
+ return node.dataShape(role)
+ elif index.column() == self.VALUE_COLUMN:
+ return node.dataValue(role)
+ elif index.column() == self.DESCRIPTION_COLUMN:
+ return node.dataDescription(role)
+ elif index.column() == self.NODE_COLUMN:
+ return node.dataNode(role)
+ else:
+ return None
+
+ def columnCount(self, parent=qt.QModelIndex()):
+ return len(self.header_labels)
+
+ def hasChildren(self, parent=qt.QModelIndex()):
+ node = self.nodeFromIndex(parent)
+ if node is None:
+ return 0
+ return node.hasChildren()
+
+ def rowCount(self, parent=qt.QModelIndex()):
+ node = self.nodeFromIndex(parent)
+ if node is None:
+ return 0
+ return node.childCount()
+
+ def parent(self, child):
+ if not child.isValid():
+ return qt.QModelIndex()
+
+ node = self.nodeFromIndex(child)
+
+ if node is None:
+ return qt.QModelIndex()
+
+ parent = node.parent
+
+ if parent is None:
+ return qt.QModelIndex()
+
+ grandparent = parent.parent
+ if grandparent is None:
+ return qt.QModelIndex()
+ row = grandparent.indexOfChild(parent)
+
+ assert row != - 1
+ return self.createIndex(row, 0, parent)
+
+ def nodeFromIndex(self, index):
+ return index.internalPointer() if index.isValid() else self.__root
+
+ def synchronizeIndex(self, index):
+ """
+ Synchronize a file a given its index.
+
+ Basically close it and load it again.
+
+ :param qt.QModelIndex index: Index of the item to update
+ """
+ node = self.nodeFromIndex(index)
+ if node.parent is not self.__root:
+ return
+
+ self.removeIndex(index)
+ filename = node.obj.filename
+ node.obj.close()
+ self.insertFileAsync(filename, index.row())
+
+ def synchronizeH5pyObject(self, h5pyObject):
+ """
+ Synchronize a h5py object in all the tree.
+
+ Basically close it and load it again.
+
+ :param h5py.File h5pyObject: A :class:`h5py.File` object.
+ """
+ index = 0
+ while index < self.__root.childCount():
+ item = self.__root.child(index)
+ if item.obj is h5pyObject:
+ qindex = self.index(index, 0, qt.QModelIndex())
+ self.synchronizeIndex(qindex)
+ else:
+ index += 1
+
+ def removeIndex(self, index):
+ """
+ Remove an item from the model using its index.
+
+ :param qt.QModelIndex index: Index of the item to remove
+ """
+ node = self.nodeFromIndex(index)
+ if node.parent is not self.__root:
+ return
+ self.beginRemoveRows(qt.QModelIndex(), index.row(), index.row())
+ self.__root.removeChildAtIndex(index.row())
+ self.endRemoveRows()
+
+ def removeH5pyObject(self, h5pyObject):
+ """
+ Remove an item from the model using the holding h5py object.
+ It can remove more than one item.
+
+ :param h5py.File h5pyObject: A :class:`h5py.File` object.
+ """
+ index = 0
+ while index < self.__root.childCount():
+ item = self.__root.child(index)
+ if item.obj is h5pyObject:
+ qindex = self.index(index, 0, qt.QModelIndex())
+ self.removeIndex(qindex)
+ else:
+ index += 1
+
+ def insertH5pyObject(self, h5pyObject, text=None, row=-1):
+ """Append an HDF5 object from h5py to the tree.
+
+ :param h5pyObject: File handle/descriptor for a :class:`h5py.File`
+ or any other class of h5py file structure.
+ """
+ if text is None:
+ if silx_io.is_file(h5pyObject):
+ text = os.path.basename(h5pyObject.filename)
+ else:
+ filename = os.path.basename(h5pyObject.file.filename)
+ path = h5pyObject.name
+ text = "%s::%s" % (filename, path)
+ if row == -1:
+ row = self.__root.childCount()
+ self.insertNode(row, Hdf5Item(text=text, obj=h5pyObject, parent=self.__root))
+
+ def insertFileAsync(self, filename, row=-1):
+ if not os.path.isfile(filename):
+ raise IOError("Filename '%s' must be a file path" % filename)
+
+ # create temporary item
+ text = os.path.basename(filename)
+ item = Hdf5LoadingItem(text=text, parent=self.__root, animatedIcon=self.__animatedIcon)
+ self.insertNode(row, item)
+
+ # start loading the real one
+ runnable = LoadingItemRunnable(filename, item)
+ runnable.itemReady.connect(self.__itemReady)
+ self.__runnerSet.add(runnable)
+ runnable.runnerFinished.connect(self.__releaseRunner)
+ qt.QThreadPool.globalInstance().start(runnable)
+
+ def __releaseRunner(self, runner):
+ self.__runnerSet.remove(runner)
+
+ def insertFile(self, filename, row=-1):
+ """Load a HDF5 file into the data model.
+
+ :param filename: file path.
+ """
+ try:
+ h5file = silx_io.open(filename)
+ self.insertH5pyObject(h5file, row=row)
+ except IOError:
+ _logger.debug("File '%s' can't be read.", filename, exc_info=True)
+ raise
+
+ def appendFile(self, filename):
+ self.insertFile(filename, -1)
diff --git a/silx/gui/hdf5/Hdf5TreeView.py b/silx/gui/hdf5/Hdf5TreeView.py
new file mode 100644
index 0000000..09f6fcf
--- /dev/null
+++ b/silx/gui/hdf5/Hdf5TreeView.py
@@ -0,0 +1,204 @@
+# 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__ = "27/09/2016"
+
+
+import logging
+from .. import qt
+from ...utils import weakref as silxweakref
+from .Hdf5TreeModel import Hdf5TreeModel
+from .Hdf5HeaderView import Hdf5HeaderView
+from .NexusSortFilterProxyModel import NexusSortFilterProxyModel
+from .Hdf5Item import Hdf5Item
+from . import _utils
+
+_logger = logging.getLogger(__name__)
+
+
+class Hdf5TreeView(qt.QTreeView):
+ """TreeView which allow to browse HDF5 file structure.
+
+ It provides columns width auto-resizing and additional
+ signals.
+
+ The default model is a :class:`NexusSortFilterProxyModel` sourcing
+ a :class:`Hdf5TreeModel`. The :class:`Hdf5TreeModel` is reachable using
+ :meth:`findHdf5TreeModel`. The default header is :class:`Hdf5HeaderView`.
+
+ Context menu is managed by the :meth:`setContextMenuPolicy` with the value
+ Qt.CustomContextMenu. This policy must not be changed, otherwise context
+ menus will not work anymore. You can use :meth:`addContextMenuCallback` and
+ :meth:`removeContextMenuCallback` to add your custum actions according
+ to the selected objects.
+ """
+ def __init__(self, parent=None):
+ """
+ Constructor
+
+ :param parent qt.QWidget: The parent widget
+ """
+ qt.QTreeView.__init__(self, parent)
+
+ model = Hdf5TreeModel(self)
+ proxy_model = NexusSortFilterProxyModel(self)
+ proxy_model.setSourceModel(model)
+ self.setModel(proxy_model)
+
+ self.setHeader(Hdf5HeaderView(qt.Qt.Horizontal, self))
+ self.setSelectionBehavior(qt.QAbstractItemView.SelectRows)
+ self.sortByColumn(0, qt.Qt.AscendingOrder)
+ # optimise the rendering
+ self.setUniformRowHeights(True)
+
+ self.setIconSize(qt.QSize(16, 16))
+ self.setAcceptDrops(True)
+ self.setDragEnabled(True)
+ self.setDragDropMode(qt.QAbstractItemView.DragDrop)
+ self.showDropIndicator()
+
+ self.__context_menu_callbacks = silxweakref.WeakList()
+ self.setContextMenuPolicy(qt.Qt.CustomContextMenu)
+ self.customContextMenuRequested.connect(self._createContextMenu)
+
+ def __removeContextMenuProxies(self, ref):
+ """Callback to remove dead proxy from the list"""
+ self.__context_menu_callbacks.remove(ref)
+
+ def _createContextMenu(self, pos):
+ """
+ Create context menu.
+
+ :param pos qt.QPoint: Position of the context menu
+ """
+ actions = []
+
+ menu = qt.QMenu(self)
+
+ hovered_index = self.indexAt(pos)
+ hovered_node = self.model().data(hovered_index, Hdf5TreeModel.H5PY_ITEM_ROLE)
+ if hovered_node is None or not isinstance(hovered_node, Hdf5Item):
+ return
+
+ hovered_object = _utils.H5Node(hovered_node)
+ event = _utils.Hdf5ContextMenuEvent(self, menu, hovered_object)
+
+ for callback in self.__context_menu_callbacks:
+ try:
+ callback(event)
+ except KeyboardInterrupt:
+ raise
+ except:
+ # make sure no user callback crash the application
+ _logger.error("Error while calling callback", exc_info=True)
+ pass
+
+ if len(menu.children()) > 0:
+ for action in actions:
+ menu.addAction(action)
+ menu.popup(self.viewport().mapToGlobal(pos))
+
+ def addContextMenuCallback(self, callback):
+ """Register a context menu callback.
+
+ The callback will be called when a context menu is requested with the
+ treeview and the list of selected h5py objects in parameters. The
+ callback must return a list of :class:`qt.QAction` object.
+
+ Callbacks are stored as saferef. The object must store a reference by
+ itself.
+ """
+ self.__context_menu_callbacks.append(callback)
+
+ def removeContextMenuCallback(self, callback):
+ """Unregister a context menu callback"""
+ self.__context_menu_callbacks.remove(callback)
+
+ def findHdf5TreeModel(self):
+ """Find the Hdf5TreeModel from the stack of model filters.
+
+ :returns: A Hdf5TreeModel, else None
+ :rtype: Hdf5TreeModel
+ """
+ model = self.model()
+ while model is not None:
+ if isinstance(model, qt.QAbstractProxyModel):
+ model = model.sourceModel()
+ else:
+ break
+ if model is None:
+ return None
+ if isinstance(model, Hdf5TreeModel):
+ return model
+ else:
+ return None
+
+ def dragEnterEvent(self, event):
+ model = self.findHdf5TreeModel()
+ if model is not None and model.isFileDropEnabled() and event.mimeData().hasFormat("text/uri-list"):
+ self.setState(qt.QAbstractItemView.DraggingState)
+ event.accept()
+ else:
+ qt.QTreeView.dragEnterEvent(self, event)
+
+ def dragMoveEvent(self, event):
+ model = self.findHdf5TreeModel()
+ if model is not None and model.isFileDropEnabled() and event.mimeData().hasFormat("text/uri-list"):
+ event.setDropAction(qt.Qt.CopyAction)
+ event.accept()
+ else:
+ qt.QTreeView.dragMoveEvent(self, event)
+
+ def selectedH5Nodes(self, ignoreBrokenLinks=True):
+ """Returns selected h5py objects like :class:`h5py.File`,
+ :class:`h5py.Group`, :class:`h5py.Dataset` or mimicked objects.
+
+ :param ignoreBrokenLinks bool: Returns objects which are not not
+ broken links.
+ :rtype: iterator(:class:`_utils.H5Node`)
+ """
+ for index in self.selectedIndexes():
+ if index.column() != 0:
+ continue
+ item = self.model().data(index, Hdf5TreeModel.H5PY_ITEM_ROLE)
+ if item is None:
+ continue
+ if isinstance(item, Hdf5Item):
+ if ignoreBrokenLinks and item.isBrokenObj():
+ continue
+ yield _utils.H5Node(item)
+
+ def mousePressEvent(self, event):
+ """Override mousePressEvent to provide a consistante compatible API
+ between Qt4 and Qt5
+ """
+ super(Hdf5TreeView, self).mousePressEvent(event)
+ if event.button() != qt.Qt.LeftButton:
+ # Qt5 only sends itemClicked on left button mouse click
+ if qt.qVersion() > "5":
+ qindex = self.indexAt(event.pos())
+ self.clicked.emit(qindex)
diff --git a/silx/gui/hdf5/NexusSortFilterProxyModel.py b/silx/gui/hdf5/NexusSortFilterProxyModel.py
new file mode 100644
index 0000000..9a4268c
--- /dev/null
+++ b/silx/gui/hdf5/NexusSortFilterProxyModel.py
@@ -0,0 +1,152 @@
+# 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__ = "12/04/2017"
+
+
+import logging
+import re
+import numpy
+from .. import qt
+from .Hdf5TreeModel import Hdf5TreeModel
+
+_logger = logging.getLogger(__name__)
+
+try:
+ import h5py
+except ImportError as e:
+ _logger.error("Module %s requires h5py", __name__)
+ raise e
+
+_logger = logging.getLogger(__name__)
+
+
+class NexusSortFilterProxyModel(qt.QSortFilterProxyModel):
+ """Try to sort items according to Nexus structure. Else sort by name."""
+
+ def __init__(self, parent=None):
+ qt.QSortFilterProxyModel.__init__(self, parent)
+ self.__split = re.compile("(\\d+|\\D+)")
+
+ def lessThan(self, sourceLeft, sourceRight):
+ """Returns True if the value of the item referred to by the given
+ index `sourceLeft` is less than the value of the item referred to by
+ the given index `sourceRight`, otherwise returns false.
+
+ :param qt.QModelIndex sourceLeft:
+ :param qt.QModelIndex sourceRight:
+ :rtype: bool
+ """
+ if sourceLeft.column() != Hdf5TreeModel.NAME_COLUMN:
+ return super(NexusSortFilterProxyModel, self).lessThan(
+ sourceLeft, sourceRight)
+
+ # Do not sort child of root (files)
+ if sourceLeft.parent() == qt.QModelIndex():
+ return sourceLeft.row() < sourceRight.row()
+
+ left = self.sourceModel().data(sourceLeft, Hdf5TreeModel.H5PY_ITEM_ROLE)
+ right = self.sourceModel().data(sourceRight, Hdf5TreeModel.H5PY_ITEM_ROLE)
+
+ if self.__isNXentry(left) and self.__isNXentry(right):
+ less = self.childDatasetLessThan(left, right, "start_time")
+ if less is not None:
+ return less
+ less = self.childDatasetLessThan(left, right, "end_time")
+ if less is not None:
+ return less
+
+ left = self.sourceModel().data(sourceLeft, qt.Qt.DisplayRole)
+ right = self.sourceModel().data(sourceRight, qt.Qt.DisplayRole)
+ return self.nameLessThan(left, right)
+
+ def __isNXentry(self, node):
+ """Returns true if the node is an NXentry"""
+ if not issubclass(node.h5pyClass, h5py.Group):
+ return False
+ nxClass = node.obj.attrs.get("NX_class", None)
+ return nxClass == "NXentry"
+
+ def getWordsAndNumbers(self, name):
+ """
+ Returns a list of words and integers composing the name.
+
+ An input `"aaa10bbb50.30"` will return
+ `["aaa", 10, "bbb", 50, ".", 30]`.
+
+ :param str name: A name
+ :rtype: list
+ """
+ words = self.__split.findall(name)
+ result = []
+ for i in words:
+ if i[0].isdigit():
+ i = int(i)
+ result.append(i)
+ return result
+
+ def nameLessThan(self, left, right):
+ """Returns True if the left string is less than the right string.
+
+ Number composing the names are compared as integers, as result "name2"
+ is smaller than "name10".
+
+ :param str left: A string
+ :param str right: A string
+ :rtype: bool
+ """
+ leftList = self.getWordsAndNumbers(left)
+ rightList = self.getWordsAndNumbers(right)
+ try:
+ return leftList < rightList
+ except TypeError:
+ # Back to string comparison if list are not type consistent
+ return left < right
+
+ def childDatasetLessThan(self, left, right, childName):
+ """
+ Reach the same children name of two items and compare their values.
+
+ Returns True if the left one is smaller than the right one.
+
+ :param Hdf5Item left: An item
+ :param Hdf5Item right: An item
+ :param str childName: Name of the children to search. Returns None if
+ the children is not found.
+ :rtype: bool
+ """
+ try:
+ left_time = left.obj[childName][()]
+ right_time = right.obj[childName][()]
+ if isinstance(left_time, numpy.ndarray):
+ return left_time[0] < right_time[0]
+ return left_time < right_time
+ except KeyboardInterrupt:
+ raise
+ except Exception as e:
+ _logger.debug("Exception occurred", exc_info=True)
+ return None
diff --git a/silx/gui/hdf5/__init__.py b/silx/gui/hdf5/__init__.py
new file mode 100644
index 0000000..1b5a602
--- /dev/null
+++ b/silx/gui/hdf5/__init__.py
@@ -0,0 +1,44 @@
+# 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.
+#
+# ###########################################################################*/
+"""This package provides a set of Qt widgets for displaying content relative to
+HDF5 format.
+
+.. note::
+
+ This package depends on *h5py*.
+"""
+
+__authors__ = ["V. Valls"]
+__license__ = "MIT"
+__date__ = "23/09/2016"
+
+
+from .Hdf5TreeView import Hdf5TreeView # noqa
+from ._utils import H5Node
+from ._utils import Hdf5ContextMenuEvent # noqa
+from .NexusSortFilterProxyModel import NexusSortFilterProxyModel # noqa
+from .Hdf5TreeModel import Hdf5TreeModel # noqa
+
+__all__ = ['Hdf5TreeView', 'H5Node', 'Hdf5ContextMenuEvent', 'NexusSortFilterProxyModel', 'Hdf5TreeModel']
diff --git a/silx/gui/hdf5/_utils.py b/silx/gui/hdf5/_utils.py
new file mode 100644
index 0000000..af9c79f
--- /dev/null
+++ b/silx/gui/hdf5/_utils.py
@@ -0,0 +1,247 @@
+# 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.
+#
+# ###########################################################################*/
+"""This package provides a set of helper class and function used by the
+package `silx.gui.hdf5` package.
+"""
+
+__authors__ = ["V. Valls"]
+__license__ = "MIT"
+__date__ = "26/04/2017"
+
+
+import logging
+import numpy
+from .. import qt
+import silx.io.utils
+from silx.utils.html import escape
+
+_logger = logging.getLogger(__name__)
+
+try:
+ import h5py
+except ImportError as e:
+ _logger.error("Module %s requires h5py", __name__)
+ raise e
+
+
+class Hdf5ContextMenuEvent(object):
+ """Hold information provided to context menu callbacks."""
+
+ def __init__(self, source, menu, hoveredObject):
+ """
+ Constructor
+
+ :param QWidget source: Widget source
+ :param QMenu menu: Context menu which will be displayed
+ :param H5Node hoveredObject: Hovered H5 node
+ """
+ self.__source = source
+ self.__menu = menu
+ self.__hoveredObject = hoveredObject
+
+ def source(self):
+ """Source of the event
+
+ :rtype: Hdf5TreeView
+ """
+ return self.__source
+
+ def menu(self):
+ """Menu which will be displayed
+
+ :rtype: qt.QMenu
+ """
+ return self.__menu
+
+ def hoveredObject(self):
+ """Item content hovered by the mouse when the context menu was
+ requested
+
+ :rtype: H5Node
+ """
+ return self.__hoveredObject
+
+
+def htmlFromDict(dictionary, title=None):
+ """Generate a readable HTML from a dictionary
+
+ :param dict dictionary: A Dictionary
+ :rtype: str
+ """
+ result = """<html>
+ <head>
+ <style type="text/css">
+ ul { -qt-list-indent: 0; list-style: none; }
+ li > b {display: inline-block; min-width: 4em; font-weight: bold; }
+ </style>
+ </head>
+ <body>
+ """
+ if title is not None:
+ result += "<b>%s</b>" % escape(title)
+ result += "<ul>"
+ for key, value in dictionary.items():
+ result += "<li><b>%s</b>: %s</li>" % (escape(key), escape(value))
+ result += "</ul>"
+ result += "</body></html>"
+ return result
+
+
+class Hdf5NodeMimeData(qt.QMimeData):
+ """Mimedata class to identify an internal drag and drop of a Hdf5Node."""
+
+ MIME_TYPE = "application/x-internal-h5py-node"
+
+ def __init__(self, node=None):
+ qt.QMimeData.__init__(self)
+ self.__node = node
+ self.setData(self.MIME_TYPE, "".encode(encoding='utf-8'))
+
+ def node(self):
+ return self.__node
+
+
+class H5Node(object):
+ """Adapter over an h5py object to provide missing informations from h5py
+ nodes, like internal node path and filename (which are not provided by
+ :mod:`h5py` for soft and external links).
+
+ It also provides an abstraction to reach node type for mimicked h5py
+ objects.
+ """
+
+ def __init__(self, h5py_item=None):
+ """Constructor
+
+ :param Hdf5Item h5py_item: An Hdf5Item
+ """
+ self.__h5py_object = h5py_item.obj
+ self.__h5py_item = h5py_item
+
+ def __getattr__(self, name):
+ return object.__getattribute__(self.__h5py_object, name)
+
+ @property
+ def h5py_object(self):
+ """Returns the internal h5py node.
+
+ :rtype: h5py.File or h5py.Group or h5py.Dataset
+ """
+ return self.__h5py_object
+
+ @property
+ def ntype(self):
+ """Returns the node type, as an h5py class.
+
+ :rtype:
+ :class:`h5py.File`, :class:`h5py.Group` or :class:`h5py.Dataset`
+ """
+ return silx.io.utils.get_h5py_class(self.__h5py_object)
+
+ @property
+ def basename(self):
+ """Returns the basename of this h5py node. It is the last identifier of
+ the path.
+
+ :rtype: str
+ """
+ return self.__h5py_object.name.split("/")[-1]
+
+ @property
+ def local_name(self):
+ """Returns the local path of this h5py node.
+
+ For links, this path is not equal to the h5py one.
+
+ :rtype: str
+ """
+ if self.__h5py_item is None:
+ raise RuntimeError("h5py_item is not defined")
+
+ result = []
+ item = self.__h5py_item
+ while item is not None:
+ if issubclass(item.h5pyClass, h5py.File):
+ break
+ result.append(item.basename)
+ item = item.parent
+ if item is None:
+ raise RuntimeError("The item does not have parent holding h5py.File")
+ if result == []:
+ return "/"
+ result.append("")
+ result.reverse()
+ return "/".join(result)
+
+ def __file_item(self):
+ """Returns the parent item holding the :class:`h5py.File` object
+
+ :rtype: h5py.File
+ :raises RuntimeException: If no file are found
+ """
+ item = self.__h5py_item
+ while item is not None:
+ if issubclass(item.h5pyClass, h5py.File):
+ return item
+ item = item.parent
+ raise RuntimeError("The item does not have parent holding h5py.File")
+
+ @property
+ def local_file(self):
+ """Returns the local :class:`h5py.File` object.
+
+ For path containing external links, this file is not equal to the h5py
+ one.
+
+ :rtype: h5py.File
+ :raises RuntimeException: If no file are found
+ """
+ item = self.__file_item()
+ return item.obj
+
+ @property
+ def local_filename(self):
+ """Returns the local filename of the h5py node.
+
+ For path containing external links, this path is not equal to the
+ filename provided by h5py.
+
+ :rtype: str
+ :raises RuntimeException: If no file are found
+ """
+ return self.local_file.filename
+
+ @property
+ def local_basename(self):
+ """Returns the local filename of the h5py node.
+
+ For path containing links, this basename can be different than the
+ basename provided by h5py.
+
+ :rtype: str
+ """
+ if issubclass(self.__h5py_item.h5pyClass, h5py.File):
+ return ""
+ return self.__h5py_item.basename
diff --git a/silx/gui/hdf5/setup.py b/silx/gui/hdf5/setup.py
new file mode 100644
index 0000000..786a851
--- /dev/null
+++ b/silx/gui/hdf5/setup.py
@@ -0,0 +1,41 @@
+# 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__ = "28/09/2016"
+
+
+from numpy.distutils.misc_util import Configuration
+
+
+def configuration(parent_package='', top_path=None):
+ config = Configuration('hdf5', parent_package, top_path)
+ config.add_subpackage('test')
+ return config
+
+
+if __name__ == "__main__":
+ from numpy.distutils.core import setup
+ setup(configuration=configuration)
diff --git a/silx/gui/hdf5/test/__init__.py b/silx/gui/hdf5/test/__init__.py
new file mode 100644
index 0000000..3000d96
--- /dev/null
+++ b/silx/gui/hdf5/test/__init__.py
@@ -0,0 +1,39 @@
+# 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.
+#
+# ###########################################################################*/
+import unittest
+
+from . import test_hdf5
+
+
+__authors__ = ["V. Valls"]
+__license__ = "MIT"
+__date__ = "28/09/2016"
+
+
+def suite():
+ test_suite = unittest.TestSuite()
+ test_suite.addTests(
+ [test_hdf5.suite()])
+ return test_suite
diff --git a/silx/gui/hdf5/test/_mock.py b/silx/gui/hdf5/test/_mock.py
new file mode 100644
index 0000000..eada590
--- /dev/null
+++ b/silx/gui/hdf5/test/_mock.py
@@ -0,0 +1,130 @@
+# 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.
+#
+# ###########################################################################*/
+"""Mock for silx.gui.hdf5 module"""
+
+__authors__ = ["V. Valls"]
+__license__ = "MIT"
+__date__ = "12/04/2017"
+
+
+import numpy
+try:
+ import h5py
+except ImportError:
+ h5py = None
+
+
+class Node(object):
+
+ def __init__(self, basename, parent, h5py_class):
+ self.basename = basename
+ self.h5py_class = h5py_class
+ self.attrs = {}
+ self.parent = parent
+ if parent is not None:
+ self.parent._add(self)
+
+ @property
+ def name(self):
+ if self.parent is None:
+ return self.basename
+ if self.parent.name == "":
+ return self.basename
+ return self.parent.name + "/" + self.basename
+
+ @property
+ def file(self):
+ if self.parent is None:
+ return self
+ return self.parent.file
+
+
+class Group(Node):
+ """Mock an h5py Group"""
+
+ def __init__(self, name, parent, h5py_class=h5py.Group):
+ super(Group, self).__init__(name, parent, h5py_class)
+ self.__items = {}
+
+ def _add(self, node):
+ self.__items[node.basename] = node
+
+ def __getitem__(self, key):
+ return self.__items[key]
+
+ def __iter__(self):
+ for k in self.__items:
+ yield k
+
+ def __len__(self):
+ return len(self.__items)
+
+ def get(self, name, getclass=False, getlink=False):
+ result = self.__items[name]
+ if getclass:
+ return result.h5py_class
+ return result
+
+ def create_dataset(self, name, data):
+ return Dataset(name, self, data)
+
+ def create_group(self, name):
+ return Group(name, self)
+
+ def create_NXentry(self, name):
+ group = Group(name, self)
+ group.attrs["NX_class"] = "NXentry"
+ return group
+
+
+class File(Group):
+ """Mock an h5py File"""
+
+ def __init__(self, filename):
+ super(File, self).__init__("", None, h5py.File)
+ self.filename = filename
+
+
+class Dataset(Node):
+ """Mock an h5py Dataset"""
+
+ def __init__(self, name, parent, value):
+ super(Dataset, self).__init__(name, parent, h5py.Dataset)
+ self.__value = value
+ self.shape = self.__value.shape
+ self.dtype = self.__value.dtype
+ self.size = self.__value.size
+ self.compression = None
+ self.compression_opts = None
+
+ def __getitem__(self, key):
+ if not isinstance(self.__value, numpy.ndarray):
+ if key == tuple():
+ return self.__value
+ elif key == Ellipsis:
+ return numpy.array(self.__value)
+ else:
+ raise ValueError("Bad key")
+ return self.__value[key]
diff --git a/silx/gui/hdf5/test/test_hdf5.py b/silx/gui/hdf5/test/test_hdf5.py
new file mode 100644
index 0000000..3bf4897
--- /dev/null
+++ b/silx/gui/hdf5/test/test_hdf5.py
@@ -0,0 +1,480 @@
+# 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.
+#
+# ###########################################################################*/
+"""Test for silx.gui.hdf5 module"""
+
+__authors__ = ["V. Valls"]
+__license__ = "MIT"
+__date__ = "12/04/2017"
+
+
+import time
+import os
+import unittest
+import tempfile
+import numpy
+from contextlib import contextmanager
+from silx.gui import qt
+from silx.gui.test.utils import TestCaseQt
+from silx.gui import hdf5
+from . import _mock
+
+try:
+ import h5py
+except ImportError:
+ h5py = None
+
+
+_called = 0
+
+
+class _Holder(object):
+ def callback(self, *args, **kvargs):
+ _called += 1
+
+
+class TestHdf5TreeModel(TestCaseQt):
+
+ def setUp(self):
+ super(TestHdf5TreeModel, self).setUp()
+ if h5py is None:
+ self.skipTest("h5py is not available")
+
+ @contextmanager
+ def h5TempFile(self):
+ # create tmp file
+ fd, tmp_name = tempfile.mkstemp(suffix=".h5")
+ os.close(fd)
+ # create h5 data
+ h5file = h5py.File(tmp_name, "w")
+ g = h5file.create_group("arrays")
+ g.create_dataset("scalar", data=10)
+ h5file.close()
+ yield tmp_name
+ # clean up
+ os.unlink(tmp_name)
+
+ def testCreate(self):
+ model = hdf5.Hdf5TreeModel()
+ self.assertIsNotNone(model)
+
+ def testAppendFilename(self):
+ with self.h5TempFile() as filename:
+ model = hdf5.Hdf5TreeModel()
+ self.assertEquals(model.rowCount(qt.QModelIndex()), 0)
+ model.appendFile(filename)
+ self.assertEquals(model.rowCount(qt.QModelIndex()), 1)
+ # clean up
+ index = model.index(0, 0, qt.QModelIndex())
+ h5File = model.data(index, hdf5.Hdf5TreeModel.H5PY_OBJECT_ROLE)
+ h5File.close()
+
+ def testAppendBadFilename(self):
+ model = hdf5.Hdf5TreeModel()
+ self.assertRaises(IOError, model.appendFile, "#%$")
+
+ def testInsertFilename(self):
+ with self.h5TempFile() as filename:
+ model = hdf5.Hdf5TreeModel()
+ self.assertEquals(model.rowCount(qt.QModelIndex()), 0)
+ model.insertFile(filename)
+ self.assertEquals(model.rowCount(qt.QModelIndex()), 1)
+ # clean up
+ index = model.index(0, 0, qt.QModelIndex())
+ h5File = model.data(index, hdf5.Hdf5TreeModel.H5PY_OBJECT_ROLE)
+ h5File.close()
+
+ def testInsertFilenameAsync(self):
+ with self.h5TempFile() as filename:
+ model = hdf5.Hdf5TreeModel()
+ self.assertEquals(model.rowCount(qt.QModelIndex()), 0)
+ model.insertFileAsync(filename)
+ index = model.index(0, 0, qt.QModelIndex())
+ self.assertIsInstance(model.nodeFromIndex(index), hdf5.Hdf5LoadingItem.Hdf5LoadingItem)
+ time.sleep(0.1)
+ self.qapp.processEvents()
+ time.sleep(0.1)
+ index = model.index(0, 0, qt.QModelIndex())
+ self.assertIsInstance(model.nodeFromIndex(index), hdf5.Hdf5Item.Hdf5Item)
+ self.assertEquals(model.rowCount(qt.QModelIndex()), 1)
+ # clean up
+ index = model.index(0, 0, qt.QModelIndex())
+ h5File = model.data(index, hdf5.Hdf5TreeModel.H5PY_OBJECT_ROLE)
+ h5File.close()
+
+ def testInsertObject(self):
+ h5 = _mock.File("/foo/bar/1.mock")
+ model = hdf5.Hdf5TreeModel()
+ self.assertEquals(model.rowCount(qt.QModelIndex()), 0)
+ model.insertH5pyObject(h5)
+ self.assertEquals(model.rowCount(qt.QModelIndex()), 1)
+
+ def testRemoveObject(self):
+ h5 = _mock.File("/foo/bar/1.mock")
+ model = hdf5.Hdf5TreeModel()
+ self.assertEquals(model.rowCount(qt.QModelIndex()), 0)
+ model.insertH5pyObject(h5)
+ self.assertEquals(model.rowCount(qt.QModelIndex()), 1)
+ model.removeH5pyObject(h5)
+ self.assertEquals(model.rowCount(qt.QModelIndex()), 0)
+
+ def testSynchronizeObject(self):
+ with self.h5TempFile() as filename:
+ h5 = h5py.File(filename)
+ model = hdf5.Hdf5TreeModel()
+ model.insertH5pyObject(h5)
+ self.assertEquals(model.rowCount(qt.QModelIndex()), 1)
+ index = model.index(0, 0, qt.QModelIndex())
+ node1 = model.nodeFromIndex(index)
+ model.synchronizeH5pyObject(h5)
+ index = model.index(0, 0, qt.QModelIndex())
+ node2 = model.nodeFromIndex(index)
+ self.assertIsNot(node1, node2)
+ # after sync
+ time.sleep(0.1)
+ self.qapp.processEvents()
+ time.sleep(0.1)
+ index = model.index(0, 0, qt.QModelIndex())
+ self.assertIsInstance(model.nodeFromIndex(index), hdf5.Hdf5Item.Hdf5Item)
+ # clean up
+ index = model.index(0, 0, qt.QModelIndex())
+ h5File = model.data(index, hdf5.Hdf5TreeModel.H5PY_OBJECT_ROLE)
+ h5File.close()
+
+ def testFileMoveState(self):
+ model = hdf5.Hdf5TreeModel()
+ self.assertEquals(model.isFileMoveEnabled(), True)
+ model.setFileMoveEnabled(False)
+ self.assertEquals(model.isFileMoveEnabled(), False)
+
+ def testFileDropState(self):
+ model = hdf5.Hdf5TreeModel()
+ self.assertEquals(model.isFileDropEnabled(), True)
+ model.setFileDropEnabled(False)
+ self.assertEquals(model.isFileDropEnabled(), False)
+
+ def testSupportedDrop(self):
+ model = hdf5.Hdf5TreeModel()
+ self.assertNotEquals(model.supportedDropActions(), 0)
+
+ model.setFileMoveEnabled(False)
+ model.setFileDropEnabled(False)
+ self.assertEquals(model.supportedDropActions(), 0)
+
+ model.setFileMoveEnabled(False)
+ model.setFileDropEnabled(True)
+ self.assertNotEquals(model.supportedDropActions(), 0)
+
+ model.setFileMoveEnabled(True)
+ model.setFileDropEnabled(False)
+ self.assertNotEquals(model.supportedDropActions(), 0)
+
+ def testDropExternalFile(self):
+ with self.h5TempFile() as filename:
+ model = hdf5.Hdf5TreeModel()
+ mimeData = qt.QMimeData()
+ mimeData.setUrls([qt.QUrl.fromLocalFile(filename)])
+ model.dropMimeData(mimeData, qt.Qt.CopyAction, 0, 0, qt.QModelIndex())
+ self.assertEquals(model.rowCount(qt.QModelIndex()), 1)
+ # after sync
+ time.sleep(0.1)
+ self.qapp.processEvents()
+ time.sleep(0.1)
+ index = model.index(0, 0, qt.QModelIndex())
+ self.assertIsInstance(model.nodeFromIndex(index), hdf5.Hdf5Item.Hdf5Item)
+ # clean up
+ index = model.index(0, 0, qt.QModelIndex())
+ h5File = model.data(index, role=hdf5.Hdf5TreeModel.H5PY_OBJECT_ROLE)
+ h5File.close()
+
+ def getRowDataAsDict(self, model, row):
+ displayed = {}
+ roles = [qt.Qt.DisplayRole, qt.Qt.DecorationRole, qt.Qt.ToolTipRole, qt.Qt.TextAlignmentRole]
+ for column in range(0, model.columnCount(qt.QModelIndex())):
+ index = model.index(0, column, qt.QModelIndex())
+ for role in roles:
+ datum = model.data(index, role)
+ displayed[column, role] = datum
+ return displayed
+
+ def getItemName(self, model, row):
+ index = model.index(row, hdf5.Hdf5TreeModel.NAME_COLUMN, qt.QModelIndex())
+ return model.data(index, qt.Qt.DisplayRole)
+
+ def testFileData(self):
+ h5 = _mock.File("/foo/bar/1.mock")
+ model = hdf5.Hdf5TreeModel()
+ model.insertH5pyObject(h5)
+ displayed = self.getRowDataAsDict(model, row=0)
+ self.assertEquals(displayed[hdf5.Hdf5TreeModel.NAME_COLUMN, qt.Qt.DisplayRole], "1.mock")
+ self.assertIsInstance(displayed[hdf5.Hdf5TreeModel.NAME_COLUMN, qt.Qt.DecorationRole], qt.QIcon)
+ self.assertEquals(displayed[hdf5.Hdf5TreeModel.TYPE_COLUMN, qt.Qt.DisplayRole], "")
+ self.assertEquals(displayed[hdf5.Hdf5TreeModel.SHAPE_COLUMN, qt.Qt.DisplayRole], "")
+ self.assertEquals(displayed[hdf5.Hdf5TreeModel.VALUE_COLUMN, qt.Qt.DisplayRole], "")
+ self.assertEquals(displayed[hdf5.Hdf5TreeModel.DESCRIPTION_COLUMN, qt.Qt.DisplayRole], "")
+ self.assertEquals(displayed[hdf5.Hdf5TreeModel.NODE_COLUMN, qt.Qt.DisplayRole], "File")
+
+ def testGroupData(self):
+ h5 = _mock.File("/foo/bar/1.mock")
+ d = h5.create_group("foo")
+ d.attrs["desc"] = "fooo"
+
+ model = hdf5.Hdf5TreeModel()
+ model.insertH5pyObject(d)
+ displayed = self.getRowDataAsDict(model, row=0)
+ self.assertEquals(displayed[hdf5.Hdf5TreeModel.NAME_COLUMN, qt.Qt.DisplayRole], "1.mock::foo")
+ self.assertIsInstance(displayed[hdf5.Hdf5TreeModel.NAME_COLUMN, qt.Qt.DecorationRole], qt.QIcon)
+ self.assertEquals(displayed[hdf5.Hdf5TreeModel.TYPE_COLUMN, qt.Qt.DisplayRole], "")
+ self.assertEquals(displayed[hdf5.Hdf5TreeModel.SHAPE_COLUMN, qt.Qt.DisplayRole], "")
+ self.assertEquals(displayed[hdf5.Hdf5TreeModel.VALUE_COLUMN, qt.Qt.DisplayRole], "")
+ self.assertEquals(displayed[hdf5.Hdf5TreeModel.DESCRIPTION_COLUMN, qt.Qt.DisplayRole], "fooo")
+ self.assertEquals(displayed[hdf5.Hdf5TreeModel.NODE_COLUMN, qt.Qt.DisplayRole], "Group")
+
+ def testDatasetData(self):
+ h5 = _mock.File("/foo/bar/1.mock")
+ value = numpy.array([1, 2, 3])
+ d = h5.create_dataset("foo", value)
+
+ model = hdf5.Hdf5TreeModel()
+ model.insertH5pyObject(d)
+ displayed = self.getRowDataAsDict(model, row=0)
+ self.assertEquals(displayed[hdf5.Hdf5TreeModel.NAME_COLUMN, qt.Qt.DisplayRole], "1.mock::foo")
+ self.assertIsInstance(displayed[hdf5.Hdf5TreeModel.NAME_COLUMN, qt.Qt.DecorationRole], qt.QIcon)
+ self.assertEquals(displayed[hdf5.Hdf5TreeModel.TYPE_COLUMN, qt.Qt.DisplayRole], value.dtype.name)
+ self.assertEquals(displayed[hdf5.Hdf5TreeModel.SHAPE_COLUMN, qt.Qt.DisplayRole], "3")
+ self.assertEquals(displayed[hdf5.Hdf5TreeModel.VALUE_COLUMN, qt.Qt.DisplayRole], "[1 2 3]")
+ self.assertEquals(displayed[hdf5.Hdf5TreeModel.DESCRIPTION_COLUMN, qt.Qt.DisplayRole], "")
+ self.assertEquals(displayed[hdf5.Hdf5TreeModel.NODE_COLUMN, qt.Qt.DisplayRole], "Dataset")
+
+ def testDropLastAsFirst(self):
+ model = hdf5.Hdf5TreeModel()
+ h5_1 = _mock.File("/foo/bar/1.mock")
+ h5_2 = _mock.File("/foo/bar/2.mock")
+ model.insertH5pyObject(h5_1)
+ model.insertH5pyObject(h5_2)
+ self.assertEquals(self.getItemName(model, 0), "1.mock")
+ self.assertEquals(self.getItemName(model, 1), "2.mock")
+ index = model.index(1, 0, qt.QModelIndex())
+ mimeData = model.mimeData([index])
+ model.dropMimeData(mimeData, qt.Qt.MoveAction, 0, 0, qt.QModelIndex())
+ self.assertEquals(self.getItemName(model, 0), "2.mock")
+ self.assertEquals(self.getItemName(model, 1), "1.mock")
+
+ def testDropFirstAsLast(self):
+ model = hdf5.Hdf5TreeModel()
+ h5_1 = _mock.File("/foo/bar/1.mock")
+ h5_2 = _mock.File("/foo/bar/2.mock")
+ model.insertH5pyObject(h5_1)
+ model.insertH5pyObject(h5_2)
+ self.assertEquals(self.getItemName(model, 0), "1.mock")
+ self.assertEquals(self.getItemName(model, 1), "2.mock")
+ index = model.index(0, 0, qt.QModelIndex())
+ mimeData = model.mimeData([index])
+ model.dropMimeData(mimeData, qt.Qt.MoveAction, 2, 0, qt.QModelIndex())
+ self.assertEquals(self.getItemName(model, 0), "2.mock")
+ self.assertEquals(self.getItemName(model, 1), "1.mock")
+
+ def testRootParent(self):
+ model = hdf5.Hdf5TreeModel()
+ h5_1 = _mock.File("/foo/bar/1.mock")
+ model.insertH5pyObject(h5_1)
+ index = model.index(0, 0, qt.QModelIndex())
+ index = model.parent(index)
+ self.assertEquals(index, qt.QModelIndex())
+
+
+class TestNexusSortFilterProxyModel(TestCaseQt):
+
+ def getChildNames(self, model, index):
+ count = model.rowCount(index)
+ result = []
+ for row in range(0, count):
+ itemIndex = model.index(row, hdf5.Hdf5TreeModel.NAME_COLUMN, index)
+ name = model.data(itemIndex, qt.Qt.DisplayRole)
+ result.append(name)
+ return result
+
+ def testNXentryStartTime(self):
+ """Test NXentry with start_time"""
+ model = hdf5.Hdf5TreeModel()
+ h5 = _mock.File("/foo/bar/1.mock")
+ h5.create_NXentry("a").create_dataset("start_time", numpy.string_("2015"))
+ h5.create_NXentry("b").create_dataset("start_time", numpy.string_("2013"))
+ h5.create_NXentry("c").create_dataset("start_time", numpy.string_("2014"))
+ model.insertH5pyObject(h5)
+
+ proxy = hdf5.NexusSortFilterProxyModel()
+ proxy.setSourceModel(model)
+ proxy.sort(0, qt.Qt.DescendingOrder)
+ names = self.getChildNames(proxy, proxy.index(0, 0, qt.QModelIndex()))
+ self.assertListEqual(names, ["a", "c", "b"])
+
+ def testNXentryStartTimeInArray(self):
+ """Test NXentry with start_time"""
+ model = hdf5.Hdf5TreeModel()
+ h5 = _mock.File("/foo/bar/1.mock")
+ h5.create_NXentry("a").create_dataset("start_time", numpy.array([numpy.string_("2015")]))
+ h5.create_NXentry("b").create_dataset("start_time", numpy.array([numpy.string_("2013")]))
+ h5.create_NXentry("c").create_dataset("start_time", numpy.array([numpy.string_("2014")]))
+ model.insertH5pyObject(h5)
+
+ proxy = hdf5.NexusSortFilterProxyModel()
+ proxy.setSourceModel(model)
+ proxy.sort(0, qt.Qt.DescendingOrder)
+ names = self.getChildNames(proxy, proxy.index(0, 0, qt.QModelIndex()))
+ self.assertListEqual(names, ["a", "c", "b"])
+
+ def testNXentryEndTimeInArray(self):
+ """Test NXentry with end_time"""
+ model = hdf5.Hdf5TreeModel()
+ h5 = _mock.File("/foo/bar/1.mock")
+ h5.create_NXentry("a").create_dataset("end_time", numpy.array([numpy.string_("2015")]))
+ h5.create_NXentry("b").create_dataset("end_time", numpy.array([numpy.string_("2013")]))
+ h5.create_NXentry("c").create_dataset("end_time", numpy.array([numpy.string_("2014")]))
+ model.insertH5pyObject(h5)
+
+ proxy = hdf5.NexusSortFilterProxyModel()
+ proxy.setSourceModel(model)
+ proxy.sort(0, qt.Qt.DescendingOrder)
+ names = self.getChildNames(proxy, proxy.index(0, 0, qt.QModelIndex()))
+ self.assertListEqual(names, ["a", "c", "b"])
+
+ def testNXentryName(self):
+ """Test NXentry without start_time or end_time"""
+ model = hdf5.Hdf5TreeModel()
+ h5 = _mock.File("/foo/bar/1.mock")
+ h5.create_NXentry("a")
+ h5.create_NXentry("c")
+ h5.create_NXentry("b")
+ model.insertH5pyObject(h5)
+
+ proxy = hdf5.NexusSortFilterProxyModel()
+ proxy.setSourceModel(model)
+ proxy.sort(0, qt.Qt.AscendingOrder)
+ names = self.getChildNames(proxy, proxy.index(0, 0, qt.QModelIndex()))
+ self.assertListEqual(names, ["a", "b", "c"])
+
+ def testStartTime(self):
+ """If it is not NXentry, start_time is not used"""
+ model = hdf5.Hdf5TreeModel()
+ h5 = _mock.File("/foo/bar/1.mock")
+ h5.create_group("a").create_dataset("start_time", numpy.string_("2015"))
+ h5.create_group("b").create_dataset("start_time", numpy.string_("2013"))
+ h5.create_group("c").create_dataset("start_time", numpy.string_("2014"))
+ model.insertH5pyObject(h5)
+
+ proxy = hdf5.NexusSortFilterProxyModel()
+ proxy.setSourceModel(model)
+ proxy.sort(0, qt.Qt.AscendingOrder)
+ names = self.getChildNames(proxy, proxy.index(0, 0, qt.QModelIndex()))
+ self.assertListEqual(names, ["a", "b", "c"])
+
+ def testName(self):
+ model = hdf5.Hdf5TreeModel()
+ h5 = _mock.File("/foo/bar/1.mock")
+ h5.create_group("a")
+ h5.create_group("c")
+ h5.create_group("b")
+ model.insertH5pyObject(h5)
+
+ proxy = hdf5.NexusSortFilterProxyModel()
+ proxy.setSourceModel(model)
+ proxy.sort(0, qt.Qt.AscendingOrder)
+ names = self.getChildNames(proxy, proxy.index(0, 0, qt.QModelIndex()))
+ self.assertListEqual(names, ["a", "b", "c"])
+
+ def testNumber(self):
+ model = hdf5.Hdf5TreeModel()
+ h5 = _mock.File("/foo/bar/1.mock")
+ h5.create_group("a1")
+ h5.create_group("a20")
+ h5.create_group("a3")
+ model.insertH5pyObject(h5)
+
+ proxy = hdf5.NexusSortFilterProxyModel()
+ proxy.setSourceModel(model)
+ proxy.sort(0, qt.Qt.AscendingOrder)
+ names = self.getChildNames(proxy, proxy.index(0, 0, qt.QModelIndex()))
+ self.assertListEqual(names, ["a1", "a3", "a20"])
+
+ def testMultiNumber(self):
+ model = hdf5.Hdf5TreeModel()
+ h5 = _mock.File("/foo/bar/1.mock")
+ h5.create_group("a1-1")
+ h5.create_group("a20-1")
+ h5.create_group("a3-1")
+ h5.create_group("a3-20")
+ h5.create_group("a3-3")
+ model.insertH5pyObject(h5)
+
+ proxy = hdf5.NexusSortFilterProxyModel()
+ proxy.setSourceModel(model)
+ proxy.sort(0, qt.Qt.AscendingOrder)
+ names = self.getChildNames(proxy, proxy.index(0, 0, qt.QModelIndex()))
+ self.assertListEqual(names, ["a1-1", "a3-1", "a3-3", "a3-20", "a20-1"])
+
+ def testUnconsistantTypes(self):
+ model = hdf5.Hdf5TreeModel()
+ h5 = _mock.File("/foo/bar/1.mock")
+ h5.create_group("aaa100")
+ h5.create_group("100aaa")
+ model.insertH5pyObject(h5)
+
+ proxy = hdf5.NexusSortFilterProxyModel()
+ proxy.setSourceModel(model)
+ proxy.sort(0, qt.Qt.AscendingOrder)
+ names = self.getChildNames(proxy, proxy.index(0, 0, qt.QModelIndex()))
+ self.assertListEqual(names, ["100aaa", "aaa100"])
+
+
+class TestHdf5(TestCaseQt):
+ """Test to check that icons module."""
+
+ def setUp(self):
+ super(TestHdf5, self).setUp()
+ if h5py is None:
+ self.skipTest("h5py is not available")
+
+ def testCreate(self):
+ view = hdf5.Hdf5TreeView()
+ self.assertIsNotNone(view)
+
+ def testContextMenu(self):
+ view = hdf5.Hdf5TreeView()
+ view._createContextMenu(qt.QPoint(0, 0))
+
+
+def suite():
+ test_suite = unittest.TestSuite()
+ test_suite.addTest(
+ unittest.defaultTestLoader.loadTestsFromTestCase(TestHdf5TreeModel))
+ test_suite.addTest(
+ unittest.defaultTestLoader.loadTestsFromTestCase(TestNexusSortFilterProxyModel))
+ test_suite.addTest(
+ unittest.defaultTestLoader.loadTestsFromTestCase(TestHdf5))
+ return test_suite
+
+
+if __name__ == '__main__':
+ unittest.main(defaultTest='suite')