import warnings
from collections.abc import Iterable
from collections import OrderedDict
from pathlib import Path
from typing import Any, Callable, Iterable as IterableType, Optional, Union, OrderedDict as OrderedDictType
import qt_themes
from multipledispatch import dispatch
from multipledispatch.dispatcher import MethodDispatcher, str_signature
from qtpy import QtCore, QtGui, QtWidgets
from qtpy.QtWidgets import QAction as QtQAction
from pymodaq_gui.qt_utils import apply_theme, theme_signaller
from pymodaq_gui.utils.styling import create_icon, create_color, as_theme_role, get_current_theme
from pymodaq_utils.utils import find_keys_from_val
from pymodaq_utils.warnings import deprecation_msg
from pymodaq_utils.config import GlobalConfig as Config
try:
from pymodaq_gui.resources.material_icons import MaterialIcon
except ImportError:
pass #this could happen when creating /importing new MaterialIcons
config = Config()
TOOLBAR_BUTTON_STYLES = {
'icon_only': QtCore.Qt.ToolButtonStyle.ToolButtonIconOnly,
'text_only': QtCore.Qt.ToolButtonStyle.ToolButtonTextOnly,
'text_beside_icon': QtCore.Qt.ToolButtonStyle.ToolButtonTextBesideIcon,
'text_under_icon': QtCore.Qt.ToolButtonStyle.ToolButtonTextUnderIcon,
'follow_style': QtCore.Qt.ToolButtonStyle.ToolButtonFollowStyle,
}
TOOLBAR_BUTTON_STYLE_LABELS = {
'icon_only': 'Icons only',
'text_only': 'Text only',
'text_beside_icon': 'Text beside icons',
'text_under_icon': 'Text under icons',
'follow_style': 'Follow system style',
}
toolbar_style_signaller = ToolbarStyleSignaller()
[docs]
class ThemeChecks(QtCore.QObject):
"""Keeps the checkable theme actions in sync, lives as long as the menu it is parented to"""
def __init__(self, actions: dict[str, QtGui.QAction], themes: dict[str, qt_themes.Theme],
parent: QtCore.QObject):
super().__init__(parent)
self._actions = actions
self._themes = themes
theme_signaller.theme_changed.connect(self.sync)
[docs]
@QtCore.Slot(object)
def sync(self, theme: qt_themes.Theme):
for name, action in self._actions.items():
action.setChecked(self._themes[name] == theme)
resource_folder = Path(__file__).parent.parent.joinpath('resources')
QtCore.QDir.addSearchPath('icons', str(resource_folder.joinpath('icon_library')))
[docs]
class QAction(QtQAction):
"""
QAction subclass to mimic signals as pushbuttons. Done to be sure of backcompatibility
when I moved from pushbuttons to QAction
"""
def __init__(self, icon_unchecked: Union[None, str, Path, QtGui.QIcon],
name: str = '',
icon_checked: Optional[Union[str, Path, QtGui.QIcon]] = None,
icon_color: Optional[Union[QtGui.QColor, bytes, str]] = None,
icon_checked_color: Optional[Union[QtGui.QColor, bytes, str]] = None,
flip_h: bool = False,
flip_v: bool = False,
rotate: int = 0,
fill: Optional[bool] = None,
follow_theme: bool = True,
) -> None:
super().__init__(name)
# What the icon is made of, kept so it can be rebuilt (refresh_icon) when the
# theme changes: icons bake their colours at construction. With follow_theme,
# theme colours are kept by name (see as_theme_role); otherwise they are
# resolved now and kept as fixed colours.
self._icon_spec = None
# Only icons given by name/path are rebuilt: an icon instance keeps the
# palette it was built under, rebuilding it would not follow the theme.
self._icon_rebuildable = isinstance(icon_unchecked, (str, Path))
self._icon_set_explicitly = False
self._checked_icon_connected = False
if icon_unchecked is not None:
keep = as_theme_role if follow_theme else create_color
self._icon_spec = dict(icon_unchecked=icon_unchecked, icon_checked=icon_checked,
icon_color=keep(icon_color),
icon_checked_color=keep(icon_checked_color),
flip_h=flip_h, flip_v=flip_v, rotate=rotate, fill=fill)
self._build_icons()
self.setIcon(self.icon_unchecked)
if self._icon_rebuildable:
# follow theme changes (apply_theme), whichever window triggers them;
# a slot of this QObject, so Qt disconnects it when the action is deleted
theme_signaller.theme_changed.connect(self.refresh_icon)
def _build_icons(self):
spec = self._icon_spec
transforms = dict(flip_h=spec['flip_h'], flip_v=spec['flip_v'], rotate=spec['rotate'],
fill=spec['fill'])
self.icon_unchecked = create_icon(spec['icon_unchecked'], spec['icon_color'],
spec['icon_checked_color'], **transforms)
icon_checked = spec['icon_checked']
if icon_checked is not None and not isinstance(icon_checked, QtGui.QIcon):
icon_checked = create_icon(icon_checked, spec['icon_checked_color'],
spec['icon_checked_color'], **transforms)
if isinstance(spec['icon_unchecked'], MaterialIcon):
self.icon_unchecked.set_icon(icon_checked, state=QtGui.QIcon.State.On)
else:
self.icon_checked = icon_checked
if not self._checked_icon_connected:
self.triggered.connect(lambda: self.set_icon())
self._checked_icon_connected = True
[docs]
@QtCore.Slot()
@QtCore.Slot(object)
def refresh_icon(self, _theme=None):
"""Rebuild the icon(s) with the current theme colours. Called automatically
when :func:`pymodaq_gui.qt_utils.apply_theme` changes the theme.
No-op for actions created without icon or with an icon instance (QIcon,
MaterialIcon) rather than a name, and for actions whose icon was then replaced
with ``set_icon(icon)``: their owner manages that icon (e.g. a state colour),
rebuilding the original one would undo it.
"""
if self._icon_spec is None or not self._icon_rebuildable or self._icon_set_explicitly:
return
self._build_icons()
if self.isChecked() and getattr(self, 'icon_checked', None) is not None:
self.setIcon(self.icon_checked)
else:
self.setIcon(self.icon_unchecked)
[docs]
def click(self) -> None:
deprecation_msg("click for PyMoDAQ's QAction is deprecated, use *trigger*",
stacklevel=3)
self.trigger()
@property
def clicked(self) -> Any:
deprecation_msg("clicked for PyMoDAQ's QAction is deprecated, use *trigger*",
stacklevel=3)
return self.triggered
[docs]
def connect_to(self, slot: Callable[..., Any]) -> None:
"""Connect the *triggered* signal to a slot"""
self.triggered.connect(slot)
[docs]
def set_icon(self, icon_name: Optional[Union[str, Path, QtGui.QIcon]] = None,
icon_color: Optional[Union[QtGui.QColor, bytes, str]] = None) -> None:
"""Set the icon of the action, by default the one of the current checked state"""
if icon_name is None:
if self.isChecked():
icon_name = self.icon_checked
else:
icon_name = self.icon_unchecked
if icon_color is None:
# Already built with their colours: passing them through create_icon
# again would reset a MaterialIcon to the default palette colour.
self.setIcon(icon_name)
return
else:
self._icon_set_explicitly = True
self.setIcon(create_icon(icon_name, icon_color))
def __repr__(self) -> str:
return f'QAction {self.text()}'
[docs]
def addaction(name: str = '', icon_name: Union[str, Path, QtGui.QIcon, None] = '', tip: str = '',
checkable: bool = False, checked: bool = False,
slot: Optional[Callable[..., Any]] = None, toolbar: Optional[QtWidgets.QToolBar] = None,
menu: Optional[QtWidgets.QMenu] = None, visible: bool = True,
shortcut: Optional[Union[str, QtCore.Qt.Key, QtGui.QKeySequence]] = None,
enabled: bool = True, icon_checked: Optional[Union[str, Path, QtGui.QIcon]] = None,
icon_color: Optional[Union[QtGui.QColor, bytes, str]] = None,
icon_checked_color: Optional[Union[QtGui.QColor, bytes, str]] = None,
flip_h: bool = False,
flip_v: bool = False,
before: Optional[QtQAction] = None,
action: Optional[QAction] = None,
fill: Optional[bool] = None,
rotate: int = 0,
follow_theme: bool = True,
) -> QAction:
"""Create a new action and add it eventually to a toolbar and a menu
Parameters
----------
name: str
Displayed name if should be displayed (for instance in menus)
icon_name: str / Path / QtGui.QIcon / enum name
str/Path: the png file name/path to produce the icon
QtGui.QIcon: the instance of a QIcon element
ThemeIcon enum: the value of QtGui.QIcon.ThemeIcon (requires Qt>=6.7)
tip: str
a tooltip to be displayed when hovering above the action
checkable: bool
set the checkable state of the action
checked: bool
set the current state of the action
slot: callable
Method or function that will be called when the action is triggered
toolbar: QToolBar
a toolbar where action should be added.
menu: QMenu
a menu where action should be added.
visible: bool
display or not the action in the toolbar/menu
shortcut: str, QKey or QKeySequence
Using this shortcut will trigger the action
enabled: bool
set the enabled state
icon_checked: : str / Path / QtGui.QIcon / enum name
str/Path: the png file name/path to produce the icon
QtGui.QIcon: the instance of a QIcon element
ThemeIcon enum: the value of QtGui.QIcon.ThemeIcon (requires Qt>=6.7)
Optional, if set, will be the icon when the action is checked (checkable will be set to True)
icon_color: ThemeColor / str / QtGui.QColor
color to be applied (if possible) to the unchecked icon. Give theme colours by
name (ThemeColor.GREEN or 'green') so they follow theme changes
icon_checked_color: ThemeColor / str / QtGui.QColor
color to be applied to the checked icon (if any), same as icon_color
flip_h: bool
mirror the icon horizontally (left ↔ right)
flip_v: bool
mirror the icon vertically (top ↔ bottom)
rotate: int
rotate the icon by the given value in degrees
before: QAction, optional
if set, the action is inserted before this action in the toolbar/menu;
if None the action is appended at the end
action: pymodaq_gui.managers.action_manager.QAction, optional
An existing action to configure instead of creating a new one
fill: bool, optional
Fill or not the icon, if None left to the user configuration
rotate: int, optional
Rotate the icon by this value in degree
follow_theme: bool
True (default): colours given as theme colours (:class:`~pymodaq_gui.utils.styling.ThemeColor`,
their names, or values resolved from the theme) follow theme changes. False: the
colours are resolved once, here, and kept as they are
Returns
-------
QAction
The created (or given) action
"""
if action is None:
if icon_name is None or icon_name == '':
action = QAction(None, name)
else:
action = QAction(icon_name, name, icon_checked=icon_checked,
icon_color=icon_color, icon_checked_color=icon_checked_color,
flip_h=flip_h, flip_v=flip_v, rotate=rotate, fill=fill,
follow_theme=follow_theme)
if slot is not None:
action.connect_to(slot)
if icon_checked is not None:
checkable = True
action.setCheckable(checkable)
if checkable:
action.setChecked(checked)
if checked and hasattr(action, 'icon_checked'):
action.set_icon()
action.setToolTip(tip)
if toolbar is not None:
if before is not None:
toolbar.insertAction(before, action)
else:
toolbar.addAction(action)
if menu is not None:
if before is not None:
menu.insertAction(before, action)
else:
menu.addAction(action)
if action.parent() is None and (toolbar is not None or menu is not None):
# an unparented action is never deleted while a slot connected to it (often a lambda holding the
# UI) is alive: it would keep the whole UI alive after it is closed
action.setParent(toolbar if toolbar is not None else menu)
if shortcut is not None:
action.setShortcut(shortcut)
action.setVisible(visible)
action.setEnabled(enabled)
return action
class _InstanceMethodDispatcher(MethodDispatcher):
""" MethodDispatcher that does not store the instance it is accessed from
multipledispatch's MethodDispatcher, shared by all instances, keeps the last one in an attribute: the last UI
that called each method stayed alive after it was closed.
"""
__slots__ = ()
def __get__(self, instance: Any, owner: Any) -> Any:
if instance is None:
return self
return lambda *args, **kwargs: self._call(instance, *args, **kwargs)
def _call(self, instance: Any, *args: Any, **kwargs: Any) -> Any:
types = tuple([type(arg) for arg in args])
func = self.dispatch(*types)
if not func:
raise NotImplementedError(f'Could not find signature for {self.name}: <{str_signature(types)}>')
return func(instance, *args, **kwargs)
def _delete_later(obj: QtCore.QObject, only_if_orphan: bool = False) -> None:
try:
if not only_if_orphan or obj.parent() is None:
obj.deleteLater()
except RuntimeError: # the C++ object was already deleted (e.g. with its parent)
pass
[docs]
class ActionManager:
"""MixIn Class to be used by all UserInterface to manage their QActions and the action they are connected to
Parameters
----------
toolbar: QToolbar, optional
The toolbar to use as default
menu: QMenu, option
The menu to use as default
"""
def __init__(self, toolbar: Optional[QtWidgets.QToolBar] = None, menu: Optional[QtWidgets.QMenu] = None) -> None:
self._actions: OrderedDictType[str, QAction] = OrderedDict([])
self._menus: OrderedDictType[str, QtWidgets.QMenu] = OrderedDict([])
self._menu_icon_names: dict[str, Union[str, Path]] = {}
self._toolbars: OrderedDictType[str, QtWidgets.QToolBar] = OrderedDict([])
self._toolbar: Optional[QtWidgets.QToolBar] = None
self._menu: Optional[QtWidgets.QMenu] = None
if toolbar is not None:
self.reference_toolbar('default', toolbar)
self.set_toolbar(toolbar)
if menu is not None:
self.reference_menu('default', menu)
self.set_menu(menu)
[docs]
def delete_actions(self) -> None:
"""Schedule the deletion of all the QActions referenced in this manager, and of its toolbars and
menus that have no parent
A Qt object keeps the slots connected to its signals (often lambdas holding the manager) alive as
long as the C++ object exists. Unparented actions, toolbars or menus therefore keep the whole UI that
created them alive after it is closed, out of reach of Python's garbage collector. Call this when the
UI is closed for good.
.. versionadded:: 5.3.9
"""
for action in self._actions.values():
_delete_later(action)
for widget in list(self._toolbars.values()) + list(self._menus.values()):
# the ones with a parent are owned (and deleted) by a window that may not be closed
_delete_later(widget, only_if_orphan=True)
def _resolve_toolbar(self, toolbar: Union[str, QtWidgets.QToolBar, None],
auto: bool = True) -> Union[QtWidgets.QToolBar, None]:
"""Resolve a toolbar reference to a QToolBar instance.
Parameters
----------
toolbar: str, QToolBar, or None
str: looked up by name via get_toolbar
QToolBar: returned as-is
None + auto=True: returns the default toolbar (creating one if needed)
None + auto=False: returns None
"""
if toolbar is not None:
if isinstance(toolbar, str):
return self.get_toolbar(toolbar)
if not isinstance(toolbar, QtWidgets.QToolBar):
raise TypeError(f'toolbar must be None, a str, or QToolBar, got {type(toolbar)}')
return toolbar
return self.toolbar if auto else None
def _resolve_menu(self, menu: Union[str, QtWidgets.QMenu, None],
auto: bool = True) -> Union[QtWidgets.QMenu, None]:
"""Resolve a menu reference to a QMenu instance.
Parameters
----------
menu: str, QMenu, QMenuBar, or None
str: looked up by name via get_menu
QMenu/QMenuBar: returned as-is
None + auto=True: returns the default menu (creating one if needed)
None + auto=False: returns None
"""
if menu is not None:
if isinstance(menu, str):
return self.get_menu(menu)
if not isinstance(menu, (QtWidgets.QMenu, QtWidgets.QMenuBar)):
raise TypeError(f'menu must be None, a str, QMenu, or QMenuBar, got {type(menu)}')
return menu
return self.menu if auto else None
def _resolve_action(self, action: Union[str, 'QAction', WidgetActionProxy, None]) -> Optional[QtQAction]:
"""Resolve an action reference to a QAction instance.
Parameters
----------
action: str, QAction, WidgetActionProxy, or None
str: looked up by name via get_action
WidgetActionProxy: unwrapped to its underlying QAction
QAction: returned as-is
None: returned as None
"""
if action is None:
return None
if isinstance(action, str):
action = self.get_action(action)
if isinstance(action, WidgetActionProxy):
return action._action
if isinstance(action, QtWidgets.QMenu):
return action.menuAction()
if not isinstance(action, QtQAction):
raise TypeError(f'action must be None, a str, QAction, WidgetActionProxy, or QMenu, got {type(action)}')
return action
[docs]
def setup_actions(self) -> None:
"""Method where to create actions to be subclassed. Mandatory
Examples
--------
>>> self.add_action('Quit', 'close2', "Quit program")
>>> self.add_action('Grab', 'camera', "Grab from camera", checkable=True)
>>> self.add_action('Load', 'Open', "Load target file (.h5, .png, .jpg) or data from camera", checkable=False)
>>> self.add_action('Save', 'SaveAs', "Save current data", checkable=False)
See Also
--------
ActionManager.add_action
"""
raise NotImplementedError(f'You have to define actions here in the following form:'
f'{self.setup_actions.__doc__}')
[docs]
def add_action(self, short_name: str = '', name: str = '',
icon_name: Union[str, Path, QtGui.QIcon] = '', tip: str = '',
checkable: bool = False,
checked: bool = False,
toolbar: Union[str, QtWidgets.QToolBar, None] = None,
menu: Union[str, QtWidgets.QMenu, None] = None,
visible: bool = True,
shortcut: Optional[Union[str, QtCore.Qt.Key, QtGui.QKeySequence]] = None,
auto_toolbar: bool = True, auto_menu: bool = False,
enabled: bool = True, icon_checked: Optional[Union[str, Path, QtGui.QIcon]] = None,
icon_color: Optional[Union[QtGui.QColor, bytes, str]] = None,
icon_checked_color: Optional[Union[QtGui.QColor, bytes, str]] = None,
flip_h: bool = False,
flip_v: bool = False,
before: Union[str, 'QAction', WidgetActionProxy, None] = None,
action: Optional[QAction] = None,
fill: Optional[bool] = None,
rotate: int = 0,
follow_theme: bool = True,
) -> QAction:
"""Create a new action and add it to toolbar and menu
Parameters
----------
short_name: str
the name as referenced in the dict self.actions
name: str
Displayed name if should be displayed in
icon_name: str / Path / QtGui.QIcon / enum name
str/Path: the png file name/path to produce the icon
QtGui.QIcon: the instance of a QIcon element
ThemeIcon enum: the value of QtGui.QIcon.ThemeIcon (requires Qt>=6.7)
tip: str
a tooltip to be displayed when hovering above the action
checkable: bool
set the checkable state of the action
checked: bool
set the current state of the action
toolbar: str or QToolBar or None
a toolbar where action should be added. Can be:
- None: adds to the default toolbar (self._toolbar)
- str: toolbar name as registered via add_toolbar()
- QToolbar: direct QToolbar instance
Actions can also be added later see *affect_to*
menu: str or QMenu or None
Where to add the action. Can be:
- None: adds to the default menu (self._menu)
- str: menu name as registered via add_menu()
- QMenu: direct QMenu instance
Actions can also be added later see *affect_to*
visible: bool
display or not the action in the toolbar/menu
shortcut: str, QKey or QKeySequence
Using this shortcut will trigger the action
auto_toolbar: bool
if True add this action to the defined toolbar
auto_menu: bool
if True add this action to the defined menu
enabled: bool
set the enabled state of this action
icon_checked: str / Path / QtGui.QIcon / enum name
str/Path: the png file name/path to produce the icon
QtGui.QIcon: the instance of a QIcon element
ThemeIcon enum: the value of QtGui.QIcon.ThemeIcon (requires Qt>=6.7)
Optional, if set, will be the icon when the action is checked
icon_color: ThemeColor / str / QtGui.QColor
color to be applied (if possible) to the unchecked icon. Give theme colours by
name (ThemeColor.GREEN or 'green') so they follow theme changes
icon_checked_color: ThemeColor / str / QtGui.QColor
color to be applied to the checked icon (if any), same as icon_color
flip_h: bool
mirror the icon horizontally (left ↔ right)
flip_v: bool
mirror the icon vertically (top ↔ bottom)
rotate: int
rotate the icon by the given value in degree
before: str, QAction, WidgetActionProxy, or None, optional
if set, the action is inserted before this action in the toolbar/menu;
accepts a short_name str, a QAction instance, or a WidgetActionProxy
action: pymodaq_gui.managers.action_manager.QAction, optional
An existing action to register instead of creating a new one
fill: bool, optional
Fill or not the icon, if None left to the user configuration
rotate: int, optional
Rotate the icon by this value in degree
follow_theme: bool
True (default): colours given as theme colours (:class:`~pymodaq_gui.utils.styling.ThemeColor`,
their names, or values resolved from the theme) follow theme changes. False: the
colours are resolved once, here, and kept as they are
Returns
-------
QAction
The created action, also referenced as *short_name*
See Also
--------
affect_to
"""
toolbar_obj = self._resolve_toolbar(toolbar, auto=auto_toolbar)
menu_obj = self._resolve_menu(menu, auto=auto_menu)
before_action = self._resolve_action(before)
self._actions[short_name] = addaction(name, icon_name, tip, checkable=checkable,
checked=checked, toolbar=toolbar_obj, menu=menu_obj,
visible=visible, shortcut=shortcut, enabled=enabled,
icon_checked=icon_checked,
icon_color=icon_color,
icon_checked_color=icon_checked_color,
flip_h=flip_h,
flip_v=flip_v,
before=before_action,
action=action,
fill=fill,
rotate=rotate,
follow_theme=follow_theme)
return self._actions[short_name]
[docs]
def refresh_icons(self) -> None:
"""Rebuild the icons of all managed actions and menus with the current theme colours.
Icons bake their colours when created. This is done automatically when
:func:`pymodaq_gui.qt_utils.apply_theme` changes the theme; call it if the
palette was changed by other means. Only icons given by name are rebuilt,
see :meth:`QAction.refresh_icon`.
"""
for action in self._actions.values():
if isinstance(action, QAction):
action.refresh_icon()
for short_name, icon_name in self._menu_icon_names.items():
menu = self._menus.get(short_name)
if menu is not None:
menu.setIcon(create_icon(icon_name))
[docs]
def remove_action(self, toolbar: QtWidgets.QToolBar | str | None = None,
action: str | QAction | WidgetActionProxy | None = None) -> None:
"""Remove an action from a toolbar and forget it
Parameters
----------
toolbar: QToolBar, str or None
The toolbar (or its name) to remove the action from. If None, the action is not removed from a toolbar
action: str, QAction, WidgetActionProxy or None
The action (or its short_name) to remove
"""
toolbar_obj = self._resolve_toolbar(toolbar, auto=False)
try:
action_object = self._resolve_action(action)
if toolbar_obj is not None and action_object is not None:
toolbar_obj.removeAction(action_object)
if isinstance(action, str):
self._actions.pop(action, None)
else:
action_str_list = find_keys_from_val(self._actions, action)
if len(action_str_list) > 0:
self._actions.pop(action_str_list[0], None)
except KeyError:
pass
[docs]
def add_action_group(self, short_name: str, name: str = '',
icon_name: Union[str, Path, QtGui.QIcon] = '', tip: str = '',
checkable: bool = False, checked: bool = False,
icon_checked: Union[str, Path, QtGui.QIcon, None] = None,
icon_color: Union[QtGui.QColor, bytes, str, None] = None,
icon_checked_color: Union[QtGui.QColor, bytes, str, None] = None,
toolbar: Union[str, QtWidgets.QToolBar, None] = None,
secondary_actions: IterableType[dict] = (),
visible: bool = True, enabled: bool = True,
follow_theme: bool = True,
) -> QtWidgets.QToolButton:
"""Add several related actions to a toolbar under a single button, to avoid
overloading the toolbar with one slot per action.
The button shows and triggers a *primary* action directly (a regular action,
created and registered exactly like :meth:`add_action`, so
:meth:`connect_action`, :meth:`is_action_checked`, :meth:`set_action_checked`
... all keep working on its ``short_name`` unchanged). A small dropdown arrow
next to it reveals the *secondary* actions, each created the same way as
:meth:`add_action` would from its spec dict, but added only to the dropdown,
never to a toolbar directly.
If there are no secondary actions, the button behaves like a plain toolbar
action (no dropdown arrow).
Parameters
----------
short_name: str
the name of the primary action, as referenced in self._actions
name, icon_name, tip, checkable, checked, icon_checked, icon_color,
icon_checked_color, follow_theme:
forwarded to :meth:`add_action` for the primary action
toolbar: str or QToolBar or None
the toolbar the button is added to (defaults to the default toolbar)
secondary_actions: iterable of dict
one spec per dropdown entry, using the same keys as :meth:`add_action`
(at least ``short_name``); order is preserved in the dropdown menu
visible, enabled: bool
forwarded to the primary action
Returns
-------
QtWidgets.QToolButton
the created button, already added to the toolbar
See Also
--------
add_action
"""
toolbar_obj = self._resolve_toolbar(toolbar, auto=True)
primary = self.add_action(short_name, name, icon_name, tip,
checkable=checkable, checked=checked,
toolbar=None, menu=None,
auto_toolbar=False, auto_menu=False,
visible=visible, enabled=enabled,
icon_checked=icon_checked,
icon_color=icon_color,
icon_checked_color=icon_checked_color,
follow_theme=follow_theme)
button = QtWidgets.QToolButton(toolbar_obj)
button.setDefaultAction(primary)
# Let the button pick up the toolbar's icon/text style (and keep following it
# on later changes) instead of freezing its style at creation time.
button.setToolButtonStyle(QtCore.Qt.ToolButtonStyle.ToolButtonFollowStyle)
if secondary_actions:
button.setPopupMode(QtWidgets.QToolButton.ToolButtonPopupMode.MenuButtonPopup)
dropdown = QtWidgets.QMenu(button)
for spec in secondary_actions:
spec = dict(spec)
spec_short_name = spec.pop('short_name')
self.add_action(spec_short_name, toolbar=None, menu=None,
auto_toolbar=False, auto_menu=False, **spec)
dropdown.addAction(self._actions[spec_short_name])
button.setMenu(dropdown)
else:
button.setPopupMode(QtWidgets.QToolButton.ToolButtonPopupMode.DelayedPopup)
if toolbar_obj is not None:
toolbar_obj.addWidget(button)
return button
[docs]
def apply_qt_theme(self, name: str) -> None:
"""Apply a qt_themes theme to the whole application and persist it as the default"""
if apply_theme(name) is None:
return
# pymodaq uses the first element of the configured list
themes = list(config('gui', 'style', 'theme'))
config['gui', 'style', 'theme'] = [name] + [t for t in themes if t != name]
config.save()
[docs]
def set_action_text(self, action_name: str, text: str) -> None:
"""Convenience method to set the displayed text on an action
Parameters
----------
action_name: str
The action name as defined in setup_actions
text: str
The text to display
"""
self.get_action(action_name).setText(text)
@property
def actions(self) -> list[QAction]:
"""Get all the actions (and widgets added to a toolbar) managed here"""
return list(self._actions.values())
@property
def actions_names(self) -> list[str]:
"""Get all the action names"""
return list(self._actions.keys())
[docs]
def get_action(self, name: str) -> Union[QAction, WidgetActionProxy]:
"""Getter of a given action
Parameters
----------
name: str
The action name as defined in setup_actions
Returns
-------
QAction
"""
if self.has_action(name):
return self._actions[name]
else:
raise KeyError(f'The action with name: {name} is not referenced'
f' in the view actions: {self._actions.keys()}')
[docs]
def has_action(self, action_name: str) -> bool:
"""Check if an action has been defined
Parameters
----------
action_name: str
The action name as defined in setup_actions
Returns
-------
bool: True if the action exists, False otherwise
"""
return action_name in self._actions
@property
def menus(self) -> list[QtWidgets.QMenu]:
"""Get all menus"""
return list(self._menus.values())
@property
def menus_names(self) -> list[str]:
"""Get all menu names"""
return list(self._menus.keys())
@property
def toolbars(self) -> list[QtWidgets.QToolBar]:
"""Get all toolbars"""
return list(self._toolbars.values())
@property
def toolbars_names(self) -> list[str]:
"""Get all toolbar names"""
return list(self._toolbars.keys())
@property
def toolbar(self) -> QtWidgets.QToolBar:
"""Get the default toolbar"""
if self._toolbar is None:
if len(self.toolbars_names) != 0:
self._toolbar = self.toolbars[0]
else:
self._toolbar = self.add_toolbar('default', 'Default')
return self._toolbar
@property
def menu(self) -> QtWidgets.QMenu:
"""Get the default menu"""
if self._menu is None:
if len(self.menus_names) != 0:
self._menu = self.menus[0]
else:
self._menu = self.add_menu('default', 'Default')
return self._menu
[docs]
def affect_to(self, action_name: Union[str, QAction, WidgetActionProxy],
obj: Union[QtWidgets.QToolBar, QtWidgets.QMenu]) -> None:
"""Affect action to an object either a toolbar or a menu
Parameters
----------
action_name: str
The action name as defined in setup_actions
obj: QToolbar or QMenu
The object where to add the action
"""
if isinstance(obj, (QtWidgets.QToolBar, QtWidgets.QMenu)):
if isinstance(action_name, QAction):
obj.addAction(action_name)
elif isinstance(action_name, WidgetActionProxy):
obj.addAction(action_name._action)
else:
obj.addAction(self._actions[action_name])
[docs]
def connect_action(self, name: str, slot: Optional[Callable[..., Any]] = None, connect: bool = True,
signal_name: str = '') -> None:
"""Connect (or disconnect) the action referenced by name to the given slot
Parameters
----------
name: str
key of the action as referenced in the self._actions dict
slot: method
a method/function
connect: bool
if True connect the trigger signal of the action to the defined slot else disconnect it
signal_name: str
try to use it as a signal (for widgets added...) otherwise use the *triggered* signal
"""
signal = 'triggered'
if name in self._actions:
if hasattr(self._actions[name], signal_name):
signal = signal_name
if connect:
getattr(self._actions[name], signal).connect(slot)
else:
try:
if slot is not None:
getattr(self._actions[name], signal).disconnect(slot)
else:
getattr(self._actions[name], signal).disconnect()
except (TypeError,) as e:
pass # the action was not connected
else:
raise KeyError(f'The action with name: {name} is not referenced'
f' in the view actions: {self._actions.keys()}')
@dispatch(str)
def is_action_visible(self, action_name: str) -> bool:
"""Check the visibility of a given action or the list of an action"""
if action_name in self._actions:
return self._actions[action_name].isVisible()
else:
raise KeyError(f'The action with name: {action_name} is not referenced'
f' in the actions list: {self._actions}')
[docs]
@dispatch(Iterable) # type: ignore[no-redef]
def is_action_visible(self, actions_name: IterableType) -> bool:
"""Check the visibility of a list of actions
Returns
-------
bool
True if all the actions are visible (and for an empty list)
"""
return all(self.is_action_visible(action_name) for action_name in actions_name)
@dispatch(str)
def is_action_checked(self, action_name: str) -> bool:
"""Get the CheckState of a given action or a list of actions"""
if action_name in self._actions:
return self._actions[action_name].isChecked()
else:
raise KeyError(f'The action with name: {action_name} is not referenced'
f' in the actions list: {self._actions}')
[docs]
@dispatch(Iterable) # type: ignore[no-redef]
def is_action_checked(self, actions_name: IterableType) -> bool:
"""Get the CheckState of a list of actions
Returns
-------
bool
True if all the actions are checked (and for an empty list)
"""
return all(self.is_action_checked(action_name) for action_name in actions_name)
@dispatch(str, bool)
def set_action_visible(self, action_name: str, visible: bool = True) -> None:
"""Set the visibility of a given action or a list of an action"""
if action_name in self._actions:
self._actions[action_name].setVisible(visible)
else:
raise KeyError(f'The action with name: {action_name} is not referenced'
f' in the actions list: {self._actions}')
[docs]
@dispatch(Iterable, bool) # type: ignore[no-redef]
def set_action_visible(self, actions_name: IterableType, visible: bool = True) -> None:
"""Set the visibility of a given action or a list of an action"""
for action_name in actions_name:
self.set_action_visible(action_name, visible)
@dispatch(str, bool)
def set_action_checked(self, action_name: str, checked: bool = True) -> None:
"""Set the CheckedState of a given action or a list of actions"""
if action_name in self._actions:
self._actions[action_name].setChecked(checked)
if hasattr(self._actions[action_name], 'icon_checked'):
self._actions[action_name].set_icon()
else:
raise KeyError(f'The action with name: {action_name} is not referenced'
f' in the actions list: {self._actions}')
[docs]
@dispatch(Iterable, bool) # type: ignore[no-redef]
def set_action_checked(self, actions_name: IterableType, checked: bool = True) -> None:
"""Set the CheckedState of a given action or a list of actions"""
for action_name in actions_name:
self.set_action_checked(action_name, checked)
@dispatch(str, bool)
def set_action_enabled(self, action_name: str, enabled: bool = True) -> None:
"""Set the EnabledState of a given action or a list of actions"""
if action_name in self._actions:
if hasattr(self._actions[action_name], 'widget'):
self._actions[action_name].widget.setEnabled(enabled) # action proxy with widget
else:
self._actions[action_name].setEnabled(enabled)
else:
raise KeyError(f'The action with name: {action_name} is not referenced'
f' in the actions list: {self._actions}')
[docs]
@dispatch(Iterable, bool) # type: ignore[no-redef]
def set_action_enabled(self, actions_name: IterableType, enabled: bool = True) -> None:
"""Set the EnabledState of a given action or a list of actions"""
for action_name in actions_name:
self.set_action_enabled(action_name, enabled)
@dispatch(str)
def is_action_enabled(self, action_name: str) -> bool:
"""Get the EnabledState of a given action or a list of actions"""
if action_name in self._actions:
return self._actions[action_name].isEnabled()
else:
raise KeyError(f'The action with name: {action_name} is not referenced'
f' in the actions list: {self._actions}')
[docs]
@dispatch(Iterable) # type: ignore[no-redef]
def is_action_enabled(self, actions_name: IterableType) -> bool:
"""Get the EnabledState of a list of actions
Returns
-------
bool
True if all the actions are enabled (and for an empty list)
"""
return all(self.is_action_enabled(action_name) for action_name in actions_name)
for _dispatcher in vars(ActionManager).values():
if isinstance(_dispatcher, MethodDispatcher):
_dispatcher.__class__ = _InstanceMethodDispatcher