diff options
Diffstat (limited to 'src/silx/app/view/CustomNxdataWidget.py')
-rw-r--r-- | src/silx/app/view/CustomNxdataWidget.py | 1005 |
1 files changed, 1005 insertions, 0 deletions
diff --git a/src/silx/app/view/CustomNxdataWidget.py b/src/silx/app/view/CustomNxdataWidget.py new file mode 100644 index 0000000..3ec62c0 --- /dev/null +++ b/src/silx/app/view/CustomNxdataWidget.py @@ -0,0 +1,1005 @@ +# /*########################################################################## +# 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. +# +# ############################################################################*/ + +"""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() + header.setSectionResizeMode(0, qt.QHeaderView.ResizeToContents) + header.setSectionResizeMode(1, qt.QHeaderView.Stretch) + header.setSectionResizeMode(2, qt.QHeaderView.ResizeToContents) + header.setSectionResizeMode(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): + # 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) + + # 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 |