From b19e898a4c06c13361e3f54daa2ddcdc705a2356 Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Sun, 8 Oct 2023 20:28:24 -0400 Subject: [PATCH 01/16] wip --- src/magicgui/backends/_ipynb/__init__.py | 2 + src/magicgui/backends/_ipynb/widgets.py | 43 ++++++++++++++++ src/magicgui/backends/_qtpy/__init__.py | 2 + src/magicgui/backends/_qtpy/widgets.py | 44 ++++++++++++++++- src/magicgui/widgets/__init__.py | 2 + src/magicgui/widgets/_concrete.py | 6 +++ src/magicgui/widgets/bases/__init__.py | 4 +- src/magicgui/widgets/bases/_toolbar.py | 62 ++++++++++++++++++++++++ src/magicgui/widgets/protocols.py | 35 +++++++++++++ 9 files changed, 198 insertions(+), 2 deletions(-) create mode 100644 src/magicgui/widgets/bases/_toolbar.py diff --git a/src/magicgui/backends/_ipynb/__init__.py b/src/magicgui/backends/_ipynb/__init__.py index 1c82d18f2..b9f98e29a 100644 --- a/src/magicgui/backends/_ipynb/__init__.py +++ b/src/magicgui/backends/_ipynb/__init__.py @@ -19,6 +19,7 @@ SpinBox, TextEdit, TimeEdit, + ToolBar, get_text_width, ) @@ -46,6 +47,7 @@ "Slider", "SpinBox", "TextEdit", + "ToolBar", "get_text_width", "show_file_dialog", ] diff --git a/src/magicgui/backends/_ipynb/widgets.py b/src/magicgui/backends/_ipynb/widgets.py index 93af80acf..f3385b5dc 100644 --- a/src/magicgui/backends/_ipynb/widgets.py +++ b/src/magicgui/backends/_ipynb/widgets.py @@ -326,6 +326,49 @@ class TimeEdit(_IPyValueWidget): _ipywidget: ipywdg.TimePicker +class ToolBar(_IPyWidget): + _ipywidget: ipywidgets.HBox + + def __init__(self, **kwargs): + super().__init__(ipywidgets.HBox, **kwargs) + + def _mgui_add_button(self, text: str, icon: str, callback: Callable) -> None: + """Add an action to the toolbar.""" + btn = ipywdg.Button(description=text, icon=icon) + # btn.layout.width = "50px" + if callback: + btn.on_click(lambda e: callback()) + # self.actions[name] = _IpyAction(btn) + + children = list(self._ipywidget.children) + children.append(btn) + self._ipywidget.children = children + btn.parent = self._ipywidget + + def _mgui_add_separator(self) -> None: + """Add a separator line to the toolbar.""" + + def _mgui_add_spacer(self) -> None: + """Add a spacer to the toolbar.""" + + def _mgui_add_widget(self, widget: "Widget") -> None: + """Add a widget to the toolbar.""" + children = list(self._ipywidget.children) + children.append(widget.native) + self._ipywidget.children = children + widget.parent = self._ipywidget + + def _mgui_get_icon_size(self) -> int: + """Return the icon size of the toolbar.""" + + def _mgui_set_icon_size(self, width: int, height: int) -> None: + """Set the icon size of the toolbar.""" + + def _mgui_clear(self) -> None: + """Clear the toolbar.""" + self._ipywidget.children = () + + class PushButton(_IPyButtonWidget): _ipywidget: ipywdg.Button diff --git a/src/magicgui/backends/_qtpy/__init__.py b/src/magicgui/backends/_qtpy/__init__.py index 046859544..a42b100c3 100644 --- a/src/magicgui/backends/_qtpy/__init__.py +++ b/src/magicgui/backends/_qtpy/__init__.py @@ -28,6 +28,7 @@ Table, TextEdit, TimeEdit, + ToolBar, get_text_width, show_file_dialog, ) @@ -64,4 +65,5 @@ "Table", "TextEdit", "TimeEdit", + "ToolBar", ] diff --git a/src/magicgui/backends/_qtpy/widgets.py b/src/magicgui/backends/_qtpy/widgets.py index 71ce796b6..7deb9b7fc 100644 --- a/src/magicgui/backends/_qtpy/widgets.py +++ b/src/magicgui/backends/_qtpy/widgets.py @@ -13,7 +13,7 @@ import qtpy import superqt from qtpy import QtWidgets as QtW -from qtpy.QtCore import QEvent, QObject, Qt, Signal +from qtpy.QtCore import QEvent, QObject, QSize, Qt, Signal from qtpy.QtGui import ( QFont, QFontMetrics, @@ -1172,6 +1172,48 @@ def _mgui_get_value(self): return self._qwidget.time().toPyTime() +class ToolBar(QBaseWidget): + _qwidget: QtW.QToolBar + + def __init__(self, **kwargs: Any) -> None: + super().__init__(QtW.QToolBar, **kwargs) + + def _mgui_add_button(self, text: str, icon: str, callback: Callable) -> None: + """Add an action to the toolbar.""" + if icon: + self._qwidget.addAction(icon, text, callback) + else: + self._qwidget.addAction(text, callback) + + def _mgui_add_separator(self) -> None: + """Add a separator line to the toolbar.""" + self._qwidget.addSeparator() + + def _mgui_add_spacer(self) -> None: + """Add a spacer to the toolbar.""" + empty = QtW.QWidget() + empty.setSizePolicy( + QtW.QSizePolicy.Policy.Expanding, QtW.QSizePolicy.Policy.Preferred + ) + self._qwidget.addWidget(empty) + + def _mgui_add_widget(self, widget: Widget) -> None: + """Add a widget to the toolbar.""" + self._qwidget.addWidget(widget.native) + + def _mgui_get_icon_size(self) -> int: + """Return the icon size of the toolbar.""" + return self._qwidget.iconSize() + + def _mgui_set_icon_size(self, width: int, height: int) -> None: + """Set the icon size of the toolbar.""" + self._qwidget.setIconSize(QSize(width, height)) + + def _mgui_clear(self) -> None: + """Clear the toolbar.""" + self._qwidget.clear() + + class Dialog(QBaseWidget, protocols.ContainerProtocol): def __init__( self, layout="vertical", scrollable: bool = False, **kwargs: Any diff --git a/src/magicgui/widgets/__init__.py b/src/magicgui/widgets/__init__.py index ffc556ee3..2478afb0d 100644 --- a/src/magicgui/widgets/__init__.py +++ b/src/magicgui/widgets/__init__.py @@ -43,6 +43,7 @@ SpinBox, TextEdit, TimeEdit, + ToolBar, TupleEdit, ) from ._dialogs import request_values, show_file_dialog @@ -107,6 +108,7 @@ "Table", "TextEdit", "TimeEdit", + "ToolBar", "TupleEdit", "Widget", "show_file_dialog", diff --git a/src/magicgui/widgets/_concrete.py b/src/magicgui/widgets/_concrete.py index 820096f3f..b6546a877 100644 --- a/src/magicgui/widgets/_concrete.py +++ b/src/magicgui/widgets/_concrete.py @@ -46,6 +46,7 @@ MultiValuedSliderWidget, RangedWidget, SliderWidget, + ToolBarWidget, TransformedRangedWidget, ValueWidget, Widget, @@ -979,6 +980,11 @@ def value(self, vals: Sequence) -> None: self.changed.emit(self.value) +@backend_widget +class ToolBar(ToolBarWidget): + """Toolbar that contains a set of controls.""" + + class _LabeledWidget(Container): """Simple container that wraps a widget and provides a label.""" diff --git a/src/magicgui/widgets/bases/__init__.py b/src/magicgui/widgets/bases/__init__.py index dcda1c7de..176a12e9f 100644 --- a/src/magicgui/widgets/bases/__init__.py +++ b/src/magicgui/widgets/bases/__init__.py @@ -48,6 +48,7 @@ def __init__( from ._create_widget import create_widget from ._ranged_widget import RangedWidget, TransformedRangedWidget from ._slider_widget import MultiValuedSliderWidget, SliderWidget +from ._toolbar import ToolBarWidget from ._value_widget import ValueWidget from ._widget import Widget @@ -55,13 +56,14 @@ def __init__( "ButtonWidget", "CategoricalWidget", "ContainerWidget", + "create_widget", "DialogWidget", "MainWindowWidget", "MultiValuedSliderWidget", "RangedWidget", "SliderWidget", + "ToolBarWidget", "TransformedRangedWidget", "ValueWidget", "Widget", - "create_widget", ] diff --git a/src/magicgui/widgets/bases/_toolbar.py b/src/magicgui/widgets/bases/_toolbar.py new file mode 100644 index 000000000..1bba39f71 --- /dev/null +++ b/src/magicgui/widgets/bases/_toolbar.py @@ -0,0 +1,62 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, Callable, Tuple, TypeVar, Union + +from ._widget import Widget + +if TYPE_CHECKING: + from magicgui.widgets import protocols + +T = TypeVar("T", int, float, Tuple[Union[int, float], ...]) +DEFAULT_MIN = 0.0 +DEFAULT_MAX = 1000.0 + + +class ToolBarWidget(Widget): + """Widget with a value, Wraps ValueWidgetProtocol. + + Parameters + ---------- + **base_widget_kwargs : Any + All additional keyword arguments are passed to the base + [`magicgui.widgets.Widget`][magicgui.widgets.Widget] constructor. + """ + + _widget: protocols.ToolBarProtocol + + def __init__(self, **base_widget_kwargs: Any) -> None: + super().__init__(**base_widget_kwargs) + + def add_button( + self, text: str = "", icon: str = "", callback: Callable | None = None + ) -> None: + """Add an action to the toolbar.""" + self._widget._mgui_add_button(text, icon, callback) + + def add_separator(self) -> None: + """Add a separator line to the toolbar.""" + self._widget._mgui_add_separator() + + def add_spacer(self) -> None: + """Add a spacer to the toolbar.""" + self._widget._mgui_add_spacer() + + def add_widget(self, widget: Widget) -> None: + """Add a widget to the toolbar.""" + self._widget._mgui_add_widget(widget) + + def get_icon_size(self) -> int: + """Return the icon size of the toolbar.""" + return self._widget._mgui_get_icon_size() + + def set_icon_size(self, height: int, width: int | None = None) -> None: + """Set the icon size of the toolbar. + + If width is not provided, it will be set to height. + """ + width = height if width is None else width + self._widget._mgui_set_icon_size(width, height) + + def clear(self) -> None: + """Clear the toolbar.""" + self._widget._mgui_clear() diff --git a/src/magicgui/widgets/protocols.py b/src/magicgui/widgets/protocols.py index 316528173..3843245c0 100644 --- a/src/magicgui/widgets/protocols.py +++ b/src/magicgui/widgets/protocols.py @@ -501,6 +501,41 @@ def _mgui_set_margins(self, margins: tuple[int, int, int, int]) -> None: raise NotImplementedError() +@runtime_checkable +class ToolBarProtocol(WidgetProtocol, Protocol): + """Toolbar that contains a set of controls.""" + + @abstractmethod + def _mgui_add_button( + self, text: str, icon: str, callback: Callable | None = None + ) -> None: + """Add a button to the toolbar.""" + + @abstractmethod + def _mgui_add_separator(self) -> None: + """Add a separator line to the toolbar.""" + + @abstractmethod + def _mgui_add_spacer(self) -> None: + """Add a spacer to the toolbar.""" + + @abstractmethod + def _mgui_add_widget(self, widget: Widget) -> None: + """Add a widget to the toolbar.""" + + @abstractmethod + def _mgui_get_icon_size(self) -> int: + """Return the icon size of the toolbar.""" + + @abstractmethod + def _mgui_set_icon_size(self, width: int, height: int) -> None: + """Set the icon size of the toolbar.""" + + @abstractmethod + def _mgui_clear(self) -> None: + """Clear the toolbar.""" + + class DialogProtocol(ContainerProtocol, Protocol): """Protocol for modal (blocking) containers.""" From a2244de86517b41ebf89d825faba2ecea35f91b6 Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Mon, 9 Oct 2023 10:42:39 -0400 Subject: [PATCH 02/16] update ipywidgets implementation --- src/magicgui/backends/_ipynb/widgets.py | 40 ++++++++++++++++++------- src/magicgui/backends/_qtpy/widgets.py | 15 +++++++--- src/magicgui/widgets/bases/_toolbar.py | 14 ++++----- src/magicgui/widgets/protocols.py | 4 +-- 4 files changed, 48 insertions(+), 25 deletions(-) diff --git a/src/magicgui/backends/_ipynb/widgets.py b/src/magicgui/backends/_ipynb/widgets.py index f3385b5dc..ae01893e2 100644 --- a/src/magicgui/backends/_ipynb/widgets.py +++ b/src/magicgui/backends/_ipynb/widgets.py @@ -331,38 +331,56 @@ class ToolBar(_IPyWidget): def __init__(self, **kwargs): super().__init__(ipywidgets.HBox, **kwargs) + self._icon_sz: tuple[int, int] | None = None def _mgui_add_button(self, text: str, icon: str, callback: Callable) -> None: """Add an action to the toolbar.""" - btn = ipywdg.Button(description=text, icon=icon) - # btn.layout.width = "50px" + btn = ipywdg.Button( + description=text, icon=icon, layout={"width": "auto", "height": "auto"} + ) if callback: btn.on_click(lambda e: callback()) - # self.actions[name] = _IpyAction(btn) + self._add_ipywidget(btn) + def _add_ipywidget(self, widget: "ipywidgets.Widget") -> None: children = list(self._ipywidget.children) - children.append(btn) + children.append(widget) self._ipywidget.children = children - btn.parent = self._ipywidget def _mgui_add_separator(self) -> None: """Add a separator line to the toolbar.""" + # Define the vertical separator + sep = ipywdg.Box( + layout=ipywdg.Layout(border_left="1px dotted gray", margin="1px 4px") + ) + self._add_ipywidget(sep) def _mgui_add_spacer(self) -> None: """Add a spacer to the toolbar.""" + self._add_ipywidget(ipywdg.Box(layout=ipywdg.Layout(flex="1"))) def _mgui_add_widget(self, widget: "Widget") -> None: """Add a widget to the toolbar.""" - children = list(self._ipywidget.children) - children.append(widget.native) - self._ipywidget.children = children - widget.parent = self._ipywidget + self._add_ipywidget(widget.native) - def _mgui_get_icon_size(self) -> int: + def _mgui_get_icon_size(self) -> tuple[int, int] | None: """Return the icon size of the toolbar.""" + return self._icon_sz - def _mgui_set_icon_size(self, width: int, height: int) -> None: + def _mgui_set_icon_size(self, size: int | tuple[int, int] | None) -> None: """Set the icon size of the toolbar.""" + if isinstance(size, int): + size = (size, size) + elif size is None: + size = (0, 0) + elif not isinstance(size, tuple): + raise ValueError("icon size must be an int or tuple of ints") + sz = max(size) + self._icon_sz = (sz, sz) + for child in self._ipywidget.children: + if hasattr(child, "style"): + child.style.font_size = f"{sz}px" if sz else None + child.layout.min_height = f"{sz*2}px" if sz else None def _mgui_clear(self) -> None: """Clear the toolbar.""" diff --git a/src/magicgui/backends/_qtpy/widgets.py b/src/magicgui/backends/_qtpy/widgets.py index 7deb9b7fc..74ef38758 100644 --- a/src/magicgui/backends/_qtpy/widgets.py +++ b/src/magicgui/backends/_qtpy/widgets.py @@ -1201,13 +1201,20 @@ def _mgui_add_widget(self, widget: Widget) -> None: """Add a widget to the toolbar.""" self._qwidget.addWidget(widget.native) - def _mgui_get_icon_size(self) -> int: + def _mgui_get_icon_size(self) -> tuple[int, int] | None: """Return the icon size of the toolbar.""" - return self._qwidget.iconSize() + sz = self._qwidget.iconSize() + return None if sz.isNull() else (sz.width(), sz.height()) - def _mgui_set_icon_size(self, width: int, height: int) -> None: + def _mgui_set_icon_size(self, size: int | tuple[int, int] | None) -> None: """Set the icon size of the toolbar.""" - self._qwidget.setIconSize(QSize(width, height)) + if isinstance(size, int): + _size = QSize(size, size) + elif isinstance(size, tuple): + _size = QSize(size[0], size[1]) + else: + _size = QSize() + self._qwidget.setIconSize(_size) def _mgui_clear(self) -> None: """Clear the toolbar.""" diff --git a/src/magicgui/widgets/bases/_toolbar.py b/src/magicgui/widgets/bases/_toolbar.py index 1bba39f71..c8d87766e 100644 --- a/src/magicgui/widgets/bases/_toolbar.py +++ b/src/magicgui/widgets/bases/_toolbar.py @@ -45,17 +45,15 @@ def add_widget(self, widget: Widget) -> None: """Add a widget to the toolbar.""" self._widget._mgui_add_widget(widget) - def get_icon_size(self) -> int: + @property + def icon_size(self) -> tuple[int, int] | None: """Return the icon size of the toolbar.""" return self._widget._mgui_get_icon_size() - def set_icon_size(self, height: int, width: int | None = None) -> None: - """Set the icon size of the toolbar. - - If width is not provided, it will be set to height. - """ - width = height if width is None else width - self._widget._mgui_set_icon_size(width, height) + @icon_size.setter + def icon_size(self, size: int | tuple[int, int] | None) -> None: + """Set the icon size of the toolbar.""" + self._widget._mgui_set_icon_size(size) def clear(self) -> None: """Clear the toolbar.""" diff --git a/src/magicgui/widgets/protocols.py b/src/magicgui/widgets/protocols.py index 3843245c0..8c227954a 100644 --- a/src/magicgui/widgets/protocols.py +++ b/src/magicgui/widgets/protocols.py @@ -524,11 +524,11 @@ def _mgui_add_widget(self, widget: Widget) -> None: """Add a widget to the toolbar.""" @abstractmethod - def _mgui_get_icon_size(self) -> int: + def _mgui_get_icon_size(self) -> tuple[int, int] | None: """Return the icon size of the toolbar.""" @abstractmethod - def _mgui_set_icon_size(self, width: int, height: int) -> None: + def _mgui_set_icon_size(self, size: int | tuple[int, int] | None) -> None: """Set the icon size of the toolbar.""" @abstractmethod From bad9c5e931e4ef32e92fe056926847305a6efcec Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Mon, 9 Oct 2023 13:03:30 -0400 Subject: [PATCH 03/16] fix hints --- src/magicgui/backends/_ipynb/widgets.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/magicgui/backends/_ipynb/widgets.py b/src/magicgui/backends/_ipynb/widgets.py index ae01893e2..9a12f3138 100644 --- a/src/magicgui/backends/_ipynb/widgets.py +++ b/src/magicgui/backends/_ipynb/widgets.py @@ -331,7 +331,7 @@ class ToolBar(_IPyWidget): def __init__(self, **kwargs): super().__init__(ipywidgets.HBox, **kwargs) - self._icon_sz: tuple[int, int] | None = None + self._icon_sz: Tuple[int, int] | None = None def _mgui_add_button(self, text: str, icon: str, callback: Callable) -> None: """Add an action to the toolbar.""" @@ -363,11 +363,11 @@ def _mgui_add_widget(self, widget: "Widget") -> None: """Add a widget to the toolbar.""" self._add_ipywidget(widget.native) - def _mgui_get_icon_size(self) -> tuple[int, int] | None: + def _mgui_get_icon_size(self) -> Tuple[int, int] | None: """Return the icon size of the toolbar.""" return self._icon_sz - def _mgui_set_icon_size(self, size: int | tuple[int, int] | None) -> None: + def _mgui_set_icon_size(self, size: int | Tuple[int, int] | None) -> None: """Set the icon size of the toolbar.""" if isinstance(size, int): size = (size, size) From 5f75b469c40a67e58ebd6f84a2e23a529272b9a4 Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Mon, 9 Oct 2023 13:05:49 -0400 Subject: [PATCH 04/16] add test --- tests/test_widgets.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/test_widgets.py b/tests/test_widgets.py index 60abd1fc2..df224f64a 100644 --- a/tests/test_widgets.py +++ b/tests/test_widgets.py @@ -1045,3 +1045,14 @@ def test_float_slider_readout(): assert sld._widget._readout_widget.value() == 4 assert sld._widget._readout_widget.minimum() == 0.5 assert sld._widget._readout_widget.maximum() == 10.5 + + +def test_toolbar(): + tb = widgets.ToolBar() + tb.add_button("test", callback=lambda: None) + tb.add_separator() + tb.add_spacer() + tb.add_button("test2", callback=lambda: None) + tb.icon_size = 26 + assert tb.icon_size == (26, 26) + tb.clear() From 804da7676cb2673232c431d4f2ece7494dd21979 Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Mon, 9 Oct 2023 19:55:30 -0400 Subject: [PATCH 05/16] feat: support button icons --- src/magicgui/backends/_ipynb/widgets.py | 15 ++++++++++++++- src/magicgui/backends/_qtpy/widgets.py | 9 +++++++-- src/magicgui/widgets/bases/_button_widget.py | 7 +++++++ src/magicgui/widgets/protocols.py | 11 ++++++++++- 4 files changed, 38 insertions(+), 4 deletions(-) diff --git a/src/magicgui/backends/_ipynb/widgets.py b/src/magicgui/backends/_ipynb/widgets.py index 93af80acf..e42fd3c1a 100644 --- a/src/magicgui/backends/_ipynb/widgets.py +++ b/src/magicgui/backends/_ipynb/widgets.py @@ -260,11 +260,24 @@ def _mgui_get_text(self) -> str: return self._ipywidget.description +class _IPySupportsIcon(protocols.SupportsIcon): + """Widget that can show an icon.""" + + _ipywidget: ipywdg.Widget + + def _mgui_set_icon(self, value: str, color: str) -> None: + """Set icon.""" + # not all ipywidget buttons support icons (like checkboxes), + # but our button protocol does. + if hasattr(self._ipywidget, "icon"): + self._ipywidget.icon = value.replace("fa-", "") + + class _IPyCategoricalWidget(_IPyValueWidget, _IPySupportsChoices): pass -class _IPyButtonWidget(_IPyValueWidget, _IPySupportsText): +class _IPyButtonWidget(_IPyValueWidget, _IPySupportsText, _IPySupportsIcon): pass diff --git a/src/magicgui/backends/_qtpy/widgets.py b/src/magicgui/backends/_qtpy/widgets.py index 71ce796b6..2872d7b6a 100644 --- a/src/magicgui/backends/_qtpy/widgets.py +++ b/src/magicgui/backends/_qtpy/widgets.py @@ -419,10 +419,12 @@ def _update_precision(self, **kwargs: Any) -> None: # BUTTONS -class QBaseButtonWidget(QBaseValueWidget, protocols.SupportsText): +class QBaseButtonWidget( + QBaseValueWidget, protocols.SupportsText, protocols.SupportsIcon +): _qwidget: QtW.QCheckBox | QtW.QPushButton | QtW.QRadioButton | QtW.QToolButton - def __init__(self, qwidg: type[QtW.QWidget], **kwargs: Any) -> None: + def __init__(self, qwidg: type[QtW.QAbstractButton], **kwargs: Any) -> None: super().__init__(qwidg, "isChecked", "setChecked", "toggled", **kwargs) def _mgui_set_text(self, value: str) -> None: @@ -433,6 +435,9 @@ def _mgui_get_text(self) -> str: """Get text.""" return self._qwidget.text() + def _mgui_set_icon(self, value: str, color: str | None) -> None: + self._qwidget.setIcon(superqt.QIconifyIcon(value, color=color)) + class PushButton(QBaseButtonWidget): def __init__(self, **kwargs: Any) -> None: diff --git a/src/magicgui/widgets/bases/_button_widget.py b/src/magicgui/widgets/bases/_button_widget.py index 52c2b2e2b..64d3bd8d5 100644 --- a/src/magicgui/widgets/bases/_button_widget.py +++ b/src/magicgui/widgets/bases/_button_widget.py @@ -48,6 +48,8 @@ def __init__( value: bool | _Undefined = Undefined, *, text: str | None = None, + icon: str | None = None, + icon_color: str | None = None, bind: bool | Callable[[ValueWidget], bool] | _Undefined = Undefined, nullable: bool = False, **base_widget_kwargs: Any, @@ -68,6 +70,8 @@ def __init__( value=value, bind=bind, nullable=nullable, **base_widget_kwargs ) self.text = (text or self.name).replace("_", " ") + if icon: + self.set_icon(icon, icon_color) @property def options(self) -> dict: @@ -89,3 +93,6 @@ def text(self, value: str) -> None: def clicked(self) -> SignalInstance: """Alias for changed event.""" return self.changed + + def set_icon(self, value: str, color: str | None) -> None: + self._widget._mgui_set_icon(str(value), color) diff --git a/src/magicgui/widgets/protocols.py b/src/magicgui/widgets/protocols.py index 316528173..57c1ae440 100644 --- a/src/magicgui/widgets/protocols.py +++ b/src/magicgui/widgets/protocols.py @@ -438,7 +438,16 @@ def _mgui_get_text(self) -> str: @runtime_checkable -class ButtonWidgetProtocol(ValueWidgetProtocol, SupportsText, Protocol): +class SupportsIcon(Protocol): + """Widget that can be reoriented.""" + + @abstractmethod + def _mgui_set_icon(self, value: str, color: str | None) -> None: + """Set icon. Value is a font-awesome v5 icon name.""" + + +@runtime_checkable +class ButtonWidgetProtocol(ValueWidgetProtocol, SupportsText, SupportsIcon, Protocol): """The "value" in a ButtonWidget is the current (checked) state.""" From c991ce0fd92f62d858f58db3738ef1705c672b85 Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Mon, 9 Oct 2023 20:57:03 -0400 Subject: [PATCH 06/16] adding iconbtn --- src/magicgui/backends/_ipynb/widgets.py | 45 +++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/src/magicgui/backends/_ipynb/widgets.py b/src/magicgui/backends/_ipynb/widgets.py index e42fd3c1a..f6914e7b0 100644 --- a/src/magicgui/backends/_ipynb/widgets.py +++ b/src/magicgui/backends/_ipynb/widgets.py @@ -9,6 +9,7 @@ "Please run `pip install ipywidgets`" ) from e + from magicgui.widgets import protocols from magicgui.widgets.bases import Widget @@ -277,6 +278,50 @@ class _IPyCategoricalWidget(_IPyValueWidget, _IPySupportsChoices): pass +class IconButton(ipywdg.HTML): + TEMPLATE = """ + + """ + + def __init__(self, description: str = "", icon: str = "", **kwargs): + from pyconify import css + + selector = f"{icon.replace(' ', '--').replace(':', '--')}" + styles = css(icon, selector=f".{selector}") + styles = styles.replace("}", "margin: 0px 3px -2px}") + value = self.TEMPLATE.format( + body=f'{description}', style=styles + ) + super().__init__(value=value, **kwargs) + self._click_handlers = ipywdg.CallbackDispatcher() + self.on_msg(self._handle_button_msg) + + def on_click(self, callback, remove=False): + """Register a callback to execute when the button is clicked. + + The callback will be called with one argument, the clicked button + widget instance. + + Parameters + ---------- + remove: bool (optional) + Set to true to remove the callback from the list of callbacks. + """ + self._click_handlers.register_callback(callback, remove=remove) + + def _handle_button_msg(self, _, content, buffers): + """Handle a msg from the front-end. + + Parameters + ---------- + content: dict + Content of the msg. + """ + if content.get("event", "") == "click": + self._click_handlers(self) + + class _IPyButtonWidget(_IPyValueWidget, _IPySupportsText, _IPySupportsIcon): pass From b3a29ecb9a644afa31a0151868e81c293c675531 Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Tue, 10 Oct 2023 12:47:00 -0400 Subject: [PATCH 07/16] match color to palette in qt --- src/magicgui/backends/_qtpy/widgets.py | 38 +++++++++++++++++--- src/magicgui/widgets/bases/_button_widget.py | 4 +-- src/magicgui/widgets/protocols.py | 9 +++-- 3 files changed, 42 insertions(+), 9 deletions(-) diff --git a/src/magicgui/backends/_qtpy/widgets.py b/src/magicgui/backends/_qtpy/widgets.py index 2872d7b6a..8aff10df7 100644 --- a/src/magicgui/backends/_qtpy/widgets.py +++ b/src/magicgui/backends/_qtpy/widgets.py @@ -17,8 +17,10 @@ from qtpy.QtGui import ( QFont, QFontMetrics, + QIcon, QImage, QKeyEvent, + QPalette, QPixmap, QResizeEvent, QTextDocument, @@ -48,10 +50,13 @@ def _signals_blocked(obj: QtW.QWidget) -> Iterator[None]: class EventFilter(QObject): parentChanged = Signal() valueChanged = Signal(object) + paletteChanged = Signal() def eventFilter(self, obj: QObject, event: QEvent) -> bool: if event.type() == QEvent.Type.ParentChange: self.parentChanged.emit() + if event.type() == QEvent.Type.PaletteChange: + self.paletteChanged.emit() return False @@ -426,6 +431,8 @@ class QBaseButtonWidget( def __init__(self, qwidg: type[QtW.QAbstractButton], **kwargs: Any) -> None: super().__init__(qwidg, "isChecked", "setChecked", "toggled", **kwargs) + self._event_filter.paletteChanged.connect(self._update_icon) + self._icon: tuple[str | None, str | None] | None = None def _mgui_set_text(self, value: str) -> None: """Set text.""" @@ -435,15 +442,36 @@ def _mgui_get_text(self) -> str: """Get text.""" return self._qwidget.text() - def _mgui_set_icon(self, value: str, color: str | None) -> None: - self._qwidget.setIcon(superqt.QIconifyIcon(value, color=color)) + def _update_icon(self) -> None: + # Called when palette changes or icon is set + if self._icon is None: + return + + value, color = self._icon + if not value: + self._qwidget.setIcon(QIcon()) + return + + if not color or color == "auto": + # use foreground color + pal = self._qwidget.palette() + color = pal.color(QPalette.ColorRole.WindowText).name() + + try: + self._qwidget.setIcon(superqt.QIconifyIcon(value, color=color)) + except (OSError, ValueError) as e: + warnings.warn(f"Could not set iconify icon: {e}", stacklevel=2) + self._icon = None # don't try again + + def _mgui_set_icon(self, value: str | None, color: str | None) -> None: + self._icon = (value, color) + self._update_icon() class PushButton(QBaseButtonWidget): def __init__(self, **kwargs: Any) -> None: - QBaseValueWidget.__init__( - self, QtW.QPushButton, "isChecked", "setChecked", "clicked", **kwargs - ) + super().__init__(QtW.QPushButton, **kwargs) + self._onchange_name = "clicked" # make enter/return "click" the button when focused. self._qwidget.setAutoDefault(True) diff --git a/src/magicgui/widgets/bases/_button_widget.py b/src/magicgui/widgets/bases/_button_widget.py index 64d3bd8d5..fe1a5833c 100644 --- a/src/magicgui/widgets/bases/_button_widget.py +++ b/src/magicgui/widgets/bases/_button_widget.py @@ -94,5 +94,5 @@ def clicked(self) -> SignalInstance: """Alias for changed event.""" return self.changed - def set_icon(self, value: str, color: str | None) -> None: - self._widget._mgui_set_icon(str(value), color) + def set_icon(self, value: str | None, color: str | None = None) -> None: + self._widget._mgui_set_icon(value, color) diff --git a/src/magicgui/widgets/protocols.py b/src/magicgui/widgets/protocols.py index 57c1ae440..c730ea4d2 100644 --- a/src/magicgui/widgets/protocols.py +++ b/src/magicgui/widgets/protocols.py @@ -442,8 +442,13 @@ class SupportsIcon(Protocol): """Widget that can be reoriented.""" @abstractmethod - def _mgui_set_icon(self, value: str, color: str | None) -> None: - """Set icon. Value is a font-awesome v5 icon name.""" + def _mgui_set_icon(self, value: str | None, color: str | None) -> None: + """Set icon. + + Value is an "prefix:name" from iconify: https://icon-sets.iconify.design + Color is any valid CSS color string. + Set value to `None` or an empty string to remove icon. + """ @runtime_checkable From d48b10dc47013498a38b859535f1453c17d4b941 Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Tue, 10 Oct 2023 13:10:19 -0400 Subject: [PATCH 08/16] update ipywidgets --- src/magicgui/backends/_ipynb/widgets.py | 59 +++++-------------------- 1 file changed, 11 insertions(+), 48 deletions(-) diff --git a/src/magicgui/backends/_ipynb/widgets.py b/src/magicgui/backends/_ipynb/widgets.py index f6914e7b0..0aecfd8f1 100644 --- a/src/magicgui/backends/_ipynb/widgets.py +++ b/src/magicgui/backends/_ipynb/widgets.py @@ -264,64 +264,27 @@ def _mgui_get_text(self) -> str: class _IPySupportsIcon(protocols.SupportsIcon): """Widget that can show an icon.""" - _ipywidget: ipywdg.Widget + _ipywidget: ipywdg.Button def _mgui_set_icon(self, value: str, color: str) -> None: """Set icon.""" - # not all ipywidget buttons support icons (like checkboxes), - # but our button protocol does. + # only ipywdg.Button actually supports icons. + # but our button protocol allows it for all buttons subclasses + # so we need this method in the concrete subclasses, but we + # can't actually set the icon for anything but ipywdg.Button if hasattr(self._ipywidget, "icon"): - self._ipywidget.icon = value.replace("fa-", "") + # by splitting on ":" we allow for "prefix:icon-name" syntax + # which works for iconify icons served by qt, while still + # allowing for bare "icon-name" syntax which works for ipywidgets. + # note however... only fa4/5 icons will work for ipywidgets. + self._ipywidget.icon = value.replace("fa-", "").split(":", 1)[-1] + self._ipywidget.style.text_color = color class _IPyCategoricalWidget(_IPyValueWidget, _IPySupportsChoices): pass -class IconButton(ipywdg.HTML): - TEMPLATE = """ - - """ - - def __init__(self, description: str = "", icon: str = "", **kwargs): - from pyconify import css - - selector = f"{icon.replace(' ', '--').replace(':', '--')}" - styles = css(icon, selector=f".{selector}") - styles = styles.replace("}", "margin: 0px 3px -2px}") - value = self.TEMPLATE.format( - body=f'{description}', style=styles - ) - super().__init__(value=value, **kwargs) - self._click_handlers = ipywdg.CallbackDispatcher() - self.on_msg(self._handle_button_msg) - - def on_click(self, callback, remove=False): - """Register a callback to execute when the button is clicked. - - The callback will be called with one argument, the clicked button - widget instance. - - Parameters - ---------- - remove: bool (optional) - Set to true to remove the callback from the list of callbacks. - """ - self._click_handlers.register_callback(callback, remove=remove) - - def _handle_button_msg(self, _, content, buffers): - """Handle a msg from the front-end. - - Parameters - ---------- - content: dict - Content of the msg. - """ - if content.get("event", "") == "click": - self._click_handlers(self) - - class _IPyButtonWidget(_IPyValueWidget, _IPySupportsText, _IPySupportsIcon): pass From a6652765c0851eb6f9e90913c5409b90d7be3a92 Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Tue, 10 Oct 2023 13:15:14 -0400 Subject: [PATCH 09/16] bump superqt --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index e3a292e2f..ed95b1516 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,7 +38,7 @@ dependencies = [ "docstring_parser>=0.7", "psygnal>=0.5.0", "qtpy>=1.7.0", - "superqt>=0.5.0", + "superqt[iconify]>=0.6.1", "typing_extensions", ] @@ -49,7 +49,7 @@ min-req = [ "docstring_parser==0.7", "psygnal==0.5.0", "qtpy==1.7.0", - "superqt==0.5.0", + "superqt==0.6.1", "typing_extensions", ] From 2c1a20186da8fd79764009e6edd9aa7f789e4016 Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Tue, 10 Oct 2023 14:03:52 -0400 Subject: [PATCH 10/16] add pytest-pretty --- .github/workflows/test_and_deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test_and_deploy.yml b/.github/workflows/test_and_deploy.yml index 86a25a443..f5fa85af2 100644 --- a/.github/workflows/test_and_deploy.yml +++ b/.github/workflows/test_and_deploy.yml @@ -98,7 +98,7 @@ jobs: python-version: "3.10" - name: Install run: | - python -m pip install --upgrade pip + python -m pip install --upgrade pip pytest-pretty python -m pip install -e .[testing] python -m pip install -e ./napari-from-github[pyqt5] From 5d1b2279cc8afe073227f6e36af867d6d14db126 Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Tue, 10 Oct 2023 15:00:02 -0400 Subject: [PATCH 11/16] test: add tests --- src/magicgui/backends/_ipynb/widgets.py | 3 ++- src/magicgui/backends/_qtpy/widgets.py | 5 +++++ tests/test_widgets.py | 11 +++++++++++ 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/magicgui/backends/_ipynb/widgets.py b/src/magicgui/backends/_ipynb/widgets.py index 0aecfd8f1..81593d453 100644 --- a/src/magicgui/backends/_ipynb/widgets.py +++ b/src/magicgui/backends/_ipynb/widgets.py @@ -266,7 +266,7 @@ class _IPySupportsIcon(protocols.SupportsIcon): _ipywidget: ipywdg.Button - def _mgui_set_icon(self, value: str, color: str) -> None: + def _mgui_set_icon(self, value: str | None, color: str | None) -> None: """Set icon.""" # only ipywdg.Button actually supports icons. # but our button protocol allows it for all buttons subclasses @@ -277,6 +277,7 @@ def _mgui_set_icon(self, value: str, color: str) -> None: # which works for iconify icons served by qt, while still # allowing for bare "icon-name" syntax which works for ipywidgets. # note however... only fa4/5 icons will work for ipywidgets. + value = value or "" self._ipywidget.icon = value.replace("fa-", "").split(":", 1)[-1] self._ipywidget.style.text_color = color diff --git a/src/magicgui/backends/_qtpy/widgets.py b/src/magicgui/backends/_qtpy/widgets.py index 8aff10df7..e7f8259f1 100644 --- a/src/magicgui/backends/_qtpy/widgets.py +++ b/src/magicgui/backends/_qtpy/widgets.py @@ -457,6 +457,11 @@ def _update_icon(self) -> None: pal = self._qwidget.palette() color = pal.color(QPalette.ColorRole.WindowText).name() + if ":" not in value: + # for parity with the other backends, assume fontawesome + # if no prefix is given. + value = f"fa-regular:{value}" + try: self._qwidget.setIcon(superqt.QIconifyIcon(value, color=color)) except (OSError, ValueError) as e: diff --git a/tests/test_widgets.py b/tests/test_widgets.py index 60abd1fc2..f2fc9c772 100644 --- a/tests/test_widgets.py +++ b/tests/test_widgets.py @@ -809,6 +809,17 @@ def test_pushbutton_click_signal(): mock2.assert_called_once() +def test_pushbutton_icon(backend: str): + use_app(backend) + btn = widgets.PushButton(icon="mdi:folder") + btn.set_icon("smile", "red") + btn.set_icon(None) + + if backend == "qt": + with pytest.warns(UserWarning, match="Could not set iconify icon"): + btn.set_icon("bad:key") + + def test_list_edit(): """Test ListEdit.""" from typing import List From 1b907d1c2b5d915facf47b49ac9f8654656ff171 Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Tue, 10 Oct 2023 16:06:46 -0400 Subject: [PATCH 12/16] fix icon --- src/magicgui/backends/_qtpy/widgets.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/magicgui/backends/_qtpy/widgets.py b/src/magicgui/backends/_qtpy/widgets.py index 74ef38758..7838121d2 100644 --- a/src/magicgui/backends/_qtpy/widgets.py +++ b/src/magicgui/backends/_qtpy/widgets.py @@ -1181,9 +1181,16 @@ def __init__(self, **kwargs: Any) -> None: def _mgui_add_button(self, text: str, icon: str, callback: Callable) -> None: """Add an action to the toolbar.""" if icon: - self._qwidget.addAction(icon, text, callback) - else: - self._qwidget.addAction(text, callback) + from superqt import QIconifyIcon + + try: + qicon = QIconifyIcon(icon) + self._qwidget.addAction(qicon, text, callback) + return + except (OSError, ValueError) as e: + warnings.warn(f"Could not load icon: {e}", stacklevel=2) + + self._qwidget.addAction(text, callback) def _mgui_add_separator(self) -> None: """Add a separator line to the toolbar.""" From 6933b3275226768739a59afff8009f99ec7c8a11 Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Tue, 10 Oct 2023 16:18:41 -0400 Subject: [PATCH 13/16] extract logic, fix 3.8 --- src/magicgui/backends/_ipynb/widgets.py | 4 +- src/magicgui/backends/_qtpy/widgets.py | 49 ++++++++++++++----------- 2 files changed, 30 insertions(+), 23 deletions(-) diff --git a/src/magicgui/backends/_ipynb/widgets.py b/src/magicgui/backends/_ipynb/widgets.py index 81593d453..58f9971db 100644 --- a/src/magicgui/backends/_ipynb/widgets.py +++ b/src/magicgui/backends/_ipynb/widgets.py @@ -1,3 +1,5 @@ +# from __future__ import annotations # NO + from typing import Any, Callable, Iterable, Optional, Tuple, Type, Union try: @@ -266,7 +268,7 @@ class _IPySupportsIcon(protocols.SupportsIcon): _ipywidget: ipywdg.Button - def _mgui_set_icon(self, value: str | None, color: str | None) -> None: + def _mgui_set_icon(self, value: Optional[str], color: Optional[str]) -> None: """Set icon.""" # only ipywdg.Button actually supports icons. # but our button protocol allows it for all buttons subclasses diff --git a/src/magicgui/backends/_qtpy/widgets.py b/src/magicgui/backends/_qtpy/widgets.py index e7f8259f1..093033197 100644 --- a/src/magicgui/backends/_qtpy/widgets.py +++ b/src/magicgui/backends/_qtpy/widgets.py @@ -444,33 +444,38 @@ def _mgui_get_text(self) -> str: def _update_icon(self) -> None: # Called when palette changes or icon is set - if self._icon is None: - return + if self._icon: + qicon = _get_qicon(*self._icon, palette=self._qwidget.palette()) + if qicon is None: + self._icon = None # an error occurred don't try again + self._qwidget.setIcon(QIcon()) + else: + self._qwidget.setIcon(qicon) - value, color = self._icon - if not value: - self._qwidget.setIcon(QIcon()) - return + def _mgui_set_icon(self, value: str | None, color: str | None) -> None: + self._icon = (value, color) + self._update_icon() - if not color or color == "auto": - # use foreground color - pal = self._qwidget.palette() - color = pal.color(QPalette.ColorRole.WindowText).name() - if ":" not in value: - # for parity with the other backends, assume fontawesome - # if no prefix is given. - value = f"fa-regular:{value}" +def _get_qicon(key: str | None, color: str | None, palette: QPalette) -> QIcon | None: + """Return a QIcon from iconify, or None if it fails.""" + if not key: + return QIcon() - try: - self._qwidget.setIcon(superqt.QIconifyIcon(value, color=color)) - except (OSError, ValueError) as e: - warnings.warn(f"Could not set iconify icon: {e}", stacklevel=2) - self._icon = None # don't try again + if not color or color == "auto": + # use foreground color + color = palette.color(QPalette.ColorRole.WindowText).name() - def _mgui_set_icon(self, value: str | None, color: str | None) -> None: - self._icon = (value, color) - self._update_icon() + if ":" not in key: + # for parity with the other backends, assume fontawesome + # if no prefix is given. + key = f"fa-regular:{key}" + + try: + return superqt.QIconifyIcon(key, color=color) + except (OSError, ValueError) as e: + warnings.warn(f"Could not set iconify icon: {e}", stacklevel=2) + return None class PushButton(QBaseButtonWidget): From d27c283decb725fcf30d524f0c52975b40765b31 Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Tue, 10 Oct 2023 16:35:29 -0400 Subject: [PATCH 14/16] update color --- src/magicgui/backends/_qtpy/widgets.py | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/src/magicgui/backends/_qtpy/widgets.py b/src/magicgui/backends/_qtpy/widgets.py index 5c27bb75a..4fffda7f7 100644 --- a/src/magicgui/backends/_qtpy/widgets.py +++ b/src/magicgui/backends/_qtpy/widgets.py @@ -465,11 +465,13 @@ def _get_qicon(key: str | None, color: str | None, palette: QPalette) -> QIcon | if not color or color == "auto": # use foreground color color = palette.color(QPalette.ColorRole.WindowText).name() + # don't use full black or white + color = {"#000000": "#333333", "#ffffff": "#cccccc"}.get(color, color) if ":" not in key: # for parity with the other backends, assume fontawesome # if no prefix is given. - key = f"fa-regular:{key}" + key = f"fa:{key}" try: return superqt.QIconifyIcon(key, color=color) @@ -1223,17 +1225,10 @@ def __init__(self, **kwargs: Any) -> None: def _mgui_add_button(self, text: str, icon: str, callback: Callable) -> None: """Add an action to the toolbar.""" - if icon: - from superqt import QIconifyIcon - - try: - qicon = QIconifyIcon(icon) - self._qwidget.addAction(qicon, text, callback) - return - except (OSError, ValueError) as e: - warnings.warn(f"Could not load icon: {e}", stacklevel=2) - - self._qwidget.addAction(text, callback) + if qicon := _get_qicon(icon, None, palette=self._qwidget.palette()): + self._qwidget.addAction(qicon, text, callback) + else: + self._qwidget.addAction(text, callback) def _mgui_add_separator(self) -> None: """Add a separator line to the toolbar.""" From 62daa67539840bb54abe4a7c4ad59d30e2d9d670 Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Tue, 10 Oct 2023 16:53:28 -0400 Subject: [PATCH 15/16] change with palette --- src/magicgui/backends/_qtpy/widgets.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/magicgui/backends/_qtpy/widgets.py b/src/magicgui/backends/_qtpy/widgets.py index 4fffda7f7..a94bd3665 100644 --- a/src/magicgui/backends/_qtpy/widgets.py +++ b/src/magicgui/backends/_qtpy/widgets.py @@ -1222,13 +1222,21 @@ class ToolBar(QBaseWidget): def __init__(self, **kwargs: Any) -> None: super().__init__(QtW.QToolBar, **kwargs) + self._qwidget.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonTextUnderIcon) + self._event_filter.paletteChanged.connect(self._on_palette_change) + + def _on_palette_change(self): + for action in self._qwidget.actions(): + if icon := action.data(): + if qicon := _get_qicon(icon, None, palette=self._qwidget.palette()): + action.setIcon(qicon) def _mgui_add_button(self, text: str, icon: str, callback: Callable) -> None: """Add an action to the toolbar.""" + act = self._qwidget.addAction(text, callback) if qicon := _get_qicon(icon, None, palette=self._qwidget.palette()): - self._qwidget.addAction(qicon, text, callback) - else: - self._qwidget.addAction(text, callback) + act.setIcon(qicon) + act.setData(icon) def _mgui_add_separator(self) -> None: """Add a separator line to the toolbar.""" From 7f39313e052989e104f8f3dde215972c315adf63 Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Wed, 11 Oct 2023 17:19:21 -0400 Subject: [PATCH 16/16] unions --- src/magicgui/backends/_ipynb/widgets.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/magicgui/backends/_ipynb/widgets.py b/src/magicgui/backends/_ipynb/widgets.py index 3afe28a79..84928691d 100644 --- a/src/magicgui/backends/_ipynb/widgets.py +++ b/src/magicgui/backends/_ipynb/widgets.py @@ -355,7 +355,7 @@ class ToolBar(_IPyWidget): def __init__(self, **kwargs): super().__init__(ipywidgets.HBox, **kwargs) - self._icon_sz: Tuple[int, int] | None = None + self._icon_sz: Optional[Tuple[int, int]] = None def _mgui_add_button(self, text: str, icon: str, callback: Callable) -> None: """Add an action to the toolbar.""" @@ -387,11 +387,11 @@ def _mgui_add_widget(self, widget: "Widget") -> None: """Add a widget to the toolbar.""" self._add_ipywidget(widget.native) - def _mgui_get_icon_size(self) -> Tuple[int, int] | None: + def _mgui_get_icon_size(self) -> Optional[Tuple[int, int]]: """Return the icon size of the toolbar.""" return self._icon_sz - def _mgui_set_icon_size(self, size: int | Tuple[int, int] | None) -> None: + def _mgui_set_icon_size(self, size: Union[int, Tuple[int, int], None]) -> None: """Set the icon size of the toolbar.""" if isinstance(size, int): size = (size, size)