Source code for magicclass.gui.mgui_ext

from __future__ import annotations
from typing import Callable, Iterable, Any, Generic, TypeVar
from qtpy.QtWidgets import (
    QPushButton,
    QAction,
    QWidgetAction,
    QToolButton,
    QWidget,
    QMenu,
)
from qtpy.QtGui import QIcon
from qtpy.QtCore import QSize
from psygnal import Signal
from magicgui.widgets import PushButton
from magicgui.widgets._concrete import _LabeledWidget
from magicgui.widgets._bases import Widget, ValueWidget
from magicgui.backends._qtpy.widgets import QBaseButtonWidget
from ._function_gui import FunctionGuiPlus

# magicgui widgets that need to be extended to fit into magicclass


[docs]class PushButtonPlus(PushButton): """ A Qt specific PushButton widget with a magicgui bound. """ def __init__(self, text: str | None = None, **kwargs): super().__init__(text=text, **kwargs) self.native: QPushButton self._icon_path = None self.mgui: FunctionGuiPlus = None # tagged function GUI self._doc = "" self._unwrapped = False @property def running(self) -> bool: return getattr(self.mgui, "running", False)
[docs] def set_shortcut(self, key): self.native.setShortcut(key)
[docs] def reset_choices(self, *_: Any): """Reset child Categorical widgets.""" if self.mgui is not None: self.mgui.reset_choices()
@property def background_color(self): return self.native.palette().button().color().getRgb() @background_color.setter def background_color(self, color: str | Iterable[float]): stylesheet = self.native.styleSheet() d = _stylesheet_to_dict(stylesheet) d.update({"background-color": _to_rgb(color)}) stylesheet = _dict_to_stylesheet(d) self.native.setStyleSheet(stylesheet) @property def icon_path(self): return self._icon_path @icon_path.setter def icon_path(self, path): path = str(path) icon = QIcon(path) self.native.setIcon(icon) @property def icon_size(self): qsize = self.native.iconSize() return qsize.width(), qsize.height() @icon_size.setter def icon_size(self, size: tuple[int, int]): w, h = size self.native.setIconSize(QSize(w, h)) @property def font_size(self): return self.native.font().pointSize() @font_size.setter def font_size(self, size: int): font = self.native.font() font.setPointSize(size) self.native.setFont(font) @property def font_color(self): return self.native.palette().text().color().getRgb() @font_color.setter def font_color(self, color: str | Iterable[float]): stylesheet = self.native.styleSheet() d = _stylesheet_to_dict(stylesheet) d.update({"color": _to_rgb(color)}) stylesheet = _dict_to_stylesheet(d) self.native.setStyleSheet(stylesheet) @property def font_family(self): return self.native.font().family() @font_family.setter def font_family(self, family: str): font = self.native.font() font.setFamily(family) self.native.setFont(font)
[docs] def from_options(self, options: dict[str] | Callable): if callable(options): try: options = options.__signature__.caller_options except AttributeError: return None for k, v in options.items(): if v is not None: setattr(self, k, v) return None
class _QToolButton(QBaseButtonWidget): def __init__(self): super().__init__(QToolButton)
[docs]class ToolButtonPlus(PushButtonPlus): def __init__(self, text: str | None = None, **kwargs): kwargs["widget_type"] = _QToolButton ValueWidget.__init__(self, **kwargs) self.text = text or self.name.replace("_", " ") self.native: QToolButton
[docs] def set_menu(self, qmenu: QMenu): self.native.setMenu(qmenu) self.native.setPopupMode(QToolButton.InstantPopup) self.native.setIcon(qmenu.icon()) # icon have to be copied.
[docs] def set_shortcut(self, key): self.native.setShortcut(key)
[docs] def reset_choices(self, *_: Any): """Reset child Categorical widgets.""" if self.mgui is not None: self.mgui.reset_choices()
[docs]class mguiLike: """Abstract class that provide magicgui.widgets like properties.""" native: QWidget | QAction @property def parent(self): self.native.parent() @parent.setter def parent(self, obj: mguiLike | Widget): self.native.setParent(obj.native) @property def name(self) -> str: return self.native.objectName() @name.setter def name(self, value: str): self.native.setObjectName(value) @property def tooltip(self) -> str: return self.native.toolTip() @tooltip.setter def tooltip(self, value: str): self.native.setToolTip(value) @property def enabled(self) -> bool: return self.native.isEnabled() @enabled.setter def enabled(self, value: bool): self.native.setEnabled(value) @property def visible(self) -> bool: return self.native.isVisible() @visible.setter def visible(self, value: bool): self.native.setVisible(value) @property def widget_type(self): return self.__class__.__name__
[docs]class AbstractAction(mguiLike): """ QAction encapsulated class with a similar API as magicgui Widget. This class makes it easier to combine QMenu to magicgui. """ changed = Signal(object) support_value: bool native: QAction | QWidgetAction @property def value(self): raise NotImplementedError()
[docs] def from_options(self, options): raise NotImplementedError()
[docs]class Action(AbstractAction): support_value = True def __init__( self, *args, name: str = None, text: str = None, gui_only: bool = True, **kwargs ): self.native = QAction(*args, **kwargs) self.mgui: FunctionGuiPlus = None self._doc = "" self._unwrapped = False self._icon_path = None if text: self.text = text if name: self.native.setObjectName(name) self._callbacks = [] self.native.triggered.connect(lambda: self.changed.emit(self.value)) @property def running(self) -> bool: return getattr(self.mgui, "running", False)
[docs] def set_shortcut(self, key): self.native.setShortcut(key)
[docs] def reset_choices(self, *_: Any): """Reset child Categorical widgets.""" if self.mgui is not None: self.mgui.reset_choices()
@property def text(self) -> str: return self.native.text() @text.setter def text(self, value: str): self.native.setText(value) @property def value(self): return self.native.isChecked() @value.setter def value(self, checked: bool): self.native.setChecked(checked) @property def icon_path(self): return self._icon_path @icon_path.setter def icon_path(self, path): path = str(path) icon = QIcon(path) self.native.setIcon(icon)
[docs] def from_options(self, options: dict[str] | Callable): if callable(options): try: options = options.__signature__.caller_options except AttributeError: return None for k, v in options.items(): if v is not None: setattr(self, k, v) return None
_W = TypeVar("_W", bound=Widget)
[docs]class WidgetAction(AbstractAction, Generic[_W]): def __init__(self, widget: _W, label: str = None, parent=None): if not isinstance(widget, (Widget, mguiLike)): raise TypeError( f"The first argument must be a magicgui-like widget, got {type(widget)}" ) self.native = QWidgetAction(parent) self.widget = widget name = widget.name self.native.setObjectName(name) self.label = label or name.replace("_", " ") self.text = getattr(widget, "text", None) or name.replace("_", " ") self.native.setDefaultWidget(widget.native) self.support_value = isinstance( widget, (ValueWidget, _LabeledWidget) ) or hasattr(widget, "value") if self.support_value: self.widget.changed.connect(lambda: self.changed.emit(self.value)) @property def widget_type(self): return self.widget.widget_type @property def value(self): if self.support_value: return self.widget.value else: msg = ( f"WidgetAction {self.name} has {type(self.widget)} as the default " "widget, which does not have value property." ) raise AttributeError(msg) @value.setter def value(self, v): if self.support_value: self.widget.value = v else: msg = ( f"WidgetAction {self.name} has {type(self.widget)} as the default " "widget, which does not have value property." ) raise AttributeError(msg) @property def text(self) -> str: return self.native.text() @text.setter def text(self, value: str): self.native.setText(value) if hasattr(self.widget, "text"): self.widget.text = value def _labeled_widget(self): return self.widget._labeled_widget()
[docs] def render(self): return self.widget.render()
def _repr_png_(self): return self.widget._repr_png_()
class _LabeledWidgetAction(WidgetAction): widget: _LabeledWidget def __init__(self, widget: Widget, label: str = None): if not isinstance(widget, Widget): raise TypeError(f"The first argument must be a Widget, got {type(widget)}") _labeled_widget = _LabeledWidget(widget, label) super().__init__(_labeled_widget) self.name = widget.name # Strangely, visible.setter does not work for sliders. widget.native.setVisible(True) @classmethod def from_action(cls, action: WidgetAction): """ Construct a labeled action using another action. """ self = cls(action.widget, action.label) action.parent = self return self @property def label_width(self): return self.widget._label_widget.width @label_width.setter def label_width(self, width): self.widget._label_widget.min_width = width def _to_rgb(color): if isinstance(color, str): from matplotlib.colors import to_rgb color = to_rgb(color) rgb = ",".join(str(max(min(int(c * 255), 255), 0)) for c in color) return f"rgb({rgb})" def _stylesheet_to_dict(stylesheet: str): if stylesheet == "": return {} lines = stylesheet.split(";") d = dict() for line in lines: k, v = line.split(":") d[k.strip()] = v.strip() return d def _dict_to_stylesheet(d: dict): stylesheet = [f"{k}: {v}" for k, v in d.items()] return ";".join(stylesheet)