Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

silx.app.view.CustomPlotSelectionWindow : Added the documentation #4140

Merged
merged 11 commits into from
Jun 24, 2024
107 changes: 81 additions & 26 deletions src/silx/app/view/CustomPlotSelectionWindow.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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()
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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()]
Expand All @@ -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))

Expand All @@ -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)
Expand All @@ -236,13 +262,15 @@ 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))
item.setData(x, y)
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())
Expand All @@ -257,6 +285,7 @@ def clearAll(self):
self._updateYCurvesWithDefaultX()

def _reset(self):
"""Reset the model to its initial state."""
self._xDataset = None

self._setXFile()
Expand All @@ -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 = {
Expand All @@ -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]


Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -341,14 +377,20 @@ 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()

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()

Expand Down Expand Up @@ -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"))
Expand All @@ -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)
Expand All @@ -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())
Expand All @@ -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):
Expand All @@ -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)

Expand All @@ -488,14 +540,17 @@ 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)
self.addAction(clearAction)


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):
Expand Down