mirror of
https://github.com/bec-project/bec_widgets.git
synced 2026-03-04 16:02:51 +01:00
feat(toolbar): splitter action added
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
241
bec_widgets/utils/toolbars/splitter.py
Normal file
241
bec_widgets/utils/toolbars/splitter.py
Normal file
@@ -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()
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user