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 magicgui.events 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 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 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]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 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)
name = widget.name
self.native.setObjectName(name)
self.label = label or name.replace("_", " ")
self.text = getattr(widget, "text", self.label)
self.widget = widget
self.native.setDefaultWidget(widget.native)
self.support_value = 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)
def _labeled_widget(self):
return self.widget._labeled_widget()
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)