mirror of
https://github.com/bec-project/bec_widgets.git
synced 2026-03-04 16:02:51 +01:00
feat: guided tour for main app
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
from bec_qthemes import material_icon
|
||||
from qtpy.QtGui import QAction # type: ignore
|
||||
from qtpy.QtWidgets import QApplication, QHBoxLayout, QStackedWidget, QWidget
|
||||
|
||||
from bec_widgets.applications.navigation_centre.reveal_animator import ANIMATION_DURATION
|
||||
@@ -8,6 +10,7 @@ from bec_widgets.applications.views.device_manager_view.device_manager_view impo
|
||||
from bec_widgets.applications.views.dock_area_view.dock_area_view import DockAreaView
|
||||
from bec_widgets.applications.views.view import ViewBase, WaveformViewInline, WaveformViewPopup
|
||||
from bec_widgets.utils.colors import apply_theme
|
||||
from bec_widgets.utils.guided_tour import GuidedTour
|
||||
from bec_widgets.utils.screen_utils import (
|
||||
apply_centered_size,
|
||||
available_screen_geometry,
|
||||
@@ -50,6 +53,10 @@ class BECMainApp(BECMainWindow):
|
||||
|
||||
self._add_views()
|
||||
|
||||
# Initialize guided tour
|
||||
self.guided_tour = GuidedTour(self)
|
||||
self._setup_guided_tour()
|
||||
|
||||
def _add_views(self):
|
||||
self.add_section("BEC Applications", "bec_apps")
|
||||
self.dock_area = DockAreaView(self)
|
||||
@@ -105,6 +112,9 @@ class BECMainApp(BECMainWindow):
|
||||
self.set_current("dock_area")
|
||||
self.sidebar.add_dark_mode_item()
|
||||
|
||||
# Add guided tour to Help menu
|
||||
self._add_guided_tour_to_menu()
|
||||
|
||||
# --- Public API ------------------------------------------------------
|
||||
def add_section(self, title: str, id: str, position: int | None = None):
|
||||
return self.sidebar.add_section(title, id, position)
|
||||
@@ -200,6 +210,125 @@ class BECMainApp(BECMainWindow):
|
||||
if hasattr(new_view, "on_enter"):
|
||||
new_view.on_enter()
|
||||
|
||||
def _setup_guided_tour(self):
|
||||
"""
|
||||
Setup the guided tour for the main application.
|
||||
Registers key UI components and delegates to views for their internal components.
|
||||
"""
|
||||
tour_steps = []
|
||||
|
||||
# --- General Layout Components ---
|
||||
|
||||
# Register the sidebar toggle button
|
||||
toggle_step = self.guided_tour.register_widget(
|
||||
widget=self.sidebar.toggle,
|
||||
title="Sidebar Toggle",
|
||||
text="Click this button to expand or collapse the sidebar. When expanded, you can see full navigation item titles and section names.",
|
||||
)
|
||||
tour_steps.append(toggle_step)
|
||||
|
||||
# Register the dark mode toggle
|
||||
dark_mode_item = self.sidebar.components.get("dark_mode")
|
||||
if dark_mode_item:
|
||||
dark_mode_step = self.guided_tour.register_widget(
|
||||
widget=dark_mode_item,
|
||||
title="Theme Toggle",
|
||||
text="Switch between light and dark themes. The theme preference is saved and will be applied when you restart the application.",
|
||||
)
|
||||
tour_steps.append(dark_mode_step)
|
||||
|
||||
# Register the client info label
|
||||
if hasattr(self, "_client_info_hover"):
|
||||
client_info_step = self.guided_tour.register_widget(
|
||||
widget=self._client_info_hover,
|
||||
title="Client Status",
|
||||
text="Displays status messages and information from the BEC Server.",
|
||||
)
|
||||
tour_steps.append(client_info_step)
|
||||
|
||||
# Register the scan progress bar if available
|
||||
if hasattr(self, "_scan_progress_hover"):
|
||||
progress_step = self.guided_tour.register_widget(
|
||||
widget=self._scan_progress_hover,
|
||||
title="Scan Progress",
|
||||
text="Monitor the progress of ongoing scans. Hover over the progress bar to see detailed information including elapsed time and estimated completion.",
|
||||
)
|
||||
tour_steps.append(progress_step)
|
||||
|
||||
# Register the notification indicator in the status bar
|
||||
if hasattr(self, "notification_indicator"):
|
||||
notif_step = self.guided_tour.register_widget(
|
||||
widget=self.notification_indicator,
|
||||
title="Notification Center",
|
||||
text="View system notifications, errors, and status updates. Click to filter notifications by type or expand to see all details.",
|
||||
)
|
||||
tour_steps.append(notif_step)
|
||||
|
||||
# --- View-Specific Components ---
|
||||
|
||||
# Register all views that can extend the tour
|
||||
for view_id, view_index in self._view_index.items():
|
||||
view_widget = self.stack.widget(view_index)
|
||||
if not view_widget or not hasattr(view_widget, "register_tour_steps"):
|
||||
continue
|
||||
|
||||
# Get the view's tour steps
|
||||
view_tour = view_widget.register_tour_steps(self.guided_tour, self)
|
||||
if view_tour is None:
|
||||
if hasattr(view_widget.content, "register_tour_steps"):
|
||||
view_tour = view_widget.content.register_tour_steps(self.guided_tour, self)
|
||||
if view_tour is None:
|
||||
continue
|
||||
|
||||
# Get the corresponding sidebar navigation item
|
||||
nav_item = self.sidebar.components.get(view_id)
|
||||
if not nav_item:
|
||||
continue
|
||||
|
||||
# Use the view's title for the navigation button
|
||||
nav_step = self.guided_tour.register_widget(
|
||||
widget=nav_item,
|
||||
title=view_tour.view_title,
|
||||
text=f"Navigate to the {view_tour.view_title} to access its features and functionality.",
|
||||
)
|
||||
tour_steps.append(nav_step)
|
||||
tour_steps.extend(view_tour.step_ids)
|
||||
|
||||
# Create the tour with all registered steps
|
||||
if tour_steps:
|
||||
self.guided_tour.create_tour(tour_steps)
|
||||
|
||||
def start_guided_tour(self):
|
||||
"""
|
||||
Public method to start the guided tour.
|
||||
This can be called programmatically or connected to a menu/button action.
|
||||
"""
|
||||
self.guided_tour.start_tour()
|
||||
|
||||
def _add_guided_tour_to_menu(self):
|
||||
"""
|
||||
Add a 'Guided Tour' action to the Help menu.
|
||||
"""
|
||||
|
||||
# Find the Help menu
|
||||
menu_bar = self.menuBar()
|
||||
help_menu = None
|
||||
for action in menu_bar.actions():
|
||||
if action.text() == "Help":
|
||||
help_menu = action.menu()
|
||||
break
|
||||
|
||||
if help_menu:
|
||||
# Add separator before the tour action
|
||||
help_menu.addSeparator()
|
||||
|
||||
# Create and add the guided tour action
|
||||
tour_action = QAction("Start Guided Tour", self)
|
||||
tour_action.setIcon(material_icon("help"))
|
||||
tour_action.triggered.connect(self.start_guided_tour)
|
||||
tour_action.setShortcut("F1") # Add keyboard shortcut
|
||||
help_menu.addAction(tour_action)
|
||||
|
||||
|
||||
def main(): # pragma: no cover
|
||||
"""
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from qtpy.QtWidgets import QWidget
|
||||
|
||||
from bec_widgets.applications.views.developer_view.developer_widget import DeveloperWidget
|
||||
from bec_widgets.applications.views.view import ViewBase
|
||||
from bec_widgets.applications.views.view import ViewBase, ViewTourSteps
|
||||
|
||||
|
||||
class DeveloperView(ViewBase):
|
||||
@@ -21,6 +21,81 @@ class DeveloperView(ViewBase):
|
||||
self.developer_widget = DeveloperWidget(parent=self)
|
||||
self.set_content(self.developer_widget)
|
||||
|
||||
def register_tour_steps(self, guided_tour, main_app) -> ViewTourSteps | None:
|
||||
"""Register Developer View components with the guided tour.
|
||||
|
||||
Args:
|
||||
guided_tour: The GuidedTour instance to register with.
|
||||
main_app: The main application instance (for accessing set_current).
|
||||
|
||||
Returns:
|
||||
ViewTourSteps | None: Model containing view title and step IDs.
|
||||
"""
|
||||
step_ids = []
|
||||
dev_widget = self.developer_widget
|
||||
|
||||
# IDE Toolbar
|
||||
def get_ide_toolbar():
|
||||
main_app.set_current("developer_view")
|
||||
return (dev_widget.toolbar, None)
|
||||
|
||||
step_id = guided_tour.register_widget(
|
||||
widget=get_ide_toolbar,
|
||||
title="IDE Toolbar",
|
||||
text="Quick access to save files, execute scripts, and configure IDE settings. Use the toolbar to manage your code and execution.",
|
||||
)
|
||||
step_ids.append(step_id)
|
||||
|
||||
# IDE Explorer
|
||||
def get_ide_explorer():
|
||||
main_app.set_current("developer_view")
|
||||
return (dev_widget.explorer_dock.widget(), None)
|
||||
|
||||
step_id = guided_tour.register_widget(
|
||||
widget=get_ide_explorer,
|
||||
title="File Explorer",
|
||||
text="Browse and manage your macro files. Create new files, open existing ones, and organize your scripts.",
|
||||
)
|
||||
step_ids.append(step_id)
|
||||
|
||||
# IDE Editor
|
||||
def get_ide_editor():
|
||||
main_app.set_current("developer_view")
|
||||
return (dev_widget.monaco_dock.widget(), None)
|
||||
|
||||
step_id = guided_tour.register_widget(
|
||||
widget=get_ide_editor,
|
||||
title="Code Editor",
|
||||
text="Write and edit Python code with syntax highlighting, auto-completion, and signature help. Monaco editor provides a modern coding experience.",
|
||||
)
|
||||
step_ids.append(step_id)
|
||||
|
||||
# IDE Console
|
||||
def get_ide_console():
|
||||
main_app.set_current("developer_view")
|
||||
return (dev_widget.console_dock.widget(), None)
|
||||
|
||||
step_id = guided_tour.register_widget(
|
||||
widget=get_ide_console,
|
||||
title="BEC Shell Console",
|
||||
text="Interactive Python console with BEC integration. Execute commands, test code snippets, and interact with the BEC system in real-time.",
|
||||
)
|
||||
step_ids.append(step_id)
|
||||
|
||||
# IDE Plotting Area
|
||||
def get_ide_plotting():
|
||||
main_app.set_current("developer_view")
|
||||
return (dev_widget.plotting_ads, None)
|
||||
|
||||
step_id = guided_tour.register_widget(
|
||||
widget=get_ide_plotting,
|
||||
title="Plotting Area",
|
||||
text="View plots and visualizations generated by your scripts. Arrange multiple plots in a flexible layout.",
|
||||
)
|
||||
step_ids.append(step_id)
|
||||
|
||||
return ViewTourSteps(view_title="Integrated Development Environment", step_ids=step_ids)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
@@ -5,7 +5,7 @@ from qtpy.QtWidgets import QWidget
|
||||
from bec_widgets.applications.views.device_manager_view.device_manager_widget import (
|
||||
DeviceManagerWidget,
|
||||
)
|
||||
from bec_widgets.applications.views.view import ViewBase
|
||||
from bec_widgets.applications.views.view import ViewBase, ViewTourSteps
|
||||
from bec_widgets.utils.error_popups import SafeSlot
|
||||
|
||||
|
||||
@@ -34,6 +34,45 @@ class DeviceManagerView(ViewBase):
|
||||
"""
|
||||
self.device_manager_widget.on_enter()
|
||||
|
||||
def register_tour_steps(self, guided_tour, main_app) -> ViewTourSteps | None:
|
||||
"""Register Device Manager components with the guided tour.
|
||||
|
||||
Args:
|
||||
guided_tour: The GuidedTour instance to register with.
|
||||
main_app: The main application instance (for accessing set_current).
|
||||
|
||||
Returns:
|
||||
ViewTourSteps | None: Model containing view title and step IDs.
|
||||
"""
|
||||
step_ids = []
|
||||
dm_widget = self.device_manager_widget
|
||||
|
||||
# Register Load Current Config button
|
||||
def get_load_current():
|
||||
main_app.set_current("device_manager")
|
||||
return (dm_widget.button_load_current_config, None)
|
||||
|
||||
step_id = guided_tour.register_widget(
|
||||
widget=get_load_current,
|
||||
title="Load Current Config",
|
||||
text="Load the current device configuration from the BEC server. This will display all available devices and their current status.",
|
||||
)
|
||||
step_ids.append(step_id)
|
||||
|
||||
# Register Load Config From File button
|
||||
def get_load_file():
|
||||
main_app.set_current("device_manager")
|
||||
return (dm_widget.button_load_config_from_file, None)
|
||||
|
||||
step_id = guided_tour.register_widget(
|
||||
widget=get_load_file,
|
||||
title="Load Config From File",
|
||||
text="Load a device configuration from a YAML file on disk. Useful for testing or working offline.",
|
||||
)
|
||||
step_ids.append(step_id)
|
||||
|
||||
return ViewTourSteps(view_title="Device Manager", step_ids=step_ids)
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
import sys
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from qtpy.QtCore import QEventLoop
|
||||
from typing import List
|
||||
|
||||
from pydantic import BaseModel
|
||||
from qtpy.QtCore import QEventLoop, Qt, QTimer
|
||||
from qtpy.QtWidgets import (
|
||||
QDialog,
|
||||
QDialogButtonBox,
|
||||
@@ -20,6 +23,54 @@ from bec_widgets.widgets.control.device_input.signal_combobox.signal_combobox im
|
||||
from bec_widgets.widgets.plots.waveform.waveform import Waveform
|
||||
|
||||
|
||||
def set_splitter_weights(splitter: QSplitter, weights: List[float]) -> None:
|
||||
"""
|
||||
Apply initial sizes to a splitter using weight ratios, e.g. [1,3,2,1].
|
||||
Works for horizontal or vertical splitters and sets matching stretch factors.
|
||||
"""
|
||||
|
||||
def apply():
|
||||
n = splitter.count()
|
||||
if n == 0:
|
||||
return
|
||||
w = list(weights[:n]) + [1] * max(0, n - len(weights))
|
||||
w = [max(0.0, float(x)) for x in w]
|
||||
tot_w = sum(w)
|
||||
if tot_w <= 0:
|
||||
w = [1.0] * n
|
||||
tot_w = float(n)
|
||||
total_px = (
|
||||
splitter.width()
|
||||
if splitter.orientation() == Qt.Orientation.Horizontal
|
||||
else splitter.height()
|
||||
)
|
||||
if total_px < 2:
|
||||
QTimer.singleShot(0, apply)
|
||||
return
|
||||
sizes = [max(1, int(total_px * (wi / tot_w))) for wi in w]
|
||||
diff = total_px - sum(sizes)
|
||||
if diff != 0:
|
||||
idx = max(range(n), key=lambda i: w[i])
|
||||
sizes[idx] = max(1, sizes[idx] + diff)
|
||||
splitter.setSizes(sizes)
|
||||
for i, wi in enumerate(w):
|
||||
splitter.setStretchFactor(i, max(1, int(round(wi * 100))))
|
||||
|
||||
QTimer.singleShot(0, apply)
|
||||
|
||||
|
||||
class ViewTourSteps(BaseModel):
|
||||
"""Model representing tour steps for a view.
|
||||
|
||||
Attributes:
|
||||
view_title: The human-readable title of the view.
|
||||
step_ids: List of registered step IDs in the order they should appear.
|
||||
"""
|
||||
|
||||
view_title: str
|
||||
step_ids: list[str]
|
||||
|
||||
|
||||
class ViewBase(QWidget):
|
||||
"""Wrapper for a content widget used inside the main app's stacked view.
|
||||
|
||||
@@ -76,6 +127,83 @@ class ViewBase(QWidget):
|
||||
"""
|
||||
return True
|
||||
|
||||
def register_tour_steps(self, guided_tour, main_app) -> ViewTourSteps | None:
|
||||
"""Register this view's components with the guided tour.
|
||||
|
||||
Args:
|
||||
guided_tour: The GuidedTour instance to register with.
|
||||
main_app: The main application instance (for accessing set_current).
|
||||
|
||||
Returns:
|
||||
ViewTourSteps | None: A model containing the view title and step IDs,
|
||||
or None if this view has no tour steps.
|
||||
|
||||
Override this method in subclasses to register view-specific components.
|
||||
"""
|
||||
return None
|
||||
|
||||
####### Default view has to be done with setting up splitters ########
|
||||
def set_default_view(self, horizontal_weights: list, vertical_weights: list):
|
||||
"""Apply initial weights to every horizontal and vertical splitter.
|
||||
|
||||
Examples:
|
||||
horizontal_weights = [1, 3, 2, 1]
|
||||
vertical_weights = [3, 7] # top:bottom = 30:70
|
||||
"""
|
||||
splitters_h = []
|
||||
splitters_v = []
|
||||
for splitter in self.findChildren(QSplitter):
|
||||
if splitter.orientation() == Qt.Orientation.Horizontal:
|
||||
splitters_h.append(splitter)
|
||||
elif splitter.orientation() == Qt.Orientation.Vertical:
|
||||
splitters_v.append(splitter)
|
||||
|
||||
def apply_all():
|
||||
for s in splitters_h:
|
||||
set_splitter_weights(s, horizontal_weights)
|
||||
for s in splitters_v:
|
||||
set_splitter_weights(s, vertical_weights)
|
||||
|
||||
QTimer.singleShot(0, apply_all)
|
||||
|
||||
def set_stretch(self, *, horizontal=None, vertical=None):
|
||||
"""Update splitter weights and re-apply to all splitters.
|
||||
|
||||
Accepts either a list/tuple of weights (e.g., [1,3,2,1]) or a role dict
|
||||
for convenience: horizontal roles = {"left","center","right"},
|
||||
vertical roles = {"top","bottom"}.
|
||||
"""
|
||||
|
||||
def _coerce_h(x):
|
||||
if x is None:
|
||||
return None
|
||||
if isinstance(x, (list, tuple)):
|
||||
return list(map(float, x))
|
||||
if isinstance(x, dict):
|
||||
return [
|
||||
float(x.get("left", 1)),
|
||||
float(x.get("center", x.get("middle", 1))),
|
||||
float(x.get("right", 1)),
|
||||
]
|
||||
return None
|
||||
|
||||
def _coerce_v(x):
|
||||
if x is None:
|
||||
return None
|
||||
if isinstance(x, (list, tuple)):
|
||||
return list(map(float, x))
|
||||
if isinstance(x, dict):
|
||||
return [float(x.get("top", 1)), float(x.get("bottom", 1))]
|
||||
return None
|
||||
|
||||
h = _coerce_h(horizontal)
|
||||
v = _coerce_v(vertical)
|
||||
if h is None:
|
||||
h = [1, 1, 1]
|
||||
if v is None:
|
||||
v = [1, 1]
|
||||
self.set_default_view(h, v)
|
||||
|
||||
|
||||
####################################################################################################
|
||||
# Example views for demonstration/testing purposes
|
||||
|
||||
@@ -19,6 +19,7 @@ from qtpy.QtWidgets import (
|
||||
QHBoxLayout,
|
||||
QLabel,
|
||||
QMainWindow,
|
||||
QMenu,
|
||||
QMenuBar,
|
||||
QPushButton,
|
||||
QToolBar,
|
||||
@@ -327,11 +328,14 @@ class GuidedTour(QObject):
|
||||
if mb and mb not in menubars:
|
||||
menubars.append(mb)
|
||||
menubars += [mb for mb in mw.findChildren(QMenuBar) if mb not in menubars]
|
||||
menubars += [mb for mb in mw.findChildren(QMenu) 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:
|
||||
@@ -575,7 +579,7 @@ class GuidedTour(QObject):
|
||||
"""
|
||||
Skip the current step (or stop the tour) when the target cannot be visualised.
|
||||
"""
|
||||
logger.warning("%s Skipping step %r.", reason, step_title)
|
||||
logger.warning(f"{reason} Skipping step {step_title!r}.")
|
||||
if self._current_index < len(self._tour_steps) - 1:
|
||||
self._current_index += 1
|
||||
self._show_current_step()
|
||||
@@ -663,8 +667,33 @@ class MainWindow(QMainWindow): # pragma: no cover
|
||||
title="Tools Menu",
|
||||
)
|
||||
|
||||
sub_menu_action = self.tools_menu_actions["notes"].action
|
||||
|
||||
def get_sub_menu_action():
|
||||
# open the tools menu
|
||||
menu_button = self.tools_menu_action._button_ref()
|
||||
if menu_button:
|
||||
menu_button.showMenu()
|
||||
|
||||
return (
|
||||
self.tools_menu_action.actions["notes"].action,
|
||||
"This action allows you to add notes.",
|
||||
)
|
||||
|
||||
sub_menu = self.guided_help.register_widget(
|
||||
widget=get_sub_menu_action,
|
||||
text="This is a sub-action within the tools menu.",
|
||||
title="Add Note Action",
|
||||
)
|
||||
|
||||
# Create tour from registered widgets
|
||||
self.tour_step_ids = [primary_step, secondary_step, toolbar_action_step, tools_menu_step]
|
||||
self.tour_step_ids = [
|
||||
sub_menu,
|
||||
primary_step,
|
||||
secondary_step,
|
||||
toolbar_action_step,
|
||||
tools_menu_step,
|
||||
]
|
||||
widget_ids = self.tour_step_ids
|
||||
self.guided_help.create_tour(widget_ids)
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ from qtpy.QtWidgets import (
|
||||
|
||||
import bec_widgets.widgets.containers.qt_ads as QtAds
|
||||
from bec_widgets import BECWidget, SafeProperty, SafeSlot
|
||||
from bec_widgets.applications.views.view import ViewTourSteps
|
||||
from bec_widgets.cli.rpc.rpc_widget_handler import widget_handler
|
||||
from bec_widgets.utils import BECDispatcher
|
||||
from bec_widgets.utils.colors import apply_theme
|
||||
@@ -1138,6 +1139,33 @@ class BECDockArea(DockAreaWidget):
|
||||
set_last_profile(name, namespace=namespace, instance=self._last_profile_instance_id())
|
||||
self._exit_snapshot_written = True
|
||||
|
||||
def register_tour_steps(self, guided_tour, main_app):
|
||||
"""Register Dock Area components with the guided tour.
|
||||
|
||||
Args:
|
||||
guided_tour: The GuidedTour instance to register with.
|
||||
main_app: The main application instance (for accessing set_current).
|
||||
|
||||
Returns:
|
||||
ViewTourSteps | None: Model containing view title and step IDs.
|
||||
"""
|
||||
|
||||
step_ids = []
|
||||
|
||||
# Register Dock Area toolbar
|
||||
def get_dock_toolbar():
|
||||
main_app.set_current("dock_area")
|
||||
return (self.toolbar, None)
|
||||
|
||||
step_id = guided_tour.register_widget(
|
||||
widget=get_dock_toolbar,
|
||||
title="Dock Area Toolbar",
|
||||
text="Use this toolbar to add widgets, manage workspaces, save and load profiles, and control the layout of your workspace.",
|
||||
)
|
||||
step_ids.append(step_id)
|
||||
|
||||
return ViewTourSteps(view_title="Dock Area Workspace", step_ids=step_ids)
|
||||
|
||||
def cleanup(self):
|
||||
"""
|
||||
Cleanup the dock area.
|
||||
|
||||
@@ -109,3 +109,77 @@ def test_on_exit_veto_prevents_switch_until_allowed(app_with_spies, qtbot):
|
||||
# Now the switch should have happened, and v1 received on_enter
|
||||
assert app.stack.currentIndex() == app._view_index["v1"]
|
||||
assert v1.enter_calls >= 1
|
||||
|
||||
|
||||
def test_guided_tour_is_initialized(app_with_spies):
|
||||
"""Test that the guided tour is initialized in the main app."""
|
||||
app, _, _, _ = app_with_spies
|
||||
|
||||
# Check that guided_tour exists
|
||||
assert hasattr(app, "guided_tour")
|
||||
assert app.guided_tour is not None
|
||||
|
||||
# Check that start_guided_tour method exists
|
||||
assert hasattr(app, "start_guided_tour")
|
||||
assert callable(app.start_guided_tour)
|
||||
|
||||
|
||||
def test_guided_tour_has_registered_widgets(app_with_spies):
|
||||
"""Test that the guided tour has registered widgets."""
|
||||
app, _, _, _ = app_with_spies
|
||||
|
||||
# Get registered widgets
|
||||
registered = app.guided_tour.get_registered_widgets()
|
||||
|
||||
# Should have at least some registered widgets
|
||||
assert len(registered) > 0
|
||||
|
||||
# Check that tour steps were created
|
||||
assert len(app.guided_tour._tour_steps) > 0
|
||||
|
||||
|
||||
def test_views_can_extend_guided_tour(app_with_spies):
|
||||
"""Test that views can register their own tour steps."""
|
||||
app, _, _, _ = app_with_spies
|
||||
|
||||
# Check that device manager has register_tour_steps method
|
||||
assert hasattr(app.device_manager, "register_tour_steps")
|
||||
assert callable(app.device_manager.register_tour_steps)
|
||||
|
||||
# Check that developer view has register_tour_steps method
|
||||
assert hasattr(app.developer_view, "register_tour_steps")
|
||||
assert callable(app.developer_view.register_tour_steps)
|
||||
|
||||
# Verify that calling register_tour_steps returns ViewTourSteps or None
|
||||
dm_tour = app.device_manager.register_tour_steps(app.guided_tour, app)
|
||||
if dm_tour is not None:
|
||||
assert hasattr(dm_tour, "view_title")
|
||||
assert hasattr(dm_tour, "step_ids")
|
||||
assert isinstance(dm_tour.step_ids, list)
|
||||
|
||||
ide_tour = app.developer_view.register_tour_steps(app.guided_tour, app)
|
||||
if ide_tour is not None:
|
||||
assert hasattr(ide_tour, "view_title")
|
||||
assert hasattr(ide_tour, "step_ids")
|
||||
assert isinstance(ide_tour.step_ids, list)
|
||||
|
||||
|
||||
def test_guided_tour_can_start_and_stop(app_with_spies, qtbot):
|
||||
"""Test that the guided tour can be started and stopped."""
|
||||
app, _, _, _ = app_with_spies
|
||||
|
||||
# Start the tour
|
||||
app.start_guided_tour()
|
||||
qtbot.wait(100)
|
||||
|
||||
# Check that tour is active
|
||||
assert app.guided_tour._active
|
||||
assert app.guided_tour.overlay is not None
|
||||
assert app.guided_tour.overlay.isVisible()
|
||||
|
||||
# Stop the tour
|
||||
app.guided_tour.stop_tour()
|
||||
qtbot.wait(100)
|
||||
|
||||
# Check that tour is stopped
|
||||
assert not app.guided_tour._active
|
||||
|
||||
Reference in New Issue
Block a user