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

MNT: Common table views, Primarily LivePVTableModel, minor skeleton work on NestableTableModel #72

Merged
merged 10 commits into from
Sep 6, 2024
24 changes: 24 additions & 0 deletions docs/source/upcoming_release_notes/72-mnt_common_views.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
72 mnt_common_views
###################

API Breaks
----------
- N/A

Features
--------
- Adds ``BaseTableEntryModel``, from which ``LivePVTableModel`` and ``NestableTableModel`` inherit.
These table models are intended to display ``Entry`` data and be reused across the application.
- Implements ``LivePVTableModel``, including polling behavior for live PV display.

Bugfixes
--------
- N/A

Maintenance
-----------
- N/A

Contributors
------------
- tangkong
4 changes: 2 additions & 2 deletions superscore/control_layers/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,12 +83,12 @@ def get(self, address: Union[str, Iterable[str]]) -> Union[EpicsData, Iterable[E

Parameters
----------
address : Union[str, list[str]]
address : Union[str, Iterable[str]]
The PV(s) to get values for.

Returns
-------
Union[EpicsData, list[EpicsData]]
Union[EpicsData, Iterable[EpicsData]]
The requested data
"""
# Dispatches to _get_single and _get_list depending on type
Expand Down
52 changes: 52 additions & 0 deletions superscore/tests/test_views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
from unittest.mock import MagicMock

import pytest
from pytestqt.qtbot import QtBot
from qtpy import QtCore

from superscore.client import Client
from superscore.model import Parameter
from superscore.widgets.views import LivePVTableModel


@pytest.fixture(scope='function')
def pv_poll_model(
mock_client: Client,
parameter_with_readback: Parameter,
qtbot: QtBot
) -> LivePVTableModel:
model = LivePVTableModel(
client=mock_client,
entries=[parameter_with_readback],
poll_period=1.0
)

# Make sure we never actually call EPICS
model.client.cl.get = MagicMock(return_value=1)
qtbot.wait_until(lambda: model._poll_thread.running)
yield model

model.stop_polling()

qtbot.wait_until(lambda: not model._poll_thread.isRunning())


def test_pvmodel_polling(pv_poll_model: LivePVTableModel, qtbot: QtBot):
thread = pv_poll_model._poll_thread
pv_poll_model.stop_polling()
qtbot.wait_until(lambda: thread.isFinished(), timeout=10000)
assert not thread.running


def test_pvmodel_update(pv_poll_model: LivePVTableModel, qtbot: QtBot):
assert pv_poll_model._data_cache

# make the mock cl return a new value
pv_poll_model.client.cl.get = MagicMock(return_value=3)

qtbot.wait_signal(pv_poll_model.dataChanged)

data_index = pv_poll_model.index_from_item(pv_poll_model.entries[0], 'Live Value')
qtbot.wait_until(
lambda: pv_poll_model.data(data_index, QtCore.Qt.DisplayRole) == '3'
)
2 changes: 1 addition & 1 deletion superscore/tests/test_widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

from superscore.model import Collection, Root
from superscore.widgets.core import DataWidget
from superscore.widgets.tree import RootTree
from superscore.widgets.views import RootTree


@pytest.mark.parametrize(
Expand Down
2 changes: 1 addition & 1 deletion superscore/widgets/page/entry.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from superscore.model import Collection
from superscore.widgets.core import DataWidget, Display, NameDescTagsWidget
from superscore.widgets.manip_helpers import insert_widget
from superscore.widgets.tree import RootTree
from superscore.widgets.views import RootTree


class CollectionPage(Display, DataWidget):
Expand Down
73 changes: 2 additions & 71 deletions superscore/widgets/page/search.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from superscore.model import Collection, Entry, Readback, Setpoint, Snapshot
from superscore.widgets import ICON_MAP
from superscore.widgets.core import Display
from superscore.widgets.views import BaseTableEntryModel, ButtonDelegate

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -150,7 +151,7 @@ def subfilter_results(self) -> None:
self.proxy_model.invalidateFilter()


class ResultModel(QtCore.QAbstractTableModel):
class ResultModel(BaseTableEntryModel):
headers: List[str] = ['Name', 'Type', 'Description', 'Created', 'Open']

def __init__(self, *args, entries: List[Entry] = None, **kwargs) -> None:
Expand Down Expand Up @@ -198,76 +199,6 @@ def data(self, index: QtCore.QModelIndex, role: int) -> Any:
# if nothing is found, return invalid QVariant
return QtCore.QVariant()

def rowCount(self, index):
return len(self.entries)

def columnCount(self, index):
return len(self.headers)

def headerData(
self,
section: int,
orientation: QtCore.Qt.Orientation,
role: int
) -> Any:
"""
Returns the header data for the model.
Currently only displays horizontal header data
"""
if role != QtCore.Qt.DisplayRole:
return

if orientation == QtCore.Qt.Horizontal:
return self.headers[section]

def flags(self, index: QtCore.QModelIndex) -> QtCore.Qt.ItemFlag:
"""
Returns the item flags for the given ``index``. The returned
item flag controls what behaviors the item supports.

Parameters
----------
index : QtCore.QModelIndex
the index referring to a cell of the TableView

Returns
-------
QtCore.Qt.ItemFlag
the ItemFlag corresponding to the cell
"""
if (index.column() == len(self.headers) - 1):
return QtCore.Qt.ItemIsEditable | QtCore.Qt.ItemIsEnabled
else:
return QtCore.Qt.ItemIsEnabled


class ButtonDelegate(QtWidgets.QStyledItemDelegate):
clicked = QtCore.Signal(QtCore.QModelIndex)

def __init__(self, *args, button_text: str = '', **kwargs):
self.button_text = button_text
super().__init__(*args, **kwargs)

def createEditor(
self,
parent: QtWidgets.QWidget,
option,
index: QtCore.QModelIndex
) -> QtWidgets.QWidget:
button = QtWidgets.QPushButton(self.button_text, parent)
button.clicked.connect(
lambda _, index=index: self.clicked.emit(index)
)
return button

def updateEditorGeometry(
self,
editor: QtWidgets.QWidget,
option: QtWidgets.QStyleOptionViewItem,
index: QtCore.QModelIndex
) -> None:
return editor.setGeometry(option.rect)


class ResultFilterProxyModel(QtCore.QSortFilterProxyModel):
"""
Expand Down
Loading