0
0
mirror of https://github.com/bec-project/bec_widgets.git synced 2025-07-13 11:11:49 +02:00

refactor: put QTreeWidget in a container, rather than inheriting from it, and avoid multiple inheritance of BECConnector

Inheriting from QTreeWidget causes havoc with display (see #245),
also it is important to parent items OR to give them a label (weird)
otherwise it also has display glitches.

Most of the time, composition has to be preferred over inheritance ;
inheritance is a question of behaviour - is the behaviour the same ?
Here, a widget is really not a BECConnector, but uses a BECConnector
(at least this is my understanding). Because a Widget and BECConnector
do not behave the same. It is easier, lighter to deal with single
inheritance.

The singleton usage is superfluous, since the underlying client is already
a singleton. Multiple BECConnector objects can be created, there won't
be more connections.

BECServiceStatusMixin has been removed in favor of the widget's own timer,
since it was causing "QObject::killTimer: Timers cannot be stopped from
another thread" errors (at least on my computer).

Also removes "redundant items check" ; where do those would come from?
This commit is contained in:
2024-07-03 14:04:34 +02:00
parent f90bc00c18
commit 011103fde3

View File

@ -11,8 +11,8 @@ from typing import TYPE_CHECKING
import qdarktheme
from bec_lib.utils.import_utils import lazy_import_from
from qtpy.QtCore import QObject, QTimer, Signal, Slot
from qtpy.QtWidgets import QTreeWidget, QTreeWidgetItem
from qtpy.QtCore import Signal, Slot
from qtpy.QtWidgets import QTreeWidget, QTreeWidgetItem, QVBoxLayout, QWidget
from bec_widgets.utils.bec_connector import BECConnector
from bec_widgets.widgets.bec_status_box.status_item import StatusItem
@ -35,30 +35,7 @@ class BECServiceInfoContainer:
metrics: dict | None
class BECServiceStatusMixin(QObject):
"""Mixin to receive the latest service status from the BEC server and emit it via services_update signal.
Args:
client (BECClient): The client object to connect to the BEC server.
"""
services_update = Signal(dict, dict)
def __init__(self, client: BECClient):
super().__init__()
self.client = client
self._service_update_timer = QTimer()
self._service_update_timer.timeout.connect(self._get_service_status)
self._service_update_timer.start(1000)
def _get_service_status(self):
"""Get the latest service status from the BEC server."""
# pylint: disable=protected-access
self.client._update_existing_services()
self.services_update.emit(self.client._services_info, self.client._services_metric)
class BECStatusBox(BECConnector, QTreeWidget):
class BECStatusBox(QWidget):
"""An autonomous widget to display the status of BEC services.
Args:
@ -74,63 +51,19 @@ class BECStatusBox(BECConnector, QTreeWidget):
service_update = Signal(BECServiceInfoContainer)
bec_core_state = Signal(str)
_initialized = False
_bec_status_box = None
def __init__(
self,
parent=None,
box_name: str = "BEC Server",
client: BECClient = None,
bec_service_status_mixin: BECServiceStatusMixin = None,
gui_id: str = None,
):
if self._initialized == True:
return
super().__init__(client=client, gui_id=gui_id)
QTreeWidget.__init__(self, parent=parent)
self.box_name = box_name
self.status_container = defaultdict(lambda: {"info": None, "item": None, "widget": None})
self._initialized = False
if not bec_service_status_mixin:
bec_service_status_mixin = BECServiceStatusMixin(client=self.client)
self.bec_service_status = bec_service_status_mixin
if not self._initialized:
self.init_ui()
self.bec_service_status.services_update.connect(self.update_service_status)
self.bec_core_state.connect(self.update_top_item_status)
self.itemDoubleClicked.connect(self.on_tree_item_double_clicked)
def __new__(cls, *args, forced: bool = False, **kwargs):
if forced:
cls._initialized = False
cls._bec_status_box = super(BECStatusBox, cls).__new__(cls)
return cls._bec_status_box
if cls._bec_status_box is not None and cls._initialized is True:
return cls._bec_status_box
cls._bec_status_box = super(BECStatusBox, cls).__new__(cls)
return cls._bec_status_box
def init_ui(self) -> None:
"""Init the UI for the BECStatusBox widget, should only take place once."""
self.init_ui_tree_widget()
top_label = self._create_status_widget(self.box_name, status=BECStatus.IDLE)
tree_item = QTreeWidgetItem()
tree_item.setExpanded(True)
tree_item.setDisabled(True)
self.status_container[self.box_name].update({"item": tree_item, "widget": top_label})
self.addTopLevelItem(tree_item)
self.setItemWidget(tree_item, 0, top_label)
self.service_update.connect(top_label.update_config)
self._initialized = True
def init_ui_tree_widget(self) -> None:
"""Initialise the tree widget for the status box."""
self.setHeaderHidden(True)
self.setStyleSheet(
QWidget.__init__(self, parent=parent)
self.setLayout(QVBoxLayout(self))
self.tree = QTreeWidget(self)
self.layout().addWidget(self.tree)
self.tree.setHeaderHidden(True)
self.tree.setStyleSheet(
"QTreeWidget::item:!selected "
"{ "
"border: 1px solid gainsboro; "
@ -139,6 +72,38 @@ class BECStatusBox(BECConnector, QTreeWidget):
"}"
"QTreeWidget::item:selected {}"
)
self.box_name = box_name
self.status_container = defaultdict(lambda: {"info": None, "item": None, "widget": None})
self.connector = BECConnector(client=client, gui_id=gui_id)
self.init_ui()
self.bec_core_state.connect(self.update_top_item_status)
self.tree.itemDoubleClicked.connect(self.on_tree_item_double_clicked)
self.startTimer(
1000
) # use qobject's own timer instead of creating one, which may be stopped from another thread(?)
def timerEvent(self, event):
"""Get the latest service status from the BEC server."""
# pylint: disable=protected-access
self.connector.client._update_existing_services()
self.update_service_status(
self.connector.client._services_info, self.connector.client._services_metric
)
def init_ui(self) -> None:
"""Init the UI for the BECStatusBox widget"""
top_label = self._create_status_widget(self.box_name, status=BECStatus.IDLE)
tree_item = QTreeWidgetItem(self.tree)
tree_item.setExpanded(True)
tree_item.setDisabled(True)
self.status_container[self.box_name].update({"item": tree_item, "widget": top_label})
self.tree.setItemWidget(tree_item, 0, top_label)
self.tree.addTopLevelItem(tree_item)
self.service_update.connect(top_label.update_config)
self._initialized = True
def _create_status_widget(
self, service_name: str, status=BECStatus, info: dict = None, metrics: dict = None
@ -158,7 +123,7 @@ class BECStatusBox(BECConnector, QTreeWidget):
if info is None:
info = {}
self._update_status_container(service_name, status, info, metrics)
item = StatusItem(parent=self, config=self.status_container[service_name]["info"])
item = StatusItem(parent=self.tree, config=self.status_container[service_name]["info"])
return item
@Slot(str)
@ -213,13 +178,10 @@ class BECStatusBox(BECConnector, QTreeWidget):
checked.append(service_name)
metric_msg = services_metric.get(service_name, None)
metrics = metric_msg.metrics if metric_msg else None
if service_name in self.status_container:
self._update_status_container(service_name, msg.status, msg.info, metrics)
self.service_update.emit(self.status_container[service_name]["info"])
continue
self.add_tree_item(service_name, msg.status, msg.info, metrics)
self.check_redundant_tree_items(checked)
if service_name not in self.status_container:
self.add_tree_item(service_name, msg.status, msg.info, metrics)
self._update_status_container(service_name, msg.status, msg.info, metrics)
self.service_update.emit(self.status_container[service_name]["info"])
def update_core_services(self, services_info: dict, services_metric: dict) -> dict:
"""Update the core services of BEC, and emit the updated status to the BECStatusBox.
@ -251,19 +213,6 @@ class BECStatusBox(BECConnector, QTreeWidget):
self.bec_core_state.emit(core_state.name if core_state else "NOTCONNECTED")
return services_info
def check_redundant_tree_items(self, checked: list) -> None:
"""Utility method to check and remove redundant objects from the BECStatusBox.
Args:
checked (list): A list of services that are currently running.
"""
to_be_deleted = [key for key in self.status_container if key not in checked]
for key in to_be_deleted:
obj = self.status_container.pop(key)
item = obj["item"]
self.status_container[self.box_name]["item"].removeChild(item)
def add_tree_item(
self, service_name: str, status: BECStatus, info: dict = None, metrics: dict = None
) -> None:
@ -276,10 +225,12 @@ class BECStatusBox(BECConnector, QTreeWidget):
metrics (dict): The metrics of the service.
"""
item_widget = self._create_status_widget(service_name, status, info, metrics)
item = QTreeWidgetItem() # setDisabled=True
toplevel_item = self.status_container[self.box_name]["item"]
item = QTreeWidgetItem(toplevel_item) # setDisabled=True
toplevel_item.addChild(item)
self.tree.setItemWidget(item, 0, item_widget)
self.service_update.connect(item_widget.update_config)
self.status_container[self.box_name]["item"].addChild(item)
self.setItemWidget(item, 0, item_widget)
self.status_container[service_name].update({"item": item, "widget": item_widget})
@Slot(QTreeWidgetItem, int)
@ -295,13 +246,7 @@ class BECStatusBox(BECConnector, QTreeWidget):
objects["widget"].show_popup()
def closeEvent(self, event):
"""Upon closing the widget, clean up the BECStatusBox and the QTreeWidget.
Args:
event: The close event.
"""
super().cleanup()
QTreeWidget().closeEvent(event)
self.connector.cleanup()
def main():