#!/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 fabio
import h5py
import numpy
import silx.gui.hdf5
from silx.gui import qt
from silx.gui.data.DataViewerFrame import DataViewerFrame
from silx.gui.widgets.ThreadPoolPushButton import ThreadPoolPushButton

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


try:
    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)


_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 f"<li><b>{name}</b>: {value}</li>"

        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
        )

        def switch_selection():
            return 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:])
