hdf5widget.py#

Sample code demonstrating silx.gui.hdf5 widgets:

#!/usr/bin/env python
# /*##########################################################################
#
# 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.
#
# ###########################################################################*/
"""Qt Hdf5 widget examples"""

import html
import logging
import sys
import tempfile

import numpy

logging.basicConfig()
_logger = logging.getLogger("hdf5widget")
"""Module logger"""

try:
    # it should be loaded before h5py
    import hdf5plugin  # noqa
except ImportError:
    message = (
        "Module 'hdf5plugin' is not installed. It supports some hdf5"
        + ' compressions. You can install it using "pip install hdf5plugin".'
    )
    _logger.warning(message)
import h5py

import silx.gui.hdf5
from silx.gui import qt
from silx.gui.data.DataViewerFrame import DataViewerFrame
from silx.gui.widgets.ThreadPoolPushButton import ThreadPoolPushButton

import fabio


_file_cache = {}


def str_attrs(str_list):
    """Return a numpy array of unicode strings"""
    text_dtype = h5py.special_dtype(vlen=str)
    return numpy.array(str_list, dtype=text_dtype)


def get_hdf5_with_all_types():
    ID = "alltypes"
    if ID in _file_cache:
        return _file_cache[ID].name

    tmp = tempfile.NamedTemporaryFile(prefix=ID + "_", suffix=".h5", delete=True)
    tmp.file.close()
    h5 = h5py.File(tmp.name, "w")

    g = h5.create_group("arrays")
    g.create_dataset("scalar", data=10)
    g.create_dataset("list", data=numpy.arange(10))
    base_image = numpy.arange(10**2).reshape(10, 10)
    images = [
        base_image,
        base_image.T,
        base_image.size - 1 - base_image,
        base_image.size - 1 - base_image.T,
    ]
    dtype = images[0].dtype
    data = numpy.empty((10 * 10, 10, 10), dtype=dtype)
    for i in range(10 * 10):
        data[i] = images[i % 4]
    data.shape = 10, 10, 10, 10
    g.create_dataset("image", data=data[0, 0])
    g.create_dataset("cube", data=data[0])
    g.create_dataset("hypercube", data=data)
    g = h5.create_group("dtypes")
    g.create_dataset("int32", data=numpy.int32(10))
    g.create_dataset("int64", data=numpy.int64(10))
    g.create_dataset("float32", data=numpy.float32(10))
    g.create_dataset("float64", data=numpy.float64(10))
    g.create_dataset("string_", data=numpy.bytes_("Hi!"))
    # g.create_dataset("string0",data=numpy.string0("Hi!\x00"))

    g.create_dataset("bool", data=True)
    g.create_dataset("bool2", data=False)
    h5.close()

    _file_cache[ID] = tmp
    return tmp.name


def get_hdf5_with_all_links():
    ID = "alllinks"
    if ID in _file_cache:
        return _file_cache[ID].name

    tmp = tempfile.NamedTemporaryFile(prefix=ID + "_", suffix=".h5", delete=True)
    tmp.file.close()
    h5 = h5py.File(tmp.name, "w")

    g = h5.create_group("group")
    g.create_dataset("dataset", data=numpy.int64(10))
    h5.create_dataset("dataset", data=numpy.int64(10))

    h5["hard_link_to_group"] = h5["/group"]
    h5["hard_link_to_dataset"] = h5["/dataset"]

    h5["soft_link_to_group"] = h5py.SoftLink("/group")
    h5["soft_link_to_dataset"] = h5py.SoftLink("/dataset")
    h5["soft_link_to_nothing"] = h5py.SoftLink("/foo/bar/2000")

    alltypes_filename = get_hdf5_with_all_types()

    h5["external_link_to_group"] = h5py.ExternalLink(alltypes_filename, "/arrays")
    h5["external_link_to_dataset"] = h5py.ExternalLink(
        alltypes_filename, "/arrays/cube"
    )
    h5["external_link_to_nothing"] = h5py.ExternalLink(
        alltypes_filename, "/foo/bar/2000"
    )
    h5["external_link_to_missing_file"] = h5py.ExternalLink("missing_file.h5", "/")
    h5.close()

    _file_cache[ID] = tmp
    return tmp.name


def get_hdf5_with_1000_datasets():
    ID = "dataset1000"
    if ID in _file_cache:
        return _file_cache[ID].name

    tmp = tempfile.NamedTemporaryFile(prefix=ID + "_", suffix=".h5", delete=True)
    tmp.file.close()
    h5 = h5py.File(tmp.name, "w")

    g = h5.create_group("group")
    for i in range(1000):
        g.create_dataset("dataset%i" % i, data=numpy.int64(10))
    h5.close()

    _file_cache[ID] = tmp
    return tmp.name


def get_hdf5_with_10000_datasets():
    ID = "dataset10000"
    if ID in _file_cache:
        return _file_cache[ID].name

    tmp = tempfile.NamedTemporaryFile(prefix=ID + "_", suffix=".h5", delete=True)
    tmp.file.close()
    h5 = h5py.File(tmp.name, "w")

    g = h5.create_group("group")
    for i in range(10000):
        g.create_dataset("dataset%i" % i, data=numpy.int64(10))
    h5.close()

    _file_cache[ID] = tmp
    return tmp.name


def get_hdf5_with_100000_datasets():
    ID = "dataset100000"
    if ID in _file_cache:
        return _file_cache[ID].name

    tmp = tempfile.NamedTemporaryFile(prefix=ID + "_", suffix=".h5", delete=True)
    tmp.file.close()
    h5 = h5py.File(tmp.name, "w")

    g = h5.create_group("group")
    for i in range(100000):
        g.create_dataset("dataset%i" % i, data=numpy.int64(10))
    h5.close()

    _file_cache[ID] = tmp
    return tmp.name


def get_hdf5_with_recursive_links():
    ID = "recursive_links"
    if ID in _file_cache:
        return _file_cache[ID].name

    tmp = tempfile.NamedTemporaryFile(prefix=ID + "_", suffix=".h5", delete=True)
    tmp.file.close()
    h5 = h5py.File(tmp.name, "w")

    g = h5.create_group("group")
    g.create_dataset("dataset", data=numpy.int64(10))
    h5.create_dataset("dataset", data=numpy.int64(10))

    h5["hard_recursive_link"] = h5["/group"]
    g["recursive"] = h5["hard_recursive_link"]
    h5["hard_link_to_dataset"] = h5["/dataset"]

    h5["soft_link_to_group"] = h5py.SoftLink("/group")
    h5["soft_link_to_link"] = h5py.SoftLink("/soft_link_to_group")
    h5["soft_link_to_itself"] = h5py.SoftLink("/soft_link_to_itself")
    h5.close()

    _file_cache[ID] = tmp
    return tmp.name


def get_hdf5_with_external_recursive_links():
    ID = "external_recursive_links"
    if ID in _file_cache:
        return _file_cache[ID][0].name

    tmp1 = tempfile.NamedTemporaryFile(prefix=ID + "_", suffix=".h5", delete=True)
    tmp1.file.close()
    h5_1 = h5py.File(tmp1.name, "w")

    tmp2 = tempfile.NamedTemporaryFile(prefix=ID + "_", suffix=".h5", delete=True)
    tmp2.file.close()
    h5_2 = h5py.File(tmp2.name, "w")

    g = h5_1.create_group("group")
    g.create_dataset("dataset", data=numpy.int64(10))
    h5_1["soft_link_to_group"] = h5py.SoftLink("/group")
    h5_1["external_link_to_link"] = h5py.ExternalLink(tmp2.name, "/soft_link_to_group")
    h5_1["external_link_to_recursive_link"] = h5py.ExternalLink(
        tmp2.name, "/external_link_to_recursive_link"
    )
    h5_1.close()

    g = h5_2.create_group("group")
    g.create_dataset("dataset", data=numpy.int64(10))
    h5_2["soft_link_to_group"] = h5py.SoftLink("/group")
    h5_2["external_link_to_link"] = h5py.ExternalLink(tmp1.name, "/soft_link_to_group")
    h5_2["external_link_to_recursive_link"] = h5py.ExternalLink(
        tmp1.name, "/external_link_to_recursive_link"
    )
    h5_2.close()

    _file_cache[ID] = (tmp1, tmp2)
    return tmp1.name


def get_hdf5_with_nxdata():
    ID = "nxdata"
    if ID in _file_cache:
        return _file_cache[ID].name

    tmp = tempfile.NamedTemporaryFile(prefix=ID + "_", suffix=".h5", delete=True)
    tmp.file.close()
    h5 = h5py.File(tmp.name, "w")

    # SCALARS
    g0d = h5.create_group("scalars")

    g0d0 = g0d.create_group("0D_scalar")
    g0d0.attrs["NX_class"] = "NXdata"
    g0d0.attrs["signal"] = "scalar"
    g0d0.create_dataset("scalar", data=10)

    g0d1 = g0d.create_group("2D_scalars")
    g0d1.attrs["NX_class"] = "NXdata"
    g0d1.attrs["signal"] = "scalars"
    ds = g0d1.create_dataset("scalars", data=numpy.arange(3 * 10).reshape((3, 10)))
    ds.attrs["interpretation"] = "scalar"

    g0d1 = g0d.create_group("4D_scalars")
    g0d1.attrs["NX_class"] = "NXdata"
    g0d1.attrs["signal"] = "scalars"
    ds = g0d1.create_dataset(
        "scalars", data=numpy.arange(2 * 2 * 3 * 10).reshape((2, 2, 3, 10))
    )
    ds.attrs["interpretation"] = "scalar"

    # SPECTRA
    g1d = h5.create_group("spectra")

    g1d0 = g1d.create_group("1D_spectrum")
    g1d0.attrs["NX_class"] = "NXdata"
    g1d0.attrs["signal"] = "count"
    g1d0.attrs["auxiliary_signals"] = str_attrs(["count.5", "count2"])
    g1d0.attrs["axes"] = "energy_calib"
    g1d0.attrs["uncertainties"] = str_attrs(["energy_errors"])
    g1d0.create_dataset("count", data=numpy.arange(10))
    g1d0.create_dataset("count.5", data=0.5 * numpy.arange(10))
    d2 = g1d0.create_dataset("count2", data=2 * numpy.arange(10))
    d2.attrs["long_name"] = "count multiplied by 2"
    g1d0.create_dataset("energy_calib", data=(10, 5))  # 10 * idx + 5
    g1d0.create_dataset("energy_errors", data=3.14 * numpy.random.rand(10))
    g1d0.create_dataset("title", data="Title example provided as dataset")

    g1d1 = g1d.create_group("2D_spectra")
    g1d1.attrs["NX_class"] = "NXdata"
    g1d1.attrs["signal"] = "counts"
    ds = g1d1.create_dataset("counts", data=numpy.arange(3 * 10).reshape((3, 10)))
    ds.attrs["interpretation"] = "spectrum"

    g1d2 = g1d.create_group("4D_spectra")
    g1d2.attrs["NX_class"] = "NXdata"
    g1d2.attrs["signal"] = "counts"
    g1d2.attrs["axes"] = str_attrs(["energy"])
    ds = g1d2.create_dataset(
        "counts", data=numpy.arange(2 * 2 * 3 * 10).reshape((2, 2, 3, 10))
    )
    ds.attrs["interpretation"] = "spectrum"
    g1d2.create_dataset("errors", data=4.5 * numpy.random.rand(2, 2, 3, 10))
    ds = g1d2.create_dataset(
        "energy", data=5 + 10 * numpy.arange(15), shuffle=True, compression="gzip"
    )
    ds.attrs["long_name"] = "Calibrated energy"
    ds.attrs["first_good"] = 3
    ds.attrs["last_good"] = 12
    g1d2.create_dataset("energy_errors", data=10 * numpy.random.rand(15))

    # IMAGES
    g2d = h5.create_group("images")

    g2d0 = g2d.create_group("2D_regular_image")
    g2d0.attrs["NX_class"] = "NXdata"
    g2d0.attrs["signal"] = "image"
    g2d0.attrs["auxiliary_signals"] = str_attrs(["image2", "image3"])
    g2d0.attrs["axes"] = str_attrs(["rows_calib", "columns_coordinates"])
    g2d0.attrs["title"] = "Title example provided as group attr"
    g2d0.create_dataset("image", data=numpy.arange(4 * 6).reshape((4, 6)))
    g2d0.create_dataset("image2", data=1 / (1.0 + numpy.arange(4 * 6).reshape((4, 6))))
    ds = g2d0.create_dataset("image3", data=-numpy.arange(4 * 6).reshape((4, 6)))
    ds.attrs["long_name"] = "3rd image (2nd auxiliary)"
    ds = g2d0.create_dataset("rows_calib", data=(10, 5))
    ds.attrs["long_name"] = "Calibrated Y"
    g2d0.create_dataset("columns_coordinates", data=0.5 + 0.02 * numpy.arange(6))

    g2d4 = g2d.create_group("RGBA_image")
    g2d4.attrs["NX_class"] = "NXdata"
    g2d4.attrs["signal"] = "image"
    g2d4.attrs["auxiliary_signals"] = "squared image"
    g2d4.attrs["axes"] = str_attrs(["rows_calib", "columns_coordinates"])
    rgba_image = numpy.linspace(0, 1, num=7 * 8 * 3).reshape((7, 8, 3))
    rgba_image[:, :, 1] = 1 - rgba_image[:, :, 1]  # invert G channel to add some color
    ds = g2d4.create_dataset("image", data=rgba_image)
    ds.attrs["interpretation"] = "rgba-image"
    ds = g2d4.create_dataset("squared image", data=rgba_image**2)
    ds.attrs["interpretation"] = "rgba-image"
    ds = g2d4.create_dataset("rows_calib", data=(10, 5))
    ds.attrs["long_name"] = "Calibrated Y"
    g2d4.create_dataset("columns_coordinates", data=0.5 + 0.02 * numpy.arange(8))

    g2d1 = g2d.create_group("2D_irregular_data")
    g2d1.attrs["NX_class"] = "NXdata"
    g2d1.attrs["signal"] = "data"
    g2d1.attrs["axes"] = str_attrs(["rows_coordinates", "columns_coordinates"])
    g2d1.create_dataset("data", data=numpy.arange(64 * 128).reshape((64, 128)))
    g2d1.create_dataset(
        "rows_coordinates", data=numpy.arange(64) + numpy.random.rand(64)
    )
    g2d1.create_dataset(
        "columns_coordinates", data=numpy.arange(128) + 2.5 * numpy.random.rand(128)
    )

    g2d2 = g2d.create_group("3D_images")
    g2d2.attrs["NX_class"] = "NXdata"
    g2d2.attrs["signal"] = "images"
    ds = g2d2.create_dataset("images", data=numpy.arange(2 * 4 * 6).reshape((2, 4, 6)))
    ds.attrs["interpretation"] = "image"

    g2d3 = g2d.create_group("5D_images")
    g2d3.attrs["NX_class"] = "NXdata"
    g2d3.attrs["signal"] = "images"
    g2d3.attrs["axes"] = str_attrs(["rows_coordinates", "columns_coordinates"])
    ds = g2d3.create_dataset(
        "images", data=numpy.arange(2 * 2 * 2 * 4 * 6).reshape((2, 2, 2, 4, 6))
    )
    ds.attrs["interpretation"] = "image"
    g2d3.create_dataset("rows_coordinates", data=5 + 10 * numpy.arange(4))
    g2d3.create_dataset("columns_coordinates", data=0.5 + 0.02 * numpy.arange(6))

    # SCATTER
    g = h5.create_group("scatters")

    gd0 = g.create_group("x_y_scatter")
    gd0.attrs["NX_class"] = "NXdata"
    gd0.attrs["signal"] = "y"
    gd0.attrs["axes"] = str_attrs(["x"])
    gd0.attrs["title"] = "simple y = f(x) scatters cannot be distinguished from curves"
    gd0.create_dataset("y", data=numpy.random.rand(128) - 0.5)
    gd0.create_dataset("x", data=2 * numpy.random.rand(128))
    gd0.create_dataset("x_errors", data=0.05 * numpy.random.rand(128))
    gd0.create_dataset("errors", data=0.05 * numpy.random.rand(128))

    gd1 = g.create_group("x_y_value_scatter")
    gd1.attrs["NX_class"] = "NXdata"
    gd1.attrs["signal"] = "values"
    gd1.attrs["auxiliary_signals"] = str_attrs(["values.5", "values2"])
    gd1.attrs["axes"] = str_attrs(["x", "y"])
    gd1.attrs["title"] = "x, y, values scatter with asymmetric y_errors"
    gd1.create_dataset("values", data=3.14 * numpy.random.rand(128))
    gd1.create_dataset("values.5", data=0.5 * 3.14 * numpy.random.rand(128))
    gd1.create_dataset("values2", data=2.0 * 3.14 * numpy.random.rand(128))
    gd1.create_dataset("y", data=numpy.random.rand(128))
    y_errors = [0.03 * numpy.random.rand(128), 0.04 * numpy.random.rand(128)]
    gd1.create_dataset("y_errors", data=y_errors)
    ds = gd1.create_dataset("x", data=2 * numpy.random.rand(128))
    ds.attrs["long_name"] = "horizontal axis"
    gd1.create_dataset("x_errors", data=0.02 * numpy.random.rand(128))

    # NDIM > 3
    g = h5.create_group("cubes")

    gd0 = g.create_group("3D_cube")
    gd0.attrs["NX_class"] = "NXdata"
    gd0.attrs["signal"] = "cube"
    gd0.attrs["axes"] = str_attrs(["img_idx", "rows_coordinates", "cols_coordinates"])
    gd0.create_dataset("cube", data=numpy.arange(4 * 5 * 6).reshape((4, 5, 6)))
    gd0.create_dataset("img_idx", data=numpy.arange(4))
    gd0.create_dataset("rows_coordinates", data=0.1 * numpy.arange(5))
    gd0.create_dataset("cols_coordinates", data=[0.2, 0.3])  # linear calibration

    gd1 = g.create_group("5D")
    gd1.attrs["NX_class"] = "NXdata"
    gd1.attrs["signal"] = "hypercube"
    gd1.create_dataset(
        "hypercube", data=numpy.arange(2 * 3 * 4 * 5 * 6).reshape((2, 3, 4, 5, 6))
    )

    gd2 = g.create_group("3D_nonlinear_scaling")
    gd2.attrs["NX_class"] = "NXdata"
    gd2.attrs["signal"] = "cube"
    gd2.attrs["axes"] = str_attrs(["img_idx", "rows_coordinates", "cols_coordinates"])
    gd2.create_dataset("cube", data=numpy.arange(4 * 5 * 6).reshape((4, 5, 6)))
    gd2.create_dataset("img_idx", data=numpy.array([2.0, -0.1, 8, 3.14]))
    gd2.create_dataset("rows_coordinates", data=0.1 * numpy.arange(5))
    gd2.create_dataset("cols_coordinates", data=[0.1, 0.6, 0.7, 8.0, 8.1, 8.2])

    # invalid NXdata
    g = h5.create_group("invalid")
    g0 = g.create_group("invalid NXdata")
    g0.attrs["NX_class"] = "NXdata"

    g1 = g.create_group("invalid NXentry")
    g1.attrs["NX_class"] = "NXentry"
    g1.attrs["default"] = "missing NXdata group"

    g2 = g.create_group("invalid NXroot")
    g2.attrs["NX_class"] = "NXroot"
    g2.attrs["default"] = "invalid NXentry in NXroot"
    g20 = g2.create_group("invalid NXentry in NXroot")
    g20.attrs["NX_class"] = "NXentry"
    g20.attrs["default"] = "missing NXdata group"

    h5.close()

    _file_cache[ID] = tmp
    return tmp.name


def get_edf_with_all_types():
    ID = "alltypesedf"
    if ID in _file_cache:
        return _file_cache[ID].name

    tmp = tempfile.NamedTemporaryFile(prefix=ID + "_", suffix=".edf", delete=True)

    header = fabio.fabioimage.OrderedDict()
    header["integer"] = "10"
    header["float"] = "10.5"
    header["string"] = "Hi!"
    header["integer_list"] = "10 20 50"
    header["float_list"] = "1.1 3.14 500.12"
    header["motor_pos"] = "10 2.5 a1"
    header["motor_mne"] = "integer_position float_position named_position"

    data = numpy.array([[10, 50], [50, 10]])
    fabiofile = fabio.edfimage.EdfImage(data, header)
    fabiofile.write(tmp.name)

    _file_cache[ID] = tmp
    return tmp.name


def get_edf_with_100000_frames():
    ID = "frame100000"
    if ID in _file_cache:
        return _file_cache[ID].name

    tmp = tempfile.NamedTemporaryFile(prefix=ID + "_", suffix=".edf", delete=True)

    fabiofile = None
    for framre_id in range(100000):
        data = numpy.array([[framre_id, 50], [50, 10]])
        if fabiofile is None:
            header = fabio.fabioimage.OrderedDict()
            header["nb_frames"] = "100000"
            fabiofile = fabio.edfimage.EdfImage(data, header)
        else:
            header = fabio.fabioimage.OrderedDict()
            header["frame_nb"] = framre_id
            fabiofile.append_frame(fabio.edfimage.Frame(data, header, framre_id))
    fabiofile.write(tmp.name)

    _file_cache[ID] = tmp
    return tmp.name


class Hdf5TreeViewExample(qt.QMainWindow):
    """
    This window show an example of use of a Hdf5TreeView.

    The tree is initialized with a list of filenames. A panel allow to play
    with internal property configuration of the widget, and a text screen
    allow to display events.
    """

    def __init__(self, filenames=None):
        """
        :param files_: List of HDF5 or Spec files (pathes or
            :class:`silx.io.spech5.SpecH5` or :class:`h5py.File`
            instances)
        """
        qt.QMainWindow.__init__(self)
        self.setWindowTitle("Silx HDF5 widget example")

        self.__asyncload = False
        self.__treeview = silx.gui.hdf5.Hdf5TreeView(self)
        """Silx HDF5 TreeView"""
        self.__text = qt.QTextEdit(self)
        """Widget displaying information"""

        self.__dataViewer = DataViewerFrame(self)
        vSpliter = qt.QSplitter(qt.Qt.Vertical)
        vSpliter.addWidget(self.__dataViewer)
        vSpliter.addWidget(self.__text)
        vSpliter.setSizes([10, 0])

        spliter = qt.QSplitter(self)
        spliter.addWidget(self.__treeview)
        spliter.addWidget(vSpliter)
        spliter.setStretchFactor(1, 1)

        main_panel = qt.QWidget(self)
        layout = qt.QVBoxLayout()
        layout.addWidget(spliter)
        layout.addWidget(self.createTreeViewConfigurationPanel(self, self.__treeview))
        layout.setStretchFactor(spliter, 1)
        main_panel.setLayout(layout)

        self.setCentralWidget(main_panel)

        # append all files to the tree
        for file_name in filenames:
            self.__treeview.findHdf5TreeModel().appendFile(file_name)

        self.__treeview.activated.connect(self.displayData)
        self.__treeview.activated.connect(
            lambda index: self.displayEvent("activated", index)
        )
        self.__treeview.clicked.connect(
            lambda index: self.displayEvent("clicked", index)
        )
        self.__treeview.doubleClicked.connect(
            lambda index: self.displayEvent("doubleClicked", index)
        )
        self.__treeview.entered.connect(
            lambda index: self.displayEvent("entered", index)
        )
        self.__treeview.pressed.connect(
            lambda index: self.displayEvent("pressed", index)
        )

        self.__treeview.addContextMenuCallback(self.customContextMenu)
        # lambda function will never be called cause we store it as weakref
        self.__treeview.addContextMenuCallback(lambda event: None)
        # you have to store it first
        self.__store_lambda = lambda event: self.closeAndSyncCustomContextMenu(event)
        self.__treeview.addContextMenuCallback(self.__store_lambda)

    def displayData(self):
        """Called to update the dataviewer with the selected data."""
        selected = list(self.__treeview.selectedH5Nodes())
        if len(selected) == 1:
            # Update the viewer for a single selection
            data = selected[0]
            # data is a hdf5.H5Node object
            # data.h5py_object is a Group/Dataset object (from h5py, spech5, fabioh5)
            # The dataviewer can display both
            self.__dataViewer.setData(data)

    def displayEvent(self, eventName, index):
        """Called to log event in widget"""

        def formatKey(name, value):
            name, value = html.escape(str(name)), html.escape(str(value))
            return "<li><b>%s</b>: %s</li>" % (name, value)

        text = "<html>"
        text += "<h1>Event</h1>"
        text += "<ul>"
        text += formatKey("name", eventName)
        text += formatKey("index", type(index))
        text += "</ul>"

        text += "<h1>Selected HDF5 objects</h1>"

        for h5_obj in self.__treeview.selectedH5Nodes():
            text += "<h2>HDF5 object</h2>"
            text += "<ul>"
            text += formatKey("local_filename", h5_obj.local_file.filename)
            text += formatKey("local_basename", h5_obj.local_basename)
            text += formatKey("local_name", h5_obj.local_name)
            text += formatKey("real_filename", h5_obj.file.filename)
            text += formatKey("real_basename", h5_obj.basename)
            text += formatKey("real_name", h5_obj.name)

            text += formatKey("obj", h5_obj.ntype)
            text += formatKey("dtype", getattr(h5_obj, "dtype", None))
            text += formatKey("shape", getattr(h5_obj, "shape", None))
            text += formatKey("attrs", getattr(h5_obj, "attrs", None))
            if hasattr(h5_obj, "attrs"):
                text += "<ul>"
                if len(h5_obj.attrs) == 0:
                    text += "<li>empty</li>"
                for key, value in h5_obj.attrs.items():
                    text += formatKey(key, value)
                text += "</ul>"
            text += "</ul>"

        text += "</html>"
        self.__text.setHtml(text)

    def useAsyncLoad(self, useAsync):
        self.__asyncload = useAsync

    def __fileCreated(self, filename):
        if self.__asyncload:
            self.__treeview.findHdf5TreeModel().insertFileAsync(filename)
        else:
            self.__treeview.findHdf5TreeModel().insertFile(filename)

    def customContextMenu(self, event):
        """Called to populate the context menu

        :param silx.gui.hdf5.Hdf5ContextMenuEvent event: Event
            containing expected information to populate the context menu
        """
        selectedObjects = event.source().selectedH5Nodes()
        menu = event.menu()

        hasDataset = False
        for obj in selectedObjects:
            if obj.ntype is h5py.Dataset:
                hasDataset = True
                break

        if not menu.isEmpty():
            menu.addSeparator()

        if hasDataset:
            action = qt.QAction("Do something on the datasets", event.source())
            menu.addAction(action)

    def closeAndSyncCustomContextMenu(self, event):
        """Called to populate the context menu

        :param silx.gui.hdf5.Hdf5ContextMenuEvent event: Event
            containing expected information to populate the context menu
        """
        selectedObjects = event.source().selectedH5Nodes()
        menu = event.menu()

        if not menu.isEmpty():
            menu.addSeparator()

        for obj in selectedObjects:
            if obj.ntype is h5py.File:
                action = qt.QAction("Remove %s" % obj.local_filename, event.source())
                action.triggered.connect(
                    lambda: self.__treeview.findHdf5TreeModel().removeH5pyObject(
                        obj.h5py_object
                    )
                )
                menu.addAction(action)
                action = qt.QAction(
                    "Synchronize %s" % obj.local_filename, event.source()
                )
                action.triggered.connect(
                    lambda: self.__treeview.findHdf5TreeModel().synchronizeH5pyObject(
                        obj.h5py_object
                    )
                )
                menu.addAction(action)

    def __hdf5ComboChanged(self, index):
        function = self.__hdf5Combo.itemData(index)
        self.__createHdf5Button.setCallable(function)

    def __edfComboChanged(self, index):
        function = self.__edfCombo.itemData(index)
        self.__createEdfButton.setCallable(function)

    def createTreeViewConfigurationPanel(self, parent, treeview):
        """Create a configuration panel to allow to play with widget states"""
        panel = qt.QWidget(parent)
        panel.setLayout(qt.QHBoxLayout())

        content = qt.QGroupBox("Create HDF5", panel)
        content.setLayout(qt.QVBoxLayout())
        panel.layout().addWidget(content)

        combo = qt.QComboBox()
        combo.addItem("Containing all types", get_hdf5_with_all_types)
        combo.addItem("Containing all links", get_hdf5_with_all_links)
        combo.addItem("Containing 1000 datasets", get_hdf5_with_1000_datasets)
        combo.addItem("Containing 10000 datasets", get_hdf5_with_10000_datasets)
        combo.addItem("Containing 100000 datasets", get_hdf5_with_100000_datasets)
        combo.addItem("Containing recursive links", get_hdf5_with_recursive_links)
        combo.addItem(
            "Containing external recursive links",
            get_hdf5_with_external_recursive_links,
        )
        combo.addItem("Containing NXdata groups", get_hdf5_with_nxdata)
        combo.activated.connect(self.__hdf5ComboChanged)
        content.layout().addWidget(combo)

        button = ThreadPoolPushButton(content, text="Create")
        button.setCallable(combo.itemData(combo.currentIndex()))
        button.succeeded.connect(self.__fileCreated)
        content.layout().addWidget(button)

        self.__hdf5Combo = combo
        self.__createHdf5Button = button

        asyncload = qt.QCheckBox("Async load", content)
        asyncload.setChecked(self.__asyncload)
        asyncload.toggled.connect(lambda: self.useAsyncLoad(asyncload.isChecked()))
        content.layout().addWidget(asyncload)

        content.layout().addStretch(1)

        content = qt.QGroupBox("Create EDF", panel)
        content.setLayout(qt.QVBoxLayout())
        panel.layout().addWidget(content)

        combo = qt.QComboBox()
        combo.addItem("Containing all types", get_edf_with_all_types)
        combo.addItem("Containing 100000 datasets", get_edf_with_100000_frames)
        combo.activated.connect(self.__edfComboChanged)
        content.layout().addWidget(combo)

        button = ThreadPoolPushButton(content, text="Create")
        button.setCallable(combo.itemData(combo.currentIndex()))
        button.succeeded.connect(self.__fileCreated)
        content.layout().addWidget(button)

        self.__edfCombo = combo
        self.__createEdfButton = button

        content.layout().addStretch(1)

        option = qt.QGroupBox("Tree options", panel)
        option.setLayout(qt.QVBoxLayout())
        panel.layout().addWidget(option)

        sorting = qt.QCheckBox("Enable sorting", option)
        sorting.setChecked(
            treeview.selectionMode() == qt.QAbstractItemView.MultiSelection
        )
        sorting.toggled.connect(lambda: treeview.setSortingEnabled(sorting.isChecked()))
        option.layout().addWidget(sorting)

        multiselection = qt.QCheckBox("Multi-selection", option)
        multiselection.setChecked(
            treeview.selectionMode() == qt.QAbstractItemView.MultiSelection
        )
        switch_selection = lambda: treeview.setSelectionMode(
            qt.QAbstractItemView.MultiSelection
            if multiselection.isChecked()
            else qt.QAbstractItemView.SingleSelection
        )
        multiselection.toggled.connect(switch_selection)
        option.layout().addWidget(multiselection)

        filedrop = qt.QCheckBox("Drop external file", option)
        filedrop.setChecked(treeview.findHdf5TreeModel().isFileDropEnabled())
        filedrop.toggled.connect(
            lambda: treeview.findHdf5TreeModel().setFileDropEnabled(
                filedrop.isChecked()
            )
        )
        option.layout().addWidget(filedrop)

        filemove = qt.QCheckBox("Reorder files", option)
        filemove.setChecked(treeview.findHdf5TreeModel().isFileMoveEnabled())
        filemove.toggled.connect(
            lambda: treeview.findHdf5TreeModel().setFileMoveEnabled(
                filedrop.isChecked()
            )
        )
        option.layout().addWidget(filemove)

        option.layout().addStretch(1)

        option = qt.QGroupBox("Header options", panel)
        option.setLayout(qt.QVBoxLayout())
        panel.layout().addWidget(option)

        autosize = qt.QCheckBox("Auto-size headers", option)
        autosize.setChecked(treeview.header().hasAutoResizeColumns())
        autosize.toggled.connect(
            lambda: treeview.header().setAutoResizeColumns(autosize.isChecked())
        )
        option.layout().addWidget(autosize)

        columnpopup = qt.QCheckBox("Popup to hide/show columns", option)
        columnpopup.setChecked(treeview.header().hasHideColumnsPopup())
        columnpopup.toggled.connect(
            lambda: treeview.header().setEnableHideColumnsPopup(columnpopup.isChecked())
        )
        option.layout().addWidget(columnpopup)

        define_columns = qt.QComboBox()
        define_columns.addItem(
            "Default columns", treeview.findHdf5TreeModel().COLUMN_IDS
        )
        define_columns.addItem(
            "Only name and Value",
            [
                treeview.findHdf5TreeModel().NAME_COLUMN,
                treeview.findHdf5TreeModel().VALUE_COLUMN,
            ],
        )
        define_columns.activated.connect(
            lambda index: treeview.header().setSections(define_columns.itemData(index))
        )
        option.layout().addWidget(define_columns)

        option.layout().addStretch(1)

        panel.layout().addStretch(1)

        return panel


def main(filenames):
    """
    :param filenames: list of file paths
    """
    app = qt.QApplication([])
    sys.excepthook = qt.exceptionHandler
    window = Hdf5TreeViewExample(filenames)
    window.show()
    result = app.exec()
    # remove ending warnings relative to QTimer
    app.deleteLater()
    sys.exit(result)


if __name__ == "__main__":
    main(sys.argv[1:])