Source code for magicclass.gui.menu_gui
from __future__ import annotations
from typing import Callable
import warnings
from inspect import signature
from magicgui.widgets import Image, Table, Label, FunctionGui
from magicgui.widgets._bases import ButtonWidget
from magicgui.widgets._bases.widget import Widget
from macrokit import Symbol
from qtpy.QtWidgets import QMenu
from .mgui_ext import AbstractAction, WidgetAction, _LabeledWidgetAction
from .keybinding import as_shortcut
from ._base import (
BaseGui,
PopUpMode,
ErrorMode,
ContainerLikeGui,
nested_function_gui_callback,
_inject_recorder,
)
from .utils import copy_class, format_error
from ..signature import get_additional_option
from ..fields import MagicField
from ..widgets import Separator, FreeWidget
from ..utils import iter_members
def _check_popupmode(popup_mode: PopUpMode):
if popup_mode in (PopUpMode.above, PopUpMode.below, PopUpMode.first):
msg = (
f"magicmenu does not support popup mode {popup_mode.value}."
"PopUpMode.popup is used instead"
)
warnings.warn(msg, UserWarning)
elif popup_mode == PopUpMode.last:
msg = (
f"magicmenu does not support popup mode {popup_mode.value}."
"PopUpMode.parentlast is used instead"
)
warnings.warn(msg, UserWarning)
popup_mode = PopUpMode.parentlast
return popup_mode
[docs]class MenuGuiBase(ContainerLikeGui):
def __init__(
self,
parent=None,
name: str = None,
close_on_run: bool = None,
popup_mode: str | PopUpMode = None,
error_mode: str | ErrorMode = None,
labels: bool = True,
):
popup_mode = _check_popupmode(popup_mode)
super().__init__(
close_on_run=close_on_run, popup_mode=popup_mode, error_mode=error_mode
)
name = name or self.__class__.__name__
self.native = QMenu(name, parent)
self.native.setToolTipsVisible(True)
self.name = name
self._list: list[MenuGuiBase | AbstractAction] = []
self.labels = labels
def _convert_attributes_into_widgets(self):
cls = self.__class__
# Add class docstring as label.
if cls.__doc__:
self.native.setToolTip(cls.__doc__)
# Bind all the methods and annotations
base_members = {x[0] for x in iter_members(MenuGuiBase)}
_hist: list[tuple[str, str, str]] = [] # for traceback
_ignore_types = (property, classmethod, staticmethod)
for name, attr in filter(lambda x: x[0] not in base_members, iter_members(cls)):
if isinstance(attr, _ignore_types):
continue
try:
if isinstance(attr, type):
if not issubclass(attr, BaseGui):
continue
# Nested magic-menu
if cls.__name__ not in attr.__qualname__.split("."):
attr = copy_class(attr)
attr.__qualname__ = f"{cls.__qualname__}.{attr.__name__}"
widget = attr()
object.__setattr__(self, name, widget)
elif isinstance(attr, MagicField):
widget = self._create_widget_from_field(name, attr)
else:
# convert class method into instance method
widget = getattr(self, name, None)
if isinstance(widget, FunctionGui):
p0 = list(signature(attr).parameters)[0]
getattr(widget, p0).bind(self) # set self to the first argument
elif isinstance(widget, BaseGui):
widget.__magicclass_parent__ = self
self.__magicclass_children__.append(widget)
widget._my_symbol = Symbol(name)
if isinstance(widget, MenuGuiBase):
widget.native.setParent(
self.native, widget.native.windowFlags()
)
else:
widget = WidgetAction(widget)
elif isinstance(widget, Widget):
if not widget.name:
widget.name = name
if hasattr(widget, "text") and not widget.text:
widget.text = widget.name.replace("_", " ")
widget = WidgetAction(widget)
if isinstance(widget, (MenuGuiBase, AbstractAction, Callable, Widget)):
if (not isinstance(widget, Widget)) and callable(widget):
if name.startswith("_") or not get_additional_option(
attr, "gui", True
):
keybinding = get_additional_option(attr, "keybinding", None)
if keybinding:
from qtpy.QtWidgets import QShortcut
shortcut = QShortcut(
as_shortcut(keybinding), self.native
)
shortcut.activated.connect(widget)
continue
widget = self._create_widget_from_method(widget)
elif hasattr(widget, "__magicclass_parent__") or hasattr(
widget.__class__, "__magicclass_parent__"
):
if isinstance(widget, BaseGui):
widget._my_symbol = Symbol(name)
# magic-class has to know its parent.
# if __magicclass_parent__ is defined as a property, hasattr must be called
# with a type object (not instance).
widget.__magicclass_parent__ = self
if name.startswith("_"):
continue
moveto = get_additional_option(attr, "into")
copyto = get_additional_option(attr, "copyto", [])
if moveto is not None or copyto:
self._unwrap_method(name, widget, moveto, copyto)
else:
self._fast_insert(len(self), widget)
_hist.append((name, str(type(attr)), type(widget).__name__))
except Exception as e:
format_error(e, _hist, name, attr)
self._unify_label_widths()
return None
def _fast_insert(
self, key: int, obj: Callable | MenuGuiBase | AbstractAction
) -> None:
if isinstance(obj, Callable):
# Sometimes users want to dynamically add new functions to GUI.
if isinstance(obj, FunctionGui):
if obj.parent is None:
f = nested_function_gui_callback(self, obj)
obj.called.connect(f)
_obj = obj
else:
obj = _inject_recorder(obj, is_method=False).__get__(self)
_obj = self._create_widget_from_method(obj)
method_name = getattr(obj, "__name__", None)
if method_name and not hasattr(self, method_name):
object.__setattr__(self, method_name, obj)
else:
_obj = obj
if isinstance(_obj, (self._component_class, MenuGuiBase)):
insert_action_like(self.native, key, _obj.native)
self._list.insert(key, _obj)
elif isinstance(_obj, WidgetAction):
from .toolbar import ToolBarGui
if isinstance(_obj.widget, Separator):
insert_action_like(self.native, key, "sep")
elif isinstance(_obj.widget, ToolBarGui):
qmenu = QMenu(_obj.widget.name, self.native)
qmenu.addAction(_obj.native)
if _obj.widget._icon_path is not None:
qmenu.setIcon(_obj.widget.native.windowIcon())
insert_action_like(self.native, key, qmenu)
else:
_hide_labels = (
_LabeledWidgetAction,
ButtonWidget,
FreeWidget,
Label,
FunctionGui,
Image,
Table,
)
_obj = _obj
if (not isinstance(_obj.widget, _hide_labels)) and self.labels:
_obj = _LabeledWidgetAction.from_action(_obj)
_obj.parent = self
insert_action_like(self.native, key, _obj.native)
self._list.insert(key, _obj)
else:
raise TypeError(f"{type(_obj)} is not supported.")
[docs] def insert(self, key: int, obj: Callable | MenuGuiBase | AbstractAction) -> None:
"""
Insert object into the menu. Could be widget or callable.
Parameters
----------
key : int
Position to insert.
obj : Callable | MenuGuiBase | AbstractAction | Widget
Object to insert.
"""
self._fast_insert(key, obj)
self._unify_label_widths()
[docs]def insert_action_like(qmenu: QMenu, key: int, obj):
"""
Insert a QObject into a QMenu in a Pythonic way, like qmenu.insert(key, obj).
Parameters
----------
qmenu : QMenu
QMenu object to which object will be inserted.
key : int
Position to insert.
obj : QMenu or QAction or "sep"
Object to be inserted.
"""
actions = qmenu.actions()
l = len(actions)
if key < 0:
key = key + l
if key == l:
if isinstance(obj, QMenu):
qmenu.addMenu(obj).setText(obj.objectName().replace("_", " "))
elif obj == "sep":
qmenu.addSeparator()
else:
qmenu.addAction(obj)
else:
new_action = actions[key]
before = new_action
if isinstance(obj, QMenu):
qmenu.insertMenu(before, obj).setText(obj.objectName().replace("_", " "))
elif obj == "sep":
qmenu.insertSeparator(before)
else:
qmenu.insertAction(before, obj)
[docs]class ContextMenuGui(MenuGuiBase):
"""Magic class that will be converted into a context menu."""
# TODO: Prevent more than one context menu