Source code for magicclass.gui.class_gui

from __future__ import annotations
from inspect import signature
from typing import Any
from qtpy.QtWidgets import QMenuBar, QWidget
from qtpy.QtCore import Qt
from magicgui.widgets import Container, MainWindow,Label, FunctionGui, Image, Table
from magicgui.widgets._bases import Widget, ButtonWidget, ValueWidget, ContainerWidget
from magicgui.widgets._concrete import _LabeledWidget

from ..signature import get_additional_option

from .mgui_ext import PushButtonPlus
from .menu_gui import MenuGui, ContextMenuGui
from ._base import BaseGui, PopUpMode, ErrorMode
from .utils import get_parameters, define_callback, MagicClassConstructionError
from ._containers import (
    ButtonContainer,
    GroupBoxContainer,
    ListContainer,
    SubWindowsContainer,
    ScrollableContainer,
    CollapsibleContainer,
    SplitterContainer,
    StackedContainer,
    TabbedContainer,
    ToolBoxContainer
    )

from ..macro import Expr, Head, Symbol, symbol
from ..utils import iter_members, extract_tooltip
from ..widgets import FreeWidget
from ..fields import MagicField

[docs]class ClassGuiBase(BaseGui): # This class is always inherited by @magicclass decorator. _component_class = PushButtonPlus _container_widget: type _remove_child_margins: bool native: QWidget def _create_widget_from_field(self, name: str, fld: MagicField): cls = self.__class__ if fld.not_ready(): try: fld.default_factory = cls.__annotations__[name] if isinstance(fld.default_factory, str): # Sometimes annotation is not type but str. from pydoc import locate fld.default_factory = locate(fld.default_factory) except (AttributeError, KeyError): pass fld.name = fld.name or name.replace("_", " ") widget = fld.get_widget(self) if isinstance(widget, (ValueWidget, Container)): # If the field has callbacks, connect it to the newly generated widget. for callback in fld.callbacks: # funcname = callback.__name__ widget.changed.connect(define_callback(self, callback)) if hasattr(widget, "value") and fld.record: # By default, set value function will be connected to the widget. f = _value_widget_callback(self, widget, name, getvalue=type(fld) is MagicField) widget.changed.connect(f) return widget def _convert_attributes_into_widgets(self): """ This function is called in dynamically created __init__. Methods, fields and nested classes are converted to magicgui widgets. """ cls = self.__class__ # Add class docstring as tooltip. doc = extract_tooltip(cls) self.tooltip = doc # Bind all the methods and annotations n_insert = 0 base_members = set(x[0] for x in iter_members(self._container_widget)) base_members |= set(x[0] for x in iter_members(ClassGuiBase)) _hist: list[tuple[str, str, str]] = [] # for traceback for name, attr in filter(lambda x: x[0] not in base_members, iter_members(cls)): if name in ClassGuiBase.__annotations__.keys() or isinstance(attr, property): continue try: if isinstance(attr, type): # Nested magic-class widget = attr() setattr(self, name, widget) self.__magicclass_children__.append(widget) elif isinstance(attr, BaseGui): widget = attr setattr(self, name, widget) self.__magicclass_children__.append(widget) elif isinstance(attr, MagicField): # If MagicField is given by field() function. widget = self._create_widget_from_field(name, attr) elif isinstance(attr, FunctionGui): widget = attr p0 = list(signature(attr).parameters)[0] getattr(widget, p0).bind(self) # set self to the first argument else: # convert class method into instance method widget = getattr(self, name, None) if isinstance(widget, MenuGui): # Add menubar to container widget.__magicclass_parent__ = self if self._menubar is None: self._menubar = QMenuBar(parent=self.native) self.native.layout().setMenuBar(self._menubar) widget.native.setParent(self._menubar, widget.native.windowFlags()) self._menubar.addMenu(widget.native) _hist.append((name, type(attr), "MenuGui")) elif isinstance(widget, ContextMenuGui): # Add context menu to container widget.__magicclass_parent__ = self self.native.setContextMenuPolicy(Qt.CustomContextMenu) self.native.customContextMenuRequested.connect( _define_context_menu(widget, self.native) ) _hist.append((name, type(attr), "ContextMenuGui")) elif isinstance(widget, Widget) or callable(widget): if (not isinstance(widget, Widget)) and callable(widget): # Methods (FunctionGui not included) widget = self._create_widget_from_method(widget) elif hasattr(widget, "__magicclass_parent__") or \ hasattr(widget.__class__, "__magicclass_parent__"): # 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 elif isinstance(widget, FunctionGui): # magic-class has to know when the nested FunctionGui is called. f = _nested_function_gui_callback(self, widget) widget.called.connect(f) else: if not widget.name: widget.name = name if hasattr(widget, "text") and not widget.text: widget.text = widget.name.replace("_", " ") # Now, "widget" is a Widget object. Add widget in a way similar to "insert" method # of Container. if name.startswith("_"): continue clsname = get_additional_option(attr, "into") if clsname is not None: self._unwrap_method(clsname, name, widget) else: self.insert(n_insert, widget) n_insert += 1 _hist.append((name, str(type(attr)), type(widget).__name__)) except Exception as e: hist_str = "\n\t".join(map( lambda x: f"{x[0]} {x[1]} -> {x[2]}", _hist )) + f"\n\t\t{name} ({type(attr).__name__}) <--- Error" if not hist_str.startswith("\n\t"): hist_str = "\n\t" + hist_str if isinstance(e, MagicClassConstructionError): e.args = (f"\n{hist_str}\n{e}",) raise e else: raise MagicClassConstructionError(f"\n{hist_str}\n\n{type(e).__name__}: {e}") from e # convert __call__ into a button if hasattr(self, "__call__"): widget = self._create_widget_from_method(self.__call__) self.insert(n_insert, widget) n_insert += 1 self._unify_label_widths() del _hist return None
[docs]def make_gui(container: type[ContainerWidget], no_margin: bool = True): """ Make a ClassGui class from a Container widget. Because GUI class inherits Container here, functions that need overriden must be defined here, not in ClassGuiBase. """ def wrapper(cls_: type[ClassGuiBase]): cls = type(cls_.__name__, (container, ClassGuiBase), {}) def __init__(self: cls, layout: str = "vertical", parent = None, close_on_run: bool = None, popup_mode: str | PopUpMode = None, error_mode: str | ErrorMode = None, labels: bool = True, name: str = None, ): container.__init__(self, layout=layout, labels=labels, name=name) BaseGui.__init__(self, close_on_run=close_on_run, popup_mode=popup_mode, error_mode=error_mode) if parent is not None: self.parent = parent self._menubar = None self.native.setObjectName(self.name) self.native.setWindowTitle(self.name) def __setattr__(self: cls, name: str, value: Any): if not isinstance(getattr(self.__class__, name, None), MagicField): container.__setattr__(self, name, value) else: object.__setattr__(self, name, value) def insert(self: cls, key: int, widget: Widget): # _hide_labels should not contain Container because some ValueWidget like widgets # are Containers. _hide_labels = (_LabeledWidget, ButtonWidget, ClassGuiBase, FreeWidget, Label, Image, Table, FunctionGui) if isinstance(widget, ValueWidget): widget.changed.connect(lambda: self.changed.emit(self)) if isinstance(widget, ClassGuiBase) and self._remove_child_margins: widget.margins = (0, 0, 0, 0) _widget = widget if self.labels: # no labels for button widgets (push buttons, checkboxes, have their own) if not isinstance(widget, _hide_labels): _widget = _LabeledWidget(widget) widget.label_changed.connect(self._unify_label_widths) self._list.insert(key, widget) if key < 0: key += len(self) self._widget._mgui_insert_widget(key, _widget) self._unify_label_widths() def show(self: cls, run: bool = False) -> None: """ Show ClassGui. If any of the parent ClassGui is a dock widget in napari, then this will also show up as a dock widget (floating if in popup mode). """ if self.__magicclass_parent__ is not None and self.parent is None: # If child magic class is closed before, we have to set parent again. self.native.setParent(self.__magicclass_parent__.native, self.native.windowFlags()) viewer = self.parent_viewer if viewer is not None and self.parent is not None: name = self.parent.objectName() if name in viewer.window._dock_widgets: viewer.window._dock_widgets[name].show() else: dock = viewer.window.add_dock_widget(self, area="right", allowed_areas=["left", "right"]) dock.setFloating(self._popup_mode == PopUpMode.popup) else: container.show(self, run=run) self.native.activateWindow() return None def close(self: cls): current_self = self._search_parent_magicclass() viewer = current_self.parent_viewer if viewer is not None: try: viewer.window.remove_dock_widget(self.parent) except Exception: pass container.close(self) return None cls.__init__ = __init__ cls.__setattr__ = __setattr__ cls.insert = insert cls.show = show cls.close = close cls._container_widget = container cls._remove_child_margins = no_margin return cls return wrapper
@make_gui(Container) class ClassGui: pass @make_gui(SplitterContainer) class SplitClassGui: pass @make_gui(ScrollableContainer) class ScrollableClassGui: pass @make_gui(CollapsibleContainer) class CollapsibleClassGui: pass @make_gui(ButtonContainer) class ButtonClassGui: pass @make_gui(ToolBoxContainer, no_margin=False) class ToolBoxClassGui: pass @make_gui(TabbedContainer, no_margin=False) class TabbedClassGui: pass @make_gui(StackedContainer, no_margin=False) class StackedClassGui: pass @make_gui(ListContainer, no_margin=False) class ListClassGui: pass @make_gui(SubWindowsContainer, no_margin=False) class SubWindowsClassGui: pass @make_gui(GroupBoxContainer, no_margin=False) class GroupBoxClassGui: pass @make_gui(MainWindow) class MainWindowClassGui: pass def _nested_function_gui_callback(cgui: ClassGuiBase, fgui: FunctionGui): def _after_run(): inputs = get_parameters(fgui) args = [Expr(head=Head.kw, args=[Symbol(k), v]) for k, v in inputs.items()] # args[0] is self sub = Expr(head=Head.getattr, args=[cgui._recorded_macro[0].args[0], Symbol(fgui.name)]) # {x}.func expr = Expr(head=Head.call, args=[sub] + args[1:]) # {x}.func(args...) if fgui._auto_call: # Auto-call will cause many redundant macros. To avoid this, only the last input # will be recorded in magic-class. last_expr = cgui._recorded_macro[-1] if last_expr.head == Head.call and last_expr.args[0].head == Head.getattr and \ last_expr.args[0].args[1] == expr.args[0].args[1]: cgui._recorded_macro.pop() cgui._recorded_macro.append(expr) return _after_run def _value_widget_callback(cgui: ClassGuiBase, widget: ValueWidget, name: str, getvalue: bool = True): def _set_value(): if not widget.enabled: # If widget is read only, it means that value is set in script (not manually). # Thus this event should not be recorded as a macro. return None cgui.changed.emit(cgui) if getvalue: sub = Expr(head=Head.getattr, args=[Symbol(name), Symbol("value")]) # name.value else: sub = Expr(head=Head.value, args=[Symbol(name)]) # Make an expression of # >>> x.name.value = value # or # >>> x.name = value expr = Expr(head=Head.assign, args=[Expr(head=Head.getattr, args=[symbol(cgui), sub]), widget.value]) last_expr = cgui._recorded_macro[-1] if (last_expr.head == expr.head and last_expr.args[0].args[1].head == expr.args[0].args[1].head and last_expr.args[0].args[1].args[0] == expr.args[0].args[1].args[0]): cgui._recorded_macro[-1] = expr else: cgui._recorded_macro.append(expr) return None return _set_value def _define_context_menu(contextmenu, parent): def rightClickContextMenu(point): contextmenu.native.exec_(parent.mapToGlobal(point)) return rightClickContextMenu