From 663c00f1a4d05d2742510584ec3e27cddaf8b122 Mon Sep 17 00:00:00 2001 From: wyzula-jan Date: Thu, 2 Oct 2025 12:33:32 +0200 Subject: [PATCH] feat(actions): actions can be created with label text with beside or under alignment --- bec_widgets/utils/toolbars/actions.py | 69 +++++++++++++++++++++++++-- bec_widgets/utils/toolbars/toolbar.py | 25 +++++++++- 2 files changed, 90 insertions(+), 4 deletions(-) diff --git a/bec_widgets/utils/toolbars/actions.py b/bec_widgets/utils/toolbars/actions.py index 4278877b..dbeb937c 100644 --- a/bec_widgets/utils/toolbars/actions.py +++ b/bec_widgets/utils/toolbars/actions.py @@ -33,6 +33,32 @@ logger = bec_logger.logger MODULE_PATH = os.path.dirname(bec_widgets.__file__) +def create_action_with_text(toolbar_action, toolbar: QToolBar): + """ + Helper function to create a toolbar button with text beside or under the icon. + + Args: + toolbar_action(ToolBarAction): The toolbar action to create the button for. + toolbar(ModularToolBar): The toolbar to add the button to. + """ + + btn = QToolButton(parent=toolbar) + if getattr(toolbar_action, "label_text", None): + toolbar_action.action.setText(toolbar_action.label_text) + if getattr(toolbar_action, "tooltip", None): + toolbar_action.action.setToolTip(toolbar_action.tooltip) + btn.setToolTip(toolbar_action.tooltip) + + btn.setDefaultAction(toolbar_action.action) + btn.setAutoRaise(True) + if toolbar_action.text_position == "beside": + btn.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) + else: + btn.setToolButtonStyle(Qt.ToolButtonTextUnderIcon) + btn.setText(toolbar_action.label_text) + toolbar.addWidget(btn) + + class NoCheckDelegate(QStyledItemDelegate): """To reduce space in combo boxes by removing the checkmark.""" @@ -114,15 +140,39 @@ class SeparatorAction(ToolBarAction): class QtIconAction(ToolBarAction): - def __init__(self, standard_icon, tooltip=None, checkable=False, parent=None): + def __init__( + self, + standard_icon, + tooltip=None, + checkable=False, + label_text: str | None = None, + text_position: Literal["beside", "under"] | None = None, + parent=None, + ): + """ + Action with a standard Qt icon for the toolbar. + + Args: + standard_icon: The standard icon from QStyle. + tooltip(str, optional): The tooltip for the action. Defaults to None. + checkable(bool, optional): Whether the action is checkable. Defaults to False. + label_text(str | None, optional): Optional label text to display beside or under the icon. + text_position(Literal["beside", "under"] | None, optional): Position of text relative to icon. + parent(QWidget or None, optional): Parent widget for the underlying QAction. + """ super().__init__(icon_path=None, tooltip=tooltip, checkable=checkable) self.standard_icon = standard_icon self.icon = QApplication.style().standardIcon(standard_icon) self.action = QAction(icon=self.icon, text=self.tooltip, parent=parent) self.action.setCheckable(self.checkable) + self.label_text = label_text + self.text_position = text_position def add_to_toolbar(self, toolbar, target): - toolbar.addAction(self.action) + if self.label_text is not None: + create_action_with_text(toolbar_action=self, toolbar=toolbar) + else: + toolbar.addAction(self.action) def get_icon(self): return self.icon @@ -139,6 +189,8 @@ class MaterialIconAction(ToolBarAction): filled (bool, optional): Whether the icon is filled. Defaults to False. color (str | tuple | QColor | dict[Literal["dark", "light"], str] | None, optional): The color of the icon. Defaults to None. + label_text (str | None, optional): Optional label text to display beside or under the icon. + text_position (Literal["beside", "under"] | None, optional): Position of text relative to icon. parent (QWidget or None, optional): Parent widget for the underlying QAction. """ @@ -149,12 +201,20 @@ class MaterialIconAction(ToolBarAction): checkable: bool = False, filled: bool = False, color: str | tuple | QColor | dict[Literal["dark", "light"], str] | None = None, + label_text: str | None = None, + text_position: Literal["beside", "under"] | None = None, parent=None, ): + """ + MaterialIconAction for toolbar: if label_text and text_position are provided, show text beside or under icon. + This enables per-action icon text without breaking the existing API. + """ super().__init__(icon_path=None, tooltip=tooltip, checkable=checkable) self.icon_name = icon_name self.filled = filled self.color = color + self.label_text = label_text + self.text_position = text_position # Generate the icon using the material_icon helper self.icon = material_icon( self.icon_name, @@ -178,7 +238,10 @@ class MaterialIconAction(ToolBarAction): toolbar(QToolBar): The toolbar to add the action to. target(QWidget): The target widget for the action. """ - toolbar.addAction(self.action) + if self.label_text is not None: + create_action_with_text(toolbar_action=self, toolbar=toolbar) + else: + toolbar.addAction(self.action) def get_icon(self): """ diff --git a/bec_widgets/utils/toolbars/toolbar.py b/bec_widgets/utils/toolbars/toolbar.py index c1b7b7f2..4b10fba8 100644 --- a/bec_widgets/utils/toolbars/toolbar.py +++ b/bec_widgets/utils/toolbars/toolbar.py @@ -6,7 +6,7 @@ from collections import defaultdict from typing import DefaultDict, Literal from bec_lib.logger import bec_logger -from qtpy.QtCore import QSize, Qt +from qtpy.QtCore import QSize, Qt, QTimer from qtpy.QtGui import QAction, QColor from qtpy.QtWidgets import QApplication, QLabel, QMainWindow, QMenu, QToolBar, QVBoxLayout, QWidget @@ -492,10 +492,33 @@ if __name__ == "__main__": # pragma: no cover self.toolbar.connect_bundle( "base", PerformanceConnection(self.toolbar.components, self) ) + self.toolbar.components.add_safe( + "text", + MaterialIconAction( + "text_fields", + tooltip="Test Text Action", + checkable=True, + label_text="text", + text_position="under", + ), + ) self.toolbar.show_bundles(["performance", "plot_export"]) self.toolbar.get_bundle("performance").add_action("save") + self.toolbar.get_bundle("performance").add_action("text") self.toolbar.refresh() + # Timer to disable and enable text button each 2s + self.timer = QTimer() + self.timer.timeout.connect(self.toggle_text_action) + self.timer.start(2000) + + def toggle_text_action(self): + text_action = self.toolbar.components.get_action("text") + if text_action.action.isEnabled(): + text_action.action.setEnabled(False) + else: + text_action.action.setEnabled(True) + def enable_fps_monitor(self, enabled: bool): """ Example method to enable or disable FPS monitoring.