Skip to main content
`Ctrl`+`K`

# Source code for silx.gui.hdf5.Hdf5TreeView

    # /*##########################################################################
    #
    # Copyright (c) 2016-2021 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__ = "30/04/2018"

    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.

        .. image:: img/Hdf5TreeView.png

        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 = self.createDefaultModel()
            self.setModel(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 createDefaultModel(self):
            """Creates and returns the default model.

            Inherite to custom the default model"""
            model = Hdf5TreeModel(self)
            proxy_model = NexusSortFilterProxyModel(self)
            proxy_model.setSourceModel(model)
            return proxy_model

        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 Exception:
                    # make sure no user callback crash the application
                    _logger.error("Error while calling callback", exc_info=True)
                    pass

            if not menu.isEmpty():
                for action in actions:
                    menu.addAction(action)
                menu.popup(self.viewport().mapToGlobal(pos))

        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 __intermediateModels(self, index):
            """Returns intermediate models from the view model to the
            model of the index."""
            models = []
            targetModel = index.model()
            model = self.model()
            while model is not None:
                if model is targetModel:
                    # found
                    return models
                models.append(model)
                if isinstance(model, qt.QAbstractProxyModel):
                    model = model.sourceModel()
                else:
                    break
            raise RuntimeError(
                "Model from the requested index is not reachable from this view"
            )

        def mapToModel(self, index):
            """Map an index from any model reachable by the view to an index from
            the very first model connected to the view.

            :param qt.QModelIndex index: Index from the Hdf5Tree model
            :rtype: qt.QModelIndex
            :return: Index from the model connected to the view
            """
            if not index.isValid():
                return index
            models = self.__intermediateModels(index)
            for model in reversed(models):
                index = model.mapFromSource(index)
            return index

        def setSelectedH5Node(self, h5Object):
            """
            Select the specified node of the tree using an h5py node.

            - If the item is found, parent items are expended, and then the item
              is selected.
            - If the item is not found, the selection do not change.
            - A none argument allow to deselect everything

            :param h5py.Node h5Object: The node to select
            """
            if h5Object is None:
                self.setCurrentIndex(qt.QModelIndex())
                return

            model = self.findHdf5TreeModel()
            index = model.indexFromH5Object(h5Object)
            index = self.mapToModel(index)
            if index.isValid():
                # Update the GUI
                i = index
                while i.isValid():
                    self.expand(i)
                    i = i.parent()
                self.setCurrentIndex(index)

        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:
                qindex = self.indexAt(event.pos())
                self.clicked.emit(qindex)