mirror of
https://github.com/bec-project/bec_widgets.git
synced 2026-01-01 11:31:19 +01:00
feat(guided_tour): added option to register QActions from toolbar
This commit is contained in:
@@ -4,7 +4,7 @@ from __future__ import annotations
|
||||
|
||||
import sys
|
||||
import weakref
|
||||
from typing import Callable, Dict, List, Optional, TypedDict
|
||||
from typing import Callable, Dict, List, TypedDict
|
||||
from uuid import uuid4
|
||||
|
||||
import louie
|
||||
@@ -12,19 +12,24 @@ from bec_lib.logger import bec_logger
|
||||
from bec_qthemes import material_icon
|
||||
from louie import saferef
|
||||
from qtpy.QtCore import QEvent, QObject, QRect, Qt, Signal
|
||||
from qtpy.QtGui import QColor, QPainter, QPen
|
||||
from qtpy.QtGui import QAction, QColor, QPainter, QPen
|
||||
from qtpy.QtWidgets import (
|
||||
QApplication,
|
||||
QFrame,
|
||||
QHBoxLayout,
|
||||
QLabel,
|
||||
QMainWindow,
|
||||
QMenuBar,
|
||||
QPushButton,
|
||||
QToolBar,
|
||||
QVBoxLayout,
|
||||
QWidget,
|
||||
)
|
||||
|
||||
from bec_widgets.utils.error_popups import SafeSlot
|
||||
from bec_widgets.utils.toolbars.actions import ExpandableMenuAction, MaterialIconAction
|
||||
from bec_widgets.utils.toolbars.bundles import ToolbarBundle
|
||||
from bec_widgets.utils.toolbars.toolbar import ModularToolBar
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
@@ -34,8 +39,10 @@ class TourStep(TypedDict):
|
||||
|
||||
widget_ref: (
|
||||
louie.saferef.BoundMethodWeakref
|
||||
| weakref.ReferenceType[QWidget | Callable[[], tuple[QWidget, str | None]]]
|
||||
| Callable[[], tuple[QWidget, str | None]]
|
||||
| weakref.ReferenceType[
|
||||
QWidget | QAction | Callable[[], tuple[QWidget | QAction, str | None]]
|
||||
]
|
||||
| Callable[[], tuple[QWidget | QAction, str | None]]
|
||||
| None
|
||||
)
|
||||
text: str
|
||||
@@ -158,6 +165,13 @@ class TutorialOverlay(QWidget):
|
||||
"""
|
||||
rect must already be in the overlay's coordinate space (i.e. mapped).
|
||||
This method positions the message box so it does not overlap the rect.
|
||||
|
||||
Args:
|
||||
rect(QRect): rectangle to highlight
|
||||
title(str): Title text for the step
|
||||
text(str): Main content text for the step
|
||||
current_step(int): Current step number
|
||||
total_steps(int): Total number of steps in the tour
|
||||
"""
|
||||
self.current_rect = rect
|
||||
|
||||
@@ -226,8 +240,9 @@ class GuidedTour(QObject):
|
||||
tour_finished = Signal()
|
||||
step_changed = Signal(int, int) # current_step, total_steps
|
||||
|
||||
def __init__(self, main_window: QWidget):
|
||||
def __init__(self, main_window: QWidget, *, enforce_visibility: bool = True):
|
||||
super().__init__()
|
||||
self._visible_check: bool = enforce_visibility
|
||||
self.main_window_ref = saferef.safe_ref(main_window)
|
||||
self.overlay = None
|
||||
self._registered_widgets: Dict[str, TourStep] = {}
|
||||
@@ -236,7 +251,7 @@ class GuidedTour(QObject):
|
||||
self._active = False
|
||||
|
||||
@property
|
||||
def main_window(self) -> Optional[QWidget]:
|
||||
def main_window(self) -> QWidget | None:
|
||||
"""Get the main window from weak reference."""
|
||||
if self.main_window_ref and callable(self.main_window_ref):
|
||||
widget = self.main_window_ref()
|
||||
@@ -247,7 +262,7 @@ class GuidedTour(QObject):
|
||||
def register_widget(
|
||||
self,
|
||||
*,
|
||||
widget: QWidget | Callable[[], tuple[QWidget, str | None]],
|
||||
widget: QWidget | QAction | Callable[[], tuple[QWidget | QAction, str | None]],
|
||||
text: str = "",
|
||||
title: str = "",
|
||||
) -> str:
|
||||
@@ -255,32 +270,70 @@ class GuidedTour(QObject):
|
||||
Register a widget with help text for tours.
|
||||
|
||||
Args:
|
||||
widget (QWidget | Callable[[], tuple[QWidget, str | None]]): The target widget or a callable that returns the widget and its help text.
|
||||
widget (QWidget | QAction | Callable[[], tuple[QWidget | QAction, str | None]]): The target widget or a callable that returns the widget and its help text.
|
||||
text (str): The help text for the widget. This will be shown during the tour.
|
||||
title (str, optional): A title for the widget (defaults to its class name).
|
||||
title (str, optional): A title for the widget (defaults to its class name or action text).
|
||||
|
||||
Returns:
|
||||
str: The unique ID for the registered widget.
|
||||
"""
|
||||
step_id = str(uuid4())
|
||||
|
||||
# Check if it is a bound method
|
||||
# If it's a plain callable
|
||||
if callable(widget) and not hasattr(widget, "__self__"):
|
||||
# We are dealing with a plain callable
|
||||
widget_ref = widget
|
||||
default_title = "Widget"
|
||||
elif isinstance(widget, QAction):
|
||||
widget_ref = weakref.ref(widget)
|
||||
default_title = widget.text() or "Action"
|
||||
elif hasattr(widget, "get_toolbar_button") and callable(widget.get_toolbar_button):
|
||||
|
||||
def _resolve_toolbar_button(toolbar_action=widget):
|
||||
button = toolbar_action.get_toolbar_button()
|
||||
return (button, None)
|
||||
|
||||
widget_ref = _resolve_toolbar_button
|
||||
default_title = getattr(widget, "tooltip", "Toolbar Menu")
|
||||
else:
|
||||
# Create weak reference for QWidget instances
|
||||
widget_ref = saferef.safe_ref(widget)
|
||||
default_title = widget.__class__.__name__ if hasattr(widget, "__class__") else "Widget"
|
||||
|
||||
self._registered_widgets[step_id] = {
|
||||
"widget_ref": widget_ref,
|
||||
"text": text,
|
||||
"title": title
|
||||
or (widget.__class__.__name__ if hasattr(widget, "__class__") else "Widget"),
|
||||
"title": title or default_title,
|
||||
}
|
||||
logger.debug(f"Registered widget {title} with ID {step_id}")
|
||||
logger.debug(f"Registered widget {title or default_title} with ID {step_id}")
|
||||
return step_id
|
||||
|
||||
def _action_highlight_rect(self, action: QAction) -> QRect | None:
|
||||
"""
|
||||
Try to find the QRect in main_window coordinates that should be highlighted for the given QAction.
|
||||
Returns None if not found (e.g. not visible).
|
||||
"""
|
||||
mw = self.main_window
|
||||
if mw is None:
|
||||
return None
|
||||
# Try toolbars first
|
||||
for tb in mw.findChildren(QToolBar):
|
||||
btn = tb.widgetForAction(action)
|
||||
if btn and btn.isVisible():
|
||||
rect = btn.rect()
|
||||
top_left = btn.mapTo(mw, rect.topLeft())
|
||||
return QRect(top_left, rect.size())
|
||||
# Try menu bars
|
||||
menubars = []
|
||||
if hasattr(mw, "menuBar") and callable(getattr(mw, "menuBar", None)):
|
||||
mb = mw.menuBar()
|
||||
if mb and mb not in menubars:
|
||||
menubars.append(mb)
|
||||
menubars += [mb for mb in mw.findChildren(QMenuBar) if mb not in menubars]
|
||||
for mb in menubars:
|
||||
if action in mb.actions():
|
||||
ar = mb.actionGeometry(action)
|
||||
top_left = mb.mapTo(mw, ar.topLeft())
|
||||
return QRect(top_left, ar.size())
|
||||
return None
|
||||
|
||||
def unregister_widget(self, step_id: str) -> bool:
|
||||
"""
|
||||
Unregister a previously registered widget.
|
||||
@@ -306,6 +359,9 @@ class GuidedTour(QObject):
|
||||
|
||||
Args:
|
||||
step_ids (List[str], optional): List of registered widget IDs to include in the tour. If None, all registered widgets will be included.
|
||||
|
||||
Returns:
|
||||
bool: True if the tour was created successfully, False if any step IDs were not found
|
||||
"""
|
||||
if step_ids is None:
|
||||
step_ids = list(self._registered_widgets.keys())
|
||||
@@ -404,57 +460,28 @@ class GuidedTour(QObject):
|
||||
return
|
||||
|
||||
step = self._tour_steps[self._current_index]
|
||||
widget_ref = step.get("widget_ref")
|
||||
|
||||
step_title = step["title"]
|
||||
widget = None
|
||||
step_text = step["text"]
|
||||
|
||||
# Resolve weak reference
|
||||
if isinstance(widget_ref, (louie.saferef.BoundMethodWeakref, weakref.ReferenceType)):
|
||||
widget = widget_ref()
|
||||
else:
|
||||
widget = widget_ref
|
||||
|
||||
# If the widget does not exist, log warning and skip to next step or stop tour
|
||||
if not widget:
|
||||
logger.warning(
|
||||
f"Widget for step {step['title']} no longer exists (weak reference is dead)"
|
||||
)
|
||||
# Skip to next step or stop tour if this was the last step
|
||||
if self._current_index < len(self._tour_steps) - 1:
|
||||
self._current_index += 1
|
||||
self._show_current_step()
|
||||
else:
|
||||
self.stop_tour()
|
||||
target, step_text = self._resolve_step_target(step)
|
||||
if target is None:
|
||||
self._advance_past_invalid_step(step_title, reason="Step target no longer exists.")
|
||||
return
|
||||
|
||||
# If the user provided a callable, resolve it to get the widget and optional alt text
|
||||
if callable(widget):
|
||||
widget, alt_text = widget()
|
||||
if alt_text:
|
||||
step_text = alt_text
|
||||
|
||||
if not widget.isVisible():
|
||||
logger.warning(f"Widget for step {step['title']} is not visible")
|
||||
return
|
||||
|
||||
# Map widget coordinates to overlay coordinates
|
||||
main_window = self.main_window
|
||||
if main_window is None:
|
||||
logger.error("Main window no longer exists (weak reference is dead)")
|
||||
self.stop_tour()
|
||||
return
|
||||
|
||||
rect = widget.rect()
|
||||
top_left = widget.mapTo(main_window, rect.topLeft())
|
||||
global_rect = QRect(top_left, rect.size())
|
||||
highlight_rect = self._get_highlight_rect(main_window, target, step_title)
|
||||
if highlight_rect is None:
|
||||
return
|
||||
|
||||
# Calculate step numbers
|
||||
current_step = self._current_index + 1
|
||||
total_steps = len(self._tour_steps)
|
||||
|
||||
self.overlay.show_step(global_rect, step_title, step_text, current_step, total_steps)
|
||||
self.overlay.show_step(highlight_rect, step_title, step_text, current_step, total_steps)
|
||||
|
||||
# Update button states
|
||||
self.overlay.back_btn.setEnabled(self._current_index > 0)
|
||||
@@ -472,6 +499,89 @@ class GuidedTour(QObject):
|
||||
|
||||
self.step_changed.emit(self._current_index + 1, len(self._tour_steps))
|
||||
|
||||
def _resolve_step_target(self, step: TourStep) -> tuple[QWidget | QAction | None, str]:
|
||||
"""
|
||||
Resolve the target widget/action for the given step.
|
||||
|
||||
Args:
|
||||
step(TourStep): The tour step to resolve.
|
||||
|
||||
Returns:
|
||||
tuple[QWidget | QAction | None, str]: The resolved target and the step text.
|
||||
"""
|
||||
widget_ref = step.get("widget_ref")
|
||||
step_text = step.get("text", "")
|
||||
|
||||
if isinstance(widget_ref, (louie.saferef.BoundMethodWeakref, weakref.ReferenceType)):
|
||||
target = widget_ref()
|
||||
else:
|
||||
target = widget_ref
|
||||
|
||||
if target is None:
|
||||
return None, step_text
|
||||
|
||||
if callable(target) and not isinstance(target, (QWidget, QAction)):
|
||||
result = target()
|
||||
if isinstance(result, tuple):
|
||||
target, alt_text = result
|
||||
if alt_text:
|
||||
step_text = alt_text
|
||||
else:
|
||||
target = result
|
||||
|
||||
return target, step_text
|
||||
|
||||
def _get_highlight_rect(
|
||||
self, main_window: QWidget, target: QWidget | QAction, step_title: str
|
||||
) -> QRect | None:
|
||||
"""
|
||||
Get the QRect in main_window coordinates to highlight for the given target.
|
||||
|
||||
Args:
|
||||
main_window(QWidget): The main window containing the target.
|
||||
target(QWidget | QAction): The target widget or action to highlight.
|
||||
step_title(str): The title of the current step (for logging purposes).
|
||||
|
||||
Returns:
|
||||
QRect | None: The rectangle to highlight, or None if not found/visible.
|
||||
"""
|
||||
if isinstance(target, QAction):
|
||||
rect = self._action_highlight_rect(target)
|
||||
if rect is None:
|
||||
self._advance_past_invalid_step(
|
||||
step_title,
|
||||
reason=f"Could not find visible widget or menu for QAction {target.text()!r}.",
|
||||
)
|
||||
return None
|
||||
return rect
|
||||
|
||||
if isinstance(target, QWidget):
|
||||
if self._visible_check:
|
||||
if not target.isVisible():
|
||||
self._advance_past_invalid_step(
|
||||
step_title, reason=f"Widget {target!r} is not visible."
|
||||
)
|
||||
return None
|
||||
rect = target.rect()
|
||||
top_left = target.mapTo(main_window, rect.topLeft())
|
||||
return QRect(top_left, rect.size())
|
||||
|
||||
self._advance_past_invalid_step(
|
||||
step_title, reason=f"Unsupported step target type: {type(target)}"
|
||||
)
|
||||
return None
|
||||
|
||||
def _advance_past_invalid_step(self, step_title: str, *, reason: str):
|
||||
"""
|
||||
Skip the current step (or stop the tour) when the target cannot be visualised.
|
||||
"""
|
||||
logger.warning("%s Skipping step %r.", reason, step_title)
|
||||
if self._current_index < len(self._tour_steps) - 1:
|
||||
self._current_index += 1
|
||||
self._show_current_step()
|
||||
else:
|
||||
self.stop_tour()
|
||||
|
||||
def get_registered_widgets(self) -> Dict[str, TourStep]:
|
||||
"""Get all registered widgets."""
|
||||
return self._registered_widgets.copy()
|
||||
@@ -484,6 +594,10 @@ class GuidedTour(QObject):
|
||||
self._tour_steps.clear()
|
||||
logger.info("Cleared all registrations")
|
||||
|
||||
def set_visibility_enforcement(self, enabled: bool):
|
||||
"""Enable or disable visibility checks when highlighting widgets."""
|
||||
self._visible_check = enabled
|
||||
|
||||
def eventFilter(self, obj, event):
|
||||
"""Handle window resize/move events to update step positioning."""
|
||||
if event.type() in (QEvent.Type.Move, QEvent.Type.Resize):
|
||||
@@ -500,52 +614,115 @@ class GuidedTour(QObject):
|
||||
class MainWindow(QMainWindow): # pragma: no cover
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.setWindowTitle("Guided Help Demo")
|
||||
self.setWindowTitle("Guided Tour Demo")
|
||||
central = QWidget()
|
||||
layout = QVBoxLayout(central)
|
||||
layout.setSpacing(12)
|
||||
|
||||
# Create demo widgets
|
||||
self.btn1 = QPushButton("Button 1")
|
||||
self.btn2 = QPushButton("Button 2")
|
||||
layout.addWidget(QLabel("Welcome to the guided tour demo with toolbar support."))
|
||||
self.btn1 = QPushButton("Primary Button")
|
||||
self.btn2 = QPushButton("Secondary Button")
|
||||
self.status_label = QLabel("Use the controls below or the toolbar to interact.")
|
||||
self.start_tour_btn = QPushButton("Start Guided Tour")
|
||||
|
||||
layout.addWidget(QLabel("Welcome to the Guided Help Demo!"))
|
||||
layout.addWidget(self.btn1)
|
||||
layout.addWidget(self.btn2)
|
||||
layout.addWidget(self.start_tour_btn)
|
||||
layout.addWidget(self.status_label)
|
||||
layout.addStretch()
|
||||
layout.addWidget(self.start_tour_btn)
|
||||
self.setCentralWidget(central)
|
||||
|
||||
# Create guided help system
|
||||
# Guided tour system
|
||||
self.guided_help = GuidedTour(self)
|
||||
|
||||
# Register widgets with help text
|
||||
self.guided_help.register_widget(
|
||||
# Menus for demonstrating QAction support in menu bars
|
||||
self._init_menu_bar()
|
||||
|
||||
# Modular toolbar showcasing QAction targets
|
||||
self._init_toolbar()
|
||||
|
||||
# Register widgets and actions with help text
|
||||
primary_step = self.guided_help.register_widget(
|
||||
widget=self.btn1,
|
||||
text="This is the first button. It demonstrates how to highlight a widget and show help text next to it.",
|
||||
title="First Button",
|
||||
text="The primary button updates the status text when clicked.",
|
||||
title="Primary Button",
|
||||
)
|
||||
|
||||
def widget_with_alt_text():
|
||||
import numpy as np
|
||||
|
||||
if np.random.randint(0, 10) % 2 == 0:
|
||||
return (self.btn2, None)
|
||||
return (self.btn2, "This is an alternative help text for Button 2.")
|
||||
|
||||
self.guided_help.register_widget(
|
||||
widget=widget_with_alt_text,
|
||||
text="This is the second button. Notice how the help tooltip is positioned smartly to stay visible.",
|
||||
title="Second Button",
|
||||
secondary_step = self.guided_help.register_widget(
|
||||
widget=self.btn2,
|
||||
text="The secondary button complements the demo layout.",
|
||||
title="Secondary Button",
|
||||
)
|
||||
toolbar_action_step = self.guided_help.register_widget(
|
||||
widget=self.toolbar_tour_action.action,
|
||||
text="Toolbar actions are supported in the guided tour. This one also starts the tour.",
|
||||
title="Toolbar Tour Action",
|
||||
)
|
||||
tools_menu_step = self.guided_help.register_widget(
|
||||
widget=self.toolbar.components.get_action("menu_tools"),
|
||||
text="Expandable toolbar menus group related actions. This button opens the tools menu.",
|
||||
title="Tools Menu",
|
||||
)
|
||||
|
||||
# Create tour from registered widgets
|
||||
widget_ids = list(self.guided_help.get_registered_widgets().keys())
|
||||
self.tour_step_ids = [primary_step, secondary_step, toolbar_action_step, tools_menu_step]
|
||||
widget_ids = self.tour_step_ids
|
||||
self.guided_help.create_tour(widget_ids)
|
||||
|
||||
# Connect start button
|
||||
self.start_tour_btn.clicked.connect(self.guided_help.start_tour)
|
||||
|
||||
def _init_menu_bar(self):
|
||||
menu_bar = self.menuBar()
|
||||
info_menu = menu_bar.addMenu("Info")
|
||||
info_menu.setObjectName("info-menu")
|
||||
self.info_menu = info_menu
|
||||
self.info_menu_action = info_menu.menuAction()
|
||||
self.about_action = info_menu.addAction("About This Demo")
|
||||
|
||||
def _init_toolbar(self):
|
||||
self.toolbar = ModularToolBar(parent=self)
|
||||
self.addToolBar(self.toolbar)
|
||||
|
||||
self.toolbar_tour_action = MaterialIconAction(
|
||||
"play_circle", tooltip="Start the guided tour", parent=self
|
||||
)
|
||||
self.toolbar.components.add_safe("tour-start", self.toolbar_tour_action)
|
||||
|
||||
self.toolbar_highlight_action = MaterialIconAction(
|
||||
"visibility", tooltip="Highlight the primary button", parent=self
|
||||
)
|
||||
self.toolbar.components.add_safe("inspect-primary", self.toolbar_highlight_action)
|
||||
|
||||
demo_bundle = self.toolbar.new_bundle("demo")
|
||||
demo_bundle.add_action("tour-start")
|
||||
demo_bundle.add_action("inspect-primary")
|
||||
|
||||
self._setup_tools_menu()
|
||||
self.toolbar.show_bundles(["demo", "menu_tools"])
|
||||
self.toolbar.refresh()
|
||||
|
||||
self.toolbar_tour_action.action.triggered.connect(self.guided_help.start_tour)
|
||||
|
||||
def _setup_tools_menu(self):
|
||||
self.tools_menu_actions: dict[str, MaterialIconAction] = {
|
||||
"notes": MaterialIconAction(
|
||||
icon_name="note_add", tooltip="Add a note", filled=True, parent=self
|
||||
),
|
||||
"bookmark": MaterialIconAction(
|
||||
icon_name="bookmark_add", tooltip="Bookmark current view", filled=True, parent=self
|
||||
),
|
||||
"settings": MaterialIconAction(
|
||||
icon_name="tune", tooltip="Adjust settings", filled=True, parent=self
|
||||
),
|
||||
}
|
||||
self.tools_menu_action = ExpandableMenuAction(
|
||||
label="Tools ", actions=self.tools_menu_actions
|
||||
)
|
||||
self.toolbar.components.add_safe("menu_tools", self.tools_menu_action)
|
||||
bundle = ToolbarBundle("menu_tools", self.toolbar.components)
|
||||
bundle.add_action("menu_tools")
|
||||
self.toolbar.add_bundle(bundle)
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
app = QApplication(sys.argv)
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import weakref
|
||||
from abc import ABC, abstractmethod
|
||||
from contextlib import contextmanager
|
||||
from typing import Dict, Literal
|
||||
@@ -25,7 +26,6 @@ from qtpy.QtWidgets import (
|
||||
)
|
||||
|
||||
import bec_widgets
|
||||
from bec_widgets.utils.guided_tour import GuidedTour
|
||||
from bec_widgets.widgets.control.device_input.base_classes.device_input_base import BECDeviceFilter
|
||||
from bec_widgets.widgets.control.device_input.device_combobox.device_combobox import DeviceComboBox
|
||||
|
||||
@@ -507,6 +507,8 @@ class ExpandableMenuAction(ToolBarAction):
|
||||
def __init__(self, label: str, actions: dict, icon_path: str = None):
|
||||
super().__init__(icon_path, label)
|
||||
self.actions = actions
|
||||
self._button_ref: weakref.ReferenceType[QToolButton] | None = None
|
||||
self._menu_ref: weakref.ReferenceType[QMenu] | None = None
|
||||
|
||||
def add_to_toolbar(self, toolbar: QToolBar, target: QWidget):
|
||||
button = QToolButton(toolbar)
|
||||
@@ -542,6 +544,14 @@ class ExpandableMenuAction(ToolBarAction):
|
||||
menu.addAction(action)
|
||||
button.setMenu(menu)
|
||||
toolbar.addWidget(button)
|
||||
self._button_ref = weakref.ref(button)
|
||||
self._menu_ref = weakref.ref(menu)
|
||||
|
||||
def get_toolbar_button(self) -> QToolButton | None:
|
||||
return self._button_ref() if self._button_ref else None
|
||||
|
||||
def get_menu(self) -> QMenu | None:
|
||||
return self._menu_ref() if self._menu_ref else None
|
||||
|
||||
|
||||
class DeviceComboBoxAction(WidgetAction):
|
||||
@@ -613,6 +623,8 @@ class TutorialAction(MaterialIconAction):
|
||||
parent=parent,
|
||||
)
|
||||
|
||||
from bec_widgets.utils.guided_tour import GuidedTour
|
||||
|
||||
self.guided_help = GuidedTour(main_window)
|
||||
self.main_window = main_window
|
||||
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
from qtpy.QtWidgets import QWidget
|
||||
from qtpy.QtWidgets import QVBoxLayout, QWidget
|
||||
|
||||
from bec_widgets.utils.guided_tour import GuidedTour
|
||||
from bec_widgets.utils.toolbars.actions import ExpandableMenuAction, MaterialIconAction
|
||||
from bec_widgets.utils.toolbars.toolbar import ModularToolBar
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -18,7 +20,7 @@ def main_window(qtbot):
|
||||
@pytest.fixture
|
||||
def guided_help(main_window):
|
||||
"""Create a GuidedTour instance for testing."""
|
||||
return GuidedTour(main_window)
|
||||
return GuidedTour(main_window, enforce_visibility=False)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -244,6 +246,33 @@ class TestGuidedTour:
|
||||
assert overlay is not None
|
||||
assert overlay.step_label.text() == "Step 1 of 2"
|
||||
|
||||
def test_register_expandable_menu_action(self, qtbot):
|
||||
"""Ensure toolbar menu actions can be registered directly."""
|
||||
window = QWidget()
|
||||
layout = QVBoxLayout(window)
|
||||
toolbar = ModularToolBar(parent=window)
|
||||
layout.addWidget(toolbar)
|
||||
qtbot.addWidget(window)
|
||||
|
||||
tools_action = ExpandableMenuAction(
|
||||
label="Tools ",
|
||||
actions={
|
||||
"notes": MaterialIconAction(
|
||||
icon_name="note_add", tooltip="Add note", filled=True, parent=window
|
||||
)
|
||||
},
|
||||
)
|
||||
toolbar.components.add_safe("menu_tools", tools_action)
|
||||
bundle = toolbar.new_bundle("menu_tools")
|
||||
bundle.add_action("menu_tools")
|
||||
toolbar.show_bundles(["menu_tools"])
|
||||
|
||||
guided = GuidedTour(window, enforce_visibility=False)
|
||||
guided.register_widget(widget=tools_action, text="Toolbar tools menu")
|
||||
guided.start_tour()
|
||||
|
||||
assert guided._active is True
|
||||
|
||||
@mock.patch("bec_widgets.utils.guided_tour.logger")
|
||||
def test_error_handling(self, mock_logger, guided_help):
|
||||
"""Test error handling and logging."""
|
||||
|
||||
Reference in New Issue
Block a user