mirror of
https://github.com/bec-project/bec_widgets.git
synced 2025-07-13 19:21: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:
@ -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)
|
||||||
|
|
||||||
|
# delay‑before‑scroll timer (single‑shot)
|
||||||
|
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 single‑shot
|
||||||
|
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()
|
@ -1,8 +1,9 @@
|
|||||||
import os
|
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.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
|
import bec_widgets
|
||||||
from bec_widgets.utils import UILoader
|
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.colors import apply_theme
|
||||||
from bec_widgets.utils.error_popups import SafeSlot
|
from bec_widgets.utils.error_popups import SafeSlot
|
||||||
from bec_widgets.utils.widget_io import WidgetHierarchy
|
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
|
from bec_widgets.widgets.containers.main_window.addons.web_links import BECWebLinksMixin
|
||||||
|
|
||||||
MODULE_PATH = os.path.dirname(bec_widgets.__file__)
|
MODULE_PATH = os.path.dirname(bec_widgets.__file__)
|
||||||
@ -35,6 +37,14 @@ class BECMainWindow(BECWidget, QMainWindow):
|
|||||||
self._init_ui()
|
self._init_ui()
|
||||||
self._connect_to_theme_change()
|
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):
|
def _init_ui(self):
|
||||||
|
|
||||||
# Set the icon
|
# Set the icon
|
||||||
@ -55,39 +65,59 @@ class BECMainWindow(BECWidget, QMainWindow):
|
|||||||
|
|
||||||
# Left: App‑ID label
|
# Left: App‑ID label
|
||||||
self._app_id_label = QLabel()
|
self._app_id_label = QLabel()
|
||||||
|
self._app_id_label.setAlignment(
|
||||||
|
Qt.AlignmentFlag.AlignHCenter | Qt.AlignmentFlag.AlignVCenter
|
||||||
|
)
|
||||||
status_bar.addWidget(self._app_id_label)
|
status_bar.addWidget(self._app_id_label)
|
||||||
|
|
||||||
|
# Add a separator after the app ID label
|
||||||
|
self._add_separator()
|
||||||
|
|
||||||
|
# Centre: Client‑info 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):
|
def _init_bec_icon(self):
|
||||||
icon = self.app.windowIcon()
|
icon = self.app.windowIcon()
|
||||||
if icon.isNull():
|
if icon.isNull():
|
||||||
print("No icon is set, setting default icon")
|
|
||||||
icon = QIcon()
|
icon = QIcon()
|
||||||
icon.addFile(
|
icon.addFile(
|
||||||
os.path.join(MODULE_PATH, "assets", "app_icons", "bec_widgets_icon.png"),
|
os.path.join(MODULE_PATH, "assets", "app_icons", "bec_widgets_icon.png"),
|
||||||
size=QSize(48, 48),
|
size=QSize(48, 48),
|
||||||
)
|
)
|
||||||
self.app.setWindowIcon(icon)
|
self.app.setWindowIcon(icon)
|
||||||
else:
|
|
||||||
print("An icon is set")
|
|
||||||
|
|
||||||
def load_ui(self, ui_file):
|
def load_ui(self, ui_file):
|
||||||
loader = UILoader(self)
|
loader = UILoader(self)
|
||||||
self.ui = loader.loader(ui_file)
|
self.ui = loader.loader(ui_file)
|
||||||
self.setCentralWidget(self.ui)
|
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:
|
def _fetch_theme(self) -> str:
|
||||||
return self.app.theme.theme
|
return self.app.theme.theme
|
||||||
|
|
||||||
@ -175,8 +205,37 @@ class BECMainWindow(BECWidget, QMainWindow):
|
|||||||
help_menu.addAction(widgets_docs)
|
help_menu.addAction(widgets_docs)
|
||||||
help_menu.addAction(bug_report)
|
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)
|
@SafeSlot(str)
|
||||||
def change_theme(self, theme: 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)
|
apply_theme(theme)
|
||||||
|
|
||||||
def event(self, event):
|
def event(self, event):
|
||||||
@ -199,6 +258,9 @@ class BECMainWindow(BECWidget, QMainWindow):
|
|||||||
child.cleanup()
|
child.cleanup()
|
||||||
child.close()
|
child.close()
|
||||||
child.deleteLater()
|
child.deleteLater()
|
||||||
|
|
||||||
|
# Status bar widgets cleanup
|
||||||
|
self._client_info_label.cleanup()
|
||||||
super().cleanup()
|
super().cleanup()
|
||||||
|
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user