from __future__ import annotations
from typing import Callable, Iterable, Any, Generic, TypeVar
import re
from qtpy.QtWidgets import QPushButton, QAction, QWidgetAction, QToolButton, QWidget, QMenu
from qtpy.QtGui import QIcon
from qtpy.QtCore import QSize
from magicgui.events import Signal
from magicgui.widgets import PushButton, FunctionGui
from magicgui.widgets._concrete import _LabeledWidget
from magicgui.widgets._bases import Widget, ValueWidget, ButtonWidget, ContainerWidget
from magicgui.widgets._function_gui import _function_name_pointing_to_widget
from magicgui.backends._qtpy.widgets import QBaseButtonWidget
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 set_option decorator:\n\n"
f" @set_option({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, ContainerWidget)):
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()
class _QToolButton(QBaseButtonWidget):
def __init__(self):
super().__init__(QToolButton)
[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)
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):
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)