mirror of
https://github.com/bec-project/bec_widgets.git
synced 2025-07-13 11:11:49 +02:00
refactor(toolbar): split toolbar into components, bundles and connections
This commit is contained in:
2
.github/workflows/pytest-matrix.yml
vendored
2
.github/workflows/pytest-matrix.yml
vendored
@ -56,4 +56,4 @@ jobs:
|
|||||||
- name: Run Pytest
|
- name: Run Pytest
|
||||||
run: |
|
run: |
|
||||||
pip install pytest pytest-random-order
|
pip install pytest pytest-random-order
|
||||||
pytest -v --maxfail=2 --junitxml=report.xml --random-order ./tests/unit_tests
|
pytest -v --junitxml=report.xml --random-order ./tests/unit_tests
|
||||||
|
@ -27,7 +27,7 @@ from bec_widgets.utils.error_popups import SafeSlot
|
|||||||
from bec_widgets.utils.name_utils import pascal_to_snake
|
from bec_widgets.utils.name_utils import pascal_to_snake
|
||||||
from bec_widgets.utils.plugin_utils import get_plugin_auto_updates
|
from bec_widgets.utils.plugin_utils import get_plugin_auto_updates
|
||||||
from bec_widgets.utils.round_frame import RoundedFrame
|
from bec_widgets.utils.round_frame import RoundedFrame
|
||||||
from bec_widgets.utils.toolbar import ModularToolBar
|
from bec_widgets.utils.toolbars.toolbar import ModularToolBar
|
||||||
from bec_widgets.utils.ui_loader import UILoader
|
from bec_widgets.utils.ui_loader import UILoader
|
||||||
from bec_widgets.widgets.containers.auto_update.auto_updates import AutoUpdates
|
from bec_widgets.widgets.containers.auto_update.auto_updates import AutoUpdates
|
||||||
from bec_widgets.widgets.containers.dock.dock_area import BECDockArea
|
from bec_widgets.widgets.containers.dock.dock_area import BECDockArea
|
||||||
|
@ -16,7 +16,8 @@ from qtpy.QtWidgets import (
|
|||||||
QWidget,
|
QWidget,
|
||||||
)
|
)
|
||||||
|
|
||||||
from bec_widgets.utils.toolbar import MaterialIconAction, ModularToolBar
|
from bec_widgets.utils.toolbars.bundles import ToolbarBundle
|
||||||
|
from bec_widgets.utils.toolbars.toolbar import MaterialIconAction, ModularToolBar
|
||||||
|
|
||||||
|
|
||||||
class SidePanel(QWidget):
|
class SidePanel(QWidget):
|
||||||
@ -61,7 +62,7 @@ class SidePanel(QWidget):
|
|||||||
self.main_layout.setContentsMargins(0, 0, 0, 0)
|
self.main_layout.setContentsMargins(0, 0, 0, 0)
|
||||||
self.main_layout.setSpacing(0)
|
self.main_layout.setSpacing(0)
|
||||||
|
|
||||||
self.toolbar = ModularToolBar(parent=self, target_widget=self, orientation="vertical")
|
self.toolbar = ModularToolBar(parent=self, orientation="vertical")
|
||||||
|
|
||||||
self.container = QWidget()
|
self.container = QWidget()
|
||||||
self.container.layout = QVBoxLayout(self.container)
|
self.container.layout = QVBoxLayout(self.container)
|
||||||
@ -92,7 +93,7 @@ class SidePanel(QWidget):
|
|||||||
self.main_layout.setContentsMargins(0, 0, 0, 0)
|
self.main_layout.setContentsMargins(0, 0, 0, 0)
|
||||||
self.main_layout.setSpacing(0)
|
self.main_layout.setSpacing(0)
|
||||||
|
|
||||||
self.toolbar = ModularToolBar(parent=self, target_widget=self, orientation="horizontal")
|
self.toolbar = ModularToolBar(parent=self, orientation="horizontal")
|
||||||
|
|
||||||
self.container = QWidget()
|
self.container = QWidget()
|
||||||
self.container.layout = QVBoxLayout(self.container)
|
self.container.layout = QVBoxLayout(self.container)
|
||||||
@ -288,8 +289,16 @@ class SidePanel(QWidget):
|
|||||||
|
|
||||||
# Add an action to the toolbar if action_id, icon_name, and tooltip are provided
|
# Add an action to the toolbar if action_id, icon_name, and tooltip are provided
|
||||||
if action_id is not None and icon_name is not None and tooltip is not None:
|
if action_id is not None and icon_name is not None and tooltip is not None:
|
||||||
action = MaterialIconAction(icon_name=icon_name, tooltip=tooltip, checkable=True)
|
action = MaterialIconAction(
|
||||||
self.toolbar.add_action(action_id, action, target_widget=self)
|
icon_name=icon_name, tooltip=tooltip, checkable=True, parent=self
|
||||||
|
)
|
||||||
|
self.toolbar.components.add_safe(action_id, action)
|
||||||
|
bundle = ToolbarBundle(action_id, self.toolbar.components)
|
||||||
|
bundle.add_action(action_id)
|
||||||
|
self.toolbar.add_bundle(bundle)
|
||||||
|
shown_bundles = self.toolbar.shown_bundles
|
||||||
|
shown_bundles.append(action_id)
|
||||||
|
self.toolbar.show_bundles(shown_bundles)
|
||||||
|
|
||||||
def on_action_toggled(checked: bool):
|
def on_action_toggled(checked: bool):
|
||||||
if self.switching_actions:
|
if self.switching_actions:
|
||||||
|
File diff suppressed because it is too large
Load Diff
524
bec_widgets/utils/toolbars/actions.py
Normal file
524
bec_widgets/utils/toolbars/actions.py
Normal file
@ -0,0 +1,524 @@
|
|||||||
|
# pylint: disable=no-name-in-module
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
from contextlib import contextmanager
|
||||||
|
from typing import Dict, Literal
|
||||||
|
|
||||||
|
from bec_lib.device import ReadoutPriority
|
||||||
|
from bec_lib.logger import bec_logger
|
||||||
|
from bec_qthemes._icon.material_icons import material_icon
|
||||||
|
from qtpy.QtCore import QSize, Qt, QTimer
|
||||||
|
from qtpy.QtGui import QAction, QColor, QIcon
|
||||||
|
from qtpy.QtWidgets import (
|
||||||
|
QApplication,
|
||||||
|
QComboBox,
|
||||||
|
QHBoxLayout,
|
||||||
|
QLabel,
|
||||||
|
QMenu,
|
||||||
|
QSizePolicy,
|
||||||
|
QStyledItemDelegate,
|
||||||
|
QToolBar,
|
||||||
|
QToolButton,
|
||||||
|
QWidget,
|
||||||
|
)
|
||||||
|
|
||||||
|
import bec_widgets
|
||||||
|
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
|
||||||
|
|
||||||
|
logger = bec_logger.logger
|
||||||
|
|
||||||
|
MODULE_PATH = os.path.dirname(bec_widgets.__file__)
|
||||||
|
|
||||||
|
|
||||||
|
class NoCheckDelegate(QStyledItemDelegate):
|
||||||
|
"""To reduce space in combo boxes by removing the checkmark."""
|
||||||
|
|
||||||
|
def initStyleOption(self, option, index):
|
||||||
|
super().initStyleOption(option, index)
|
||||||
|
# Remove any check indicator
|
||||||
|
option.checkState = Qt.Unchecked
|
||||||
|
|
||||||
|
|
||||||
|
class LongPressToolButton(QToolButton):
|
||||||
|
def __init__(self, *args, long_press_threshold=500, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.long_press_threshold = long_press_threshold
|
||||||
|
self._long_press_timer = QTimer(self)
|
||||||
|
self._long_press_timer.setSingleShot(True)
|
||||||
|
self._long_press_timer.timeout.connect(self.handleLongPress)
|
||||||
|
self._pressed = False
|
||||||
|
self._longPressed = False
|
||||||
|
|
||||||
|
def mousePressEvent(self, event):
|
||||||
|
self._pressed = True
|
||||||
|
self._longPressed = False
|
||||||
|
self._long_press_timer.start(self.long_press_threshold)
|
||||||
|
super().mousePressEvent(event)
|
||||||
|
|
||||||
|
def mouseReleaseEvent(self, event):
|
||||||
|
self._pressed = False
|
||||||
|
if self._longPressed:
|
||||||
|
self._longPressed = False
|
||||||
|
self._long_press_timer.stop()
|
||||||
|
event.accept() # Prevent normal click action after a long press
|
||||||
|
return
|
||||||
|
self._long_press_timer.stop()
|
||||||
|
super().mouseReleaseEvent(event)
|
||||||
|
|
||||||
|
def handleLongPress(self):
|
||||||
|
if self._pressed:
|
||||||
|
self._longPressed = True
|
||||||
|
self.showMenu()
|
||||||
|
|
||||||
|
|
||||||
|
class ToolBarAction(ABC):
|
||||||
|
"""
|
||||||
|
Abstract base class for toolbar actions.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
icon_path (str, optional): The name of the icon file from `assets/toolbar_icons`. Defaults to None.
|
||||||
|
tooltip (str, optional): The tooltip for the action. Defaults to None.
|
||||||
|
checkable (bool, optional): Whether the action is checkable. Defaults to False.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, icon_path: str = None, tooltip: str = None, checkable: bool = False):
|
||||||
|
self.icon_path = (
|
||||||
|
os.path.join(MODULE_PATH, "assets", "toolbar_icons", icon_path) if icon_path else None
|
||||||
|
)
|
||||||
|
self.tooltip = tooltip
|
||||||
|
self.checkable = checkable
|
||||||
|
self.action = None
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def add_to_toolbar(self, toolbar: QToolBar, target: QWidget):
|
||||||
|
"""Adds an action or widget to a toolbar.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
toolbar (QToolBar): The toolbar to add the action or widget to.
|
||||||
|
target (QWidget): The target widget for the action.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def cleanup(self):
|
||||||
|
"""Cleans up the action, if necessary."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class SeparatorAction(ToolBarAction):
|
||||||
|
"""Separator action for the toolbar."""
|
||||||
|
|
||||||
|
def add_to_toolbar(self, toolbar: QToolBar, target: QWidget):
|
||||||
|
toolbar.addSeparator()
|
||||||
|
|
||||||
|
|
||||||
|
class QtIconAction(ToolBarAction):
|
||||||
|
def __init__(self, standard_icon, tooltip=None, checkable=False, parent=None):
|
||||||
|
super().__init__(icon_path=None, tooltip=tooltip, checkable=checkable)
|
||||||
|
self.standard_icon = standard_icon
|
||||||
|
self.icon = QApplication.style().standardIcon(standard_icon)
|
||||||
|
self.action = QAction(icon=self.icon, text=self.tooltip, parent=parent)
|
||||||
|
self.action.setCheckable(self.checkable)
|
||||||
|
|
||||||
|
def add_to_toolbar(self, toolbar, target):
|
||||||
|
toolbar.addAction(self.action)
|
||||||
|
|
||||||
|
def get_icon(self):
|
||||||
|
return self.icon
|
||||||
|
|
||||||
|
|
||||||
|
class MaterialIconAction(ToolBarAction):
|
||||||
|
"""
|
||||||
|
Action with a Material icon for the toolbar.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
icon_name (str, optional): The name of the Material icon. Defaults to None.
|
||||||
|
tooltip (str, optional): The tooltip for the action. Defaults to None.
|
||||||
|
checkable (bool, optional): Whether the action is checkable. Defaults to False.
|
||||||
|
filled (bool, optional): Whether the icon is filled. Defaults to False.
|
||||||
|
color (str | tuple | QColor | dict[Literal["dark", "light"], str] | None, optional): The color of the icon.
|
||||||
|
Defaults to None.
|
||||||
|
parent (QWidget or None, optional): Parent widget for the underlying QAction.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
icon_name: str = None,
|
||||||
|
tooltip: str = None,
|
||||||
|
checkable: bool = False,
|
||||||
|
filled: bool = False,
|
||||||
|
color: str | tuple | QColor | dict[Literal["dark", "light"], str] | None = None,
|
||||||
|
parent=None,
|
||||||
|
):
|
||||||
|
super().__init__(icon_path=None, tooltip=tooltip, checkable=checkable)
|
||||||
|
self.icon_name = icon_name
|
||||||
|
self.filled = filled
|
||||||
|
self.color = color
|
||||||
|
# Generate the icon using the material_icon helper
|
||||||
|
self.icon = material_icon(
|
||||||
|
self.icon_name,
|
||||||
|
size=(20, 20),
|
||||||
|
convert_to_pixmap=False,
|
||||||
|
filled=self.filled,
|
||||||
|
color=self.color,
|
||||||
|
)
|
||||||
|
if parent is None:
|
||||||
|
logger.warning(
|
||||||
|
"MaterialIconAction was created without a parent. Please consider adding one. Using None as parent may cause issues."
|
||||||
|
)
|
||||||
|
self.action = QAction(icon=self.icon, text=self.tooltip, parent=parent)
|
||||||
|
self.action.setCheckable(self.checkable)
|
||||||
|
|
||||||
|
def add_to_toolbar(self, toolbar: QToolBar, target: QWidget):
|
||||||
|
"""
|
||||||
|
Adds the action to the toolbar.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
toolbar(QToolBar): The toolbar to add the action to.
|
||||||
|
target(QWidget): The target widget for the action.
|
||||||
|
"""
|
||||||
|
toolbar.addAction(self.action)
|
||||||
|
|
||||||
|
def get_icon(self):
|
||||||
|
"""
|
||||||
|
Returns the icon for the action.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
QIcon: The icon for the action.
|
||||||
|
"""
|
||||||
|
return self.icon
|
||||||
|
|
||||||
|
|
||||||
|
class DeviceSelectionAction(ToolBarAction):
|
||||||
|
"""
|
||||||
|
Action for selecting a device in a combobox.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
label (str): The label for the combobox.
|
||||||
|
device_combobox (DeviceComboBox): The combobox for selecting the device.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, label: str | None = None, device_combobox=None):
|
||||||
|
super().__init__()
|
||||||
|
self.label = label
|
||||||
|
self.device_combobox = device_combobox
|
||||||
|
self.device_combobox.currentIndexChanged.connect(lambda: self.set_combobox_style("#ffa700"))
|
||||||
|
|
||||||
|
def add_to_toolbar(self, toolbar, target):
|
||||||
|
widget = QWidget(parent=target)
|
||||||
|
layout = QHBoxLayout(widget)
|
||||||
|
layout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
layout.setSpacing(0)
|
||||||
|
if self.label is not None:
|
||||||
|
label = QLabel(text=f"{self.label}", parent=target)
|
||||||
|
layout.addWidget(label)
|
||||||
|
if self.device_combobox is not None:
|
||||||
|
layout.addWidget(self.device_combobox)
|
||||||
|
toolbar.addWidget(widget)
|
||||||
|
|
||||||
|
def set_combobox_style(self, color: str):
|
||||||
|
self.device_combobox.setStyleSheet(f"QComboBox {{ background-color: {color}; }}")
|
||||||
|
|
||||||
|
|
||||||
|
class SwitchableToolBarAction(ToolBarAction):
|
||||||
|
"""
|
||||||
|
A split toolbar action that combines a main action and a drop-down menu for additional actions.
|
||||||
|
|
||||||
|
The main button displays the currently selected action's icon and tooltip. Clicking on the main button
|
||||||
|
triggers that action. Clicking on the drop-down arrow displays a menu with alternative actions. When an
|
||||||
|
alternative action is selected, it becomes the new default and its callback is immediately executed.
|
||||||
|
|
||||||
|
This design mimics the behavior seen in Adobe Photoshop or Affinity Designer toolbars.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
actions (dict): A dictionary mapping a unique key to a ToolBarAction instance.
|
||||||
|
initial_action (str, optional): The key of the initial default action. If not provided, the first action is used.
|
||||||
|
tooltip (str, optional): An optional tooltip for the split action; if provided, it overrides the default action's tooltip.
|
||||||
|
checkable (bool, optional): Whether the action is checkable. Defaults to True.
|
||||||
|
parent (QWidget, optional): Parent widget for the underlying QAction.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
actions: Dict[str, ToolBarAction],
|
||||||
|
initial_action: str = None,
|
||||||
|
tooltip: str = None,
|
||||||
|
checkable: bool = True,
|
||||||
|
default_state_checked: bool = False,
|
||||||
|
parent=None,
|
||||||
|
):
|
||||||
|
super().__init__(icon_path=None, tooltip=tooltip, checkable=checkable)
|
||||||
|
self.actions = actions
|
||||||
|
self.current_key = initial_action if initial_action is not None else next(iter(actions))
|
||||||
|
self.parent = parent
|
||||||
|
self.checkable = checkable
|
||||||
|
self.default_state_checked = default_state_checked
|
||||||
|
self.main_button = None
|
||||||
|
self.menu_actions: Dict[str, QAction] = {}
|
||||||
|
|
||||||
|
def add_to_toolbar(self, toolbar: QToolBar, target: QWidget):
|
||||||
|
"""
|
||||||
|
Adds the split action to the toolbar.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
toolbar (QToolBar): The toolbar to add the action to.
|
||||||
|
target (QWidget): The target widget for the action.
|
||||||
|
"""
|
||||||
|
self.main_button = LongPressToolButton(toolbar)
|
||||||
|
self.main_button.setPopupMode(QToolButton.MenuButtonPopup)
|
||||||
|
self.main_button.setCheckable(self.checkable)
|
||||||
|
default_action = self.actions[self.current_key]
|
||||||
|
self.main_button.setIcon(default_action.get_icon())
|
||||||
|
self.main_button.setToolTip(default_action.tooltip)
|
||||||
|
self.main_button.clicked.connect(self._trigger_current_action)
|
||||||
|
menu = QMenu(self.main_button)
|
||||||
|
for key, action_obj in self.actions.items():
|
||||||
|
menu_action = QAction(
|
||||||
|
icon=action_obj.get_icon(), text=action_obj.tooltip, parent=self.main_button
|
||||||
|
)
|
||||||
|
menu_action.setIconVisibleInMenu(True)
|
||||||
|
menu_action.setCheckable(self.checkable)
|
||||||
|
menu_action.setChecked(key == self.current_key)
|
||||||
|
menu_action.triggered.connect(lambda checked, k=key: self.set_default_action(k))
|
||||||
|
menu.addAction(menu_action)
|
||||||
|
self.main_button.setMenu(menu)
|
||||||
|
if self.default_state_checked:
|
||||||
|
self.main_button.setChecked(True)
|
||||||
|
self.action = toolbar.addWidget(self.main_button)
|
||||||
|
|
||||||
|
def _trigger_current_action(self):
|
||||||
|
"""
|
||||||
|
Triggers the current action associated with the main button.
|
||||||
|
"""
|
||||||
|
action_obj = self.actions[self.current_key]
|
||||||
|
action_obj.action.trigger()
|
||||||
|
|
||||||
|
def set_default_action(self, key: str):
|
||||||
|
"""
|
||||||
|
Sets the default action for the split action.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
key(str): The key of the action to set as default.
|
||||||
|
"""
|
||||||
|
if self.main_button is None:
|
||||||
|
return
|
||||||
|
self.current_key = key
|
||||||
|
new_action = self.actions[self.current_key]
|
||||||
|
self.main_button.setIcon(new_action.get_icon())
|
||||||
|
self.main_button.setToolTip(new_action.tooltip)
|
||||||
|
# Update check state of menu items
|
||||||
|
for k, menu_act in self.actions.items():
|
||||||
|
menu_act.action.setChecked(False)
|
||||||
|
new_action.action.trigger()
|
||||||
|
# Active action chosen from menu is always checked, uncheck through main button
|
||||||
|
if self.checkable:
|
||||||
|
new_action.action.setChecked(True)
|
||||||
|
self.main_button.setChecked(True)
|
||||||
|
|
||||||
|
def block_all_signals(self, block: bool = True):
|
||||||
|
"""
|
||||||
|
Blocks or unblocks all signals for the actions in the toolbar.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
block (bool): Whether to block signals. Defaults to True.
|
||||||
|
"""
|
||||||
|
if self.main_button is not None:
|
||||||
|
self.main_button.blockSignals(block)
|
||||||
|
|
||||||
|
for action in self.actions.values():
|
||||||
|
action.action.blockSignals(block)
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def signal_blocker(self):
|
||||||
|
"""
|
||||||
|
Context manager to block signals for all actions in the toolbar.
|
||||||
|
"""
|
||||||
|
self.block_all_signals(True)
|
||||||
|
try:
|
||||||
|
yield
|
||||||
|
finally:
|
||||||
|
self.block_all_signals(False)
|
||||||
|
|
||||||
|
def set_state_all(self, state: bool):
|
||||||
|
"""
|
||||||
|
Uncheck all actions in the toolbar.
|
||||||
|
"""
|
||||||
|
for action in self.actions.values():
|
||||||
|
action.action.setChecked(state)
|
||||||
|
if self.main_button is None:
|
||||||
|
return
|
||||||
|
self.main_button.setChecked(state)
|
||||||
|
|
||||||
|
def get_icon(self) -> QIcon:
|
||||||
|
return self.actions[self.current_key].get_icon()
|
||||||
|
|
||||||
|
|
||||||
|
class WidgetAction(ToolBarAction):
|
||||||
|
"""
|
||||||
|
Action for adding any widget to the toolbar.
|
||||||
|
Please note that the injected widget must be life-cycled by the parent widget,
|
||||||
|
i.e., the widget must be properly cleaned up outside of this action. The WidgetAction
|
||||||
|
will not perform any cleanup on the widget itself, only on the container that holds it.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
label (str|None): The label for the widget.
|
||||||
|
widget (QWidget): The widget to be added to the toolbar.
|
||||||
|
adjust_size (bool): Whether to adjust the size of the widget based on its contents. Defaults to True.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
label: str | None = None,
|
||||||
|
widget: QWidget = None,
|
||||||
|
adjust_size: bool = True,
|
||||||
|
parent=None,
|
||||||
|
):
|
||||||
|
super().__init__(icon_path=None, tooltip=label, checkable=False)
|
||||||
|
self.label = label
|
||||||
|
self.widget = widget
|
||||||
|
self.container = None
|
||||||
|
self.adjust_size = adjust_size
|
||||||
|
|
||||||
|
def add_to_toolbar(self, toolbar: QToolBar, target: QWidget):
|
||||||
|
"""
|
||||||
|
Adds the widget to the toolbar.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
toolbar (QToolBar): The toolbar to add the widget to.
|
||||||
|
target (QWidget): The target widget for the action.
|
||||||
|
"""
|
||||||
|
self.container = QWidget(parent=target)
|
||||||
|
layout = QHBoxLayout(self.container)
|
||||||
|
layout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
layout.setSpacing(0)
|
||||||
|
|
||||||
|
if self.label is not None:
|
||||||
|
label_widget = QLabel(text=f"{self.label}", parent=target)
|
||||||
|
label_widget.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Fixed)
|
||||||
|
label_widget.setAlignment(Qt.AlignVCenter | Qt.AlignRight)
|
||||||
|
layout.addWidget(label_widget)
|
||||||
|
|
||||||
|
if isinstance(self.widget, QComboBox) and self.adjust_size:
|
||||||
|
self.widget.setSizeAdjustPolicy(QComboBox.AdjustToContents)
|
||||||
|
|
||||||
|
size_policy = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
|
||||||
|
self.widget.setSizePolicy(size_policy)
|
||||||
|
|
||||||
|
self.widget.setMinimumWidth(self.calculate_minimum_width(self.widget))
|
||||||
|
|
||||||
|
layout.addWidget(self.widget)
|
||||||
|
|
||||||
|
toolbar.addWidget(self.container)
|
||||||
|
# Store the container as the action to allow toggling visibility.
|
||||||
|
self.action = self.container
|
||||||
|
|
||||||
|
def cleanup(self):
|
||||||
|
"""
|
||||||
|
Cleans up the action by closing and deleting the container widget.
|
||||||
|
This method will be called automatically when the toolbar is cleaned up.
|
||||||
|
"""
|
||||||
|
if self.container is not None:
|
||||||
|
self.container.close()
|
||||||
|
self.container.deleteLater()
|
||||||
|
return super().cleanup()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def calculate_minimum_width(combo_box: QComboBox) -> int:
|
||||||
|
font_metrics = combo_box.fontMetrics()
|
||||||
|
max_width = max(font_metrics.width(combo_box.itemText(i)) for i in range(combo_box.count()))
|
||||||
|
return max_width + 60
|
||||||
|
|
||||||
|
|
||||||
|
class ExpandableMenuAction(ToolBarAction):
|
||||||
|
"""
|
||||||
|
Action for an expandable menu in the toolbar.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
label (str): The label for the menu.
|
||||||
|
actions (dict): A dictionary of actions to populate the menu.
|
||||||
|
icon_path (str, optional): The path to the icon file. Defaults to None.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, label: str, actions: dict, icon_path: str = None):
|
||||||
|
super().__init__(icon_path, label)
|
||||||
|
self.actions = actions
|
||||||
|
|
||||||
|
def add_to_toolbar(self, toolbar: QToolBar, target: QWidget):
|
||||||
|
button = QToolButton(toolbar)
|
||||||
|
if self.icon_path:
|
||||||
|
button.setIcon(QIcon(self.icon_path))
|
||||||
|
button.setText(self.tooltip)
|
||||||
|
button.setPopupMode(QToolButton.InstantPopup)
|
||||||
|
button.setStyleSheet(
|
||||||
|
"""
|
||||||
|
QToolButton {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
QMenu {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
menu = QMenu(button)
|
||||||
|
for action_container in self.actions.values():
|
||||||
|
action: QAction = action_container.action
|
||||||
|
action.setIconVisibleInMenu(True)
|
||||||
|
if action_container.icon_path:
|
||||||
|
icon = QIcon()
|
||||||
|
icon.addFile(action_container.icon_path, size=QSize(20, 20))
|
||||||
|
action.setIcon(icon)
|
||||||
|
elif hasattr(action, "get_icon") and callable(action_container.get_icon):
|
||||||
|
sub_icon = action_container.get_icon()
|
||||||
|
if sub_icon and not sub_icon.isNull():
|
||||||
|
action.setIcon(sub_icon)
|
||||||
|
action.setCheckable(action_container.checkable)
|
||||||
|
menu.addAction(action)
|
||||||
|
button.setMenu(menu)
|
||||||
|
toolbar.addWidget(button)
|
||||||
|
|
||||||
|
|
||||||
|
class DeviceComboBoxAction(WidgetAction):
|
||||||
|
"""
|
||||||
|
Action for a device selection combobox in the toolbar.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
label (str): The label for the combobox.
|
||||||
|
device_combobox (QComboBox): The combobox for selecting the device.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
target_widget: QWidget,
|
||||||
|
device_filter: list[BECDeviceFilter] | None = None,
|
||||||
|
readout_priority_filter: (
|
||||||
|
str | ReadoutPriority | list[str] | list[ReadoutPriority] | None
|
||||||
|
) = None,
|
||||||
|
tooltip: str | None = None,
|
||||||
|
add_empty_item: bool = False,
|
||||||
|
no_check_delegate: bool = False,
|
||||||
|
):
|
||||||
|
self.combobox = DeviceComboBox(
|
||||||
|
parent=target_widget,
|
||||||
|
device_filter=device_filter,
|
||||||
|
readout_priority_filter=readout_priority_filter,
|
||||||
|
)
|
||||||
|
super().__init__(widget=self.combobox, adjust_size=False)
|
||||||
|
|
||||||
|
if add_empty_item:
|
||||||
|
self.combobox.addItem("", None)
|
||||||
|
self.combobox.setCurrentText("")
|
||||||
|
if tooltip is not None:
|
||||||
|
self.combobox.setToolTip(tooltip)
|
||||||
|
if no_check_delegate:
|
||||||
|
self.combobox.setItemDelegate(NoCheckDelegate(self.combobox))
|
||||||
|
|
||||||
|
def cleanup(self):
|
||||||
|
"""
|
||||||
|
Cleans up the action by closing and deleting the combobox widget.
|
||||||
|
This method will be called automatically when the toolbar is cleaned up.
|
||||||
|
"""
|
||||||
|
if self.combobox is not None:
|
||||||
|
self.combobox.close()
|
||||||
|
self.combobox.deleteLater()
|
||||||
|
return super().cleanup()
|
244
bec_widgets/utils/toolbars/bundles.py
Normal file
244
bec_widgets/utils/toolbars/bundles.py
Normal file
@ -0,0 +1,244 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections import defaultdict
|
||||||
|
from typing import TYPE_CHECKING, DefaultDict
|
||||||
|
from weakref import ReferenceType
|
||||||
|
|
||||||
|
import louie
|
||||||
|
from bec_lib.logger import bec_logger
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
from bec_widgets.utils.toolbars.actions import SeparatorAction, ToolBarAction
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from bec_widgets.utils.toolbars.connections import BundleConnection
|
||||||
|
from bec_widgets.utils.toolbars.toolbar import ModularToolBar
|
||||||
|
|
||||||
|
logger = bec_logger.logger
|
||||||
|
|
||||||
|
|
||||||
|
class ActionInfo(BaseModel):
|
||||||
|
action: ToolBarAction
|
||||||
|
toolbar_bundle: ToolbarBundle | None = None
|
||||||
|
|
||||||
|
model_config = {"arbitrary_types_allowed": True}
|
||||||
|
|
||||||
|
|
||||||
|
class ToolbarComponents:
|
||||||
|
def __init__(self, toolbar: ModularToolBar):
|
||||||
|
"""
|
||||||
|
Initializes the toolbar components.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
toolbar (ModularToolBar): The toolbar to which the components will be added.
|
||||||
|
"""
|
||||||
|
self.toolbar = toolbar
|
||||||
|
|
||||||
|
self._components: dict[str, ActionInfo] = {}
|
||||||
|
self.add("separator", SeparatorAction())
|
||||||
|
|
||||||
|
def add(self, name: str, component: ToolBarAction):
|
||||||
|
"""
|
||||||
|
Adds a component to the toolbar.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
component (ToolBarAction): The component to add.
|
||||||
|
"""
|
||||||
|
if name in self._components:
|
||||||
|
raise ValueError(f"Component with name '{name}' already exists.")
|
||||||
|
self._components[name] = ActionInfo(action=component, toolbar_bundle=None)
|
||||||
|
|
||||||
|
def add_safe(self, name: str, component: ToolBarAction):
|
||||||
|
"""
|
||||||
|
Adds a component to the toolbar, ensuring it does not already exist.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name (str): The name of the component.
|
||||||
|
component (ToolBarAction): The component to add.
|
||||||
|
"""
|
||||||
|
if self.exists(name):
|
||||||
|
logger.info(f"Component with name '{name}' already exists. Skipping addition.")
|
||||||
|
return
|
||||||
|
self.add(name, component)
|
||||||
|
|
||||||
|
def exists(self, name: str) -> bool:
|
||||||
|
"""
|
||||||
|
Checks if a component exists in the toolbar.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name (str): The name of the component to check.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if the component exists, False otherwise.
|
||||||
|
"""
|
||||||
|
return name in self._components
|
||||||
|
|
||||||
|
def get_action_reference(self, name: str) -> ReferenceType[ToolBarAction]:
|
||||||
|
"""
|
||||||
|
Retrieves a component by name.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name (str): The name of the component to retrieve.
|
||||||
|
|
||||||
|
"""
|
||||||
|
if not self.exists(name):
|
||||||
|
raise KeyError(f"Component with name '{name}' does not exist.")
|
||||||
|
return louie.saferef.safe_ref(self._components[name].action)
|
||||||
|
|
||||||
|
def get_action(self, name: str) -> ToolBarAction:
|
||||||
|
"""
|
||||||
|
Retrieves a component by name.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name (str): The name of the component to retrieve.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
ToolBarAction: The action associated with the given name.
|
||||||
|
"""
|
||||||
|
if not self.exists(name):
|
||||||
|
raise KeyError(
|
||||||
|
f"Component with name '{name}' does not exist. The following components are available: {list(self._components.keys())}"
|
||||||
|
)
|
||||||
|
return self._components[name].action
|
||||||
|
|
||||||
|
def set_bundle(self, name: str, bundle: ToolbarBundle):
|
||||||
|
"""
|
||||||
|
Sets the bundle for a component.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name (str): The name of the component.
|
||||||
|
bundle (ToolbarBundle): The bundle to set.
|
||||||
|
"""
|
||||||
|
if not self.exists(name):
|
||||||
|
raise KeyError(f"Component with name '{name}' does not exist.")
|
||||||
|
comp = self._components[name]
|
||||||
|
if comp.toolbar_bundle is not None:
|
||||||
|
logger.info(
|
||||||
|
f"Component '{name}' already has a bundle ({comp.toolbar_bundle.name}). Setting it to {bundle.name}."
|
||||||
|
)
|
||||||
|
comp.toolbar_bundle.bundle_actions.pop(name, None)
|
||||||
|
comp.toolbar_bundle = bundle
|
||||||
|
|
||||||
|
def remove_action(self, name: str):
|
||||||
|
"""
|
||||||
|
Removes a component from the toolbar.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name (str): The name of the component to remove.
|
||||||
|
"""
|
||||||
|
if not self.exists(name):
|
||||||
|
raise KeyError(f"Action with ID '{name}' does not exist.")
|
||||||
|
action_info = self._components.pop(name)
|
||||||
|
if action_info.toolbar_bundle:
|
||||||
|
action_info.toolbar_bundle.bundle_actions.pop(name, None)
|
||||||
|
self.toolbar.refresh()
|
||||||
|
action_info.toolbar_bundle = None
|
||||||
|
if hasattr(action_info.action, "cleanup"):
|
||||||
|
# Call cleanup if the action has a cleanup method
|
||||||
|
action_info.action.cleanup()
|
||||||
|
|
||||||
|
def cleanup(self):
|
||||||
|
"""
|
||||||
|
Cleans up the toolbar components by removing all actions and bundles.
|
||||||
|
"""
|
||||||
|
for action_info in self._components.values():
|
||||||
|
if hasattr(action_info.action, "cleanup"):
|
||||||
|
# Call cleanup if the action has a cleanup method
|
||||||
|
action_info.action.cleanup()
|
||||||
|
self._components.clear()
|
||||||
|
|
||||||
|
|
||||||
|
class ToolbarBundle:
|
||||||
|
def __init__(self, name: str, components: ToolbarComponents):
|
||||||
|
"""
|
||||||
|
Initializes a new bundle component.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
bundle_name (str): Unique identifier for the bundle.
|
||||||
|
"""
|
||||||
|
self.name = name
|
||||||
|
self.components = components
|
||||||
|
self.bundle_actions: DefaultDict[str, ReferenceType[ToolBarAction]] = defaultdict()
|
||||||
|
self._connections: dict[str, BundleConnection] = {}
|
||||||
|
|
||||||
|
def add_action(self, name: str):
|
||||||
|
"""
|
||||||
|
Adds an action to the bundle.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name (str): Unique identifier for the action.
|
||||||
|
action (ToolBarAction): The action to add.
|
||||||
|
"""
|
||||||
|
if name in self.bundle_actions:
|
||||||
|
raise ValueError(f"Action with name '{name}' already exists in bundle '{self.name}'.")
|
||||||
|
if not self.components.exists(name):
|
||||||
|
raise ValueError(
|
||||||
|
f"Component with name '{name}' does not exist in the toolbar. Please add it first using the `ToolbarComponents.add` method."
|
||||||
|
)
|
||||||
|
self.bundle_actions[name] = self.components.get_action_reference(name)
|
||||||
|
self.components.set_bundle(name, self)
|
||||||
|
|
||||||
|
def remove_action(self, name: str):
|
||||||
|
"""
|
||||||
|
Removes an action from the bundle.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name (str): The name of the action to remove.
|
||||||
|
"""
|
||||||
|
if name not in self.bundle_actions:
|
||||||
|
raise KeyError(f"Action with name '{name}' does not exist in bundle '{self.name}'.")
|
||||||
|
del self.bundle_actions[name]
|
||||||
|
|
||||||
|
def add_separator(self):
|
||||||
|
"""
|
||||||
|
Adds a separator action to the bundle.
|
||||||
|
"""
|
||||||
|
self.add_action("separator")
|
||||||
|
|
||||||
|
def add_connection(self, name: str, connection):
|
||||||
|
"""
|
||||||
|
Adds a connection to the bundle.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name (str): Unique identifier for the connection.
|
||||||
|
connection: The connection to add.
|
||||||
|
"""
|
||||||
|
if name in self._connections:
|
||||||
|
raise ValueError(
|
||||||
|
f"Connection with name '{name}' already exists in bundle '{self.name}'."
|
||||||
|
)
|
||||||
|
self._connections[name] = connection
|
||||||
|
|
||||||
|
def remove_connection(self, name: str):
|
||||||
|
"""
|
||||||
|
Removes a connection from the bundle.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name (str): The name of the connection to remove.
|
||||||
|
"""
|
||||||
|
if name not in self._connections:
|
||||||
|
raise KeyError(f"Connection with name '{name}' does not exist in bundle '{self.name}'.")
|
||||||
|
self._connections[name].disconnect()
|
||||||
|
del self._connections[name]
|
||||||
|
|
||||||
|
def get_connection(self, name: str):
|
||||||
|
"""
|
||||||
|
Retrieves a connection by name.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name (str): The name of the connection to retrieve.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The connection associated with the given name.
|
||||||
|
"""
|
||||||
|
if name not in self._connections:
|
||||||
|
raise KeyError(f"Connection with name '{name}' does not exist in bundle '{self.name}'.")
|
||||||
|
return self._connections[name]
|
||||||
|
|
||||||
|
def disconnect(self):
|
||||||
|
"""
|
||||||
|
Disconnects all connections in the bundle.
|
||||||
|
"""
|
||||||
|
for connection in self._connections.values():
|
||||||
|
connection.disconnect()
|
||||||
|
self._connections.clear()
|
23
bec_widgets/utils/toolbars/connections.py
Normal file
23
bec_widgets/utils/toolbars/connections.py
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from abc import abstractmethod
|
||||||
|
|
||||||
|
from qtpy.QtCore import QObject
|
||||||
|
|
||||||
|
|
||||||
|
class BundleConnection(QObject):
|
||||||
|
bundle_name: str
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def connect(self):
|
||||||
|
"""
|
||||||
|
Connects the bundle to the target widget or application.
|
||||||
|
This method should be implemented by subclasses to define how the bundle interacts with the target.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def disconnect(self):
|
||||||
|
"""
|
||||||
|
Disconnects the bundle from the target widget or application.
|
||||||
|
This method should be implemented by subclasses to define how to clean up connections.
|
||||||
|
"""
|
58
bec_widgets/utils/toolbars/performance.py
Normal file
58
bec_widgets/utils/toolbars/performance.py
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
from bec_widgets.utils.toolbars.actions import MaterialIconAction
|
||||||
|
from bec_widgets.utils.toolbars.bundles import ToolbarBundle
|
||||||
|
from bec_widgets.utils.toolbars.connections import BundleConnection
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from bec_widgets.utils.toolbars.toolbar import ToolbarComponents
|
||||||
|
|
||||||
|
|
||||||
|
def performance_bundle(components: ToolbarComponents) -> ToolbarBundle:
|
||||||
|
"""
|
||||||
|
Creates a performance toolbar bundle.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
components (ToolbarComponents): The components to be added to the bundle.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
ToolbarBundle: The performance toolbar bundle.
|
||||||
|
"""
|
||||||
|
components.add_safe(
|
||||||
|
"fps_monitor",
|
||||||
|
MaterialIconAction(
|
||||||
|
icon_name="speed", tooltip="Show FPS Monitor", checkable=True, parent=components.toolbar
|
||||||
|
),
|
||||||
|
)
|
||||||
|
bundle = ToolbarBundle("performance", components)
|
||||||
|
bundle.add_action("fps_monitor")
|
||||||
|
return bundle
|
||||||
|
|
||||||
|
|
||||||
|
class PerformanceConnection(BundleConnection):
|
||||||
|
|
||||||
|
def __init__(self, components: ToolbarComponents, target_widget=None):
|
||||||
|
self.bundle_name = "performance"
|
||||||
|
self.components = components
|
||||||
|
self.target_widget = target_widget
|
||||||
|
if not hasattr(self.target_widget, "enable_fps_monitor"):
|
||||||
|
raise AttributeError("Target widget must implement 'enable_fps_monitor'.")
|
||||||
|
super().__init__()
|
||||||
|
self._connected = False
|
||||||
|
|
||||||
|
def connect(self):
|
||||||
|
self._connected = True
|
||||||
|
# Connect the action to the target widget's method
|
||||||
|
self.components.get_action_reference("fps_monitor")().action.toggled.connect(
|
||||||
|
lambda checked: setattr(self.target_widget, "enable_fps_monitor", checked)
|
||||||
|
)
|
||||||
|
|
||||||
|
def disconnect(self):
|
||||||
|
if not self._connected:
|
||||||
|
return
|
||||||
|
# Disconnect the action from the target widget's method
|
||||||
|
self.components.get_action_reference("fps_monitor")().action.toggled.disconnect(
|
||||||
|
lambda checked: setattr(self.target_widget, "enable_fps_monitor", checked)
|
||||||
|
)
|
513
bec_widgets/utils/toolbars/toolbar.py
Normal file
513
bec_widgets/utils/toolbars/toolbar.py
Normal file
@ -0,0 +1,513 @@
|
|||||||
|
# pylint: disable=no-name-in-module
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import sys
|
||||||
|
from collections import defaultdict
|
||||||
|
from typing import DefaultDict, Literal
|
||||||
|
|
||||||
|
from bec_lib.logger import bec_logger
|
||||||
|
from qtpy.QtCore import QSize, Qt
|
||||||
|
from qtpy.QtGui import QAction, QColor
|
||||||
|
from qtpy.QtWidgets import QApplication, QLabel, QMainWindow, QMenu, QToolBar, QVBoxLayout, QWidget
|
||||||
|
|
||||||
|
from bec_widgets.utils.colors import get_theme_name, set_theme
|
||||||
|
from bec_widgets.utils.toolbars.actions import MaterialIconAction, ToolBarAction
|
||||||
|
from bec_widgets.utils.toolbars.bundles import ToolbarBundle, ToolbarComponents
|
||||||
|
from bec_widgets.utils.toolbars.connections import BundleConnection
|
||||||
|
|
||||||
|
logger = bec_logger.logger
|
||||||
|
|
||||||
|
# Ensure that icons are shown in menus (especially on macOS)
|
||||||
|
QApplication.setAttribute(Qt.AA_DontShowIconsInMenus, False)
|
||||||
|
|
||||||
|
|
||||||
|
class ModularToolBar(QToolBar):
|
||||||
|
"""Modular toolbar with optional automatic initialization.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
parent (QWidget, optional): The parent widget of the toolbar. Defaults to None.
|
||||||
|
actions (dict, optional): A dictionary of action creators to populate the toolbar. Defaults to None.
|
||||||
|
target_widget (QWidget, optional): The widget that the actions will target. Defaults to None.
|
||||||
|
orientation (Literal["horizontal", "vertical"], optional): The initial orientation of the toolbar. Defaults to "horizontal".
|
||||||
|
background_color (str, optional): The background color of the toolbar. Defaults to "rgba(0, 0, 0, 0)".
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
parent=None,
|
||||||
|
orientation: Literal["horizontal", "vertical"] = "horizontal",
|
||||||
|
background_color: str = "rgba(0, 0, 0, 0)",
|
||||||
|
):
|
||||||
|
super().__init__(parent=parent)
|
||||||
|
|
||||||
|
self.background_color = background_color
|
||||||
|
self.set_background_color(self.background_color)
|
||||||
|
|
||||||
|
# Set the initial orientation
|
||||||
|
self.set_orientation(orientation)
|
||||||
|
|
||||||
|
self.components = ToolbarComponents(self)
|
||||||
|
|
||||||
|
# Initialize bundles
|
||||||
|
self.bundles: dict[str, ToolbarBundle] = {}
|
||||||
|
self.shown_bundles: list[str] = []
|
||||||
|
|
||||||
|
#########################
|
||||||
|
# outdated items... remove
|
||||||
|
self.available_widgets: DefaultDict[str, ToolBarAction] = defaultdict()
|
||||||
|
########################
|
||||||
|
|
||||||
|
def new_bundle(self, name: str) -> ToolbarBundle:
|
||||||
|
"""
|
||||||
|
Creates a new bundle component.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name (str): Unique identifier for the bundle.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
ToolbarBundle: The new bundle component.
|
||||||
|
"""
|
||||||
|
if name in self.bundles:
|
||||||
|
raise ValueError(f"Bundle with name '{name}' already exists.")
|
||||||
|
bundle = ToolbarBundle(name=name, components=self.components)
|
||||||
|
self.bundles[name] = bundle
|
||||||
|
return bundle
|
||||||
|
|
||||||
|
def add_bundle(self, bundle: ToolbarBundle):
|
||||||
|
"""
|
||||||
|
Adds a bundle component to the toolbar.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
bundle (ToolbarBundle): The bundle component to add.
|
||||||
|
"""
|
||||||
|
if bundle.name in self.bundles:
|
||||||
|
raise ValueError(f"Bundle with name '{bundle.name}' already exists.")
|
||||||
|
self.bundles[bundle.name] = bundle
|
||||||
|
if not bundle.bundle_actions:
|
||||||
|
logger.warning(f"Bundle '{bundle.name}' has no actions.")
|
||||||
|
|
||||||
|
def remove_bundle(self, name: str):
|
||||||
|
"""
|
||||||
|
Removes a bundle component by name.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name (str): The name of the bundle to remove.
|
||||||
|
"""
|
||||||
|
if name not in self.bundles:
|
||||||
|
raise KeyError(f"Bundle with name '{name}' does not exist.")
|
||||||
|
del self.bundles[name]
|
||||||
|
if name in self.shown_bundles:
|
||||||
|
self.shown_bundles.remove(name)
|
||||||
|
logger.info(f"Bundle '{name}' removed from the toolbar.")
|
||||||
|
|
||||||
|
def get_bundle(self, name: str) -> ToolbarBundle:
|
||||||
|
"""
|
||||||
|
Retrieves a bundle component by name.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name (str): The name of the bundle to retrieve.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
ToolbarBundle: The bundle component.
|
||||||
|
"""
|
||||||
|
if name not in self.bundles:
|
||||||
|
raise KeyError(
|
||||||
|
f"Bundle with name '{name}' does not exist. Available bundles: {list(self.bundles.keys())}"
|
||||||
|
)
|
||||||
|
return self.bundles[name]
|
||||||
|
|
||||||
|
def show_bundles(self, bundle_names: list[str]):
|
||||||
|
"""
|
||||||
|
Sets the bundles to be shown for the toolbar.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
bundle_names (list[str]): A list of bundle names to show. If a bundle is not in this list, its actions will be hidden.
|
||||||
|
"""
|
||||||
|
self.clear()
|
||||||
|
for requested_bundle in bundle_names:
|
||||||
|
bundle = self.get_bundle(requested_bundle)
|
||||||
|
for bundle_action in bundle.bundle_actions.values():
|
||||||
|
action = bundle_action()
|
||||||
|
if action is None:
|
||||||
|
logger.warning(
|
||||||
|
f"Action for bundle '{requested_bundle}' has been deleted. Skipping."
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
action.add_to_toolbar(self, self.parent())
|
||||||
|
separator = self.components.get_action_reference("separator")()
|
||||||
|
if separator is not None:
|
||||||
|
separator.add_to_toolbar(self, self.parent())
|
||||||
|
self.update_separators() # Ensure separators are updated after showing bundles
|
||||||
|
self.shown_bundles = bundle_names
|
||||||
|
|
||||||
|
def add_action(self, action_name: str, action: ToolBarAction):
|
||||||
|
"""
|
||||||
|
Adds a single action to the toolbar. It will create a new bundle
|
||||||
|
with the same name as the action.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
action_name (str): Unique identifier for the action.
|
||||||
|
action (ToolBarAction): The action to add.
|
||||||
|
"""
|
||||||
|
self.components.add_safe(action_name, action)
|
||||||
|
bundle = ToolbarBundle(name=action_name, components=self.components)
|
||||||
|
bundle.add_action(action_name)
|
||||||
|
self.add_bundle(bundle)
|
||||||
|
|
||||||
|
def hide_action(self, action_name: str):
|
||||||
|
"""
|
||||||
|
Hides a specific action in the toolbar.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
action_name (str): Unique identifier for the action to hide.
|
||||||
|
"""
|
||||||
|
action = self.components.get_action(action_name)
|
||||||
|
if hasattr(action, "action") and action.action is not None:
|
||||||
|
action.action.setVisible(False)
|
||||||
|
self.update_separators()
|
||||||
|
|
||||||
|
def show_action(self, action_name: str):
|
||||||
|
"""
|
||||||
|
Shows a specific action in the toolbar.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
action_name (str): Unique identifier for the action to show.
|
||||||
|
"""
|
||||||
|
action = self.components.get_action(action_name)
|
||||||
|
if hasattr(action, "action") and action.action is not None:
|
||||||
|
action.action.setVisible(True)
|
||||||
|
self.update_separators()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def toolbar_actions(self) -> list[ToolBarAction]:
|
||||||
|
"""
|
||||||
|
Returns a list of all actions currently in the toolbar.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list[ToolBarAction]: List of actions in the toolbar.
|
||||||
|
"""
|
||||||
|
actions = []
|
||||||
|
for bundle in self.shown_bundles:
|
||||||
|
if bundle not in self.bundles:
|
||||||
|
continue
|
||||||
|
for action in self.bundles[bundle].bundle_actions.values():
|
||||||
|
action_instance = action()
|
||||||
|
if action_instance is not None:
|
||||||
|
actions.append(action_instance)
|
||||||
|
return actions
|
||||||
|
|
||||||
|
def refresh(self):
|
||||||
|
"""Refreshes the toolbar by clearing and re-populating it."""
|
||||||
|
self.clear()
|
||||||
|
self.show_bundles(self.shown_bundles)
|
||||||
|
|
||||||
|
def connect_bundle(self, connection_name: str, connector: BundleConnection):
|
||||||
|
"""
|
||||||
|
Connects a bundle to a target widget or application.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
bundle_name (str): The name of the bundle to connect.
|
||||||
|
connector (BundleConnection): The connector instance that implements the connection logic.
|
||||||
|
"""
|
||||||
|
bundle_name = connector.bundle_name
|
||||||
|
if bundle_name not in self.bundles:
|
||||||
|
raise KeyError(f"Bundle with name '{bundle_name}' does not exist.")
|
||||||
|
connector.connect()
|
||||||
|
self.bundles[bundle_name].add_connection(connection_name, connector)
|
||||||
|
|
||||||
|
def disconnect_bundle(self, bundle_name: str, connection_name: str | None = None):
|
||||||
|
"""
|
||||||
|
Disconnects a bundle connection.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
bundle_name (str): The name of the bundle to disconnect.
|
||||||
|
connection_name (str): The name of the connection to disconnect. If None, disconnects all connections for the bundle.
|
||||||
|
"""
|
||||||
|
if bundle_name not in self.bundles:
|
||||||
|
raise KeyError(f"Bundle with name '{bundle_name}' does not exist.")
|
||||||
|
bundle = self.bundles[bundle_name]
|
||||||
|
if connection_name is None:
|
||||||
|
# Disconnect all connections in the bundle
|
||||||
|
bundle.disconnect()
|
||||||
|
else:
|
||||||
|
bundle.remove_connection(name=connection_name)
|
||||||
|
|
||||||
|
def set_background_color(self, color: str = "rgba(0, 0, 0, 0)"):
|
||||||
|
"""
|
||||||
|
Sets the background color and other appearance settings.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
color (str): The background color of the toolbar.
|
||||||
|
"""
|
||||||
|
self.setIconSize(QSize(20, 20))
|
||||||
|
self.setMovable(False)
|
||||||
|
self.setFloatable(False)
|
||||||
|
self.setContentsMargins(0, 0, 0, 0)
|
||||||
|
self.background_color = color
|
||||||
|
self.setStyleSheet(f"QToolBar {{ background-color: {color}; border: none; }}")
|
||||||
|
|
||||||
|
def set_orientation(self, orientation: Literal["horizontal", "vertical"]):
|
||||||
|
"""Sets the orientation of the toolbar.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
orientation (Literal["horizontal", "vertical"]): The desired orientation of the toolbar.
|
||||||
|
"""
|
||||||
|
if orientation == "horizontal":
|
||||||
|
self.setOrientation(Qt.Horizontal)
|
||||||
|
elif orientation == "vertical":
|
||||||
|
self.setOrientation(Qt.Vertical)
|
||||||
|
else:
|
||||||
|
raise ValueError("Orientation must be 'horizontal' or 'vertical'.")
|
||||||
|
|
||||||
|
def update_material_icon_colors(self, new_color: str | tuple | QColor):
|
||||||
|
"""
|
||||||
|
Updates the color of all MaterialIconAction icons.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
new_color (str | tuple | QColor): The new color.
|
||||||
|
"""
|
||||||
|
for action in self.available_widgets.values():
|
||||||
|
if isinstance(action, MaterialIconAction):
|
||||||
|
action.color = new_color
|
||||||
|
updated_icon = action.get_icon()
|
||||||
|
action.action.setIcon(updated_icon)
|
||||||
|
|
||||||
|
def contextMenuEvent(self, event):
|
||||||
|
"""
|
||||||
|
Overrides the context menu event to show toolbar actions with checkboxes and icons.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
event (QContextMenuEvent): The context menu event.
|
||||||
|
"""
|
||||||
|
menu = QMenu(self)
|
||||||
|
theme = get_theme_name()
|
||||||
|
if theme == "dark":
|
||||||
|
menu.setStyleSheet(
|
||||||
|
"""
|
||||||
|
QMenu {
|
||||||
|
background-color: rgba(50, 50, 50, 0.9);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
|
}
|
||||||
|
QMenu::item:selected {
|
||||||
|
background-color: rgba(0, 0, 255, 0.2);
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# Light theme styling
|
||||||
|
menu.setStyleSheet(
|
||||||
|
"""
|
||||||
|
QMenu {
|
||||||
|
background-color: rgba(255, 255, 255, 0.9);
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
QMenu::item:selected {
|
||||||
|
background-color: rgba(0, 0, 255, 0.2);
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
for ii, bundle in enumerate(self.shown_bundles):
|
||||||
|
self.handle_bundle_context_menu(menu, bundle)
|
||||||
|
if ii < len(self.shown_bundles) - 1:
|
||||||
|
menu.addSeparator()
|
||||||
|
menu.triggered.connect(self.handle_menu_triggered)
|
||||||
|
menu.exec_(event.globalPos())
|
||||||
|
|
||||||
|
def handle_bundle_context_menu(self, menu: QMenu, bundle_id: str):
|
||||||
|
"""
|
||||||
|
Adds bundle actions to the context menu.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
menu (QMenu): The context menu.
|
||||||
|
bundle_id (str): The bundle identifier.
|
||||||
|
"""
|
||||||
|
bundle = self.bundles.get(bundle_id)
|
||||||
|
if not bundle:
|
||||||
|
return
|
||||||
|
for act_id in bundle.bundle_actions:
|
||||||
|
toolbar_action = self.components.get_action(act_id)
|
||||||
|
if not isinstance(toolbar_action, ToolBarAction) or not hasattr(
|
||||||
|
toolbar_action, "action"
|
||||||
|
):
|
||||||
|
continue
|
||||||
|
qaction = toolbar_action.action
|
||||||
|
if not isinstance(qaction, QAction):
|
||||||
|
continue
|
||||||
|
self._add_qaction_to_menu(menu, qaction, toolbar_action, act_id)
|
||||||
|
|
||||||
|
def _add_qaction_to_menu(
|
||||||
|
self, menu: QMenu, qaction: QAction, toolbar_action: ToolBarAction, act_id: str
|
||||||
|
):
|
||||||
|
display_name = qaction.text() or toolbar_action.tooltip or act_id
|
||||||
|
menu_action = QAction(display_name, self)
|
||||||
|
menu_action.setCheckable(True)
|
||||||
|
menu_action.setChecked(qaction.isVisible())
|
||||||
|
menu_action.setData(act_id) # Store the action_id
|
||||||
|
|
||||||
|
# Set the icon if available
|
||||||
|
if qaction.icon() and not qaction.icon().isNull():
|
||||||
|
menu_action.setIcon(qaction.icon())
|
||||||
|
menu.addAction(menu_action)
|
||||||
|
|
||||||
|
def handle_action_context_menu(self, menu: QMenu, action_id: str):
|
||||||
|
"""
|
||||||
|
Adds a single toolbar action to the context menu.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
menu (QMenu): The context menu to which the action is added.
|
||||||
|
action_id (str): Unique identifier for the action.
|
||||||
|
"""
|
||||||
|
toolbar_action = self.available_widgets.get(action_id)
|
||||||
|
if not isinstance(toolbar_action, ToolBarAction) or not hasattr(toolbar_action, "action"):
|
||||||
|
return
|
||||||
|
qaction = toolbar_action.action
|
||||||
|
if not isinstance(qaction, QAction):
|
||||||
|
return
|
||||||
|
display_name = qaction.text() or toolbar_action.tooltip or action_id
|
||||||
|
menu_action = QAction(display_name, self)
|
||||||
|
menu_action.setCheckable(True)
|
||||||
|
menu_action.setChecked(qaction.isVisible())
|
||||||
|
menu_action.setIconVisibleInMenu(True)
|
||||||
|
menu_action.setData(action_id) # Store the action_id
|
||||||
|
|
||||||
|
# Set the icon if available
|
||||||
|
if qaction.icon() and not qaction.icon().isNull():
|
||||||
|
menu_action.setIcon(qaction.icon())
|
||||||
|
|
||||||
|
menu.addAction(menu_action)
|
||||||
|
|
||||||
|
def handle_menu_triggered(self, action):
|
||||||
|
"""
|
||||||
|
Handles the triggered signal from the context menu.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
action: Action triggered.
|
||||||
|
"""
|
||||||
|
action_id = action.data()
|
||||||
|
if action_id:
|
||||||
|
self.toggle_action_visibility(action_id)
|
||||||
|
|
||||||
|
def toggle_action_visibility(self, action_id: str, visible: bool | None = None):
|
||||||
|
"""
|
||||||
|
Toggles the visibility of a specific action.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
action_id (str): Unique identifier.
|
||||||
|
visible (bool): Whether the action should be visible. If None, toggles the current visibility.
|
||||||
|
"""
|
||||||
|
if not self.components.exists(action_id):
|
||||||
|
return
|
||||||
|
tool_action = self.components.get_action(action_id)
|
||||||
|
if hasattr(tool_action, "action") and tool_action.action is not None:
|
||||||
|
if visible is None:
|
||||||
|
visible = not tool_action.action.isVisible()
|
||||||
|
tool_action.action.setVisible(visible)
|
||||||
|
self.update_separators()
|
||||||
|
|
||||||
|
def update_separators(self):
|
||||||
|
"""
|
||||||
|
Hide separators that are adjacent to another separator or have no non-separator actions between them.
|
||||||
|
"""
|
||||||
|
toolbar_actions = self.actions()
|
||||||
|
# First pass: set visibility based on surrounding non-separator actions.
|
||||||
|
for i, action in enumerate(toolbar_actions):
|
||||||
|
if not action.isSeparator():
|
||||||
|
continue
|
||||||
|
prev_visible = None
|
||||||
|
for j in range(i - 1, -1, -1):
|
||||||
|
if toolbar_actions[j].isVisible():
|
||||||
|
prev_visible = toolbar_actions[j]
|
||||||
|
break
|
||||||
|
next_visible = None
|
||||||
|
for j in range(i + 1, len(toolbar_actions)):
|
||||||
|
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()
|
||||||
|
):
|
||||||
|
action.setVisible(False)
|
||||||
|
else:
|
||||||
|
action.setVisible(True)
|
||||||
|
# Second pass: ensure no two visible separators are adjacent.
|
||||||
|
prev = None
|
||||||
|
for action in toolbar_actions:
|
||||||
|
if action.isVisible() and action.isSeparator():
|
||||||
|
if prev and prev.isSeparator():
|
||||||
|
action.setVisible(False)
|
||||||
|
else:
|
||||||
|
prev = action
|
||||||
|
else:
|
||||||
|
if action.isVisible():
|
||||||
|
prev = action
|
||||||
|
|
||||||
|
if not toolbar_actions:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Make sure the first visible action is not a separator
|
||||||
|
for i, action in enumerate(toolbar_actions):
|
||||||
|
if action.isVisible():
|
||||||
|
if action.isSeparator():
|
||||||
|
action.setVisible(False)
|
||||||
|
break
|
||||||
|
|
||||||
|
# Make sure the last visible action is not a separator
|
||||||
|
for i, action in enumerate(reversed(toolbar_actions)):
|
||||||
|
if action.isVisible():
|
||||||
|
if action.isSeparator():
|
||||||
|
action.setVisible(False)
|
||||||
|
break
|
||||||
|
|
||||||
|
def cleanup(self):
|
||||||
|
"""
|
||||||
|
Cleans up the toolbar by removing all actions and bundles.
|
||||||
|
"""
|
||||||
|
# First, disconnect all bundles
|
||||||
|
for bundle_name in list(self.bundles.keys()):
|
||||||
|
self.disconnect_bundle(bundle_name)
|
||||||
|
|
||||||
|
# Clear all components
|
||||||
|
self.components.cleanup()
|
||||||
|
self.bundles.clear()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__": # pragma: no cover
|
||||||
|
from bec_widgets.utils.toolbars.performance import PerformanceConnection, performance_bundle
|
||||||
|
from bec_widgets.widgets.plots.toolbar_components.plot_export import plot_export_bundle
|
||||||
|
|
||||||
|
class MainWindow(QMainWindow): # pragma: no cover
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
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.central_widget.layout = QVBoxLayout(self.central_widget)
|
||||||
|
self.central_widget.layout.addWidget(self.test_label)
|
||||||
|
|
||||||
|
self.toolbar = ModularToolBar(parent=self)
|
||||||
|
self.addToolBar(self.toolbar)
|
||||||
|
self.toolbar.add_bundle(performance_bundle(self.toolbar.components))
|
||||||
|
self.toolbar.add_bundle(plot_export_bundle(self.toolbar.components))
|
||||||
|
self.toolbar.connect_bundle(
|
||||||
|
"base", PerformanceConnection(self.toolbar.components, self)
|
||||||
|
)
|
||||||
|
self.toolbar.show_bundles(["performance", "plot_export"])
|
||||||
|
self.toolbar.get_bundle("performance").add_action("save")
|
||||||
|
self.toolbar.refresh()
|
||||||
|
|
||||||
|
def enable_fps_monitor(self, enabled: bool):
|
||||||
|
"""
|
||||||
|
Example method to enable or disable FPS monitoring.
|
||||||
|
This method should be implemented in the target widget.
|
||||||
|
"""
|
||||||
|
if enabled:
|
||||||
|
self.test_label.setText("FPS Monitor Enabled")
|
||||||
|
else:
|
||||||
|
self.test_label.setText("FPS Monitor Disabled")
|
||||||
|
|
||||||
|
app = QApplication(sys.argv)
|
||||||
|
set_theme("light")
|
||||||
|
main_window = MainWindow()
|
||||||
|
main_window.show()
|
||||||
|
sys.exit(app.exec_())
|
@ -15,12 +15,13 @@ from bec_widgets.utils import ConnectionConfig, WidgetContainerUtils
|
|||||||
from bec_widgets.utils.bec_widget import BECWidget
|
from bec_widgets.utils.bec_widget import BECWidget
|
||||||
from bec_widgets.utils.error_popups import SafeSlot
|
from bec_widgets.utils.error_popups import SafeSlot
|
||||||
from bec_widgets.utils.name_utils import pascal_to_snake
|
from bec_widgets.utils.name_utils import pascal_to_snake
|
||||||
from bec_widgets.utils.toolbar import (
|
from bec_widgets.utils.toolbars.actions import (
|
||||||
ExpandableMenuAction,
|
ExpandableMenuAction,
|
||||||
MaterialIconAction,
|
MaterialIconAction,
|
||||||
ModularToolBar,
|
WidgetAction,
|
||||||
SeparatorAction,
|
|
||||||
)
|
)
|
||||||
|
from bec_widgets.utils.toolbars.bundles import ToolbarBundle
|
||||||
|
from bec_widgets.utils.toolbars.toolbar import ModularToolBar
|
||||||
from bec_widgets.utils.widget_io import WidgetHierarchy
|
from bec_widgets.utils.widget_io import WidgetHierarchy
|
||||||
from bec_widgets.widgets.containers.dock.dock import BECDock, DockConfig
|
from bec_widgets.widgets.containers.dock.dock import BECDock, DockConfig
|
||||||
from bec_widgets.widgets.containers.main_window.main_window import BECMainWindow
|
from bec_widgets.widgets.containers.main_window.main_window import BECMainWindow
|
||||||
@ -104,151 +105,227 @@ class BECDockArea(BECWidget, QWidget):
|
|||||||
|
|
||||||
self.dark_mode_button = DarkModeButton(parent=self, toolbar=True)
|
self.dark_mode_button = DarkModeButton(parent=self, toolbar=True)
|
||||||
self.dock_area = DockArea(parent=self)
|
self.dock_area = DockArea(parent=self)
|
||||||
self.toolbar = ModularToolBar(
|
self.toolbar = ModularToolBar(parent=self)
|
||||||
parent=self,
|
self._setup_toolbar()
|
||||||
actions={
|
|
||||||
"menu_plots": ExpandableMenuAction(
|
self.layout.addWidget(self.toolbar)
|
||||||
|
self.layout.addWidget(self.dock_area)
|
||||||
|
|
||||||
|
self._hook_toolbar()
|
||||||
|
self.toolbar.show_bundles(
|
||||||
|
["menu_plots", "menu_devices", "menu_utils", "dock_actions", "dark_mode"]
|
||||||
|
)
|
||||||
|
|
||||||
|
def minimumSizeHint(self):
|
||||||
|
return QSize(800, 600)
|
||||||
|
|
||||||
|
def _setup_toolbar(self):
|
||||||
|
|
||||||
|
# Add plot menu
|
||||||
|
self.toolbar.components.add_safe(
|
||||||
|
"menu_plots",
|
||||||
|
ExpandableMenuAction(
|
||||||
label="Add Plot ",
|
label="Add Plot ",
|
||||||
actions={
|
actions={
|
||||||
"waveform": MaterialIconAction(
|
"waveform": MaterialIconAction(
|
||||||
icon_name=Waveform.ICON_NAME, tooltip="Add Waveform", filled=True
|
icon_name=Waveform.ICON_NAME,
|
||||||
|
tooltip="Add Waveform",
|
||||||
|
filled=True,
|
||||||
|
parent=self,
|
||||||
),
|
),
|
||||||
"scatter_waveform": MaterialIconAction(
|
"scatter_waveform": MaterialIconAction(
|
||||||
icon_name=ScatterWaveform.ICON_NAME,
|
icon_name=ScatterWaveform.ICON_NAME,
|
||||||
tooltip="Add Scatter Waveform",
|
tooltip="Add Scatter Waveform",
|
||||||
filled=True,
|
filled=True,
|
||||||
|
parent=self,
|
||||||
),
|
),
|
||||||
"multi_waveform": MaterialIconAction(
|
"multi_waveform": MaterialIconAction(
|
||||||
icon_name=MultiWaveform.ICON_NAME,
|
icon_name=MultiWaveform.ICON_NAME,
|
||||||
tooltip="Add Multi Waveform",
|
tooltip="Add Multi Waveform",
|
||||||
filled=True,
|
filled=True,
|
||||||
|
parent=self,
|
||||||
),
|
),
|
||||||
"image": MaterialIconAction(
|
"image": MaterialIconAction(
|
||||||
icon_name=Image.ICON_NAME, tooltip="Add Image", filled=True
|
icon_name=Image.ICON_NAME, tooltip="Add Image", filled=True, parent=self
|
||||||
),
|
),
|
||||||
"motor_map": MaterialIconAction(
|
"motor_map": MaterialIconAction(
|
||||||
icon_name=MotorMap.ICON_NAME, tooltip="Add Motor Map", filled=True
|
icon_name=MotorMap.ICON_NAME,
|
||||||
|
tooltip="Add Motor Map",
|
||||||
|
filled=True,
|
||||||
|
parent=self,
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
"separator_0": SeparatorAction(),
|
)
|
||||||
"menu_devices": ExpandableMenuAction(
|
|
||||||
|
bundle = ToolbarBundle("menu_plots", self.toolbar.components)
|
||||||
|
bundle.add_action("menu_plots")
|
||||||
|
self.toolbar.add_bundle(bundle)
|
||||||
|
|
||||||
|
# Add control menu
|
||||||
|
self.toolbar.components.add_safe(
|
||||||
|
"menu_devices",
|
||||||
|
ExpandableMenuAction(
|
||||||
label="Add Device Control ",
|
label="Add Device Control ",
|
||||||
actions={
|
actions={
|
||||||
"scan_control": MaterialIconAction(
|
"scan_control": MaterialIconAction(
|
||||||
icon_name=ScanControl.ICON_NAME, tooltip="Add Scan Control", filled=True
|
icon_name=ScanControl.ICON_NAME,
|
||||||
|
tooltip="Add Scan Control",
|
||||||
|
filled=True,
|
||||||
|
parent=self,
|
||||||
),
|
),
|
||||||
"positioner_box": MaterialIconAction(
|
"positioner_box": MaterialIconAction(
|
||||||
icon_name=PositionerBox.ICON_NAME, tooltip="Add Device Box", filled=True
|
icon_name=PositionerBox.ICON_NAME,
|
||||||
|
tooltip="Add Device Box",
|
||||||
|
filled=True,
|
||||||
|
parent=self,
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
"separator_1": SeparatorAction(),
|
)
|
||||||
"menu_utils": ExpandableMenuAction(
|
bundle = ToolbarBundle("menu_devices", self.toolbar.components)
|
||||||
|
bundle.add_action("menu_devices")
|
||||||
|
self.toolbar.add_bundle(bundle)
|
||||||
|
|
||||||
|
# Add utils menu
|
||||||
|
self.toolbar.components.add_safe(
|
||||||
|
"menu_utils",
|
||||||
|
ExpandableMenuAction(
|
||||||
label="Add Utils ",
|
label="Add Utils ",
|
||||||
actions={
|
actions={
|
||||||
"queue": MaterialIconAction(
|
"queue": MaterialIconAction(
|
||||||
icon_name=BECQueue.ICON_NAME, tooltip="Add Scan Queue", filled=True
|
icon_name=BECQueue.ICON_NAME,
|
||||||
|
tooltip="Add Scan Queue",
|
||||||
|
filled=True,
|
||||||
|
parent=self,
|
||||||
),
|
),
|
||||||
"vs_code": MaterialIconAction(
|
"vs_code": MaterialIconAction(
|
||||||
icon_name=VSCodeEditor.ICON_NAME, tooltip="Add VS Code", filled=True
|
icon_name=VSCodeEditor.ICON_NAME,
|
||||||
|
tooltip="Add VS Code",
|
||||||
|
filled=True,
|
||||||
|
parent=self,
|
||||||
),
|
),
|
||||||
"status": MaterialIconAction(
|
"status": MaterialIconAction(
|
||||||
icon_name=BECStatusBox.ICON_NAME,
|
icon_name=BECStatusBox.ICON_NAME,
|
||||||
tooltip="Add BEC Status Box",
|
tooltip="Add BEC Status Box",
|
||||||
filled=True,
|
filled=True,
|
||||||
|
parent=self,
|
||||||
),
|
),
|
||||||
"progress_bar": MaterialIconAction(
|
"progress_bar": MaterialIconAction(
|
||||||
icon_name=RingProgressBar.ICON_NAME,
|
icon_name=RingProgressBar.ICON_NAME,
|
||||||
tooltip="Add Circular ProgressBar",
|
tooltip="Add Circular ProgressBar",
|
||||||
filled=True,
|
filled=True,
|
||||||
|
parent=self,
|
||||||
),
|
),
|
||||||
# FIXME temporarily disabled -> issue #644
|
# FIXME temporarily disabled -> issue #644
|
||||||
"log_panel": MaterialIconAction(
|
"log_panel": MaterialIconAction(
|
||||||
icon_name=LogPanel.ICON_NAME,
|
icon_name=LogPanel.ICON_NAME,
|
||||||
tooltip="Add LogPanel - Disabled",
|
tooltip="Add LogPanel - Disabled",
|
||||||
filled=True,
|
filled=True,
|
||||||
|
parent=self,
|
||||||
),
|
),
|
||||||
"sbb_monitor": MaterialIconAction(
|
"sbb_monitor": MaterialIconAction(
|
||||||
icon_name="train", tooltip="Add SBB Monitor", filled=True
|
icon_name="train", tooltip="Add SBB Monitor", filled=True, parent=self
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
"separator_2": SeparatorAction(),
|
)
|
||||||
"attach_all": MaterialIconAction(
|
bundle = ToolbarBundle("menu_utils", self.toolbar.components)
|
||||||
icon_name="zoom_in_map", tooltip="Attach all floating docks"
|
bundle.add_action("menu_utils")
|
||||||
),
|
self.toolbar.add_bundle(bundle)
|
||||||
"save_state": MaterialIconAction(icon_name="bookmark", tooltip="Save Dock State"),
|
|
||||||
"restore_state": MaterialIconAction(
|
########## Dock Actions ##########
|
||||||
icon_name="frame_reload", tooltip="Restore Dock State"
|
spacer = QWidget(parent=self)
|
||||||
),
|
spacer.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
|
||||||
},
|
self.toolbar.components.add_safe("spacer", WidgetAction(widget=spacer, adjust_size=False))
|
||||||
target_widget=self,
|
|
||||||
|
self.toolbar.components.add_safe(
|
||||||
|
"dark_mode", WidgetAction(widget=self.dark_mode_button, adjust_size=False)
|
||||||
)
|
)
|
||||||
|
|
||||||
self.layout.addWidget(self.toolbar)
|
bundle = ToolbarBundle("dark_mode", self.toolbar.components)
|
||||||
self.layout.addWidget(self.dock_area)
|
bundle.add_action("spacer")
|
||||||
self.spacer = QWidget(parent=self)
|
bundle.add_action("dark_mode")
|
||||||
self.spacer.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
|
self.toolbar.add_bundle(bundle)
|
||||||
self.toolbar.addWidget(self.spacer)
|
|
||||||
self.toolbar.addWidget(self.dark_mode_button)
|
|
||||||
self._hook_toolbar()
|
|
||||||
|
|
||||||
def minimumSizeHint(self):
|
self.toolbar.components.add_safe(
|
||||||
return QSize(800, 600)
|
"attach_all",
|
||||||
|
MaterialIconAction(
|
||||||
|
icon_name="zoom_in_map", tooltip="Attach all floating docks", parent=self
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
self.toolbar.components.add_safe(
|
||||||
|
"save_state",
|
||||||
|
MaterialIconAction(icon_name="bookmark", tooltip="Save Dock State", parent=self),
|
||||||
|
)
|
||||||
|
self.toolbar.components.add_safe(
|
||||||
|
"restore_state",
|
||||||
|
MaterialIconAction(icon_name="frame_reload", tooltip="Restore Dock State", parent=self),
|
||||||
|
)
|
||||||
|
|
||||||
|
bundle = ToolbarBundle("dock_actions", self.toolbar.components)
|
||||||
|
bundle.add_action("attach_all")
|
||||||
|
bundle.add_action("save_state")
|
||||||
|
bundle.add_action("restore_state")
|
||||||
|
self.toolbar.add_bundle(bundle)
|
||||||
|
|
||||||
def _hook_toolbar(self):
|
def _hook_toolbar(self):
|
||||||
# Menu Plot
|
menu_plots = self.toolbar.components.get_action("menu_plots")
|
||||||
self.toolbar.widgets["menu_plots"].widgets["waveform"].triggered.connect(
|
menu_devices = self.toolbar.components.get_action("menu_devices")
|
||||||
|
menu_utils = self.toolbar.components.get_action("menu_utils")
|
||||||
|
|
||||||
|
menu_plots.actions["waveform"].action.triggered.connect(
|
||||||
lambda: self._create_widget_from_toolbar(widget_name="Waveform")
|
lambda: self._create_widget_from_toolbar(widget_name="Waveform")
|
||||||
)
|
)
|
||||||
self.toolbar.widgets["menu_plots"].widgets["scatter_waveform"].triggered.connect(
|
|
||||||
|
menu_plots.actions["scatter_waveform"].action.triggered.connect(
|
||||||
lambda: self._create_widget_from_toolbar(widget_name="ScatterWaveform")
|
lambda: self._create_widget_from_toolbar(widget_name="ScatterWaveform")
|
||||||
)
|
)
|
||||||
self.toolbar.widgets["menu_plots"].widgets["multi_waveform"].triggered.connect(
|
menu_plots.actions["multi_waveform"].action.triggered.connect(
|
||||||
lambda: self._create_widget_from_toolbar(widget_name="MultiWaveform")
|
lambda: self._create_widget_from_toolbar(widget_name="MultiWaveform")
|
||||||
)
|
)
|
||||||
self.toolbar.widgets["menu_plots"].widgets["image"].triggered.connect(
|
menu_plots.actions["image"].action.triggered.connect(
|
||||||
lambda: self._create_widget_from_toolbar(widget_name="Image")
|
lambda: self._create_widget_from_toolbar(widget_name="Image")
|
||||||
)
|
)
|
||||||
self.toolbar.widgets["menu_plots"].widgets["motor_map"].triggered.connect(
|
menu_plots.actions["motor_map"].action.triggered.connect(
|
||||||
lambda: self._create_widget_from_toolbar(widget_name="MotorMap")
|
lambda: self._create_widget_from_toolbar(widget_name="MotorMap")
|
||||||
)
|
)
|
||||||
|
|
||||||
# Menu Devices
|
# Menu Devices
|
||||||
self.toolbar.widgets["menu_devices"].widgets["scan_control"].triggered.connect(
|
menu_devices.actions["scan_control"].action.triggered.connect(
|
||||||
lambda: self._create_widget_from_toolbar(widget_name="ScanControl")
|
lambda: self._create_widget_from_toolbar(widget_name="ScanControl")
|
||||||
)
|
)
|
||||||
self.toolbar.widgets["menu_devices"].widgets["positioner_box"].triggered.connect(
|
menu_devices.actions["positioner_box"].action.triggered.connect(
|
||||||
lambda: self._create_widget_from_toolbar(widget_name="PositionerBox")
|
lambda: self._create_widget_from_toolbar(widget_name="PositionerBox")
|
||||||
)
|
)
|
||||||
|
|
||||||
# Menu Utils
|
# Menu Utils
|
||||||
self.toolbar.widgets["menu_utils"].widgets["queue"].triggered.connect(
|
menu_utils.actions["queue"].action.triggered.connect(
|
||||||
lambda: self._create_widget_from_toolbar(widget_name="BECQueue")
|
lambda: self._create_widget_from_toolbar(widget_name="BECQueue")
|
||||||
)
|
)
|
||||||
self.toolbar.widgets["menu_utils"].widgets["status"].triggered.connect(
|
menu_utils.actions["status"].action.triggered.connect(
|
||||||
lambda: self._create_widget_from_toolbar(widget_name="BECStatusBox")
|
lambda: self._create_widget_from_toolbar(widget_name="BECStatusBox")
|
||||||
)
|
)
|
||||||
self.toolbar.widgets["menu_utils"].widgets["vs_code"].triggered.connect(
|
menu_utils.actions["vs_code"].action.triggered.connect(
|
||||||
lambda: self._create_widget_from_toolbar(widget_name="VSCodeEditor")
|
lambda: self._create_widget_from_toolbar(widget_name="VSCodeEditor")
|
||||||
)
|
)
|
||||||
self.toolbar.widgets["menu_utils"].widgets["progress_bar"].triggered.connect(
|
menu_utils.actions["progress_bar"].action.triggered.connect(
|
||||||
lambda: self._create_widget_from_toolbar(widget_name="RingProgressBar")
|
lambda: self._create_widget_from_toolbar(widget_name="RingProgressBar")
|
||||||
)
|
)
|
||||||
# FIXME temporarily disabled -> issue #644
|
# FIXME temporarily disabled -> issue #644
|
||||||
self.toolbar.widgets["menu_utils"].widgets["log_panel"].setEnabled(False)
|
menu_utils.actions["log_panel"].action.setEnabled(False)
|
||||||
# self.toolbar.widgets["menu_utils"].widgets["log_panel"].triggered.connect(
|
|
||||||
# lambda: self._create_widget_from_toolbar(widget_name="LogPanel")
|
menu_utils.actions["sbb_monitor"].action.triggered.connect(
|
||||||
# )
|
|
||||||
self.toolbar.widgets["menu_utils"].widgets["sbb_monitor"].triggered.connect(
|
|
||||||
lambda: self._create_widget_from_toolbar(widget_name="SBBMonitor")
|
lambda: self._create_widget_from_toolbar(widget_name="SBBMonitor")
|
||||||
)
|
)
|
||||||
|
|
||||||
# Icons
|
# Icons
|
||||||
self.toolbar.widgets["attach_all"].action.triggered.connect(self.attach_all)
|
self.toolbar.components.get_action("attach_all").action.triggered.connect(self.attach_all)
|
||||||
self.toolbar.widgets["save_state"].action.triggered.connect(self.save_state)
|
self.toolbar.components.get_action("save_state").action.triggered.connect(self.save_state)
|
||||||
self.toolbar.widgets["restore_state"].action.triggered.connect(self.restore_state)
|
self.toolbar.components.get_action("restore_state").action.triggered.connect(
|
||||||
|
self.restore_state
|
||||||
|
)
|
||||||
|
|
||||||
@SafeSlot()
|
@SafeSlot()
|
||||||
def _create_widget_from_toolbar(self, widget_name: str) -> None:
|
def _create_widget_from_toolbar(self, widget_name: str) -> None:
|
||||||
|
@ -7,11 +7,19 @@ import numpy as np
|
|||||||
from bec_lib import bec_logger
|
from bec_lib import bec_logger
|
||||||
from bec_lib.endpoints import MessageEndpoints
|
from bec_lib.endpoints import MessageEndpoints
|
||||||
from pydantic import BaseModel, Field, field_validator
|
from pydantic import BaseModel, Field, field_validator
|
||||||
from qtpy.QtWidgets import QWidget
|
from qtpy.QtCore import Qt, QTimer
|
||||||
|
from qtpy.QtWidgets import QComboBox, QStyledItemDelegate, QWidget
|
||||||
|
|
||||||
from bec_widgets.utils import ConnectionConfig
|
from bec_widgets.utils import ConnectionConfig
|
||||||
from bec_widgets.utils.colors import Colors
|
from bec_widgets.utils.colors import Colors
|
||||||
from bec_widgets.utils.error_popups import SafeProperty, SafeSlot
|
from bec_widgets.utils.error_popups import SafeProperty, SafeSlot
|
||||||
|
from bec_widgets.utils.toolbars.actions import NoCheckDelegate, WidgetAction
|
||||||
|
from bec_widgets.utils.toolbars.bundles import ToolbarBundle
|
||||||
|
from bec_widgets.widgets.control.device_input.base_classes.device_input_base import (
|
||||||
|
BECDeviceFilter,
|
||||||
|
ReadoutPriority,
|
||||||
|
)
|
||||||
|
from bec_widgets.widgets.control.device_input.device_combobox.device_combobox import DeviceComboBox
|
||||||
from bec_widgets.widgets.plots.image.image_base import ImageBase
|
from bec_widgets.widgets.plots.image.image_base import ImageBase
|
||||||
from bec_widgets.widgets.plots.image.image_item import ImageItem
|
from bec_widgets.widgets.plots.image.image_item import ImageItem
|
||||||
|
|
||||||
@ -139,9 +147,119 @@ class Image(ImageBase):
|
|||||||
super().__init__(
|
super().__init__(
|
||||||
parent=parent, config=config, client=client, gui_id=gui_id, popups=popups, **kwargs
|
parent=parent, config=config, client=client, gui_id=gui_id, popups=popups, **kwargs
|
||||||
)
|
)
|
||||||
|
self._init_toolbar_image()
|
||||||
self.layer_removed.connect(self._on_layer_removed)
|
self.layer_removed.connect(self._on_layer_removed)
|
||||||
self.scan_id = None
|
self.scan_id = None
|
||||||
|
|
||||||
|
##################################
|
||||||
|
### Toolbar Initialization
|
||||||
|
##################################
|
||||||
|
|
||||||
|
def _init_toolbar_image(self):
|
||||||
|
"""
|
||||||
|
Initializes the toolbar for the image widget.
|
||||||
|
"""
|
||||||
|
self.device_combo_box = DeviceComboBox(
|
||||||
|
parent=self,
|
||||||
|
device_filter=BECDeviceFilter.DEVICE,
|
||||||
|
readout_priority_filter=[ReadoutPriority.ASYNC],
|
||||||
|
)
|
||||||
|
self.device_combo_box.addItem("", None)
|
||||||
|
self.device_combo_box.setCurrentText("")
|
||||||
|
self.device_combo_box.setToolTip("Select Device")
|
||||||
|
self.device_combo_box.setFixedWidth(150)
|
||||||
|
self.device_combo_box.setItemDelegate(NoCheckDelegate(self.device_combo_box))
|
||||||
|
|
||||||
|
self.dim_combo_box = QComboBox(parent=self)
|
||||||
|
self.dim_combo_box.addItems(["auto", "1d", "2d"])
|
||||||
|
self.dim_combo_box.setCurrentText("auto")
|
||||||
|
self.dim_combo_box.setToolTip("Monitor Dimension")
|
||||||
|
self.dim_combo_box.setFixedWidth(100)
|
||||||
|
self.dim_combo_box.setItemDelegate(NoCheckDelegate(self.dim_combo_box))
|
||||||
|
|
||||||
|
self.toolbar.components.add_safe(
|
||||||
|
"image_device_combo", WidgetAction(widget=self.device_combo_box, adjust_size=False)
|
||||||
|
)
|
||||||
|
self.toolbar.components.add_safe(
|
||||||
|
"image_dim_combo", WidgetAction(widget=self.dim_combo_box, adjust_size=False)
|
||||||
|
)
|
||||||
|
|
||||||
|
bundle = ToolbarBundle("monitor_selection", self.toolbar.components)
|
||||||
|
bundle.add_action("image_device_combo")
|
||||||
|
bundle.add_action("image_dim_combo")
|
||||||
|
|
||||||
|
self.toolbar.add_bundle(bundle)
|
||||||
|
self.device_combo_box.currentTextChanged.connect(self.connect_monitor)
|
||||||
|
self.dim_combo_box.currentTextChanged.connect(self.connect_monitor)
|
||||||
|
|
||||||
|
crosshair_bundle = self.toolbar.get_bundle("image_crosshair")
|
||||||
|
crosshair_bundle.add_action("image_autorange")
|
||||||
|
crosshair_bundle.add_action("image_colorbar_switch")
|
||||||
|
|
||||||
|
self.toolbar.show_bundles(
|
||||||
|
[
|
||||||
|
"monitor_selection",
|
||||||
|
"plot_export",
|
||||||
|
"mouse_interaction",
|
||||||
|
"image_crosshair",
|
||||||
|
"image_processing",
|
||||||
|
"axis_popup",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
QTimer.singleShot(0, self._adjust_and_connect)
|
||||||
|
|
||||||
|
def _adjust_and_connect(self):
|
||||||
|
"""
|
||||||
|
Adjust the size of the device combo box and populate it with preview signals.
|
||||||
|
Has to be done with QTimer.singleShot to ensure the UI is fully initialized, needed for testing.
|
||||||
|
"""
|
||||||
|
self._populate_preview_signals()
|
||||||
|
self._reverse_device_items()
|
||||||
|
self.device_combo_box.setCurrentText("") # set again default to empty string
|
||||||
|
|
||||||
|
def _populate_preview_signals(self) -> None:
|
||||||
|
"""
|
||||||
|
Populate the device combo box with preview-signal devices in the
|
||||||
|
format '<device>_<signal>' and store the tuple(device, signal) in
|
||||||
|
the item's userData for later use.
|
||||||
|
"""
|
||||||
|
preview_signals = self.client.device_manager.get_bec_signals("PreviewSignal")
|
||||||
|
for device, signal, signal_config in preview_signals:
|
||||||
|
label = signal_config.get("obj_name", f"{device}_{signal}")
|
||||||
|
self.device_combo_box.addItem(label, (device, signal, signal_config))
|
||||||
|
|
||||||
|
def _reverse_device_items(self) -> None:
|
||||||
|
"""
|
||||||
|
Reverse the current order of items in the device combo box while
|
||||||
|
keeping their userData and restoring the previous selection.
|
||||||
|
"""
|
||||||
|
current_text = self.device_combo_box.currentText()
|
||||||
|
items = [
|
||||||
|
(self.device_combo_box.itemText(i), self.device_combo_box.itemData(i))
|
||||||
|
for i in range(self.device_combo_box.count())
|
||||||
|
]
|
||||||
|
self.device_combo_box.clear()
|
||||||
|
for text, data in reversed(items):
|
||||||
|
self.device_combo_box.addItem(text, data)
|
||||||
|
if current_text:
|
||||||
|
self.device_combo_box.setCurrentText(current_text)
|
||||||
|
|
||||||
|
@SafeSlot()
|
||||||
|
def connect_monitor(self, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Connect the target widget to the selected monitor based on the current device and dimension.
|
||||||
|
|
||||||
|
If the selected device is a preview-signal device, it will use the tuple (device, signal) as the monitor.
|
||||||
|
"""
|
||||||
|
dim = self.dim_combo_box.currentText()
|
||||||
|
data = self.device_combo_box.currentData()
|
||||||
|
|
||||||
|
if isinstance(data, tuple):
|
||||||
|
self.image(monitor=data, monitor_type="auto")
|
||||||
|
else:
|
||||||
|
self.image(monitor=self.device_combo_box.currentText(), monitor_type=dim)
|
||||||
|
|
||||||
################################################################################
|
################################################################################
|
||||||
# Data Acquisition
|
# Data Acquisition
|
||||||
|
|
||||||
@ -227,35 +345,21 @@ class Image(ImageBase):
|
|||||||
"""
|
"""
|
||||||
config = self.subscriptions["main"]
|
config = self.subscriptions["main"]
|
||||||
if config.monitor is not None:
|
if config.monitor is not None:
|
||||||
for combo in (
|
for combo in (self.device_combo_box, self.dim_combo_box):
|
||||||
self.selection_bundle.device_combo_box,
|
|
||||||
self.selection_bundle.dim_combo_box,
|
|
||||||
):
|
|
||||||
combo.blockSignals(True)
|
combo.blockSignals(True)
|
||||||
if isinstance(config.monitor, tuple):
|
if isinstance(config.monitor, tuple):
|
||||||
self.selection_bundle.device_combo_box.setCurrentText(
|
self.device_combo_box.setCurrentText(f"{config.monitor[0]}_{config.monitor[1]}")
|
||||||
f"{config.monitor[0]}_{config.monitor[1]}"
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
self.selection_bundle.device_combo_box.setCurrentText(config.monitor)
|
self.device_combo_box.setCurrentText(config.monitor)
|
||||||
self.selection_bundle.dim_combo_box.setCurrentText(config.monitor_type)
|
self.dim_combo_box.setCurrentText(config.monitor_type)
|
||||||
for combo in (
|
for combo in (self.device_combo_box, self.dim_combo_box):
|
||||||
self.selection_bundle.device_combo_box,
|
|
||||||
self.selection_bundle.dim_combo_box,
|
|
||||||
):
|
|
||||||
combo.blockSignals(False)
|
combo.blockSignals(False)
|
||||||
else:
|
else:
|
||||||
for combo in (
|
for combo in (self.device_combo_box, self.dim_combo_box):
|
||||||
self.selection_bundle.device_combo_box,
|
|
||||||
self.selection_bundle.dim_combo_box,
|
|
||||||
):
|
|
||||||
combo.blockSignals(True)
|
combo.blockSignals(True)
|
||||||
self.selection_bundle.device_combo_box.setCurrentText("")
|
self.device_combo_box.setCurrentText("")
|
||||||
self.selection_bundle.dim_combo_box.setCurrentText("auto")
|
self.dim_combo_box.setCurrentText("auto")
|
||||||
for combo in (
|
for combo in (self.device_combo_box, self.dim_combo_box):
|
||||||
self.selection_bundle.device_combo_box,
|
|
||||||
self.selection_bundle.dim_combo_box,
|
|
||||||
):
|
|
||||||
combo.blockSignals(False)
|
combo.blockSignals(False)
|
||||||
|
|
||||||
################################################################################
|
################################################################################
|
||||||
@ -554,8 +658,10 @@ class Image(ImageBase):
|
|||||||
self.subscriptions.clear()
|
self.subscriptions.clear()
|
||||||
|
|
||||||
# Toolbar cleanup
|
# Toolbar cleanup
|
||||||
self.toolbar.widgets["monitor"].widget.close()
|
self.device_combo_box.close()
|
||||||
self.toolbar.widgets["monitor"].widget.deleteLater()
|
self.device_combo_box.deleteLater()
|
||||||
|
self.dim_combo_box.close()
|
||||||
|
self.dim_combo_box.deleteLater()
|
||||||
super().cleanup()
|
super().cleanup()
|
||||||
|
|
||||||
|
|
||||||
@ -570,10 +676,10 @@ if __name__ == "__main__": # pragma: no cover
|
|||||||
ml = QHBoxLayout(win)
|
ml = QHBoxLayout(win)
|
||||||
|
|
||||||
image_popup = Image(popups=True)
|
image_popup = Image(popups=True)
|
||||||
image_side_panel = Image(popups=False)
|
# image_side_panel = Image(popups=False)
|
||||||
|
|
||||||
ml.addWidget(image_popup)
|
ml.addWidget(image_popup)
|
||||||
ml.addWidget(image_side_panel)
|
# ml.addWidget(image_side_panel)
|
||||||
|
|
||||||
win.resize(1500, 800)
|
win.resize(1500, 800)
|
||||||
win.show()
|
win.show()
|
||||||
|
@ -12,14 +12,19 @@ from qtpy.QtWidgets import QDialog, QVBoxLayout
|
|||||||
from bec_widgets.utils.container_utils import WidgetContainerUtils
|
from bec_widgets.utils.container_utils import WidgetContainerUtils
|
||||||
from bec_widgets.utils.error_popups import SafeProperty, SafeSlot
|
from bec_widgets.utils.error_popups import SafeProperty, SafeSlot
|
||||||
from bec_widgets.utils.side_panel import SidePanel
|
from bec_widgets.utils.side_panel import SidePanel
|
||||||
from bec_widgets.utils.toolbar import MaterialIconAction, SwitchableToolBarAction
|
from bec_widgets.utils.toolbars.actions import MaterialIconAction, SwitchableToolBarAction
|
||||||
from bec_widgets.widgets.plots.image.image_item import ImageItem
|
from bec_widgets.widgets.plots.image.image_item import ImageItem
|
||||||
from bec_widgets.widgets.plots.image.image_roi_plot import ImageROIPlot
|
from bec_widgets.widgets.plots.image.image_roi_plot import ImageROIPlot
|
||||||
from bec_widgets.widgets.plots.image.setting_widgets.image_roi_tree import ROIPropertyTree
|
from bec_widgets.widgets.plots.image.setting_widgets.image_roi_tree import ROIPropertyTree
|
||||||
from bec_widgets.widgets.plots.image.toolbar_bundles.image_selection import (
|
from bec_widgets.widgets.plots.image.toolbar_components.image_base_actions import (
|
||||||
MonitorSelectionToolbarBundle,
|
ImageColorbarConnection,
|
||||||
|
ImageProcessingConnection,
|
||||||
|
ImageRoiConnection,
|
||||||
|
image_autorange,
|
||||||
|
image_colorbar,
|
||||||
|
image_processing,
|
||||||
|
image_roi_bundle,
|
||||||
)
|
)
|
||||||
from bec_widgets.widgets.plots.image.toolbar_bundles.processing import ImageProcessingToolbarBundle
|
|
||||||
from bec_widgets.widgets.plots.plot_base import PlotBase
|
from bec_widgets.widgets.plots.plot_base import PlotBase
|
||||||
from bec_widgets.widgets.plots.roi.image_roi import (
|
from bec_widgets.widgets.plots.roi.image_roi import (
|
||||||
BaseROI,
|
BaseROI,
|
||||||
@ -256,6 +261,7 @@ class ImageBase(PlotBase):
|
|||||||
self.x_roi = None
|
self.x_roi = None
|
||||||
self.y_roi = None
|
self.y_roi = None
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
self.roi_controller = ROIController(colormap="viridis")
|
self.roi_controller = ROIController(colormap="viridis")
|
||||||
|
|
||||||
# Headless controller keeps the canonical list.
|
# Headless controller keeps the canonical list.
|
||||||
@ -264,6 +270,7 @@ class ImageBase(PlotBase):
|
|||||||
self, plot_item=self.plot_item, on_add=self.layer_added, on_remove=self.layer_removed
|
self, plot_item=self.plot_item, on_add=self.layer_added, on_remove=self.layer_removed
|
||||||
)
|
)
|
||||||
self.layer_manager.add("main")
|
self.layer_manager.add("main")
|
||||||
|
self._init_image_base_toolbar()
|
||||||
|
|
||||||
self.autorange = True
|
self.autorange = True
|
||||||
self.autorange_mode = "mean"
|
self.autorange_mode = "mean"
|
||||||
@ -274,6 +281,16 @@ class ImageBase(PlotBase):
|
|||||||
# Refresh theme for ROI plots
|
# Refresh theme for ROI plots
|
||||||
self._update_theme()
|
self._update_theme()
|
||||||
|
|
||||||
|
self.toolbar.show_bundles(
|
||||||
|
[
|
||||||
|
"image_crosshair",
|
||||||
|
"mouse_interaction",
|
||||||
|
"image_autorange",
|
||||||
|
"image_colorbar",
|
||||||
|
"image_processing",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
################################################################################
|
################################################################################
|
||||||
# Widget Specific GUI interactions
|
# Widget Specific GUI interactions
|
||||||
################################################################################
|
################################################################################
|
||||||
@ -318,134 +335,65 @@ class ImageBase(PlotBase):
|
|||||||
"""
|
"""
|
||||||
return list(self.layer_manager.layers.values())
|
return list(self.layer_manager.layers.values())
|
||||||
|
|
||||||
def _init_toolbar(self):
|
def _init_image_base_toolbar(self):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# add to the first position
|
|
||||||
self.selection_bundle = MonitorSelectionToolbarBundle(
|
|
||||||
bundle_id="selection", target_widget=self
|
|
||||||
)
|
|
||||||
self.toolbar.add_bundle(self.selection_bundle, self)
|
|
||||||
|
|
||||||
super()._init_toolbar()
|
# ROI Actions
|
||||||
|
self.toolbar.add_bundle(image_roi_bundle(self.toolbar.components))
|
||||||
# Image specific changes to PlotBase toolbar
|
self.toolbar.connect_bundle(
|
||||||
self.toolbar.widgets["reset_legend"].action.setVisible(False)
|
"image_base", ImageRoiConnection(self.toolbar.components, target_widget=self)
|
||||||
|
|
||||||
# ROI Bundle replacement with switchable crosshair
|
|
||||||
self.toolbar.remove_bundle("roi")
|
|
||||||
crosshair = MaterialIconAction(
|
|
||||||
icon_name="point_scan", tooltip="Show Crosshair", checkable=True, parent=self
|
|
||||||
)
|
|
||||||
crosshair_roi = MaterialIconAction(
|
|
||||||
icon_name="my_location",
|
|
||||||
tooltip="Show Crosshair with ROI plots",
|
|
||||||
checkable=True,
|
|
||||||
parent=self,
|
|
||||||
)
|
|
||||||
crosshair_roi.action.toggled.connect(self.toggle_roi_panels)
|
|
||||||
crosshair.action.toggled.connect(self.toggle_crosshair)
|
|
||||||
switch_crosshair = SwitchableToolBarAction(
|
|
||||||
actions={"crosshair_simple": crosshair, "crosshair_roi": crosshair_roi},
|
|
||||||
initial_action="crosshair_simple",
|
|
||||||
tooltip="Crosshair",
|
|
||||||
checkable=True,
|
|
||||||
parent=self,
|
|
||||||
)
|
|
||||||
self.toolbar.add_action(
|
|
||||||
action_id="switch_crosshair", action=switch_crosshair, target_widget=self
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Lock aspect ratio button
|
# Lock Aspect Ratio Action
|
||||||
self.lock_aspect_ratio_action = MaterialIconAction(
|
lock_aspect_ratio_action = MaterialIconAction(
|
||||||
icon_name="aspect_ratio", tooltip="Lock Aspect Ratio", checkable=True, parent=self
|
icon_name="aspect_ratio", tooltip="Lock Aspect Ratio", checkable=True, parent=self
|
||||||
)
|
)
|
||||||
self.toolbar.add_action_to_bundle(
|
self.toolbar.components.add_safe("lock_aspect_ratio", lock_aspect_ratio_action)
|
||||||
bundle_id="mouse_interaction",
|
self.toolbar.get_bundle("mouse_interaction").add_action("lock_aspect_ratio")
|
||||||
action_id="lock_aspect_ratio",
|
lock_aspect_ratio_action.action.toggled.connect(
|
||||||
action=self.lock_aspect_ratio_action,
|
|
||||||
target_widget=self,
|
|
||||||
)
|
|
||||||
self.lock_aspect_ratio_action.action.toggled.connect(
|
|
||||||
lambda checked: self.setProperty("lock_aspect_ratio", checked)
|
lambda checked: self.setProperty("lock_aspect_ratio", checked)
|
||||||
)
|
)
|
||||||
self.lock_aspect_ratio_action.action.setChecked(True)
|
lock_aspect_ratio_action.action.setChecked(True)
|
||||||
|
|
||||||
self._init_autorange_action()
|
# Autorange Action
|
||||||
self._init_colorbar_action()
|
self.toolbar.add_bundle(image_autorange(self.toolbar.components))
|
||||||
|
action = self.toolbar.components.get_action("image_autorange")
|
||||||
# Processing Bundle
|
action.actions["mean"].action.toggled.connect(
|
||||||
self.processing_bundle = ImageProcessingToolbarBundle(
|
|
||||||
bundle_id="processing", target_widget=self
|
|
||||||
)
|
|
||||||
self.toolbar.add_bundle(self.processing_bundle, target_widget=self)
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error initializing toolbar: {e}")
|
|
||||||
|
|
||||||
def _init_autorange_action(self):
|
|
||||||
|
|
||||||
self.autorange_mean_action = MaterialIconAction(
|
|
||||||
icon_name="hdr_auto", tooltip="Enable Auto Range (Mean)", checkable=True, parent=self
|
|
||||||
)
|
|
||||||
self.autorange_max_action = MaterialIconAction(
|
|
||||||
icon_name="hdr_auto",
|
|
||||||
tooltip="Enable Auto Range (Max)",
|
|
||||||
checkable=True,
|
|
||||||
filled=True,
|
|
||||||
parent=self,
|
|
||||||
)
|
|
||||||
|
|
||||||
self.autorange_switch = SwitchableToolBarAction(
|
|
||||||
actions={
|
|
||||||
"auto_range_mean": self.autorange_mean_action,
|
|
||||||
"auto_range_max": self.autorange_max_action,
|
|
||||||
},
|
|
||||||
initial_action="auto_range_mean",
|
|
||||||
tooltip="Enable Auto Range",
|
|
||||||
checkable=True,
|
|
||||||
parent=self,
|
|
||||||
)
|
|
||||||
|
|
||||||
self.toolbar.add_action(
|
|
||||||
action_id="autorange_image", action=self.autorange_switch, target_widget=self
|
|
||||||
)
|
|
||||||
|
|
||||||
self.autorange_mean_action.action.toggled.connect(
|
|
||||||
lambda checked: self.toggle_autorange(checked, mode="mean")
|
lambda checked: self.toggle_autorange(checked, mode="mean")
|
||||||
)
|
)
|
||||||
self.autorange_max_action.action.toggled.connect(
|
action.actions["max"].action.toggled.connect(
|
||||||
lambda checked: self.toggle_autorange(checked, mode="max")
|
lambda checked: self.toggle_autorange(checked, mode="max")
|
||||||
)
|
)
|
||||||
|
|
||||||
def _init_colorbar_action(self):
|
# Colorbar Actions
|
||||||
self.full_colorbar_action = MaterialIconAction(
|
self.toolbar.add_bundle(image_colorbar(self.toolbar.components))
|
||||||
icon_name="edgesensor_low", tooltip="Enable Full Colorbar", checkable=True, parent=self
|
|
||||||
)
|
self.toolbar.connect_bundle(
|
||||||
self.simple_colorbar_action = MaterialIconAction(
|
"image_colorbar",
|
||||||
icon_name="smartphone", tooltip="Enable Simple Colorbar", checkable=True, parent=self
|
ImageColorbarConnection(self.toolbar.components, target_widget=self),
|
||||||
)
|
)
|
||||||
|
|
||||||
self.colorbar_switch = SwitchableToolBarAction(
|
# Image Processing Actions
|
||||||
actions={
|
self.toolbar.add_bundle(image_processing(self.toolbar.components))
|
||||||
"full_colorbar": self.full_colorbar_action,
|
self.toolbar.connect_bundle(
|
||||||
"simple_colorbar": self.simple_colorbar_action,
|
"image_processing",
|
||||||
},
|
ImageProcessingConnection(self.toolbar.components, target_widget=self),
|
||||||
initial_action="full_colorbar",
|
|
||||||
tooltip="Enable Full Colorbar",
|
|
||||||
checkable=True,
|
|
||||||
parent=self,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
self.toolbar.add_action(
|
# ROI Manager Action
|
||||||
action_id="switch_colorbar", action=self.colorbar_switch, target_widget=self
|
self.toolbar.components.add_safe(
|
||||||
|
"roi_mgr",
|
||||||
|
MaterialIconAction(
|
||||||
|
icon_name="view_list", tooltip="ROI Manager", checkable=True, parent=self
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
self.toolbar.get_bundle("axis_popup").add_action("roi_mgr")
|
||||||
self.simple_colorbar_action.action.toggled.connect(
|
self.toolbar.components.get_action("roi_mgr").action.triggered.connect(
|
||||||
lambda checked: self.enable_colorbar(checked, style="simple")
|
self.show_roi_manager_popup
|
||||||
)
|
|
||||||
self.full_colorbar_action.action.toggled.connect(
|
|
||||||
lambda checked: self.enable_colorbar(checked, style="full")
|
|
||||||
)
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error initializing toolbar: {e}")
|
||||||
|
|
||||||
########################################
|
########################################
|
||||||
# ROI Gui Manager
|
# ROI Gui Manager
|
||||||
@ -461,20 +409,8 @@ class ImageBase(PlotBase):
|
|||||||
title="ROI Manager",
|
title="ROI Manager",
|
||||||
)
|
)
|
||||||
|
|
||||||
def add_popups(self):
|
|
||||||
super().add_popups() # keep Axis Settings
|
|
||||||
|
|
||||||
roi_action = MaterialIconAction(
|
|
||||||
icon_name="view_list", tooltip="ROI Manager", checkable=True, parent=self
|
|
||||||
)
|
|
||||||
# self.popup_bundle.add_action("roi_mgr", roi_action)
|
|
||||||
self.toolbar.add_action_to_bundle(
|
|
||||||
bundle_id="popup_bundle", action_id="roi_mgr", action=roi_action, target_widget=self
|
|
||||||
)
|
|
||||||
self.toolbar.widgets["roi_mgr"].action.triggered.connect(self.show_roi_manager_popup)
|
|
||||||
|
|
||||||
def show_roi_manager_popup(self):
|
def show_roi_manager_popup(self):
|
||||||
roi_action = self.toolbar.widgets["roi_mgr"].action
|
roi_action = self.toolbar.components.get_action("roi_mgr").action
|
||||||
if self.roi_manager_dialog is None or not self.roi_manager_dialog.isVisible():
|
if self.roi_manager_dialog is None or not self.roi_manager_dialog.isVisible():
|
||||||
self.roi_mgr = ROIPropertyTree(parent=self, image_widget=self)
|
self.roi_mgr = ROIPropertyTree(parent=self, image_widget=self)
|
||||||
self.roi_manager_dialog = QDialog(modal=False)
|
self.roi_manager_dialog = QDialog(modal=False)
|
||||||
@ -494,7 +430,7 @@ class ImageBase(PlotBase):
|
|||||||
self.roi_manager_dialog.close()
|
self.roi_manager_dialog.close()
|
||||||
self.roi_manager_dialog.deleteLater()
|
self.roi_manager_dialog.deleteLater()
|
||||||
self.roi_manager_dialog = None
|
self.roi_manager_dialog = None
|
||||||
self.toolbar.widgets["roi_mgr"].action.setChecked(False)
|
self.toolbar.components.get_action("roi_mgr").action.setChecked(False)
|
||||||
|
|
||||||
def enable_colorbar(
|
def enable_colorbar(
|
||||||
self,
|
self,
|
||||||
@ -518,12 +454,11 @@ class ImageBase(PlotBase):
|
|||||||
self.plot_widget.removeItem(self._color_bar)
|
self.plot_widget.removeItem(self._color_bar)
|
||||||
self._color_bar = None
|
self._color_bar = None
|
||||||
|
|
||||||
if style == "simple":
|
|
||||||
|
|
||||||
def disable_autorange():
|
def disable_autorange():
|
||||||
print("Disabling autorange")
|
print("Disabling autorange")
|
||||||
self.setProperty("autorange", False)
|
self.setProperty("autorange", False)
|
||||||
|
|
||||||
|
if style == "simple":
|
||||||
self._color_bar = pg.ColorBarItem(colorMap=self.config.color_map)
|
self._color_bar = pg.ColorBarItem(colorMap=self.config.color_map)
|
||||||
self._color_bar.setImageItem(self.layer_manager["main"].image)
|
self._color_bar.setImageItem(self.layer_manager["main"].image)
|
||||||
self._color_bar.sigLevelsChangeFinished.connect(disable_autorange)
|
self._color_bar.sigLevelsChangeFinished.connect(disable_autorange)
|
||||||
@ -532,9 +467,7 @@ class ImageBase(PlotBase):
|
|||||||
self._color_bar = pg.HistogramLUTItem()
|
self._color_bar = pg.HistogramLUTItem()
|
||||||
self._color_bar.setImageItem(self.layer_manager["main"].image)
|
self._color_bar.setImageItem(self.layer_manager["main"].image)
|
||||||
self._color_bar.gradient.loadPreset(self.config.color_map)
|
self._color_bar.gradient.loadPreset(self.config.color_map)
|
||||||
self._color_bar.sigLevelsChanged.connect(
|
self._color_bar.sigLevelsChanged.connect(disable_autorange)
|
||||||
lambda: self.setProperty("autorange", False)
|
|
||||||
)
|
|
||||||
|
|
||||||
self.plot_widget.addItem(self._color_bar, row=0, col=1)
|
self.plot_widget.addItem(self._color_bar, row=0, col=1)
|
||||||
self.config.color_bar = style
|
self.config.color_bar = style
|
||||||
@ -827,6 +760,9 @@ class ImageBase(PlotBase):
|
|||||||
Args:
|
Args:
|
||||||
value(tuple | list | QPointF): The range of values to set.
|
value(tuple | list | QPointF): The range of values to set.
|
||||||
"""
|
"""
|
||||||
|
self._set_vrange(value, disable_autorange=True)
|
||||||
|
|
||||||
|
def _set_vrange(self, value: tuple | list | QPointF, disable_autorange: bool = True):
|
||||||
if isinstance(value, (tuple, list)):
|
if isinstance(value, (tuple, list)):
|
||||||
value = self._tuple_to_qpointf(value)
|
value = self._tuple_to_qpointf(value)
|
||||||
|
|
||||||
@ -835,7 +771,7 @@ class ImageBase(PlotBase):
|
|||||||
for layer in self.layer_manager:
|
for layer in self.layer_manager:
|
||||||
if not layer.sync.v_range:
|
if not layer.sync.v_range:
|
||||||
continue
|
continue
|
||||||
layer.image.v_range = (vmin, vmax)
|
layer.image.set_v_range((vmin, vmax), disable_autorange=disable_autorange)
|
||||||
|
|
||||||
# propagate to colorbar if exists
|
# propagate to colorbar if exists
|
||||||
if self._color_bar:
|
if self._color_bar:
|
||||||
@ -845,7 +781,7 @@ class ImageBase(PlotBase):
|
|||||||
self._color_bar.setLevels(min=vmin, max=vmax)
|
self._color_bar.setLevels(min=vmin, max=vmax)
|
||||||
self._color_bar.setHistogramRange(vmin - 0.1 * vmin, vmax + 0.1 * vmax)
|
self._color_bar.setHistogramRange(vmin - 0.1 * vmin, vmax + 0.1 * vmax)
|
||||||
|
|
||||||
self.autorange_switch.set_state_all(False)
|
# self.toolbar.components.get_action("image_autorange").set_state_all(False)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def v_min(self) -> float:
|
def v_min(self) -> float:
|
||||||
@ -919,14 +855,27 @@ class ImageBase(PlotBase):
|
|||||||
Args:
|
Args:
|
||||||
enabled(bool): Whether to enable autorange.
|
enabled(bool): Whether to enable autorange.
|
||||||
"""
|
"""
|
||||||
|
self._set_autorange(enabled)
|
||||||
|
|
||||||
|
def _set_autorange(self, enabled: bool, sync: bool = True):
|
||||||
|
"""
|
||||||
|
Set the autorange for all layers.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
enabled(bool): Whether to enable autorange.
|
||||||
|
sync(bool): Whether to synchronize the autorange state across all layers.
|
||||||
|
"""
|
||||||
|
print(f"Setting autorange to {enabled}")
|
||||||
for layer in self.layer_manager:
|
for layer in self.layer_manager:
|
||||||
if not layer.sync.autorange:
|
if not layer.sync.autorange:
|
||||||
continue
|
continue
|
||||||
layer.image.autorange = enabled
|
layer.image.autorange = enabled
|
||||||
if enabled and layer.image.raw_data is not None:
|
if enabled and layer.image.raw_data is not None:
|
||||||
layer.image.apply_autorange()
|
layer.image.apply_autorange()
|
||||||
|
# if sync:
|
||||||
self._sync_colorbar_levels()
|
self._sync_colorbar_levels()
|
||||||
self._sync_autorange_switch()
|
self._sync_autorange_switch()
|
||||||
|
print(f"Autorange set to {enabled}")
|
||||||
|
|
||||||
@SafeProperty(str)
|
@SafeProperty(str)
|
||||||
def autorange_mode(self) -> str:
|
def autorange_mode(self) -> str:
|
||||||
@ -948,6 +897,7 @@ class ImageBase(PlotBase):
|
|||||||
Args:
|
Args:
|
||||||
mode(str): The autorange mode. Options are "max" or "mean".
|
mode(str): The autorange mode. Options are "max" or "mean".
|
||||||
"""
|
"""
|
||||||
|
print(f"Setting autorange mode to {mode}")
|
||||||
# for qt Designer
|
# for qt Designer
|
||||||
if mode not in ["max", "mean"]:
|
if mode not in ["max", "mean"]:
|
||||||
return
|
return
|
||||||
@ -969,7 +919,7 @@ class ImageBase(PlotBase):
|
|||||||
"""
|
"""
|
||||||
if not self.layer_manager:
|
if not self.layer_manager:
|
||||||
return
|
return
|
||||||
|
print(f"Toggling autorange to {enabled} with mode {mode}")
|
||||||
for layer in self.layer_manager:
|
for layer in self.layer_manager:
|
||||||
if layer.sync.autorange:
|
if layer.sync.autorange:
|
||||||
layer.image.autorange = enabled
|
layer.image.autorange = enabled
|
||||||
@ -981,19 +931,16 @@ class ImageBase(PlotBase):
|
|||||||
# We only need to apply autorange if we enabled it
|
# We only need to apply autorange if we enabled it
|
||||||
layer.image.apply_autorange()
|
layer.image.apply_autorange()
|
||||||
|
|
||||||
if enabled:
|
|
||||||
self._sync_colorbar_levels()
|
self._sync_colorbar_levels()
|
||||||
|
|
||||||
def _sync_autorange_switch(self):
|
def _sync_autorange_switch(self):
|
||||||
"""
|
"""
|
||||||
Synchronize the autorange switch with the current autorange state and mode if changed from outside.
|
Synchronize the autorange switch with the current autorange state and mode if changed from outside.
|
||||||
"""
|
"""
|
||||||
self.autorange_switch.block_all_signals(True)
|
action: SwitchableToolBarAction = self.toolbar.components.get_action("image_autorange") # type: ignore
|
||||||
self.autorange_switch.set_default_action(
|
with action.signal_blocker():
|
||||||
f"auto_range_{self.layer_manager['main'].image.autorange_mode}"
|
action.set_default_action(f"{self.layer_manager['main'].image.autorange_mode}")
|
||||||
)
|
action.set_state_all(self.layer_manager["main"].image.autorange)
|
||||||
self.autorange_switch.set_state_all(self.layer_manager["main"].image.autorange)
|
|
||||||
self.autorange_switch.block_all_signals(False)
|
|
||||||
|
|
||||||
def _sync_colorbar_levels(self):
|
def _sync_colorbar_levels(self):
|
||||||
"""Immediately propagate current levels to the active colorbar."""
|
"""Immediately propagate current levels to the active colorbar."""
|
||||||
@ -1009,20 +956,22 @@ class ImageBase(PlotBase):
|
|||||||
total_vrange = (min(total_vrange[0], img.v_min), max(total_vrange[1], img.v_max))
|
total_vrange = (min(total_vrange[0], img.v_min), max(total_vrange[1], img.v_max))
|
||||||
|
|
||||||
self._color_bar.blockSignals(True)
|
self._color_bar.blockSignals(True)
|
||||||
self.v_range = total_vrange # type: ignore
|
self._set_vrange(total_vrange, disable_autorange=False) # type: ignore
|
||||||
self._color_bar.blockSignals(False)
|
self._color_bar.blockSignals(False)
|
||||||
|
|
||||||
def _sync_colorbar_actions(self):
|
def _sync_colorbar_actions(self):
|
||||||
"""
|
"""
|
||||||
Synchronize the colorbar actions with the current colorbar state.
|
Synchronize the colorbar actions with the current colorbar state.
|
||||||
"""
|
"""
|
||||||
self.colorbar_switch.block_all_signals(True)
|
colorbar_switch: SwitchableToolBarAction = self.toolbar.components.get_action(
|
||||||
|
"image_colorbar_switch"
|
||||||
|
)
|
||||||
|
with colorbar_switch.signal_blocker():
|
||||||
if self._color_bar is not None:
|
if self._color_bar is not None:
|
||||||
self.colorbar_switch.set_default_action(f"{self.config.color_bar}_colorbar")
|
colorbar_switch.set_default_action(f"{self.config.color_bar}_colorbar")
|
||||||
self.colorbar_switch.set_state_all(True)
|
colorbar_switch.set_state_all(True)
|
||||||
else:
|
else:
|
||||||
self.colorbar_switch.set_state_all(False)
|
colorbar_switch.set_state_all(False)
|
||||||
self.colorbar_switch.block_all_signals(False)
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def cleanup_histogram_lut_item(histogram_lut_item: pg.HistogramLUTItem):
|
def cleanup_histogram_lut_item(histogram_lut_item: pg.HistogramLUTItem):
|
||||||
|
@ -20,7 +20,9 @@ from qtpy.QtWidgets import (
|
|||||||
|
|
||||||
from bec_widgets import BECWidget
|
from bec_widgets import BECWidget
|
||||||
from bec_widgets.utils import BECDispatcher, ConnectionConfig
|
from bec_widgets.utils import BECDispatcher, ConnectionConfig
|
||||||
from bec_widgets.utils.toolbar import MaterialIconAction, ModularToolBar
|
from bec_widgets.utils.toolbars.actions import WidgetAction
|
||||||
|
from bec_widgets.utils.toolbars.bundles import ToolbarBundle
|
||||||
|
from bec_widgets.utils.toolbars.toolbar import MaterialIconAction, ModularToolBar
|
||||||
from bec_widgets.widgets.plots.roi.image_roi import (
|
from bec_widgets.widgets.plots.roi.image_roi import (
|
||||||
BaseROI,
|
BaseROI,
|
||||||
CircularROI,
|
CircularROI,
|
||||||
@ -121,20 +123,33 @@ class ROIPropertyTree(BECWidget, QWidget):
|
|||||||
|
|
||||||
# --------------------------------------------------------------------- UI
|
# --------------------------------------------------------------------- UI
|
||||||
def _init_toolbar(self):
|
def _init_toolbar(self):
|
||||||
tb = ModularToolBar(self, self, orientation="horizontal")
|
tb = self.toolbar = ModularToolBar(self, orientation="horizontal")
|
||||||
self._draw_actions: dict[str, MaterialIconAction] = {}
|
self._draw_actions: dict[str, MaterialIconAction] = {}
|
||||||
# --- ROI draw actions (toggleable) ---
|
# --- ROI draw actions (toggleable) ---
|
||||||
self.add_rect_action = MaterialIconAction("add_box", "Add Rect ROI", True, self)
|
|
||||||
tb.add_action("Add Rect ROI", self.add_rect_action, self)
|
|
||||||
self._draw_actions["rect"] = self.add_rect_action
|
|
||||||
self.add_circle_action = MaterialIconAction("add_circle", "Add Circle ROI", True, self)
|
|
||||||
tb.add_action("Add Circle ROI", self.add_circle_action, self)
|
|
||||||
self._draw_actions["circle"] = self.add_circle_action
|
|
||||||
# --- Ellipse ROI draw action ---
|
|
||||||
self.add_ellipse_action = MaterialIconAction("vignette", "Add Ellipse ROI", True, self)
|
|
||||||
tb.add_action("Add Ellipse ROI", self.add_ellipse_action, self)
|
|
||||||
self._draw_actions["ellipse"] = self.add_ellipse_action
|
|
||||||
|
|
||||||
|
tb.components.add_safe(
|
||||||
|
"roi_rectangle",
|
||||||
|
MaterialIconAction("add_box", "Add Rect ROI", checkable=True, parent=self),
|
||||||
|
)
|
||||||
|
tb.components.add_safe(
|
||||||
|
"roi_circle",
|
||||||
|
MaterialIconAction("add_circle", "Add Circle ROI", checkable=True, parent=self),
|
||||||
|
)
|
||||||
|
tb.components.add_safe(
|
||||||
|
"roi_ellipse",
|
||||||
|
MaterialIconAction("vignette", "Add Ellipse ROI", checkable=True, parent=self),
|
||||||
|
)
|
||||||
|
bundle = ToolbarBundle("roi_draw", tb.components)
|
||||||
|
bundle.add_action("roi_rectangle")
|
||||||
|
bundle.add_action("roi_circle")
|
||||||
|
bundle.add_action("roi_ellipse")
|
||||||
|
tb.add_bundle(bundle)
|
||||||
|
|
||||||
|
self._draw_actions = {
|
||||||
|
"rect": tb.components.get_action("roi_rectangle"),
|
||||||
|
"circle": tb.components.get_action("roi_circle"),
|
||||||
|
"ellipse": tb.components.get_action("roi_ellipse"),
|
||||||
|
}
|
||||||
for mode, act in self._draw_actions.items():
|
for mode, act in self._draw_actions.items():
|
||||||
act.action.toggled.connect(lambda on, m=mode: self._on_draw_action_toggled(m, on))
|
act.action.toggled.connect(lambda on, m=mode: self._on_draw_action_toggled(m, on))
|
||||||
|
|
||||||
@ -142,7 +157,7 @@ class ROIPropertyTree(BECWidget, QWidget):
|
|||||||
self.expand_toggle = MaterialIconAction(
|
self.expand_toggle = MaterialIconAction(
|
||||||
"unfold_more", "Expand/Collapse", checkable=True, parent=self # icon when collapsed
|
"unfold_more", "Expand/Collapse", checkable=True, parent=self # icon when collapsed
|
||||||
)
|
)
|
||||||
tb.add_action("Expand/Collapse", self.expand_toggle, self)
|
tb.components.add_safe("expand_toggle", self.expand_toggle)
|
||||||
|
|
||||||
def _exp_toggled(on: bool):
|
def _exp_toggled(on: bool):
|
||||||
if on:
|
if on:
|
||||||
@ -163,7 +178,7 @@ class ROIPropertyTree(BECWidget, QWidget):
|
|||||||
self.lock_all_action = MaterialIconAction(
|
self.lock_all_action = MaterialIconAction(
|
||||||
"lock_open_right", "Lock/Unlock all ROIs", checkable=True, parent=self
|
"lock_open_right", "Lock/Unlock all ROIs", checkable=True, parent=self
|
||||||
)
|
)
|
||||||
tb.add_action("Lock/Unlock all ROIs", self.lock_all_action, self)
|
tb.components.add_safe("lock_unlock_all", self.lock_all_action)
|
||||||
|
|
||||||
def _lock_all(checked: bool):
|
def _lock_all(checked: bool):
|
||||||
# checked -> everything locked (movable = False)
|
# checked -> everything locked (movable = False)
|
||||||
@ -178,12 +193,23 @@ class ROIPropertyTree(BECWidget, QWidget):
|
|||||||
|
|
||||||
# colormap widget
|
# colormap widget
|
||||||
self.cmap = BECColorMapWidget(cmap=self.controller.colormap)
|
self.cmap = BECColorMapWidget(cmap=self.controller.colormap)
|
||||||
tb.addWidget(QWidget()) # spacer
|
|
||||||
tb.addWidget(self.cmap)
|
tb.components.add_safe("roi_tree_spacer", WidgetAction(widget=QWidget()))
|
||||||
|
tb.components.add_safe("roi_tree_cmap", WidgetAction(widget=self.cmap))
|
||||||
|
|
||||||
self.cmap.colormap_changed_signal.connect(self.controller.set_colormap)
|
self.cmap.colormap_changed_signal.connect(self.controller.set_colormap)
|
||||||
self.layout.addWidget(tb)
|
self.layout.addWidget(tb)
|
||||||
self.controller.paletteChanged.connect(lambda cmap: setattr(self.cmap, "colormap", cmap))
|
self.controller.paletteChanged.connect(lambda cmap: setattr(self.cmap, "colormap", cmap))
|
||||||
|
|
||||||
|
bundle = ToolbarBundle("roi_tools", tb.components)
|
||||||
|
bundle.add_action("expand_toggle")
|
||||||
|
bundle.add_action("lock_unlock_all")
|
||||||
|
bundle.add_action("roi_tree_spacer")
|
||||||
|
bundle.add_action("roi_tree_cmap")
|
||||||
|
tb.add_bundle(bundle)
|
||||||
|
|
||||||
|
tb.show_bundles(["roi_draw", "roi_tools"])
|
||||||
|
|
||||||
# ROI drawing state
|
# ROI drawing state
|
||||||
self._roi_draw_mode = None # 'rect' | 'circle' | 'ellipse' | None
|
self._roi_draw_mode = None # 'rect' | 'circle' | 'ellipse' | None
|
||||||
self._roi_start_pos = None # QPointF in image coords
|
self._roi_start_pos = None # QPointF in image coords
|
||||||
|
@ -1,107 +0,0 @@
|
|||||||
from bec_lib.device import ReadoutPriority
|
|
||||||
from qtpy.QtCore import Qt, QTimer
|
|
||||||
from qtpy.QtWidgets import QComboBox, QStyledItemDelegate
|
|
||||||
|
|
||||||
from bec_widgets.utils.error_popups import SafeSlot
|
|
||||||
from bec_widgets.utils.toolbar import ToolbarBundle, WidgetAction
|
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
class NoCheckDelegate(QStyledItemDelegate):
|
|
||||||
"""To reduce space in combo boxes by removing the checkmark."""
|
|
||||||
|
|
||||||
def initStyleOption(self, option, index):
|
|
||||||
super().initStyleOption(option, index)
|
|
||||||
# Remove any check indicator
|
|
||||||
option.checkState = Qt.Unchecked
|
|
||||||
|
|
||||||
|
|
||||||
class MonitorSelectionToolbarBundle(ToolbarBundle):
|
|
||||||
"""
|
|
||||||
A bundle of actions for a toolbar that controls monitor selection on a plot.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, bundle_id="device_selection", target_widget=None, **kwargs):
|
|
||||||
super().__init__(bundle_id=bundle_id, actions=[], **kwargs)
|
|
||||||
self.target_widget = target_widget
|
|
||||||
|
|
||||||
# 1) Device combo box
|
|
||||||
self.device_combo_box = DeviceComboBox(
|
|
||||||
parent=self.target_widget,
|
|
||||||
device_filter=BECDeviceFilter.DEVICE,
|
|
||||||
readout_priority_filter=[ReadoutPriority.ASYNC],
|
|
||||||
)
|
|
||||||
self.device_combo_box.addItem("", None)
|
|
||||||
self.device_combo_box.setCurrentText("")
|
|
||||||
self.device_combo_box.setToolTip("Select Device")
|
|
||||||
self.device_combo_box.setFixedWidth(150)
|
|
||||||
self.device_combo_box.setItemDelegate(NoCheckDelegate(self.device_combo_box))
|
|
||||||
|
|
||||||
self.add_action("monitor", WidgetAction(widget=self.device_combo_box, adjust_size=False))
|
|
||||||
|
|
||||||
# 2) Dimension combo box
|
|
||||||
self.dim_combo_box = QComboBox(parent=self.target_widget)
|
|
||||||
self.dim_combo_box.addItems(["auto", "1d", "2d"])
|
|
||||||
self.dim_combo_box.setCurrentText("auto")
|
|
||||||
self.dim_combo_box.setToolTip("Monitor Dimension")
|
|
||||||
self.dim_combo_box.setFixedWidth(100)
|
|
||||||
self.dim_combo_box.setItemDelegate(NoCheckDelegate(self.dim_combo_box))
|
|
||||||
|
|
||||||
self.add_action("dim_combo", WidgetAction(widget=self.dim_combo_box, adjust_size=False))
|
|
||||||
|
|
||||||
self.device_combo_box.currentTextChanged.connect(self.connect_monitor)
|
|
||||||
self.dim_combo_box.currentTextChanged.connect(self.connect_monitor)
|
|
||||||
|
|
||||||
QTimer.singleShot(0, self._adjust_and_connect)
|
|
||||||
|
|
||||||
def _adjust_and_connect(self):
|
|
||||||
"""
|
|
||||||
Adjust the size of the device combo box and populate it with preview signals.
|
|
||||||
Has to be done with QTimer.singleShot to ensure the UI is fully initialized, needed for testing.
|
|
||||||
"""
|
|
||||||
self._populate_preview_signals()
|
|
||||||
self._reverse_device_items()
|
|
||||||
self.device_combo_box.setCurrentText("") # set again default to empty string
|
|
||||||
|
|
||||||
def _populate_preview_signals(self) -> None:
|
|
||||||
"""
|
|
||||||
Populate the device combo box with preview‑signal devices in the
|
|
||||||
format '<device>_<signal>' and store the tuple(device, signal) in
|
|
||||||
the item's userData for later use.
|
|
||||||
"""
|
|
||||||
preview_signals = self.target_widget.client.device_manager.get_bec_signals("PreviewSignal")
|
|
||||||
for device, signal, signal_config in preview_signals:
|
|
||||||
label = signal_config.get("obj_name", f"{device}_{signal}")
|
|
||||||
self.device_combo_box.addItem(label, (device, signal, signal_config))
|
|
||||||
|
|
||||||
def _reverse_device_items(self) -> None:
|
|
||||||
"""
|
|
||||||
Reverse the current order of items in the device combo box while
|
|
||||||
keeping their userData and restoring the previous selection.
|
|
||||||
"""
|
|
||||||
current_text = self.device_combo_box.currentText()
|
|
||||||
items = [
|
|
||||||
(self.device_combo_box.itemText(i), self.device_combo_box.itemData(i))
|
|
||||||
for i in range(self.device_combo_box.count())
|
|
||||||
]
|
|
||||||
self.device_combo_box.clear()
|
|
||||||
for text, data in reversed(items):
|
|
||||||
self.device_combo_box.addItem(text, data)
|
|
||||||
if current_text:
|
|
||||||
self.device_combo_box.setCurrentText(current_text)
|
|
||||||
|
|
||||||
@SafeSlot()
|
|
||||||
def connect_monitor(self, *args, **kwargs):
|
|
||||||
"""
|
|
||||||
Connect the target widget to the selected monitor based on the current device and dimension.
|
|
||||||
|
|
||||||
If the selected device is a preview-signal device, it will use the tuple (device, signal) as the monitor.
|
|
||||||
"""
|
|
||||||
dim = self.dim_combo_box.currentText()
|
|
||||||
data = self.device_combo_box.currentData()
|
|
||||||
|
|
||||||
if isinstance(data, tuple):
|
|
||||||
self.target_widget.image(monitor=data, monitor_type="auto")
|
|
||||||
else:
|
|
||||||
self.target_widget.image(monitor=self.device_combo_box.currentText(), monitor_type=dim)
|
|
@ -1,92 +0,0 @@
|
|||||||
from bec_widgets.utils.error_popups import SafeSlot
|
|
||||||
from bec_widgets.utils.toolbar import MaterialIconAction, ToolbarBundle
|
|
||||||
|
|
||||||
|
|
||||||
class ImageProcessingToolbarBundle(ToolbarBundle):
|
|
||||||
"""
|
|
||||||
A bundle of actions for a toolbar that controls processing of monitor.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, bundle_id="mouse_interaction", target_widget=None, **kwargs):
|
|
||||||
super().__init__(bundle_id=bundle_id, actions=[], **kwargs)
|
|
||||||
self.target_widget = target_widget
|
|
||||||
|
|
||||||
self.fft = MaterialIconAction(
|
|
||||||
icon_name="fft", tooltip="Toggle FFT", checkable=True, parent=self.target_widget
|
|
||||||
)
|
|
||||||
self.log = MaterialIconAction(
|
|
||||||
icon_name="log_scale", tooltip="Toggle Log", checkable=True, parent=self.target_widget
|
|
||||||
)
|
|
||||||
self.transpose = MaterialIconAction(
|
|
||||||
icon_name="transform",
|
|
||||||
tooltip="Transpose Image",
|
|
||||||
checkable=True,
|
|
||||||
parent=self.target_widget,
|
|
||||||
)
|
|
||||||
self.right = MaterialIconAction(
|
|
||||||
icon_name="rotate_right",
|
|
||||||
tooltip="Rotate image clockwise by 90 deg",
|
|
||||||
parent=self.target_widget,
|
|
||||||
)
|
|
||||||
self.left = MaterialIconAction(
|
|
||||||
icon_name="rotate_left",
|
|
||||||
tooltip="Rotate image counterclockwise by 90 deg",
|
|
||||||
parent=self.target_widget,
|
|
||||||
)
|
|
||||||
self.reset = MaterialIconAction(
|
|
||||||
icon_name="reset_settings", tooltip="Reset Image Settings", parent=self.target_widget
|
|
||||||
)
|
|
||||||
|
|
||||||
self.add_action("fft", self.fft)
|
|
||||||
self.add_action("log", self.log)
|
|
||||||
self.add_action("transpose", self.transpose)
|
|
||||||
self.add_action("rotate_right", self.right)
|
|
||||||
self.add_action("rotate_left", self.left)
|
|
||||||
self.add_action("reset", self.reset)
|
|
||||||
|
|
||||||
self.fft.action.triggered.connect(self.toggle_fft)
|
|
||||||
self.log.action.triggered.connect(self.toggle_log)
|
|
||||||
self.transpose.action.triggered.connect(self.toggle_transpose)
|
|
||||||
self.right.action.triggered.connect(self.rotate_right)
|
|
||||||
self.left.action.triggered.connect(self.rotate_left)
|
|
||||||
self.reset.action.triggered.connect(self.reset_settings)
|
|
||||||
|
|
||||||
@SafeSlot()
|
|
||||||
def toggle_fft(self):
|
|
||||||
checked = self.fft.action.isChecked()
|
|
||||||
self.target_widget.fft = checked
|
|
||||||
|
|
||||||
@SafeSlot()
|
|
||||||
def toggle_log(self):
|
|
||||||
checked = self.log.action.isChecked()
|
|
||||||
self.target_widget.log = checked
|
|
||||||
|
|
||||||
@SafeSlot()
|
|
||||||
def toggle_transpose(self):
|
|
||||||
checked = self.transpose.action.isChecked()
|
|
||||||
self.target_widget.transpose = checked
|
|
||||||
|
|
||||||
@SafeSlot()
|
|
||||||
def rotate_right(self):
|
|
||||||
if self.target_widget.num_rotation_90 is None:
|
|
||||||
return
|
|
||||||
rotation = (self.target_widget.num_rotation_90 - 1) % 4
|
|
||||||
self.target_widget.num_rotation_90 = rotation
|
|
||||||
|
|
||||||
@SafeSlot()
|
|
||||||
def rotate_left(self):
|
|
||||||
if self.target_widget.num_rotation_90 is None:
|
|
||||||
return
|
|
||||||
rotation = (self.target_widget.num_rotation_90 + 1) % 4
|
|
||||||
self.target_widget.num_rotation_90 = rotation
|
|
||||||
|
|
||||||
@SafeSlot()
|
|
||||||
def reset_settings(self):
|
|
||||||
self.target_widget.fft = False
|
|
||||||
self.target_widget.log = False
|
|
||||||
self.target_widget.transpose = False
|
|
||||||
self.target_widget.num_rotation_90 = 0
|
|
||||||
|
|
||||||
self.fft.action.setChecked(False)
|
|
||||||
self.log.action.setChecked(False)
|
|
||||||
self.transpose.action.setChecked(False)
|
|
@ -0,0 +1,390 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from bec_widgets.utils.error_popups import SafeSlot
|
||||||
|
from bec_widgets.utils.toolbars.actions import MaterialIconAction, SwitchableToolBarAction
|
||||||
|
from bec_widgets.utils.toolbars.bundles import ToolbarBundle, ToolbarComponents
|
||||||
|
from bec_widgets.utils.toolbars.connections import BundleConnection
|
||||||
|
|
||||||
|
|
||||||
|
def image_roi_bundle(components: ToolbarComponents) -> ToolbarBundle:
|
||||||
|
"""
|
||||||
|
Creates a toolbar bundle for ROI and crosshair interaction.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
components (ToolbarComponents): The components to be added to the bundle.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
ToolbarBundle: The ROI toolbar bundle.
|
||||||
|
"""
|
||||||
|
components.add_safe(
|
||||||
|
"image_crosshair",
|
||||||
|
MaterialIconAction(
|
||||||
|
icon_name="point_scan",
|
||||||
|
tooltip="Show Crosshair",
|
||||||
|
checkable=True,
|
||||||
|
parent=components.toolbar,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
components.add_safe(
|
||||||
|
"image_crosshair_roi",
|
||||||
|
MaterialIconAction(
|
||||||
|
icon_name="my_location",
|
||||||
|
tooltip="Show Crosshair with ROI plots",
|
||||||
|
checkable=True,
|
||||||
|
parent=components.toolbar,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
components.add_safe(
|
||||||
|
"image_switch_crosshair",
|
||||||
|
SwitchableToolBarAction(
|
||||||
|
actions={
|
||||||
|
"crosshair": components.get_action_reference("image_crosshair")(),
|
||||||
|
"crosshair_roi": components.get_action_reference("image_crosshair_roi")(),
|
||||||
|
},
|
||||||
|
initial_action="crosshair",
|
||||||
|
tooltip="Crosshair",
|
||||||
|
checkable=True,
|
||||||
|
parent=components.toolbar,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
bundle = ToolbarBundle("image_crosshair", components)
|
||||||
|
bundle.add_action("image_switch_crosshair")
|
||||||
|
return bundle
|
||||||
|
|
||||||
|
|
||||||
|
class ImageRoiConnection(BundleConnection):
|
||||||
|
"""
|
||||||
|
Connection class for the ROI toolbar bundle.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, components: ToolbarComponents, target_widget=None):
|
||||||
|
self.bundle_name = "roi"
|
||||||
|
self.components = components
|
||||||
|
self.target_widget = target_widget
|
||||||
|
if not hasattr(self.target_widget, "toggle_roi_panels") or not hasattr(
|
||||||
|
self.target_widget, "toggle_crosshair"
|
||||||
|
):
|
||||||
|
raise AttributeError(
|
||||||
|
"Target widget must implement 'toggle_roi_panels' and 'toggle_crosshair'."
|
||||||
|
)
|
||||||
|
super().__init__()
|
||||||
|
self._connected = False
|
||||||
|
|
||||||
|
def connect(self):
|
||||||
|
self._connected = True
|
||||||
|
# Connect the action to the target widget's method
|
||||||
|
self.components.get_action("image_crosshair").action.toggled.connect(
|
||||||
|
self.target_widget.toggle_crosshair
|
||||||
|
)
|
||||||
|
self.components.get_action("image_crosshair_roi").action.triggered.connect(
|
||||||
|
self.target_widget.toggle_roi_panels
|
||||||
|
)
|
||||||
|
|
||||||
|
def disconnect(self):
|
||||||
|
if not self._connected:
|
||||||
|
return
|
||||||
|
# Disconnect the action from the target widget's method
|
||||||
|
self.components.get_action("image_crosshair").action.toggled.disconnect(
|
||||||
|
self.target_widget.toggle_crosshair
|
||||||
|
)
|
||||||
|
self.components.get_action("image_crosshair_roi").action.triggered.disconnect(
|
||||||
|
self.target_widget.toggle_roi_panels
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def image_autorange(components: ToolbarComponents) -> ToolbarBundle:
|
||||||
|
"""
|
||||||
|
Creates a toolbar bundle for image autorange functionality.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
components (ToolbarComponents): The components to be added to the bundle.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
ToolbarBundle: The autorange toolbar bundle.
|
||||||
|
"""
|
||||||
|
components.add_safe(
|
||||||
|
"image_autorange_mean",
|
||||||
|
MaterialIconAction(
|
||||||
|
icon_name="hdr_auto",
|
||||||
|
tooltip="Enable Auto Range (Mean)",
|
||||||
|
checkable=True,
|
||||||
|
parent=components.toolbar,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
components.add_safe(
|
||||||
|
"image_autorange_max",
|
||||||
|
MaterialIconAction(
|
||||||
|
icon_name="hdr_auto",
|
||||||
|
tooltip="Enable Auto Range (Max)",
|
||||||
|
checkable=True,
|
||||||
|
parent=components.toolbar,
|
||||||
|
filled=True,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
components.add_safe(
|
||||||
|
"image_autorange",
|
||||||
|
SwitchableToolBarAction(
|
||||||
|
actions={
|
||||||
|
"mean": components.get_action_reference("image_autorange_mean")(),
|
||||||
|
"max": components.get_action_reference("image_autorange_max")(),
|
||||||
|
},
|
||||||
|
initial_action="mean",
|
||||||
|
tooltip="Autorange",
|
||||||
|
checkable=True,
|
||||||
|
parent=components.toolbar,
|
||||||
|
default_state_checked=True,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
bundle = ToolbarBundle("image_autorange", components)
|
||||||
|
bundle.add_action("image_autorange")
|
||||||
|
return bundle
|
||||||
|
|
||||||
|
|
||||||
|
def image_colorbar(components: ToolbarComponents) -> ToolbarBundle:
|
||||||
|
"""
|
||||||
|
Creates a toolbar bundle for image colorbar functionality.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
components (ToolbarComponents): The components to be added to the bundle.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
ToolbarBundle: The colorbar toolbar bundle.
|
||||||
|
"""
|
||||||
|
components.add_safe(
|
||||||
|
"image_full_colorbar",
|
||||||
|
MaterialIconAction(
|
||||||
|
icon_name="edgesensor_low",
|
||||||
|
tooltip="Enable Full Colorbar",
|
||||||
|
checkable=True,
|
||||||
|
parent=components.toolbar,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
components.add_safe(
|
||||||
|
"image_simple_colorbar",
|
||||||
|
MaterialIconAction(
|
||||||
|
icon_name="smartphone",
|
||||||
|
tooltip="Enable Simple Colorbar",
|
||||||
|
checkable=True,
|
||||||
|
parent=components.toolbar,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
components.add_safe(
|
||||||
|
"image_colorbar_switch",
|
||||||
|
SwitchableToolBarAction(
|
||||||
|
actions={
|
||||||
|
"full_colorbar": components.get_action_reference("image_full_colorbar")(),
|
||||||
|
"simple_colorbar": components.get_action_reference("image_simple_colorbar")(),
|
||||||
|
},
|
||||||
|
initial_action="full_colorbar",
|
||||||
|
tooltip="Colorbar",
|
||||||
|
checkable=True,
|
||||||
|
parent=components.toolbar,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
bundle = ToolbarBundle("image_colorbar", components)
|
||||||
|
bundle.add_action("image_colorbar_switch")
|
||||||
|
return bundle
|
||||||
|
|
||||||
|
|
||||||
|
class ImageColorbarConnection(BundleConnection):
|
||||||
|
"""
|
||||||
|
Connection class for the image colorbar toolbar bundle.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, components: ToolbarComponents, target_widget=None):
|
||||||
|
self.bundle_name = "image_colorbar"
|
||||||
|
self.components = components
|
||||||
|
self.target_widget = target_widget
|
||||||
|
if not hasattr(self.target_widget, "enable_colorbar"):
|
||||||
|
raise AttributeError("Target widget must implement 'enable_colorbar' method.")
|
||||||
|
super().__init__()
|
||||||
|
self._connected = False
|
||||||
|
|
||||||
|
def _enable_full_colorbar(self, checked: bool):
|
||||||
|
"""
|
||||||
|
Enable or disable the full colorbar based on the checked state.
|
||||||
|
"""
|
||||||
|
self.target_widget.enable_colorbar(checked, style="full")
|
||||||
|
|
||||||
|
def _enable_simple_colorbar(self, checked: bool):
|
||||||
|
"""
|
||||||
|
Enable or disable the simple colorbar based on the checked state.
|
||||||
|
"""
|
||||||
|
self.target_widget.enable_colorbar(checked, style="simple")
|
||||||
|
|
||||||
|
def connect(self):
|
||||||
|
self._connected = True
|
||||||
|
# Connect the action to the target widget's method
|
||||||
|
self.components.get_action("image_full_colorbar").action.toggled.connect(
|
||||||
|
self._enable_full_colorbar
|
||||||
|
)
|
||||||
|
self.components.get_action("image_simple_colorbar").action.toggled.connect(
|
||||||
|
self._enable_simple_colorbar
|
||||||
|
)
|
||||||
|
|
||||||
|
def disconnect(self):
|
||||||
|
if not self._connected:
|
||||||
|
return
|
||||||
|
# Disconnect the action from the target widget's method
|
||||||
|
self.components.get_action("image_full_colorbar").action.toggled.disconnect(
|
||||||
|
self._enable_full_colorbar
|
||||||
|
)
|
||||||
|
self.components.get_action("image_simple_colorbar").action.toggled.disconnect(
|
||||||
|
self._enable_simple_colorbar
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def image_processing(components: ToolbarComponents) -> ToolbarBundle:
|
||||||
|
"""
|
||||||
|
Creates a toolbar bundle for image processing functionality.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
components (ToolbarComponents): The components to be added to the bundle.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
ToolbarBundle: The image processing toolbar bundle.
|
||||||
|
"""
|
||||||
|
components.add_safe(
|
||||||
|
"image_processing_fft",
|
||||||
|
MaterialIconAction(
|
||||||
|
icon_name="fft", tooltip="Toggle FFT", checkable=True, parent=components.toolbar
|
||||||
|
),
|
||||||
|
)
|
||||||
|
components.add_safe(
|
||||||
|
"image_processing_log",
|
||||||
|
MaterialIconAction(
|
||||||
|
icon_name="log_scale", tooltip="Toggle Log", checkable=True, parent=components.toolbar
|
||||||
|
),
|
||||||
|
)
|
||||||
|
components.add_safe(
|
||||||
|
"image_processing_transpose",
|
||||||
|
MaterialIconAction(
|
||||||
|
icon_name="transform",
|
||||||
|
tooltip="Transpose Image",
|
||||||
|
checkable=True,
|
||||||
|
parent=components.toolbar,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
components.add_safe(
|
||||||
|
"image_processing_rotate_right",
|
||||||
|
MaterialIconAction(
|
||||||
|
icon_name="rotate_right",
|
||||||
|
tooltip="Rotate image clockwise by 90 deg",
|
||||||
|
parent=components.toolbar,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
components.add_safe(
|
||||||
|
"image_processing_rotate_left",
|
||||||
|
MaterialIconAction(
|
||||||
|
icon_name="rotate_left",
|
||||||
|
tooltip="Rotate image counterclockwise by 90 deg",
|
||||||
|
parent=components.toolbar,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
components.add_safe(
|
||||||
|
"image_processing_reset",
|
||||||
|
MaterialIconAction(
|
||||||
|
icon_name="reset_settings", tooltip="Reset Image Settings", parent=components.toolbar
|
||||||
|
),
|
||||||
|
)
|
||||||
|
bundle = ToolbarBundle("image_processing", components)
|
||||||
|
bundle.add_action("image_processing_fft")
|
||||||
|
bundle.add_action("image_processing_log")
|
||||||
|
bundle.add_action("image_processing_transpose")
|
||||||
|
bundle.add_action("image_processing_rotate_right")
|
||||||
|
bundle.add_action("image_processing_rotate_left")
|
||||||
|
bundle.add_action("image_processing_reset")
|
||||||
|
return bundle
|
||||||
|
|
||||||
|
|
||||||
|
class ImageProcessingConnection(BundleConnection):
|
||||||
|
"""
|
||||||
|
Connection class for the image processing toolbar bundle.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, components: ToolbarComponents, target_widget=None):
|
||||||
|
self.bundle_name = "image_processing"
|
||||||
|
self.components = components
|
||||||
|
self.target_widget = target_widget
|
||||||
|
if (
|
||||||
|
not hasattr(self.target_widget, "fft")
|
||||||
|
or not hasattr(self.target_widget, "log")
|
||||||
|
or not hasattr(self.target_widget, "transpose")
|
||||||
|
or not hasattr(self.target_widget, "num_rotation_90")
|
||||||
|
):
|
||||||
|
raise AttributeError(
|
||||||
|
"Target widget must implement 'fft', 'log', 'transpose', and 'num_rotation_90' attributes."
|
||||||
|
)
|
||||||
|
super().__init__()
|
||||||
|
self.fft = components.get_action("image_processing_fft")
|
||||||
|
self.log = components.get_action("image_processing_log")
|
||||||
|
self.transpose = components.get_action("image_processing_transpose")
|
||||||
|
self.right = components.get_action("image_processing_rotate_right")
|
||||||
|
self.left = components.get_action("image_processing_rotate_left")
|
||||||
|
self.reset = components.get_action("image_processing_reset")
|
||||||
|
self._connected = False
|
||||||
|
|
||||||
|
@SafeSlot()
|
||||||
|
def toggle_fft(self):
|
||||||
|
checked = self.fft.action.isChecked()
|
||||||
|
self.target_widget.fft = checked
|
||||||
|
|
||||||
|
@SafeSlot()
|
||||||
|
def toggle_log(self):
|
||||||
|
checked = self.log.action.isChecked()
|
||||||
|
self.target_widget.log = checked
|
||||||
|
|
||||||
|
@SafeSlot()
|
||||||
|
def toggle_transpose(self):
|
||||||
|
checked = self.transpose.action.isChecked()
|
||||||
|
self.target_widget.transpose = checked
|
||||||
|
|
||||||
|
@SafeSlot()
|
||||||
|
def rotate_right(self):
|
||||||
|
if self.target_widget.num_rotation_90 is None:
|
||||||
|
return
|
||||||
|
rotation = (self.target_widget.num_rotation_90 - 1) % 4
|
||||||
|
self.target_widget.num_rotation_90 = rotation
|
||||||
|
|
||||||
|
@SafeSlot()
|
||||||
|
def rotate_left(self):
|
||||||
|
if self.target_widget.num_rotation_90 is None:
|
||||||
|
return
|
||||||
|
rotation = (self.target_widget.num_rotation_90 + 1) % 4
|
||||||
|
self.target_widget.num_rotation_90 = rotation
|
||||||
|
|
||||||
|
@SafeSlot()
|
||||||
|
def reset_settings(self):
|
||||||
|
self.target_widget.fft = False
|
||||||
|
self.target_widget.log = False
|
||||||
|
self.target_widget.transpose = False
|
||||||
|
self.target_widget.num_rotation_90 = 0
|
||||||
|
|
||||||
|
self.fft.action.setChecked(False)
|
||||||
|
self.log.action.setChecked(False)
|
||||||
|
self.transpose.action.setChecked(False)
|
||||||
|
|
||||||
|
def connect(self):
|
||||||
|
"""
|
||||||
|
Connect the actions to the target widget's methods.
|
||||||
|
"""
|
||||||
|
self._connected = True
|
||||||
|
self.fft.action.triggered.connect(self.toggle_fft)
|
||||||
|
self.log.action.triggered.connect(self.toggle_log)
|
||||||
|
self.transpose.action.triggered.connect(self.toggle_transpose)
|
||||||
|
self.right.action.triggered.connect(self.rotate_right)
|
||||||
|
self.left.action.triggered.connect(self.rotate_left)
|
||||||
|
self.reset.action.triggered.connect(self.reset_settings)
|
||||||
|
|
||||||
|
def disconnect(self):
|
||||||
|
"""
|
||||||
|
Disconnect the actions from the target widget's methods.
|
||||||
|
"""
|
||||||
|
if not self._connected:
|
||||||
|
return
|
||||||
|
self.fft.action.triggered.disconnect(self.toggle_fft)
|
||||||
|
self.log.action.triggered.disconnect(self.toggle_log)
|
||||||
|
self.transpose.action.triggered.disconnect(self.toggle_transpose)
|
||||||
|
self.right.action.triggered.disconnect(self.rotate_right)
|
||||||
|
self.left.action.triggered.disconnect(self.rotate_left)
|
||||||
|
self.reset.action.triggered.disconnect(self.reset_settings)
|
@ -1,6 +1,5 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import numpy as np
|
|
||||||
import pyqtgraph as pg
|
import pyqtgraph as pg
|
||||||
from bec_lib import bec_logger
|
from bec_lib import bec_logger
|
||||||
from bec_lib.endpoints import MessageEndpoints
|
from bec_lib.endpoints import MessageEndpoints
|
||||||
@ -15,12 +14,12 @@ from bec_widgets.utils import Colors, ConnectionConfig
|
|||||||
from bec_widgets.utils.colors import set_theme
|
from bec_widgets.utils.colors import set_theme
|
||||||
from bec_widgets.utils.error_popups import SafeProperty, SafeSlot
|
from bec_widgets.utils.error_popups import SafeProperty, SafeSlot
|
||||||
from bec_widgets.utils.settings_dialog import SettingsDialog
|
from bec_widgets.utils.settings_dialog import SettingsDialog
|
||||||
from bec_widgets.utils.toolbar import MaterialIconAction
|
from bec_widgets.utils.toolbars.toolbar import MaterialIconAction
|
||||||
from bec_widgets.widgets.plots.motor_map.settings.motor_map_settings import MotorMapSettings
|
from bec_widgets.widgets.plots.motor_map.settings.motor_map_settings import MotorMapSettings
|
||||||
from bec_widgets.widgets.plots.motor_map.toolbar_bundles.motor_selection import (
|
from bec_widgets.widgets.plots.motor_map.toolbar_components.motor_selection import (
|
||||||
MotorSelectionToolbarBundle,
|
MotorSelectionAction,
|
||||||
)
|
)
|
||||||
from bec_widgets.widgets.plots.plot_base import PlotBase
|
from bec_widgets.widgets.plots.plot_base import PlotBase, UIMode
|
||||||
|
|
||||||
logger = bec_logger.logger
|
logger = bec_logger.logger
|
||||||
|
|
||||||
@ -182,33 +181,60 @@ class MotorMap(PlotBase):
|
|||||||
self.proxy_update_plot = pg.SignalProxy(
|
self.proxy_update_plot = pg.SignalProxy(
|
||||||
self.update_signal, rateLimit=25, slot=self._update_plot
|
self.update_signal, rateLimit=25, slot=self._update_plot
|
||||||
)
|
)
|
||||||
|
self._init_motor_map_toolbar()
|
||||||
self._add_motor_map_settings()
|
self._add_motor_map_settings()
|
||||||
|
|
||||||
################################################################################
|
################################################################################
|
||||||
# Widget Specific GUI interactions
|
# Widget Specific GUI interactions
|
||||||
################################################################################
|
################################################################################
|
||||||
|
|
||||||
def _init_toolbar(self):
|
def _init_motor_map_toolbar(self):
|
||||||
"""
|
"""
|
||||||
Initialize the toolbar for the motor map widget.
|
Initialize the toolbar for the motor map widget.
|
||||||
"""
|
"""
|
||||||
self.motor_selection_bundle = MotorSelectionToolbarBundle(
|
motor_selection = MotorSelectionAction(parent=self)
|
||||||
bundle_id="motor_selection", target_widget=self
|
self.toolbar.add_action("motor_selection", motor_selection)
|
||||||
)
|
|
||||||
self.toolbar.add_bundle(self.motor_selection_bundle, target_widget=self)
|
|
||||||
super()._init_toolbar()
|
|
||||||
self.toolbar.widgets["reset_legend"].action.setVisible(False)
|
|
||||||
|
|
||||||
self.reset_legend_action = MaterialIconAction(
|
motor_selection.motor_x.currentTextChanged.connect(self.on_motor_selection_changed)
|
||||||
icon_name="history", tooltip="Reset the position of legend."
|
motor_selection.motor_y.currentTextChanged.connect(self.on_motor_selection_changed)
|
||||||
|
|
||||||
|
self.toolbar.components.get_action("reset_legend").action.setVisible(False)
|
||||||
|
|
||||||
|
reset_legend = MaterialIconAction(
|
||||||
|
icon_name="history",
|
||||||
|
tooltip="Reset the position of legend.",
|
||||||
|
checkable=False,
|
||||||
|
parent=self,
|
||||||
)
|
)
|
||||||
self.toolbar.add_action_to_bundle(
|
self.toolbar.components.add_safe("reset_motor_map_legend", reset_legend)
|
||||||
bundle_id="roi",
|
self.toolbar.get_bundle("roi").add_action("reset_motor_map_legend")
|
||||||
action_id="motor_map_history",
|
reset_legend.action.triggered.connect(self.reset_history)
|
||||||
action=self.reset_legend_action,
|
|
||||||
target_widget=self,
|
settings_brightness = MaterialIconAction(
|
||||||
|
icon_name="settings_brightness",
|
||||||
|
tooltip="Show Motor Map Settings",
|
||||||
|
checkable=True,
|
||||||
|
parent=self,
|
||||||
)
|
)
|
||||||
self.reset_legend_action.action.triggered.connect(self.reset_history)
|
self.toolbar.components.add_safe("motor_map_settings", settings_brightness)
|
||||||
|
self.toolbar.get_bundle("axis_popup").add_action("motor_map_settings")
|
||||||
|
|
||||||
|
settings_brightness.action.triggered.connect(self.show_motor_map_settings)
|
||||||
|
|
||||||
|
bundles = ["motor_selection", "plot_export", "mouse_interaction", "roi"]
|
||||||
|
if self.ui_mode == UIMode.POPUP:
|
||||||
|
bundles.append("axis_popup")
|
||||||
|
self.toolbar.show_bundles(bundles)
|
||||||
|
|
||||||
|
@SafeSlot()
|
||||||
|
def on_motor_selection_changed(self, _):
|
||||||
|
action: MotorSelectionAction = self.toolbar.components.get_action("motor_selection")
|
||||||
|
motor_x = action.motor_x.currentText()
|
||||||
|
motor_y = action.motor_y.currentText()
|
||||||
|
|
||||||
|
if motor_x != "" and motor_y != "":
|
||||||
|
if motor_x != self.config.x_motor.name or motor_y != self.config.y_motor.name:
|
||||||
|
self.map(motor_x, motor_y)
|
||||||
|
|
||||||
def _add_motor_map_settings(self):
|
def _add_motor_map_settings(self):
|
||||||
"""Add the motor map settings to the side panel."""
|
"""Add the motor map settings to the side panel."""
|
||||||
@ -221,32 +247,11 @@ class MotorMap(PlotBase):
|
|||||||
title="Motor Map Settings",
|
title="Motor Map Settings",
|
||||||
)
|
)
|
||||||
|
|
||||||
def add_popups(self):
|
|
||||||
"""
|
|
||||||
Add popups to the ScatterWaveform widget.
|
|
||||||
"""
|
|
||||||
super().add_popups()
|
|
||||||
scatter_curve_setting_action = MaterialIconAction(
|
|
||||||
icon_name="settings_brightness",
|
|
||||||
tooltip="Show Motor Map Settings",
|
|
||||||
checkable=True,
|
|
||||||
parent=self,
|
|
||||||
)
|
|
||||||
self.toolbar.add_action_to_bundle(
|
|
||||||
bundle_id="popup_bundle",
|
|
||||||
action_id="motor_map_settings",
|
|
||||||
action=scatter_curve_setting_action,
|
|
||||||
target_widget=self,
|
|
||||||
)
|
|
||||||
self.toolbar.widgets["motor_map_settings"].action.triggered.connect(
|
|
||||||
self.show_motor_map_settings
|
|
||||||
)
|
|
||||||
|
|
||||||
def show_motor_map_settings(self):
|
def show_motor_map_settings(self):
|
||||||
"""
|
"""
|
||||||
Show the DAP summary popup.
|
Show the DAP summary popup.
|
||||||
"""
|
"""
|
||||||
action = self.toolbar.widgets["motor_map_settings"].action
|
action = self.toolbar.components.get_action("motor_map_settings").action
|
||||||
if self.motor_map_settings is None or not self.motor_map_settings.isVisible():
|
if self.motor_map_settings is None or not self.motor_map_settings.isVisible():
|
||||||
motor_map_settings = MotorMapSettings(parent=self, target_widget=self, popup=True)
|
motor_map_settings = MotorMapSettings(parent=self, target_widget=self, popup=True)
|
||||||
self.motor_map_settings = SettingsDialog(
|
self.motor_map_settings = SettingsDialog(
|
||||||
@ -272,7 +277,7 @@ class MotorMap(PlotBase):
|
|||||||
"""
|
"""
|
||||||
self.motor_map_settings.deleteLater()
|
self.motor_map_settings.deleteLater()
|
||||||
self.motor_map_settings = None
|
self.motor_map_settings = None
|
||||||
self.toolbar.widgets["motor_map_settings"].action.setChecked(False)
|
self.toolbar.components.get_action("motor_map_settings").action.setChecked(False)
|
||||||
|
|
||||||
################################################################################
|
################################################################################
|
||||||
# Widget Specific Properties
|
# Widget Specific Properties
|
||||||
@ -766,20 +771,21 @@ class MotorMap(PlotBase):
|
|||||||
"""
|
"""
|
||||||
Sync the motor map selection toolbar with the current motor map.
|
Sync the motor map selection toolbar with the current motor map.
|
||||||
"""
|
"""
|
||||||
if self.motor_selection_bundle is not None:
|
motor_selection = self.toolbar.components.get_action("motor_selection")
|
||||||
motor_x = self.motor_selection_bundle.motor_x.currentText()
|
|
||||||
motor_y = self.motor_selection_bundle.motor_y.currentText()
|
motor_x = motor_selection.motor_x.currentText()
|
||||||
|
motor_y = motor_selection.motor_y.currentText()
|
||||||
|
|
||||||
if motor_x != self.config.x_motor.name:
|
if motor_x != self.config.x_motor.name:
|
||||||
self.motor_selection_bundle.motor_x.blockSignals(True)
|
motor_selection.motor_x.blockSignals(True)
|
||||||
self.motor_selection_bundle.motor_x.set_device(self.config.x_motor.name)
|
motor_selection.motor_x.set_device(self.config.x_motor.name)
|
||||||
self.motor_selection_bundle.motor_x.check_validity(self.config.x_motor.name)
|
motor_selection.motor_x.check_validity(self.config.x_motor.name)
|
||||||
self.motor_selection_bundle.motor_x.blockSignals(False)
|
motor_selection.motor_x.blockSignals(False)
|
||||||
if motor_y != self.config.y_motor.name:
|
if motor_y != self.config.y_motor.name:
|
||||||
self.motor_selection_bundle.motor_y.blockSignals(True)
|
motor_selection.motor_y.blockSignals(True)
|
||||||
self.motor_selection_bundle.motor_y.set_device(self.config.y_motor.name)
|
motor_selection.motor_y.set_device(self.config.y_motor.name)
|
||||||
self.motor_selection_bundle.motor_y.check_validity(self.config.y_motor.name)
|
motor_selection.motor_y.check_validity(self.config.y_motor.name)
|
||||||
self.motor_selection_bundle.motor_y.blockSignals(False)
|
motor_selection.motor_y.blockSignals(False)
|
||||||
|
|
||||||
################################################################################
|
################################################################################
|
||||||
# Export Methods
|
# Export Methods
|
||||||
@ -795,10 +801,6 @@ class MotorMap(PlotBase):
|
|||||||
data = {"x": self._buffer["x"], "y": self._buffer["y"]}
|
data = {"x": self._buffer["x"], "y": self._buffer["y"]}
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def cleanup(self):
|
|
||||||
self.motor_selection_bundle.cleanup()
|
|
||||||
super().cleanup()
|
|
||||||
|
|
||||||
|
|
||||||
class DemoApp(QMainWindow): # pragma: no cover
|
class DemoApp(QMainWindow): # pragma: no cover
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
@ -1,70 +0,0 @@
|
|||||||
from bec_lib.device import ReadoutPriority
|
|
||||||
from qtpy.QtCore import Qt
|
|
||||||
from qtpy.QtWidgets import QStyledItemDelegate
|
|
||||||
|
|
||||||
from bec_widgets.utils.error_popups import SafeSlot
|
|
||||||
from bec_widgets.utils.toolbar import ToolbarBundle, WidgetAction
|
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
class NoCheckDelegate(QStyledItemDelegate):
|
|
||||||
"""To reduce space in combo boxes by removing the checkmark."""
|
|
||||||
|
|
||||||
def initStyleOption(self, option, index):
|
|
||||||
super().initStyleOption(option, index)
|
|
||||||
# Remove any check indicator
|
|
||||||
option.checkState = Qt.Unchecked
|
|
||||||
|
|
||||||
|
|
||||||
class MotorSelectionToolbarBundle(ToolbarBundle):
|
|
||||||
"""
|
|
||||||
A bundle of actions for a toolbar that selects motors.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, bundle_id="motor_selection", target_widget=None, **kwargs):
|
|
||||||
super().__init__(bundle_id=bundle_id, actions=[], **kwargs)
|
|
||||||
self.target_widget = target_widget
|
|
||||||
|
|
||||||
# Motor X
|
|
||||||
self.motor_x = DeviceComboBox(
|
|
||||||
parent=self.target_widget, device_filter=[BECDeviceFilter.POSITIONER]
|
|
||||||
)
|
|
||||||
self.motor_x.addItem("", None)
|
|
||||||
self.motor_x.setCurrentText("")
|
|
||||||
self.motor_x.setToolTip("Select Motor X")
|
|
||||||
self.motor_x.setItemDelegate(NoCheckDelegate(self.motor_x))
|
|
||||||
|
|
||||||
# Motor X
|
|
||||||
self.motor_y = DeviceComboBox(
|
|
||||||
parent=self.target_widget, device_filter=[BECDeviceFilter.POSITIONER]
|
|
||||||
)
|
|
||||||
self.motor_y.addItem("", None)
|
|
||||||
self.motor_y.setCurrentText("")
|
|
||||||
self.motor_y.setToolTip("Select Motor Y")
|
|
||||||
self.motor_y.setItemDelegate(NoCheckDelegate(self.motor_y))
|
|
||||||
|
|
||||||
self.add_action("motor_x", WidgetAction(widget=self.motor_x, adjust_size=False))
|
|
||||||
self.add_action("motor_y", WidgetAction(widget=self.motor_y, adjust_size=False))
|
|
||||||
|
|
||||||
# Connect slots, a device will be connected upon change of any combobox
|
|
||||||
self.motor_x.currentTextChanged.connect(lambda: self.connect_motors())
|
|
||||||
self.motor_y.currentTextChanged.connect(lambda: self.connect_motors())
|
|
||||||
|
|
||||||
@SafeSlot()
|
|
||||||
def connect_motors(self):
|
|
||||||
motor_x = self.motor_x.currentText()
|
|
||||||
motor_y = self.motor_y.currentText()
|
|
||||||
|
|
||||||
if motor_x != "" and motor_y != "":
|
|
||||||
if (
|
|
||||||
motor_x != self.target_widget.config.x_motor.name
|
|
||||||
or motor_y != self.target_widget.config.y_motor.name
|
|
||||||
):
|
|
||||||
self.target_widget.map(motor_x, motor_y)
|
|
||||||
|
|
||||||
def cleanup(self):
|
|
||||||
self.motor_x.close()
|
|
||||||
self.motor_x.deleteLater()
|
|
||||||
self.motor_y.close()
|
|
||||||
self.motor_y.deleteLater()
|
|
@ -0,0 +1,51 @@
|
|||||||
|
from qtpy.QtWidgets import QHBoxLayout, QToolBar, QWidget
|
||||||
|
|
||||||
|
from bec_widgets.utils.toolbars.actions import NoCheckDelegate, ToolBarAction
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
class MotorSelectionAction(ToolBarAction):
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
super().__init__(icon_path=None, tooltip=None, checkable=False)
|
||||||
|
self.motor_x = DeviceComboBox(parent=parent, device_filter=[BECDeviceFilter.POSITIONER])
|
||||||
|
self.motor_x.addItem("", None)
|
||||||
|
self.motor_x.setCurrentText("")
|
||||||
|
self.motor_x.setToolTip("Select Motor X")
|
||||||
|
self.motor_x.setItemDelegate(NoCheckDelegate(self.motor_x))
|
||||||
|
self.motor_y = DeviceComboBox(parent=parent, device_filter=[BECDeviceFilter.POSITIONER])
|
||||||
|
self.motor_y.addItem("", None)
|
||||||
|
self.motor_y.setCurrentText("")
|
||||||
|
self.motor_y.setToolTip("Select Motor Y")
|
||||||
|
self.motor_y.setItemDelegate(NoCheckDelegate(self.motor_y))
|
||||||
|
|
||||||
|
self.container = QWidget(parent)
|
||||||
|
layout = QHBoxLayout(self.container)
|
||||||
|
layout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
layout.setSpacing(0)
|
||||||
|
layout.addWidget(self.motor_x)
|
||||||
|
layout.addWidget(self.motor_y)
|
||||||
|
self.container.setLayout(layout)
|
||||||
|
self.action = self.container
|
||||||
|
|
||||||
|
def add_to_toolbar(self, toolbar: QToolBar, target: QWidget):
|
||||||
|
"""
|
||||||
|
Adds the widget to the toolbar.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
toolbar (QToolBar): The toolbar to add the widget to.
|
||||||
|
target (QWidget): The target widget for the action.
|
||||||
|
"""
|
||||||
|
|
||||||
|
toolbar.addWidget(self.container)
|
||||||
|
|
||||||
|
def cleanup(self):
|
||||||
|
"""
|
||||||
|
Cleans up the action, if necessary.
|
||||||
|
"""
|
||||||
|
self.motor_x.close()
|
||||||
|
self.motor_x.deleteLater()
|
||||||
|
self.motor_y.close()
|
||||||
|
self.motor_y.deleteLater()
|
||||||
|
self.container.close()
|
||||||
|
self.container.deleteLater()
|
@ -1,6 +1,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from collections import deque
|
from collections import deque
|
||||||
|
from typing import TYPE_CHECKING, cast
|
||||||
|
|
||||||
import pyqtgraph as pg
|
import pyqtgraph as pg
|
||||||
from bec_lib.endpoints import MessageEndpoints
|
from bec_lib.endpoints import MessageEndpoints
|
||||||
@ -12,13 +13,15 @@ from qtpy.QtWidgets import QWidget
|
|||||||
from bec_widgets.utils import Colors, ConnectionConfig
|
from bec_widgets.utils import Colors, ConnectionConfig
|
||||||
from bec_widgets.utils.error_popups import SafeProperty, SafeSlot
|
from bec_widgets.utils.error_popups import SafeProperty, SafeSlot
|
||||||
from bec_widgets.utils.side_panel import SidePanel
|
from bec_widgets.utils.side_panel import SidePanel
|
||||||
|
from bec_widgets.widgets.control.device_input.device_combobox.device_combobox import DeviceComboBox
|
||||||
from bec_widgets.widgets.plots.multi_waveform.settings.control_panel import (
|
from bec_widgets.widgets.plots.multi_waveform.settings.control_panel import (
|
||||||
MultiWaveformControlPanel,
|
MultiWaveformControlPanel,
|
||||||
)
|
)
|
||||||
from bec_widgets.widgets.plots.multi_waveform.toolbar_bundles.monitor_selection import (
|
from bec_widgets.widgets.plots.multi_waveform.toolbar_components.monitor_selection import (
|
||||||
MultiWaveformSelectionToolbarBundle,
|
monitor_selection_bundle,
|
||||||
)
|
)
|
||||||
from bec_widgets.widgets.plots.plot_base import PlotBase
|
from bec_widgets.widgets.plots.plot_base import PlotBase
|
||||||
|
from bec_widgets.widgets.utility.visual.colormap_widget.colormap_widget import BECColorMapWidget
|
||||||
|
|
||||||
logger = bec_logger.logger
|
logger = bec_logger.logger
|
||||||
|
|
||||||
@ -141,33 +144,54 @@ class MultiWaveform(PlotBase):
|
|||||||
self.visible_curves = []
|
self.visible_curves = []
|
||||||
self.number_of_visible_curves = 0
|
self.number_of_visible_curves = 0
|
||||||
|
|
||||||
self._init_control_panel()
|
self._init_multiwaveform_toolbar()
|
||||||
|
|
||||||
################################################################################
|
################################################################################
|
||||||
# Widget Specific GUI interactions
|
# Widget Specific GUI interactions
|
||||||
################################################################################
|
################################################################################
|
||||||
def _init_toolbar(self):
|
def _init_multiwaveform_toolbar(self):
|
||||||
self.monitor_selection_bundle = MultiWaveformSelectionToolbarBundle(
|
self.toolbar.add_bundle(
|
||||||
bundle_id="motor_selection", target_widget=self
|
monitor_selection_bundle(self.toolbar.components, target_widget=self)
|
||||||
)
|
)
|
||||||
self.toolbar.add_bundle(self.monitor_selection_bundle, target_widget=self)
|
self.toolbar.toggle_action_visibility("reset_legend", visible=False)
|
||||||
super()._init_toolbar()
|
|
||||||
self.toolbar.widgets["reset_legend"].action.setVisible(False)
|
combobox = self.toolbar.components.get_action("monitor_selection").widget
|
||||||
|
combobox.currentTextChanged.connect(self.connect_monitor)
|
||||||
|
|
||||||
|
cmap = self.toolbar.components.get_action("color_map").widget
|
||||||
|
cmap.colormap_changed_signal.connect(self.change_colormap)
|
||||||
|
|
||||||
|
bundles = self.toolbar.shown_bundles
|
||||||
|
bundles.insert(0, "monitor_selection")
|
||||||
|
self.toolbar.show_bundles(bundles)
|
||||||
|
|
||||||
|
self._init_control_panel()
|
||||||
|
|
||||||
def _init_control_panel(self):
|
def _init_control_panel(self):
|
||||||
self.control_panel = SidePanel(self, orientation="top", panel_max_width=90)
|
control_panel = SidePanel(self, orientation="top", panel_max_width=90)
|
||||||
self.layout_manager.add_widget_relative(
|
self.layout_manager.add_widget_relative(control_panel, self.round_plot_widget, "bottom")
|
||||||
self.control_panel, self.round_plot_widget, "bottom"
|
|
||||||
)
|
|
||||||
self.controls = MultiWaveformControlPanel(parent=self, target_widget=self)
|
self.controls = MultiWaveformControlPanel(parent=self, target_widget=self)
|
||||||
self.control_panel.add_menu(
|
control_panel.add_menu(
|
||||||
action_id="control",
|
action_id="control",
|
||||||
icon_name="tune",
|
icon_name="tune",
|
||||||
tooltip="Show Control panel",
|
tooltip="Show Control panel",
|
||||||
widget=self.controls,
|
widget=self.controls,
|
||||||
title=None,
|
title=None,
|
||||||
)
|
)
|
||||||
self.control_panel.toolbar.widgets["control"].action.trigger()
|
control_panel.toolbar.components.get_action("control").action.trigger()
|
||||||
|
|
||||||
|
@SafeSlot()
|
||||||
|
def connect_monitor(self, _):
|
||||||
|
combobox = self.toolbar.components.get_action("monitor_selection").widget
|
||||||
|
monitor = combobox.currentText()
|
||||||
|
|
||||||
|
if monitor != "":
|
||||||
|
if monitor != self.config.monitor:
|
||||||
|
self.config.monitor = monitor
|
||||||
|
|
||||||
|
@SafeSlot(str)
|
||||||
|
def change_colormap(self, colormap: str):
|
||||||
|
self.color_palette = colormap
|
||||||
|
|
||||||
################################################################################
|
################################################################################
|
||||||
# Widget Specific Properties
|
# Widget Specific Properties
|
||||||
@ -488,23 +512,30 @@ class MultiWaveform(PlotBase):
|
|||||||
"""
|
"""
|
||||||
Sync the motor map selection toolbar with the current motor map.
|
Sync the motor map selection toolbar with the current motor map.
|
||||||
"""
|
"""
|
||||||
if self.monitor_selection_bundle is not None:
|
|
||||||
monitor = self.monitor_selection_bundle.monitor.currentText()
|
combobox_widget: DeviceComboBox = cast(
|
||||||
color_palette = self.monitor_selection_bundle.colormap_widget.colormap
|
DeviceComboBox, self.toolbar.components.get_action("monitor_selection").widget
|
||||||
|
)
|
||||||
|
cmap_widget: BECColorMapWidget = cast(
|
||||||
|
BECColorMapWidget, self.toolbar.components.get_action("color_map").widget
|
||||||
|
)
|
||||||
|
|
||||||
|
monitor = combobox_widget.currentText()
|
||||||
|
color_palette = cmap_widget.colormap
|
||||||
|
|
||||||
if monitor != self.config.monitor:
|
if monitor != self.config.monitor:
|
||||||
self.monitor_selection_bundle.monitor.blockSignals(True)
|
combobox_widget.setCurrentText(monitor)
|
||||||
self.monitor_selection_bundle.monitor.set_device(self.config.monitor)
|
combobox_widget.blockSignals(True)
|
||||||
self.monitor_selection_bundle.monitor.check_validity(self.config.monitor)
|
combobox_widget.set_device(self.config.monitor)
|
||||||
self.monitor_selection_bundle.monitor.blockSignals(False)
|
combobox_widget.check_validity(self.config.monitor)
|
||||||
|
combobox_widget.blockSignals(False)
|
||||||
|
|
||||||
if color_palette != self.config.color_palette:
|
if color_palette != self.config.color_palette:
|
||||||
self.monitor_selection_bundle.colormap_widget.blockSignals(True)
|
cmap_widget.blockSignals(True)
|
||||||
self.monitor_selection_bundle.colormap_widget.colormap = self.config.color_palette
|
cmap_widget.colormap = self.config.color_palette
|
||||||
self.monitor_selection_bundle.colormap_widget.blockSignals(False)
|
cmap_widget.blockSignals(False)
|
||||||
|
|
||||||
def cleanup(self):
|
def cleanup(self):
|
||||||
self._disconnect_monitor()
|
self._disconnect_monitor()
|
||||||
self.clear_curves()
|
self.clear_curves()
|
||||||
self.monitor_selection_bundle.cleanup()
|
|
||||||
super().cleanup()
|
super().cleanup()
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
from bec_lib.device import ReadoutPriority
|
from bec_lib.device import ReadoutPriority
|
||||||
from qtpy.QtCore import Qt
|
from qtpy.QtCore import Qt
|
||||||
from qtpy.QtWidgets import QStyledItemDelegate
|
from qtpy.QtWidgets import QStyledItemDelegate, QWidget
|
||||||
|
|
||||||
from bec_widgets.utils.error_popups import SafeSlot
|
from bec_widgets.utils.error_popups import SafeSlot
|
||||||
from bec_widgets.utils.toolbar import ToolbarBundle, WidgetAction
|
from bec_widgets.utils.toolbars.actions import DeviceComboBoxAction, WidgetAction
|
||||||
|
from bec_widgets.utils.toolbars.bundles import ToolbarComponents
|
||||||
|
from bec_widgets.utils.toolbars.toolbar import ToolbarBundle
|
||||||
from bec_widgets.widgets.control.device_input.base_classes.device_input_base import BECDeviceFilter
|
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
|
from bec_widgets.widgets.control.device_input.device_combobox.device_combobox import DeviceComboBox
|
||||||
from bec_widgets.widgets.utility.visual.colormap_widget.colormap_widget import BECColorMapWidget
|
from bec_widgets.widgets.utility.visual.colormap_widget.colormap_widget import BECColorMapWidget
|
||||||
@ -18,6 +20,37 @@ class NoCheckDelegate(QStyledItemDelegate):
|
|||||||
option.checkState = Qt.Unchecked
|
option.checkState = Qt.Unchecked
|
||||||
|
|
||||||
|
|
||||||
|
def monitor_selection_bundle(
|
||||||
|
components: ToolbarComponents, target_widget: QWidget
|
||||||
|
) -> ToolbarBundle:
|
||||||
|
"""
|
||||||
|
Creates a monitor selection toolbar bundle.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
components (ToolbarComponents): The components to be added to the bundle.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
ToolbarBundle: The monitor selection toolbar bundle.
|
||||||
|
"""
|
||||||
|
components.add_safe(
|
||||||
|
"monitor_selection",
|
||||||
|
DeviceComboBoxAction(
|
||||||
|
target_widget=target_widget,
|
||||||
|
device_filter=[BECDeviceFilter.DEVICE],
|
||||||
|
readout_priority_filter=ReadoutPriority.ASYNC,
|
||||||
|
add_empty_item=True,
|
||||||
|
no_check_delegate=True,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
components.add_safe(
|
||||||
|
"color_map", WidgetAction(widget=BECColorMapWidget(cmap="plasma"), adjust_size=False)
|
||||||
|
)
|
||||||
|
bundle = ToolbarBundle("monitor_selection", components)
|
||||||
|
bundle.add_action("monitor_selection")
|
||||||
|
bundle.add_action("color_map")
|
||||||
|
return bundle
|
||||||
|
|
||||||
|
|
||||||
class MultiWaveformSelectionToolbarBundle(ToolbarBundle):
|
class MultiWaveformSelectionToolbarBundle(ToolbarBundle):
|
||||||
"""
|
"""
|
||||||
A bundle of actions for a toolbar that selects motors.
|
A bundle of actions for a toolbar that selects motors.
|
@ -14,17 +14,25 @@ from bec_widgets.utils.error_popups import SafeProperty, SafeSlot
|
|||||||
from bec_widgets.utils.fps_counter import FPSCounter
|
from bec_widgets.utils.fps_counter import FPSCounter
|
||||||
from bec_widgets.utils.plot_indicator_items import BECArrowItem, BECTickItem
|
from bec_widgets.utils.plot_indicator_items import BECArrowItem, BECTickItem
|
||||||
from bec_widgets.utils.round_frame import RoundedFrame
|
from bec_widgets.utils.round_frame import RoundedFrame
|
||||||
from bec_widgets.utils.settings_dialog import SettingsDialog
|
|
||||||
from bec_widgets.utils.side_panel import SidePanel
|
from bec_widgets.utils.side_panel import SidePanel
|
||||||
from bec_widgets.utils.toolbar import MaterialIconAction, ModularToolBar, ToolbarBundle
|
from bec_widgets.utils.toolbars.performance import PerformanceConnection, performance_bundle
|
||||||
|
from bec_widgets.utils.toolbars.toolbar import ModularToolBar
|
||||||
from bec_widgets.utils.widget_state_manager import WidgetStateManager
|
from bec_widgets.utils.widget_state_manager import WidgetStateManager
|
||||||
from bec_widgets.widgets.containers.layout_manager.layout_manager import LayoutManagerWidget
|
from bec_widgets.widgets.containers.layout_manager.layout_manager import LayoutManagerWidget
|
||||||
from bec_widgets.widgets.plots.setting_menus.axis_settings import AxisSettings
|
from bec_widgets.widgets.plots.setting_menus.axis_settings import AxisSettings
|
||||||
from bec_widgets.widgets.plots.toolbar_bundles.mouse_interactions import (
|
from bec_widgets.widgets.plots.toolbar_components.axis_settings_popup import (
|
||||||
MouseInteractionToolbarBundle,
|
AxisSettingsPopupConnection,
|
||||||
|
axis_popup_bundle,
|
||||||
)
|
)
|
||||||
from bec_widgets.widgets.plots.toolbar_bundles.plot_export import PlotExportBundle
|
from bec_widgets.widgets.plots.toolbar_components.mouse_interactions import (
|
||||||
from bec_widgets.widgets.plots.toolbar_bundles.roi_bundle import ROIBundle
|
MouseInteractionConnection,
|
||||||
|
mouse_interaction_bundle,
|
||||||
|
)
|
||||||
|
from bec_widgets.widgets.plots.toolbar_components.plot_export import (
|
||||||
|
PlotExportConnection,
|
||||||
|
plot_export_bundle,
|
||||||
|
)
|
||||||
|
from bec_widgets.widgets.plots.toolbar_components.roi import RoiConnection, roi_bundle
|
||||||
|
|
||||||
logger = bec_logger.logger
|
logger = bec_logger.logger
|
||||||
|
|
||||||
@ -102,8 +110,6 @@ class PlotBase(BECWidget, QWidget):
|
|||||||
self.plot_item = pg.PlotItem(viewBox=BECViewBox(enableMenu=True))
|
self.plot_item = pg.PlotItem(viewBox=BECViewBox(enableMenu=True))
|
||||||
self.plot_widget.addItem(self.plot_item)
|
self.plot_widget.addItem(self.plot_item)
|
||||||
self.side_panel = SidePanel(self, orientation="left", panel_max_width=280)
|
self.side_panel = SidePanel(self, orientation="left", panel_max_width=280)
|
||||||
self.toolbar = ModularToolBar(parent=self, target_widget=self, orientation="horizontal")
|
|
||||||
self._init_toolbar()
|
|
||||||
|
|
||||||
# PlotItem Addons
|
# PlotItem Addons
|
||||||
self.plot_item.addLegend()
|
self.plot_item.addLegend()
|
||||||
@ -122,6 +128,9 @@ class PlotBase(BECWidget, QWidget):
|
|||||||
self.tick_item = BECTickItem(parent=self, plot_item=self.plot_item)
|
self.tick_item = BECTickItem(parent=self, plot_item=self.plot_item)
|
||||||
self.arrow_item = BECArrowItem(parent=self, plot_item=self.plot_item)
|
self.arrow_item = BECArrowItem(parent=self, plot_item=self.plot_item)
|
||||||
|
|
||||||
|
self.toolbar = ModularToolBar(parent=self, orientation="horizontal")
|
||||||
|
self._init_toolbar()
|
||||||
|
|
||||||
self._init_ui()
|
self._init_ui()
|
||||||
|
|
||||||
self._connect_to_theme_change()
|
self._connect_to_theme_change()
|
||||||
@ -146,36 +155,33 @@ class PlotBase(BECWidget, QWidget):
|
|||||||
self.plot_item.vb.sigStateChanged.connect(self.viewbox_state_changed)
|
self.plot_item.vb.sigStateChanged.connect(self.viewbox_state_changed)
|
||||||
|
|
||||||
def _init_toolbar(self):
|
def _init_toolbar(self):
|
||||||
self.popup_bundle = None
|
self.toolbar.add_bundle(performance_bundle(self.toolbar.components))
|
||||||
self.performance_bundle = ToolbarBundle("performance")
|
self.toolbar.add_bundle(plot_export_bundle(self.toolbar.components))
|
||||||
self.plot_export_bundle = PlotExportBundle("plot_export", target_widget=self)
|
self.toolbar.add_bundle(mouse_interaction_bundle(self.toolbar.components))
|
||||||
self.mouse_bundle = MouseInteractionToolbarBundle("mouse_interaction", target_widget=self)
|
self.toolbar.add_bundle(roi_bundle(self.toolbar.components))
|
||||||
# self.state_export_bundle = SaveStateBundle("state_export", target_widget=self) #TODO ATM disabled, cannot be used in DockArea, which is exposed to the user
|
self.toolbar.add_bundle(axis_popup_bundle(self.toolbar.components))
|
||||||
self.roi_bundle = ROIBundle("roi", target_widget=self)
|
|
||||||
|
|
||||||
# Add elements to toolbar
|
self.toolbar.connect_bundle(
|
||||||
self.toolbar.add_bundle(self.plot_export_bundle, target_widget=self)
|
"plot_base", PlotExportConnection(self.toolbar.components, self)
|
||||||
# self.toolbar.add_bundle(self.state_export_bundle, target_widget=self) #TODO ATM disabled, cannot be used in DockArea, which is exposed to the user
|
|
||||||
self.toolbar.add_bundle(self.mouse_bundle, target_widget=self)
|
|
||||||
self.toolbar.add_bundle(self.roi_bundle, target_widget=self)
|
|
||||||
|
|
||||||
self.performance_bundle.add_action(
|
|
||||||
"fps_monitor",
|
|
||||||
MaterialIconAction(
|
|
||||||
icon_name="speed", tooltip="Show FPS Monitor", checkable=True, parent=self
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
self.toolbar.add_bundle(self.performance_bundle, target_widget=self)
|
self.toolbar.connect_bundle(
|
||||||
|
"plot_base", PerformanceConnection(self.toolbar.components, self)
|
||||||
self.toolbar.widgets["fps_monitor"].action.toggled.connect(
|
)
|
||||||
lambda checked: setattr(self, "enable_fps_monitor", checked)
|
self.toolbar.connect_bundle(
|
||||||
|
"plot_base", MouseInteractionConnection(self.toolbar.components, self)
|
||||||
|
)
|
||||||
|
self.toolbar.connect_bundle("plot_base", RoiConnection(self.toolbar.components, self))
|
||||||
|
self.toolbar.connect_bundle(
|
||||||
|
"plot_base", AxisSettingsPopupConnection(self.toolbar.components, self)
|
||||||
)
|
)
|
||||||
|
|
||||||
# hide some options by default
|
# hide some options by default
|
||||||
self.toolbar.toggle_action_visibility("fps_monitor", False)
|
self.toolbar.toggle_action_visibility("fps_monitor", False)
|
||||||
|
|
||||||
# Get default viewbox state
|
# Get default viewbox state
|
||||||
self.mouse_bundle.get_viewbox_mode()
|
self.toolbar.show_bundles(
|
||||||
|
["plot_export", "mouse_interaction", "roi", "performance", "axis_popup"]
|
||||||
|
)
|
||||||
|
|
||||||
def add_side_menus(self):
|
def add_side_menus(self):
|
||||||
"""Adds multiple menus to the side panel."""
|
"""Adds multiple menus to the side panel."""
|
||||||
@ -192,45 +198,6 @@ class PlotBase(BECWidget, QWidget):
|
|||||||
except ValueError:
|
except ValueError:
|
||||||
return
|
return
|
||||||
|
|
||||||
def add_popups(self):
|
|
||||||
"""
|
|
||||||
Add popups to the toolbar.
|
|
||||||
"""
|
|
||||||
self.popup_bundle = ToolbarBundle("popup_bundle")
|
|
||||||
settings = MaterialIconAction(
|
|
||||||
icon_name="settings", tooltip="Show Axis Settings", checkable=True, parent=self
|
|
||||||
)
|
|
||||||
self.popup_bundle.add_action("axis", settings)
|
|
||||||
self.toolbar.add_bundle(self.popup_bundle, target_widget=self)
|
|
||||||
self.toolbar.widgets["axis"].action.triggered.connect(self.show_axis_settings_popup)
|
|
||||||
|
|
||||||
def show_axis_settings_popup(self):
|
|
||||||
"""
|
|
||||||
Show the axis settings dialog.
|
|
||||||
"""
|
|
||||||
settings_action = self.toolbar.widgets["axis"].action
|
|
||||||
if self.axis_settings_dialog is None or not self.axis_settings_dialog.isVisible():
|
|
||||||
axis_setting = AxisSettings(parent=self, target_widget=self, popup=True)
|
|
||||||
self.axis_settings_dialog = SettingsDialog(
|
|
||||||
self, settings_widget=axis_setting, window_title="Axis Settings", modal=False
|
|
||||||
)
|
|
||||||
# When the dialog is closed, update the toolbar icon and clear the reference
|
|
||||||
self.axis_settings_dialog.finished.connect(self._axis_settings_closed)
|
|
||||||
self.axis_settings_dialog.show()
|
|
||||||
settings_action.setChecked(True)
|
|
||||||
else:
|
|
||||||
# If already open, bring it to the front
|
|
||||||
self.axis_settings_dialog.raise_()
|
|
||||||
self.axis_settings_dialog.activateWindow()
|
|
||||||
settings_action.setChecked(True) # keep it toggled
|
|
||||||
|
|
||||||
def _axis_settings_closed(self):
|
|
||||||
"""
|
|
||||||
Slot for when the axis settings dialog is closed.
|
|
||||||
"""
|
|
||||||
self.axis_settings_dialog = None
|
|
||||||
self.toolbar.widgets["axis"].action.setChecked(False)
|
|
||||||
|
|
||||||
def reset_legend(self):
|
def reset_legend(self):
|
||||||
"""In the case that the legend is not visible, reset it to be visible to top left corner"""
|
"""In the case that the legend is not visible, reset it to be visible to top left corner"""
|
||||||
self.plot_item.legend.autoAnchor(50)
|
self.plot_item.legend.autoAnchor(50)
|
||||||
@ -257,22 +224,23 @@ class PlotBase(BECWidget, QWidget):
|
|||||||
raise ValueError("ui_mode must be an instance of UIMode")
|
raise ValueError("ui_mode must be an instance of UIMode")
|
||||||
self._ui_mode = mode
|
self._ui_mode = mode
|
||||||
|
|
||||||
# First, clear both UI elements:
|
|
||||||
if self.popup_bundle is not None:
|
|
||||||
for action_id in self.toolbar.bundles["popup_bundle"]:
|
|
||||||
self.toolbar.widgets[action_id].action.setVisible(False)
|
|
||||||
if self.axis_settings_dialog is not None and self.axis_settings_dialog.isVisible():
|
|
||||||
self.axis_settings_dialog.close()
|
|
||||||
self.side_panel.hide()
|
|
||||||
|
|
||||||
# Now, apply the new mode:
|
# Now, apply the new mode:
|
||||||
if mode == UIMode.POPUP:
|
if mode == UIMode.POPUP:
|
||||||
if self.popup_bundle is None:
|
shown_bundles = self.toolbar.shown_bundles
|
||||||
self.add_popups()
|
if "axis_popup" not in shown_bundles:
|
||||||
else:
|
shown_bundles.append("axis_popup")
|
||||||
for action_id in self.toolbar.bundles["popup_bundle"]:
|
self.toolbar.show_bundles(shown_bundles)
|
||||||
self.toolbar.widgets[action_id].action.setVisible(True)
|
self.side_panel.hide()
|
||||||
|
|
||||||
elif mode == UIMode.SIDE:
|
elif mode == UIMode.SIDE:
|
||||||
|
shown_bundles = self.toolbar.shown_bundles
|
||||||
|
if "axis_popup" in shown_bundles:
|
||||||
|
shown_bundles.remove("axis_popup")
|
||||||
|
self.toolbar.show_bundles(shown_bundles)
|
||||||
|
pb_connection = self.toolbar.bundles["axis_popup"].get_connection("plot_base")
|
||||||
|
if pb_connection.axis_settings_dialog is not None:
|
||||||
|
pb_connection.axis_settings_dialog.close()
|
||||||
|
pb_connection.axis_settings_dialog = None
|
||||||
self.add_side_menus()
|
self.add_side_menus()
|
||||||
self.side_panel.show()
|
self.side_panel.show()
|
||||||
|
|
||||||
@ -1049,6 +1017,7 @@ class PlotBase(BECWidget, QWidget):
|
|||||||
self.axis_settings_dialog = None
|
self.axis_settings_dialog = None
|
||||||
self.cleanup_pyqtgraph()
|
self.cleanup_pyqtgraph()
|
||||||
self.round_plot_widget.close()
|
self.round_plot_widget.close()
|
||||||
|
self.toolbar.cleanup()
|
||||||
super().cleanup()
|
super().cleanup()
|
||||||
|
|
||||||
def cleanup_pyqtgraph(self, item: pg.PlotItem | None = None):
|
def cleanup_pyqtgraph(self, item: pg.PlotItem | None = None):
|
||||||
@ -1087,8 +1056,12 @@ if __name__ == "__main__": # pragma: no cover:
|
|||||||
|
|
||||||
from qtpy.QtWidgets import QApplication
|
from qtpy.QtWidgets import QApplication
|
||||||
|
|
||||||
|
from bec_widgets.widgets.containers.main_window.main_window import BECMainWindow
|
||||||
|
|
||||||
app = QApplication(sys.argv)
|
app = QApplication(sys.argv)
|
||||||
window = PlotBase()
|
launch_window = BECMainWindow()
|
||||||
window.show()
|
pb = PlotBase(popups=False)
|
||||||
|
launch_window.setCentralWidget(pb)
|
||||||
|
launch_window.show()
|
||||||
|
|
||||||
sys.exit(app.exec_())
|
sys.exit(app.exec_())
|
||||||
|
@ -13,7 +13,7 @@ from bec_widgets.utils import Colors, ConnectionConfig
|
|||||||
from bec_widgets.utils.colors import set_theme
|
from bec_widgets.utils.colors import set_theme
|
||||||
from bec_widgets.utils.error_popups import SafeProperty, SafeSlot
|
from bec_widgets.utils.error_popups import SafeProperty, SafeSlot
|
||||||
from bec_widgets.utils.settings_dialog import SettingsDialog
|
from bec_widgets.utils.settings_dialog import SettingsDialog
|
||||||
from bec_widgets.utils.toolbar import MaterialIconAction
|
from bec_widgets.utils.toolbars.toolbar import MaterialIconAction
|
||||||
from bec_widgets.widgets.plots.plot_base import PlotBase, UIMode
|
from bec_widgets.widgets.plots.plot_base import PlotBase, UIMode
|
||||||
from bec_widgets.widgets.plots.scatter_waveform.scatter_curve import (
|
from bec_widgets.widgets.plots.scatter_waveform.scatter_curve import (
|
||||||
ScatterCurve,
|
ScatterCurve,
|
||||||
@ -131,7 +131,7 @@ class ScatterWaveform(PlotBase):
|
|||||||
self.proxy_update_sync = pg.SignalProxy(
|
self.proxy_update_sync = pg.SignalProxy(
|
||||||
self.sync_signal_update, rateLimit=25, slot=self.update_sync_curves
|
self.sync_signal_update, rateLimit=25, slot=self.update_sync_curves
|
||||||
)
|
)
|
||||||
if self.ui_mode == UIMode.SIDE:
|
|
||||||
self._init_scatter_curve_settings()
|
self._init_scatter_curve_settings()
|
||||||
self.update_with_scan_history(-1)
|
self.update_with_scan_history(-1)
|
||||||
|
|
||||||
@ -143,7 +143,7 @@ class ScatterWaveform(PlotBase):
|
|||||||
"""
|
"""
|
||||||
Initialize the scatter curve settings menu.
|
Initialize the scatter curve settings menu.
|
||||||
"""
|
"""
|
||||||
|
if self.ui_mode == UIMode.SIDE:
|
||||||
self.scatter_curve_settings = ScatterCurveSettings(
|
self.scatter_curve_settings = ScatterCurveSettings(
|
||||||
parent=self, target_widget=self, popup=False
|
parent=self, target_widget=self, popup=False
|
||||||
)
|
)
|
||||||
@ -154,33 +154,29 @@ class ScatterWaveform(PlotBase):
|
|||||||
widget=self.scatter_curve_settings,
|
widget=self.scatter_curve_settings,
|
||||||
title="Scatter Curve Settings",
|
title="Scatter Curve Settings",
|
||||||
)
|
)
|
||||||
|
else:
|
||||||
def add_popups(self):
|
scatter_curve_action = MaterialIconAction(
|
||||||
"""
|
|
||||||
Add popups to the ScatterWaveform widget.
|
|
||||||
"""
|
|
||||||
super().add_popups()
|
|
||||||
scatter_curve_setting_action = MaterialIconAction(
|
|
||||||
icon_name="scatter_plot",
|
icon_name="scatter_plot",
|
||||||
tooltip="Show Scatter Curve Settings",
|
tooltip="Show Scatter Curve Settings",
|
||||||
checkable=True,
|
checkable=True,
|
||||||
parent=self,
|
parent=self,
|
||||||
)
|
)
|
||||||
self.toolbar.add_action_to_bundle(
|
self.toolbar.components.add_safe("scatter_waveform_settings", scatter_curve_action)
|
||||||
bundle_id="popup_bundle",
|
self.toolbar.get_bundle("axis_popup").add_action("scatter_waveform_settings")
|
||||||
action_id="scatter_waveform_settings",
|
scatter_curve_action.action.triggered.connect(self.show_scatter_curve_settings)
|
||||||
action=scatter_curve_setting_action,
|
|
||||||
target_widget=self,
|
shown_bundles = self.toolbar.shown_bundles
|
||||||
)
|
if "performance" in shown_bundles:
|
||||||
self.toolbar.widgets["scatter_waveform_settings"].action.triggered.connect(
|
shown_bundles.remove("performance")
|
||||||
self.show_scatter_curve_settings
|
self.toolbar.show_bundles(shown_bundles)
|
||||||
)
|
|
||||||
|
|
||||||
def show_scatter_curve_settings(self):
|
def show_scatter_curve_settings(self):
|
||||||
"""
|
"""
|
||||||
Show the scatter curve settings dialog.
|
Show the scatter curve settings dialog.
|
||||||
"""
|
"""
|
||||||
scatter_settings_action = self.toolbar.widgets["scatter_waveform_settings"].action
|
scatter_settings_action = self.toolbar.components.get_action(
|
||||||
|
"scatter_waveform_settings"
|
||||||
|
).action
|
||||||
if self.scatter_dialog is None or not self.scatter_dialog.isVisible():
|
if self.scatter_dialog is None or not self.scatter_dialog.isVisible():
|
||||||
scatter_settings = ScatterCurveSettings(parent=self, target_widget=self, popup=True)
|
scatter_settings = ScatterCurveSettings(parent=self, target_widget=self, popup=True)
|
||||||
self.scatter_dialog = SettingsDialog(
|
self.scatter_dialog = SettingsDialog(
|
||||||
@ -205,7 +201,7 @@ class ScatterWaveform(PlotBase):
|
|||||||
Slot for when the scatter curve settings dialog is closed.
|
Slot for when the scatter curve settings dialog is closed.
|
||||||
"""
|
"""
|
||||||
self.scatter_dialog = None
|
self.scatter_dialog = None
|
||||||
self.toolbar.widgets["scatter_waveform_settings"].action.setChecked(False)
|
self.toolbar.components.get_action("scatter_waveform_settings").action.setChecked(False)
|
||||||
|
|
||||||
################################################################################
|
################################################################################
|
||||||
# Widget Specific Properties
|
# Widget Specific Properties
|
||||||
|
@ -1,108 +0,0 @@
|
|||||||
import pyqtgraph as pg
|
|
||||||
from qtpy.QtCore import QTimer
|
|
||||||
|
|
||||||
from bec_widgets.utils.error_popups import SafeSlot
|
|
||||||
from bec_widgets.utils.toolbar import MaterialIconAction, SwitchableToolBarAction, ToolbarBundle
|
|
||||||
|
|
||||||
|
|
||||||
class MouseInteractionToolbarBundle(ToolbarBundle):
|
|
||||||
"""
|
|
||||||
A bundle of actions that are hooked in this constructor itself,
|
|
||||||
so that you can immediately connect the signals and toggle states.
|
|
||||||
|
|
||||||
This bundle is for a toolbar that controls mouse interactions on a plot.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, bundle_id="mouse_interaction", target_widget=None, **kwargs):
|
|
||||||
super().__init__(bundle_id=bundle_id, actions=[], **kwargs)
|
|
||||||
self.target_widget = target_widget
|
|
||||||
self.mouse_mode = None
|
|
||||||
|
|
||||||
# Create each MaterialIconAction with a parent
|
|
||||||
# so the signals can fire even if the toolbar isn't added yet.
|
|
||||||
drag = MaterialIconAction(
|
|
||||||
icon_name="drag_pan",
|
|
||||||
tooltip="Drag Mouse Mode",
|
|
||||||
checkable=True,
|
|
||||||
parent=self.target_widget, # or any valid parent
|
|
||||||
)
|
|
||||||
rect = MaterialIconAction(
|
|
||||||
icon_name="frame_inspect",
|
|
||||||
tooltip="Rectangle Zoom Mode",
|
|
||||||
checkable=True,
|
|
||||||
parent=self.target_widget,
|
|
||||||
)
|
|
||||||
auto = MaterialIconAction(
|
|
||||||
icon_name="open_in_full",
|
|
||||||
tooltip="Autorange Plot",
|
|
||||||
checkable=False,
|
|
||||||
parent=self.target_widget,
|
|
||||||
)
|
|
||||||
|
|
||||||
self.switch_mouse_action = SwitchableToolBarAction(
|
|
||||||
actions={"drag_mode": drag, "rectangle_mode": rect},
|
|
||||||
initial_action="drag_mode",
|
|
||||||
tooltip="Mouse Modes",
|
|
||||||
checkable=True,
|
|
||||||
parent=self.target_widget,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Add them to the bundle
|
|
||||||
self.add_action("switch_mouse", self.switch_mouse_action)
|
|
||||||
self.add_action("auto_range", auto)
|
|
||||||
|
|
||||||
# Immediately connect signals
|
|
||||||
drag.action.toggled.connect(self.enable_mouse_pan_mode)
|
|
||||||
rect.action.toggled.connect(self.enable_mouse_rectangle_mode)
|
|
||||||
auto.action.triggered.connect(self.autorange_plot)
|
|
||||||
|
|
||||||
def get_viewbox_mode(self):
|
|
||||||
"""
|
|
||||||
Returns the current interaction mode of a PyQtGraph ViewBox and sets the corresponding action.
|
|
||||||
"""
|
|
||||||
|
|
||||||
if self.target_widget:
|
|
||||||
viewbox = self.target_widget.plot_item.getViewBox()
|
|
||||||
if viewbox.getState()["mouseMode"] == 3:
|
|
||||||
self.switch_mouse_action.set_default_action("drag_mode")
|
|
||||||
self.switch_mouse_action.main_button.setChecked(True)
|
|
||||||
self.mouse_mode = "PanMode"
|
|
||||||
elif viewbox.getState()["mouseMode"] == 1:
|
|
||||||
self.switch_mouse_action.set_default_action("rectangle_mode")
|
|
||||||
self.switch_mouse_action.main_button.setChecked(True)
|
|
||||||
self.mouse_mode = "RectMode"
|
|
||||||
|
|
||||||
@SafeSlot(bool)
|
|
||||||
def enable_mouse_rectangle_mode(self, checked: bool):
|
|
||||||
"""
|
|
||||||
Enable the rectangle zoom mode on the plot widget.
|
|
||||||
"""
|
|
||||||
if self.mouse_mode == "RectMode":
|
|
||||||
self.switch_mouse_action.main_button.setChecked(True)
|
|
||||||
return
|
|
||||||
self.actions["switch_mouse"].actions["drag_mode"].action.setChecked(not checked)
|
|
||||||
if self.target_widget and checked:
|
|
||||||
self.target_widget.plot_item.getViewBox().setMouseMode(pg.ViewBox.RectMode)
|
|
||||||
self.mouse_mode = "RectMode"
|
|
||||||
|
|
||||||
@SafeSlot(bool)
|
|
||||||
def enable_mouse_pan_mode(self, checked: bool):
|
|
||||||
"""
|
|
||||||
Enable the pan mode on the plot widget.
|
|
||||||
"""
|
|
||||||
if self.mouse_mode == "PanMode":
|
|
||||||
self.switch_mouse_action.main_button.setChecked(True)
|
|
||||||
return
|
|
||||||
self.actions["switch_mouse"].actions["rectangle_mode"].action.setChecked(not checked)
|
|
||||||
if self.target_widget and checked:
|
|
||||||
self.target_widget.plot_item.getViewBox().setMouseMode(pg.ViewBox.PanMode)
|
|
||||||
self.mouse_mode = "PanMode"
|
|
||||||
|
|
||||||
@SafeSlot()
|
|
||||||
def autorange_plot(self):
|
|
||||||
"""
|
|
||||||
Enable autorange on the plot widget.
|
|
||||||
"""
|
|
||||||
if self.target_widget:
|
|
||||||
self.target_widget.auto_range_x = True
|
|
||||||
self.target_widget.auto_range_y = True
|
|
@ -1,81 +0,0 @@
|
|||||||
import traceback
|
|
||||||
|
|
||||||
from pyqtgraph.exporters import MatplotlibExporter
|
|
||||||
|
|
||||||
from bec_widgets.utils.error_popups import SafeSlot, WarningPopupUtility
|
|
||||||
from bec_widgets.utils.toolbar import MaterialIconAction, SwitchableToolBarAction, ToolbarBundle
|
|
||||||
|
|
||||||
|
|
||||||
class PlotExportBundle(ToolbarBundle):
|
|
||||||
"""
|
|
||||||
A bundle of actions that are hooked in this constructor itself,
|
|
||||||
so that you can immediately connect the signals and toggle states.
|
|
||||||
|
|
||||||
This bundle is for a toolbar that controls exporting a plot.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, bundle_id="mouse_interaction", target_widget=None, **kwargs):
|
|
||||||
super().__init__(bundle_id=bundle_id, actions=[], **kwargs)
|
|
||||||
self.target_widget = target_widget
|
|
||||||
|
|
||||||
# Create each MaterialIconAction with a parent
|
|
||||||
# so the signals can fire even if the toolbar isn't added yet.
|
|
||||||
save = MaterialIconAction(
|
|
||||||
icon_name="save", tooltip="Open Export Dialog", parent=self.target_widget
|
|
||||||
)
|
|
||||||
matplotlib = MaterialIconAction(
|
|
||||||
icon_name="photo_library", tooltip="Open Matplotlib Dialog", parent=self.target_widget
|
|
||||||
)
|
|
||||||
|
|
||||||
switch_export_action = SwitchableToolBarAction(
|
|
||||||
actions={"save": save, "matplotlib": matplotlib},
|
|
||||||
initial_action="save",
|
|
||||||
tooltip="Switchable Action",
|
|
||||||
checkable=False,
|
|
||||||
parent=self,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Add them to the bundle
|
|
||||||
self.add_action("export_switch", switch_export_action)
|
|
||||||
|
|
||||||
# Immediately connect signals
|
|
||||||
save.action.triggered.connect(self.export_dialog)
|
|
||||||
matplotlib.action.triggered.connect(self.matplotlib_dialog)
|
|
||||||
|
|
||||||
@SafeSlot()
|
|
||||||
def export_dialog(self):
|
|
||||||
"""
|
|
||||||
Open the export dialog for the plot widget.
|
|
||||||
"""
|
|
||||||
if self.target_widget:
|
|
||||||
scene = self.target_widget.plot_item.scene()
|
|
||||||
scene.contextMenuItem = self.target_widget.plot_item
|
|
||||||
scene.showExportDialog()
|
|
||||||
|
|
||||||
@SafeSlot()
|
|
||||||
def matplotlib_dialog(self):
|
|
||||||
"""
|
|
||||||
Export the plot widget to Matplotlib.
|
|
||||||
"""
|
|
||||||
if self.target_widget:
|
|
||||||
try:
|
|
||||||
import matplotlib as mpl
|
|
||||||
|
|
||||||
MatplotlibExporter(self.target_widget.plot_item).export()
|
|
||||||
except ModuleNotFoundError:
|
|
||||||
warning_util = WarningPopupUtility()
|
|
||||||
warning_util.show_warning(
|
|
||||||
title="Matplotlib not installed",
|
|
||||||
message="Matplotlib is required for this feature.",
|
|
||||||
detailed_text="Please install matplotlib in your Python environment by using 'pip install matplotlib'.",
|
|
||||||
)
|
|
||||||
return
|
|
||||||
except TypeError:
|
|
||||||
warning_util = WarningPopupUtility()
|
|
||||||
error_msg = traceback.format_exc()
|
|
||||||
warning_util.show_warning(
|
|
||||||
title="Matplotlib TypeError",
|
|
||||||
message="Matplotlib exporter could not resolve the plot item.",
|
|
||||||
detailed_text=error_msg,
|
|
||||||
)
|
|
||||||
return
|
|
@ -1,31 +0,0 @@
|
|||||||
from bec_widgets.utils.toolbar import MaterialIconAction, ToolbarBundle
|
|
||||||
|
|
||||||
|
|
||||||
class ROIBundle(ToolbarBundle):
|
|
||||||
"""
|
|
||||||
A bundle of actions that are hooked in this constructor itself,
|
|
||||||
so that you can immediately connect the signals and toggle states.
|
|
||||||
|
|
||||||
This bundle is for a toolbar that controls crosshair and ROI interaction.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, bundle_id="roi", target_widget=None, **kwargs):
|
|
||||||
super().__init__(bundle_id=bundle_id, actions=[], **kwargs)
|
|
||||||
self.target_widget = target_widget
|
|
||||||
|
|
||||||
# Create each MaterialIconAction with a parent
|
|
||||||
# so the signals can fire even if the toolbar isn't added yet.
|
|
||||||
crosshair = MaterialIconAction(
|
|
||||||
icon_name="point_scan", tooltip="Show Crosshair", checkable=True
|
|
||||||
)
|
|
||||||
reset_legend = MaterialIconAction(
|
|
||||||
icon_name="restart_alt", tooltip="Reset the position of legend.", checkable=False
|
|
||||||
)
|
|
||||||
|
|
||||||
# Add them to the bundle
|
|
||||||
self.add_action("crosshair", crosshair)
|
|
||||||
self.add_action("reset_legend", reset_legend)
|
|
||||||
|
|
||||||
# Immediately connect signals
|
|
||||||
crosshair.action.toggled.connect(self.target_widget.toggle_crosshair)
|
|
||||||
reset_legend.action.triggered.connect(self.target_widget.reset_legend)
|
|
@ -1,48 +0,0 @@
|
|||||||
from bec_widgets.utils.error_popups import SafeSlot
|
|
||||||
from bec_widgets.utils.toolbar import MaterialIconAction, ToolbarBundle
|
|
||||||
|
|
||||||
|
|
||||||
class SaveStateBundle(ToolbarBundle):
|
|
||||||
"""
|
|
||||||
A bundle of actions that are hooked in this constructor itself,
|
|
||||||
so that you can immediately connect the signals and toggle states.
|
|
||||||
|
|
||||||
This bundle is for a toolbar that controls saving the state of the widget.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, bundle_id="mouse_interaction", target_widget=None, **kwargs):
|
|
||||||
super().__init__(bundle_id=bundle_id, actions=[], **kwargs)
|
|
||||||
self.target_widget = target_widget
|
|
||||||
|
|
||||||
# Create each MaterialIconAction with a parent
|
|
||||||
# so the signals can fire even if the toolbar isn't added yet.
|
|
||||||
save_state = MaterialIconAction(
|
|
||||||
icon_name="download", tooltip="Save Widget State", parent=self.target_widget
|
|
||||||
)
|
|
||||||
load_state = MaterialIconAction(
|
|
||||||
icon_name="upload", tooltip="Load Widget State", parent=self.target_widget
|
|
||||||
)
|
|
||||||
|
|
||||||
# Add them to the bundle
|
|
||||||
self.add_action("save", save_state)
|
|
||||||
self.add_action("matplotlib", load_state)
|
|
||||||
|
|
||||||
# Immediately connect signals
|
|
||||||
save_state.action.triggered.connect(self.save_state_dialog)
|
|
||||||
load_state.action.triggered.connect(self.load_state_dialog)
|
|
||||||
|
|
||||||
@SafeSlot()
|
|
||||||
def save_state_dialog(self):
|
|
||||||
"""
|
|
||||||
Open the export dialog to save a state of the widget.
|
|
||||||
"""
|
|
||||||
if self.target_widget:
|
|
||||||
self.target_widget.state_manager.save_state()
|
|
||||||
|
|
||||||
@SafeSlot()
|
|
||||||
def load_state_dialog(self):
|
|
||||||
"""
|
|
||||||
Load a saved state of the widget.
|
|
||||||
"""
|
|
||||||
if self.target_widget:
|
|
||||||
self.target_widget.state_manager.load_state()
|
|
@ -0,0 +1,94 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
from bec_widgets.utils.settings_dialog import SettingsDialog
|
||||||
|
from bec_widgets.utils.toolbars.actions import MaterialIconAction
|
||||||
|
from bec_widgets.utils.toolbars.bundles import ToolbarBundle
|
||||||
|
from bec_widgets.utils.toolbars.connections import BundleConnection
|
||||||
|
from bec_widgets.widgets.plots.setting_menus.axis_settings import AxisSettings
|
||||||
|
|
||||||
|
if TYPE_CHECKING: # pragma: no cover
|
||||||
|
from bec_widgets.utils.toolbars.toolbar import ToolbarComponents
|
||||||
|
|
||||||
|
|
||||||
|
def axis_popup_bundle(components: ToolbarComponents) -> ToolbarBundle:
|
||||||
|
"""
|
||||||
|
Creates an axis popup toolbar bundle.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
components (ToolbarComponents): The components to be added to the bundle.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
ToolbarBundle: The axis popup toolbar bundle.
|
||||||
|
"""
|
||||||
|
components.add_safe(
|
||||||
|
"axis_settings_popup",
|
||||||
|
MaterialIconAction(
|
||||||
|
icon_name="settings",
|
||||||
|
tooltip="Show Axis Settings",
|
||||||
|
checkable=True,
|
||||||
|
parent=components.toolbar,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
bundle = ToolbarBundle("axis_popup", components)
|
||||||
|
bundle.add_action("axis_settings_popup")
|
||||||
|
return bundle
|
||||||
|
|
||||||
|
|
||||||
|
class AxisSettingsPopupConnection(BundleConnection):
|
||||||
|
|
||||||
|
def __init__(self, components: ToolbarComponents, target_widget=None):
|
||||||
|
self.bundle_name = "axis_popup"
|
||||||
|
self.components = components
|
||||||
|
self.target_widget = target_widget
|
||||||
|
self.axis_settings_dialog = None
|
||||||
|
self._connected = False
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
def connect(self):
|
||||||
|
self._connected = True
|
||||||
|
# Connect the action to the target widget's method
|
||||||
|
self.components.get_action_reference("axis_settings_popup")().action.triggered.connect(
|
||||||
|
self.show_axis_settings_popup
|
||||||
|
)
|
||||||
|
|
||||||
|
def disconnect(self):
|
||||||
|
if not self._connected:
|
||||||
|
return
|
||||||
|
# Disconnect the action from the target widget's method
|
||||||
|
self.components.get_action_reference("axis_settings_popup")().action.triggered.disconnect(
|
||||||
|
self.show_axis_settings_popup
|
||||||
|
)
|
||||||
|
|
||||||
|
def show_axis_settings_popup(self):
|
||||||
|
"""
|
||||||
|
Show the axis settings dialog.
|
||||||
|
"""
|
||||||
|
settings_action = self.components.get_action_reference("axis_settings_popup")().action
|
||||||
|
if self.axis_settings_dialog is None or not self.axis_settings_dialog.isVisible():
|
||||||
|
axis_setting = AxisSettings(
|
||||||
|
parent=self.target_widget, target_widget=self.target_widget, popup=True
|
||||||
|
)
|
||||||
|
self.axis_settings_dialog = SettingsDialog(
|
||||||
|
self.target_widget,
|
||||||
|
settings_widget=axis_setting,
|
||||||
|
window_title="Axis Settings",
|
||||||
|
modal=False,
|
||||||
|
)
|
||||||
|
# When the dialog is closed, update the toolbar icon and clear the reference
|
||||||
|
self.axis_settings_dialog.finished.connect(self._axis_settings_closed)
|
||||||
|
self.axis_settings_dialog.show()
|
||||||
|
settings_action.setChecked(True)
|
||||||
|
else:
|
||||||
|
# If already open, bring it to the front
|
||||||
|
self.axis_settings_dialog.raise_()
|
||||||
|
self.axis_settings_dialog.activateWindow()
|
||||||
|
settings_action.setChecked(True) # keep it toggled
|
||||||
|
|
||||||
|
def _axis_settings_closed(self):
|
||||||
|
"""
|
||||||
|
Slot for when the axis settings dialog is closed.
|
||||||
|
"""
|
||||||
|
self.axis_settings_dialog = None
|
||||||
|
self.components.get_action_reference("axis_settings_popup")().action.setChecked(False)
|
@ -0,0 +1,169 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
import pyqtgraph as pg
|
||||||
|
|
||||||
|
from bec_widgets.utils.error_popups import SafeSlot
|
||||||
|
from bec_widgets.utils.toolbars.actions import MaterialIconAction, SwitchableToolBarAction
|
||||||
|
from bec_widgets.utils.toolbars.bundles import ToolbarBundle
|
||||||
|
from bec_widgets.utils.toolbars.connections import BundleConnection
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from bec_widgets.utils.toolbars.toolbar import ToolbarComponents
|
||||||
|
|
||||||
|
|
||||||
|
def mouse_interaction_bundle(components: ToolbarComponents) -> ToolbarBundle:
|
||||||
|
"""
|
||||||
|
Creates a mouse interaction toolbar bundle.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
components (ToolbarComponents): The components to be added to the bundle.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
ToolbarBundle: The mouse interaction toolbar bundle.
|
||||||
|
"""
|
||||||
|
components.add_safe(
|
||||||
|
"mouse_drag",
|
||||||
|
MaterialIconAction(
|
||||||
|
icon_name="drag_pan",
|
||||||
|
tooltip="Drag Mouse Mode",
|
||||||
|
checkable=True,
|
||||||
|
parent=components.toolbar,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
components.add_safe(
|
||||||
|
"mouse_rect",
|
||||||
|
MaterialIconAction(
|
||||||
|
icon_name="frame_inspect",
|
||||||
|
tooltip="Rectangle Zoom Mode",
|
||||||
|
checkable=True,
|
||||||
|
parent=components.toolbar,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
components.add_safe(
|
||||||
|
"auto_range",
|
||||||
|
MaterialIconAction(
|
||||||
|
icon_name="open_in_full",
|
||||||
|
tooltip="Autorange Plot",
|
||||||
|
checkable=False,
|
||||||
|
parent=components.toolbar,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
components.add_safe(
|
||||||
|
"switch_mouse_mode",
|
||||||
|
SwitchableToolBarAction(
|
||||||
|
actions={
|
||||||
|
"drag_mode": components.get_action_reference("mouse_drag")(),
|
||||||
|
"rectangle_mode": components.get_action_reference("mouse_rect")(),
|
||||||
|
},
|
||||||
|
initial_action="drag_mode",
|
||||||
|
tooltip="Mouse Modes",
|
||||||
|
checkable=True,
|
||||||
|
parent=components.toolbar,
|
||||||
|
default_state_checked=True,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
bundle = ToolbarBundle("mouse_interaction", components)
|
||||||
|
bundle.add_action("switch_mouse_mode")
|
||||||
|
bundle.add_action("auto_range")
|
||||||
|
return bundle
|
||||||
|
|
||||||
|
|
||||||
|
class MouseInteractionConnection(BundleConnection):
|
||||||
|
"""
|
||||||
|
Connection class for mouse interaction toolbar bundle.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, components: ToolbarComponents, target_widget=None):
|
||||||
|
self.bundle_name = "mouse_interaction"
|
||||||
|
self.components = components
|
||||||
|
self.target_widget = target_widget
|
||||||
|
self.mouse_mode = None
|
||||||
|
if (
|
||||||
|
not hasattr(self.target_widget, "plot_item")
|
||||||
|
or not hasattr(self.target_widget, "auto_range_x")
|
||||||
|
or not hasattr(self.target_widget, "auto_range_y")
|
||||||
|
):
|
||||||
|
raise AttributeError(
|
||||||
|
"Target widget must implement required methods for mouse interactions."
|
||||||
|
)
|
||||||
|
super().__init__()
|
||||||
|
self._connected = False # Track if the connection has been made
|
||||||
|
|
||||||
|
def connect(self):
|
||||||
|
self._connected = True
|
||||||
|
drag = self.components.get_action_reference("mouse_drag")()
|
||||||
|
rect = self.components.get_action_reference("mouse_rect")()
|
||||||
|
auto = self.components.get_action_reference("auto_range")()
|
||||||
|
|
||||||
|
drag.action.toggled.connect(self.enable_mouse_pan_mode)
|
||||||
|
rect.action.toggled.connect(self.enable_mouse_rectangle_mode)
|
||||||
|
auto.action.triggered.connect(self.autorange_plot)
|
||||||
|
|
||||||
|
def disconnect(self):
|
||||||
|
if not self._connected:
|
||||||
|
return
|
||||||
|
# Disconnect the action from the target widget's method
|
||||||
|
drag = self.components.get_action_reference("mouse_drag")()
|
||||||
|
rect = self.components.get_action_reference("mouse_rect")()
|
||||||
|
auto = self.components.get_action_reference("auto_range")()
|
||||||
|
drag.action.toggled.disconnect(self.enable_mouse_pan_mode)
|
||||||
|
rect.action.toggled.disconnect(self.enable_mouse_rectangle_mode)
|
||||||
|
auto.action.triggered.disconnect(self.autorange_plot)
|
||||||
|
|
||||||
|
def get_viewbox_mode(self):
|
||||||
|
"""
|
||||||
|
Returns the current interaction mode of a PyQtGraph ViewBox and sets the corresponding action.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if self.target_widget:
|
||||||
|
viewbox = self.target_widget.plot_item.getViewBox()
|
||||||
|
switch_mouse_action = self.components.get_action_reference("switch_mouse_mode")()
|
||||||
|
if viewbox.getState()["mouseMode"] == 3:
|
||||||
|
switch_mouse_action.set_default_action("drag_mode")
|
||||||
|
switch_mouse_action.main_button.setChecked(True)
|
||||||
|
self.mouse_mode = "PanMode"
|
||||||
|
elif viewbox.getState()["mouseMode"] == 1:
|
||||||
|
switch_mouse_action.set_default_action("rectangle_mode")
|
||||||
|
switch_mouse_action.main_button.setChecked(True)
|
||||||
|
self.mouse_mode = "RectMode"
|
||||||
|
|
||||||
|
@SafeSlot(bool)
|
||||||
|
def enable_mouse_rectangle_mode(self, checked: bool):
|
||||||
|
"""
|
||||||
|
Enable the rectangle zoom mode on the plot widget.
|
||||||
|
"""
|
||||||
|
switch_mouse_action = self.components.get_action_reference("switch_mouse_mode")()
|
||||||
|
if self.mouse_mode == "RectMode":
|
||||||
|
switch_mouse_action.main_button.setChecked(True)
|
||||||
|
return
|
||||||
|
drag_mode = self.components.get_action_reference("mouse_drag")()
|
||||||
|
drag_mode.action.setChecked(not checked)
|
||||||
|
if self.target_widget and checked:
|
||||||
|
self.target_widget.plot_item.getViewBox().setMouseMode(pg.ViewBox.RectMode)
|
||||||
|
self.mouse_mode = "RectMode"
|
||||||
|
|
||||||
|
@SafeSlot(bool)
|
||||||
|
def enable_mouse_pan_mode(self, checked: bool):
|
||||||
|
"""
|
||||||
|
Enable the pan mode on the plot widget.
|
||||||
|
"""
|
||||||
|
if self.mouse_mode == "PanMode":
|
||||||
|
switch_mouse_action = self.components.get_action_reference("switch_mouse_mode")()
|
||||||
|
switch_mouse_action.main_button.setChecked(True)
|
||||||
|
return
|
||||||
|
rect_mode = self.components.get_action_reference("mouse_rect")()
|
||||||
|
rect_mode.action.setChecked(not checked)
|
||||||
|
if self.target_widget and checked:
|
||||||
|
self.target_widget.plot_item.getViewBox().setMouseMode(pg.ViewBox.PanMode)
|
||||||
|
self.mouse_mode = "PanMode"
|
||||||
|
|
||||||
|
@SafeSlot()
|
||||||
|
def autorange_plot(self):
|
||||||
|
"""
|
||||||
|
Enable autorange on the plot widget.
|
||||||
|
"""
|
||||||
|
if self.target_widget:
|
||||||
|
self.target_widget.auto_range_x = True
|
||||||
|
self.target_widget.auto_range_y = True
|
123
bec_widgets/widgets/plots/toolbar_components/plot_export.py
Normal file
123
bec_widgets/widgets/plots/toolbar_components/plot_export.py
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
from bec_widgets.utils.error_popups import SafeSlot, WarningPopupUtility
|
||||||
|
from bec_widgets.utils.toolbars.actions import MaterialIconAction, SwitchableToolBarAction
|
||||||
|
from bec_widgets.utils.toolbars.bundles import ToolbarBundle, ToolbarComponents
|
||||||
|
from bec_widgets.utils.toolbars.connections import BundleConnection
|
||||||
|
|
||||||
|
|
||||||
|
def plot_export_bundle(components: ToolbarComponents) -> ToolbarBundle:
|
||||||
|
"""
|
||||||
|
Creates a plot export toolbar bundle.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
components (ToolbarComponents): The components to be added to the bundle.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
ToolbarBundle: The plot export toolbar bundle.
|
||||||
|
"""
|
||||||
|
components.add_safe(
|
||||||
|
"save",
|
||||||
|
MaterialIconAction(
|
||||||
|
icon_name="save", tooltip="Open Export Dialog", parent=components.toolbar
|
||||||
|
),
|
||||||
|
)
|
||||||
|
components.add_safe(
|
||||||
|
"matplotlib",
|
||||||
|
MaterialIconAction(
|
||||||
|
icon_name="photo_library", tooltip="Open Matplotlib Dialog", parent=components.toolbar
|
||||||
|
),
|
||||||
|
)
|
||||||
|
components.add_safe(
|
||||||
|
"export_switch",
|
||||||
|
SwitchableToolBarAction(
|
||||||
|
actions={
|
||||||
|
"save": components.get_action_reference("save")(),
|
||||||
|
"matplotlib": components.get_action_reference("matplotlib")(),
|
||||||
|
},
|
||||||
|
initial_action="save",
|
||||||
|
tooltip="Export Plot",
|
||||||
|
checkable=False,
|
||||||
|
parent=components.toolbar,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
bundle = ToolbarBundle("plot_export", components)
|
||||||
|
bundle.add_action("export_switch")
|
||||||
|
return bundle
|
||||||
|
|
||||||
|
|
||||||
|
def plot_export_connection(components: ToolbarComponents, target_widget=None):
|
||||||
|
"""
|
||||||
|
Connects the plot export actions to the target widget.
|
||||||
|
Args:
|
||||||
|
components (ToolbarComponents): The components to be connected.
|
||||||
|
target_widget: The widget to which the actions will be connected.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class PlotExportConnection(BundleConnection):
|
||||||
|
def __init__(self, components: ToolbarComponents, target_widget):
|
||||||
|
super().__init__()
|
||||||
|
self.bundle_name = "plot_export"
|
||||||
|
self.components = components
|
||||||
|
self.target_widget = target_widget
|
||||||
|
self._connected = False # Track if the connection has been made
|
||||||
|
|
||||||
|
def connect(self):
|
||||||
|
self._connected = True
|
||||||
|
# Connect the actions to the target widget
|
||||||
|
self.components.get_action_reference("save")().action.triggered.connect(self.export_dialog)
|
||||||
|
self.components.get_action_reference("matplotlib")().action.triggered.connect(
|
||||||
|
self.matplotlib_dialog
|
||||||
|
)
|
||||||
|
|
||||||
|
def disconnect(self):
|
||||||
|
if not self._connected:
|
||||||
|
return
|
||||||
|
# Disconnect the actions from the target widget
|
||||||
|
self.components.get_action_reference("save")().action.triggered.disconnect(
|
||||||
|
self.export_dialog
|
||||||
|
)
|
||||||
|
self.components.get_action_reference("matplotlib")().action.triggered.disconnect(
|
||||||
|
self.matplotlib_dialog
|
||||||
|
)
|
||||||
|
|
||||||
|
@SafeSlot()
|
||||||
|
def export_dialog(self):
|
||||||
|
"""
|
||||||
|
Open the export dialog for the plot widget.
|
||||||
|
"""
|
||||||
|
if self.target_widget:
|
||||||
|
scene = self.target_widget.plot_item.scene()
|
||||||
|
scene.contextMenuItem = self.target_widget.plot_item
|
||||||
|
scene.showExportDialog()
|
||||||
|
|
||||||
|
@SafeSlot()
|
||||||
|
def matplotlib_dialog(self):
|
||||||
|
"""
|
||||||
|
Export the plot widget to Matplotlib.
|
||||||
|
"""
|
||||||
|
if self.target_widget:
|
||||||
|
try:
|
||||||
|
import matplotlib as mpl
|
||||||
|
|
||||||
|
MatplotlibExporter(self.target_widget.plot_item).export()
|
||||||
|
except ModuleNotFoundError:
|
||||||
|
warning_util = WarningPopupUtility()
|
||||||
|
warning_util.show_warning(
|
||||||
|
title="Matplotlib not installed",
|
||||||
|
message="Matplotlib is required for this feature.",
|
||||||
|
detailed_text="Please install matplotlib in your Python environment by using 'pip install matplotlib'.",
|
||||||
|
)
|
||||||
|
return
|
||||||
|
except TypeError:
|
||||||
|
warning_util = WarningPopupUtility()
|
||||||
|
error_msg = traceback.format_exc()
|
||||||
|
warning_util.show_warning(
|
||||||
|
title="Matplotlib TypeError",
|
||||||
|
message="Matplotlib exporter could not resolve the plot item.",
|
||||||
|
detailed_text=error_msg,
|
||||||
|
)
|
||||||
|
return
|
79
bec_widgets/widgets/plots/toolbar_components/roi.py
Normal file
79
bec_widgets/widgets/plots/toolbar_components/roi.py
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from bec_widgets.utils.toolbars.actions import MaterialIconAction
|
||||||
|
from bec_widgets.utils.toolbars.bundles import ToolbarBundle, ToolbarComponents
|
||||||
|
from bec_widgets.utils.toolbars.connections import BundleConnection
|
||||||
|
|
||||||
|
|
||||||
|
def roi_bundle(components: ToolbarComponents) -> ToolbarBundle:
|
||||||
|
"""
|
||||||
|
Creates a toolbar bundle for ROI and crosshair interaction.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
components (ToolbarComponents): The components to be added to the bundle.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
ToolbarBundle: The ROI toolbar bundle.
|
||||||
|
"""
|
||||||
|
components.add_safe(
|
||||||
|
"crosshair",
|
||||||
|
MaterialIconAction(
|
||||||
|
icon_name="point_scan",
|
||||||
|
tooltip="Show Crosshair",
|
||||||
|
checkable=True,
|
||||||
|
parent=components.toolbar,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
components.add_safe(
|
||||||
|
"reset_legend",
|
||||||
|
MaterialIconAction(
|
||||||
|
icon_name="restart_alt",
|
||||||
|
tooltip="Reset the position of legend.",
|
||||||
|
checkable=False,
|
||||||
|
parent=components.toolbar,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
bundle = ToolbarBundle("roi", components)
|
||||||
|
bundle.add_action("crosshair")
|
||||||
|
bundle.add_action("reset_legend")
|
||||||
|
return bundle
|
||||||
|
|
||||||
|
|
||||||
|
class RoiConnection(BundleConnection):
|
||||||
|
"""
|
||||||
|
Connection class for the ROI toolbar bundle.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, components: ToolbarComponents, target_widget=None):
|
||||||
|
self.bundle_name = "roi"
|
||||||
|
self.components = components
|
||||||
|
self.target_widget = target_widget
|
||||||
|
if not hasattr(self.target_widget, "toggle_crosshair") or not hasattr(
|
||||||
|
self.target_widget, "reset_legend"
|
||||||
|
):
|
||||||
|
raise AttributeError(
|
||||||
|
"Target widget must implement 'toggle_crosshair' and 'reset_legend'."
|
||||||
|
)
|
||||||
|
super().__init__()
|
||||||
|
self._connected = False
|
||||||
|
|
||||||
|
def connect(self):
|
||||||
|
self._connected = True
|
||||||
|
# Connect the action to the target widget's method
|
||||||
|
self.components.get_action_reference("crosshair")().action.toggled.connect(
|
||||||
|
self.target_widget.toggle_crosshair
|
||||||
|
)
|
||||||
|
self.components.get_action_reference("reset_legend")().action.triggered.connect(
|
||||||
|
self.target_widget.reset_legend
|
||||||
|
)
|
||||||
|
|
||||||
|
def disconnect(self):
|
||||||
|
if not self._connected:
|
||||||
|
return
|
||||||
|
# Disconnect the action from the target widget's method
|
||||||
|
self.components.get_action_reference("crosshair")().action.toggled.disconnect(
|
||||||
|
self.target_widget.toggle_crosshair
|
||||||
|
)
|
||||||
|
self.components.get_action_reference("reset_legend")().action.triggered.disconnect(
|
||||||
|
self.target_widget.reset_legend
|
||||||
|
)
|
@ -25,7 +25,9 @@ from bec_widgets import SafeSlot
|
|||||||
from bec_widgets.utils import ConnectionConfig, EntryValidator
|
from bec_widgets.utils import ConnectionConfig, EntryValidator
|
||||||
from bec_widgets.utils.bec_widget import BECWidget
|
from bec_widgets.utils.bec_widget import BECWidget
|
||||||
from bec_widgets.utils.colors import Colors
|
from bec_widgets.utils.colors import Colors
|
||||||
from bec_widgets.utils.toolbar import MaterialIconAction, ModularToolBar
|
from bec_widgets.utils.toolbars.actions import WidgetAction
|
||||||
|
from bec_widgets.utils.toolbars.bundles import ToolbarBundle
|
||||||
|
from bec_widgets.utils.toolbars.toolbar import MaterialIconAction, ModularToolBar
|
||||||
from bec_widgets.widgets.control.device_input.device_combobox.device_combobox import DeviceComboBox
|
from bec_widgets.widgets.control.device_input.device_combobox.device_combobox import DeviceComboBox
|
||||||
from bec_widgets.widgets.control.device_input.signal_combobox.signal_combobox import SignalComboBox
|
from bec_widgets.widgets.control.device_input.signal_combobox.signal_combobox import SignalComboBox
|
||||||
from bec_widgets.widgets.dap.dap_combo_box.dap_combo_box import DapComboBox
|
from bec_widgets.widgets.dap.dap_combo_box.dap_combo_box import DapComboBox
|
||||||
@ -379,43 +381,67 @@ class CurveTree(BECWidget, QWidget):
|
|||||||
|
|
||||||
def _init_toolbar(self):
|
def _init_toolbar(self):
|
||||||
"""Initialize the toolbar with actions: add, send, refresh, expand, collapse, renormalize."""
|
"""Initialize the toolbar with actions: add, send, refresh, expand, collapse, renormalize."""
|
||||||
self.toolbar = ModularToolBar(parent=self, target_widget=self, orientation="horizontal")
|
self.toolbar = ModularToolBar(parent=self, orientation="horizontal")
|
||||||
add = MaterialIconAction(
|
self.toolbar.components.add_safe(
|
||||||
|
"add",
|
||||||
|
MaterialIconAction(
|
||||||
icon_name="add", tooltip="Add new curve", checkable=False, parent=self
|
icon_name="add", tooltip="Add new curve", checkable=False, parent=self
|
||||||
|
),
|
||||||
)
|
)
|
||||||
expand = MaterialIconAction(
|
self.toolbar.components.add_safe(
|
||||||
|
"expand",
|
||||||
|
MaterialIconAction(
|
||||||
icon_name="unfold_more", tooltip="Expand All DAP", checkable=False, parent=self
|
icon_name="unfold_more", tooltip="Expand All DAP", checkable=False, parent=self
|
||||||
|
),
|
||||||
)
|
)
|
||||||
collapse = MaterialIconAction(
|
self.toolbar.components.add_safe(
|
||||||
|
"collapse",
|
||||||
|
MaterialIconAction(
|
||||||
icon_name="unfold_less", tooltip="Collapse All DAP", checkable=False, parent=self
|
icon_name="unfold_less", tooltip="Collapse All DAP", checkable=False, parent=self
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
bundle = ToolbarBundle("curve_tree", self.toolbar.components)
|
||||||
self.toolbar.add_action("add", add, self)
|
bundle.add_action("add")
|
||||||
self.toolbar.add_action("expand_all", expand, self)
|
bundle.add_action("expand")
|
||||||
self.toolbar.add_action("collapse_all", collapse, self)
|
bundle.add_action("collapse")
|
||||||
|
self.toolbar.add_bundle(bundle)
|
||||||
|
|
||||||
# Add colormap widget (not updating waveform's color_palette until Send is pressed)
|
# Add colormap widget (not updating waveform's color_palette until Send is pressed)
|
||||||
self.spacer = QWidget()
|
spacer = QWidget()
|
||||||
self.spacer.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
|
spacer.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
|
||||||
self.toolbar.addWidget(self.spacer)
|
|
||||||
|
self.toolbar.components.add_safe("spacer", WidgetAction(widget=spacer, adjust_size=False))
|
||||||
|
bundle.add_action("spacer")
|
||||||
|
|
||||||
# Renormalize colors button
|
# Renormalize colors button
|
||||||
renorm_action = MaterialIconAction(
|
self.toolbar.components.add_safe(
|
||||||
|
"renormalize_colors",
|
||||||
|
MaterialIconAction(
|
||||||
icon_name="palette", tooltip="Normalize All Colors", checkable=False, parent=self
|
icon_name="palette", tooltip="Normalize All Colors", checkable=False, parent=self
|
||||||
|
),
|
||||||
)
|
)
|
||||||
self.toolbar.add_action("renormalize_colors", renorm_action, self)
|
bundle.add_action("renormalize_colors")
|
||||||
|
renorm_action = self.toolbar.components.get_action("renormalize_colors")
|
||||||
renorm_action.action.triggered.connect(lambda checked: self.renormalize_colors())
|
renorm_action.action.triggered.connect(lambda checked: self.renormalize_colors())
|
||||||
|
|
||||||
self.colormap_widget = BECColorMapWidget(cmap=self.color_palette or "plasma")
|
self.colormap_widget = BECColorMapWidget(cmap=self.color_palette or "plasma")
|
||||||
self.toolbar.addWidget(self.colormap_widget)
|
self.toolbar.components.add_safe(
|
||||||
|
"colormap_widget", WidgetAction(widget=self.colormap_widget)
|
||||||
|
)
|
||||||
|
bundle.add_action("colormap_widget")
|
||||||
self.colormap_widget.colormap_changed_signal.connect(self.handle_colormap_changed)
|
self.colormap_widget.colormap_changed_signal.connect(self.handle_colormap_changed)
|
||||||
|
|
||||||
|
add = self.toolbar.components.get_action("add")
|
||||||
|
expand = self.toolbar.components.get_action("expand")
|
||||||
|
collapse = self.toolbar.components.get_action("collapse")
|
||||||
add.action.triggered.connect(lambda checked: self.add_new_curve())
|
add.action.triggered.connect(lambda checked: self.add_new_curve())
|
||||||
expand.action.triggered.connect(lambda checked: self.expand_all_daps())
|
expand.action.triggered.connect(lambda checked: self.expand_all_daps())
|
||||||
collapse.action.triggered.connect(lambda checked: self.collapse_all_daps())
|
collapse.action.triggered.connect(lambda checked: self.collapse_all_daps())
|
||||||
|
|
||||||
self.layout.addWidget(self.toolbar)
|
self.layout.addWidget(self.toolbar)
|
||||||
|
|
||||||
|
self.toolbar.show_bundles(["curve_tree"])
|
||||||
|
|
||||||
def _init_tree(self):
|
def _init_tree(self):
|
||||||
"""Initialize the QTreeWidget with 7 columns and compact widths."""
|
"""Initialize the QTreeWidget with 7 columns and compact widths."""
|
||||||
self.tree = QTreeWidget()
|
self.tree = QTreeWidget()
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import json
|
import json
|
||||||
from typing import Any, Literal
|
from typing import Literal
|
||||||
|
|
||||||
import lmfit
|
import lmfit
|
||||||
import numpy as np
|
import numpy as np
|
||||||
@ -29,7 +29,7 @@ from bec_widgets.utils.colors import Colors, set_theme
|
|||||||
from bec_widgets.utils.container_utils import WidgetContainerUtils
|
from bec_widgets.utils.container_utils import WidgetContainerUtils
|
||||||
from bec_widgets.utils.error_popups import SafeProperty, SafeSlot
|
from bec_widgets.utils.error_popups import SafeProperty, SafeSlot
|
||||||
from bec_widgets.utils.settings_dialog import SettingsDialog
|
from bec_widgets.utils.settings_dialog import SettingsDialog
|
||||||
from bec_widgets.utils.toolbar import MaterialIconAction
|
from bec_widgets.utils.toolbars.toolbar import MaterialIconAction
|
||||||
from bec_widgets.widgets.dap.lmfit_dialog.lmfit_dialog import LMFitDialog
|
from bec_widgets.widgets.dap.lmfit_dialog.lmfit_dialog import LMFitDialog
|
||||||
from bec_widgets.widgets.plots.plot_base import PlotBase
|
from bec_widgets.widgets.plots.plot_base import PlotBase
|
||||||
from bec_widgets.widgets.plots.waveform.curve import Curve, CurveConfig, DeviceSignal
|
from bec_widgets.widgets.plots.waveform.curve import Curve, CurveConfig, DeviceSignal
|
||||||
@ -182,6 +182,7 @@ class Waveform(PlotBase):
|
|||||||
self._init_roi_manager()
|
self._init_roi_manager()
|
||||||
self.dap_summary = None
|
self.dap_summary = None
|
||||||
self.dap_summary_dialog = None
|
self.dap_summary_dialog = None
|
||||||
|
self._add_fit_parameters_popup()
|
||||||
self._enable_roi_toolbar_action(False) # default state where are no dap curves
|
self._enable_roi_toolbar_action(False) # default state where are no dap curves
|
||||||
self._init_curve_dialog()
|
self._init_curve_dialog()
|
||||||
self.curve_settings_dialog = None
|
self.curve_settings_dialog = None
|
||||||
@ -214,6 +215,8 @@ class Waveform(PlotBase):
|
|||||||
# To fix the ViewAll action with clipToView activated
|
# To fix the ViewAll action with clipToView activated
|
||||||
self._connect_viewbox_menu_actions()
|
self._connect_viewbox_menu_actions()
|
||||||
|
|
||||||
|
self.toolbar.show_bundles(["plot_export", "mouse_interaction", "roi", "axis_popup"])
|
||||||
|
|
||||||
def _connect_viewbox_menu_actions(self):
|
def _connect_viewbox_menu_actions(self):
|
||||||
"""Connect the viewbox menu action ViewAll to the custom reset_view method."""
|
"""Connect the viewbox menu action ViewAll to the custom reset_view method."""
|
||||||
menu = self.plot_item.vb.menu
|
menu = self.plot_item.vb.menu
|
||||||
@ -247,21 +250,21 @@ class Waveform(PlotBase):
|
|||||||
super().add_side_menus()
|
super().add_side_menus()
|
||||||
self._add_dap_summary_side_menu()
|
self._add_dap_summary_side_menu()
|
||||||
|
|
||||||
def add_popups(self):
|
def _add_fit_parameters_popup(self):
|
||||||
"""
|
"""
|
||||||
Add popups to the Waveform widget.
|
Add popups to the Waveform widget.
|
||||||
"""
|
"""
|
||||||
super().add_popups()
|
self.toolbar.components.add_safe(
|
||||||
LMFitDialog_action = MaterialIconAction(
|
"fit_params",
|
||||||
|
MaterialIconAction(
|
||||||
icon_name="monitoring", tooltip="Open Fit Parameters", checkable=True, parent=self
|
icon_name="monitoring", tooltip="Open Fit Parameters", checkable=True, parent=self
|
||||||
|
),
|
||||||
)
|
)
|
||||||
self.toolbar.add_action_to_bundle(
|
self.toolbar.get_bundle("axis_popup").add_action("fit_params")
|
||||||
bundle_id="popup_bundle",
|
|
||||||
action_id="fit_params",
|
self.toolbar.components.get_action("fit_params").action.triggered.connect(
|
||||||
action=LMFitDialog_action,
|
self.show_dap_summary_popup
|
||||||
target_widget=self,
|
|
||||||
)
|
)
|
||||||
self.toolbar.widgets["fit_params"].action.triggered.connect(self.show_dap_summary_popup)
|
|
||||||
|
|
||||||
@SafeSlot()
|
@SafeSlot()
|
||||||
def _reset_view(self):
|
def _reset_view(self):
|
||||||
@ -290,14 +293,17 @@ class Waveform(PlotBase):
|
|||||||
Initialize the ROI manager for the Waveform widget.
|
Initialize the ROI manager for the Waveform widget.
|
||||||
"""
|
"""
|
||||||
# Add toolbar icon
|
# Add toolbar icon
|
||||||
roi = MaterialIconAction(
|
self.toolbar.components.add_safe(
|
||||||
|
"roi_linear",
|
||||||
|
MaterialIconAction(
|
||||||
icon_name="align_justify_space_between",
|
icon_name="align_justify_space_between",
|
||||||
tooltip="Add ROI region for DAP",
|
tooltip="Add ROI region for DAP",
|
||||||
checkable=True,
|
checkable=True,
|
||||||
|
parent=self,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
self.toolbar.add_action_to_bundle(
|
self.toolbar.get_bundle("roi").add_action("roi_linear")
|
||||||
bundle_id="roi", action_id="roi_linear", action=roi, target_widget=self
|
|
||||||
)
|
|
||||||
self._roi_manager = WaveformROIManager(self.plot_item, parent=self)
|
self._roi_manager = WaveformROIManager(self.plot_item, parent=self)
|
||||||
|
|
||||||
# Connect manager signals -> forward them via Waveform's own signals
|
# Connect manager signals -> forward them via Waveform's own signals
|
||||||
@ -307,23 +313,30 @@ class Waveform(PlotBase):
|
|||||||
# Example: connect ROI changed to re-request DAP
|
# Example: connect ROI changed to re-request DAP
|
||||||
self.roi_changed.connect(self._on_roi_changed_for_dap)
|
self.roi_changed.connect(self._on_roi_changed_for_dap)
|
||||||
self._roi_manager.roi_active.connect(self.request_dap_update)
|
self._roi_manager.roi_active.connect(self.request_dap_update)
|
||||||
self.toolbar.widgets["roi_linear"].action.toggled.connect(self._roi_manager.toggle_roi)
|
self.toolbar.components.get_action("roi_linear").action.toggled.connect(
|
||||||
|
self._roi_manager.toggle_roi
|
||||||
|
)
|
||||||
|
|
||||||
def _init_curve_dialog(self):
|
def _init_curve_dialog(self):
|
||||||
"""
|
"""
|
||||||
Initializes the Curve dialog within the toolbar.
|
Initializes the Curve dialog within the toolbar.
|
||||||
"""
|
"""
|
||||||
curve_settings = MaterialIconAction(
|
self.toolbar.components.add_safe(
|
||||||
icon_name="timeline", tooltip="Show Curve dialog.", checkable=True
|
"curve",
|
||||||
|
MaterialIconAction(
|
||||||
|
icon_name="timeline", tooltip="Show Curve dialog.", checkable=True, parent=self
|
||||||
|
),
|
||||||
|
)
|
||||||
|
self.toolbar.get_bundle("axis_popup").add_action("curve")
|
||||||
|
self.toolbar.components.get_action("curve").action.triggered.connect(
|
||||||
|
self.show_curve_settings_popup
|
||||||
)
|
)
|
||||||
self.toolbar.add_action("curve", curve_settings, target_widget=self)
|
|
||||||
self.toolbar.widgets["curve"].action.triggered.connect(self.show_curve_settings_popup)
|
|
||||||
|
|
||||||
def show_curve_settings_popup(self):
|
def show_curve_settings_popup(self):
|
||||||
"""
|
"""
|
||||||
Displays the curve settings popup to allow users to modify curve-related configurations.
|
Displays the curve settings popup to allow users to modify curve-related configurations.
|
||||||
"""
|
"""
|
||||||
curve_action = self.toolbar.widgets["curve"].action
|
curve_action = self.toolbar.components.get_action("curve").action
|
||||||
|
|
||||||
if self.curve_settings_dialog is None or not self.curve_settings_dialog.isVisible():
|
if self.curve_settings_dialog is None or not self.curve_settings_dialog.isVisible():
|
||||||
curve_setting = CurveSetting(parent=self, target_widget=self)
|
curve_setting = CurveSetting(parent=self, target_widget=self)
|
||||||
@ -347,7 +360,7 @@ class Waveform(PlotBase):
|
|||||||
self.curve_settings_dialog.close()
|
self.curve_settings_dialog.close()
|
||||||
self.curve_settings_dialog.deleteLater()
|
self.curve_settings_dialog.deleteLater()
|
||||||
self.curve_settings_dialog = None
|
self.curve_settings_dialog = None
|
||||||
self.toolbar.widgets["curve"].action.setChecked(False)
|
self.toolbar.components.get_action("curve").action.setChecked(False)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def roi_region(self) -> tuple[float, float] | None:
|
def roi_region(self) -> tuple[float, float] | None:
|
||||||
@ -394,9 +407,9 @@ class Waveform(PlotBase):
|
|||||||
Args:
|
Args:
|
||||||
enable(bool): Enable or disable the ROI toolbar action.
|
enable(bool): Enable or disable the ROI toolbar action.
|
||||||
"""
|
"""
|
||||||
self.toolbar.widgets["roi_linear"].action.setEnabled(enable)
|
self.toolbar.components.get_action("roi_linear").action.setEnabled(enable)
|
||||||
if enable is False:
|
if enable is False:
|
||||||
self.toolbar.widgets["roi_linear"].action.setChecked(False)
|
self.toolbar.components.get_action("roi_linear").action.setChecked(False)
|
||||||
self._roi_manager.toggle_roi(False)
|
self._roi_manager.toggle_roi(False)
|
||||||
|
|
||||||
################################################################################
|
################################################################################
|
||||||
@ -420,7 +433,7 @@ class Waveform(PlotBase):
|
|||||||
"""
|
"""
|
||||||
Show the DAP summary popup.
|
Show the DAP summary popup.
|
||||||
"""
|
"""
|
||||||
fit_action = self.toolbar.widgets["fit_params"].action
|
fit_action = self.toolbar.components.get_action("fit_params").action
|
||||||
if self.dap_summary_dialog is None or not self.dap_summary_dialog.isVisible():
|
if self.dap_summary_dialog is None or not self.dap_summary_dialog.isVisible():
|
||||||
self.dap_summary = LMFitDialog(parent=self)
|
self.dap_summary = LMFitDialog(parent=self)
|
||||||
self.dap_summary_dialog = QDialog(modal=False)
|
self.dap_summary_dialog = QDialog(modal=False)
|
||||||
@ -446,7 +459,7 @@ class Waveform(PlotBase):
|
|||||||
self.dap_summary.deleteLater()
|
self.dap_summary.deleteLater()
|
||||||
self.dap_summary_dialog.deleteLater()
|
self.dap_summary_dialog.deleteLater()
|
||||||
self.dap_summary_dialog = None
|
self.dap_summary_dialog = None
|
||||||
self.toolbar.widgets["fit_params"].action.setChecked(False)
|
self.toolbar.components.get_action("fit_params").action.setChecked(False)
|
||||||
|
|
||||||
def _get_dap_from_target_widget(self) -> None:
|
def _get_dap_from_target_widget(self) -> None:
|
||||||
"""Get the DAP data from the target widget and update the DAP dialog manually on creation."""
|
"""Get the DAP data from the target widget and update the DAP dialog manually on creation."""
|
||||||
|
@ -9,7 +9,9 @@ from qtpy.QtWidgets import QHeaderView, QLabel, QTableWidget, QTableWidgetItem,
|
|||||||
from bec_widgets.utils.bec_connector import ConnectionConfig
|
from bec_widgets.utils.bec_connector import ConnectionConfig
|
||||||
from bec_widgets.utils.bec_widget import BECWidget
|
from bec_widgets.utils.bec_widget import BECWidget
|
||||||
from bec_widgets.utils.compact_popup import CompactPopupWidget
|
from bec_widgets.utils.compact_popup import CompactPopupWidget
|
||||||
from bec_widgets.utils.toolbar import ModularToolBar, SeparatorAction, WidgetAction
|
from bec_widgets.utils.toolbars.actions import WidgetAction
|
||||||
|
from bec_widgets.utils.toolbars.bundles import ToolbarBundle
|
||||||
|
from bec_widgets.utils.toolbars.toolbar import ModularToolBar
|
||||||
from bec_widgets.widgets.control.buttons.button_abort.button_abort import AbortButton
|
from bec_widgets.widgets.control.buttons.button_abort.button_abort import AbortButton
|
||||||
from bec_widgets.widgets.control.buttons.button_reset.button_reset import ResetButton
|
from bec_widgets.widgets.control.buttons.button_reset.button_reset import ResetButton
|
||||||
from bec_widgets.widgets.control.buttons.button_resume.button_resume import ResumeButton
|
from bec_widgets.widgets.control.buttons.button_resume.button_resume import ResumeButton
|
||||||
@ -76,17 +78,26 @@ class BECQueue(BECWidget, CompactPopupWidget):
|
|||||||
"""
|
"""
|
||||||
widget_label = QLabel(text="Live Queue", parent=self)
|
widget_label = QLabel(text="Live Queue", parent=self)
|
||||||
widget_label.setStyleSheet("font-weight: bold;")
|
widget_label.setStyleSheet("font-weight: bold;")
|
||||||
self.toolbar = ModularToolBar(
|
self.toolbar = ModularToolBar(parent=self)
|
||||||
parent=self,
|
self.toolbar.components.add_safe("widget_label", WidgetAction(widget=widget_label))
|
||||||
actions={
|
bundle = ToolbarBundle("queue_label", self.toolbar.components)
|
||||||
"widget_label": WidgetAction(widget=widget_label),
|
bundle.add_action("widget_label")
|
||||||
"separator_1": SeparatorAction(),
|
self.toolbar.add_bundle(bundle)
|
||||||
"resume": WidgetAction(widget=ResumeButton(parent=self, toolbar=False)),
|
|
||||||
"stop": WidgetAction(widget=StopButton(parent=self, toolbar=False)),
|
self.toolbar.add_action(
|
||||||
"reset": WidgetAction(widget=ResetButton(parent=self, toolbar=False)),
|
"resume", WidgetAction(widget=ResumeButton(parent=self, toolbar=True))
|
||||||
},
|
|
||||||
target_widget=self,
|
|
||||||
)
|
)
|
||||||
|
self.toolbar.add_action("stop", WidgetAction(widget=StopButton(parent=self, toolbar=True)))
|
||||||
|
self.toolbar.add_action(
|
||||||
|
"reset", WidgetAction(widget=ResetButton(parent=self, toolbar=True))
|
||||||
|
)
|
||||||
|
|
||||||
|
control_bundle = ToolbarBundle("control", self.toolbar.components)
|
||||||
|
control_bundle.add_action("resume")
|
||||||
|
control_bundle.add_action("stop")
|
||||||
|
control_bundle.add_action("reset")
|
||||||
|
self.toolbar.add_bundle(control_bundle)
|
||||||
|
self.toolbar.show_bundles(["queue_label", "control"])
|
||||||
|
|
||||||
self.addWidget(self.toolbar)
|
self.addWidget(self.toolbar)
|
||||||
|
|
||||||
|
@ -97,13 +97,15 @@ def test_new_dock_raises_for_invalid_name(bec_dock_area):
|
|||||||
# Toolbar Actions
|
# Toolbar Actions
|
||||||
###################################
|
###################################
|
||||||
def test_toolbar_add_plot_waveform(bec_dock_area):
|
def test_toolbar_add_plot_waveform(bec_dock_area):
|
||||||
bec_dock_area.toolbar.widgets["menu_plots"].widgets["waveform"].trigger()
|
bec_dock_area.toolbar.components.get_action("menu_plots").actions["waveform"].action.trigger()
|
||||||
assert "waveform_0" in bec_dock_area.panels
|
assert "waveform_0" in bec_dock_area.panels
|
||||||
assert bec_dock_area.panels["waveform_0"].widgets[0].config.widget_class == "Waveform"
|
assert bec_dock_area.panels["waveform_0"].widgets[0].config.widget_class == "Waveform"
|
||||||
|
|
||||||
|
|
||||||
def test_toolbar_add_plot_scatter_waveform(bec_dock_area):
|
def test_toolbar_add_plot_scatter_waveform(bec_dock_area):
|
||||||
bec_dock_area.toolbar.widgets["menu_plots"].widgets["scatter_waveform"].trigger()
|
bec_dock_area.toolbar.components.get_action("menu_plots").actions[
|
||||||
|
"scatter_waveform"
|
||||||
|
].action.trigger()
|
||||||
assert "scatter_waveform_0" in bec_dock_area.panels
|
assert "scatter_waveform_0" in bec_dock_area.panels
|
||||||
assert (
|
assert (
|
||||||
bec_dock_area.panels["scatter_waveform_0"].widgets[0].config.widget_class
|
bec_dock_area.panels["scatter_waveform_0"].widgets[0].config.widget_class
|
||||||
@ -112,19 +114,22 @@ def test_toolbar_add_plot_scatter_waveform(bec_dock_area):
|
|||||||
|
|
||||||
|
|
||||||
def test_toolbar_add_plot_image(bec_dock_area):
|
def test_toolbar_add_plot_image(bec_dock_area):
|
||||||
bec_dock_area.toolbar.widgets["menu_plots"].widgets["image"].trigger()
|
bec_dock_area.toolbar.components.get_action("menu_plots").actions["image"].action.trigger()
|
||||||
assert "image_0" in bec_dock_area.panels
|
assert "image_0" in bec_dock_area.panels
|
||||||
assert bec_dock_area.panels["image_0"].widgets[0].config.widget_class == "Image"
|
assert bec_dock_area.panels["image_0"].widgets[0].config.widget_class == "Image"
|
||||||
|
|
||||||
|
|
||||||
def test_toolbar_add_plot_motor_map(bec_dock_area):
|
def test_toolbar_add_plot_motor_map(bec_dock_area):
|
||||||
bec_dock_area.toolbar.widgets["menu_plots"].widgets["motor_map"].trigger()
|
bec_dock_area.toolbar.components.get_action("menu_plots").actions["motor_map"].action.trigger()
|
||||||
assert "motor_map_0" in bec_dock_area.panels
|
assert "motor_map_0" in bec_dock_area.panels
|
||||||
assert bec_dock_area.panels["motor_map_0"].widgets[0].config.widget_class == "MotorMap"
|
assert bec_dock_area.panels["motor_map_0"].widgets[0].config.widget_class == "MotorMap"
|
||||||
|
|
||||||
|
|
||||||
def test_toolbar_add_multi_waveform(bec_dock_area):
|
def test_toolbar_add_multi_waveform(bec_dock_area):
|
||||||
bec_dock_area.toolbar.widgets["menu_plots"].widgets["multi_waveform"].trigger()
|
bec_dock_area.toolbar.components.get_action("menu_plots").actions[
|
||||||
|
"multi_waveform"
|
||||||
|
].action.trigger()
|
||||||
|
# Check if the MultiWaveform panel is created
|
||||||
assert "multi_waveform_0" in bec_dock_area.panels
|
assert "multi_waveform_0" in bec_dock_area.panels
|
||||||
assert (
|
assert (
|
||||||
bec_dock_area.panels["multi_waveform_0"].widgets[0].config.widget_class == "MultiWaveform"
|
bec_dock_area.panels["multi_waveform_0"].widgets[0].config.widget_class == "MultiWaveform"
|
||||||
@ -132,7 +137,9 @@ def test_toolbar_add_multi_waveform(bec_dock_area):
|
|||||||
|
|
||||||
|
|
||||||
def test_toolbar_add_device_positioner_box(bec_dock_area):
|
def test_toolbar_add_device_positioner_box(bec_dock_area):
|
||||||
bec_dock_area.toolbar.widgets["menu_devices"].widgets["positioner_box"].trigger()
|
bec_dock_area.toolbar.components.get_action("menu_devices").actions[
|
||||||
|
"positioner_box"
|
||||||
|
].action.trigger()
|
||||||
assert "positioner_box_0" in bec_dock_area.panels
|
assert "positioner_box_0" in bec_dock_area.panels
|
||||||
assert (
|
assert (
|
||||||
bec_dock_area.panels["positioner_box_0"].widgets[0].config.widget_class == "PositionerBox"
|
bec_dock_area.panels["positioner_box_0"].widgets[0].config.widget_class == "PositionerBox"
|
||||||
@ -143,19 +150,21 @@ def test_toolbar_add_utils_queue(bec_dock_area, bec_queue_msg_full):
|
|||||||
bec_dock_area.client.connector.set_and_publish(
|
bec_dock_area.client.connector.set_and_publish(
|
||||||
MessageEndpoints.scan_queue_status(), bec_queue_msg_full
|
MessageEndpoints.scan_queue_status(), bec_queue_msg_full
|
||||||
)
|
)
|
||||||
bec_dock_area.toolbar.widgets["menu_utils"].widgets["queue"].trigger()
|
bec_dock_area.toolbar.components.get_action("menu_utils").actions["queue"].action.trigger()
|
||||||
assert "bec_queue_0" in bec_dock_area.panels
|
assert "bec_queue_0" in bec_dock_area.panels
|
||||||
assert bec_dock_area.panels["bec_queue_0"].widgets[0].config.widget_class == "BECQueue"
|
assert bec_dock_area.panels["bec_queue_0"].widgets[0].config.widget_class == "BECQueue"
|
||||||
|
|
||||||
|
|
||||||
def test_toolbar_add_utils_status(bec_dock_area):
|
def test_toolbar_add_utils_status(bec_dock_area):
|
||||||
bec_dock_area.toolbar.widgets["menu_utils"].widgets["status"].trigger()
|
bec_dock_area.toolbar.components.get_action("menu_utils").actions["status"].action.trigger()
|
||||||
assert "bec_status_box_0" in bec_dock_area.panels
|
assert "bec_status_box_0" in bec_dock_area.panels
|
||||||
assert bec_dock_area.panels["bec_status_box_0"].widgets[0].config.widget_class == "BECStatusBox"
|
assert bec_dock_area.panels["bec_status_box_0"].widgets[0].config.widget_class == "BECStatusBox"
|
||||||
|
|
||||||
|
|
||||||
def test_toolbar_add_utils_progress_bar(bec_dock_area):
|
def test_toolbar_add_utils_progress_bar(bec_dock_area):
|
||||||
bec_dock_area.toolbar.widgets["menu_utils"].widgets["progress_bar"].trigger()
|
bec_dock_area.toolbar.components.get_action("menu_utils").actions[
|
||||||
|
"progress_bar"
|
||||||
|
].action.trigger()
|
||||||
assert "ring_progress_bar_0" in bec_dock_area.panels
|
assert "ring_progress_bar_0" in bec_dock_area.panels
|
||||||
assert (
|
assert (
|
||||||
bec_dock_area.panels["ring_progress_bar_0"].widgets[0].config.widget_class
|
bec_dock_area.panels["ring_progress_bar_0"].widgets[0].config.widget_class
|
||||||
|
@ -157,10 +157,10 @@ def test_curve_tree_init(curve_tree_fixture):
|
|||||||
assert curve_tree.color_palette == "plasma"
|
assert curve_tree.color_palette == "plasma"
|
||||||
assert curve_tree.tree.columnCount() == 7
|
assert curve_tree.tree.columnCount() == 7
|
||||||
|
|
||||||
assert "add" in curve_tree.toolbar.widgets
|
assert curve_tree.toolbar.components.exists("add")
|
||||||
assert "expand_all" in curve_tree.toolbar.widgets
|
assert curve_tree.toolbar.components.exists("expand")
|
||||||
assert "collapse_all" in curve_tree.toolbar.widgets
|
assert curve_tree.toolbar.components.exists("collapse")
|
||||||
assert "renormalize_colors" in curve_tree.toolbar.widgets
|
assert curve_tree.toolbar.components.exists("renormalize_colors")
|
||||||
|
|
||||||
|
|
||||||
def test_add_new_curve(curve_tree_fixture):
|
def test_add_new_curve(curve_tree_fixture):
|
||||||
|
@ -39,9 +39,11 @@ def test_initialization(roi_tree, image_widget):
|
|||||||
assert len(roi_tree.tree.findItems("", Qt.MatchContains)) == 0 # Empty tree initially
|
assert len(roi_tree.tree.findItems("", Qt.MatchContains)) == 0 # Empty tree initially
|
||||||
|
|
||||||
# Check toolbar actions
|
# Check toolbar actions
|
||||||
assert hasattr(roi_tree, "add_rect_action")
|
assert roi_tree.toolbar.components.get_action("roi_rectangle")
|
||||||
assert hasattr(roi_tree, "add_circle_action")
|
assert roi_tree.toolbar.components.get_action("roi_circle")
|
||||||
assert hasattr(roi_tree, "expand_toggle")
|
assert roi_tree.toolbar.components.get_action("roi_ellipse")
|
||||||
|
assert roi_tree.toolbar.components.get_action("expand_toggle")
|
||||||
|
assert roi_tree.toolbar.components.get_action("lock_unlock_all")
|
||||||
|
|
||||||
# Check tree view setup
|
# Check tree view setup
|
||||||
assert roi_tree.tree.columnCount() == 3
|
assert roi_tree.tree.columnCount() == 3
|
||||||
@ -216,23 +218,25 @@ def test_draw_mode_toggle(roi_tree, qtbot):
|
|||||||
assert roi_tree._roi_draw_mode is None
|
assert roi_tree._roi_draw_mode is None
|
||||||
|
|
||||||
# Toggle rect mode on
|
# Toggle rect mode on
|
||||||
roi_tree.add_rect_action.action.toggle()
|
rect_action = roi_tree.toolbar.components.get_action("roi_rectangle").action
|
||||||
|
circle_action = roi_tree.toolbar.components.get_action("roi_circle").action
|
||||||
|
rect_action.toggle()
|
||||||
assert roi_tree._roi_draw_mode == "rect"
|
assert roi_tree._roi_draw_mode == "rect"
|
||||||
assert roi_tree.add_rect_action.action.isChecked()
|
assert rect_action.isChecked()
|
||||||
assert not roi_tree.add_circle_action.action.isChecked()
|
assert not circle_action.isChecked()
|
||||||
|
|
||||||
# Toggle circle mode on (should turn off rect mode)
|
# Toggle circle mode on (should turn off rect mode)
|
||||||
roi_tree.add_circle_action.action.toggle()
|
circle_action.toggle()
|
||||||
qtbot.wait(200)
|
qtbot.wait(200)
|
||||||
assert roi_tree._roi_draw_mode == "circle"
|
assert roi_tree._roi_draw_mode == "circle"
|
||||||
assert not roi_tree.add_rect_action.action.isChecked()
|
assert not rect_action.isChecked()
|
||||||
assert roi_tree.add_circle_action.action.isChecked()
|
assert circle_action.isChecked()
|
||||||
|
|
||||||
# Toggle circle mode off
|
# Toggle circle mode off
|
||||||
roi_tree.add_circle_action.action.toggle()
|
circle_action.toggle()
|
||||||
assert roi_tree._roi_draw_mode is None
|
assert roi_tree._roi_draw_mode is None
|
||||||
assert not roi_tree.add_rect_action.action.isChecked()
|
assert not circle_action.isChecked()
|
||||||
assert not roi_tree.add_circle_action.action.isChecked()
|
assert not rect_action.isChecked()
|
||||||
|
|
||||||
|
|
||||||
def test_add_roi_from_toolbar(qtbot, mocked_client):
|
def test_add_roi_from_toolbar(qtbot, mocked_client):
|
||||||
@ -250,7 +254,7 @@ def test_add_roi_from_toolbar(qtbot, mocked_client):
|
|||||||
|
|
||||||
# Test rectangle ROI creation
|
# Test rectangle ROI creation
|
||||||
# 1. Activate rectangle drawing mode
|
# 1. Activate rectangle drawing mode
|
||||||
roi_tree.add_rect_action.action.setChecked(True)
|
roi_tree.toolbar.components.get_action("roi_rectangle").action.setChecked(True)
|
||||||
assert roi_tree._roi_draw_mode == "rect"
|
assert roi_tree._roi_draw_mode == "rect"
|
||||||
|
|
||||||
# Get plot widget and view
|
# Get plot widget and view
|
||||||
@ -294,8 +298,8 @@ def test_add_roi_from_toolbar(qtbot, mocked_client):
|
|||||||
|
|
||||||
# Test circle ROI creation
|
# Test circle ROI creation
|
||||||
# Reset ROI draw mode
|
# Reset ROI draw mode
|
||||||
roi_tree.add_rect_action.action.setChecked(False)
|
roi_tree.toolbar.components.get_action("roi_rectangle").action.setChecked(False)
|
||||||
roi_tree.add_circle_action.action.setChecked(True)
|
roi_tree.toolbar.components.get_action("roi_circle").action.setChecked(True)
|
||||||
assert roi_tree._roi_draw_mode == "circle"
|
assert roi_tree._roi_draw_mode == "circle"
|
||||||
|
|
||||||
# Define new positions for circle ROI
|
# Define new positions for circle ROI
|
||||||
|
@ -242,10 +242,11 @@ def test_image_data_update_1d(qtbot, mocked_client):
|
|||||||
|
|
||||||
def test_toolbar_actions_presence(qtbot, mocked_client):
|
def test_toolbar_actions_presence(qtbot, mocked_client):
|
||||||
bec_image_view = create_widget(qtbot, Image, client=mocked_client)
|
bec_image_view = create_widget(qtbot, Image, client=mocked_client)
|
||||||
assert "autorange_image" in bec_image_view.toolbar.widgets
|
assert bec_image_view.toolbar.components.exists("image_autorange")
|
||||||
assert "lock_aspect_ratio" in bec_image_view.toolbar.bundles["mouse_interaction"]
|
assert bec_image_view.toolbar.components.exists("lock_aspect_ratio")
|
||||||
assert "processing" in bec_image_view.toolbar.bundles
|
assert bec_image_view.toolbar.components.exists("image_processing_fft")
|
||||||
assert "selection" in bec_image_view.toolbar.bundles
|
assert bec_image_view.toolbar.components.exists("image_device_combo")
|
||||||
|
assert bec_image_view.toolbar.components.exists("image_dim_combo")
|
||||||
|
|
||||||
|
|
||||||
def test_image_processing_fft_toggle(qtbot, mocked_client):
|
def test_image_processing_fft_toggle(qtbot, mocked_client):
|
||||||
@ -304,8 +305,8 @@ def test_setting_vrange_with_colorbar(qtbot, mocked_client, colorbar_type):
|
|||||||
def test_setup_image_from_toolbar(qtbot, mocked_client):
|
def test_setup_image_from_toolbar(qtbot, mocked_client):
|
||||||
bec_image_view = create_widget(qtbot, Image, client=mocked_client)
|
bec_image_view = create_widget(qtbot, Image, client=mocked_client)
|
||||||
|
|
||||||
bec_image_view.selection_bundle.device_combo_box.setCurrentText("eiger")
|
bec_image_view.device_combo_box.setCurrentText("eiger")
|
||||||
bec_image_view.selection_bundle.dim_combo_box.setCurrentText("2d")
|
bec_image_view.dim_combo_box.setCurrentText("2d")
|
||||||
|
|
||||||
assert bec_image_view.monitor == "eiger"
|
assert bec_image_view.monitor == "eiger"
|
||||||
assert bec_image_view.subscriptions["main"].source == "device_monitor_2d"
|
assert bec_image_view.subscriptions["main"].source == "device_monitor_2d"
|
||||||
@ -318,17 +319,17 @@ def test_image_actions_interactions(qtbot, mocked_client):
|
|||||||
bec_image_view = create_widget(qtbot, Image, client=mocked_client)
|
bec_image_view = create_widget(qtbot, Image, client=mocked_client)
|
||||||
bec_image_view.autorange = False # Change the initial state to False
|
bec_image_view.autorange = False # Change the initial state to False
|
||||||
|
|
||||||
bec_image_view.autorange_mean_action.action.trigger()
|
bec_image_view.toolbar.components.get_action("image_autorange_mean").action.trigger()
|
||||||
assert bec_image_view.autorange is True
|
assert bec_image_view.autorange is True
|
||||||
assert bec_image_view.main_image.autorange is True
|
assert bec_image_view.main_image.autorange is True
|
||||||
assert bec_image_view.autorange_mode == "mean"
|
assert bec_image_view.autorange_mode == "mean"
|
||||||
|
|
||||||
bec_image_view.autorange_max_action.action.trigger()
|
bec_image_view.toolbar.components.get_action("image_autorange_max").action.trigger()
|
||||||
assert bec_image_view.autorange is True
|
assert bec_image_view.autorange is True
|
||||||
assert bec_image_view.main_image.autorange is True
|
assert bec_image_view.main_image.autorange is True
|
||||||
assert bec_image_view.autorange_mode == "max"
|
assert bec_image_view.autorange_mode == "max"
|
||||||
|
|
||||||
bec_image_view.toolbar.widgets["lock_aspect_ratio"].action.trigger()
|
bec_image_view.toolbar.components.get_action("lock_aspect_ratio").action.trigger()
|
||||||
assert bec_image_view.lock_aspect_ratio is False
|
assert bec_image_view.lock_aspect_ratio is False
|
||||||
assert bool(bec_image_view.plot_item.getViewBox().state["aspectLocked"]) is False
|
assert bool(bec_image_view.plot_item.getViewBox().state["aspectLocked"]) is False
|
||||||
|
|
||||||
@ -336,7 +337,7 @@ def test_image_actions_interactions(qtbot, mocked_client):
|
|||||||
def test_image_toggle_action_fft(qtbot, mocked_client):
|
def test_image_toggle_action_fft(qtbot, mocked_client):
|
||||||
bec_image_view = create_widget(qtbot, Image, client=mocked_client)
|
bec_image_view = create_widget(qtbot, Image, client=mocked_client)
|
||||||
|
|
||||||
bec_image_view.processing_bundle.fft.action.trigger()
|
bec_image_view.toolbar.components.get_action("image_processing_fft").action.trigger()
|
||||||
|
|
||||||
assert bec_image_view.fft is True
|
assert bec_image_view.fft is True
|
||||||
assert bec_image_view.main_image.fft is True
|
assert bec_image_view.main_image.fft is True
|
||||||
@ -346,7 +347,7 @@ def test_image_toggle_action_fft(qtbot, mocked_client):
|
|||||||
def test_image_toggle_action_log(qtbot, mocked_client):
|
def test_image_toggle_action_log(qtbot, mocked_client):
|
||||||
bec_image_view = create_widget(qtbot, Image, client=mocked_client)
|
bec_image_view = create_widget(qtbot, Image, client=mocked_client)
|
||||||
|
|
||||||
bec_image_view.processing_bundle.log.action.trigger()
|
bec_image_view.toolbar.components.get_action("image_processing_log").action.trigger()
|
||||||
|
|
||||||
assert bec_image_view.log is True
|
assert bec_image_view.log is True
|
||||||
assert bec_image_view.main_image.log is True
|
assert bec_image_view.main_image.log is True
|
||||||
@ -356,7 +357,7 @@ def test_image_toggle_action_log(qtbot, mocked_client):
|
|||||||
def test_image_toggle_action_transpose(qtbot, mocked_client):
|
def test_image_toggle_action_transpose(qtbot, mocked_client):
|
||||||
bec_image_view = create_widget(qtbot, Image, client=mocked_client)
|
bec_image_view = create_widget(qtbot, Image, client=mocked_client)
|
||||||
|
|
||||||
bec_image_view.processing_bundle.transpose.action.trigger()
|
bec_image_view.toolbar.components.get_action("image_processing_transpose").action.trigger()
|
||||||
|
|
||||||
assert bec_image_view.transpose is True
|
assert bec_image_view.transpose is True
|
||||||
assert bec_image_view.main_image.transpose is True
|
assert bec_image_view.main_image.transpose is True
|
||||||
@ -366,7 +367,7 @@ def test_image_toggle_action_transpose(qtbot, mocked_client):
|
|||||||
def test_image_toggle_action_rotate_right(qtbot, mocked_client):
|
def test_image_toggle_action_rotate_right(qtbot, mocked_client):
|
||||||
bec_image_view = create_widget(qtbot, Image, client=mocked_client)
|
bec_image_view = create_widget(qtbot, Image, client=mocked_client)
|
||||||
|
|
||||||
bec_image_view.processing_bundle.right.action.trigger()
|
bec_image_view.toolbar.components.get_action("image_processing_rotate_right").action.trigger()
|
||||||
|
|
||||||
assert bec_image_view.num_rotation_90 == 3
|
assert bec_image_view.num_rotation_90 == 3
|
||||||
assert bec_image_view.main_image.num_rotation_90 == 3
|
assert bec_image_view.main_image.num_rotation_90 == 3
|
||||||
@ -376,7 +377,7 @@ def test_image_toggle_action_rotate_right(qtbot, mocked_client):
|
|||||||
def test_image_toggle_action_rotate_left(qtbot, mocked_client):
|
def test_image_toggle_action_rotate_left(qtbot, mocked_client):
|
||||||
bec_image_view = create_widget(qtbot, Image, client=mocked_client)
|
bec_image_view = create_widget(qtbot, Image, client=mocked_client)
|
||||||
|
|
||||||
bec_image_view.processing_bundle.left.action.trigger()
|
bec_image_view.toolbar.components.get_action("image_processing_rotate_left").action.trigger()
|
||||||
|
|
||||||
assert bec_image_view.num_rotation_90 == 1
|
assert bec_image_view.num_rotation_90 == 1
|
||||||
assert bec_image_view.main_image.num_rotation_90 == 1
|
assert bec_image_view.main_image.num_rotation_90 == 1
|
||||||
@ -392,7 +393,7 @@ def test_image_toggle_action_reset(qtbot, mocked_client):
|
|||||||
bec_image_view.transpose = True
|
bec_image_view.transpose = True
|
||||||
bec_image_view.num_rotation_90 = 2
|
bec_image_view.num_rotation_90 = 2
|
||||||
|
|
||||||
bec_image_view.processing_bundle.reset.action.trigger()
|
bec_image_view.toolbar.components.get_action("image_processing_reset").action.trigger()
|
||||||
|
|
||||||
assert bec_image_view.num_rotation_90 == 0
|
assert bec_image_view.num_rotation_90 == 0
|
||||||
assert bec_image_view.main_image.num_rotation_90 == 0
|
assert bec_image_view.main_image.num_rotation_90 == 0
|
||||||
@ -473,8 +474,8 @@ def test_show_roi_manager_popup(qtbot, mocked_client):
|
|||||||
view = create_widget(qtbot, Image, client=mocked_client, popups=True)
|
view = create_widget(qtbot, Image, client=mocked_client, popups=True)
|
||||||
|
|
||||||
# ROI-manager toggle is exposed via the toolbar.
|
# ROI-manager toggle is exposed via the toolbar.
|
||||||
assert "roi_mgr" in view.toolbar.widgets
|
assert view.toolbar.components.exists("roi_mgr")
|
||||||
roi_action = view.toolbar.widgets["roi_mgr"].action
|
roi_action = view.toolbar.components.get_action("roi_mgr").action
|
||||||
assert roi_action.isChecked() is False, "Should start unchecked"
|
assert roi_action.isChecked() is False, "Should start unchecked"
|
||||||
|
|
||||||
# Open the popup.
|
# Open the popup.
|
||||||
@ -497,10 +498,10 @@ def test_show_roi_manager_popup(qtbot, mocked_client):
|
|||||||
|
|
||||||
def test_crosshair_roi_panels_visibility(qtbot, mocked_client):
|
def test_crosshair_roi_panels_visibility(qtbot, mocked_client):
|
||||||
"""
|
"""
|
||||||
Verify that enabling the ROI‑crosshair shows ROI panels and disabling hides them.
|
Verify that enabling the ROI-crosshair shows ROI panels and disabling hides them.
|
||||||
"""
|
"""
|
||||||
bec_image_view = create_widget(qtbot, Image, client=mocked_client)
|
bec_image_view = create_widget(qtbot, Image, client=mocked_client)
|
||||||
switch = bec_image_view.toolbar.widgets["switch_crosshair"]
|
switch = bec_image_view.toolbar.components.get_action("image_switch_crosshair")
|
||||||
|
|
||||||
# Initially panels should be hidden
|
# Initially panels should be hidden
|
||||||
assert bec_image_view.side_panel_x.panel_height == 0
|
assert bec_image_view.side_panel_x.panel_height == 0
|
||||||
@ -548,7 +549,7 @@ def test_roi_plot_data_from_image(qtbot, mocked_client):
|
|||||||
bec_image_view.on_image_update_2d({"data": test_data}, {})
|
bec_image_view.on_image_update_2d({"data": test_data}, {})
|
||||||
|
|
||||||
# Activate ROI crosshair
|
# Activate ROI crosshair
|
||||||
switch = bec_image_view.toolbar.widgets["switch_crosshair"]
|
switch = bec_image_view.toolbar.components.get_action("image_switch_crosshair")
|
||||||
switch.actions["crosshair_roi"].action.trigger()
|
switch.actions["crosshair_roi"].action.trigger()
|
||||||
qtbot.wait(50)
|
qtbot.wait(50)
|
||||||
|
|
||||||
@ -579,11 +580,10 @@ def test_roi_plot_data_from_image(qtbot, mocked_client):
|
|||||||
def test_monitor_selection_reverse_device_items(qtbot, mocked_client):
|
def test_monitor_selection_reverse_device_items(qtbot, mocked_client):
|
||||||
"""
|
"""
|
||||||
Verify that _reverse_device_items correctly reverses the order of items in the
|
Verify that _reverse_device_items correctly reverses the order of items in the
|
||||||
device combo‑box while preserving the current selection.
|
device combobox while preserving the current selection.
|
||||||
"""
|
"""
|
||||||
view = create_widget(qtbot, Image, client=mocked_client)
|
view = create_widget(qtbot, Image, client=mocked_client)
|
||||||
bundle = view.selection_bundle
|
combo = view.device_combo_box
|
||||||
combo = bundle.device_combo_box
|
|
||||||
|
|
||||||
# Replace existing items with a deterministic list
|
# Replace existing items with a deterministic list
|
||||||
combo.clear()
|
combo.clear()
|
||||||
@ -593,7 +593,7 @@ def test_monitor_selection_reverse_device_items(qtbot, mocked_client):
|
|||||||
combo.setCurrentText("samy")
|
combo.setCurrentText("samy")
|
||||||
|
|
||||||
# Reverse the items
|
# Reverse the items
|
||||||
bundle._reverse_device_items()
|
view._reverse_device_items()
|
||||||
|
|
||||||
# Order should be reversed and selection preserved
|
# Order should be reversed and selection preserved
|
||||||
assert [combo.itemText(i) for i in range(combo.count())] == ["samz", "samy", "samx"]
|
assert [combo.itemText(i) for i in range(combo.count())] == ["samz", "samy", "samx"]
|
||||||
@ -606,7 +606,6 @@ def test_monitor_selection_populate_preview_signals(qtbot, mocked_client, monkey
|
|||||||
with the correct userData.
|
with the correct userData.
|
||||||
"""
|
"""
|
||||||
view = create_widget(qtbot, Image, client=mocked_client)
|
view = create_widget(qtbot, Image, client=mocked_client)
|
||||||
bundle = view.selection_bundle
|
|
||||||
|
|
||||||
# Provide a deterministic fake device_manager with get_bec_signals
|
# Provide a deterministic fake device_manager with get_bec_signals
|
||||||
class _FakeDM:
|
class _FakeDM:
|
||||||
@ -618,27 +617,26 @@ def test_monitor_selection_populate_preview_signals(qtbot, mocked_client, monkey
|
|||||||
|
|
||||||
monkeypatch.setattr(view.client, "device_manager", _FakeDM())
|
monkeypatch.setattr(view.client, "device_manager", _FakeDM())
|
||||||
|
|
||||||
initial_count = bundle.device_combo_box.count()
|
initial_count = view.device_combo_box.count()
|
||||||
|
|
||||||
bundle._populate_preview_signals()
|
view._populate_preview_signals()
|
||||||
|
|
||||||
# Two new entries should have been added
|
# Two new entries should have been added
|
||||||
assert bundle.device_combo_box.count() == initial_count + 2
|
assert view.device_combo_box.count() == initial_count + 2
|
||||||
|
|
||||||
# The first newly added item should carry tuple userData describing the device/signal
|
# The first newly added item should carry tuple userData describing the device/signal
|
||||||
data = bundle.device_combo_box.itemData(initial_count)
|
data = view.device_combo_box.itemData(initial_count)
|
||||||
assert isinstance(data, tuple) and data[0] == "eiger"
|
assert isinstance(data, tuple) and data[0] == "eiger"
|
||||||
|
|
||||||
|
|
||||||
def test_monitor_selection_adjust_and_connect(qtbot, mocked_client, monkeypatch):
|
def test_monitor_selection_adjust_and_connect(qtbot, mocked_client, monkeypatch):
|
||||||
"""
|
"""
|
||||||
Verify that _adjust_and_connect performs the full set‑up:
|
Verify that _adjust_and_connect performs the full set-up:
|
||||||
‑ fills the combo‑box with preview signals,
|
- fills the combobox with preview signals,
|
||||||
‑ reverses their order,
|
- reverses their order,
|
||||||
‑ and resets the currentText to an empty string.
|
- and resets the currentText to an empty string.
|
||||||
"""
|
"""
|
||||||
view = create_widget(qtbot, Image, client=mocked_client)
|
view = create_widget(qtbot, Image, client=mocked_client)
|
||||||
bundle = view.selection_bundle
|
|
||||||
|
|
||||||
# Deterministic fake device_manager
|
# Deterministic fake device_manager
|
||||||
class _FakeDM:
|
class _FakeDM:
|
||||||
@ -647,14 +645,14 @@ def test_monitor_selection_adjust_and_connect(qtbot, mocked_client, monkeypatch)
|
|||||||
|
|
||||||
monkeypatch.setattr(view.client, "device_manager", _FakeDM())
|
monkeypatch.setattr(view.client, "device_manager", _FakeDM())
|
||||||
|
|
||||||
combo = bundle.device_combo_box
|
combo = view.device_combo_box
|
||||||
# Start from a clean state
|
# Start from a clean state
|
||||||
combo.clear()
|
combo.clear()
|
||||||
combo.addItem("", None)
|
combo.addItem("", None)
|
||||||
combo.setCurrentText("")
|
combo.setCurrentText("")
|
||||||
|
|
||||||
# Execute the method under test
|
# Execute the method under test
|
||||||
bundle._adjust_and_connect()
|
view._adjust_and_connect()
|
||||||
|
|
||||||
# Expect exactly two items: preview label followed by the empty default
|
# Expect exactly two items: preview label followed by the empty default
|
||||||
assert combo.count() == 2
|
assert combo.count() == 2
|
||||||
|
@ -5,19 +5,18 @@ from qtpy.QtCore import QPoint, Qt
|
|||||||
from qtpy.QtGui import QContextMenuEvent
|
from qtpy.QtGui import QContextMenuEvent
|
||||||
from qtpy.QtWidgets import QComboBox, QLabel, QMenu, QStyle, QToolButton, QWidget
|
from qtpy.QtWidgets import QComboBox, QLabel, QMenu, QStyle, QToolButton, QWidget
|
||||||
|
|
||||||
from bec_widgets.utils.toolbar import (
|
from bec_widgets.utils.toolbars.actions import (
|
||||||
DeviceSelectionAction,
|
DeviceSelectionAction,
|
||||||
ExpandableMenuAction,
|
ExpandableMenuAction,
|
||||||
IconAction,
|
|
||||||
LongPressToolButton,
|
LongPressToolButton,
|
||||||
MaterialIconAction,
|
MaterialIconAction,
|
||||||
ModularToolBar,
|
|
||||||
QtIconAction,
|
QtIconAction,
|
||||||
SeparatorAction,
|
SeparatorAction,
|
||||||
SwitchableToolBarAction,
|
SwitchableToolBarAction,
|
||||||
ToolbarBundle,
|
|
||||||
WidgetAction,
|
WidgetAction,
|
||||||
)
|
)
|
||||||
|
from bec_widgets.utils.toolbars.bundles import ToolbarBundle
|
||||||
|
from bec_widgets.utils.toolbars.toolbar import ModularToolBar
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
@ -34,14 +33,12 @@ def toolbar_fixture(qtbot, request, dummy_widget):
|
|||||||
"""Parametrized fixture to create a ModularToolBar with different orientations."""
|
"""Parametrized fixture to create a ModularToolBar with different orientations."""
|
||||||
orientation: Literal["horizontal", "vertical"] = request.param
|
orientation: Literal["horizontal", "vertical"] = request.param
|
||||||
toolbar = ModularToolBar(
|
toolbar = ModularToolBar(
|
||||||
target_widget=dummy_widget,
|
|
||||||
orientation=orientation,
|
orientation=orientation,
|
||||||
background_color="rgba(255, 255, 255, 255)", # White background for testing
|
background_color="rgba(255, 255, 255, 255)", # White background for testing
|
||||||
)
|
)
|
||||||
qtbot.addWidget(toolbar)
|
qtbot.addWidget(toolbar)
|
||||||
qtbot.waitExposed(toolbar)
|
qtbot.waitExposed(toolbar)
|
||||||
yield toolbar
|
yield toolbar
|
||||||
toolbar.close()
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
@ -50,12 +47,6 @@ def separator_action():
|
|||||||
return SeparatorAction()
|
return SeparatorAction()
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def icon_action():
|
|
||||||
"""Fixture to create an IconAction."""
|
|
||||||
return IconAction(icon_path="assets/BEC-Icon.png", tooltip="Test Icon Action", checkable=True)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def material_icon_action():
|
def material_icon_action():
|
||||||
"""Fixture to create a MaterialIconAction."""
|
"""Fixture to create a MaterialIconAction."""
|
||||||
@ -64,6 +55,14 @@ def material_icon_action():
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def material_icon_action_2():
|
||||||
|
"""Fixture to create another MaterialIconAction."""
|
||||||
|
return MaterialIconAction(
|
||||||
|
icon_name="home", tooltip="Test Material Icon Action 2", checkable=False
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def qt_icon_action():
|
def qt_icon_action():
|
||||||
"""Fixture to create a QtIconAction."""
|
"""Fixture to create a QtIconAction."""
|
||||||
@ -121,7 +120,7 @@ def test_initialization(toolbar_fixture):
|
|||||||
else:
|
else:
|
||||||
pytest.fail("Toolbar orientation is neither horizontal nor vertical.")
|
pytest.fail("Toolbar orientation is neither horizontal nor vertical.")
|
||||||
assert toolbar.background_color == "rgba(255, 255, 255, 255)"
|
assert toolbar.background_color == "rgba(255, 255, 255, 255)"
|
||||||
assert toolbar.widgets == {}
|
assert len(toolbar.components._components) == 1 # only the separator
|
||||||
assert not toolbar.isMovable()
|
assert not toolbar.isMovable()
|
||||||
assert not toolbar.isFloatable()
|
assert not toolbar.isFloatable()
|
||||||
|
|
||||||
@ -152,80 +151,60 @@ def test_set_orientation(toolbar_fixture, qtbot, dummy_widget):
|
|||||||
assert toolbar.orientation() == Qt.Vertical
|
assert toolbar.orientation() == Qt.Vertical
|
||||||
|
|
||||||
|
|
||||||
def test_add_action(
|
def test_add_action(toolbar_fixture, material_icon_action, qt_icon_action):
|
||||||
toolbar_fixture,
|
"""Test adding different types of actions to the toolbar components."""
|
||||||
icon_action,
|
|
||||||
separator_action,
|
|
||||||
material_icon_action,
|
|
||||||
qt_icon_action,
|
|
||||||
dummy_widget,
|
|
||||||
):
|
|
||||||
"""Test adding different types of actions to the toolbar."""
|
|
||||||
toolbar = toolbar_fixture
|
toolbar = toolbar_fixture
|
||||||
|
|
||||||
# Add IconAction
|
|
||||||
toolbar.add_action("icon_action", icon_action, dummy_widget)
|
|
||||||
assert "icon_action" in toolbar.widgets
|
|
||||||
assert toolbar.widgets["icon_action"] == icon_action
|
|
||||||
assert icon_action.action in toolbar.actions()
|
|
||||||
|
|
||||||
# Add SeparatorAction
|
|
||||||
toolbar.add_action("separator_action", separator_action, dummy_widget)
|
|
||||||
assert "separator_action" in toolbar.widgets
|
|
||||||
assert toolbar.widgets["separator_action"] == separator_action
|
|
||||||
|
|
||||||
# Add MaterialIconAction
|
# Add MaterialIconAction
|
||||||
toolbar.add_action("material_icon_action", material_icon_action, dummy_widget)
|
toolbar.add_action("material_icon_action", material_icon_action)
|
||||||
assert "material_icon_action" in toolbar.widgets
|
assert toolbar.components.exists("material_icon_action")
|
||||||
assert toolbar.widgets["material_icon_action"] == material_icon_action
|
assert toolbar.components.get_action("material_icon_action") == material_icon_action
|
||||||
assert material_icon_action.action in toolbar.actions()
|
|
||||||
|
|
||||||
# Add QtIconAction
|
# Add QtIconAction
|
||||||
toolbar.add_action("qt_icon_action", qt_icon_action, dummy_widget)
|
toolbar.add_action("qt_icon_action", qt_icon_action)
|
||||||
assert "qt_icon_action" in toolbar.widgets
|
assert toolbar.components.exists("qt_icon_action")
|
||||||
assert toolbar.widgets["qt_icon_action"] == qt_icon_action
|
assert toolbar.components.get_action("qt_icon_action") == qt_icon_action
|
||||||
assert qt_icon_action.action in toolbar.actions()
|
|
||||||
|
|
||||||
|
|
||||||
def test_hide_show_action(toolbar_fixture, icon_action, qtbot, dummy_widget):
|
def test_hide_show_action(toolbar_fixture, qt_icon_action, qtbot):
|
||||||
"""Test hiding and showing actions on the toolbar."""
|
"""Test hiding and showing actions on the toolbar."""
|
||||||
toolbar = toolbar_fixture
|
toolbar = toolbar_fixture
|
||||||
|
|
||||||
# Add an action
|
# Add an action
|
||||||
toolbar.add_action("icon_action", icon_action, dummy_widget)
|
toolbar.add_action("icon_action", qt_icon_action)
|
||||||
assert icon_action.action.isVisible()
|
assert qt_icon_action.action.isVisible()
|
||||||
|
|
||||||
# Hide the action
|
# Hide the action
|
||||||
toolbar.hide_action("icon_action")
|
toolbar.hide_action("icon_action")
|
||||||
qtbot.wait(100)
|
qtbot.wait(100)
|
||||||
assert not icon_action.action.isVisible()
|
assert not qt_icon_action.action.isVisible()
|
||||||
|
|
||||||
# Show the action
|
# Show the action
|
||||||
toolbar.show_action("icon_action")
|
toolbar.show_action("icon_action")
|
||||||
qtbot.wait(100)
|
qtbot.wait(100)
|
||||||
assert icon_action.action.isVisible()
|
assert qt_icon_action.action.isVisible()
|
||||||
|
|
||||||
|
|
||||||
def test_add_duplicate_action(toolbar_fixture, icon_action, dummy_widget):
|
def test_add_duplicate_action(toolbar_fixture, qt_icon_action):
|
||||||
"""Test that adding an action with a duplicate action_id raises a ValueError."""
|
"""Test that adding an action with a duplicate action_id raises a ValueError."""
|
||||||
toolbar = toolbar_fixture
|
toolbar = toolbar_fixture
|
||||||
|
|
||||||
# Add an action
|
# Add an action
|
||||||
toolbar.add_action("icon_action", icon_action, dummy_widget)
|
toolbar.add_action("qt_icon_action", qt_icon_action)
|
||||||
assert "icon_action" in toolbar.widgets
|
assert toolbar.components.exists("qt_icon_action")
|
||||||
|
|
||||||
# Attempt to add another action with the same ID
|
# Attempt to add another action with the same ID
|
||||||
with pytest.raises(ValueError) as excinfo:
|
with pytest.raises(ValueError) as excinfo:
|
||||||
toolbar.add_action("icon_action", icon_action, dummy_widget)
|
toolbar.add_action("qt_icon_action", qt_icon_action)
|
||||||
assert "Action with ID 'icon_action' already exists." in str(excinfo.value)
|
assert "Bundle with name 'qt_icon_action' already exists." in str(excinfo.value)
|
||||||
|
|
||||||
|
|
||||||
def test_update_material_icon_colors(toolbar_fixture, material_icon_action, dummy_widget):
|
def test_update_material_icon_colors(toolbar_fixture, material_icon_action):
|
||||||
"""Test updating the color of MaterialIconAction icons."""
|
"""Test updating the color of MaterialIconAction icons."""
|
||||||
toolbar = toolbar_fixture
|
toolbar = toolbar_fixture
|
||||||
|
|
||||||
# Add MaterialIconAction
|
# Add MaterialIconAction
|
||||||
toolbar.add_action("material_icon_action", material_icon_action, dummy_widget)
|
toolbar.add_action("material_icon_action", material_icon_action)
|
||||||
assert material_icon_action.action is not None
|
assert material_icon_action.action is not None
|
||||||
|
|
||||||
# Initial icon
|
# Initial icon
|
||||||
@ -242,11 +221,12 @@ def test_update_material_icon_colors(toolbar_fixture, material_icon_action, dumm
|
|||||||
assert initial_icon != updated_icon
|
assert initial_icon != updated_icon
|
||||||
|
|
||||||
|
|
||||||
def test_device_selection_action(toolbar_fixture, device_selection_action, dummy_widget):
|
def test_device_selection_action(toolbar_fixture, device_selection_action):
|
||||||
"""Test adding a DeviceSelectionAction to the toolbar."""
|
"""Test adding a DeviceSelectionAction to the toolbar."""
|
||||||
toolbar = toolbar_fixture
|
toolbar = toolbar_fixture
|
||||||
toolbar.add_action("device_selection", device_selection_action, dummy_widget)
|
toolbar.add_action("device_selection", device_selection_action)
|
||||||
assert "device_selection" in toolbar.widgets
|
assert toolbar.components.exists("device_selection")
|
||||||
|
toolbar.show_bundles(["device_selection"])
|
||||||
# DeviceSelectionAction adds a QWidget, so it should be present in the toolbar's widgets
|
# DeviceSelectionAction adds a QWidget, so it should be present in the toolbar's widgets
|
||||||
# Check if the widget is added
|
# Check if the widget is added
|
||||||
widget = device_selection_action.device_combobox.parentWidget()
|
widget = device_selection_action.device_combobox.parentWidget()
|
||||||
@ -256,11 +236,12 @@ def test_device_selection_action(toolbar_fixture, device_selection_action, dummy
|
|||||||
assert label.text() == "Select Device:"
|
assert label.text() == "Select Device:"
|
||||||
|
|
||||||
|
|
||||||
def test_widget_action(toolbar_fixture, widget_action, dummy_widget):
|
def test_widget_action(toolbar_fixture, widget_action):
|
||||||
"""Test adding a WidgetAction to the toolbar."""
|
"""Test adding a WidgetAction to the toolbar."""
|
||||||
toolbar = toolbar_fixture
|
toolbar = toolbar_fixture
|
||||||
toolbar.add_action("widget_action", widget_action, dummy_widget)
|
toolbar.add_action("widget_action", widget_action)
|
||||||
assert "widget_action" in toolbar.widgets
|
assert toolbar.components.exists("widget_action")
|
||||||
|
toolbar.show_bundles(["widget_action"])
|
||||||
# WidgetAction adds a QWidget to the toolbar
|
# WidgetAction adds a QWidget to the toolbar
|
||||||
container = widget_action.widget.parentWidget()
|
container = widget_action.widget.parentWidget()
|
||||||
assert container in toolbar.findChildren(QWidget)
|
assert container in toolbar.findChildren(QWidget)
|
||||||
@ -269,11 +250,12 @@ def test_widget_action(toolbar_fixture, widget_action, dummy_widget):
|
|||||||
assert label.text() == "Sample Label:"
|
assert label.text() == "Sample Label:"
|
||||||
|
|
||||||
|
|
||||||
def test_expandable_menu_action(toolbar_fixture, expandable_menu_action, dummy_widget):
|
def test_expandable_menu_action(toolbar_fixture, expandable_menu_action):
|
||||||
"""Test adding an ExpandableMenuAction to the toolbar."""
|
"""Test adding an ExpandableMenuAction to the toolbar."""
|
||||||
toolbar = toolbar_fixture
|
toolbar = toolbar_fixture
|
||||||
toolbar.add_action("expandable_menu", expandable_menu_action, dummy_widget)
|
toolbar.add_action("expandable_menu", expandable_menu_action)
|
||||||
assert "expandable_menu" in toolbar.widgets
|
assert toolbar.components.exists("expandable_menu")
|
||||||
|
toolbar.show_bundles(["expandable_menu"])
|
||||||
# ExpandableMenuAction adds a QToolButton with a QMenu
|
# ExpandableMenuAction adds a QToolButton with a QMenu
|
||||||
# Find the QToolButton
|
# Find the QToolButton
|
||||||
tool_buttons = toolbar.findChildren(QToolButton)
|
tool_buttons = toolbar.findChildren(QToolButton)
|
||||||
@ -300,44 +282,47 @@ def test_update_material_icon_colors_no_material_actions(toolbar_fixture, dummy_
|
|||||||
|
|
||||||
|
|
||||||
def test_hide_action_nonexistent(toolbar_fixture):
|
def test_hide_action_nonexistent(toolbar_fixture):
|
||||||
"""Test hiding an action that does not exist raises a ValueError."""
|
"""Test hiding an action that does not exist raises a KeyError."""
|
||||||
toolbar = toolbar_fixture
|
toolbar = toolbar_fixture
|
||||||
with pytest.raises(ValueError) as excinfo:
|
with pytest.raises(KeyError) as excinfo:
|
||||||
toolbar.hide_action("nonexistent_action")
|
toolbar.hide_action("nonexistent_action")
|
||||||
assert "Action with ID 'nonexistent_action' does not exist." in str(excinfo.value)
|
excinfo.match("Component with name 'nonexistent_action' does not exist.")
|
||||||
|
|
||||||
|
|
||||||
def test_show_action_nonexistent(toolbar_fixture):
|
def test_show_action_nonexistent(toolbar_fixture):
|
||||||
"""Test showing an action that does not exist raises a ValueError."""
|
"""Test showing an action that does not exist raises a KeyError."""
|
||||||
toolbar = toolbar_fixture
|
toolbar = toolbar_fixture
|
||||||
with pytest.raises(ValueError) as excinfo:
|
with pytest.raises(KeyError) as excinfo:
|
||||||
toolbar.show_action("nonexistent_action")
|
toolbar.show_action("nonexistent_action")
|
||||||
assert "Action with ID 'nonexistent_action' does not exist." in str(excinfo.value)
|
excinfo.match("Component with name 'nonexistent_action' does not exist.")
|
||||||
|
|
||||||
|
|
||||||
def test_add_bundle(toolbar_fixture, dummy_widget, icon_action, material_icon_action):
|
def test_add_bundle(toolbar_fixture, material_icon_action):
|
||||||
"""Test adding a bundle of actions to the toolbar."""
|
"""Test adding a bundle of actions to the toolbar."""
|
||||||
toolbar = toolbar_fixture
|
toolbar = toolbar_fixture
|
||||||
bundle = ToolbarBundle(
|
toolbar.add_action("material_icon_in_bundle", material_icon_action)
|
||||||
bundle_id="test_bundle",
|
bundle = ToolbarBundle("test_bundle", toolbar.components)
|
||||||
actions=[
|
bundle.add_action("material_icon_in_bundle")
|
||||||
("icon_action_in_bundle", icon_action),
|
|
||||||
("material_icon_in_bundle", material_icon_action),
|
toolbar.add_bundle(bundle)
|
||||||
],
|
|
||||||
)
|
assert toolbar.get_bundle("test_bundle")
|
||||||
toolbar.add_bundle(bundle, dummy_widget)
|
assert toolbar.components.exists("material_icon_in_bundle")
|
||||||
assert "test_bundle" in toolbar.bundles
|
|
||||||
assert "icon_action_in_bundle" in toolbar.widgets
|
toolbar.show_bundles(["test_bundle"])
|
||||||
assert "material_icon_in_bundle" in toolbar.widgets
|
|
||||||
assert icon_action.action in toolbar.actions()
|
|
||||||
assert material_icon_action.action in toolbar.actions()
|
assert material_icon_action.action in toolbar.actions()
|
||||||
|
|
||||||
|
|
||||||
def test_invalid_orientation(dummy_widget):
|
def test_invalid_orientation():
|
||||||
"""Test that an invalid orientation raises a ValueError."""
|
"""Test that an invalid orientation raises a ValueError."""
|
||||||
toolbar = ModularToolBar(target_widget=dummy_widget, orientation="horizontal")
|
try:
|
||||||
|
toolbar = ModularToolBar(orientation="horizontal")
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
toolbar.set_orientation("diagonal")
|
toolbar.set_orientation("diagonal")
|
||||||
|
finally:
|
||||||
|
toolbar.close()
|
||||||
|
toolbar.deleteLater()
|
||||||
|
|
||||||
|
|
||||||
def test_widget_action_calculate_minimum_width(qtbot):
|
def test_widget_action_calculate_minimum_width(qtbot):
|
||||||
@ -353,24 +338,26 @@ def test_widget_action_calculate_minimum_width(qtbot):
|
|||||||
|
|
||||||
def test_add_action_to_bundle(toolbar_fixture, dummy_widget, material_icon_action):
|
def test_add_action_to_bundle(toolbar_fixture, dummy_widget, material_icon_action):
|
||||||
# Create an initial bundle with one action
|
# Create an initial bundle with one action
|
||||||
bundle = ToolbarBundle(
|
toolbar_fixture.add_action("initial_action", material_icon_action)
|
||||||
bundle_id="test_bundle", actions=[("initial_action", material_icon_action)]
|
bundle = ToolbarBundle("test_bundle", toolbar_fixture.components)
|
||||||
)
|
bundle.add_action("initial_action")
|
||||||
toolbar_fixture.add_bundle(bundle, dummy_widget)
|
toolbar_fixture.add_bundle(bundle)
|
||||||
|
|
||||||
# Create a new action to add to the existing bundle
|
# Create a new action to add to the existing bundle
|
||||||
new_action = MaterialIconAction(
|
new_action = MaterialIconAction(
|
||||||
icon_name="counter_1", tooltip="New Action", checkable=True, parent=dummy_widget
|
icon_name="counter_1", tooltip="New Action", checkable=True, parent=dummy_widget
|
||||||
)
|
)
|
||||||
toolbar_fixture.add_action_to_bundle("test_bundle", "new_action", new_action, dummy_widget)
|
toolbar_fixture.components.add_safe("new_action", new_action)
|
||||||
|
toolbar_fixture.get_bundle("test_bundle").add_action("new_action")
|
||||||
|
|
||||||
|
toolbar_fixture.show_bundles(["test_bundle"])
|
||||||
|
|
||||||
# Verify the new action is registered in the toolbar's widgets
|
# Verify the new action is registered in the toolbar's widgets
|
||||||
assert "new_action" in toolbar_fixture.widgets
|
assert toolbar_fixture.components.exists("new_action")
|
||||||
assert toolbar_fixture.widgets["new_action"] == new_action
|
assert toolbar_fixture.components.get_action("new_action") == new_action
|
||||||
|
|
||||||
# Verify the new action is included in the bundle tracking
|
# Verify the new action is included in the bundle tracking
|
||||||
assert "new_action" in toolbar_fixture.bundles["test_bundle"]
|
assert toolbar_fixture.bundles["test_bundle"].bundle_actions["new_action"]() == new_action
|
||||||
assert toolbar_fixture.bundles["test_bundle"][-1] == "new_action"
|
|
||||||
|
|
||||||
# Verify the new action's QAction is present in the toolbar's action list
|
# Verify the new action's QAction is present in the toolbar's action list
|
||||||
actions_list = toolbar_fixture.actions()
|
actions_list = toolbar_fixture.actions()
|
||||||
@ -384,7 +371,7 @@ def test_add_action_to_bundle(toolbar_fixture, dummy_widget, material_icon_actio
|
|||||||
|
|
||||||
|
|
||||||
def test_context_menu_contains_added_actions(
|
def test_context_menu_contains_added_actions(
|
||||||
toolbar_fixture, icon_action, material_icon_action, dummy_widget, monkeypatch
|
toolbar_fixture, material_icon_action, material_icon_action_2, monkeypatch
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Test that the toolbar's context menu lists all added toolbar actions.
|
Test that the toolbar's context menu lists all added toolbar actions.
|
||||||
@ -392,9 +379,13 @@ def test_context_menu_contains_added_actions(
|
|||||||
toolbar = toolbar_fixture
|
toolbar = toolbar_fixture
|
||||||
|
|
||||||
# Add two different actions
|
# Add two different actions
|
||||||
toolbar.add_action("icon_action", icon_action, dummy_widget)
|
toolbar.components.add_safe("material_icon_action", material_icon_action)
|
||||||
toolbar.add_action("material_icon_action", material_icon_action, dummy_widget)
|
toolbar.components.add_safe("material_icon_action_2", material_icon_action_2)
|
||||||
|
bundle = toolbar.new_bundle("test_bundle")
|
||||||
|
bundle.add_action("material_icon_action")
|
||||||
|
bundle.add_action("material_icon_action_2")
|
||||||
|
|
||||||
|
toolbar.show_bundles(["test_bundle"])
|
||||||
# Mock the QMenu.exec_ method to prevent the context menu from being displayed and block CI pipeline
|
# Mock the QMenu.exec_ method to prevent the context menu from being displayed and block CI pipeline
|
||||||
monkeypatch.setattr(QMenu, "exec_", lambda self, pos=None: None)
|
monkeypatch.setattr(QMenu, "exec_", lambda self, pos=None: None)
|
||||||
event = QContextMenuEvent(QContextMenuEvent.Mouse, QPoint(10, 10))
|
event = QContextMenuEvent(QContextMenuEvent.Mouse, QPoint(10, 10))
|
||||||
@ -404,23 +395,26 @@ def test_context_menu_contains_added_actions(
|
|||||||
assert len(menus) > 0
|
assert len(menus) > 0
|
||||||
menu = menus[-1]
|
menu = menus[-1]
|
||||||
menu_action_texts = [action.text() for action in menu.actions()]
|
menu_action_texts = [action.text() for action in menu.actions()]
|
||||||
assert any(icon_action.tooltip in text or "icon_action" in text for text in menu_action_texts)
|
tooltips = [
|
||||||
assert any(
|
action.action.tooltip
|
||||||
material_icon_action.tooltip in text or "material_icon_action" in text
|
for action in toolbar.components._components.values()
|
||||||
for text in menu_action_texts
|
if not isinstance(action.action, SeparatorAction)
|
||||||
)
|
]
|
||||||
|
menu_actions_tooltips = [
|
||||||
|
action.toolTip() for action in menu.actions() if action.toolTip() != ""
|
||||||
|
]
|
||||||
|
assert menu_action_texts == tooltips
|
||||||
|
|
||||||
|
|
||||||
def test_context_menu_toggle_action_visibility(
|
def test_context_menu_toggle_action_visibility(toolbar_fixture, material_icon_action, monkeypatch):
|
||||||
toolbar_fixture, icon_action, dummy_widget, monkeypatch
|
|
||||||
):
|
|
||||||
"""
|
"""
|
||||||
Test that toggling action visibility works correctly through the toolbar's context menu.
|
Test that toggling action visibility works correctly through the toolbar's context menu.
|
||||||
"""
|
"""
|
||||||
toolbar = toolbar_fixture
|
toolbar = toolbar_fixture
|
||||||
# Add an action
|
# Add an action
|
||||||
toolbar.add_action("icon_action", icon_action, dummy_widget)
|
toolbar.add_action("material_icon_action", material_icon_action)
|
||||||
assert icon_action.action.isVisible()
|
toolbar.show_bundles(["material_icon_action"])
|
||||||
|
assert material_icon_action.action.isVisible()
|
||||||
|
|
||||||
# Manually trigger the context menu event
|
# Manually trigger the context menu event
|
||||||
monkeypatch.setattr(QMenu, "exec_", lambda self, pos=None: None)
|
monkeypatch.setattr(QMenu, "exec_", lambda self, pos=None: None)
|
||||||
@ -433,7 +427,7 @@ def test_context_menu_toggle_action_visibility(
|
|||||||
menu = menus[-1]
|
menu = menus[-1]
|
||||||
|
|
||||||
# Locate the QAction in the menu
|
# Locate the QAction in the menu
|
||||||
matching_actions = [m for m in menu.actions() if m.text() == icon_action.tooltip]
|
matching_actions = [m for m in menu.actions() if m.text() == material_icon_action.tooltip]
|
||||||
assert len(matching_actions) == 1
|
assert len(matching_actions) == 1
|
||||||
action_in_menu = matching_actions[0]
|
action_in_menu = matching_actions[0]
|
||||||
|
|
||||||
@ -441,23 +435,24 @@ def test_context_menu_toggle_action_visibility(
|
|||||||
action_in_menu.setChecked(False)
|
action_in_menu.setChecked(False)
|
||||||
menu.triggered.emit(action_in_menu)
|
menu.triggered.emit(action_in_menu)
|
||||||
# The action on the toolbar should now be hidden
|
# The action on the toolbar should now be hidden
|
||||||
assert not icon_action.action.isVisible()
|
assert not material_icon_action.action.isVisible()
|
||||||
|
|
||||||
# Toggle it on (check)
|
# Toggle it on (check)
|
||||||
action_in_menu.setChecked(True)
|
action_in_menu.setChecked(True)
|
||||||
menu.triggered.emit(action_in_menu)
|
menu.triggered.emit(action_in_menu)
|
||||||
# The action on the toolbar should be visible again
|
# The action on the toolbar should be visible again
|
||||||
assert icon_action.action.isVisible()
|
assert material_icon_action.action.isVisible()
|
||||||
|
|
||||||
|
|
||||||
def test_switchable_toolbar_action_add(toolbar_fixture, dummy_widget, switchable_toolbar_action):
|
def test_switchable_toolbar_action_add(toolbar_fixture, switchable_toolbar_action):
|
||||||
"""Test that a switchable toolbar action can be added to the toolbar correctly."""
|
"""Test that a switchable toolbar action can be added to the toolbar correctly."""
|
||||||
toolbar = toolbar_fixture
|
toolbar = toolbar_fixture
|
||||||
toolbar.add_action("switch_action", switchable_toolbar_action, dummy_widget)
|
toolbar.add_action("switch_action", switchable_toolbar_action)
|
||||||
|
toolbar.show_bundles(["switch_action"])
|
||||||
|
|
||||||
# Verify the action was added correctly
|
# Verify the action was added correctly
|
||||||
assert "switch_action" in toolbar.widgets
|
assert toolbar.components.exists("switch_action")
|
||||||
assert toolbar.widgets["switch_action"] == switchable_toolbar_action
|
assert toolbar.components.get_action("switch_action") == switchable_toolbar_action
|
||||||
|
|
||||||
# Verify the button is present and is the correct type
|
# Verify the button is present and is the correct type
|
||||||
button = switchable_toolbar_action.main_button
|
button = switchable_toolbar_action.main_button
|
||||||
@ -468,11 +463,10 @@ def test_switchable_toolbar_action_add(toolbar_fixture, dummy_widget, switchable
|
|||||||
assert button.toolTip() == "Action 1"
|
assert button.toolTip() == "Action 1"
|
||||||
|
|
||||||
|
|
||||||
def test_switchable_toolbar_action_switching(
|
def test_switchable_toolbar_action_switching(toolbar_fixture, switchable_toolbar_action, qtbot):
|
||||||
toolbar_fixture, dummy_widget, switchable_toolbar_action, qtbot
|
|
||||||
):
|
|
||||||
toolbar = toolbar_fixture
|
toolbar = toolbar_fixture
|
||||||
toolbar.add_action("switch_action", switchable_toolbar_action, dummy_widget)
|
toolbar.add_action("switch_action", switchable_toolbar_action)
|
||||||
|
toolbar.show_bundles(["switch_action"])
|
||||||
# Verify initial state is set to action1
|
# Verify initial state is set to action1
|
||||||
assert switchable_toolbar_action.current_key == "action1"
|
assert switchable_toolbar_action.current_key == "action1"
|
||||||
assert switchable_toolbar_action.main_button.toolTip() == "Action 1"
|
assert switchable_toolbar_action.main_button.toolTip() == "Action 1"
|
||||||
@ -494,9 +488,10 @@ def test_switchable_toolbar_action_switching(
|
|||||||
assert switchable_toolbar_action.main_button.toolTip() == "Action 2"
|
assert switchable_toolbar_action.main_button.toolTip() == "Action 2"
|
||||||
|
|
||||||
|
|
||||||
def test_long_pressbutton(toolbar_fixture, dummy_widget, switchable_toolbar_action, qtbot):
|
def test_long_pressbutton(toolbar_fixture, switchable_toolbar_action, qtbot):
|
||||||
toolbar = toolbar_fixture
|
toolbar = toolbar_fixture
|
||||||
toolbar.add_action("switch_action", switchable_toolbar_action, dummy_widget)
|
toolbar.add_action("switch_action", switchable_toolbar_action)
|
||||||
|
toolbar.show_bundles(["switch_action"])
|
||||||
|
|
||||||
# Verify the button is a LongPressToolButton
|
# Verify the button is a LongPressToolButton
|
||||||
button = switchable_toolbar_action.main_button
|
button = switchable_toolbar_action.main_button
|
||||||
@ -521,92 +516,73 @@ def test_long_pressbutton(toolbar_fixture, dummy_widget, switchable_toolbar_acti
|
|||||||
|
|
||||||
|
|
||||||
# Additional tests for action/bundle removal
|
# Additional tests for action/bundle removal
|
||||||
def test_remove_standalone_action(toolbar_fixture, icon_action, dummy_widget):
|
def test_remove_standalone_action(toolbar_fixture, material_icon_action):
|
||||||
"""
|
"""
|
||||||
Ensure that a standalone action is fully removed and no longer accessible.
|
Ensure that a standalone action is fully removed and no longer accessible.
|
||||||
"""
|
"""
|
||||||
toolbar = toolbar_fixture
|
toolbar = toolbar_fixture
|
||||||
# Add the action and check it is present
|
# Add the action and check it is present
|
||||||
toolbar.add_action("icon_action", icon_action, dummy_widget)
|
toolbar.add_action("icon_action", material_icon_action)
|
||||||
assert "icon_action" in toolbar.widgets
|
|
||||||
assert icon_action.action in toolbar.actions()
|
assert toolbar.components.exists("icon_action")
|
||||||
|
|
||||||
|
toolbar.show_bundles(["icon_action"])
|
||||||
|
assert material_icon_action.action in toolbar.actions()
|
||||||
|
|
||||||
# Now remove it
|
# Now remove it
|
||||||
toolbar.remove_action("icon_action")
|
toolbar.components.remove_action("icon_action")
|
||||||
|
|
||||||
# Action bookkeeping
|
# Action bookkeeping
|
||||||
assert "icon_action" not in toolbar.widgets
|
assert not toolbar.components.exists("icon_action")
|
||||||
# QAction list
|
# QAction list
|
||||||
assert icon_action.action not in toolbar.actions()
|
assert material_icon_action.action not in toolbar.actions()
|
||||||
# Attempting to hide / show it should raise
|
# Attempting to hide / show it should raise
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(KeyError):
|
||||||
toolbar.hide_action("icon_action")
|
toolbar.hide_action("icon_action")
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(KeyError):
|
||||||
toolbar.show_action("icon_action")
|
toolbar.show_action("icon_action")
|
||||||
|
|
||||||
|
|
||||||
def test_remove_action_from_bundle(
|
def test_remove_action_from_bundle(toolbar_fixture, material_icon_action, material_icon_action_2):
|
||||||
toolbar_fixture, dummy_widget, icon_action, material_icon_action
|
|
||||||
):
|
|
||||||
"""
|
"""
|
||||||
Remove a single action that is part of a bundle and verify clean‑up.
|
Remove a single action that is part of a bundle. This should not remove the action
|
||||||
|
from the toolbar's components, but only from the bundle tracking.
|
||||||
"""
|
"""
|
||||||
toolbar = toolbar_fixture
|
toolbar = toolbar_fixture
|
||||||
bundle = ToolbarBundle(
|
bundle = toolbar.new_bundle("test_bundle")
|
||||||
bundle_id="test_bundle",
|
# Add two actions to the bundle
|
||||||
actions=[("icon_action", icon_action), ("material_action", material_icon_action)],
|
toolbar.components.add_safe("material_action", material_icon_action)
|
||||||
)
|
toolbar.components.add_safe("material_action_2", material_icon_action_2)
|
||||||
toolbar.add_bundle(bundle, dummy_widget)
|
bundle.add_action("material_action")
|
||||||
|
bundle.add_action("material_action_2")
|
||||||
|
|
||||||
|
toolbar.show_bundles(["test_bundle"])
|
||||||
|
|
||||||
# Initial assertions
|
# Initial assertions
|
||||||
assert "test_bundle" in toolbar.bundles
|
assert "test_bundle" in toolbar.bundles
|
||||||
assert "icon_action" in toolbar.widgets
|
assert toolbar.components.exists("material_action")
|
||||||
assert "material_action" in toolbar.widgets
|
assert toolbar.components.exists("material_action_2")
|
||||||
|
|
||||||
# Remove one action from the bundle
|
# Remove one action from the bundle
|
||||||
toolbar.remove_action("icon_action")
|
toolbar.get_bundle("test_bundle").remove_action("material_action")
|
||||||
|
|
||||||
# icon_action should be fully gone
|
# The bundle should still exist
|
||||||
assert "icon_action" not in toolbar.widgets
|
assert "test_bundle" in toolbar.bundles
|
||||||
assert icon_action.action not in toolbar.actions()
|
# The removed action should still exist in the components
|
||||||
# Bundle tracking should be updated
|
assert toolbar.components.exists("material_action")
|
||||||
assert "icon_action" not in toolbar.bundles["test_bundle"]
|
|
||||||
# The other action must still exist
|
# The removed action should not be in the bundle anymore
|
||||||
assert "material_action" in toolbar.widgets
|
assert "material_action" not in toolbar.bundles["test_bundle"].bundle_actions
|
||||||
assert material_icon_action.action in toolbar.actions()
|
|
||||||
|
|
||||||
|
|
||||||
def test_remove_last_action_from_bundle_removes_bundle(toolbar_fixture, dummy_widget, icon_action):
|
def test_remove_entire_bundle(toolbar_fixture, material_icon_action, material_icon_action_2):
|
||||||
"""
|
|
||||||
Removing the final action from a bundle should delete the bundle entry itself.
|
|
||||||
"""
|
|
||||||
toolbar = toolbar_fixture
|
toolbar = toolbar_fixture
|
||||||
bundle = ToolbarBundle(bundle_id="single_action_bundle", actions=[("only_action", icon_action)])
|
toolbar.components.add_safe("material_action", material_icon_action)
|
||||||
toolbar.add_bundle(bundle, dummy_widget)
|
toolbar.components.add_safe("material_action_2", material_icon_action_2)
|
||||||
|
# Create a bundle with two actions
|
||||||
# Sanity check
|
bundle = toolbar.new_bundle("to_remove")
|
||||||
assert "single_action_bundle" in toolbar.bundles
|
bundle.add_action("material_action")
|
||||||
assert "only_action" in toolbar.widgets
|
bundle.add_action("material_action_2")
|
||||||
|
|
||||||
# Remove the sole action
|
|
||||||
toolbar.remove_action("only_action")
|
|
||||||
|
|
||||||
# Bundle should be gone
|
|
||||||
assert "single_action_bundle" not in toolbar.bundles
|
|
||||||
# QAction removed
|
|
||||||
assert icon_action.action not in toolbar.actions()
|
|
||||||
|
|
||||||
|
|
||||||
def test_remove_entire_bundle(toolbar_fixture, dummy_widget, icon_action, material_icon_action):
|
|
||||||
"""
|
|
||||||
Ensure that removing a bundle deletes all its actions and separators.
|
|
||||||
"""
|
|
||||||
toolbar = toolbar_fixture
|
|
||||||
bundle = ToolbarBundle(
|
|
||||||
bundle_id="to_remove",
|
|
||||||
actions=[("icon_action", icon_action), ("material_action", material_icon_action)],
|
|
||||||
)
|
|
||||||
toolbar.add_bundle(bundle, dummy_widget)
|
|
||||||
|
|
||||||
# Confirm bundle presence
|
# Confirm bundle presence
|
||||||
assert "to_remove" in toolbar.bundles
|
assert "to_remove" in toolbar.bundles
|
||||||
@ -616,58 +592,23 @@ def test_remove_entire_bundle(toolbar_fixture, dummy_widget, icon_action, materi
|
|||||||
|
|
||||||
# Bundle mapping gone
|
# Bundle mapping gone
|
||||||
assert "to_remove" not in toolbar.bundles
|
assert "to_remove" not in toolbar.bundles
|
||||||
# All actions gone
|
|
||||||
for aid, act in [("icon_action", icon_action), ("material_action", material_icon_action)]:
|
|
||||||
assert aid not in toolbar.widgets
|
|
||||||
assert act.action not in toolbar.actions()
|
|
||||||
|
|
||||||
|
|
||||||
def test_trigger_removed_action_raises(toolbar_fixture, icon_action, dummy_widget, qtbot):
|
|
||||||
"""
|
|
||||||
Add an action, connect a mock slot, then remove the action and verify that
|
|
||||||
attempting to trigger it afterwards raises RuntimeError (since the underlying
|
|
||||||
QAction has been deleted).
|
|
||||||
"""
|
|
||||||
toolbar = toolbar_fixture
|
|
||||||
|
|
||||||
# Add the action and connect a mock slot
|
|
||||||
toolbar.add_action("icon_action", icon_action, dummy_widget)
|
|
||||||
called = []
|
|
||||||
|
|
||||||
def mock_slot():
|
|
||||||
called.append(True)
|
|
||||||
|
|
||||||
icon_action.action.triggered.connect(mock_slot)
|
|
||||||
|
|
||||||
# Trigger once to confirm connection works
|
|
||||||
icon_action.action.trigger()
|
|
||||||
assert called == [True]
|
|
||||||
|
|
||||||
# Now remove the action
|
|
||||||
toolbar.remove_action("icon_action")
|
|
||||||
# Allow deleteLater event to process
|
|
||||||
qtbot.wait(50)
|
|
||||||
|
|
||||||
# The underlying C++ object should be deleted; triggering should raise
|
|
||||||
with pytest.raises(RuntimeError):
|
|
||||||
icon_action.action.trigger()
|
|
||||||
|
|
||||||
|
|
||||||
def test_remove_nonexistent_action(toolbar_fixture):
|
def test_remove_nonexistent_action(toolbar_fixture):
|
||||||
"""
|
"""
|
||||||
Attempting to remove an action that does not exist should raise ValueError.
|
Attempting to remove an action that does not exist should raise KeyError.
|
||||||
"""
|
"""
|
||||||
toolbar = toolbar_fixture
|
toolbar = toolbar_fixture
|
||||||
with pytest.raises(ValueError) as excinfo:
|
with pytest.raises(KeyError) as excinfo:
|
||||||
toolbar.remove_action("nonexistent_action")
|
toolbar.components.remove_action("nonexistent_action")
|
||||||
assert "Action with ID 'nonexistent_action' does not exist." in str(excinfo.value)
|
assert "Action with ID 'nonexistent_action' does not exist." in str(excinfo.value)
|
||||||
|
|
||||||
|
|
||||||
def test_remove_nonexistent_bundle(toolbar_fixture):
|
def test_remove_nonexistent_bundle(toolbar_fixture):
|
||||||
"""
|
"""
|
||||||
Attempting to remove a bundle that does not exist should raise ValueError.
|
Attempting to remove a bundle that does not exist should raise KeyError.
|
||||||
"""
|
"""
|
||||||
toolbar = toolbar_fixture
|
toolbar = toolbar_fixture
|
||||||
with pytest.raises(ValueError) as excinfo:
|
with pytest.raises(KeyError) as excinfo:
|
||||||
toolbar.remove_bundle("nonexistent_bundle")
|
toolbar.remove_bundle("nonexistent_bundle")
|
||||||
assert "Bundle 'nonexistent_bundle' does not exist." in str(excinfo.value)
|
excinfo.match("Bundle with name 'nonexistent_bundle' does not exist.")
|
||||||
|
@ -272,16 +272,15 @@ def test_motor_map_toolbar_selection(qtbot, mocked_client):
|
|||||||
mm = create_widget(qtbot, MotorMap, client=mocked_client)
|
mm = create_widget(qtbot, MotorMap, client=mocked_client)
|
||||||
|
|
||||||
# Verify toolbar bundle was created during initialization
|
# Verify toolbar bundle was created during initialization
|
||||||
assert hasattr(mm, "motor_selection_bundle")
|
motor_selection = mm.toolbar.components.get_action("motor_selection")
|
||||||
assert mm.motor_selection_bundle is not None
|
|
||||||
|
|
||||||
mm.motor_selection_bundle.motor_x.setCurrentText("samx")
|
motor_selection.motor_x.setCurrentText("samx")
|
||||||
mm.motor_selection_bundle.motor_y.setCurrentText("samy")
|
motor_selection.motor_y.setCurrentText("samy")
|
||||||
|
|
||||||
assert mm.config.x_motor.name == "samx"
|
assert mm.config.x_motor.name == "samx"
|
||||||
assert mm.config.y_motor.name == "samy"
|
assert mm.config.y_motor.name == "samy"
|
||||||
|
|
||||||
mm.motor_selection_bundle.motor_y.setCurrentText("samz")
|
motor_selection.motor_y.setCurrentText("samz")
|
||||||
|
|
||||||
assert mm.config.x_motor.name == "samx"
|
assert mm.config.x_motor.name == "samx"
|
||||||
assert mm.config.y_motor.name == "samz"
|
assert mm.config.y_motor.name == "samz"
|
||||||
@ -291,9 +290,9 @@ def test_motor_map_settings_dialog(qtbot, mocked_client):
|
|||||||
"""Test the settings dialog for the motor map."""
|
"""Test the settings dialog for the motor map."""
|
||||||
mm = create_widget(qtbot, MotorMap, client=mocked_client, popups=True)
|
mm = create_widget(qtbot, MotorMap, client=mocked_client, popups=True)
|
||||||
|
|
||||||
assert "popup_bundle" in mm.toolbar.bundles
|
assert "axis_popup" in mm.toolbar.bundles
|
||||||
for action_id in mm.toolbar.bundles["popup_bundle"]:
|
for action_ref in mm.toolbar.bundles["axis_popup"].bundle_actions.values():
|
||||||
assert mm.toolbar.widgets[action_id].action.isVisible() is True
|
assert action_ref().action.isVisible()
|
||||||
|
|
||||||
# set properties to be fetched by dialog
|
# set properties to be fetched by dialog
|
||||||
mm.map(x_name="samx", y_name="samy")
|
mm.map(x_name="samx", y_name="samy")
|
||||||
|
@ -244,15 +244,14 @@ def test_selection_toolbar_updates_widget(qtbot, mocked_client):
|
|||||||
updates the widget properties.
|
updates the widget properties.
|
||||||
"""
|
"""
|
||||||
mw = create_widget(qtbot, MultiWaveform, client=mocked_client)
|
mw = create_widget(qtbot, MultiWaveform, client=mocked_client)
|
||||||
toolbar = mw.monitor_selection_bundle
|
monitor_selection_action = mw.toolbar.components.get_action("monitor_selection")
|
||||||
monitor_combo = toolbar.monitor
|
cmap_action = mw.toolbar.components.get_action("color_map")
|
||||||
colormap_widget = toolbar.colormap_widget
|
|
||||||
|
|
||||||
monitor_combo.addItem("waveform1d")
|
monitor_selection_action.combobox.addItem("waveform1d")
|
||||||
monitor_combo.setCurrentText("waveform1d")
|
monitor_selection_action.combobox.setCurrentText("waveform1d")
|
||||||
assert mw.monitor == "waveform1d"
|
assert mw.monitor == "waveform1d"
|
||||||
|
|
||||||
colormap_widget.colormap = "viridis"
|
cmap_action.widget.colormap = "viridis"
|
||||||
assert mw.color_palette == "viridis"
|
assert mw.color_palette == "viridis"
|
||||||
|
|
||||||
|
|
||||||
@ -290,11 +289,10 @@ def test_control_panel_opacity_slider_spinbox(qtbot, mocked_client):
|
|||||||
def test_control_panel_highlight_slider_spinbox(qtbot, mocked_client):
|
def test_control_panel_highlight_slider_spinbox(qtbot, mocked_client):
|
||||||
"""
|
"""
|
||||||
Test that the slider and spinbox for curve highlighting update
|
Test that the slider and spinbox for curve highlighting update
|
||||||
the widget’s highlighted_index property, and are disabled if
|
the widget's highlighted_index property, and are disabled if
|
||||||
highlight_last_curve is True.
|
highlight_last_curve is True.
|
||||||
"""
|
"""
|
||||||
mw = create_widget(qtbot, MultiWaveform, client=mocked_client)
|
mw = create_widget(qtbot, MultiWaveform, client=mocked_client)
|
||||||
|
|
||||||
slider_index = mw.controls.ui.highlighted_index
|
slider_index = mw.controls.ui.highlighted_index
|
||||||
spinbox_index = mw.controls.ui.spinbox_index
|
spinbox_index = mw.controls.ui.spinbox_index
|
||||||
checkbox_highlight_last = mw.controls.ui.highlight_last_curve
|
checkbox_highlight_last = mw.controls.ui.highlight_last_curve
|
||||||
|
@ -265,54 +265,56 @@ def test_ui_mode_popup(qtbot, mocked_client):
|
|||||||
pb = create_widget(qtbot, PlotBase, client=mocked_client)
|
pb = create_widget(qtbot, PlotBase, client=mocked_client)
|
||||||
pb.ui_mode = UIMode.POPUP
|
pb.ui_mode = UIMode.POPUP
|
||||||
# The popup bundle should be created and its actions made visible.
|
# The popup bundle should be created and its actions made visible.
|
||||||
assert "popup_bundle" in pb.toolbar.bundles
|
assert "axis_popup" in pb.toolbar.bundles
|
||||||
for action_id in pb.toolbar.bundles["popup_bundle"]:
|
for action_ref in pb.toolbar.bundles["axis_popup"].bundle_actions.values():
|
||||||
assert pb.toolbar.widgets[action_id].action.isVisible() is True
|
assert action_ref().action.isVisible() is True
|
||||||
# The side panel should be hidden.
|
# The side panel should be hidden.
|
||||||
assert not pb.side_panel.isVisible()
|
assert not pb.side_panel.isVisible()
|
||||||
|
|
||||||
|
|
||||||
def test_ui_mode_side(qtbot, mocked_client):
|
# Side panels are not properly implemented yet. Once the logic is fixed, we can re-enable this test.
|
||||||
"""
|
# See issue #742
|
||||||
Test that setting ui_mode to SIDE shows the side panel and ensures any popup actions
|
# def test_ui_mode_side(qtbot, mocked_client):
|
||||||
are hidden.
|
# """
|
||||||
"""
|
# Test that setting ui_mode to SIDE shows the side panel and ensures any popup actions
|
||||||
pb = create_widget(qtbot, PlotBase, client=mocked_client)
|
# are hidden.
|
||||||
pb.ui_mode = UIMode.SIDE
|
# """
|
||||||
# If a popup bundle exists, its actions should be hidden.
|
# pb = create_widget(qtbot, PlotBase, client=mocked_client)
|
||||||
if "popup_bundle" in pb.toolbar.bundles:
|
# pb.ui_mode = UIMode.SIDE
|
||||||
for action_id in pb.toolbar.bundles["popup_bundle"]:
|
# # If a popup bundle exists, its actions should be hidden.
|
||||||
assert pb.toolbar.widgets[action_id].action.isVisible() is False
|
# if "axis_popup" in pb.toolbar.bundles:
|
||||||
|
# for action_ref in pb.toolbar.bundles["axis_popup"].bundle_actions.values():
|
||||||
|
# assert action_ref().action.isVisible() is False
|
||||||
|
|
||||||
|
|
||||||
def test_enable_popups_property(qtbot, mocked_client):
|
# def test_enable_popups_property(qtbot, mocked_client):
|
||||||
"""
|
# """
|
||||||
Test the enable_popups property: when enabled, ui_mode should be POPUP,
|
# Test the enable_popups property: when enabled, ui_mode should be POPUP,
|
||||||
and when disabled, ui_mode should change to NONE.
|
# and when disabled, ui_mode should change to NONE.
|
||||||
"""
|
# """
|
||||||
pb = create_widget(qtbot, PlotBase, client=mocked_client)
|
# pb = create_widget(qtbot, PlotBase, client=mocked_client)
|
||||||
pb.enable_popups = True
|
# pb.enable_popups = True
|
||||||
assert pb.ui_mode == UIMode.POPUP
|
# assert pb.ui_mode == UIMode.POPUP
|
||||||
# The popup bundle actions should be visible.
|
# # The popup bundle actions should be visible.
|
||||||
assert "popup_bundle" in pb.toolbar.bundles
|
# assert "popup_bundle" in pb.toolbar.bundles
|
||||||
for action_id in pb.toolbar.bundles["popup_bundle"]:
|
# for action_id in pb.toolbar.bundles["popup_bundle"]:
|
||||||
assert pb.toolbar.widgets[action_id].action.isVisible() is True
|
# assert pb.toolbar.widgets[action_id].action.isVisible() is True
|
||||||
|
|
||||||
pb.enable_popups = False
|
# pb.enable_popups = False
|
||||||
assert pb.ui_mode == UIMode.NONE
|
# assert pb.ui_mode == UIMode.NONE
|
||||||
|
|
||||||
|
|
||||||
def test_enable_side_panel_property(qtbot, mocked_client):
|
# def test_enable_side_panel_property(qtbot, mocked_client):
|
||||||
"""
|
# """
|
||||||
Test the enable_side_panel property: when enabled, ui_mode should be SIDE,
|
# Test the enable_side_panel property: when enabled, ui_mode should be SIDE,
|
||||||
and when disabled, ui_mode should change to NONE.
|
# and when disabled, ui_mode should change to NONE.
|
||||||
"""
|
# """
|
||||||
pb = create_widget(qtbot, PlotBase, client=mocked_client)
|
# pb = create_widget(qtbot, PlotBase, client=mocked_client)
|
||||||
pb.enable_side_panel = True
|
# pb.enable_side_panel = True
|
||||||
assert pb.ui_mode == UIMode.SIDE
|
# assert pb.ui_mode == UIMode.SIDE
|
||||||
|
|
||||||
pb.enable_side_panel = False
|
# pb.enable_side_panel = False
|
||||||
assert pb.ui_mode == UIMode.NONE
|
# assert pb.ui_mode == UIMode.NONE
|
||||||
|
|
||||||
|
|
||||||
def test_switching_between_popup_and_side_panel_closes_dialog(qtbot, mocked_client):
|
def test_switching_between_popup_and_side_panel_closes_dialog(qtbot, mocked_client):
|
||||||
@ -323,18 +325,19 @@ def test_switching_between_popup_and_side_panel_closes_dialog(qtbot, mocked_clie
|
|||||||
pb = create_widget(qtbot, PlotBase, client=mocked_client)
|
pb = create_widget(qtbot, PlotBase, client=mocked_client)
|
||||||
pb.ui_mode = UIMode.POPUP
|
pb.ui_mode = UIMode.POPUP
|
||||||
# Open the axis settings popup.
|
# Open the axis settings popup.
|
||||||
pb.show_axis_settings_popup()
|
pb_connection = pb.toolbar.bundles["axis_popup"].get_connection("plot_base")
|
||||||
|
pb_connection.show_axis_settings_popup()
|
||||||
qtbot.wait(100)
|
qtbot.wait(100)
|
||||||
# The dialog should now exist and be visible.
|
# The dialog should now exist and be visible.
|
||||||
assert pb.axis_settings_dialog is not None
|
assert pb_connection.axis_settings_dialog is not None
|
||||||
assert pb.axis_settings_dialog.isVisible() is True
|
assert pb_connection.axis_settings_dialog.isVisible() is True
|
||||||
|
|
||||||
# Switch to side panel mode.
|
# Switch to side panel mode.
|
||||||
pb.ui_mode = UIMode.SIDE
|
pb.ui_mode = UIMode.SIDE
|
||||||
qtbot.wait(100)
|
qtbot.wait(100)
|
||||||
# The axis settings dialog should be closed (and reference cleared).
|
# The axis settings dialog should be closed (and reference cleared).
|
||||||
|
|
||||||
qtbot.waitUntil(lambda: pb.axis_settings_dialog is None, timeout=5000)
|
qtbot.waitUntil(lambda: pb_connection.axis_settings_dialog is None, timeout=5000)
|
||||||
|
|
||||||
|
|
||||||
def test_enable_fps_monitor_property(qtbot, mocked_client):
|
def test_enable_fps_monitor_property(qtbot, mocked_client):
|
||||||
|
@ -136,7 +136,7 @@ def test_add_menu(side_panel_fixture, menu_widget, qtbot):
|
|||||||
|
|
||||||
assert panel.stack_widget.count() == initial_count + 1
|
assert panel.stack_widget.count() == initial_count + 1
|
||||||
# Verify the action is added to the toolbar
|
# Verify the action is added to the toolbar
|
||||||
action = panel.toolbar.widgets.get("test_action")
|
action = panel.toolbar.components.get_action("test_action")
|
||||||
assert action is not None
|
assert action is not None
|
||||||
assert action.tooltip == "Test Tooltip"
|
assert action.tooltip == "Test Tooltip"
|
||||||
assert action.action in panel.toolbar.actions()
|
assert action.action in panel.toolbar.actions()
|
||||||
@ -155,7 +155,7 @@ def test_toggle_action_show_panel(side_panel_fixture, menu_widget, qtbot):
|
|||||||
)
|
)
|
||||||
qtbot.wait(100)
|
qtbot.wait(100)
|
||||||
|
|
||||||
action = panel.toolbar.widgets.get("toggle_action")
|
action = panel.toolbar.components.get_action("toggle_action")
|
||||||
assert action is not None
|
assert action is not None
|
||||||
|
|
||||||
# Initially, panel should be hidden
|
# Initially, panel should be hidden
|
||||||
@ -199,8 +199,8 @@ def test_switch_actions(side_panel_fixture, menu_widget, qtbot):
|
|||||||
)
|
)
|
||||||
qtbot.wait(100)
|
qtbot.wait(100)
|
||||||
|
|
||||||
action1 = panel.toolbar.widgets.get("action1")
|
action1 = panel.toolbar.components.get_action("action1")
|
||||||
action2 = panel.toolbar.widgets.get("action2")
|
action2 = panel.toolbar.components.get_action("action2")
|
||||||
assert action1 is not None
|
assert action1 is not None
|
||||||
assert action2 is not None
|
assert action2 is not None
|
||||||
|
|
||||||
@ -241,7 +241,7 @@ def test_multiple_add_menu(side_panel_fixture, menu_widget, qtbot):
|
|||||||
)
|
)
|
||||||
qtbot.wait(100)
|
qtbot.wait(100)
|
||||||
assert panel.stack_widget.count() == initial_count + i + 1
|
assert panel.stack_widget.count() == initial_count + i + 1
|
||||||
action = panel.toolbar.widgets.get(f"action{i}")
|
action = panel.toolbar.components.get_action(f"action{i}")
|
||||||
assert action is not None
|
assert action is not None
|
||||||
assert action.tooltip == f"Tooltip{i}"
|
assert action.tooltip == f"Tooltip{i}"
|
||||||
assert action.action in panel.toolbar.actions()
|
assert action.action in panel.toolbar.actions()
|
||||||
@ -360,7 +360,7 @@ def test_add_multiple_menus(side_panel_fixture, menu_widget, qtbot):
|
|||||||
)
|
)
|
||||||
qtbot.wait(100)
|
qtbot.wait(100)
|
||||||
assert panel.stack_widget.count() == initial_count + i + 1
|
assert panel.stack_widget.count() == initial_count + i + 1
|
||||||
action = panel.toolbar.widgets.get(f"action{i}")
|
action = panel.toolbar.components.get_action(f"action{i}")
|
||||||
assert action is not None
|
assert action is not None
|
||||||
assert action.tooltip == f"Tooltip{i}"
|
assert action.tooltip == f"Tooltip{i}"
|
||||||
assert action.action in panel.toolbar.actions()
|
assert action.action in panel.toolbar.actions()
|
||||||
|
@ -797,7 +797,7 @@ def test_show_curve_settings_popup(qtbot, mocked_client):
|
|||||||
"""
|
"""
|
||||||
wf = create_widget(qtbot, Waveform, client=mocked_client)
|
wf = create_widget(qtbot, Waveform, client=mocked_client)
|
||||||
|
|
||||||
curve_action = wf.toolbar.widgets["curve"].action
|
curve_action = wf.toolbar.components.get_action("curve").action
|
||||||
assert not curve_action.isChecked(), "Should start unchecked"
|
assert not curve_action.isChecked(), "Should start unchecked"
|
||||||
|
|
||||||
wf.show_curve_settings_popup()
|
wf.show_curve_settings_popup()
|
||||||
@ -807,8 +807,9 @@ def test_show_curve_settings_popup(qtbot, mocked_client):
|
|||||||
assert curve_action.isChecked()
|
assert curve_action.isChecked()
|
||||||
|
|
||||||
# add a new row to the curve tree
|
# add a new row to the curve tree
|
||||||
wf.curve_settings_dialog.widget.curve_manager.toolbar.widgets["add"].action.trigger()
|
add_action = wf.curve_settings_dialog.widget.curve_manager.toolbar.components.get_action("add")
|
||||||
wf.curve_settings_dialog.widget.curve_manager.toolbar.widgets["add"].action.trigger()
|
add_action.action.trigger()
|
||||||
|
add_action.action.trigger()
|
||||||
qtbot.wait(100)
|
qtbot.wait(100)
|
||||||
# Check that the new row is added
|
# Check that the new row is added
|
||||||
assert wf.curve_settings_dialog.widget.curve_manager.tree.model().rowCount() == 2
|
assert wf.curve_settings_dialog.widget.curve_manager.tree.model().rowCount() == 2
|
||||||
@ -824,9 +825,9 @@ def test_show_dap_summary_popup(qtbot, mocked_client):
|
|||||||
"""
|
"""
|
||||||
wf = create_widget(qtbot, Waveform, client=mocked_client, popups=True)
|
wf = create_widget(qtbot, Waveform, client=mocked_client, popups=True)
|
||||||
|
|
||||||
assert "fit_params" in wf.toolbar.widgets
|
assert wf.toolbar.components.exists("fit_params")
|
||||||
|
|
||||||
fit_action = wf.toolbar.widgets["fit_params"].action
|
fit_action = wf.toolbar.components.get_action("fit_params").action
|
||||||
assert fit_action.isChecked() is False
|
assert fit_action.isChecked() is False
|
||||||
|
|
||||||
wf.show_dap_summary_popup()
|
wf.show_dap_summary_popup()
|
||||||
|
Reference in New Issue
Block a user