From 45e9f03093be2ebfd860f6d296d4766925acd807 Mon Sep 17 00:00:00 2001 From: wyzula-jan Date: Tue, 20 Jan 2026 13:03:07 +0100 Subject: [PATCH] feat(toolbar): splitter action added --- bec_widgets/utils/toolbars/actions.py | 77 ++++++++ bec_widgets/utils/toolbars/bundles.py | 87 +++++++- bec_widgets/utils/toolbars/splitter.py | 241 +++++++++++++++++++++++ bec_widgets/utils/toolbars/toolbar.py | 76 +++++-- tests/unit_tests/test_modular_toolbar.py | 127 ++++++++++++ 5 files changed, 593 insertions(+), 15 deletions(-) create mode 100644 bec_widgets/utils/toolbars/splitter.py diff --git a/bec_widgets/utils/toolbars/actions.py b/bec_widgets/utils/toolbars/actions.py index 5c0b0955..f82753ff 100644 --- a/bec_widgets/utils/toolbars/actions.py +++ b/bec_widgets/utils/toolbars/actions.py @@ -26,6 +26,7 @@ from qtpy.QtWidgets import ( ) import bec_widgets +from bec_widgets.utils.toolbars.splitter import ResizableSpacer 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 @@ -498,6 +499,82 @@ class WidgetAction(ToolBarAction): return max_width + 60 +class SplitterAction(ToolBarAction): + """ + Action for adding a draggable splitter/spacer to the toolbar. + + This creates a resizable spacer that allows users to control how much space + is allocated to toolbar sections before and after it. When dragged, it expands/contracts, + pushing other toolbar elements left or right. + + Args: + orientation (Literal["horizontal", "vertical", "auto"]): The orientation of the splitter. + parent (QWidget): The parent widget. + initial_width (int): Fixed size of the spacer in pixels along the toolbar's orientation (default: 20). + min_width (int | None): Minimum size of the target widget along the orientation axis (width for horizontal, height for vertical). If ``None``, no minimum constraint is applied. + max_width (int | None): Maximum size of the target widget along the orientation axis (width for horizontal, height for vertical). If ``None``, no maximum constraint is applied. + target_widget (QWidget | None): Widget whose size (width or height, depending on orientation) is controlled by the spacer within the given min/max bounds. + """ + + def __init__( + self, + orientation: Literal["horizontal", "vertical", "auto"] = "auto", + parent=None, + initial_width=20, + min_width: int | None = None, + max_width: int | None = None, + target_widget=None, + ): + super().__init__(icon_path=None, tooltip="Drag to resize toolbar sections", checkable=False) + self.orientation = orientation + self.initial_width = initial_width + self.min_width = min_width + self.max_width = max_width + self._splitter_widget = None + self._target_widget = target_widget + + def _resolve_orientation(self, toolbar: QToolBar) -> Literal["horizontal", "vertical"]: + if self.orientation in (None, "auto"): + return ( + "horizontal" if toolbar.orientation() == Qt.Orientation.Horizontal else "vertical" + ) + return self.orientation + + def set_target_widget(self, widget): + """Set the target widget after creation.""" + self._target_widget = widget + if self._splitter_widget: + self._splitter_widget.set_target_widget(widget) + + def add_to_toolbar(self, toolbar: QToolBar, target: QWidget): + """ + Adds the splitter/spacer to the toolbar. + + Args: + toolbar (QToolBar): The toolbar to add the splitter to. + target (QWidget): The target widget for the action. + """ + + effective_orientation = self._resolve_orientation(toolbar) + self._splitter_widget = ResizableSpacer( + parent=target, + orientation=effective_orientation, + initial_width=self.initial_width, + min_target_size=self.min_width, + max_target_size=self.max_width, + target_widget=self._target_widget, + ) + toolbar.addWidget(self._splitter_widget) + self.action = self._splitter_widget # type: ignore + + def cleanup(self): + """Clean up the splitter widget.""" + if self._splitter_widget is not None: + self._splitter_widget.close() + self._splitter_widget.deleteLater() + return super().cleanup() + + class ExpandableMenuAction(ToolBarAction): """ Action for an expandable menu in the toolbar. diff --git a/bec_widgets/utils/toolbars/bundles.py b/bec_widgets/utils/toolbars/bundles.py index 36876995..5c312c6b 100644 --- a/bec_widgets/utils/toolbars/bundles.py +++ b/bec_widgets/utils/toolbars/bundles.py @@ -7,10 +7,17 @@ from weakref import ReferenceType import louie from bec_lib.logger import bec_logger from pydantic import BaseModel +from qtpy.QtCore import Qt +from qtpy.QtWidgets import QSizePolicy -from bec_widgets.utils.toolbars.actions import SeparatorAction, ToolBarAction +from bec_widgets.utils.toolbars.actions import SeparatorAction, SplitterAction, ToolBarAction + +DEFAULT_SIZE = 400 +MAX_SIZE = 10_000_000 if TYPE_CHECKING: + from qtpy.QtWidgets import QWidget + from bec_widgets.utils.toolbars.connections import BundleConnection from bec_widgets.utils.toolbars.toolbar import ModularToolBar @@ -195,6 +202,84 @@ class ToolbarBundle: """ self.add_action("separator") + def add_splitter( + self, + name: str = "splitter", + target_widget: QWidget | None = None, + initial_width: int = 10, + min_width: int | None = None, + max_width: int | None = None, + size_policy_expanding: bool = True, + ): + """ + Adds a resizable splitter action to the bundle. + + Args: + name (str): Unique identifier for the splitter action. + target_widget (QWidget, optional): The widget whose size (width for horizontal, + height for vertical orientation) will be controlled by the splitter. If None, + the splitter will not control any widget. + initial_width (int): The initial size of the splitter (width for horizontal, + height for vertical orientation). + min_width (int, optional): The minimum size the target widget can be resized to + (width for horizontal, height for vertical orientation). If None, the target + widget's minimum size hint in that orientation will be used. + max_width (int, optional): The maximum size the target widget can be resized to + (width for horizontal, height for vertical orientation). If None, the target + widget's maximum size hint in that orientation will be used. + size_policy_expanding (bool): If True, the size policy of the target_widget will be + set to Expanding in the appropriate orientation if it is not already set. + """ + + # Resolve effective bounds + eff_min = min_width if min_width is not None else None + eff_max = max_width if max_width is not None else None + + is_horizontal = self.components.toolbar.orientation() == Qt.Orientation.Horizontal + + if target_widget is not None: + # Use widget hints if bounds not provided + if eff_min is None: + eff_min = ( + target_widget.minimumWidth() if is_horizontal else target_widget.minimumHeight() + ) or 6 + if eff_max is None: + mw = ( + target_widget.maximumWidth() if is_horizontal else target_widget.maximumHeight() + ) + eff_max = mw if mw and mw < MAX_SIZE else DEFAULT_SIZE # avoid "no limit" + + # Adjust size policy if needed + if size_policy_expanding: + size_policy = target_widget.sizePolicy() + + if is_horizontal: + if size_policy.horizontalPolicy() not in ( + QSizePolicy.Policy.Expanding, + QSizePolicy.Policy.MinimumExpanding, + ): + size_policy.setHorizontalPolicy(QSizePolicy.Policy.Expanding) + target_widget.setSizePolicy(size_policy) + else: + if size_policy.verticalPolicy() not in ( + QSizePolicy.Policy.Expanding, + QSizePolicy.Policy.MinimumExpanding, + ): + size_policy.setVerticalPolicy(QSizePolicy.Policy.Expanding) + target_widget.setSizePolicy(size_policy) + + splitter_action = SplitterAction( + orientation="auto", + parent=self.components.toolbar, + initial_width=initial_width, + min_width=eff_min, + max_width=eff_max, + target_widget=target_widget, + ) + + self.components.add_safe(name, splitter_action) + self.add_action(name) + def add_connection(self, name: str, connection): """ Adds a connection to the bundle. diff --git a/bec_widgets/utils/toolbars/splitter.py b/bec_widgets/utils/toolbars/splitter.py new file mode 100644 index 00000000..91805fc1 --- /dev/null +++ b/bec_widgets/utils/toolbars/splitter.py @@ -0,0 +1,241 @@ +""" +Draggable splitter for toolbars to allow resizing of toolbar sections. +""" + +from typing import Literal + +from bec_qthemes import material_icon +from qtpy.QtCore import QPoint, QSize, Qt, Signal +from qtpy.QtGui import QPainter +from qtpy.QtWidgets import QSizePolicy, QWidget + + +class ResizableSpacer(QWidget): + """ + A resizable spacer widget for toolbars that can be dragged to expand/contract. + + When connected to a widget, it controls that widget's size along the spacer's + orientation (maximum width for horizontal, maximum height for vertical), + ensuring the widget stays flush against the spacer with no gaps. + + Args: + parent(QWidget | None): Parent widget. + orientation(Literal["horizontal", "vertical"]): Orientation of the spacer. + initial_width(int): Initial size of the spacer in pixels along the orientation + (width for horizontal, height for vertical). + min_target_size(int): Minimum size of the target widget when resized along the + orientation (width for horizontal, height for vertical). + max_target_size(int): Maximum size of the target widget when resized along the + orientation (width for horizontal, height for vertical). + target_widget: QWidget | None. The widget whose size along the orientation + is controlled by this spacer. + """ + + size_changed = Signal(int) + + def __init__( + self, + parent=None, + orientation: Literal["horizontal", "vertical"] = "horizontal", + initial_width: int = 10, + min_target_size: int = 6, + max_target_size: int = 500, + target_widget: QWidget = None, + ): + from bec_widgets.utils.toolbars.bundles import DEFAULT_SIZE, MAX_SIZE + + super().__init__(parent) + self._target_start_size = None + self.orientation = orientation + self._current_width = initial_width + self._min_width = min_target_size + self._max_width = max_target_size + self._dragging = False + self._drag_start_pos = QPoint() + self._target_widget = target_widget + + # Determine bounds from kwargs or target hints + is_horizontal = orientation == "horizontal" + target_min = target_widget.minimumWidth() if (target_widget and is_horizontal) else 0 + if target_widget and not is_horizontal: + target_min = target_widget.minimumHeight() + target_hint = target_widget.sizeHint().width() if (target_widget and is_horizontal) else 0 + if target_widget and not is_horizontal: + target_hint = target_widget.sizeHint().height() + target_max_hint = ( + target_widget.maximumWidth() if (target_widget and is_horizontal) else None + ) + if target_widget and not is_horizontal: + target_max_hint = target_widget.maximumHeight() + self._min_target = min_target_size if min_target_size is not None else (target_min or 6) + self._max_target = ( + max_target_size + if max_target_size is not None + else ( + target_max_hint if target_max_hint and target_max_hint < MAX_SIZE else DEFAULT_SIZE + ) + ) + + # Determine a reasonable base width and clamp to bounds + if target_widget: + current_size = target_widget.width() if is_horizontal else target_widget.height() + if current_size > 0: + self._base_width = current_size + elif target_min > 0: + self._base_width = target_min + elif target_hint > 0: + self._base_width = target_hint + else: + self._base_width = 240 + else: + self._base_width = 240 + self._base_width = max(self._min_target, min(self._max_target, self._base_width)) + + # Set size constraints - Fixed policy to prevent automatic resizing + # Match toolbar height for proper alignment + self._toolbar_height = 32 # Standard toolbar height + + if orientation == "horizontal": + self.setFixedWidth(initial_width) + self.setFixedHeight(self._toolbar_height) + self.setCursor(Qt.CursorShape.SplitHCursor) + else: + self.setFixedHeight(initial_width) + self.setFixedWidth(self._toolbar_height) + self.setCursor(Qt.CursorShape.SplitVCursor) + + self.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed) + + self.setStyleSheet( + """ + ResizableSpacer { + background-color: transparent; + margin: 0px; + padding: 0px; + border: none; + } + ResizableSpacer:hover { + background-color: rgba(100, 100, 200, 80); + } + """ + ) + + self.setContentsMargins(0, 0, 0, 0) + + if self._target_widget: + size_policy = self._target_widget.sizePolicy() + if is_horizontal: + vertical_policy = size_policy.verticalPolicy() + self._target_widget.setSizePolicy(QSizePolicy.Policy.Fixed, vertical_policy) + else: + horizontal_policy = size_policy.horizontalPolicy() + self._target_widget.setSizePolicy(horizontal_policy, QSizePolicy.Policy.Fixed) + + # Load Material icon based on orientation + icon_name = "more_vert" if orientation == "horizontal" else "more_horiz" + icon_size = 24 + self._icon = material_icon(icon_name, size=(icon_size, icon_size), convert_to_pixmap=False) + self._icon_size = icon_size + + def set_target_widget(self, widget): + """Set the widget whose size is controlled by this spacer.""" + self._target_widget = widget + if widget: + is_horizontal = self.orientation == "horizontal" + target_min = widget.minimumWidth() if is_horizontal else widget.minimumHeight() + target_hint = widget.sizeHint().width() if is_horizontal else widget.sizeHint().height() + target_max_hint = widget.maximumWidth() if is_horizontal else widget.maximumHeight() + self._min_target = self._min_target or (target_min or 6) + self._max_target = ( + self._max_target + if self._max_target is not None + else (target_max_hint if target_max_hint and target_max_hint < 10_000_000 else 400) + ) + current_size = widget.width() if is_horizontal else widget.height() + if current_size is not None and current_size > 0: + base = current_size + elif target_min is not None and target_min > 0: + base = target_min + elif target_hint is not None and target_hint > 0: + base = target_hint + else: + base = self._base_width + base = max(self._min_target, min(self._max_target, base)) + if is_horizontal: + widget.setFixedWidth(base) + else: + widget.setFixedHeight(base) + + def get_target_widget(self): + """Get the widget whose size is controlled by this spacer.""" + return self._target_widget + + def sizeHint(self): + if self.orientation == "horizontal": + return QSize(self._current_width, self._toolbar_height) + else: + return QSize(self._toolbar_height, self._current_width) + + def paintEvent(self, event): + painter = QPainter(self) + painter.setRenderHint(QPainter.RenderHint.Antialiasing) + + # Draw the Material icon centered in the widget using stored icon size + x = (self.width() - self._icon_size) // 2 + y = (self.height() - self._icon_size) // 2 + + self._icon.paint(painter, x, y, self._icon_size, self._icon_size) + painter.end() + + def mousePressEvent(self, event): + if event.button() == Qt.MouseButton.LeftButton: + self._dragging = True + self._drag_start_pos = event.globalPos() + # Store target's current width if it exists + if self._target_widget: + if self.orientation == "horizontal": + self._target_start_size = self._target_widget.width() + else: + self._target_start_size = self._target_widget.height() + + size_policy = self._target_widget.sizePolicy() + if self.orientation == "horizontal": + vertical_policy = size_policy.verticalPolicy() + self._target_widget.setSizePolicy(QSizePolicy.Policy.Fixed, vertical_policy) + self._target_widget.setFixedWidth(self._target_start_size) + else: + horizontal_policy = size_policy.horizontalPolicy() + self._target_widget.setSizePolicy(horizontal_policy, QSizePolicy.Policy.Fixed) + self._target_widget.setFixedHeight(self._target_start_size) + + event.accept() + + def mouseMoveEvent(self, event): + if self._dragging: + current_pos = event.globalPos() + delta = current_pos - self._drag_start_pos + + if self.orientation == "horizontal": + delta_pixels = delta.x() + else: + delta_pixels = delta.y() + + if self._target_widget: + new_target_size = self._target_start_size + delta_pixels + new_target_size = max(self._min_target, min(self._max_target, new_target_size)) + + if self.orientation == "horizontal": + if new_target_size != self._target_widget.width(): + self._target_widget.setFixedWidth(new_target_size) + self.size_changed.emit(new_target_size) + else: + if new_target_size != self._target_widget.height(): + self._target_widget.setFixedHeight(new_target_size) + self.size_changed.emit(new_target_size) + + event.accept() + + def mouseReleaseEvent(self, event): + if event.button() == Qt.MouseButton.LeftButton: + self._dragging = False + event.accept() diff --git a/bec_widgets/utils/toolbars/toolbar.py b/bec_widgets/utils/toolbars/toolbar.py index 4b10fba8..fc2ec7ad 100644 --- a/bec_widgets/utils/toolbars/toolbar.py +++ b/bec_widgets/utils/toolbars/toolbar.py @@ -8,10 +8,19 @@ from typing import DefaultDict, Literal from bec_lib.logger import bec_logger from qtpy.QtCore import QSize, Qt, QTimer from qtpy.QtGui import QAction, QColor -from qtpy.QtWidgets import QApplication, QLabel, QMainWindow, QMenu, QToolBar, QVBoxLayout, QWidget +from qtpy.QtWidgets import ( + QApplication, + QComboBox, + QLabel, + QMainWindow, + QMenu, + QToolBar, + QVBoxLayout, + QWidget, +) from bec_widgets.utils.colors import apply_theme, get_theme_name -from bec_widgets.utils.toolbars.actions import MaterialIconAction, ToolBarAction +from bec_widgets.utils.toolbars.actions import MaterialIconAction, ToolBarAction, WidgetAction from bec_widgets.utils.toolbars.bundles import ToolbarBundle, ToolbarComponents from bec_widgets.utils.toolbars.connections import BundleConnection @@ -406,9 +415,18 @@ class ModularToolBar(QToolBar): def update_separators(self): """ - Hide separators that are adjacent to another separator or have no non-separator actions between them. + Hide separators that are adjacent to another separator, splitters, or have no non-separator actions between them. + Splitters (ResizableSpacer) already provide visual separation, so we don't need separators next to them. """ + from bec_widgets.utils.toolbars.splitter import ResizableSpacer + toolbar_actions = self.actions() + + # Helper function to check if a widget is a splitter + def is_splitter_widget(action): + widget = self.widgetForAction(action) + return widget is not None and isinstance(widget, ResizableSpacer) + # First pass: set visibility based on surrounding non-separator actions. for i, action in enumerate(toolbar_actions): if not action.isSeparator(): @@ -423,23 +441,32 @@ class ModularToolBar(QToolBar): if toolbar_actions[j].isVisible(): next_visible = toolbar_actions[j] break - if (prev_visible is None or prev_visible.isSeparator()) and ( - next_visible is None or next_visible.isSeparator() + + # Hide separator if adjacent to another separator, splitter, or at edges + if ( + prev_visible is None + or prev_visible.isSeparator() + or is_splitter_widget(prev_visible) + ) and ( + next_visible is None + or next_visible.isSeparator() + or is_splitter_widget(next_visible) ): action.setVisible(False) else: action.setVisible(True) - # Second pass: ensure no two visible separators are adjacent. + # Second pass: ensure no two visible separators are adjacent, and no separators next to splitters. prev = None for action in toolbar_actions: - if action.isVisible() and action.isSeparator(): - if prev and prev.isSeparator(): - action.setVisible(False) + if action.isVisible(): + if action.isSeparator(): + # Hide separator if previous visible item was a separator or splitter + if prev and (prev.isSeparator() or is_splitter_widget(prev)): + action.setVisible(False) + else: + prev = action else: prev = action - else: - if action.isVisible(): - prev = action if not toolbar_actions: return @@ -481,12 +508,31 @@ if __name__ == "__main__": # pragma: no cover self.setWindowTitle("Toolbar / ToolbarBundle Demo") self.central_widget = QWidget() self.setCentralWidget(self.central_widget) - self.test_label = QLabel(text="This is a test label.") + self.test_label = QLabel(text="Drag the splitter (⋮) to resize!") self.central_widget.layout = QVBoxLayout(self.central_widget) self.central_widget.layout.addWidget(self.test_label) self.toolbar = ModularToolBar(parent=self) self.addToolBar(self.toolbar) + + # Example: Bare combobox (no container). Give it a stable starting width + self.example_combo = QComboBox(parent=self) + self.example_combo.addItems(["device_1", "device_2", "device_3"]) + + self.toolbar.components.add_safe( + "example_combo", WidgetAction(widget=self.example_combo) + ) + + # Create a bundle with the combobox and a splitter + self.bundle_combo_splitter = ToolbarBundle("example_combo", self.toolbar.components) + self.bundle_combo_splitter.add_action("example_combo") + # Add splitter; target the bare widget + self.bundle_combo_splitter.add_splitter( + name="splitter_example", target_widget=self.example_combo, min_width=100 + ) + + # Add other bundles + self.toolbar.add_bundle(self.bundle_combo_splitter) self.toolbar.add_bundle(performance_bundle(self.toolbar.components)) self.toolbar.add_bundle(plot_export_bundle(self.toolbar.components)) self.toolbar.connect_bundle( @@ -502,7 +548,9 @@ if __name__ == "__main__": # pragma: no cover text_position="under", ), ) - self.toolbar.show_bundles(["performance", "plot_export"]) + + # Show bundles - notice how performance and plot_export appear compactly after splitter! + self.toolbar.show_bundles(["example_combo", "performance", "plot_export"]) self.toolbar.get_bundle("performance").add_action("save") self.toolbar.get_bundle("performance").add_action("text") self.toolbar.refresh() diff --git a/tests/unit_tests/test_modular_toolbar.py b/tests/unit_tests/test_modular_toolbar.py index e9238f7c..307485d0 100644 --- a/tests/unit_tests/test_modular_toolbar.py +++ b/tests/unit_tests/test_modular_toolbar.py @@ -16,6 +16,7 @@ from bec_widgets.utils.toolbars.actions import ( WidgetAction, ) from bec_widgets.utils.toolbars.bundles import ToolbarBundle +from bec_widgets.utils.toolbars.splitter import ResizableSpacer from bec_widgets.utils.toolbars.toolbar import ModularToolBar @@ -612,3 +613,129 @@ def test_remove_nonexistent_bundle(toolbar_fixture): with pytest.raises(KeyError) as excinfo: toolbar.remove_bundle("nonexistent_bundle") excinfo.match("Bundle with name 'nonexistent_bundle' does not exist.") + + +def _find_splitter_widget(toolbar: ModularToolBar) -> ResizableSpacer: + for action in toolbar.actions(): + widget = toolbar.widgetForAction(action) + if isinstance(widget, ResizableSpacer): + return widget + raise AssertionError("ResizableSpacer not found in toolbar actions.") + + +def test_add_splitter_auto_orientation(toolbar_fixture, qtbot): + toolbar = toolbar_fixture + combo = QComboBox() + combo.addItems(["One", "Two", "Three"]) + combo_action = WidgetAction(label="Combo:", widget=combo) + toolbar.components.add_safe("combo_action", combo_action) + + bundle = toolbar.new_bundle("splitter_bundle") + bundle.add_action("combo_action") + bundle.add_splitter(name="splitter", target_widget=combo, min_width=80) + + toolbar.show_bundles(["splitter_bundle"]) + qtbot.wait(50) + + splitter_widget = _find_splitter_widget(toolbar) + if toolbar.orientation() == Qt.Horizontal: + assert splitter_widget.orientation == "horizontal" + assert splitter_widget.cursor().shape() == Qt.CursorShape.SplitHCursor + else: + assert splitter_widget.orientation == "vertical" + assert splitter_widget.cursor().shape() == Qt.CursorShape.SplitVCursor + + +def test_separator_hidden_next_to_splitter(toolbar_fixture, material_icon_action): + toolbar = toolbar_fixture + combo = QComboBox() + combo.addItems(["One", "Two", "Three"]) + combo_action = WidgetAction(label="Combo:", widget=combo) + toolbar.components.add_safe("combo_action", combo_action) + + bundle_with_splitter = toolbar.new_bundle("bundle_with_splitter") + bundle_with_splitter.add_action("combo_action") + bundle_with_splitter.add_splitter(name="splitter", target_widget=combo, min_width=80) + + toolbar.components.add_safe("icon_action", material_icon_action) + bundle_next = toolbar.new_bundle("bundle_next") + bundle_next.add_action("icon_action") + + toolbar.show_bundles(["bundle_with_splitter", "bundle_next"]) + + actions = toolbar.actions() + splitter_index = None + for idx, action in enumerate(actions): + if isinstance(toolbar.widgetForAction(action), ResizableSpacer): + splitter_index = idx + break + assert splitter_index is not None + + separator_action = actions[splitter_index + 1] + assert separator_action.isSeparator() + assert not separator_action.isVisible() + + +def test_splitter_action_set_target_widget_after_show(toolbar_fixture, qtbot): + toolbar = toolbar_fixture + combo = QComboBox() + combo.addItems(["One", "Two", "Three"]) + combo_action = WidgetAction(label="Combo:", widget=combo) + toolbar.components.add_safe("combo_action", combo_action) + + bundle = toolbar.new_bundle("splitter_bundle") + bundle.add_action("combo_action") + bundle.add_splitter(name="splitter", min_width=80, max_width=160) + + toolbar.show_bundles(["splitter_bundle"]) + qtbot.wait(200) + + splitter_action = toolbar.components.get_action("splitter") + splitter_action.set_target_widget(combo) + + splitter_widget = _find_splitter_widget(toolbar) + if hasattr(splitter_widget, "get_target_widget"): + assert splitter_widget.get_target_widget() is combo + if splitter_widget.orientation == "horizontal": + assert 80 <= combo.width() <= 160 + else: + assert 80 <= combo.height() <= 160 + + +@pytest.mark.parametrize( + "orientation, delta", [("horizontal", QPoint(40, 0)), ("vertical", QPoint(0, 40))] +) +def test_splitter_mouse_events_resize_target(qtbot, orientation, delta): + from qtpy.QtWidgets import QVBoxLayout + + parent = QWidget() + layout = QVBoxLayout(parent) + layout.setContentsMargins(0, 0, 0, 0) + + target = QComboBox() + target.addItems(["One", "Two", "Three"]) + layout.addWidget(target) + + splitter = ResizableSpacer( + parent=parent, + orientation=orientation, + initial_width=10, + min_target_size=60, + max_target_size=200, + target_widget=target, + ) + layout.addWidget(splitter) + + qtbot.addWidget(parent) + parent.show() + qtbot.waitExposed(parent) + + start_size = target.width() if orientation == "horizontal" else target.height() + + qtbot.mousePress(splitter, Qt.LeftButton, pos=splitter.rect().center()) + qtbot.mouseMove(splitter, splitter.rect().center() + delta) + qtbot.mouseRelease(splitter, Qt.LeftButton, pos=splitter.rect().center() + delta) + + end_size = target.width() if orientation == "horizontal" else target.height() + assert end_size != start_size + assert 60 <= end_size <= 200