summaryrefslogtreecommitdiff
path: root/silx/app/view/CustomNxdataWidget.py
diff options
context:
space:
mode:
Diffstat (limited to 'silx/app/view/CustomNxdataWidget.py')
-rw-r--r--silx/app/view/CustomNxdataWidget.py1008
1 files changed, 1008 insertions, 0 deletions
diff --git a/silx/app/view/CustomNxdataWidget.py b/silx/app/view/CustomNxdataWidget.py
new file mode 100644
index 0000000..02ae6c0
--- /dev/null
+++ b/silx/app/view/CustomNxdataWidget.py
@@ -0,0 +1,1008 @@
+# 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