Source code for magicclass.gui.mgui_ext
from __future__ import annotations
from typing import Callable, Iterable, Any
import re
from qtpy.QtWidgets import QPushButton, QAction, QWidgetAction
from qtpy.QtGui import QIcon
from qtpy.QtCore import QSize
from magicgui.events import Signal
from magicgui.widgets import Container, PushButton, FunctionGui
from magicgui.widgets._concrete import _LabeledWidget
from magicgui.widgets._bases import Widget, ValueWidget, ButtonWidget
from magicgui.widgets._function_gui import _function_name_pointing_to_widget
from matplotlib.colors import to_rgb
from ..widgets import Separator
# magicgui widgets that need to be extended to fit into magicclass
[docs]class FunctionGuiPlus(FunctionGui):
"""
FunctionGui class with a parameter recording functionality etc.
"""
def __call__(self, *args: Any, **kwargs: Any):
sig = self.__signature__
try:
bound = sig.bind(*args, **kwargs)
except TypeError as e:
if "missing a required argument" in str(e):
match = re.search("argument: '(.+)'", str(e))
missing = match.groups()[0] if match else "<param>"
msg = (
f"{e} in call to '{self._callable_name}{sig}'.\n"
"To avoid this error, you can bind a value or callback to the "
f"parameter:\n\n {self._callable_name}.{missing}.bind(value)"
"\n\nOr use the 'bind' option in the magicgui decorator:\n\n"
f" @magicgui({missing}={{'bind': value}})\n"
f" def {self._callable_name}{sig}: ..."
)
raise TypeError(msg) from None
else:
raise
bound.apply_defaults()
# 1. Parameter recording
# This is important when bound function set by {"bind": f} updates something.
# When the value is referred via "__signature__" the bound function get called
# and updated againg.
self._previous_bound = bound
self._tqdm_depth = 0 # reset the tqdm stack count
with _function_name_pointing_to_widget(self):
# 2. Running flag
# We sometimes want to know if the function is called programmatically or
# from GUI. The "running" argument is True only when it's called via GUI.
self.running = True
try:
value = self._function(*bound.args, **bound.kwargs)
finally:
self.running = False
self._call_count += 1
if self._result_widget is not None:
with self._result_widget.changed.blocked():
self._result_widget.value = value
return_type = sig.return_annotation
if return_type:
from magicgui.type_map import _type2callback
for callback in _type2callback(return_type):
callback(self, value, return_type)
self.called.emit(value)
return value
[docs] def insert(self, key: int, widget: Widget):
"""Insert widget at ``key``."""
if isinstance(widget, ValueWidget):
widget.changed.connect(lambda: self.changed.emit(self))
_widget = widget
if self.labels:
# no labels for button widgets (push buttons, checkboxes, have their own)
if not isinstance(widget, (_LabeledWidget, ButtonWidget, Separator)):
_widget = _LabeledWidget(widget)
widget.label_changed.connect(self._unify_label_widths)
self._list.insert(key, widget)
if key < 0:
key += len(self)
# NOTE: if someone has manually mucked around with self.native.layout()
# it's possible that indices will be off.
self._widget._mgui_insert_widget(key, _widget)
self._unify_label_widths()
[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: Container = None # tagged function GUI
@property
def running(self) -> bool:
return getattr(self.mgui, "running", False)
@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:str):
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():
v = options.get(k, None)
if v is not None:
setattr(self, k, v)
return None
[docs]class AbstractAction:
"""
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 name(self) -> str:
return self.native.objectName()
@name.setter
def name(self, value: str):
self.native.setObjectName(value)
@property
def value(self):
raise NotImplementedError()
@property
def tooltip(self) -> str:
return self.native.toolTip()
@tooltip.setter
def tooltip(self, value: str):
self.native.setToolTip(value)
@property
def enabled(self):
return self.native.isEnabled()
@enabled.setter
def enabled(self, value: bool):
self.native.setEnabled(value)
@property
def visible(self):
return self.native.isVisible()
@visible.setter
def visible(self, value: bool):
self.native.setVisible(value)
@property
def parent(self):
return self.native.parent()
@parent.setter
def parent(self, widget: Widget):
return self.native.setParent(widget.native if widget else None)
[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._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)
@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: str):
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():
v = options.get(k, None)
if v is not None:
setattr(self, k, v)
return None
[docs]class WidgetAction(AbstractAction):
def __init__(self, widget: Widget, name: str = None, label: str = None, gui_only: bool = True,
parent=None):
if not isinstance(widget, Widget):
raise TypeError(f"The first argument must be a Widget, got {type(widget)}")
self.native = QWidgetAction(parent)
name = name or ""
self.native.setObjectName(name)
self.label = label or name.replace("_", " ")
self.widget = widget
self.native.setDefaultWidget(widget.native)
self.support_value = hasattr(widget, "value")
self.widget_type = widget.widget_type
if self.support_value:
self.widget.changed.connect(lambda: self.changed.emit(self.value))
@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 width(self) -> int:
return self.widget.width
@width.setter
def width(self, value: int):
self.widget.width = value
@property
def max_width(self) -> int:
return self.widget.max_width
@max_width.setter
def max_width(self, value: int):
self.widget.max_width = value
@property
def min_width(self) -> int:
return self.widget.min_width
@min_width.setter
def min_width(self, value: int):
self.widget.min_width = value
def _labeled_widget(self) -> _LabeledWidget | None:
"""Return _LabeledWidget container, if applicable."""
return self.widget._labeled_widget()
# repr methods similar to magicgui's Widget
[docs] def render(self):
try:
import numpy as np
except ImportError:
raise ModuleNotFoundError(
"could not find module 'numpy'. "
"Please `pip install numpy` to render widgets."
) from None
import qtpy
img = self.widget.native.grab().toImage()
bits = img.constBits()
h, w, c = img.height(), img.width(), 4
if qtpy.API_NAME == "PySide2":
arr = np.array(bits).reshape(h, w, c)
else:
bits.setsize(h * w * c)
arr = np.frombuffer(bits, np.uint8).reshape(h, w, c)
return arr[:, :, [2, 1, 0, 3]]
def __repr__(self) -> str:
"""Return representation of widget of instsance."""
return f"{self.widget_type}(name={self.name!r}, widget={self.widget!r})"
def _repr_png_(self):
"""Return PNG representation of the widget for QtConsole."""
from io import BytesIO
try:
from imageio import imsave
except ImportError:
print(
"(For a nicer magicgui widget representation in "
"Jupyter, please `pip install imageio`)"
)
return None
with BytesIO() as file_obj:
imsave(file_obj, self.render(), format="png")
file_obj.seek(0)
return file_obj.read()
class _LabeledWidgetAction(WidgetAction):
def __init__(self, widget: Widget, name: str = None, label: str = None, gui_only: bool = True):
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, name)
self.widget: _LabeledWidget
# 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.name, 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):
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)