diff --git a/bec_widgets/widgets/containers/advanced_dock_area/advanced_dock_area.py b/bec_widgets/widgets/containers/advanced_dock_area/advanced_dock_area.py index 0f35149e..539ffd24 100644 --- a/bec_widgets/widgets/containers/advanced_dock_area/advanced_dock_area.py +++ b/bec_widgets/widgets/containers/advanced_dock_area/advanced_dock_area.py @@ -5,8 +5,7 @@ from typing import cast import PySide6QtAds as QtAds from PySide6QtAds import CDockManager, CDockWidget -from qtpy.QtCore import QSettings, QSize, Qt -from qtpy.QtGui import QAction +from qtpy.QtCore import Property, QSettings, QSize, Signal from qtpy.QtWidgets import ( QApplication, QCheckBox, @@ -39,7 +38,7 @@ from bec_widgets.widgets.containers.advanced_dock_area.toolbar_components.worksp WorkspaceConnection, workspace_bundle, ) -from bec_widgets.widgets.containers.main_window.main_window import BECMainWindow +from bec_widgets.widgets.containers.main_window.main_window import BECMainWindowNoRPC from bec_widgets.widgets.control.device_control.positioner_box import PositionerBox from bec_widgets.widgets.control.scan_control import ScanControl from bec_widgets.widgets.editors.vscode.vscode import VSCodeEditor @@ -200,45 +199,64 @@ class SaveProfileDialog(QDialog): return self.readonly_checkbox.isChecked() -class AdvancedDockArea(BECMainWindow): +class AdvancedDockArea(BECWidget, QWidget): RPC = True PLUGIN = False USER_ACCESS = ["new", "widget_map", "widget_list", "lock_workspace", "attach_all", "delete_all"] - def __init__(self, parent=None, *args, **kwargs): + # Define a signal for mode changes + mode_changed = Signal(str) + + def __init__(self, parent=None, mode: str = "developer", *args, **kwargs): super().__init__(parent=parent, *args, **kwargs) + # Title (as a top-level QWidget it can have a window title) + self.setWindowTitle("Advanced Dock Area") + + # Top-level layout hosting a toolbar and the dock manager + self._root_layout = QVBoxLayout(self) + self._root_layout.setContentsMargins(0, 0, 0, 0) + self._root_layout.setSpacing(0) + # Setting the dock manager with flags QtAds.CDockManager.setConfigFlag(QtAds.CDockManager.eConfigFlag.FocusHighlighting, True) QtAds.CDockManager.setConfigFlag( QtAds.CDockManager.eConfigFlag.RetainTabSizeWhenCloseButtonHidden, True ) - QtAds.CDockManager.setConfigFlag( - QtAds.CDockManager.eConfigFlag.HideSingleCentralWidgetTitleBar, True - ) self.dock_manager = CDockManager(self) # Dock manager helper variables self._locked = False # Lock state of the workspace + # Initialize mode property first (before toolbar setup) + self._mode = "developer" + # Toolbar self.dark_mode_button = DarkModeButton(parent=self, toolbar=True) self._setup_toolbar() self._hook_toolbar() + # Place toolbar and dock manager into layout + self._root_layout.addWidget(self.toolbar) + self._root_layout.addWidget(self.dock_manager, 1) + # Populate and hook the workspace combo self._refresh_workspace_list() # State manager self.state_manager = WidgetStateManager(self) - # Insert Mode menu + # Developer mode state self._editable = None - self._setup_developer_mode_menu() + # Initialize default editable state based on current lock + self._set_editable(True) # default to editable; will sync toolbar toggle below - # Notification center re-raise - self.notification_centre.raise_() - self.statusBar().raise_() + # Sync Developer toggle icon state after initial setup + dev_action = self.toolbar.components.get_action("developer_mode").action + dev_action.setChecked(self._editable) + + # Apply the requested mode after everything is set up + self.mode = mode def minimumSizeHint(self): return QSize(1200, 800) @@ -350,6 +368,7 @@ class AdvancedDockArea(BECMainWindow): "sbb_monitor": ("train", "Add SBB Monitor", "SBBMonitor"), } + # Create expandable menu actions (original behavior) def _build_menu(key: str, label: str, mapping: dict[str, tuple[str, str, str]]): self.toolbar.components.add_safe( key, @@ -371,6 +390,27 @@ class AdvancedDockArea(BECMainWindow): _build_menu("menu_devices", "Add Device Control ", DEVICE_ACTIONS) _build_menu("menu_utils", "Add Utils ", UTIL_ACTIONS) + # Create flat toolbar bundles for each widget type + def _build_flat_bundles(category: str, mapping: dict[str, tuple[str, str, str]]): + bundle = ToolbarBundle(f"flat_{category}", self.toolbar.components) + + for action_id, (icon_name, tooltip, widget_type) in mapping.items(): + # Create individual action for each widget type + flat_action_id = f"flat_{action_id}" + self.toolbar.components.add_safe( + flat_action_id, + MaterialIconAction( + icon_name=icon_name, tooltip=tooltip, filled=True, parent=self + ), + ) + bundle.add_action(flat_action_id) + + self.toolbar.add_bundle(bundle) + + _build_flat_bundles("plots", PLOT_ACTIONS) + _build_flat_bundles("devices", DEVICE_ACTIONS) + _build_flat_bundles("utils", UTIL_ACTIONS) + # Workspace spacer_bundle = ToolbarBundle("spacer_bundle", self.toolbar.components) spacer = QWidget(parent=self.toolbar.components.toolbar) @@ -398,12 +438,21 @@ class AdvancedDockArea(BECMainWindow): self.toolbar.components.add_safe( "dark_mode", WidgetAction(widget=self.dark_mode_button, adjust_size=False, parent=self) ) + # Developer mode toggle (moved from menu into toolbar) + self.toolbar.components.add_safe( + "developer_mode", + MaterialIconAction( + icon_name="code", tooltip="Developer Mode", checkable=True, parent=self + ), + ) bda = ToolbarBundle("dock_actions", self.toolbar.components) bda.add_action("attach_all") bda.add_action("screenshot") bda.add_action("dark_mode") + bda.add_action("developer_mode") self.toolbar.add_bundle(bda) + # Default bundle configuration (show menus by default) self.toolbar.show_bundles( [ "menu_plots", @@ -414,7 +463,6 @@ class AdvancedDockArea(BECMainWindow): "dock_actions", ] ) - self.addToolBar(Qt.TopToolBarArea, self.toolbar) # Store mappings on self for use in _hook_toolbar self._ACTION_MAPPINGS = { @@ -439,42 +487,26 @@ class AdvancedDockArea(BECMainWindow): _connect_menu("menu_devices") _connect_menu("menu_utils") + # Connect flat toolbar actions + def _connect_flat_actions(category: str, mapping: dict[str, tuple[str, str, str]]): + for action_id, (_, _, widget_type) in mapping.items(): + flat_action_id = f"flat_{action_id}" + flat_action = self.toolbar.components.get_action(flat_action_id).action + if widget_type == "LogPanel": + flat_action.setEnabled(False) # keep disabled per issue #644 + else: + flat_action.triggered.connect(lambda _, t=widget_type: self.new(widget=t)) + + _connect_flat_actions("plots", self._ACTION_MAPPINGS["menu_plots"]) + _connect_flat_actions("devices", self._ACTION_MAPPINGS["menu_devices"]) + _connect_flat_actions("utils", self._ACTION_MAPPINGS["menu_utils"]) + self.toolbar.components.get_action("attach_all").action.triggered.connect(self.attach_all) self.toolbar.components.get_action("screenshot").action.triggered.connect(self.screenshot) - - def _setup_developer_mode_menu(self): - """Add a 'Developer' checkbox to the View menu after theme actions.""" - mb = self.menuBar() - - # Find the View menu (inherited from BECMainWindow) - view_menu = None - for action in mb.actions(): - if action.menu() and action.menu().title() == "View": - view_menu = action.menu() - break - - if view_menu is None: - # If View menu doesn't exist, create it - view_menu = mb.addMenu("View") - - # Add separator after existing theme actions - view_menu.addSeparator() - - # Add Developer mode checkbox - self._developer_mode_action = QAction("Developer", self, checkable=True) - - # Default selection based on current lock state - self._editable = not self.lock_workspace - self._developer_mode_action.setChecked(self._editable) - - # Wire up action - self._developer_mode_action.triggered.connect(self._on_developer_mode_toggled) - - view_menu.addAction(self._developer_mode_action) - - def _on_developer_mode_toggled(self, checked: bool) -> None: - """Handle developer mode checkbox toggle.""" - self._set_editable(checked) + # Developer mode toggle + self.toolbar.components.get_action("developer_mode").action.toggled.connect( + self._on_developer_mode_toggled + ) def _set_editable(self, editable: bool) -> None: self.lock_workspace = not editable @@ -504,8 +536,11 @@ class AdvancedDockArea(BECMainWindow): self.toolbar.show_bundles(["spacer_bundle", "workspace", "dock_actions"]) # Keep Developer mode UI in sync - if hasattr(self, "_developer_mode_action"): - self._developer_mode_action.setChecked(editable) + self.toolbar.components.get_action("developer_mode").action.setChecked(editable) + + def _on_developer_mode_toggled(self, checked: bool) -> None: + """Handle developer mode checkbox toggle.""" + self._set_editable(checked) ################################################################################ # Adding widgets @@ -718,7 +753,9 @@ class AdvancedDockArea(BECMainWindow): # Save the profile settings = open_settings(name) settings.setValue(SETTINGS_KEYS["geom"], self.saveGeometry()) - settings.setValue(SETTINGS_KEYS["state"], self.saveState()) + settings.setValue( + SETTINGS_KEYS["state"], b"" + ) # No QMainWindow state; placeholder for backward compat settings.setValue(SETTINGS_KEYS["ads_state"], self.dock_manager.saveState()) self.dock_manager.addPerspective(name) self.dock_manager.savePerspectives(settings) @@ -766,9 +803,8 @@ class AdvancedDockArea(BECMainWindow): geom = settings.value(SETTINGS_KEYS["geom"]) if geom: self.restoreGeometry(geom) - window_state = settings.value(SETTINGS_KEYS["state"]) - if window_state: - self.restoreState(window_state) + # No window state for QWidget-based host; keep for backwards compat read + # window_state = settings.value(SETTINGS_KEYS["state"]) # ignored dock_state = settings.value(SETTINGS_KEYS["ads_state"]) if dock_state: self.dock_manager.restoreState(dock_state) @@ -831,9 +867,60 @@ class AdvancedDockArea(BECMainWindow): combo.blockSignals(False) ################################################################################ - # Styling + # Mode Switching ################################################################################ + @SafeProperty(str) + def mode(self) -> str: + return self._mode + + @mode.setter + def mode(self, new_mode: str): + if new_mode not in ["plot", "device", "utils", "developer", "user"]: + raise ValueError(f"Invalid mode: {new_mode}") + self._mode = new_mode + self.mode_changed.emit(new_mode) + + # Update toolbar visibility based on mode + if new_mode == "user": + # User mode: show only essential tools + self.toolbar.show_bundles(["spacer_bundle", "workspace", "dock_actions"]) + elif new_mode == "developer": + # Developer mode: show all tools (use menu bundles) + self.toolbar.show_bundles( + [ + "menu_plots", + "menu_devices", + "menu_utils", + "spacer_bundle", + "workspace", + "dock_actions", + ] + ) + elif new_mode in ["plot", "device", "utils"]: + # Specific modes: show flat toolbar for that category + bundle_name = f"flat_{new_mode}s" if new_mode != "utils" else "flat_utils" + self.toolbar.show_bundles([bundle_name]) + # self.toolbar.show_bundles([bundle_name, "spacer_bundle", "workspace", "dock_actions"]) + else: + # Fallback to user mode + self.toolbar.show_bundles(["spacer_bundle", "workspace", "dock_actions"]) + + def switch_to_plot_mode(self): + self.mode = "plot" + + def switch_to_device_mode(self): + self.mode = "device" + + def switch_to_utils_mode(self): + self.mode = "utils" + + def switch_to_developer_mode(self): + self.mode = "developer" + + def switch_to_user_mode(self): + self.mode = "user" + def cleanup(self): """ Cleanup the dock area. @@ -841,6 +928,7 @@ class AdvancedDockArea(BECMainWindow): self.delete_all() self.dark_mode_button.close() self.dark_mode_button.deleteLater() + self.toolbar.cleanup() super().cleanup() @@ -849,7 +937,9 @@ if __name__ == "__main__": app = QApplication(sys.argv) dispatcher = BECDispatcher(gui_id="ads") - main_window = AdvancedDockArea() - main_window.show() - main_window.resize(800, 600) + window = BECMainWindowNoRPC() + ads = AdvancedDockArea(parent=window, mode="developer") + window.setCentralWidget(ads) + window.show() + window.resize(800, 600) sys.exit(app.exec()) diff --git a/bec_widgets/widgets/containers/advanced_dock_area/toolbar_components/workspace_actions.py b/bec_widgets/widgets/containers/advanced_dock_area/toolbar_components/workspace_actions.py index b994b3ca..616dcc08 100644 --- a/bec_widgets/widgets/containers/advanced_dock_area/toolbar_components/workspace_actions.py +++ b/bec_widgets/widgets/containers/advanced_dock_area/toolbar_components/workspace_actions.py @@ -7,6 +7,11 @@ from qtpy.QtWidgets import QComboBox, QSizePolicy, QWidget from bec_widgets import SafeSlot from bec_widgets.utils.toolbars.actions import MaterialIconAction, WidgetAction from bec_widgets.utils.toolbars.bundles import ToolbarBundle, ToolbarComponents +from bec_widgets.utils.toolbars.connections import BundleConnection +from bec_widgets.widgets.containers.advanced_dock_area.profile_utils import ( + is_profile_readonly, + list_profiles, +) class ProfileComboBox(QComboBox): @@ -18,7 +23,6 @@ class ProfileComboBox(QComboBox): def refresh_profiles(self): """Refresh the profile list with appropriate icons.""" - from ..advanced_dock_area import is_profile_readonly, list_profiles current_text = self.currentText() self.blockSignals(True) @@ -107,18 +111,18 @@ def workspace_bundle(components: ToolbarComponents) -> ToolbarBundle: return bundle -class WorkspaceConnection: +class WorkspaceConnection(BundleConnection): """ Connection class for workspace actions in AdvancedDockArea. """ def __init__(self, components: ToolbarComponents, target_widget=None): + super().__init__(parent=components.toolbar) self.bundle_name = "workspace" self.components = components self.target_widget = target_widget if not hasattr(self.target_widget, "lock_workspace"): raise AttributeError("Target widget must implement 'lock_workspace'.") - super().__init__() self._connected = False def connect(self): @@ -155,6 +159,7 @@ class WorkspaceConnection: self.components.get_action("delete_workspace").action.triggered.disconnect( self.target_widget.delete_profile ) + self._connected = False @SafeSlot(bool) def _lock_workspace(self, value: bool): diff --git a/tests/unit_tests/test_advanced_dock_area.py b/tests/unit_tests/test_advanced_dock_area.py index 48cc770f..2207a31e 100644 --- a/tests/unit_tests/test_advanced_dock_area.py +++ b/tests/unit_tests/test_advanced_dock_area.py @@ -6,8 +6,7 @@ from unittest import mock from unittest.mock import MagicMock, patch import pytest -from qtpy.QtCore import QSettings, Qt -from qtpy.QtGui import QAction +from qtpy.QtCore import QSettings from qtpy.QtWidgets import QDialog, QMessageBox from bec_widgets.widgets.containers.advanced_dock_area.advanced_dock_area import ( @@ -50,6 +49,7 @@ class TestAdvancedDockAreaInit: def test_init(self, advanced_dock_area): assert advanced_dock_area is not None assert isinstance(advanced_dock_area, AdvancedDockArea) + assert advanced_dock_area.mode == "developer" assert hasattr(advanced_dock_area, "dock_manager") assert hasattr(advanced_dock_area, "toolbar") assert hasattr(advanced_dock_area, "dark_mode_button") @@ -173,28 +173,6 @@ class TestDockManagement: new_widget_list = advanced_dock_area.widget_list() assert len(new_widget_list) == initial_count + 1 - def test_attach_all(self, advanced_dock_area, qtbot): - """Test attach_all functionality.""" - # Create multiple widgets - advanced_dock_area.new("DarkModeButton", start_floating=True) - advanced_dock_area.new("DarkModeButton", start_floating=True) - - # Wait for docks to be created - qtbot.wait(200) - - # Should have floating widgets - initial_floating = len(advanced_dock_area.dock_manager.floatingWidgets()) - - # Attach all floating docks - advanced_dock_area.attach_all() - - # Wait a bit for the operation to complete - qtbot.wait(200) - - # Should have fewer floating widgets (or none if all were attached) - final_floating = len(advanced_dock_area.dock_manager.floatingWidgets()) - assert final_floating <= initial_floating - def test_delete_all(self, advanced_dock_area, qtbot): """Test delete_all functionality.""" # Create multiple widgets @@ -252,13 +230,6 @@ class TestWorkspaceLocking: class TestDeveloperMode: """Test developer mode functionality.""" - def test_setup_developer_mode_menu(self, advanced_dock_area): - """Test developer mode menu setup.""" - # The menu should be set up during initialization - assert hasattr(advanced_dock_area, "_developer_mode_action") - assert isinstance(advanced_dock_area._developer_mode_action, QAction) - assert advanced_dock_area._developer_mode_action.isCheckable() - def test_developer_mode_toggle(self, advanced_dock_area): """Test developer mode toggle functionality.""" # Check initial state @@ -715,19 +686,6 @@ class TestWorkspaceProfileOperations: class TestCleanupAndMisc: """Test cleanup and miscellaneous functionality.""" - def test_cleanup(self, advanced_dock_area): - """Test cleanup functionality.""" - with patch.object(advanced_dock_area.dark_mode_button, "close") as mock_close: - with patch.object(advanced_dock_area.dark_mode_button, "deleteLater") as mock_delete: - with patch( - "bec_widgets.widgets.containers.main_window.main_window.BECMainWindow.cleanup" - ) as mock_super_cleanup: - advanced_dock_area.cleanup() - - mock_close.assert_called_once() - mock_delete.assert_called_once() - mock_super_cleanup.assert_called_once() - def test_delete_dock(self, advanced_dock_area, qtbot): """Test _delete_dock functionality.""" from bec_widgets.widgets.utility.visual.dark_mode_button.dark_mode_button import ( @@ -804,3 +762,332 @@ class TestCleanupAndMisc: # Verify title bar actions were set title_bar_actions = dock.titleBarActions() assert len(title_bar_actions) >= 1 + + +class TestModeSwitching: + """Test mode switching functionality.""" + + def test_mode_property_setter_valid_modes(self, advanced_dock_area): + """Test setting valid modes.""" + valid_modes = ["plot", "device", "utils", "developer", "user"] + + for mode in valid_modes: + advanced_dock_area.mode = mode + assert advanced_dock_area.mode == mode + + def test_mode_changed_signal_emission(self, advanced_dock_area, qtbot): + """Test that mode_changed signal is emitted when mode changes.""" + # Set up signal spy + with qtbot.waitSignal(advanced_dock_area.mode_changed, timeout=1000) as blocker: + advanced_dock_area.mode = "plot" + + # Check signal was emitted with correct argument + assert blocker.args == ["plot"] + + def test_switch_to_plot_mode(self, advanced_dock_area): + """Test switch_to_plot_mode method.""" + advanced_dock_area.switch_to_plot_mode() + assert advanced_dock_area.mode == "plot" + + def test_switch_to_device_mode(self, advanced_dock_area): + """Test switch_to_device_mode method.""" + advanced_dock_area.switch_to_device_mode() + assert advanced_dock_area.mode == "device" + + def test_switch_to_utils_mode(self, advanced_dock_area): + """Test switch_to_utils_mode method.""" + advanced_dock_area.switch_to_utils_mode() + assert advanced_dock_area.mode == "utils" + + def test_switch_to_developer_mode(self, advanced_dock_area): + """Test switch_to_developer_mode method.""" + advanced_dock_area.switch_to_developer_mode() + assert advanced_dock_area.mode == "developer" + + def test_switch_to_user_mode(self, advanced_dock_area): + """Test switch_to_user_mode method.""" + advanced_dock_area.switch_to_user_mode() + assert advanced_dock_area.mode == "user" + + +class TestToolbarModeBundles: + """Test toolbar bundle creation and visibility for different modes.""" + + def test_flat_bundles_created(self, advanced_dock_area): + """Test that flat bundles are created during toolbar setup.""" + # Check that flat bundles exist + assert "flat_plots" in advanced_dock_area.toolbar.bundles + assert "flat_devices" in advanced_dock_area.toolbar.bundles + assert "flat_utils" in advanced_dock_area.toolbar.bundles + + def test_plot_mode_toolbar_visibility(self, advanced_dock_area): + """Test toolbar bundle visibility in plot mode.""" + advanced_dock_area.mode = "plot" + + # Should show only flat_plots bundle (and essential bundles in real implementation) + shown_bundles = advanced_dock_area.toolbar.shown_bundles + assert "flat_plots" in shown_bundles + + # Should not show other flat bundles + assert "flat_devices" not in shown_bundles + assert "flat_utils" not in shown_bundles + + # Should not show menu bundles + assert "menu_plots" not in shown_bundles + assert "menu_devices" not in shown_bundles + assert "menu_utils" not in shown_bundles + + def test_device_mode_toolbar_visibility(self, advanced_dock_area): + """Test toolbar bundle visibility in device mode.""" + advanced_dock_area.mode = "device" + + shown_bundles = advanced_dock_area.toolbar.shown_bundles + assert "flat_devices" in shown_bundles + + # Should not show other flat bundles + assert "flat_plots" not in shown_bundles + assert "flat_utils" not in shown_bundles + + def test_utils_mode_toolbar_visibility(self, advanced_dock_area): + """Test toolbar bundle visibility in utils mode.""" + advanced_dock_area.mode = "utils" + + shown_bundles = advanced_dock_area.toolbar.shown_bundles + assert "flat_utils" in shown_bundles + + # Should not show other flat bundles + assert "flat_plots" not in shown_bundles + assert "flat_devices" not in shown_bundles + + def test_developer_mode_toolbar_visibility(self, advanced_dock_area): + """Test toolbar bundle visibility in developer mode.""" + advanced_dock_area.mode = "developer" + + shown_bundles = advanced_dock_area.toolbar.shown_bundles + + # Should show menu bundles + assert "menu_plots" in shown_bundles + assert "menu_devices" in shown_bundles + assert "menu_utils" in shown_bundles + + # Should show essential bundles + assert "spacer_bundle" in shown_bundles + assert "workspace" in shown_bundles + assert "dock_actions" in shown_bundles + + def test_user_mode_toolbar_visibility(self, advanced_dock_area): + """Test toolbar bundle visibility in user mode.""" + advanced_dock_area.mode = "user" + + shown_bundles = advanced_dock_area.toolbar.shown_bundles + + # Should show only essential bundles + assert "spacer_bundle" in shown_bundles + assert "workspace" in shown_bundles + assert "dock_actions" in shown_bundles + + # Should not show any widget creation bundles + assert "menu_plots" not in shown_bundles + assert "menu_devices" not in shown_bundles + assert "menu_utils" not in shown_bundles + assert "flat_plots" not in shown_bundles + assert "flat_devices" not in shown_bundles + assert "flat_utils" not in shown_bundles + + +class TestFlatToolbarActions: + """Test flat toolbar actions functionality.""" + + def test_flat_plot_actions_created(self, advanced_dock_area): + """Test that flat plot actions are created.""" + plot_actions = [ + "flat_waveform", + "flat_scatter_waveform", + "flat_multi_waveform", + "flat_image", + "flat_motor_map", + "flat_heatmap", + ] + + for action_name in plot_actions: + assert advanced_dock_area.toolbar.components.exists(action_name) + + def test_flat_device_actions_created(self, advanced_dock_area): + """Test that flat device actions are created.""" + device_actions = ["flat_scan_control", "flat_positioner_box"] + + for action_name in device_actions: + assert advanced_dock_area.toolbar.components.exists(action_name) + + def test_flat_utils_actions_created(self, advanced_dock_area): + """Test that flat utils actions are created.""" + utils_actions = [ + "flat_queue", + "flat_vs_code", + "flat_status", + "flat_progress_bar", + "flat_log_panel", + "flat_sbb_monitor", + ] + + for action_name in utils_actions: + assert advanced_dock_area.toolbar.components.exists(action_name) + + def test_flat_plot_actions_trigger_widget_creation(self, advanced_dock_area): + """Test flat plot actions trigger widget creation.""" + plot_action_mapping = { + "flat_waveform": "Waveform", + "flat_scatter_waveform": "ScatterWaveform", + "flat_multi_waveform": "MultiWaveform", + "flat_image": "Image", + "flat_motor_map": "MotorMap", + "flat_heatmap": "Heatmap", + } + + for action_name, widget_type in plot_action_mapping.items(): + with patch.object(advanced_dock_area, "new") as mock_new: + action = advanced_dock_area.toolbar.components.get_action(action_name).action + action.trigger() + mock_new.assert_called_once_with(widget=widget_type) + + def test_flat_device_actions_trigger_widget_creation(self, advanced_dock_area): + """Test flat device actions trigger widget creation.""" + device_action_mapping = { + "flat_scan_control": "ScanControl", + "flat_positioner_box": "PositionerBox", + } + + for action_name, widget_type in device_action_mapping.items(): + with patch.object(advanced_dock_area, "new") as mock_new: + action = advanced_dock_area.toolbar.components.get_action(action_name).action + action.trigger() + mock_new.assert_called_once_with(widget=widget_type) + + def test_flat_utils_actions_trigger_widget_creation(self, advanced_dock_area): + """Test flat utils actions trigger widget creation.""" + utils_action_mapping = { + "flat_queue": "BECQueue", + "flat_vs_code": "VSCodeEditor", + "flat_status": "BECStatusBox", + "flat_progress_bar": "RingProgressBar", + "flat_sbb_monitor": "SBBMonitor", + } + + for action_name, widget_type in utils_action_mapping.items(): + with patch.object(advanced_dock_area, "new") as mock_new: + action = advanced_dock_area.toolbar.components.get_action(action_name).action + + # Skip log_panel as it's disabled + if action_name == "flat_log_panel": + assert not action.isEnabled() + continue + + action.trigger() + mock_new.assert_called_once_with(widget=widget_type) + + def test_flat_log_panel_action_disabled(self, advanced_dock_area): + """Test that flat log panel action is disabled.""" + action = advanced_dock_area.toolbar.components.get_action("flat_log_panel").action + assert not action.isEnabled() + + +class TestModeTransitions: + """Test mode transitions and state consistency.""" + + def test_mode_transition_sequence(self, advanced_dock_area, qtbot): + """Test sequence of mode transitions.""" + modes = ["plot", "device", "utils", "developer", "user"] + + for mode in modes: + with qtbot.waitSignal(advanced_dock_area.mode_changed, timeout=1000) as blocker: + advanced_dock_area.mode = mode + + assert advanced_dock_area.mode == mode + assert blocker.args == [mode] + + def test_mode_consistency_after_multiple_changes(self, advanced_dock_area): + """Test mode consistency after multiple rapid changes.""" + # Rapidly change modes + advanced_dock_area.mode = "plot" + advanced_dock_area.mode = "device" + advanced_dock_area.mode = "utils" + advanced_dock_area.mode = "developer" + advanced_dock_area.mode = "user" + + # Final state should be consistent + assert advanced_dock_area.mode == "user" + + # Toolbar should show correct bundles for user mode + shown_bundles = advanced_dock_area.toolbar.shown_bundles + assert "spacer_bundle" in shown_bundles + assert "workspace" in shown_bundles + assert "dock_actions" in shown_bundles + + def test_toolbar_refresh_on_mode_change(self, advanced_dock_area): + """Test that toolbar is properly refreshed when mode changes.""" + initial_bundles = set(advanced_dock_area.toolbar.shown_bundles) + + # Change to a different mode + advanced_dock_area.mode = "plot" + plot_bundles = set(advanced_dock_area.toolbar.shown_bundles) + + # Bundles should be different + assert initial_bundles != plot_bundles + assert "flat_plots" in plot_bundles + + def test_mode_switching_preserves_existing_docks(self, advanced_dock_area, qtbot): + """Test that mode switching doesn't affect existing docked widgets.""" + # Create some widgets + advanced_dock_area.new("DarkModeButton") + advanced_dock_area.new("DarkModeButton") + qtbot.wait(200) + + initial_dock_count = len(advanced_dock_area.dock_list()) + initial_widget_count = len(advanced_dock_area.widget_list()) + + # Switch modes + advanced_dock_area.mode = "plot" + advanced_dock_area.mode = "device" + advanced_dock_area.mode = "user" + + # Dock and widget counts should remain the same + assert len(advanced_dock_area.dock_list()) == initial_dock_count + assert len(advanced_dock_area.widget_list()) == initial_widget_count + + +class TestModeProperty: + """Test mode property getter and setter behavior.""" + + def test_mode_property_getter(self, advanced_dock_area): + """Test mode property getter returns correct value.""" + # Set internal mode directly and test getter + advanced_dock_area._mode = "plot" + assert advanced_dock_area.mode == "plot" + + advanced_dock_area._mode = "device" + assert advanced_dock_area.mode == "device" + + def test_mode_property_setter_updates_internal_state(self, advanced_dock_area): + """Test mode property setter updates internal state.""" + advanced_dock_area.mode = "plot" + assert advanced_dock_area._mode == "plot" + + advanced_dock_area.mode = "utils" + assert advanced_dock_area._mode == "utils" + + def test_mode_property_setter_triggers_toolbar_update(self, advanced_dock_area): + """Test mode property setter triggers toolbar update.""" + with patch.object(advanced_dock_area.toolbar, "show_bundles") as mock_show_bundles: + advanced_dock_area.mode = "plot" + mock_show_bundles.assert_called_once() + + def test_multiple_mode_changes(self, advanced_dock_area, qtbot): + """Test multiple rapid mode changes.""" + modes = ["plot", "device", "utils", "developer", "user"] + + for i, mode in enumerate(modes): + with qtbot.waitSignal(advanced_dock_area.mode_changed, timeout=1000) as blocker: + advanced_dock_area.mode = mode + + assert advanced_dock_area.mode == mode + assert blocker.args == [mode]