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 +
superscore.widgets.views
+
+ + NestableTableView + QTableView +
superscore.widgets.views
+
+
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)