0
0
mirror of https://github.com/bec-project/bec_widgets.git synced 2025-07-12 18:51:50 +02:00

feat(main_window): main window can display the messages from the send_client_info as a scrolling horizontal text; closes #700

This commit is contained in:
2025-06-13 14:41:16 +02:00
committed by Jan Wyzula
parent 57b9a57a63
commit 0dec78afba
2 changed files with 169 additions and 18 deletions

View File

@ -0,0 +1,89 @@
from qtpy.QtCore import QTimer
from qtpy.QtGui import QFontMetrics, QPainter
from qtpy.QtWidgets import QLabel
class ScrollLabel(QLabel):
"""A QLabel that scrolls its text horizontally across the widget."""
def __init__(self, parent=None, speed_ms=30, step_px=1, delay_ms=2000):
super().__init__(parent=parent)
self._offset = 0
self._text_width = 0
# scrolling timer (runs continuously once started)
self._timer = QTimer(self)
self._timer.setInterval(speed_ms)
self._timer.timeout.connect(self._scroll)
# delaybeforescroll timer (singleshot)
self._delay_timer = QTimer(self)
self._delay_timer.setSingleShot(True)
self._delay_timer.setInterval(delay_ms)
self._delay_timer.timeout.connect(self._timer.start)
self._step_px = step_px
def setText(self, text):
super().setText(text)
fm = QFontMetrics(self.font())
self._text_width = fm.horizontalAdvance(text)
self._offset = 0
self._update_timer()
def resizeEvent(self, event):
super().resizeEvent(event)
self._update_timer()
def _update_timer(self):
"""
Decide whether to start or stop scrolling.
If the text is wider than the visible area, start a singleshot
delay timer (2s by default). Scrolling begins only after this
delay. Any change (resize or new text) restarts the logic.
"""
needs_scroll = self._text_width > self.width()
if needs_scroll:
if self._timer.isActive():
self._timer.stop()
self._offset = 0
if not self._delay_timer.isActive():
self._delay_timer.start()
else:
if self._delay_timer.isActive():
self._delay_timer.stop()
if self._timer.isActive():
self._timer.stop()
self.update()
def _scroll(self):
self._offset += self._step_px
if self._offset >= self._text_width:
self._offset = 0
self.update()
def paintEvent(self, event):
painter = QPainter(self)
painter.setRenderHint(QPainter.TextAntialiasing)
text = self.text()
if not text:
return
fm = QFontMetrics(self.font())
y = (self.height() + fm.ascent() - fm.descent()) // 2
if self._text_width <= self.width():
painter.drawText(0, y, text)
else:
x = -self._offset
gap = 50 # space between repeating text blocks
while x < self.width():
painter.drawText(x, y, text)
x += self._text_width + gap
def cleanup(self):
"""Stop all timers to prevent memory leaks."""
if self._timer.isActive():
self._timer.stop()
if self._delay_timer.isActive():
self._delay_timer.stop()

View File

@ -1,8 +1,9 @@
import os
from qtpy.QtCore import QEvent, QSize
from bec_lib.endpoints import MessageEndpoints
from qtpy.QtCore import QEvent, QSize, Qt
from qtpy.QtGui import QAction, QActionGroup, QIcon
from qtpy.QtWidgets import QApplication, QLabel, QMainWindow, QStyle
from qtpy.QtWidgets import QApplication, QFrame, QLabel, QMainWindow, QStyle, QVBoxLayout, QWidget
import bec_widgets
from bec_widgets.utils import UILoader
@ -10,6 +11,7 @@ from bec_widgets.utils.bec_widget import BECWidget
from bec_widgets.utils.colors import apply_theme
from bec_widgets.utils.error_popups import SafeSlot
from bec_widgets.utils.widget_io import WidgetHierarchy
from bec_widgets.widgets.containers.main_window.addons.scroll_label import ScrollLabel
from bec_widgets.widgets.containers.main_window.addons.web_links import BECWebLinksMixin
MODULE_PATH = os.path.dirname(bec_widgets.__file__)
@ -35,6 +37,14 @@ class BECMainWindow(BECWidget, QMainWindow):
self._init_ui()
self._connect_to_theme_change()
# Connections to BEC Notifications
self.bec_dispatcher.connect_slot(
self.display_client_message, MessageEndpoints.client_info()
)
################################################################################
# MainWindow Elements Initialization
################################################################################
def _init_ui(self):
# Set the icon
@ -55,39 +65,59 @@ class BECMainWindow(BECWidget, QMainWindow):
# Left: AppID label
self._app_id_label = QLabel()
self._app_id_label.setAlignment(
Qt.AlignmentFlag.AlignHCenter | Qt.AlignmentFlag.AlignVCenter
)
status_bar.addWidget(self._app_id_label)
# Add a separator after the app ID label
self._add_separator()
# Centre: Clientinfo label (stretch=1 so it expands)
self._client_info_label = ScrollLabel()
self._client_info_label.setAlignment(
Qt.AlignmentFlag.AlignHCenter | Qt.AlignmentFlag.AlignVCenter
)
status_bar.addWidget(self._client_info_label, 1)
def _add_separator(self):
"""
Add a vertically centred separator to the status bar.
"""
status_bar = self.statusBar()
# The actual line
line = QFrame()
line.setFrameShape(QFrame.VLine)
line.setFrameShadow(QFrame.Sunken)
line.setFixedHeight(status_bar.sizeHint().height() - 2)
# Wrapper to center the line vertically -> work around for QFrame not being able to center itself
wrapper = QWidget()
vbox = QVBoxLayout(wrapper)
vbox.setContentsMargins(0, 0, 0, 0)
vbox.addStretch()
vbox.addWidget(line, alignment=Qt.AlignHCenter)
vbox.addStretch()
wrapper.setFixedWidth(line.sizeHint().width())
status_bar.addWidget(wrapper)
def _init_bec_icon(self):
icon = self.app.windowIcon()
if icon.isNull():
print("No icon is set, setting default icon")
icon = QIcon()
icon.addFile(
os.path.join(MODULE_PATH, "assets", "app_icons", "bec_widgets_icon.png"),
size=QSize(48, 48),
)
self.app.setWindowIcon(icon)
else:
print("An icon is set")
def load_ui(self, ui_file):
loader = UILoader(self)
self.ui = loader.loader(ui_file)
self.setCentralWidget(self.ui)
def display_app_id(self):
"""
Display the app ID in the status bar.
"""
if self.bec_dispatcher.cli_server is None:
status_message = "Not connected"
else:
# Get the server ID from the dispatcher
server_id = self.bec_dispatcher.cli_server.gui_id
status_message = f"App ID: {server_id}"
if hasattr(self, "_app_id_label"):
self._app_id_label.setText(status_message)
def _fetch_theme(self) -> str:
return self.app.theme.theme
@ -175,8 +205,37 @@ class BECMainWindow(BECWidget, QMainWindow):
help_menu.addAction(widgets_docs)
help_menu.addAction(bug_report)
################################################################################
# Status Bar Addons
################################################################################
def display_app_id(self):
"""
Display the app ID in the status bar.
"""
if self.bec_dispatcher.cli_server is None:
status_message = "Not connected"
else:
# Get the server ID from the dispatcher
server_id = self.bec_dispatcher.cli_server.gui_id
status_message = f"App ID: {server_id}"
self._app_id_label.setText(status_message)
@SafeSlot(dict, dict)
def display_client_message(self, msg: dict, meta: dict):
message = msg.get("message", "")
self._client_info_label.setText(message)
################################################################################
# General and Cleanup Methods
################################################################################
@SafeSlot(str)
def change_theme(self, theme: str):
"""
Change the theme of the application.
Args:
theme(str): The theme to apply, either "light" or "dark".
"""
apply_theme(theme)
def event(self, event):
@ -199,6 +258,9 @@ class BECMainWindow(BECWidget, QMainWindow):
child.cleanup()
child.close()
child.deleteLater()
# Status bar widgets cleanup
self._client_info_label.cleanup()
super().cleanup()