# coding: utf-8
# /*##########################################################################
# Copyright (C) 2016-2018 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.
#
# ############################################################################*/

"""Widget to custom NXdata groups"""

__authors__ = ["V. Valls"]
__license__ = "MIT"
__date__ = "15/06/2018"

import logging
import numpy
import weakref

from silx.gui import qt
from silx.io import commonh5
import silx.io.nxdata
from silx.gui.hdf5._utils import Hdf5DatasetMimeData
from silx.gui.data.TextFormatter import TextFormatter
from silx.gui.hdf5.Hdf5Formatter import Hdf5Formatter
from silx.gui import icons


_logger = logging.getLogger(__name__)
_formatter = TextFormatter()
_hdf5Formatter = Hdf5Formatter(textFormatter=_formatter)


class _RowItems(qt.QStandardItem):
    """Define the list of items used for a specific row."""

    def type(self):
        return qt.QStandardItem.UserType + 1

    def getRowItems(self):
        """Returns the list of items used for a specific row.

        The first item should be this class.

        :rtype: List[qt.QStandardItem]
        """
        raise NotImplementedError()


class _DatasetItemRow(_RowItems):
    """Define a row which can contain a dataset."""

    def __init__(self, label="", dataset=None):
        """Constructor"""
        super(_DatasetItemRow, self).__init__(label)
        self.setEditable(False)
        self.setDropEnabled(False)
        self.setDragEnabled(False)

        self.__name = qt.QStandardItem()
        self.__name.setEditable(False)
        self.__name.setDropEnabled(True)

        self.__type = qt.QStandardItem()
        self.__type.setEditable(False)
        self.__type.setDropEnabled(False)
        self.__type.setDragEnabled(False)

        self.__shape = qt.QStandardItem()
        self.__shape.setEditable(False)
        self.__shape.setDropEnabled(False)
        self.__shape.setDragEnabled(False)

        self.setDataset(dataset)

    def getDefaultFormatter(self):
        """Get the formatter used to display dataset informations.

        :rtype: Hdf5Formatter
        """
        return _hdf5Formatter

    def setDataset(self, dataset):
        """Set the dataset stored in this item.

        :param Union[numpy.ndarray,h5py.Dataset,silx.io.commonh5.Dataset] dataset:
            The dataset to store.
        """
        self.__dataset = dataset
        if self.__dataset is not None:
            name = self.__dataset.name

            if silx.io.is_dataset(dataset):
                type_ = self.getDefaultFormatter().humanReadableType(dataset)
                shape = self.getDefaultFormatter().humanReadableShape(dataset)

                if dataset.shape is None:
                    icon_name = "item-none"
                elif len(dataset.shape) < 4:
                    icon_name = "item-%ddim" % len(dataset.shape)
                else:
                    icon_name = "item-ndim"
                icon = icons.getQIcon(icon_name)
            else:
                type_ = ""
                shape = ""
                icon = qt.QIcon()
        else:
            name = ""
            type_ = ""
            shape = ""
            icon = qt.QIcon()

        self.__icon = icon
        self.__name.setText(name)
        self.__name.setDragEnabled(self.__dataset is not None)
        self.__name.setIcon(self.__icon)
        self.__type.setText(type_)
        self.__shape.setText(shape)

        parent = self.parent()
        if parent is not None:
            self.parent()._datasetUpdated()

    def getDataset(self):
        """Returns the dataset stored within the item."""
        return self.__dataset

    def getRowItems(self):
        """Returns the list of items used for a specific row.

        The first item should be this class.

        :rtype: List[qt.QStandardItem]
        """
        return [self, self.__name, self.__type, self.__shape]


class _DatasetAxisItemRow(_DatasetItemRow):
    """Define a row describing an axis."""

    def __init__(self):
        """Constructor"""
        super(_DatasetAxisItemRow, self).__init__()

    def setAxisId(self, axisId):
        """Set the id of the axis (the first axis is 0)

        :param int axisId: Identifier of this axis.
        """
        self.__axisId = axisId
        label = "Axis %d" % (axisId + 1)
        self.setText(label)

    def getAxisId(self):
        """Returns the identifier of this axis.

        :rtype: int
        """
        return self.__axisId


class _NxDataItem(qt.QStandardItem):
    """
    Define a custom NXdata.
    """

    def __init__(self):
        """Constructor"""
        qt.QStandardItem.__init__(self)
        self.__error = None
        self.__title = None
        self.__axes = []
        self.__virtual = None

        item = _DatasetItemRow("Signal", None)
        self.appendRow(item.getRowItems())
        self.__signal = item

        self.setEditable(False)
        self.setDragEnabled(False)
        self.setDropEnabled(False)
        self.__setError(None)

    def getRowItems(self):
        """Returns the list of items used for a specific row.

        The first item should be this class.

        :rtype: List[qt.QStandardItem]
        """
        row = [self]
        for _ in range(3):
            item = qt.QStandardItem("")
            item.setEditable(False)
            item.setDragEnabled(False)
            item.setDropEnabled(False)
            row.append(item)
        return row

    def _datasetUpdated(self):
        """Called when the NXdata contained of the item have changed.

        It invalidates the NXdata stored and send an event `sigNxdataUpdated`.
        """
        self.__virtual = None
        self.__setError(None)
        model = self.model()
        if model is not None:
            model.sigNxdataUpdated.emit(self.index())

    def createVirtualGroup(self):
        """Returns a new virtual Group using a NeXus NXdata structure to store
        data

        :rtype: silx.io.commonh5.Group
        """
        name = ""
        if self.__title is not None:
            name = self.__title
        virtual = commonh5.Group(name)
        virtual.attrs["NX_class"] = "NXdata"

        if self.__title is not None:
            virtual.attrs["title"] = self.__title

        if self.__signal is not None:
            signal = self.__signal.getDataset()
            if signal is not None:
                # Could be done using a link instead of a copy
                node = commonh5.DatasetProxy("signal", target=signal)
                virtual.attrs["signal"] = "signal"
                virtual.add_node(node)

        axesAttr = []
        for i, axis in enumerate(self.__axes):
            if axis is None:
                name = "."
            else:
                axis = axis.getDataset()
                if axis is None:
                    name = "."
                else:
                    name = "axis%d" % i
                    node = commonh5.DatasetProxy(name, target=axis)
                    virtual.add_node(node)
            axesAttr.append(name)

        if axesAttr != []:
            virtual.attrs["axes"] = numpy.array(axesAttr)

        validator = silx.io.nxdata.NXdata(virtual)
        if not validator.is_valid:
            message = "<html>"
            message += "This NXdata is not consistant"
            message += "<ul>"
            for issue in validator.issues:
                message += "<li>%s</li>" % issue
            message += "</ul>"
            message += "</html>"
            self.__setError(message)
        else:
            self.__setError(None)
        return virtual

    def isValid(self):
        """Returns true if the stored NXdata is valid

        :rtype: bool
        """
        return self.__error is None

    def getVirtualGroup(self):
        """Returns a cached virtual Group using a NeXus NXdata structure to
        store data.

        If the stored NXdata was invalidated, :meth:`createVirtualGroup` is
        internally called to update the cache.

        :rtype: silx.io.commonh5.Group
        """
        if self.__virtual is None:
            self.__virtual = self.createVirtualGroup()
        return self.__virtual

    def getTitle(self):
        """Returns the title of the NXdata

        :rtype: str
        """
        return self.text()

    def setTitle(self, title):
        """Set the title of the NXdata

        :param str title: The title of this NXdata
        """
        self.setText(title)

    def __setError(self, error):
        """Set the error message in case of the current state of the stored
        NXdata is not valid.

        :param str error: Message to display
        """
        self.__error = error
        style = qt.QApplication.style()
        if error is None:
            message = ""
            icon = style.standardIcon(qt.QStyle.SP_DirLinkIcon)
        else:
            message = error
            icon = style.standardIcon(qt.QStyle.SP_MessageBoxCritical)
        self.setIcon(icon)
        self.setToolTip(message)

    def getError(self):
        """Returns the error message in case the NXdata is not valid.

        :rtype: str"""
        return self.__error

    def setSignalDataset(self, dataset):
        """Set the dataset to use as signal with this NXdata.

        :param Union[numpy.ndarray,h5py.Dataset,silx.io.commonh5.Dataset] dataset:
            The dataset to use as signal.
        """

        self.__signal.setDataset(dataset)
        self._datasetUpdated()

    def getSignalDataset(self):
        """Returns the dataset used as signal.

        :rtype: Union[numpy.ndarray,h5py.Dataset,silx.io.commonh5.Dataset]
        """
        return self.__signal.getDataset()

    def setAxesDatasets(self, datasets):
        """Set all the available dataset used as axes.

        Axes will be created or removed from the GUI in order to provide the
        same amount of requested axes.

        A `None` element is an axes with no dataset.

        :param List[Union[numpy.ndarray,h5py.Dataset,silx.io.commonh5.Dataset,None]] datasets:
            List of dataset to use as axes.
        """
        for i, dataset in enumerate(datasets):
            if i < len(self.__axes):
                mustAppend = False
                item = self.__axes[i]
            else:
                mustAppend = True
                item = _DatasetAxisItemRow()
            item.setAxisId(i)
            item.setDataset(dataset)
            if mustAppend:
                self.__axes.append(item)
                self.appendRow(item.getRowItems())

        # Clean up extra axis
        for i in range(len(datasets), len(self.__axes)):
            item = self.__axes.pop(len(datasets))
            self.removeRow(item.row())

        self._datasetUpdated()

    def getAxesDatasets(self):
        """Returns available axes as dataset.

        A `None` element is an axes with no dataset.

        :rtype: List[Union[numpy.ndarray,h5py.Dataset,silx.io.commonh5.Dataset,None]]
        """
        datasets = []
        for axis in self.__axes:
            datasets.append(axis.getDataset())
        return datasets


class _Model(qt.QStandardItemModel):
    """Model storing a list of custom NXdata items.

    Supports drag and drop of datasets.
    """

    sigNxdataUpdated = qt.Signal(qt.QModelIndex)
    """Emitted when stored NXdata was edited"""

    def __init__(self, parent=None):
        """Constructor"""
        qt.QStandardItemModel.__init__(self, parent)
        root = self.invisibleRootItem()
        root.setDropEnabled(True)
        root.setDragEnabled(False)

    def supportedDropActions(self):
        """Inherited method to redefine supported drop actions."""
        return qt.Qt.CopyAction | qt.Qt.MoveAction

    def mimeTypes(self):
        """Inherited method to redefine draggable mime types."""
        return [Hdf5DatasetMimeData.MIME_TYPE]

    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 len(indexes) > 1:
            return None
        if len(indexes) == 0:
            return None

        qindex = indexes[0]
        qindex = self.index(qindex.row(), 0, parent=qindex.parent())
        item = self.itemFromIndex(qindex)
        if isinstance(item, _DatasetItemRow):
            dataset = item.getDataset()
            if dataset is None:
                return None
            else:
                mimeData = Hdf5DatasetMimeData(dataset=item.getDataset())
        else:
            mimeData = None
        return mimeData

    def dropMimeData(self, mimedata, action, row, column, parentIndex):
        """Inherited method to handle a drop operation to this model."""
        if action == qt.Qt.IgnoreAction:
            return True

        if mimedata.hasFormat(Hdf5DatasetMimeData.MIME_TYPE):
            if row != -1 or column != -1:
                # It is not a drop on a specific item
                return False
            item = self.itemFromIndex(parentIndex)
            if item is None or item is self.invisibleRootItem():
                # Drop at the end
                dataset = mimedata.dataset()
                if silx.io.is_dataset(dataset):
                    self.createFromSignal(dataset)
                elif silx.io.is_group(dataset):
                    nxdata = dataset
                    try:
                        self.createFromNxdata(nxdata)
                    except ValueError:
                        _logger.error("Error while dropping a group as an NXdata")
                        _logger.debug("Backtrace", exc_info=True)
                        return False
                else:
                    _logger.error("Dropping a wrong object")
                    return False
            else:
                item = item.parent().child(item.row(), 0)
                if not isinstance(item, _DatasetItemRow):
                    # Dropped at a bad place
                    return False
                dataset = mimedata.dataset()
                if silx.io.is_dataset(dataset):
                    item.setDataset(dataset)
                else:
                    _logger.error("Dropping a wrong object")
                    return False
            return True

        return False

    def __getNxdataByTitle(self, title):
        """Returns an NXdata item by its title, else None.

        :rtype: Union[_NxDataItem,None]
        """
        for row in range(self.rowCount()):
            qindex = self.index(row, 0)
            item = self.itemFromIndex(qindex)
            if item.getTitle() == title:
                return item
        return None

    def findFreeNxdataTitle(self):
        """Returns an NXdata title which is not yet used.

        :rtype: str
        """
        for i in range(self.rowCount() + 1):
            name = "NXData #%d" % (i + 1)
            group = self.__getNxdataByTitle(name)
            if group is None:
                break
        return name

    def createNewNxdata(self, name=None):
        """Create a new NXdata item.

        :param Union[str,None] name: A title for the new NXdata
        """
        item = _NxDataItem()
        if name is None:
            name = self.findFreeNxdataTitle()
        item.setTitle(name)
        self.appendRow(item.getRowItems())

    def createFromSignal(self, dataset):
        """Create a new NXdata item from a signal dataset.

        This signal will also define an amount of axes according to its number
        of dimensions.

        :param Union[numpy.ndarray,h5py.Dataset,silx.io.commonh5.Dataset] dataset:
            A dataset uses as signal.
        """

        item = _NxDataItem()
        name = self.findFreeNxdataTitle()
        item.setTitle(name)
        item.setSignalDataset(dataset)
        item.setAxesDatasets([None] * len(dataset.shape))
        self.appendRow(item.getRowItems())

    def createFromNxdata(self, nxdata):
        """Create a new custom NXdata item from an existing NXdata group.

        If the NXdata is not valid, nothing is created, and an exception is
        returned.

        :param Union[h5py.Group,silx.io.commonh5.Group] nxdata: An h5py group
            following the NXData specification.
        :raise ValueError:If `nxdata` is not valid.
        """
        validator = silx.io.nxdata.NXdata(nxdata)
        if validator.is_valid:
            item = _NxDataItem()
            title = validator.title
            if title in [None or ""]:
                title = self.findFreeNxdataTitle()
            item.setTitle(title)
            item.setSignalDataset(validator.signal)
            item.setAxesDatasets(validator.axes)
            self.appendRow(item.getRowItems())
        else:
            raise ValueError("Not a valid NXdata")

    def removeNxdataItem(self, item):
        """Remove an NXdata item from this model.

        :param _NxDataItem item: An item
        """
        if isinstance(item, _NxDataItem):
            parent = item.parent()
            assert(parent is None)
            model = item.model()
            model.removeRow(item.row())
        else:
            _logger.error("Unexpected item")

    def appendAxisToNxdataItem(self, item):
        """Append a new axes to this item (or the NXdata item own by this item).

        :param Union[_NxDataItem,qt.QStandardItem] item: An item
        """
        if item is not None and not isinstance(item, _NxDataItem):
            item = item.parent()
        nxdataItem = item
        if isinstance(item, _NxDataItem):
            datasets = nxdataItem.getAxesDatasets()
            datasets.append(None)
            nxdataItem.setAxesDatasets(datasets)
        else:
            _logger.error("Unexpected item")

    def removeAxisItem(self, item):
        """Remove an axis item from this model.

        :param _DatasetAxisItemRow item: An axis item
        """
        if isinstance(item, _DatasetAxisItemRow):
            axisId = item.getAxisId()
            nxdataItem = item.parent()
            datasets = nxdataItem.getAxesDatasets()
            del datasets[axisId]
            nxdataItem.setAxesDatasets(datasets)
        else:
            _logger.error("Unexpected item")


class CustomNxDataToolBar(qt.QToolBar):
    """A specialised toolbar to manage custom NXdata model and items."""

    def __init__(self, parent=None):
        """Constructor"""
        super(CustomNxDataToolBar, self).__init__(parent=parent)
        self.__nxdataWidget = None
        self.__initContent()
        # Initialize action state
        self.__currentSelectionChanged(qt.QModelIndex(), qt.QModelIndex())

    def __initContent(self):
        """Create all expected actions and set the content of this toolbar."""
        action = qt.QAction("Create a new custom NXdata", self)
        action.setIcon(icons.getQIcon("nxdata-create"))
        action.triggered.connect(self.__createNewNxdata)
        self.addAction(action)
        self.__addNxDataAction = action

        action = qt.QAction("Remove the selected NXdata", self)
        action.setIcon(icons.getQIcon("nxdata-remove"))
        action.triggered.connect(self.__removeSelectedNxdata)
        self.addAction(action)
        self.__removeNxDataAction = action

        self.addSeparator()

        action = qt.QAction("Create a new axis to the selected NXdata", self)
        action.setIcon(icons.getQIcon("nxdata-axis-add"))
        action.triggered.connect(self.__appendNewAxisToSelectedNxdata)
        self.addAction(action)
        self.__addNxDataAxisAction = action

        action = qt.QAction("Remove the selected NXdata axis", self)
        action.setIcon(icons.getQIcon("nxdata-axis-remove"))
        action.triggered.connect(self.__removeSelectedAxis)
        self.addAction(action)
        self.__removeNxDataAxisAction = action

    def __getSelectedItem(self):
        """Get the selected item from the linked CustomNxdataWidget.

        :rtype: qt.QStandardItem
        """
        selectionModel = self.__nxdataWidget.selectionModel()
        index = selectionModel.currentIndex()
        if not index.isValid():
            return
        model = self.__nxdataWidget.model()
        index = model.index(index.row(), 0, index.parent())
        item = model.itemFromIndex(index)
        return item

    def __createNewNxdata(self):
        """Create a new NXdata item to the linked CustomNxdataWidget."""
        if self.__nxdataWidget is None:
            return
        model = self.__nxdataWidget.model()
        model.createNewNxdata()

    def __removeSelectedNxdata(self):
        """Remove the NXdata item currently selected in the linked
        CustomNxdataWidget."""
        if self.__nxdataWidget is None:
            return
        model = self.__nxdataWidget.model()
        item = self.__getSelectedItem()
        model.removeNxdataItem(item)

    def __appendNewAxisToSelectedNxdata(self):
        """Append a new axis to the NXdata item currently selected in the
        linked CustomNxdataWidget."""
        if self.__nxdataWidget is None:
            return
        model = self.__nxdataWidget.model()
        item = self.__getSelectedItem()
        model.appendAxisToNxdataItem(item)

    def __removeSelectedAxis(self):
        """Remove the axis item currently selected in the linked
        CustomNxdataWidget."""
        if self.__nxdataWidget is None:
            return
        model = self.__nxdataWidget.model()
        item = self.__getSelectedItem()
        model.removeAxisItem(item)

    def setCustomNxDataWidget(self, widget):
        """Set the linked CustomNxdataWidget to this toolbar."""
        assert(isinstance(widget, CustomNxdataWidget))
        if self.__nxdataWidget is not None:
            selectionModel = self.__nxdataWidget.selectionModel()
            selectionModel.currentChanged.disconnect(self.__currentSelectionChanged)
        self.__nxdataWidget = widget
        if self.__nxdataWidget is not None:
            selectionModel = self.__nxdataWidget.selectionModel()
            selectionModel.currentChanged.connect(self.__currentSelectionChanged)

    def __currentSelectionChanged(self, current, previous):
        """Update the actions according to the linked CustomNxdataWidget
        item selection"""
        if not current.isValid():
            item = None
        else:
            model = self.__nxdataWidget.model()
            index = model.index(current.row(), 0, current.parent())
            item = model.itemFromIndex(index)
        self.__removeNxDataAction.setEnabled(isinstance(item, _NxDataItem))
        self.__removeNxDataAxisAction.setEnabled(isinstance(item, _DatasetAxisItemRow))
        self.__addNxDataAxisAction.setEnabled(isinstance(item, _NxDataItem) or isinstance(item, _DatasetItemRow))


class _HashDropZones(qt.QStyledItemDelegate):
    """Delegate item displaying a drop zone when the item do not contains
    dataset."""

    def __init__(self, parent=None):
        """Constructor"""
        super(_HashDropZones, self).__init__(parent)
        pen = qt.QPen()
        pen.setColor(qt.QColor("#D0D0D0"))
        pen.setStyle(qt.Qt.DotLine)
        pen.setWidth(2)
        self.__dropPen = pen

    def paint(self, painter, option, index):
        """
        Paint the item

        :param qt.QPainter painter: A painter
        :param qt.QStyleOptionViewItem option: Options of the item to paint
        :param qt.QModelIndex index: Index of the item to paint
        """
        displayDropZone = False
        if index.isValid():
            model = index.model()
            rowIndex = model.index(index.row(), 0, index.parent())
            rowItem = model.itemFromIndex(rowIndex)
            if isinstance(rowItem, _DatasetItemRow):
                displayDropZone = rowItem.getDataset() is None

        if displayDropZone:
            painter.save()

            # Draw background if selected
            if option.state & qt.QStyle.State_Selected:
                colorGroup = qt.QPalette.Inactive
                if option.state & qt.QStyle.State_Active:
                    colorGroup = qt.QPalette.Active
                if not option.state & qt.QStyle.State_Enabled:
                    colorGroup = qt.QPalette.Disabled
                brush = option.palette.brush(colorGroup, qt.QPalette.Highlight)
                painter.fillRect(option.rect, brush)

            painter.setPen(self.__dropPen)
            painter.drawRect(option.rect.adjusted(3, 3, -3, -3))
            painter.restore()
        else:
            qt.QStyledItemDelegate.paint(self, painter, option, index)


class CustomNxdataWidget(qt.QTreeView):
    """Widget providing a table displaying and allowing to custom virtual
    NXdata."""

    sigNxdataItemUpdated = qt.Signal(qt.QStandardItem)
    """Emitted when the NXdata from an NXdata item was edited"""

    sigNxdataItemRemoved = qt.Signal(qt.QStandardItem)
    """Emitted when an NXdata item was removed"""

    def __init__(self, parent=None):
        """Constructor"""
        qt.QTreeView.__init__(self, parent=None)
        self.__model = _Model(self)
        self.__model.setColumnCount(4)
        self.__model.setHorizontalHeaderLabels(["Name", "Dataset", "Type", "Shape"])
        self.setModel(self.__model)

        self.setItemDelegateForColumn(1, _HashDropZones(self))

        self.__model.sigNxdataUpdated.connect(self.__nxdataUpdate)
        self.__model.rowsAboutToBeRemoved.connect(self.__rowsAboutToBeRemoved)
        self.__model.rowsAboutToBeInserted.connect(self.__rowsAboutToBeInserted)

        header = self.header()
        if qt.qVersion() < "5.0":
            setResizeMode = header.setResizeMode
        else:
            setResizeMode = header.setSectionResizeMode
        setResizeMode(0, qt.QHeaderView.ResizeToContents)
        setResizeMode(1, qt.QHeaderView.Stretch)
        setResizeMode(2, qt.QHeaderView.ResizeToContents)
        setResizeMode(3, qt.QHeaderView.ResizeToContents)

        self.setSelectionMode(qt.QAbstractItemView.SingleSelection)
        self.setDropIndicatorShown(True)
        self.setDragDropOverwriteMode(True)
        self.setDragEnabled(True)
        self.viewport().setAcceptDrops(True)

        self.setContextMenuPolicy(qt.Qt.CustomContextMenu)
        self.customContextMenuRequested[qt.QPoint].connect(self.__executeContextMenu)

    def __rowsAboutToBeInserted(self, parentIndex, start, end):
        if qt.qVersion()[0:2] == "5.":
            # FIXME: workaround for https://github.com/silx-kit/silx/issues/1919
            # Uses of ResizeToContents looks to break nice update of cells with Qt5
            # This patch make the view blinking
            self.repaint()

    def __rowsAboutToBeRemoved(self, parentIndex, start, end):
        """Called when an item was removed from the model."""
        items = []
        model = self.model()
        for index in range(start, end):
            qindex = model.index(index, 0, parent=parentIndex)
            item = self.__model.itemFromIndex(qindex)
            if isinstance(item, _NxDataItem):
                items.append(item)
        for item in items:
            self.sigNxdataItemRemoved.emit(item)

        if qt.qVersion()[0:2] == "5.":
            # FIXME: workaround for https://github.com/silx-kit/silx/issues/1919
            # Uses of ResizeToContents looks to break nice update of cells with Qt5
            # This patch make the view blinking
            self.repaint()

    def __nxdataUpdate(self, index):
        """Called when a virtual NXdata was updated from the model."""
        model = self.model()
        item = model.itemFromIndex(index)
        self.sigNxdataItemUpdated.emit(item)

    def createDefaultContextMenu(self, index):
        """Create a default context menu at this position.

        :param qt.QModelIndex index: Index of the item
        """
        index = self.__model.index(index.row(), 0, parent=index.parent())
        item = self.__model.itemFromIndex(index)

        menu = qt.QMenu()

        weakself = weakref.proxy(self)

        if isinstance(item, _NxDataItem):
            action = qt.QAction("Add a new axis", menu)
            action.triggered.connect(lambda: weakself.model().appendAxisToNxdataItem(item))
            action.setIcon(icons.getQIcon("nxdata-axis-add"))
            action.setIconVisibleInMenu(True)
            menu.addAction(action)
            menu.addSeparator()
            action = qt.QAction("Remove this NXdata", menu)
            action.triggered.connect(lambda: weakself.model().removeNxdataItem(item))
            action.setIcon(icons.getQIcon("remove"))
            action.setIconVisibleInMenu(True)
            menu.addAction(action)
        else:
            if isinstance(item, _DatasetItemRow):
                if item.getDataset() is not None:
                    action = qt.QAction("Remove this dataset", menu)
                    action.triggered.connect(lambda: item.setDataset(None))
                    menu.addAction(action)

            if isinstance(item, _DatasetAxisItemRow):
                menu.addSeparator()
                action = qt.QAction("Remove this axis", menu)
                action.triggered.connect(lambda: weakself.model().removeAxisItem(item))
                action.setIcon(icons.getQIcon("remove"))
                action.setIconVisibleInMenu(True)
                menu.addAction(action)

        return menu

    def __executeContextMenu(self, point):
        """Execute the context menu at this position."""
        index = self.indexAt(point)
        menu = self.createDefaultContextMenu(index)
        if menu is None or menu.isEmpty():
            return
        menu.exec_(qt.QCursor.pos())

    def removeDatasetsFrom(self, root):
        """
        Remove all datasets provided by this root

        :param root: The root file of datasets to remove
        """
        for row in range(self.__model.rowCount()):
            qindex = self.__model.index(row, 0)
            item = self.model().itemFromIndex(qindex)

            edited = False
            datasets = item.getAxesDatasets()
            for i, dataset in enumerate(datasets):
                if dataset is not None:
                    # That's an approximation, IS can't be used as h5py generates
                    # To objects for each requests to a node
                    if dataset.file.filename == root.file.filename:
                        datasets[i] = None
                        edited = True
            if edited:
                item.setAxesDatasets(datasets)

            dataset = item.getSignalDataset()
            if dataset is not None:
                # That's an approximation, IS can't be used as h5py generates
                # To objects for each requests to a node
                if dataset.file.filename == root.file.filename:
                    item.setSignalDataset(None)

    def replaceDatasetsFrom(self, removedRoot, loadedRoot):
        """
        Replace any dataset from any NXdata items using the same dataset name
        from another root.

        Usually used when a file was synchronized.

        :param removedRoot: The h5py root file which is replaced
            (which have to be removed)
        :param loadedRoot: The new h5py root file which have to be used
            instread.
        """
        for row in range(self.__model.rowCount()):
            qindex = self.__model.index(row, 0)
            item = self.model().itemFromIndex(qindex)

            edited = False
            datasets = item.getAxesDatasets()
            for i, dataset in enumerate(datasets):
                newDataset = self.__replaceDatasetRoot(dataset, removedRoot, loadedRoot)
                if dataset is not newDataset:
                    datasets[i] = newDataset
                    edited = True
            if edited:
                item.setAxesDatasets(datasets)

        dataset = item.getSignalDataset()
        newDataset = self.__replaceDatasetRoot(dataset, removedRoot, loadedRoot)
        if dataset is not newDataset:
            item.setSignalDataset(newDataset)

    def __replaceDatasetRoot(self, dataset, fromRoot, toRoot):
        """
        Replace the dataset by the same dataset name from another root.
        """
        if dataset is None:
            return None

        if dataset.file is None:
            # Not from the expected root
            return dataset

        # That's an approximation, IS can't be used as h5py generates
        # To objects for each requests to a node
        if dataset.file.filename == fromRoot.file.filename:
            # Try to find the same dataset name
            try:
                return toRoot[dataset.name]
            except Exception:
                _logger.debug("Backtrace", exc_info=True)
                return None
        else:
            # Not from the expected root
            return dataset

    def selectedItems(self):
        """Returns the list of selected items containing NXdata

        :rtype: List[qt.QStandardItem]
        """
        result = []
        for qindex in self.selectedIndexes():
            if qindex.column() != 0:
                continue
            if not qindex.isValid():
                continue
            item = self.__model.itemFromIndex(qindex)
            if not isinstance(item, _NxDataItem):
                continue
            result.append(item)
        return result

    def selectedNxdata(self):
        """Returns the list of selected NXdata

        :rtype: List[silx.io.commonh5.Group]
        """
        result = []
        for qindex in self.selectedIndexes():
            if qindex.column() != 0:
                continue
            if not qindex.isValid():
                continue
            item = self.__model.itemFromIndex(qindex)
            if not isinstance(item, _NxDataItem):
                continue
            result.append(item.getVirtualGroup())
        return result
