Source code for magicclass.gui.toolbar
from __future__ import annotations
from typing import Callable, TYPE_CHECKING, Any
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 QToolBar, QMenu, QWidgetAction, QTabWidget
from .mgui_ext import AbstractAction, _LabeledWidgetAction, WidgetAction, ToolButtonPlus
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, set_context_menu
from .menu_gui import ContextMenuGui, MenuGui, MenuGuiBase, insert_action_like
from ..signature import get_additional_option
from ..fields import MagicField
from ..widgets import FreeWidget, Separator
from ..utils import iter_members
if TYPE_CHECKING:
from napari.viewer import Viewer
def _check_popupmode(popup_mode: PopUpMode):
if popup_mode in (PopUpMode.above, PopUpMode.below, PopUpMode.first):
msg = (
f"magictoolbar does not support popup mode {popup_mode.value}."
"PopUpMode.popup is used instead"
)
warnings.warn(msg, UserWarning)
elif popup_mode == PopUpMode.last:
msg = (
f"magictoolbat 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 QtTabToolBar(QToolBar):
"""ToolBar widget with tabs."""
def __init__(self, title: str, parent=None):
super().__init__(title, parent)
self._tab = QTabWidget(self)
self._tab.setContentsMargins(0, 0, 0, 0)
self._tab.setTabBarAutoHide(True)
self._tab.setStyleSheet(
"QTabWidget {" " margin: 0px, 0px, 0px, 0px;" " padding: 0px;}"
)
self.addWidget(self._tab)
[docs] def addToolBar(self, toolbar: QToolBar, name: str) -> None:
"""Add a toolbar as a tab."""
self._tab.addTab(toolbar, name)
toolbar.setContentsMargins(0, 0, 0, 0)
return None
[docs]class ToolBarGui(ContainerLikeGui):
"""Magic class that will be converted into a toolbar"""
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 = QToolBar(name, parent)
self.name = name
self._list: list[MenuGuiBase | AbstractAction] = []
self.labels = labels
[docs] def reset_choices(self, *_: Any):
"""Reset child Categorical widgets"""
super().reset_choices()
# If parent magic-class is added to napari viewer, the style sheet need update because
# QToolButton has inappropriate style.
# Detecting this event using "reset_choices" is not a elegant way, but works for now.
viewer = self.parent_viewer
if viewer is not None:
style = _create_stylesheet(viewer)
self.native.setStyleSheet(style)
return None
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(ToolBarGui)}
_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, MenuGui):
tb = ToolButtonPlus(widget.name)
tb.set_menu(widget.native)
widget.native.setParent(tb.native, widget.native.windowFlags())
tb.tooltip = widget.__doc__
widget = WidgetAction(tb)
elif isinstance(widget, ContextMenuGui):
# Add context menu to toolbar
set_context_menu(widget, self)
_hist.append((name, type(attr), "ContextMenuGui"))
elif isinstance(widget, ToolBarGui):
tb = ToolButtonPlus(widget.name)
tb.tooltip = widget.__doc__
qmenu = QMenu(self.native)
waction = QWidgetAction(qmenu)
waction.setDefaultWidget(widget.native)
qmenu.addAction(waction)
tb.set_menu(qmenu)
widget = WidgetAction(tb)
else:
widget = WidgetAction(widget)
elif isinstance(widget, Widget):
widget = WidgetAction(widget)
if isinstance(widget, (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.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: AbstractAction | Callable) -> None:
"""
Insert object into the toolbar. Could be widget or callable.
Parameters
----------
key : int
Position to insert.
obj : Callable or AbstractAction
Object to insert.
"""
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
# _hide_labels should not contain Container because some ValueWidget like widgets
# are Containers.
if isinstance(_obj, self._component_class):
insert_action_like(self.native, key, _obj.native)
self._list.insert(key, _obj)
elif isinstance(_obj, WidgetAction):
if isinstance(_obj.widget, Separator):
insert_action_like(self.native, key, "sep")
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: AbstractAction) -> None:
self._fast_insert(key, obj)
self._unify_label_widths()
def _create_stylesheet(viewer: Viewer):
if viewer is None:
return ""
w = viewer.window._qt_window
styles = []
for s in w.styleSheet().split("\n\n"):
if s.startswith("QPushButton ") or s.startswith("QPushButton:"):
styles.append("QToolButton" + s[11:])
return "\n".join(styles)