diff --git a/src/silx/app/view/CustomPlotSelectionWindow.py b/src/silx/app/view/CustomPlotSelectionWindow.py index 8a2d68d938..8c722dd2da 100644 --- a/src/silx/app/view/CustomPlotSelectionWindow.py +++ b/src/silx/app/view/CustomPlotSelectionWindow.py @@ -21,24 +21,29 @@ # THE SOFTWARE. # ###########################################################################*/ -"""""" +"""Custom plot selection window for selecting 1D datasets to plot.""" +from __future__ import annotations from silx.gui import qt, plot, icons +import silx.gui +import silx.gui.plot +import silx.gui.plot.items import silx.io import numpy import functools from silx.gui.plot.LegendSelector import LegendIcon +import silx.io.url import silx.io.utils from silx.gui.hdf5 import _utils - +# Custom role for highlighting the drop zones _DROP_HIGHLIGHT_ROLE = qt.Qt.UserRole + 1 class _HashDropZones(qt.QStyledItemDelegate): """Delegate item displaying a drop zone when the item does not contain a dataset.""" - def __init__(self, parent=None): + def __init__(self, parent: qt.QWidget | None = None): super(_HashDropZones, self).__init__(parent) self.__dropPen = qt.QPen(qt.QColor("#D0D0D0"), 2, qt.Qt.DotLine) self.__highlightDropPen = qt.QPen(qt.QColor("#000000"), 2, qt.Qt.SolidLine) @@ -97,6 +102,8 @@ def paint(self, painter, option, index): class _FileListModel(qt.QStandardItemModel): + """Model for displaying dropped file names in a TreeView widget""" + def __init__(self, plot, parent=None): super().__init__(parent) root = self.invisibleRootItem() @@ -122,22 +129,25 @@ def __init__(self, plot, parent=None): self.addUrl(silx.io.url.DataUrl()) - self.rowsAboutToBeRemoved.connect(self._rowAboutToBeRemoved) + self.rowsAboutToBeRemoved.connect(self._rowsAboutToBeRemoved) - def getXParent(self): + def getXParent(self) -> qt.QStandardItem: + """Return the parent item for the X dataset""" return self._xParent - def getYParent(self): + def getYParent(self) -> qt.QStandardItem: + """Return the parent item for the Y datasets""" return self._yParent - def _setXFile(self, url=None, curve=None): + def _setXFile(self, url: silx.io.url.DataUrl | None = None): + """Set the X dataset file in the model""" if url is None: dataName = "" else: dataName = self._getBasename(url.data_path()) self._plot1D.setXAxisLabel(dataName) - fileItem, iconItem, removeItem = self._createRowItems(dataName, curve) + fileItem, iconItem, removeItem = self._createRowItems(dataName, None) fileItem.setData(qt.QSize(0, 30), qt.Qt.SizeHintRole) fileItem.setData(self._createToolTip(url), qt.Qt.ToolTipRole) @@ -147,13 +157,19 @@ def _setXFile(self, url=None, curve=None): self.setItem(xIndex, 1, fileItem) self.setItem(xIndex, 2, removeItem) - def _addYFile(self, url=None, curve=None, node="X"): + def _addYFile( + self, + url: silx.io.url.DataUrl | None = None, + curve: silx.gui.plot.items.Curve | None = None, + ): + """Add a Y dataset file to the model""" if url is None: dataName = "" else: dataName = self._getBasename(url.data_path()) fileItem, iconItem, removeItem = self._createRowItems(dataName, curve) if not dataName: + # provide size hint for 'empty' item fileItem.setData(qt.QSize(0, 30), qt.Qt.SizeHintRole) fileItem.setData(self._createToolTip(url), qt.Qt.ToolTipRole) @@ -166,14 +182,18 @@ def _addYFile(self, url=None, curve=None, node="X"): self.getYParent().rowCount() - 1, [iconItem, fileItem, removeItem] ) - def _findRowContainingText(self, text): + def _findRowContainingText(self, text: str) -> int | None: + """Return the row index containing the given text, or None if not found.""" for row in range(self.rowCount()): item = self.item(row, 0) if item and item.text() == text: return row return None - def _createRowItems(self, filename, curve): + def _createRowItems( + self, filename: str, curve: silx.gui.plot.items.Curve | None + ) -> tuple[qt.QStandardItem, qt.QStandardItem, qt.QStandardItem]: + """Create the items for a row in the model""" fileItem = qt.QStandardItem(filename) fileItem.setData(curve, qt.Qt.UserRole) fileItem.setData(False, _DROP_HIGHLIGHT_ROLE) @@ -186,14 +206,16 @@ def _createRowItems(self, filename, curve): return fileItem, iconItem, removeItem - def fileItemExists(self, filename): + def fileItemExists(self, filename: str) -> bool: + """Return if a file item with the given filename exists in the model for the Y datasets.""" for row in range(self.getYParent().rowCount()): item = self.item(row, 1) if item and item.text() == filename: return True return False - def addUrl(self, url, node="X"): + def addUrl(self, url: silx.io.url.DataUrl, node: str = "X"): + """Add a dataset to the model""" if url.file_path() is not None: file = silx.io.open(url.file_path()) data = file[url.data_path()] @@ -215,7 +237,10 @@ def addUrl(self, url, node="X"): curve = self._addPlot(self._xDataset, data) self._addYFile(url, curve) - def _addPlot(self, x, y): + def _addPlot( + self, x: numpy.ndarray | None, y: numpy.ndarray + ) -> silx.gui.plot.items.Curve: + """Add a curve to the plot with the given x and y data.""" if x is None: x = numpy.arange(len(y)) @@ -227,7 +252,8 @@ def _addPlot(self, x, y): curve.setInfo(y[()]) return curve - def _rowAboutToBeRemoved(self, parentIndex, first, last): + def _rowsAboutToBeRemoved(self, parentIndex: qt.QModelIndex, first: int, last: int): + """Remove the curves from the plot when a row is removed from the model.""" parentItem = self.itemFromIndex(parentIndex) for row in range(first, last + 1): fileItem = parentItem.child(row, 1) @@ -236,6 +262,7 @@ def _rowAboutToBeRemoved(self, parentIndex, first, last): self._plot1D.removeItem(curve) def _updateYCurvesWithDefaultX(self): + """Update the Y curves with the default X dataset.""" for item in self._plot1D.getItems(): y = item.getInfo() x = numpy.arange(len(y)) @@ -243,6 +270,7 @@ def _updateYCurvesWithDefaultX(self): self._plot1D.resetZoom() def clearAll(self): + """Clear all datasets from the model and the plot.""" self._xDataset = None self._yParent.removeRows(0, self._yParent.rowCount()) @@ -257,6 +285,7 @@ def clearAll(self): self._updateYCurvesWithDefaultX() def _reset(self): + """Reset the model to its initial state.""" self._xDataset = None self._setXFile() @@ -265,7 +294,8 @@ def _reset(self): self._plot1D.clear() self._updateYCurvesWithDefaultX() - def _createToolTip(self, url): + def _createToolTip(self, url: silx.io.url.DataUrl) -> str: + """Create the tooltip for a dataset.""" if url is None: return "" attrs = { @@ -275,7 +305,8 @@ def _createToolTip(self, url): } return _utils.htmlFromDict(attrs, title="HDF5 Dataset") - def _getBasename(self, text): + def _getBasename(self, text: str) -> str: + """Return the basename of a file path.""" return text.split("/")[-1] @@ -306,7 +337,8 @@ def __init__(self, model, parent=None): self.setDragEnabled(False) self.viewport().setAcceptDrops(True) - def _createIconWidget(self, row, parentItem): + def _createIconWidget(self, row: int, parentItem: qt.QStandardItem): + """Create the icon widget for a row in the model""" fileItem = parentItem.child(row, 1) iconItem = parentItem.child(row, 0) curve = fileItem.data(qt.Qt.UserRole) @@ -320,7 +352,10 @@ def _createIconWidget(self, row, parentItem): layout.setContentsMargins(4, 0, 4, 0) self.setIndexWidget(iconItem.index(), widget) - def _createRemoveButton(self, row, parentItem): + def _createRemoveButton(self, row: int, parentItem: qt.QStandardItem | None): + """Create the remove button for a row in the model""" + + # If the parentItem is None, the remove button is for the X dataset if parentItem is None: parentItem = self.model().getXParent() index = self.model().index(0, 2) @@ -332,6 +367,7 @@ def _createRemoveButton(self, row, parentItem): self.setIndexWidget(index, button) return + # If the parentItem is not None, the remove button is for a Y dataset removeItem = parentItem.child(row, 2) if removeItem: button = qt.QToolButton(self) @@ -341,7 +377,12 @@ def _createRemoveButton(self, row, parentItem): ) self.setIndexWidget(removeItem.index(), button) - def _removeFile(self, removeItem, parentItem): + def _removeFile( + self, removeItem: qt.QStandardItem | None, parentItem: qt.QStandardItem + ): + """Remove a file from the model and the plot.""" + + # If removeItem is None, the file to remove is the X dataset if removeItem is None: row = 0 removeItem = self.model().getXParent() @@ -349,6 +390,7 @@ def _removeFile(self, removeItem, parentItem): if removeItem: row = removeItem.row() + # If the parentItem is None, the file to remove is the X dataset if parentItem is None: parentItem = self.model().getXParent() @@ -399,6 +441,8 @@ def dropEvent(self, event): event.acceptProposedAction() def acceptDragEvent(self, event): + """Accept the drag event if the mime data contains a 1D dataset.""" + if event.mimeData().hasFormat("application/x-silx-uri"): byteString = event.mimeData().data("application/x-silx-uri") url = silx.io.url.DataUrl(byteString.data().decode("utf-8")) @@ -413,14 +457,16 @@ def acceptDragEvent(self, event): else: event.ignore() - def setX(self, url): + def setX(self, url: silx.io.url.DataUrl): + """Set the X dataset in the model and the plot.""" targetNode = "X" node = self.model().getXParent() self.model().addUrl(url, targetNode) self._createRemoveButton(node, None) self.model()._plot1D.resetZoom() - def addY(self, url): + def addY(self, url: silx.io.url.DataUrl): + """Add a Y dataset to the model and the plot.""" targetNode = "Y" node = self.model().getYParent() self.model().addUrl(url, targetNode) @@ -429,11 +475,13 @@ def addY(self, url): self.model()._plot1D.resetZoom() def clear(self): + """Clear all datasets from the model and the plot.""" self.model().clearAll() index = self.model().index(0, 2) self.setIndexWidget(index, None) - def setDropHighlight(self, value): + def setDropHighlight(self, value: str | None): + """Set the drop highlight for the X and Y datasets.""" xDropFileItem = self.model().itemFromIndex( self.model().sibling( 0, 1, self.model().indexFromItem(self.model().getXParent()) @@ -447,12 +495,15 @@ def setDropHighlight(self, value): class _DropPlot1D(plot.Plot1D): + """Plot1D widget for displaying 1D datasets that can accept drops.""" + def __init__(self, parent=None): super().__init__(parent) self.setAcceptDrops(True) self._treeView = None - def setTreeView(self, treeView): + def setTreeView(self, treeView: qt.QTreeView): + """Set the TreeView widget for the plot.""" self._treeView = treeView def dragEnterEvent(self, event): @@ -479,7 +530,8 @@ def dropEvent(self, event): self.resetZoom() event.acceptProposedAction() - def setXAxisLabel(self, label): + def setXAxisLabel(self, label: str): + """Set the label for the X axis.""" xAxis = self.getXAxis() xAxis.setLabel(label) @@ -488,7 +540,8 @@ class _PlotToolBar(qt.QToolBar): def __init__(self, parent=None): super().__init__(parent) - def addClearAction(self, treeView): + def addClearAction(self, treeView: qt.QTreeView): + """Add the clear action to the toolbar.""" icon = self.style().standardIcon(qt.QStyle.SP_TrashIcon) clearAction = qt.QAction(icon, "Clear All", self) clearAction.triggered.connect(treeView.clear) @@ -496,6 +549,8 @@ def addClearAction(self, treeView): class CustomPlotSelectionWindow(qt.QMainWindow): + """A customized plot selection window allowing the user to select and display 1D data sets.""" + sigVisibilityChanged = qt.Signal(bool) def __init__(self, parent=None):