diff --git a/superscore/tests/test_page.py b/superscore/tests/test_page.py
index 8f0482d..f812050 100644
--- a/superscore/tests/test_page.py
+++ b/superscore/tests/test_page.py
@@ -30,8 +30,8 @@ def collection_builder_page(qtbot: QtBot, sample_client: Client):
page = CollectionBuilderPage(client=sample_client)
qtbot.addWidget(page)
yield page
- page.pv_model.stop_polling()
- qtbot.waitUntil(lambda: page.pv_model._poll_thread.isFinished())
+ page.close()
+ qtbot.waitUntil(lambda: page.sub_pv_table_view._model._poll_thread.isFinished())
@pytest.mark.parametrize(
@@ -79,10 +79,10 @@ def test_coll_builder_add(collection_builder_page: CollectionBuilderPage):
assert len(page.data.children) == 1
assert "THIS:PV" in page.data.children[0].pv_name
assert isinstance(page.data.children[0], Parameter)
- assert page.pv_model.rowCount() == 1
+ assert page.sub_pv_table_view._model.rowCount() == 1
page.coll_combo_box.setCurrentIndex(0)
added_collection = page._coll_options[0]
page.add_collection_button.clicked.emit()
assert added_collection is page.data.children[1]
- assert page.coll_model.rowCount() == 1
+ assert page.sub_coll_table_view._model.rowCount() == 1
diff --git a/superscore/ui/collection_builder_page.ui b/superscore/ui/collection_builder_page.ui
index e62e688..58d5b31 100644
--- a/superscore/ui/collection_builder_page.ui
+++ b/superscore/ui/collection_builder_page.ui
@@ -60,8 +60,8 @@
Qt::Vertical
-
-
+
+
0
@@ -202,6 +202,18 @@
+
+
+ LivePVTableView
+ QTableView
+
+
+
+ NestableTableView
+ QTableView
+
+
+
diff --git a/superscore/widgets/page/collection_builder.py b/superscore/widgets/page/collection_builder.py
index 912d7d0..3bb7cca 100644
--- a/superscore/widgets/page/collection_builder.py
+++ b/superscore/widgets/page/collection_builder.py
@@ -9,9 +9,9 @@
from superscore.widgets.core import DataWidget, Display, NameDescTagsWidget
from superscore.widgets.enhanced import FilterComboBox
from superscore.widgets.manip_helpers import insert_widget
-from superscore.widgets.views import (BaseTableEntryModel, ButtonDelegate,
- LivePVHeader, LivePVTableModel,
- NestableTableModel, RootTree)
+from superscore.widgets.views import (BaseTableEntryModel, LivePVHeader,
+ LivePVTableView, NestableTableView,
+ RootTree)
logger = logging.getLogger(__name__)
@@ -25,14 +25,14 @@ class CollectionBuilderPage(Display, DataWidget):
tree_view: QtWidgets.QTreeView
- sub_coll_table_view: QtWidgets.QTableView
- sub_pv_table_view: QtWidgets.QTableView
+ sub_coll_table_view: NestableTableView
+ sub_pv_table_view: LivePVTableView
tab_widget: QtWidgets.QTabWidget
# PV tab
pv_line_edit: QtWidgets.QLineEdit
rbv_line_edit: QtWidgets.QLineEdit
- # Colleciton tab
+ # Collection tab
add_collection_button: QtWidgets.QPushButton
coll_combo_box: FilterComboBox
coll_combo_box_placeholder: QtWidgets.QComboBox
@@ -55,8 +55,6 @@ def __init__(
super().__init__(*args, data=data, **kwargs)
self.client = client
self.open_page_slot = open_page_slot
- self.pv_model = None
- self.coll_model = None
self.tree_model = None
self._coll_options: list[Collection] = []
self._title = self.data.title
@@ -78,30 +76,20 @@ def setup_ui(self):
self.add_pvs_button.clicked.connect(self.add_pv)
self.ro_checkbox.stateChanged.connect(self.set_rbv_enabled)
- self.update_model_data()
+ # set up views
+ self.sub_pv_table_view.client = self.client
+ self.sub_pv_table_view.set_data(self.data)
+ for i in [LivePVHeader.STORED_VALUE, LivePVHeader.STORED_SEVERITY,
+ LivePVHeader.STORED_STATUS]:
+ self.sub_pv_table_view.setColumnHidden(i, True)
+
+ self.sub_coll_table_view.client = self.client
+ self.sub_coll_table_view.set_data(self.data)
- # Configure button delegates
- self.pv_open_delegate = ButtonDelegate(button_text='open details')
- self.sub_pv_table_view.setItemDelegateForColumn(LivePVHeader.OPEN,
- self.pv_open_delegate)
- self.pv_open_delegate.clicked.connect(self.open_sub_pv_row)
-
- self.pv_remove_delegate = ButtonDelegate(button_text='remove')
- self.sub_pv_table_view.setItemDelegateForColumn(LivePVHeader.REMOVE,
- self.pv_remove_delegate)
- self.pv_remove_delegate.clicked.connect(self.remove_sub_pv_row)
-
- self.nest_open_delegate = ButtonDelegate(button_text='open details')
- self.sub_coll_table_view.setItemDelegateForColumn(
- 3, self.nest_open_delegate
- )
- self.nest_open_delegate.clicked.connect(self.open_sub_coll_row)
-
- self.nest_remove_delegate = ButtonDelegate(button_text='remove')
- self.sub_coll_table_view.setItemDelegateForColumn(
- 4, self.nest_remove_delegate
- )
- self.nest_remove_delegate.clicked.connect(self.remove_sub_coll_row)
+ self.tree_model = RootTree(base_entry=self.data)
+ self.tree_view.setModel(self.tree_model)
+ self.sub_coll_table_view.data_updated.connect(self.tree_model.refresh_tree)
+ self.sub_pv_table_view.data_updated.connect(self.tree_model.refresh_tree)
def _update_title(self):
"""Set title attribute for access by containing widgets"""
@@ -115,24 +103,6 @@ def open_row_details(
entry = model.entries[index.row()]
self.open_page_slot(entry)
- def open_sub_pv_row(self, index: QtCore.QModelIndex) -> None:
- self.open_row_details(self.pv_model, index)
-
- def open_sub_coll_row(self, index: QtCore.QModelIndex) -> None:
- self.open_row_details(self.coll_model, index)
-
- def remove_entry(self, entry: Entry) -> None:
- self.data.children.remove(entry)
- self.update_model_data()
-
- def remove_sub_pv_row(self, index: QtCore.QModelIndex) -> None:
- entry = self.pv_model.entries[index.row()]
- self.remove_entry(entry)
-
- def remove_sub_coll_row(self, index: QtCore.QModelIndex) -> None:
- entry = self.coll_model.entries[index.row()]
- self.remove_entry(entry)
-
def set_rbv_enabled(self, state: int):
"""Disable RBV line edit if read-only checkbox is enabled"""
self.rbv_line_edit.clear()
@@ -140,44 +110,11 @@ def set_rbv_enabled(self, state: int):
def update_model_data(self):
"""
- Update the model data. If no models exist, initialize new ones.
- If models have already been initialized,
+ Update the model data. Signal the models to re-read the data
"""
- # tree model
- if self.tree_model is None:
- self.tree_model = RootTree(base_entry=self.data)
- self.tree_view.setModel(self.tree_model)
- else:
- self.tree_model.refresh_tree()
-
- # initialize tables
- self.sub_colls = [child for child in self.data.children
- if isinstance(child, Collection)]
- self.sub_pvs = [child for child in self.data.children
- if not isinstance(child, Collection)]
-
- logger.debug(f"Updating view with {len(self.sub_pvs)} parameters "
- f"and {len(self.sub_colls)} collections")
-
- # PVEntry model
- if self.pv_model is None:
- self.pv_model = LivePVTableModel(entries=self.sub_pvs, client=self.client)
- self.sub_pv_table_view.setModel(self.pv_model)
-
- # TODO: un-hard code this once there is a better way of managing columns
- # Potentially dealing with columns that have moved
- for i in [LivePVHeader.STORED_VALUE, LivePVHeader.STORED_SEVERITY,
- LivePVHeader.STORED_STATUS]:
- self.sub_pv_table_view.setColumnHidden(i, True)
- else:
- self.pv_model.set_entries(self.sub_pvs)
-
- # Nestable Model
- if self.coll_model is None:
- self.coll_model = NestableTableModel(entries=self.sub_colls)
- self.sub_coll_table_view.setModel(self.coll_model)
- else:
- self.coll_model.set_entries(self.sub_colls)
+ self.tree_model.refresh_tree()
+ self.sub_pv_table_view.set_data(self.data)
+ self.sub_coll_table_view.set_data(self.data)
def save_collection(self):
"""Save current collection to database via Client"""
@@ -246,5 +183,5 @@ def add_sub_collection(self):
def closeEvent(self, a0: QCloseEvent) -> None:
logger.debug("Stopping pv_model polling")
- self.pv_model.stop_polling(wait_time=5000)
+ self.sub_pv_table_view._model.stop_polling(wait_time=5000)
return super().closeEvent(a0)
diff --git a/superscore/widgets/views.py b/superscore/widgets/views.py
index 76cbca2..6e79a37 100644
--- a/superscore/widgets/views.py
+++ b/superscore/widgets/views.py
@@ -492,10 +492,6 @@ def set_entries(self, entries: List[Entry]):
"""
self.layoutAboutToBeChanged.emit()
self.entries = entries
- self.dataChanged.emit(
- self.createIndex(0, 0),
- self.createIndex(self.rowCount(), self.columnCount()),
- )
self.layoutChanged.emit()
def headerData(
@@ -538,7 +534,21 @@ def add_entry(self, entry: Entry) -> None:
if entry in self.entries or not isinstance(entry, Entry):
return
+ self.layoutAboutToBeChanged.emit()
self.entries.append[entry]
+ self.layoutChanged()
+
+ def remove_row(self, row_index: int) -> None:
+ self.remove_entry(self.entries[row_index])
+
+ def remove_entry(self, entry: Entry) -> None:
+ self.layoutAboutToBeChanged.emit()
+ try:
+ self.entries.remove(entry)
+ except ValueError:
+ logger.debug(f"Entry of type ({type(entry).__name__})"
+ "not found in table, could not remove.")
+ self.layoutChanged()
def icon(self, entry: Entry) -> Optional[QtGui.QIcon]:
"""return icon for this ``entry``"""
@@ -587,7 +597,6 @@ def __init__(
*args,
client: Client,
entries: Optional[List[PVEntry]] = None,
- open_page_slot: Optional[Callable] = None,
poll_period: float = 1.0,
**kwargs
) -> None:
@@ -595,7 +604,6 @@ def __init__(
self.headers = [h.header_name() for h in LivePVHeader]
self.client = client
- self.open_page_slot = open_page_slot
self.poll_period = poll_period
self._data_cache = {e.pv_name: None for e in entries}
self._poll_thread = None
@@ -683,6 +691,14 @@ def set_entries(self, entries: list[PVEntry]) -> None:
)
self.layoutChanged.emit()
+ def remove_entry(self, entry: PVEntry) -> None:
+ """Remove ``entry`` from the table model"""
+ super().remove_entry(entry)
+ self.layoutAboutToBeChanged.emit()
+ self._data_cache = {e.pv_name: None for e in self.entries}
+ self._poll_thread.data = self._data_cache
+ self.layoutChanged.emit()
+
def index_from_item(
self,
item: PVEntry,
@@ -730,11 +746,9 @@ def data(self, index: QtCore.QModelIndex, role: int) -> Any:
the requested data
"""
entry: PVEntry = self.entries[index.row()]
-
- # Special handling for open button delegate
- if index.column() == (len(self.headers) - 1):
- if role in (QtCore.Qt.DisplayRole, QtCore.Qt.EditRole):
- return 'click to open'
+ if isinstance(entry, UUID):
+ entry = self.client.backend.get_entry(self.entries[index.row()])
+ self.entries[index.row()] = entry
if index.column() == LivePVHeader.PV_NAME:
if role == QtCore.Qt.DecorationRole:
@@ -915,6 +929,161 @@ def run(self):
time.sleep(max((0, self.poll_period - elapsed)))
+class BaseDataTableView(QtWidgets.QTableView):
+ """
+ Base TableView for holding and manipulating an entry / list of entries
+ TODO: signature docs
+ """
+ # signal indicating the contained has been updated
+ data_updated: ClassVar[QtCore.Signal] = QtCore.Signal()
+ _model: Optional[Union[LivePVTableModel, NestableTableModel]] = None
+ _model_cls: Union[LivePVTableModel, NestableTableModel] = LivePVTableModel
+ open_column: int = 0
+ remove_column: int = 0
+
+ def __init__(
+ self,
+ *args,
+ client: Optional[Client] = None,
+ data: Optional[Union[Entry, List[Entry]]] = None,
+ open_page_slot: Optional[Callable] = None,
+ **kwargs,
+ ) -> None:
+ """need to set open_column, close_column in subclass"""
+ super().__init__(*args, **kwargs)
+ self.data = data
+ self._client = client
+ self.open_page_slot = open_page_slot
+ self.sub_entries = []
+
+ # only these for now, may need an update later
+ self.setup_ui()
+
+ def setup_ui(self):
+ """initialize ui elements for this table"""
+ # set delegates
+ self.open_delegate = ButtonDelegate(button_text='open details')
+ self.setItemDelegateForColumn(self.open_column, self.open_delegate)
+ self.open_delegate.clicked.connect(self.open_row_details)
+
+ self.remove_delegate = ButtonDelegate(button_text='remove')
+ self.setItemDelegateForColumn(self.remove_column, self.remove_delegate)
+ self.remove_delegate.clicked.connect(self.remove_row)
+
+ def open_row_details(self, index: QtCore.QModelIndex) -> None:
+ entry = self._model.entries[index.row()]
+ self.open_page_slot(entry)
+
+ def remove_row(self, index: QtCore.QModelIndex) -> None:
+ entry = self._model.entries[index.row()]
+ self._model.remove_row(index.row())
+
+ if isinstance(self.data, list):
+ self.data.remove(entry)
+ elif isinstance(self.data, Nestable):
+ self.data.children.remove(entry)
+ # edit data held by widget
+ self.data_updated.emit()
+
+ def set_data(self, data: Any):
+ """Set the data for this view, re-setup ui"""
+ if not isinstance(data, (list, Nestable)):
+ raise ValueError(
+ f"Attempted to set an incompatable data type ({type(data)})"
+ )
+ self.data = data
+ self.gather_sub_entries()
+
+ if self.client is None:
+ logger.debug("Client not yet set, cannot initialize model")
+ return
+
+ if self._model is None:
+ self._model = self._model_cls(
+ client=self.client,
+ entries=self.sub_entries
+ )
+ self.setModel(self._model)
+ else:
+ self._model.set_entries(self.sub_entries)
+
+ self.data_updated.emit()
+
+ def gather_sub_entries(self):
+ raise NotImplementedError
+
+ @property
+ def client(self):
+ return self._client
+
+ @client.setter
+ def client(self, client: Client):
+ self._set_client(client)
+
+ def _set_client(self, client: Client):
+ if client is self._client:
+ return
+
+ if not isinstance(client, Client):
+ raise ValueError("Provided client is not a superscore Client")
+
+ self._client = client
+
+
+class LivePVTableView(BaseDataTableView):
+ """
+ table widget for LivePVTableModel. Meant to provide a standard, easy-to-use
+ interface for this table model, with common configuration options exposed
+
+ compatible with list of entries or full entry
+ - updates entry when changes made
+ - maintains order for rebuilding of parent collections
+ Configures delegates, ignoring open page slot if provided
+ Column manipulation
+ - show/hide
+ - re-order
+ flattening of base data
+ - handling of readbacks associated with base entries?
+ - handling of nested nestables
+ ediable stored fields if desired, updating entry
+ """
+ # TODO: add config args / methods: {show/hide}, poll period, column order?
+ _model: Optional[LivePVTableModel]
+
+ def __init__(self, *args, **kwargs):
+ self._model_cls = LivePVTableModel
+ self.open_column = LivePVHeader.OPEN
+ self.remove_column = LivePVHeader.REMOVE
+ super().__init__(*args, **kwargs)
+
+ def gather_sub_entries(self):
+ if isinstance(self.data, UUID):
+ self.data = self.client.backend.get_entry(self.data)
+
+ # TODO: gather and fill entries where necessary
+ if isinstance(self.data, Nestable):
+ # gather sub_nestables
+ self.sub_entries = [child for child in self.data.children
+ if not isinstance(child, Nestable)]
+
+ for i, sub_nest in enumerate(self.sub_entries):
+ if isinstance(sub_nest, UUID):
+ new_entry = self._client.backend.get_entry(sub_nest)
+ self.sub_entries[i] = new_entry
+
+ if isinstance(self.data, (Parameter, Setpoint, Readback)):
+ self.sub_entries = [self.data]
+
+ @BaseDataTableView.client.setter
+ def client(self, client: Optional[Client]):
+ super()._set_client(client)
+ # reset model poll thread
+ if self._model is not None:
+ self._model.stop_polling()
+ self._model.client = self._client
+ self._model.start_polling()
+
+
class NestableTableModel(BaseTableEntryModel):
# Shows simplified details (created time, description, # pvs, # child colls)
# Open details delegate
@@ -923,6 +1092,7 @@ class NestableTableModel(BaseTableEntryModel):
def __init__(
self,
*args,
+ client: Optional[Client] = None,
entries: Optional[List[Union[Snapshot, Collection]]] = None,
open_page_slot: Optional[Callable] = None,
**kwargs
@@ -949,11 +1119,6 @@ def data(self, index: QtCore.QModelIndex, role: int) -> Any:
"""
entry: Entry = self.entries[index.row()]
- # Special handling for open button delegate
- if index.column() == (len(self.headers) - 1):
- if role in (QtCore.Qt.DisplayRole, QtCore.Qt.EditRole):
- return 'click to open'
-
if role != QtCore.Qt.DisplayRole:
# table is read only
return QtCore.QVariant()
@@ -973,6 +1138,28 @@ def data(self, index: QtCore.QModelIndex, role: int) -> Any:
return "Remove"
+class NestableTableView(BaseDataTableView):
+ def __init__(self, *args, **kwargs):
+ self._model_cls = NestableTableModel
+ self.open_column = 3
+ self.remove_column = 4
+ super().__init__(*args, **kwargs)
+
+ def gather_sub_entries(self):
+ if isinstance(self.data, UUID):
+ self.data = self.client.backend.get_entry(self.data)
+ # TODO: gather and fill entries where necessary
+ if isinstance(self.data, Nestable):
+ # gather sub_nestables
+ self.sub_entries = [child for child in self.data.children
+ if isinstance(child, Nestable)]
+
+ for i, sub_nest in enumerate(self.sub_entries):
+ if isinstance(sub_nest, UUID):
+ new_entry = self._client.backend.get_entry(sub_nest)
+ self.sub_entries[i] = new_entry
+
+
class ButtonDelegate(QtWidgets.QStyledItemDelegate):
clicked = QtCore.Signal(QtCore.QModelIndex)