From b19e898a4c06c13361e3f54daa2ddcdc705a2356 Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Sun, 8 Oct 2023 20:28:24 -0400 Subject: [PATCH 01/32] 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/32] 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/32] 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/32] 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/32] 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/32] 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/32] 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/32] 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/32] 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/32] 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/32] 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/32] 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/32] 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/32] 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/32] 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/32] 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) From ff3b0012e17c1521744c0a82ae6aa5c188040516 Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Wed, 18 Oct 2023 16:39:36 -0400 Subject: [PATCH 17/32] wip --- src/magicgui/application.py | 18 ++- src/magicgui/backends/_ipynb/__init__.py | 2 + src/magicgui/backends/_ipynb/widgets.py | 112 +++++++++++++++++- .../widgets/bases/_container_widget.py | 48 ++++++++ src/magicgui/widgets/protocols.py | 19 +++ x.py | 10 ++ 6 files changed, 205 insertions(+), 4 deletions(-) create mode 100644 x.py diff --git a/src/magicgui/application.py b/src/magicgui/application.py index 9df1e3e91..a564acb50 100644 --- a/src/magicgui/application.py +++ b/src/magicgui/application.py @@ -2,7 +2,7 @@ from __future__ import annotations import signal -from contextlib import contextmanager +from contextlib import contextmanager, suppress from importlib import import_module from typing import TYPE_CHECKING, Any, Callable, Iterator, Union @@ -12,10 +12,22 @@ from types import ModuleType from magicgui.widgets.protocols import BaseApplicationBackend -DEFAULT_BACKEND = "qt" APPLICATION_NAME = "magicgui" +def _in_jupyter() -> bool: + """Return true if we're running in jupyter notebook/lab or qtconsole.""" + with suppress(ImportError): + from IPython import get_ipython + + return get_ipython().__class__.__name__ == "ZMQInteractiveShell" + return False + + +def _choose_backend(): + return "ipynb" if _in_jupyter() else "qt" + + @contextmanager def event_loop(backend: str | None = None) -> Iterator[Application]: """Start an event loop in which to run the application.""" @@ -49,7 +61,7 @@ def backend_module(self) -> ModuleType: def _use(self, backend_name: str | None = None) -> None: """Select a backend by name.""" if not backend_name: - backend_name = DEFAULT_BACKEND + backend_name = _choose_backend() if not backend_name or backend_name.lower() not in BACKENDS: raise ValueError( f"backend_name must be one of {set(BACKENDS)!r}, " diff --git a/src/magicgui/backends/_ipynb/__init__.py b/src/magicgui/backends/_ipynb/__init__.py index 1c82d18f2..780f6b270 100644 --- a/src/magicgui/backends/_ipynb/__init__.py +++ b/src/magicgui/backends/_ipynb/__init__.py @@ -11,6 +11,7 @@ Label, LineEdit, LiteralEvalLineEdit, + MainWindow, Password, PushButton, RadioButton, @@ -39,6 +40,7 @@ "Label", "LineEdit", "LiteralEvalLineEdit", + "MainWindow", "Password", "PushButton", "RadioButton", diff --git a/src/magicgui/backends/_ipynb/widgets.py b/src/magicgui/backends/_ipynb/widgets.py index 58f9971db..ec0d1fea0 100644 --- a/src/magicgui/backends/_ipynb/widgets.py +++ b/src/magicgui/backends/_ipynb/widgets.py @@ -1,6 +1,6 @@ # from __future__ import annotations # NO -from typing import Any, Callable, Iterable, Optional, Tuple, Type, Union +from typing import Any, Callable, Iterable, Literal, Optional, Tuple, Type, Union try: import ipywidgets @@ -463,6 +463,116 @@ def _mgui_get_orientation(self) -> str: return "vertical" if isinstance(self._ipywidget, ipywdg.VBox) else "horizontal" +from ipywidgets import Button, GridspecLayout, Layout + + +class IpyMainWindow(GridspecLayout): + IDX_MENUBAR = (0, slice(None)) + IDX_STATUSBAR = (6, slice(None)) + IDX_TOOLBAR_TOP = (1, slice(None)) + IDX_TOOLBAR_BOTTOM = (5, slice(None)) + IDX_TOOLBAR_LEFT = (slice(2, 5), 0) + IDX_TOOLBAR_RIGHT = (slice(2, 5), 4) + IDX_DOCK_TOP = (2, slice(1, 4)) + IDX_DOCK_BOTTOM = (4, slice(1, 4)) + IDX_DOCK_LEFT = (3, 1) + IDX_DOCK_RIGHT = (3, 3) + IDX_CENTRAL_WIDGET = (3, 2) + + def __init__(self, **kwargs): + n_rows = 7 + n_columns = 5 + kwargs.setdefault("width", "600px") + kwargs.setdefault("height", "600px") + super().__init__(n_rows, n_columns, **kwargs) + + hlay = ipywdg.Layout(height="30px", width="auto") + vlay = ipywdg.Layout(height="auto", width="30px") + self[self.IDX_TOOLBAR_TOP] = self._tbars_top = ipywdg.HBox(layout=hlay) + self[self.IDX_TOOLBAR_BOTTOM] = self._tbars_bottom = ipywdg.HBox(layout=hlay) + self[self.IDX_TOOLBAR_LEFT] = self._tbars_left = ipywdg.VBox(layout=vlay) + self[self.IDX_TOOLBAR_RIGHT] = self._tbars_right = ipywdg.VBox(layout=vlay) + self[self.IDX_DOCK_TOP] = self._dwdgs_top = ipywdg.HBox(layout=hlay) + self[self.IDX_DOCK_BOTTOM] = self._dwdgs_bottom = ipywdg.HBox(layout=hlay) + self[self.IDX_DOCK_LEFT] = self._dwdgs_left = ipywdg.VBox(layout=vlay) + self[self.IDX_DOCK_RIGHT] = self._dwdgs_right = ipywdg.VBox(layout=vlay) + + self.layout.grid_template_columns = "34px 34px 1fr 34px 34px" + self.layout.grid_template_rows = "34px 34px 34px 1fr 34px 34px 34px" + + def set_menu_bar(self, widget): + self[self.IDX_MENUBAR] = widget + + def set_status_bar(self, widget): + self[self.IDX_STATUSBAR] = widget + + def add_toolbar(self, widget, area: Literal["left", "top", "right", "bottom"]): + if area == "top": + self._tbars_top.children += (widget,) + elif area == "bottom": + self._tbars_bottom.children += (widget,) + elif area == "left": + self._tbars_left.children += (widget,) + elif area == "right": + self._tbars_right.children += (widget,) + else: + raise ValueError(f"Invalid area: {area!r}") + + def add_dock_widget(self, widget, area: Literal["left", "top", "right", "bottom"]): + if area == "top": + self._dwdgs_top.children += (widget,) + elif area == "bottom": + self._dwdgs_bottom.children += (widget,) + elif area == "left": + self._dwdgs_left.children += (widget,) + elif area == "right": + self._dwdgs_right.children += (widget,) + else: + raise ValueError(f"Invalid area: {area!r}") + + +# grid = GridspecLayout(7, 5, layout=layout, width="600px", height="600px") + +# grid[0, :] = Button( +# description="Menu Bar", button_style="danger", layout=Layout(**he_vf) +# ) +# grid[1, :] = Button(description="Toolbars", button_style="info", layout=Layout(**he_vf)) +# grid[2, 1:4] = Button( +# description="Dock Widgets", button_style="success", layout=Layout(**he_vf) +# ) +# grid[2:5, 0] = Button(description="T", button_style="info", layout=Layout(**hf_ve)) +# grid[3, 1] = Button(description="D", button_style="success", layout=Layout(**hf_ve)) +# grid[3, 2] = Button( +# description="Central Widget", +# button_style="warning", +# layout=Layout(height="auto", width="auto"), +# ) +# grid[3, 3] = Button(description="D", button_style="success", layout=Layout(**hf_ve)) +# grid[2:5, 4] = Button(description="T", button_style="info", layout=Layout(**hf_ve)) +# grid[4, 1:4] = Button(description="D", button_style="success", layout=Layout(**he_vf)) +# grid[5, :] = Button(description="T", button_style="info", layout=Layout(**he_vf)) +# grid[6, :] = Button( +# description="Status Bar", button_style="danger", layout=Layout(**he_vf) +# ) +# grid.layout.grid_template_columns = "34px 34px 1fr 34px 34px" +# grid.layout.grid_template_rows = "34px 34px 34px 1fr 34px 34px 34px" + + +class MainWindow(Container): + def __init__(self, layout="horizontal", scrollable: bool = False, **kwargs): + self._ipywidget = IpyMainWindow() + print(self._ipywidget) + + def _mgui_create_menu_item( + self, + menu_name: str, + action_name: str, + callback: Callable | None = None, + shortcut: str | None = None, + ): + pass + + def get_text_width(text): # FIXME: how to do this in ipywidgets? return 40 diff --git a/src/magicgui/widgets/bases/_container_widget.py b/src/magicgui/widgets/bases/_container_widget.py index a1fc17ee9..651db4401 100644 --- a/src/magicgui/widgets/bases/_container_widget.py +++ b/src/magicgui/widgets/bases/_container_widget.py @@ -422,6 +422,54 @@ def create_menu_item( """ self._widget._mgui_create_menu_item(menu_name, item_name, callback, shortcut) + def add_dock_widget( + self, widget: Widget, *, area: protocols.Area = "right" + ) -> None: + """Add a dock widget to the main window. + + Parameters + ---------- + widget : Widget + The widget to add to the main window. + area : str, optional + The area in which to add the widget, must be one of + `{'left', 'right', 'top', 'bottom'}`, by default "right". + """ + self._widget._mgui_add_dock_widget(widget, area) + + def add_tool_bar(self, widget: Widget, *, area: protocols.Area = "top") -> None: + """Add a toolbar to the main window. + + Parameters + ---------- + widget : Widget + The widget to add to the main window. + area : str, optional + The area in which to add the widget, must be one of + `{'left', 'right', 'top', 'bottom'}`, by default "top". + """ + self._widget._mgui_add_tool_bar(widget, area) + + def set_menubar(self, widget: Widget) -> None: + """Set the menubar of the main window. + + Parameters + ---------- + widget : Widget + The widget to add to the main window. + """ + self._widget._mgui_set_menu_bar(widget) + + def set_status_bar(self, widget: Widget) -> None: + """Set the statusbar of the main window. + + Parameters + ---------- + widget : Widget + The widget to add to the main window. + """ + self._widget._mgui_set_status_bar(widget) + class DialogWidget(ContainerWidget): """Modal Container.""" diff --git a/src/magicgui/widgets/protocols.py b/src/magicgui/widgets/protocols.py index c730ea4d2..ac2a9b736 100644 --- a/src/magicgui/widgets/protocols.py +++ b/src/magicgui/widgets/protocols.py @@ -15,6 +15,7 @@ Any, Callable, Iterable, + Literal, NoReturn, Protocol, Sequence, @@ -26,6 +27,8 @@ from magicgui.widgets.bases import Widget + Area = Literal["left", "right", "top", "bottom"] + def assert_protocol(widget_class: type, protocol: type) -> None | NoReturn: """Ensure that widget_class implements protocol, or raise helpful error.""" @@ -550,6 +553,22 @@ def _mgui_create_menu_item( """ raise NotImplementedError() + @abstractmethod + def _mgui_add_dock_widget(self, widget: Widget, area: Area) -> None: + raise NotImplementedError() + + @abstractmethod + def _mgui_add_tool_bar(self, widget: Widget, area: Area) -> None: + raise NotImplementedError() + + @abstractmethod + def _mgui_set_menu_bar(self, widget: Widget) -> None: + raise NotImplementedError() + + @abstractmethod + def _mgui_set_status_bar(self, widget: Widget) -> None: + raise NotImplementedError() + # APPLICATION -------------------------------------------------------------------- diff --git a/x.py b/x.py new file mode 100644 index 000000000..704eaba85 --- /dev/null +++ b/x.py @@ -0,0 +1,10 @@ +from magicgui import widgets + +dw = widgets.PushButton(text="Hello World!") +tb = widgets.ToolBar() + +main = widgets.MainWindow() +main.add_dock_widget(dw) +main.add_tool_bar(tb) + +main.show(run=True) From 8242141ec0ff922eb8771f27d93aaacda62d3d6e Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Thu, 19 Oct 2023 10:50:55 -0400 Subject: [PATCH 18/32] wpi --- src/magicgui/backends/_qtpy/__init__.py | 2 + src/magicgui/backends/_qtpy/widgets.py | 90 ++++++++++++++- src/magicgui/widgets/__init__.py | 2 + src/magicgui/widgets/_concrete.py | 6 + src/magicgui/widgets/bases/__init__.py | 5 +- .../widgets/bases/_container_widget.py | 66 +---------- src/magicgui/widgets/bases/_main_window.py | 103 ++++++++++++++++++ src/magicgui/widgets/bases/_statusbar.py | 55 ++++++++++ src/magicgui/widgets/protocols.py | 62 ++++++++++- x.py | 9 ++ 10 files changed, 329 insertions(+), 71 deletions(-) create mode 100644 src/magicgui/widgets/bases/_main_window.py create mode 100644 src/magicgui/widgets/bases/_statusbar.py diff --git a/src/magicgui/backends/_qtpy/__init__.py b/src/magicgui/backends/_qtpy/__init__.py index a42b100c3..3fa320c69 100644 --- a/src/magicgui/backends/_qtpy/__init__.py +++ b/src/magicgui/backends/_qtpy/__init__.py @@ -25,6 +25,7 @@ Select, Slider, SpinBox, + StatusBar, Table, TextEdit, TimeEdit, @@ -62,6 +63,7 @@ "show_file_dialog", "Slider", "SpinBox", + "StatusBar", "Table", "TextEdit", "TimeEdit", diff --git a/src/magicgui/backends/_qtpy/widgets.py b/src/magicgui/backends/_qtpy/widgets.py index a94bd3665..dcf3bad3d 100644 --- a/src/magicgui/backends/_qtpy/widgets.py +++ b/src/magicgui/backends/_qtpy/widgets.py @@ -27,12 +27,15 @@ ) from magicgui.types import FileDialogMode -from magicgui.widgets import Widget, protocols +from magicgui.widgets import protocols from magicgui.widgets._concrete import _LabeledWidget +from magicgui.widgets.bases import Widget if TYPE_CHECKING: import numpy + from magicgui.widgets.protocols import Area + @contextmanager def _signals_blocked(obj: QtW.QWidget) -> Iterator[None]: @@ -580,7 +583,7 @@ def _mgui_get_orientation(self) -> str: return "vertical" -class MainWindow(Container): +class MainWindow(Container, protocols.MainWindowProtocol): def __init__( self, layout="vertical", scrollable: bool = False, **kwargs: Any ) -> None: @@ -618,6 +621,53 @@ def _mgui_create_menu_item( action.triggered.connect(callback) menu.addAction(action) + def _mgui_add_tool_bar(self, widget: Widget, area: Area) -> None: + native = widget.native + if not isinstance(native, QtW.QToolBar): + raise TypeError( + f"Expected widget to be a {QtW.QToolBar}, got {type(native)}" + ) + self._main_window.addToolBar(Q_TB_AREA[area], native) + + def _mgui_add_dock_widget(self, widget: Widget, area: Area) -> None: + native = widget.native + if isinstance(native, QtW.QDockWidget): + dw = native + else: + # TODO: allowed areas + dw = QtW.QDockWidget() + dw.setWidget(native) + self._main_window.addDockWidget(Q_DW_AREA[area], dw) + + def _mgui_set_status_bar(self, widget: Widget | None) -> None: + if widget is None: + self._main_window.setStatusBar(None) + return + + native = widget.native + if not isinstance(native, QtW.QStatusBar): + raise TypeError( + f"Expected widget to be a {QtW.QStatusBar}, got {type(native)}" + ) + self._main_window.setStatusBar(native) + + def _mgui_set_menu_bar(self, widget: Widget) -> None: + raise NotImplementedError() + + +Q_TB_AREA: dict[Area, Qt.ToolBarArea] = { + "top": Qt.ToolBarArea.TopToolBarArea, + "bottom": Qt.ToolBarArea.BottomToolBarArea, + "left": Qt.ToolBarArea.LeftToolBarArea, + "right": Qt.ToolBarArea.RightToolBarArea, +} +Q_DW_AREA: dict[Area, Qt.DockWidgetArea] = { + "top": Qt.DockWidgetArea.TopDockWidgetArea, + "bottom": Qt.DockWidgetArea.BottomDockWidgetArea, + "left": Qt.DockWidgetArea.LeftDockWidgetArea, + "right": Qt.DockWidgetArea.RightDockWidgetArea, +} + class SpinBox(QBaseRangedWidget): def __init__(self, **kwargs: Any) -> None: @@ -1217,7 +1267,7 @@ def _mgui_get_value(self): return self._qwidget.time().toPyTime() -class ToolBar(QBaseWidget): +class ToolBar(QBaseWidget, protocols.ToolBarProtocol): _qwidget: QtW.QToolBar def __init__(self, **kwargs: Any) -> None: @@ -1233,7 +1283,10 @@ def _on_palette_change(self): 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 callback: + act = self._qwidget.addAction(text, callback) + else: + act = self._qwidget.addAction(text) if qicon := _get_qicon(icon, None, palette=self._qwidget.palette()): act.setIcon(qicon) act.setData(icon) @@ -1274,6 +1327,35 @@ def _mgui_clear(self) -> None: self._qwidget.clear() +class StatusBar(QBaseWidget, protocols.StatusBarProtocol): + _qwidget: QtW.QStatusBar + + def __init__(self, **kwargs: Any) -> None: + super().__init__(QtW.QStatusBar, **kwargs) + + def _mgui_insert_widget(self, position: int, widget: Widget) -> None: + """Insert `widget` at the given `position`.""" + self._qwidget.insertWidget(position, widget.native) + + def _mgui_remove_widget(self, widget: Widget) -> None: + """Remove the specified widget.""" + self._qwidget.removeWidget(widget.native) + + def _mgui_get_message(self) -> str: + """Return currently shown message, or empty string if None.""" + return self._qwidget.currentMessage() + + def _mgui_set_message(self, message: str, timeout: int = 0) -> None: + """Show a message in the status bar for a given timeout. + + To clear the message, set it to the empty string + """ + if message: + self._qwidget.showMessage(message, timeout) + else: + self._qwidget.clearMessage() + + 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 2478afb0d..02f4662fa 100644 --- a/src/magicgui/widgets/__init__.py +++ b/src/magicgui/widgets/__init__.py @@ -41,6 +41,7 @@ SliceEdit, Slider, SpinBox, + StatusBar, TextEdit, TimeEdit, ToolBar, @@ -105,6 +106,7 @@ "SliceEdit", "Slider", "SpinBox", + "StatusBar", "Table", "TextEdit", "TimeEdit", diff --git a/src/magicgui/widgets/_concrete.py b/src/magicgui/widgets/_concrete.py index e2fc1d938..8a3a692de 100644 --- a/src/magicgui/widgets/_concrete.py +++ b/src/magicgui/widgets/_concrete.py @@ -46,6 +46,7 @@ MultiValuedSliderWidget, RangedWidget, SliderWidget, + StatusBarWidget, ToolBarWidget, TransformedRangedWidget, ValueWidget, @@ -975,6 +976,11 @@ class ToolBar(ToolBarWidget): """Toolbar that contains a set of controls.""" +@backend_widget +class StatusBar(StatusBarWidget): + """Status bar that displays status information.""" + + 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 176a12e9f..5d64fe4c8 100644 --- a/src/magicgui/widgets/bases/__init__.py +++ b/src/magicgui/widgets/bases/__init__.py @@ -44,10 +44,12 @@ def __init__( """ from ._button_widget import ButtonWidget from ._categorical_widget import CategoricalWidget -from ._container_widget import ContainerWidget, DialogWidget, MainWindowWidget +from ._container_widget import ContainerWidget, DialogWidget from ._create_widget import create_widget +from ._main_window import MainWindowWidget from ._ranged_widget import RangedWidget, TransformedRangedWidget from ._slider_widget import MultiValuedSliderWidget, SliderWidget +from ._statusbar import StatusBarWidget from ._toolbar import ToolBarWidget from ._value_widget import ValueWidget from ._widget import Widget @@ -63,6 +65,7 @@ def __init__( "RangedWidget", "SliderWidget", "ToolBarWidget", + "StatusBarWidget", "TransformedRangedWidget", "ValueWidget", "Widget", diff --git a/src/magicgui/widgets/bases/_container_widget.py b/src/magicgui/widgets/bases/_container_widget.py index 651db4401..327598f70 100644 --- a/src/magicgui/widgets/bases/_container_widget.py +++ b/src/magicgui/widgets/bases/_container_widget.py @@ -35,6 +35,7 @@ from typing_extensions import Unpack from magicgui.widgets import Container, protocols + from magicgui.widgets._concrete import StatusBar from ._widget import WidgetKwargs @@ -404,71 +405,6 @@ def _load(self, path: str | Path, quiet: bool = False) -> None: getattr(self, key).value = val -class MainWindowWidget(ContainerWidget): - """Top level Application widget that can contain other widgets.""" - - _widget: protocols.MainWindowProtocol - - def create_menu_item( - self, - menu_name: str, - item_name: str, - callback: Callable | None = None, - shortcut: str | None = None, - ) -> None: - """Create a menu item ``item_name`` under menu ``menu_name``. - - ``menu_name`` will be created if it does not already exist. - """ - self._widget._mgui_create_menu_item(menu_name, item_name, callback, shortcut) - - def add_dock_widget( - self, widget: Widget, *, area: protocols.Area = "right" - ) -> None: - """Add a dock widget to the main window. - - Parameters - ---------- - widget : Widget - The widget to add to the main window. - area : str, optional - The area in which to add the widget, must be one of - `{'left', 'right', 'top', 'bottom'}`, by default "right". - """ - self._widget._mgui_add_dock_widget(widget, area) - - def add_tool_bar(self, widget: Widget, *, area: protocols.Area = "top") -> None: - """Add a toolbar to the main window. - - Parameters - ---------- - widget : Widget - The widget to add to the main window. - area : str, optional - The area in which to add the widget, must be one of - `{'left', 'right', 'top', 'bottom'}`, by default "top". - """ - self._widget._mgui_add_tool_bar(widget, area) - - def set_menubar(self, widget: Widget) -> None: - """Set the menubar of the main window. - - Parameters - ---------- - widget : Widget - The widget to add to the main window. - """ - self._widget._mgui_set_menu_bar(widget) - - def set_status_bar(self, widget: Widget) -> None: - """Set the statusbar of the main window. - - Parameters - ---------- - widget : Widget - The widget to add to the main window. - """ - self._widget._mgui_set_status_bar(widget) class DialogWidget(ContainerWidget): diff --git a/src/magicgui/widgets/bases/_main_window.py b/src/magicgui/widgets/bases/_main_window.py new file mode 100644 index 000000000..b39b3f50d --- /dev/null +++ b/src/magicgui/widgets/bases/_main_window.py @@ -0,0 +1,103 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, Callable, cast + +from ._container_widget import ContainerWidget + +if TYPE_CHECKING: + from magicgui.widgets import protocols + from magicgui.widgets._concrete import StatusBar + + from ._widget import Widget + + +class MainWindowWidget(ContainerWidget): + """Top level Application widget that can contain other widgets.""" + + _widget: protocols.MainWindowProtocol + _status_bar: StatusBar | None = None + + def create_menu_item( + self, + menu_name: str, + item_name: str, + callback: Callable | None = None, + shortcut: str | None = None, + ) -> None: + """Create a menu item ``item_name`` under menu ``menu_name``. + + ``menu_name`` will be created if it does not already exist. + """ + self._widget._mgui_create_menu_item(menu_name, item_name, callback, shortcut) + + def add_dock_widget( + self, widget: Widget, *, area: protocols.Area = "right" + ) -> None: + """Add a dock widget to the main window. + + Parameters + ---------- + widget : Widget + The widget to add to the main window. + area : str, optional + The area in which to add the widget, must be one of + `{'left', 'right', 'top', 'bottom'}`, by default "right". + """ + self._widget._mgui_add_dock_widget(widget, area) + + def add_tool_bar(self, widget: Widget, *, area: protocols.Area = "top") -> None: + """Add a toolbar to the main window. + + Parameters + ---------- + widget : Widget + The widget to add to the main window. + area : str, optional + The area in which to add the widget, must be one of + `{'left', 'right', 'top', 'bottom'}`, by default "top". + """ + self._widget._mgui_add_tool_bar(widget, area) + + def set_menubar(self, widget: Widget) -> None: + """Set the menubar of the main window. + + Parameters + ---------- + widget : Widget + The widget to add to the main window. + """ + self._widget._mgui_set_menu_bar(widget) + + @property + def menu_bar(self) -> StatusBar: + """Return the status bar widget.""" + if self._status_bar is None: + from magicgui.widgets._concrete import StatusBar + + self.status_bar = StatusBar() + return cast("StatusBar", self._status_bar) + + # def set_status_bar(self, widget: Widget) -> None: + # """Set the statusbar of the main window. + + # Parameters + # ---------- + # widget : Widget + # The widget to add to the main window. + # """ + # self._widget._mgui_set_status_bar(widget) + + @property + def status_bar(self) -> StatusBar: + """Return the status bar widget.""" + if self._status_bar is None: + from magicgui.widgets._concrete import StatusBar + + self.status_bar = StatusBar() + return cast("StatusBar", self._status_bar) + + @status_bar.setter + def status_bar(self, widget: StatusBar | None) -> None: + """Set the status bar widget.""" + self._status_bar = widget + self._widget._mgui_set_status_bar(widget) diff --git a/src/magicgui/widgets/bases/_statusbar.py b/src/magicgui/widgets/bases/_statusbar.py new file mode 100644 index 000000000..efa9462dd --- /dev/null +++ b/src/magicgui/widgets/bases/_statusbar.py @@ -0,0 +1,55 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, Tuple, TypeVar, Union + +from ._widget import Widget + +if TYPE_CHECKING: + from magicgui.widgets import protocols + +T = TypeVar("T", int, float, Tuple[Union[int, float], ...]) + + +class StatusBarWidget(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.StatusBarProtocol + + def __init__(self, **base_widget_kwargs: Any) -> None: + super().__init__(**base_widget_kwargs) + + def add_widget(self, widget: Widget) -> None: + """Add a widget to the toolbar.""" + self.insert_widget(-1, widget) + + def insert_widget(self, position: int, widget: Widget) -> None: + """Insert a widget at the given position.""" + self._widget._mgui_insert_widget(position, widget) + + def remove_widget(self, widget: Widget) -> None: + """Remove a widget from the toolbar.""" + self._widget._mgui_remove_widget(widget) + + @property + def message(self) -> str: + """Return currently shown message, or empty string if None.""" + return self._widget._mgui_get_message() + + @message.setter + def message(self, message: str) -> None: + """Return the message timeout in milliseconds.""" + self.set_message(message) + + def set_message(self, message: str, timeout: int = 0) -> None: + """Show a message in the status bar for a given timeout. + + To clear the message, set it to the empty string + """ + self._widget._mgui_set_message(message, timeout) diff --git a/src/magicgui/widgets/protocols.py b/src/magicgui/widgets/protocols.py index bd34a62dc..2db36c1b6 100644 --- a/src/magicgui/widgets/protocols.py +++ b/src/magicgui/widgets/protocols.py @@ -553,6 +553,66 @@ def _mgui_clear(self) -> None: """Clear the toolbar.""" +class StatusBarProtocol(WidgetProtocol, Protocol): + """Status bar that contains a set of controls.""" + + @abstractmethod + def _mgui_insert_widget(self, position: int, widget: Widget) -> None: + """Insert `widget` at the given `position`.""" + raise NotImplementedError() + + @abstractmethod + def _mgui_remove_widget(self, widget: Widget) -> None: + """Remove the specified widget.""" + raise NotImplementedError() + + @abstractmethod + def _mgui_get_message(self) -> str: + """Return currently shown message, or empty string if None.""" + raise NotImplementedError() + + @abstractmethod + def _mgui_set_message(self, message: str, timeout: int = 0) -> None: + """Show a message in the status bar for a given timeout. + + To clear the message, set it to the empty string + """ + raise NotImplementedError() + + +class MenuProtocol(WidgetProtocol, Protocol): + """Menu that contains a set of actions.""" + + @abstractmethod + def _mgui_insert_action(self, before: str | None, action: Widget) -> None: + """Insert action before the specified action.""" + raise NotImplementedError() + + @abstractmethod + def _mgui_add_separator(self) -> None: + """Add a separator line to the menu.""" + raise NotImplementedError() + + @abstractmethod + def _mgui_add_menu(self, title: str, icon: str | None) -> None: + """Add a menu to the menu.""" + raise NotImplementedError() + + +class MenuBarProtocol(WidgetProtocol, Protocol): + """Menu bar that contains a set of menus.""" + + @abstractmethod + def _mgui_add_menu(self, title: str, icon: str | None) -> None: + """Add a menu to the menu bar.""" + raise NotImplementedError() + + @abstractmethod + def _mgui_clear(self) -> None: + """Clear the menu bar.""" + raise NotImplementedError() + + class DialogProtocol(ContainerProtocol, Protocol): """Protocol for modal (blocking) containers.""" @@ -601,7 +661,7 @@ def _mgui_set_menu_bar(self, widget: Widget) -> None: raise NotImplementedError() @abstractmethod - def _mgui_set_status_bar(self, widget: Widget) -> None: + def _mgui_set_status_bar(self, widget: Widget | None) -> None: raise NotImplementedError() diff --git a/x.py b/x.py index 704eaba85..e6fbc4942 100644 --- a/x.py +++ b/x.py @@ -2,9 +2,18 @@ dw = widgets.PushButton(text="Hello World!") tb = widgets.ToolBar() +tb.add_button(text="Hello", icon="mdi:folder") +tb.add_spacer() +tb.add_button(text="World!", icon="mdi:square-edit-outline") + +# sb = widgets.StatusBar() +# sb.set_message("Hello Status!") + main = widgets.MainWindow() main.add_dock_widget(dw) main.add_tool_bar(tb) +# main.set_status_bar(sb) +main.status_bar.message = "Hello Status!" main.show(run=True) From db0e68fe6012e948c90b08aa574d5847242e045b Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Thu, 19 Oct 2023 20:27:40 -0400 Subject: [PATCH 19/32] adding menus --- src/magicgui/application.py | 5 +- src/magicgui/backends/_qtpy/__init__.py | 2 + src/magicgui/backends/_qtpy/widgets.py | 94 ++++++++++++++++++++-- src/magicgui/widgets/_concrete.py | 12 +++ src/magicgui/widgets/bases/__init__.py | 5 +- src/magicgui/widgets/bases/_main_window.py | 53 ++++++------ src/magicgui/widgets/bases/_menubar.py | 66 +++++++++++++++ src/magicgui/widgets/bases/_statusbar.py | 4 +- src/magicgui/widgets/bases/_toolbar.py | 6 +- src/magicgui/widgets/bases/_widget.py | 37 +++++---- src/magicgui/widgets/protocols.py | 45 +++++++---- x.py | 18 ++++- 12 files changed, 272 insertions(+), 75 deletions(-) create mode 100644 src/magicgui/widgets/bases/_menubar.py diff --git a/src/magicgui/application.py b/src/magicgui/application.py index a564acb50..c23242ae6 100644 --- a/src/magicgui/application.py +++ b/src/magicgui/application.py @@ -20,11 +20,12 @@ def _in_jupyter() -> bool: with suppress(ImportError): from IPython import get_ipython - return get_ipython().__class__.__name__ == "ZMQInteractiveShell" + ipy_class = get_ipython().__class__.__name__ + return bool(ipy_class == "ZMQInteractiveShell") return False -def _choose_backend(): +def _choose_backend() -> str: return "ipynb" if _in_jupyter() else "qt" diff --git a/src/magicgui/backends/_qtpy/__init__.py b/src/magicgui/backends/_qtpy/__init__.py index 3fa320c69..a2c345c5b 100644 --- a/src/magicgui/backends/_qtpy/__init__.py +++ b/src/magicgui/backends/_qtpy/__init__.py @@ -15,6 +15,7 @@ LineEdit, LiteralEvalLineEdit, MainWindow, + MenuBar, Password, ProgressBar, PushButton, @@ -52,6 +53,7 @@ "LineEdit", "LiteralEvalLineEdit", "MainWindow", + "MenuBar", "Password", "ProgressBar", "PushButton", diff --git a/src/magicgui/backends/_qtpy/widgets.py b/src/magicgui/backends/_qtpy/widgets.py index dcf3bad3d..4949b4454 100644 --- a/src/magicgui/backends/_qtpy/widgets.py +++ b/src/magicgui/backends/_qtpy/widgets.py @@ -69,10 +69,17 @@ class QBaseWidget(protocols.WidgetProtocol): _qwidget: QtW.QWidget def __init__( - self, qwidg: type[QtW.QWidget], parent: QtW.QWidget | None = None, **kwargs: Any + self, + qwidg: type[QtW.QWidget] | QtW.QWidget, + parent: QtW.QWidget | None = None, + **kwargs: Any, ) -> None: - self._qwidget = qwidg(parent=parent) - self._qwidget.setObjectName(f"magicgui.{qwidg.__name__}") + if isinstance(qwidg, QtW.QWidget): + self._qwidget = qwidg + self._qwidget.setObjectName(f"magicgui.{type(qwidg).__name__}") + else: + self._qwidget = qwidg(parent=parent) + self._qwidget.setObjectName(f"magicgui.{qwidg.__name__}") self._event_filter = EventFilter() self._qwidget.installEventFilter(self._event_filter) @@ -583,6 +590,74 @@ def _mgui_get_orientation(self) -> str: return "vertical" +class MenuBar(QBaseWidget, protocols.MenuBarProtocol): + _qwidget: QtW.QMenuBar + + def __init__(self, **kwargs: Any) -> None: + super().__init__(QtW.QMenuBar, **kwargs) + + def _mgui_add_menu(self, title: str, icon: str | None) -> protocols.MenuProtocol: + """Add a menu to the menu bar.""" + if not title.startswith("&"): + title = f"&{title}" + + if icon and (qicon := _get_qicon(icon, None, self._qwidget.palette())): + self._qwidget.addMenu(qicon, title) + return + menu_qwidg = self._qwidget.addMenu(title) + return Menu(menu_qwidg) + + def _mgui_clear(self) -> None: + """Clear the menu bar.""" + + +class Menu(QBaseWidget, protocols.MenuProtocol): + _qwidget: QtW.QMenu + + def __init__( + self, qwidg: type[QtW.QMenu] | QtW.QMenu = QtW.QMenu, **kwargs: Any + ) -> None: + super().__init__(qwidg, **kwargs) + + def _mgui_add_action( + self, + text: str, + shortcut: str | None = None, + icon: str | None = None, + tooltip: str | None = None, + callback: Callable | None = None, + ) -> None: + """Add an action to the menu.""" + if icon and (qicon := _get_qicon(icon, None, self._qwidget.palette())): + action = self._qwidget.addAction(qicon, text) + else: + action = self._qwidget.addAction(text) + if shortcut: + action.setShortcut(shortcut) + if tooltip: + action.setToolTip(tooltip) + if callback: + action.triggered.connect(callback) + + def _mgui_add_separator(self) -> None: + """Add a separator to the menu.""" + self._qwidget.addSeparator() + + def _mgui_add_menu(self, title: str, icon: str | None) -> None: + """Add a menu to the menu bar.""" + if not title.startswith("&"): + title = f"&{title}" + if icon and (qicon := _get_qicon(icon, None, self._qwidget.palette())): + self._qwidget.addMenu(qicon, title) + return + menu_qwidg = self._qwidget.addMenu(title) + return Menu(menu_qwidg) + + def _mgui_clear(self) -> None: + """Clear the menu bar.""" + self._qwidget.clear() + + class MainWindow(Container, protocols.MainWindowProtocol): def __init__( self, layout="vertical", scrollable: bool = False, **kwargs: Any @@ -651,8 +726,17 @@ def _mgui_set_status_bar(self, widget: Widget | None) -> None: ) self._main_window.setStatusBar(native) - def _mgui_set_menu_bar(self, widget: Widget) -> None: - raise NotImplementedError() + def _mgui_set_menu_bar(self, widget: Widget | None) -> None: + if widget is None: + self._main_window.setMenuBar(QtW.QMenuBar()) + return + + native = widget.native + if not isinstance(native, QtW.QMenuBar): + raise TypeError( + f"Expected widget to be a {QtW.QMenuBar}, got {type(native)}" + ) + self._main_window.setMenuBar(native) Q_TB_AREA: dict[Area, Qt.ToolBarArea] = { diff --git a/src/magicgui/widgets/_concrete.py b/src/magicgui/widgets/_concrete.py index 8a3a692de..2f7bf37d5 100644 --- a/src/magicgui/widgets/_concrete.py +++ b/src/magicgui/widgets/_concrete.py @@ -43,6 +43,8 @@ ContainerWidget, DialogWidget, MainWindowWidget, + MenuBarWidget, + MenuWidget, MultiValuedSliderWidget, RangedWidget, SliderWidget, @@ -981,6 +983,16 @@ class StatusBar(StatusBarWidget): """Status bar that displays status information.""" +@backend_widget +class MenuBar(MenuBarWidget): + """Menu bar that contains multiple menus.""" + + +@backend_widget +class Menu(MenuWidget): + """A menu that contains actions.""" + + 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 5d64fe4c8..b0882dbf4 100644 --- a/src/magicgui/widgets/bases/__init__.py +++ b/src/magicgui/widgets/bases/__init__.py @@ -47,6 +47,7 @@ def __init__( from ._container_widget import ContainerWidget, DialogWidget from ._create_widget import create_widget from ._main_window import MainWindowWidget +from ._menubar import MenuBarWidget, MenuWidget from ._ranged_widget import RangedWidget, TransformedRangedWidget from ._slider_widget import MultiValuedSliderWidget, SliderWidget from ._statusbar import StatusBarWidget @@ -61,11 +62,13 @@ def __init__( "create_widget", "DialogWidget", "MainWindowWidget", + "MenuBarWidget", + "MenuWidget", "MultiValuedSliderWidget", "RangedWidget", "SliderWidget", - "ToolBarWidget", "StatusBarWidget", + "ToolBarWidget", "TransformedRangedWidget", "ValueWidget", "Widget", diff --git a/src/magicgui/widgets/bases/_main_window.py b/src/magicgui/widgets/bases/_main_window.py index b39b3f50d..5fcd3fd68 100644 --- a/src/magicgui/widgets/bases/_main_window.py +++ b/src/magicgui/widgets/bases/_main_window.py @@ -6,7 +6,7 @@ if TYPE_CHECKING: from magicgui.widgets import protocols - from magicgui.widgets._concrete import StatusBar + from magicgui.widgets._concrete import MenuBar, StatusBar from ._widget import Widget @@ -16,6 +16,7 @@ class MainWindowWidget(ContainerWidget): _widget: protocols.MainWindowProtocol _status_bar: StatusBar | None = None + _menu_bar: MenuBar | None = None def create_menu_item( self, @@ -58,25 +59,36 @@ def add_tool_bar(self, widget: Widget, *, area: protocols.Area = "top") -> None: """ self._widget._mgui_add_tool_bar(widget, area) - def set_menubar(self, widget: Widget) -> None: - """Set the menubar of the main window. + @property + def menu_bar(self) -> MenuBar: + """Return the status bar widget.""" + if self._menu_bar is None: + from magicgui.widgets._concrete import MenuBar - Parameters - ---------- - widget : Widget - The widget to add to the main window. - """ + self.menu_bar = MenuBar() + return cast("MenuBar", self._menu_bar) + + @menu_bar.setter + def menu_bar(self, widget: MenuBar | None) -> None: + """Set the status bar widget.""" + self._menu_bar = widget self._widget._mgui_set_menu_bar(widget) @property - def menu_bar(self) -> StatusBar: + def status_bar(self) -> StatusBar: """Return the status bar widget.""" if self._status_bar is None: from magicgui.widgets._concrete import StatusBar self.status_bar = StatusBar() return cast("StatusBar", self._status_bar) - + + @status_bar.setter + def status_bar(self, widget: StatusBar | None) -> None: + """Set the status bar widget.""" + self._status_bar = widget + self._widget._mgui_set_status_bar(widget) + # def set_status_bar(self, widget: Widget) -> None: # """Set the statusbar of the main window. @@ -87,17 +99,12 @@ def menu_bar(self) -> StatusBar: # """ # self._widget._mgui_set_status_bar(widget) - @property - def status_bar(self) -> StatusBar: - """Return the status bar widget.""" - if self._status_bar is None: - from magicgui.widgets._concrete import StatusBar + # def set_menubar(self, widget: Widget) -> None: + # """Set the menubar of the main window. - self.status_bar = StatusBar() - return cast("StatusBar", self._status_bar) - - @status_bar.setter - def status_bar(self, widget: StatusBar | None) -> None: - """Set the status bar widget.""" - self._status_bar = widget - self._widget._mgui_set_status_bar(widget) + # Parameters + # ---------- + # widget : Widget + # The widget to add to the main window. + # """ + # self._widget._mgui_set_menu_bar(widget) diff --git a/src/magicgui/widgets/bases/_menubar.py b/src/magicgui/widgets/bases/_menubar.py new file mode 100644 index 000000000..d8914b49b --- /dev/null +++ b/src/magicgui/widgets/bases/_menubar.py @@ -0,0 +1,66 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, Callable + +from ._widget import Widget + +if TYPE_CHECKING: + from magicgui.widgets import protocols + + +class MenuBarWidget(Widget): + """Menu bar containing menus. Can be added to a MainWindowWidget.""" + + _widget: protocols.MenuBarProtocol + + def __init__(self, **base_widget_kwargs: Any) -> None: + super().__init__(**base_widget_kwargs) + self._menus: dict[str, MenuWidget] = {} + + def __getitem__(self, key: str) -> MenuWidget: + return self._menus[key] + + def add_menu(self, title: str, icon: str | None = None) -> MenuWidget: + """Add a menu to the menu bar.""" + menu_widg = self._widget._mgui_add_menu(title, icon) + self._menus[title] = wrapped = MenuWidget(widget_type=menu_widg) + return wrapped + + def clear(self) -> None: + """Clear the menu bar.""" + self._widget._mgui_clear() + + +class MenuWidget(Widget): + """Menu widget. Can be added to a MenuBarWidget or another MenuWidget.""" + + _widget: protocols.MenuProtocol + + def __init__(self, **base_widget_kwargs: Any) -> None: + super().__init__(**base_widget_kwargs) + self._menus: dict[str, MenuWidget] = {} + + def add_action( + self, + text: str, + shortcut: str | None = None, + icon: str | None = None, + tooltip: str | None = None, + callback: Callable | None = None, + ) -> None: + """Add an action to the menu.""" + self._widget._mgui_add_action(text, shortcut, icon, tooltip, callback) + + def add_separator(self) -> None: + """Add a separator line to the menu.""" + self._widget._mgui_add_separator() + + def add_menu(self, title: str, icon: str | None = None) -> MenuWidget: + """Add a menu to the menu.""" + menu_widg = self._widget._mgui_add_menu(title, icon) + self._menus[title] = wrapped = MenuWidget(widget_type=menu_widg) + return wrapped + + def clear(self) -> None: + """Clear the menu bar.""" + self._widget._mgui_clear() diff --git a/src/magicgui/widgets/bases/_statusbar.py b/src/magicgui/widgets/bases/_statusbar.py index efa9462dd..332840134 100644 --- a/src/magicgui/widgets/bases/_statusbar.py +++ b/src/magicgui/widgets/bases/_statusbar.py @@ -1,14 +1,12 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any, Tuple, TypeVar, Union +from typing import TYPE_CHECKING, Any from ._widget import Widget if TYPE_CHECKING: from magicgui.widgets import protocols -T = TypeVar("T", int, float, Tuple[Union[int, float], ...]) - class StatusBarWidget(Widget): """Widget with a value, Wraps ValueWidgetProtocol. diff --git a/src/magicgui/widgets/bases/_toolbar.py b/src/magicgui/widgets/bases/_toolbar.py index c8d87766e..727791ca5 100644 --- a/src/magicgui/widgets/bases/_toolbar.py +++ b/src/magicgui/widgets/bases/_toolbar.py @@ -1,16 +1,12 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any, Callable, Tuple, TypeVar, Union +from typing import TYPE_CHECKING, Any, Callable 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. diff --git a/src/magicgui/widgets/bases/_widget.py b/src/magicgui/widgets/bases/_widget.py index 791e7d7ad..08f8387be 100644 --- a/src/magicgui/widgets/bases/_widget.py +++ b/src/magicgui/widgets/bases/_widget.py @@ -85,7 +85,7 @@ class Widget: def __init__( self, - widget_type: type[WidgetProtocol], + widget_type: type[WidgetProtocol] | WidgetProtocol, name: str = "", annotation: Any | None = None, label: str | None = None, @@ -116,26 +116,31 @@ def __init__( ) if not isinstance(_prot, str): _prot = _prot.__name__ + prot = getattr(protocols, _prot.replace("protocols.", "")) - protocols.assert_protocol(widget_type, prot) self.__magicgui_app__ = use_app() assert self.__magicgui_app__.native if isinstance(parent, Widget): parent = parent.native - try: - self._widget = widget_type(parent=parent, **backend_kwargs) - except TypeError as e: - if "unexpected keyword" not in str(e) and "no arguments" not in str(e): - raise - - warnings.warn( - "Beginning with magicgui v0.6, the `widget_type` class passed to " - "`magicgui.Widget` must accept a `parent` Argument. In v0.7 this " - "will raise an exception. " - f"Please update '{widget_type.__name__}.__init__()'", - stacklevel=2, - ) - self._widget = widget_type(**backend_kwargs) + if not isinstance(widget_type, type): + protocols.assert_protocol(type(widget_type), prot) + self._widget = widget_type + else: + protocols.assert_protocol(widget_type, prot) + try: + self._widget = widget_type(parent=parent, **backend_kwargs) + except TypeError as e: + if "unexpected keyword" not in str(e) and "no arguments" not in str(e): + raise + + warnings.warn( + "Beginning with magicgui v0.6, the `widget_type` class passed to " + "`magicgui.Widget` must accept a `parent` Argument. In v0.7 this " + "will raise an exception. " + f"Please update '{widget_type.__name__}.__init__()'", + stacklevel=2, + ) + self._widget = widget_type(**backend_kwargs) self.name: str = name self.param_kind = inspect.Parameter.POSITIONAL_OR_KEYWORD diff --git a/src/magicgui/widgets/protocols.py b/src/magicgui/widgets/protocols.py index 2db36c1b6..ccd8c8371 100644 --- a/src/magicgui/widgets/protocols.py +++ b/src/magicgui/widgets/protocols.py @@ -580,37 +580,50 @@ def _mgui_set_message(self, message: str, timeout: int = 0) -> None: raise NotImplementedError() -class MenuProtocol(WidgetProtocol, Protocol): - """Menu that contains a set of actions.""" +class MenuBarProtocol(WidgetProtocol, Protocol): + """Menu bar that contains a set of menus.""" @abstractmethod - def _mgui_insert_action(self, before: str | None, action: Widget) -> None: - """Insert action before the specified action.""" + def _mgui_add_menu(self, title: str, icon: str | None) -> MenuProtocol: + """Add a menu to the menu bar.""" raise NotImplementedError() @abstractmethod - def _mgui_add_separator(self) -> None: - """Add a separator line to the menu.""" + def _mgui_clear(self) -> None: + """Clear the menu bar.""" raise NotImplementedError() - @abstractmethod - def _mgui_add_menu(self, title: str, icon: str | None) -> None: - """Add a menu to the menu.""" - raise NotImplementedError() +class MenuProtocol(WidgetProtocol, Protocol): + """Menu that contains a set of actions.""" -class MenuBarProtocol(WidgetProtocol, Protocol): - """Menu bar that contains a set of menus.""" + # @abstractmethod + # def _mgui_insert_action(self, before: str | None, action: Widget) -> None: + # """Insert action before the specified action.""" + # raise NotImplementedError() + + @abstractmethod + def _mgui_add_action( + self, + text: str, + shortcut: str | None = None, + icon: str | None = None, + tooltip: str | None = None, + callback: Callable | None = None, + ) -> None: + """Add an action to the menu.""" + + @abstractmethod + def _mgui_add_separator(self) -> None: + """Add a separator line to the menu.""" @abstractmethod def _mgui_add_menu(self, title: str, icon: str | None) -> None: - """Add a menu to the menu bar.""" - raise NotImplementedError() + """Add a menu to the menu.""" @abstractmethod def _mgui_clear(self) -> None: """Clear the menu bar.""" - raise NotImplementedError() class DialogProtocol(ContainerProtocol, Protocol): @@ -657,7 +670,7 @@ def _mgui_add_tool_bar(self, widget: Widget, area: Area) -> None: raise NotImplementedError() @abstractmethod - def _mgui_set_menu_bar(self, widget: Widget) -> None: + def _mgui_set_menu_bar(self, widget: Widget | None) -> None: raise NotImplementedError() @abstractmethod diff --git a/x.py b/x.py index e6fbc4942..b74a35b43 100644 --- a/x.py +++ b/x.py @@ -6,14 +6,24 @@ tb.add_spacer() tb.add_button(text="World!", icon="mdi:square-edit-outline") -# sb = widgets.StatusBar() -# sb.set_message("Hello Status!") - main = widgets.MainWindow() main.add_dock_widget(dw) main.add_tool_bar(tb) + +# sb = widgets.StatusBar() +# sb.set_message("Hello Status!") # main.set_status_bar(sb) -main.status_bar.message = "Hello Status!" +# or +main.status_bar.set_message("") + +file = main.menu_bar.add_menu("File") +main.menu_bar["File"].add_action("Open", callback=lambda: print("Open")) +assert file is main.menu_bar["File"] +subm = file.add_menu("Submenu") +subm.add_action("Subaction", callback=lambda: print("Subaction")) +subm.add_separator() +subm.add_action("Subaction2", callback=lambda: print("Subaction2")) + main.show(run=True) From db81067729f5e86c6a32e3f51b77b1963e102731 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 20 Oct 2023 12:12:54 +0000 Subject: [PATCH 20/32] style(pre-commit.ci): auto fixes [...] --- src/magicgui/backends/_ipynb/widgets.py | 2 +- src/magicgui/widgets/bases/_container_widget.py | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/src/magicgui/backends/_ipynb/widgets.py b/src/magicgui/backends/_ipynb/widgets.py index 4a9b217d2..cb20c51df 100644 --- a/src/magicgui/backends/_ipynb/widgets.py +++ b/src/magicgui/backends/_ipynb/widgets.py @@ -524,7 +524,7 @@ def _mgui_get_orientation(self) -> str: return "vertical" if isinstance(self._ipywidget, ipywdg.VBox) else "horizontal" -from ipywidgets import Button, GridspecLayout, Layout +from ipywidgets import GridspecLayout class IpyMainWindow(GridspecLayout): diff --git a/src/magicgui/widgets/bases/_container_widget.py b/src/magicgui/widgets/bases/_container_widget.py index 327598f70..a08066ed8 100644 --- a/src/magicgui/widgets/bases/_container_widget.py +++ b/src/magicgui/widgets/bases/_container_widget.py @@ -35,7 +35,6 @@ from typing_extensions import Unpack from magicgui.widgets import Container, protocols - from magicgui.widgets._concrete import StatusBar from ._widget import WidgetKwargs @@ -405,8 +404,6 @@ def _load(self, path: str | Path, quiet: bool = False) -> None: getattr(self, key).value = val - - class DialogWidget(ContainerWidget): """Modal Container.""" From b100bda0b32773b1c832c9d5c67c894af8b537ff Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Fri, 20 Oct 2023 08:25:23 -0400 Subject: [PATCH 21/32] extend menu --- src/magicgui/widgets/bases/_menubar.py | 24 +++++++++++++++++++++++- src/magicgui/widgets/protocols.py | 20 ++++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/src/magicgui/widgets/bases/_menubar.py b/src/magicgui/widgets/bases/_menubar.py index d8914b49b..8a3f3caf3 100644 --- a/src/magicgui/widgets/bases/_menubar.py +++ b/src/magicgui/widgets/bases/_menubar.py @@ -36,10 +36,32 @@ class MenuWidget(Widget): _widget: protocols.MenuProtocol - def __init__(self, **base_widget_kwargs: Any) -> None: + def __init__( + self, title: str = "", icon: str = "", **base_widget_kwargs: Any + ) -> None: super().__init__(**base_widget_kwargs) + self.title = title + self.icon = icon self._menus: dict[str, MenuWidget] = {} + @property + def title(self) -> str: + """Title of the menu.""" + return self._widget._mgui_get_title() + + @title.setter + def title(self, value: str) -> None: + self._widget._mgui_set_title(value) + + @property + def icon(self) -> str | None: + """Icon of the menu.""" + return self._widget._mgui_get_icon() + + @icon.setter + def icon(self, value: str | None) -> None: + self._widget._mgui_set_icon(value) + def add_action( self, text: str, diff --git a/src/magicgui/widgets/protocols.py b/src/magicgui/widgets/protocols.py index ccd8c8371..d6ebc5ddf 100644 --- a/src/magicgui/widgets/protocols.py +++ b/src/magicgui/widgets/protocols.py @@ -602,6 +602,26 @@ class MenuProtocol(WidgetProtocol, Protocol): # """Insert action before the specified action.""" # raise NotImplementedError() + @abstractmethod + def _mgui_get_title(self) -> str: + """Return the title of the menu.""" + raise NotImplementedError() + + @abstractmethod + def _mgui_set_title(self, title: str) -> None: + """Set the title of the menu.""" + raise NotImplementedError() + + @abstractmethod + def _mgui_get_icon(self) -> str | None: + """Return the icon of the menu.""" + raise NotImplementedError() + + @abstractmethod + def _mgui_set_icon(self, icon: str | None) -> None: + """Set the icon of the menu.""" + raise NotImplementedError() + @abstractmethod def _mgui_add_action( self, From 025323dca5f224ae8914a8fd27501a0012152efd Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Fri, 20 Oct 2023 13:15:46 -0400 Subject: [PATCH 22/32] menu stuff --- src/magicgui/backends/_qtpy/__init__.py | 2 + src/magicgui/backends/_qtpy/widgets.py | 52 ++++++++++++++++--------- src/magicgui/widgets/bases/_menubar.py | 18 +++++---- src/magicgui/widgets/bases/_widget.py | 37 ++++++++---------- src/magicgui/widgets/protocols.py | 17 ++++++-- x.py | 10 ++--- 6 files changed, 82 insertions(+), 54 deletions(-) diff --git a/src/magicgui/backends/_qtpy/__init__.py b/src/magicgui/backends/_qtpy/__init__.py index a2c345c5b..5ac2f9d87 100644 --- a/src/magicgui/backends/_qtpy/__init__.py +++ b/src/magicgui/backends/_qtpy/__init__.py @@ -15,6 +15,7 @@ LineEdit, LiteralEvalLineEdit, MainWindow, + Menu, MenuBar, Password, ProgressBar, @@ -54,6 +55,7 @@ "LiteralEvalLineEdit", "MainWindow", "MenuBar", + "Menu", "Password", "ProgressBar", "PushButton", diff --git a/src/magicgui/backends/_qtpy/widgets.py b/src/magicgui/backends/_qtpy/widgets.py index 4949b4454..22810fac3 100644 --- a/src/magicgui/backends/_qtpy/widgets.py +++ b/src/magicgui/backends/_qtpy/widgets.py @@ -29,7 +29,7 @@ from magicgui.types import FileDialogMode from magicgui.widgets import protocols from magicgui.widgets._concrete import _LabeledWidget -from magicgui.widgets.bases import Widget +from magicgui.widgets.bases import Widget, MenuWidget if TYPE_CHECKING: import numpy @@ -590,22 +590,26 @@ def _mgui_get_orientation(self) -> str: return "vertical" +def _add_qmenu(wdg: QtW.QMenu | QtW.QMenuBar, mgui_menu: MenuWidget): + """Add a magicgui menu to a QMenu or QMenuBar.""" + native = mgui_menu.native + if not isinstance(native, QtW.QMenu): + raise TypeError( + f"Expected menu to be a {QtW.QMenu}, got {type(native)}: {native}" + ) + wdg.addMenu(native) + + class MenuBar(QBaseWidget, protocols.MenuBarProtocol): _qwidget: QtW.QMenuBar def __init__(self, **kwargs: Any) -> None: super().__init__(QtW.QMenuBar, **kwargs) - def _mgui_add_menu(self, title: str, icon: str | None) -> protocols.MenuProtocol: + # def _mgui_add_menu(self, title: str, icon: str | None) -> protocols.MenuProtocol: + def _mgui_add_menu_widget(self, widget: MenuWidget) -> None: """Add a menu to the menu bar.""" - if not title.startswith("&"): - title = f"&{title}" - - if icon and (qicon := _get_qicon(icon, None, self._qwidget.palette())): - self._qwidget.addMenu(qicon, title) - return - menu_qwidg = self._qwidget.addMenu(title) - return Menu(menu_qwidg) + _add_qmenu(self._qwidget, widget) def _mgui_clear(self) -> None: """Clear the menu bar.""" @@ -619,6 +623,23 @@ def __init__( ) -> None: super().__init__(qwidg, **kwargs) + def _mgui_get_title(self) -> str: + return self._qwidget.title() + + def _mgui_set_title(self, value: str) -> None: + self._qwidget.setTitle(value) + + def _mgui_get_icon(self) -> str | None: + # see also: https://github.com/pyapp-kit/superqt/pull/213 + return self._icon + + def _mgui_set_icon(self, icon: str | None) -> None: + self._icon = icon + if icon and (qicon := _get_qicon(icon, None, self._qwidget.palette())): + self._qwidget.setIcon(qicon) + else: + self._qwidget.setIcon(QIcon()) + def _mgui_add_action( self, text: str, @@ -643,15 +664,9 @@ def _mgui_add_separator(self) -> None: """Add a separator to the menu.""" self._qwidget.addSeparator() - def _mgui_add_menu(self, title: str, icon: str | None) -> None: + def _mgui_add_menu_widget(self, widget: MenuWidget) -> None: """Add a menu to the menu bar.""" - if not title.startswith("&"): - title = f"&{title}" - if icon and (qicon := _get_qicon(icon, None, self._qwidget.palette())): - self._qwidget.addMenu(qicon, title) - return - menu_qwidg = self._qwidget.addMenu(title) - return Menu(menu_qwidg) + _add_qmenu(self._qwidget, widget) def _mgui_clear(self) -> None: """Clear the menu bar.""" @@ -669,6 +684,7 @@ def __init__( self._main_window.setCentralWidget(self._scroll) else: self._main_window.setCentralWidget(self._qwidget) + # self._qwidget = self._main_window # TODO def _mgui_get_visible(self): return self._main_window.isVisible() diff --git a/src/magicgui/widgets/bases/_menubar.py b/src/magicgui/widgets/bases/_menubar.py index 8a3f3caf3..05b699483 100644 --- a/src/magicgui/widgets/bases/_menubar.py +++ b/src/magicgui/widgets/bases/_menubar.py @@ -22,9 +22,11 @@ def __getitem__(self, key: str) -> MenuWidget: def add_menu(self, title: str, icon: str | None = None) -> MenuWidget: """Add a menu to the menu bar.""" - menu_widg = self._widget._mgui_add_menu(title, icon) - self._menus[title] = wrapped = MenuWidget(widget_type=menu_widg) - return wrapped + from magicgui.widgets._concrete import Menu + + self._menus[title] = menu = Menu(title=title, icon=icon) + self._widget._mgui_add_menu_widget(menu) + return menu def clear(self) -> None: """Clear the menu bar.""" @@ -37,7 +39,7 @@ class MenuWidget(Widget): _widget: protocols.MenuProtocol def __init__( - self, title: str = "", icon: str = "", **base_widget_kwargs: Any + self, title: str = "", icon: str | None = "", **base_widget_kwargs: Any ) -> None: super().__init__(**base_widget_kwargs) self.title = title @@ -79,9 +81,11 @@ def add_separator(self) -> None: def add_menu(self, title: str, icon: str | None = None) -> MenuWidget: """Add a menu to the menu.""" - menu_widg = self._widget._mgui_add_menu(title, icon) - self._menus[title] = wrapped = MenuWidget(widget_type=menu_widg) - return wrapped + from magicgui.widgets._concrete import Menu + + self._menus[title] = menu = Menu(title=title, icon=icon) + self._widget._mgui_add_menu_widget(menu) + return menu def clear(self) -> None: """Clear the menu bar.""" diff --git a/src/magicgui/widgets/bases/_widget.py b/src/magicgui/widgets/bases/_widget.py index 08f8387be..791e7d7ad 100644 --- a/src/magicgui/widgets/bases/_widget.py +++ b/src/magicgui/widgets/bases/_widget.py @@ -85,7 +85,7 @@ class Widget: def __init__( self, - widget_type: type[WidgetProtocol] | WidgetProtocol, + widget_type: type[WidgetProtocol], name: str = "", annotation: Any | None = None, label: str | None = None, @@ -116,31 +116,26 @@ def __init__( ) if not isinstance(_prot, str): _prot = _prot.__name__ - prot = getattr(protocols, _prot.replace("protocols.", "")) + protocols.assert_protocol(widget_type, prot) self.__magicgui_app__ = use_app() assert self.__magicgui_app__.native if isinstance(parent, Widget): parent = parent.native - if not isinstance(widget_type, type): - protocols.assert_protocol(type(widget_type), prot) - self._widget = widget_type - else: - protocols.assert_protocol(widget_type, prot) - try: - self._widget = widget_type(parent=parent, **backend_kwargs) - except TypeError as e: - if "unexpected keyword" not in str(e) and "no arguments" not in str(e): - raise - - warnings.warn( - "Beginning with magicgui v0.6, the `widget_type` class passed to " - "`magicgui.Widget` must accept a `parent` Argument. In v0.7 this " - "will raise an exception. " - f"Please update '{widget_type.__name__}.__init__()'", - stacklevel=2, - ) - self._widget = widget_type(**backend_kwargs) + try: + self._widget = widget_type(parent=parent, **backend_kwargs) + except TypeError as e: + if "unexpected keyword" not in str(e) and "no arguments" not in str(e): + raise + + warnings.warn( + "Beginning with magicgui v0.6, the `widget_type` class passed to " + "`magicgui.Widget` must accept a `parent` Argument. In v0.7 this " + "will raise an exception. " + f"Please update '{widget_type.__name__}.__init__()'", + stacklevel=2, + ) + self._widget = widget_type(**backend_kwargs) self.name: str = name self.param_kind = inspect.Parameter.POSITIONAL_OR_KEYWORD diff --git a/src/magicgui/widgets/protocols.py b/src/magicgui/widgets/protocols.py index d6ebc5ddf..b6a4a8d34 100644 --- a/src/magicgui/widgets/protocols.py +++ b/src/magicgui/widgets/protocols.py @@ -26,6 +26,7 @@ import numpy as np from magicgui.widgets.bases import Widget + from magicgui.widgets.bases import MenuWidget Area = Literal["left", "right", "top", "bottom"] @@ -583,8 +584,13 @@ def _mgui_set_message(self, message: str, timeout: int = 0) -> None: class MenuBarProtocol(WidgetProtocol, Protocol): """Menu bar that contains a set of menus.""" + # @abstractmethod + # def _mgui_add_menu(self, title: str, icon: str | None) -> MenuProtocol: + # """Add a menu to the menu bar.""" + # raise NotImplementedError() + @abstractmethod - def _mgui_add_menu(self, title: str, icon: str | None) -> MenuProtocol: + def _mgui_add_menu_widget(self, widget: MenuWidget) -> None: """Add a menu to the menu bar.""" raise NotImplementedError() @@ -638,8 +644,13 @@ def _mgui_add_separator(self) -> None: """Add a separator line to the menu.""" @abstractmethod - def _mgui_add_menu(self, title: str, icon: str | None) -> None: - """Add a menu to the menu.""" + def _mgui_add_menu_widget(self, widget: MenuWidget) -> None: + """Add a menu to the menu bar.""" + raise NotImplementedError() + + # @abstractmethod + # def _mgui_add_menu(self, title: str, icon: str | None) -> None: + # """Add a menu to the menu.""" @abstractmethod def _mgui_clear(self) -> None: diff --git a/x.py b/x.py index b74a35b43..b2e78ef5b 100644 --- a/x.py +++ b/x.py @@ -17,13 +17,13 @@ # or main.status_bar.set_message("") -file = main.menu_bar.add_menu("File") -main.menu_bar["File"].add_action("Open", callback=lambda: print("Open")) -assert file is main.menu_bar["File"] -subm = file.add_menu("Submenu") +file_menu = main.menu_bar.add_menu("File") +assert file_menu is main.menu_bar["File"] # can also access like this +file_menu.add_action("Open", callback=lambda: print("Open")) +subm = file_menu.add_menu("Submenu") subm.add_action("Subaction", callback=lambda: print("Subaction")) subm.add_separator() subm.add_action("Subaction2", callback=lambda: print("Subaction2")) - +main.width = 800 main.show(run=True) From b2584d94e636608375cae060d0bd708c488d4811 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 20 Oct 2023 17:16:08 +0000 Subject: [PATCH 23/32] style(pre-commit.ci): auto fixes [...] --- src/magicgui/backends/_qtpy/widgets.py | 2 +- src/magicgui/widgets/protocols.py | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/magicgui/backends/_qtpy/widgets.py b/src/magicgui/backends/_qtpy/widgets.py index 22810fac3..7386fbaad 100644 --- a/src/magicgui/backends/_qtpy/widgets.py +++ b/src/magicgui/backends/_qtpy/widgets.py @@ -29,7 +29,7 @@ from magicgui.types import FileDialogMode from magicgui.widgets import protocols from magicgui.widgets._concrete import _LabeledWidget -from magicgui.widgets.bases import Widget, MenuWidget +from magicgui.widgets.bases import MenuWidget, Widget if TYPE_CHECKING: import numpy diff --git a/src/magicgui/widgets/protocols.py b/src/magicgui/widgets/protocols.py index b6a4a8d34..920d79f65 100644 --- a/src/magicgui/widgets/protocols.py +++ b/src/magicgui/widgets/protocols.py @@ -25,8 +25,7 @@ if TYPE_CHECKING: import numpy as np - from magicgui.widgets.bases import Widget - from magicgui.widgets.bases import MenuWidget + from magicgui.widgets.bases import MenuWidget, Widget Area = Literal["left", "right", "top", "bottom"] From 5704bee46d2426688133e4914717d10df64fb389 Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Fri, 20 Oct 2023 13:33:15 -0400 Subject: [PATCH 24/32] update menus --- src/magicgui/backends/_qtpy/widgets.py | 2 +- src/magicgui/widgets/bases/_menubar.py | 76 +++++++++++++++++++------- x.py | 1 + 3 files changed, 57 insertions(+), 22 deletions(-) diff --git a/src/magicgui/backends/_qtpy/widgets.py b/src/magicgui/backends/_qtpy/widgets.py index 22810fac3..7386fbaad 100644 --- a/src/magicgui/backends/_qtpy/widgets.py +++ b/src/magicgui/backends/_qtpy/widgets.py @@ -29,7 +29,7 @@ from magicgui.types import FileDialogMode from magicgui.widgets import protocols from magicgui.widgets._concrete import _LabeledWidget -from magicgui.widgets.bases import Widget, MenuWidget +from magicgui.widgets.bases import MenuWidget, Widget if TYPE_CHECKING: import numpy diff --git a/src/magicgui/widgets/bases/_menubar.py b/src/magicgui/widgets/bases/_menubar.py index 05b699483..b66ae99ea 100644 --- a/src/magicgui/widgets/bases/_menubar.py +++ b/src/magicgui/widgets/bases/_menubar.py @@ -1,39 +1,82 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any, Callable +from typing import TYPE_CHECKING, Any, Callable, overload from ._widget import Widget if TYPE_CHECKING: from magicgui.widgets import protocols + from magicgui.widgets._concrete import Menu -class MenuBarWidget(Widget): - """Menu bar containing menus. Can be added to a MainWindowWidget.""" +class _SupportsMenus: + """Mixin for widgets that support menus.""" - _widget: protocols.MenuBarProtocol + _widget: protocols.MenuBarProtocol | protocols.MenuProtocol - def __init__(self, **base_widget_kwargs: Any) -> None: - super().__init__(**base_widget_kwargs) + def __init__(self, *args: Any, **kwargs: Any): self._menus: dict[str, MenuWidget] = {} + super().__init__(*args, **kwargs) def __getitem__(self, key: str) -> MenuWidget: return self._menus[key] + @overload + def add_menu(self, widget: Menu) -> MenuWidget: + ... + + @overload def add_menu(self, title: str, icon: str | None = None) -> MenuWidget: + ... + + def add_menu( + self, + *args: Any, + widget: Menu | None = None, + title: str = "", + icon: str | None = None, + ) -> MenuWidget: """Add a menu to the menu bar.""" - from magicgui.widgets._concrete import Menu + widget = _parse_menu_overload(args, widget, title, icon) + self._menus[widget.title] = widget + self._widget._mgui_add_menu_widget(widget) + return widget + + +def _parse_menu_overload( + args: tuple, widget: Menu | None = None, title: str = "", icon: str | None = None +) -> Menu: + from magicgui.widgets._concrete import Menu + + if len(args) == 2: + title, icon = args + elif len(args) == 1: + if not isinstance(arg0 := args[0], (str, Menu)): + raise TypeError("First argument must be a string or Menu") + if isinstance(arg0, Menu): + widget = arg0 + else: + title = arg0 + + if widget is None: + widget = Menu(title=title, icon=icon) + return widget - self._menus[title] = menu = Menu(title=title, icon=icon) - self._widget._mgui_add_menu_widget(menu) - return menu + +class MenuBarWidget(_SupportsMenus, Widget): + """Menu bar containing menus. Can be added to a MainWindowWidget.""" + + _widget: protocols.MenuBarProtocol + + def __init__(self, **base_widget_kwargs: Any) -> None: + super().__init__(**base_widget_kwargs) def clear(self) -> None: """Clear the menu bar.""" self._widget._mgui_clear() -class MenuWidget(Widget): +class MenuWidget(_SupportsMenus, Widget): """Menu widget. Can be added to a MenuBarWidget or another MenuWidget.""" _widget: protocols.MenuProtocol @@ -44,7 +87,6 @@ def __init__( super().__init__(**base_widget_kwargs) self.title = title self.icon = icon - self._menus: dict[str, MenuWidget] = {} @property def title(self) -> str: @@ -79,14 +121,6 @@ def add_separator(self) -> None: """Add a separator line to the menu.""" self._widget._mgui_add_separator() - def add_menu(self, title: str, icon: str | None = None) -> MenuWidget: - """Add a menu to the menu.""" - from magicgui.widgets._concrete import Menu - - self._menus[title] = menu = Menu(title=title, icon=icon) - self._widget._mgui_add_menu_widget(menu) - return menu - def clear(self) -> None: - """Clear the menu bar.""" + """Clear the menu.""" self._widget._mgui_clear() diff --git a/x.py b/x.py index b2e78ef5b..54d80a159 100644 --- a/x.py +++ b/x.py @@ -20,6 +20,7 @@ file_menu = main.menu_bar.add_menu("File") assert file_menu is main.menu_bar["File"] # can also access like this file_menu.add_action("Open", callback=lambda: print("Open")) + subm = file_menu.add_menu("Submenu") subm.add_action("Subaction", callback=lambda: print("Subaction")) subm.add_separator() From f9840467ec72abfa3189573213a46bef75109f1c Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Sun, 22 Oct 2023 13:30:36 -0400 Subject: [PATCH 25/32] fix main window sizing --- src/magicgui/backends/_qtpy/widgets.py | 95 ++++++++++++++++++++------ 1 file changed, 73 insertions(+), 22 deletions(-) diff --git a/src/magicgui/backends/_qtpy/widgets.py b/src/magicgui/backends/_qtpy/widgets.py index 7386fbaad..f12a6a48c 100644 --- a/src/magicgui/backends/_qtpy/widgets.py +++ b/src/magicgui/backends/_qtpy/widgets.py @@ -673,27 +673,44 @@ def _mgui_clear(self) -> None: self._qwidget.clear() -class MainWindow(Container, protocols.MainWindowProtocol): +class MainWindow(QBaseWidget, protocols.MainWindowProtocol): + _qwidget: QtW.QMainWindow + def __init__( self, layout="vertical", scrollable: bool = False, **kwargs: Any ) -> None: - super().__init__(layout=layout, scrollable=scrollable, **kwargs) - self._main_window = QtW.QMainWindow() - self._menus: dict[str, QtW.QMenu] = {} - if scrollable: - self._main_window.setCentralWidget(self._scroll) + QBaseWidget.__init__(self, QtW.QMainWindow, **kwargs) + if layout == "horizontal": + self._layout: QtW.QBoxLayout = QtW.QHBoxLayout() else: - self._main_window.setCentralWidget(self._qwidget) - # self._qwidget = self._main_window # TODO + self._layout = QtW.QVBoxLayout() + self._central_widget = QtW.QWidget() + self._central_widget.setLayout(self._layout) - def _mgui_get_visible(self): - return self._main_window.isVisible() + if scrollable: + self._scroll = QtW.QScrollArea() + # Allow widget to resize when window is larger than min widget size + self._scroll.setWidgetResizable(True) + if layout == "horizontal": + horiz_policy = Qt.ScrollBarPolicy.ScrollBarAsNeeded + vert_policy = Qt.ScrollBarPolicy.ScrollBarAlwaysOff + else: + horiz_policy = Qt.ScrollBarPolicy.ScrollBarAlwaysOff + vert_policy = Qt.ScrollBarPolicy.ScrollBarAsNeeded + self._scroll.setHorizontalScrollBarPolicy(horiz_policy) + self._scroll.setVerticalScrollBarPolicy(vert_policy) + self._scroll.setWidget(self._central_widget) + self._central_widget = self._scroll - def _mgui_set_visible(self, value: bool): - self._main_window.setVisible(value) + self._menus: dict[str, QtW.QMenu] = {} + if scrollable: + self._qwidget.setCentralWidget(self._scroll) + else: + self._qwidget.setCentralWidget(self._central_widget) - def _mgui_get_native_widget(self) -> QtW.QMainWindow: - return self._main_window + @property + def _is_scrollable(self) -> bool: + return isinstance(self._central_widget, QtW.QScrollArea) def _mgui_create_menu_item( self, @@ -703,9 +720,9 @@ def _mgui_create_menu_item( shortcut: str | None = None, ): menu = self._menus.setdefault( - menu_name, self._main_window.menuBar().addMenu(f"&{menu_name}") + menu_name, self._qwidget.menuBar().addMenu(f"&{menu_name}") ) - action = QtW.QAction(action_name, self._main_window) + action = QtW.QAction(action_name, self._qwidget) if shortcut is not None: action.setShortcut(shortcut) if callback is not None: @@ -718,7 +735,7 @@ def _mgui_add_tool_bar(self, widget: Widget, area: Area) -> None: raise TypeError( f"Expected widget to be a {QtW.QToolBar}, got {type(native)}" ) - self._main_window.addToolBar(Q_TB_AREA[area], native) + self._qwidget.addToolBar(Q_TB_AREA[area], native) def _mgui_add_dock_widget(self, widget: Widget, area: Area) -> None: native = widget.native @@ -728,11 +745,11 @@ def _mgui_add_dock_widget(self, widget: Widget, area: Area) -> None: # TODO: allowed areas dw = QtW.QDockWidget() dw.setWidget(native) - self._main_window.addDockWidget(Q_DW_AREA[area], dw) + self._qwidget.addDockWidget(Q_DW_AREA[area], dw) def _mgui_set_status_bar(self, widget: Widget | None) -> None: if widget is None: - self._main_window.setStatusBar(None) + self._qwidget.setStatusBar(None) return native = widget.native @@ -740,11 +757,11 @@ def _mgui_set_status_bar(self, widget: Widget | None) -> None: raise TypeError( f"Expected widget to be a {QtW.QStatusBar}, got {type(native)}" ) - self._main_window.setStatusBar(native) + self._qwidget.setStatusBar(native) def _mgui_set_menu_bar(self, widget: Widget | None) -> None: if widget is None: - self._main_window.setMenuBar(QtW.QMenuBar()) + self._qwidget.setMenuBar(QtW.QMenuBar()) return native = widget.native @@ -752,7 +769,41 @@ def _mgui_set_menu_bar(self, widget: Widget | None) -> None: raise TypeError( f"Expected widget to be a {QtW.QMenuBar}, got {type(native)}" ) - self._main_window.setMenuBar(native) + self._qwidget.setMenuBar(native) + + def _mgui_insert_widget(self, position: int, widget: Widget): + self._layout.insertWidget(position, widget.native) + if self._is_scrollable: + min_size = self._layout.totalMinimumSize() + if isinstance(self._layout, QtW.QHBoxLayout): + self._scroll.setMinimumHeight(min_size.height()) + else: + self._scroll.setMinimumWidth(min_size.width() + 20) + + def _mgui_remove_widget(self, widget: Widget): + self._layout.removeWidget(widget.native) + widget.native.setParent(None) + + def _mgui_get_margins(self) -> tuple[int, int, int, int]: + m = self._layout.contentsMargins() + return m.left(), m.top(), m.right(), m.bottom() + + def _mgui_set_margins(self, margins: tuple[int, int, int, int]) -> None: + self._layout.setContentsMargins(*margins) + + def _mgui_set_orientation(self, value) -> None: + """Set orientation, value will be 'horizontal' or 'vertical'.""" + raise NotImplementedError( + "Sorry, changing orientation after instantiation " + "is not yet implemented for Qt." + ) + + def _mgui_get_orientation(self) -> str: + """Set orientation, return either 'horizontal' or 'vertical'.""" + if isinstance(self, QtW.QHBoxLayout): + return "horizontal" + else: + return "vertical" Q_TB_AREA: dict[Area, Qt.ToolBarArea] = { From e88d3c058ef8159f659126eebe4c11690cc13e5d Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Sun, 22 Oct 2023 13:36:26 -0400 Subject: [PATCH 26/32] fix test --- src/magicgui/backends/_ipynb/widgets.py | 48 ++++++++----------------- tests/test_main_window.py | 34 ++++++++++++++++++ tests/test_widgets.py | 33 ----------------- 3 files changed, 49 insertions(+), 66 deletions(-) create mode 100644 tests/test_main_window.py diff --git a/src/magicgui/backends/_ipynb/widgets.py b/src/magicgui/backends/_ipynb/widgets.py index cb20c51df..50c1d5b8b 100644 --- a/src/magicgui/backends/_ipynb/widgets.py +++ b/src/magicgui/backends/_ipynb/widgets.py @@ -2,6 +2,7 @@ from typing import Any, Callable, Iterable, Literal, Optional, Tuple, Type, Union + try: import ipywidgets from ipywidgets import widgets as ipywdg @@ -11,7 +12,6 @@ "Please run `pip install ipywidgets`" ) from e - from magicgui.widgets import protocols from magicgui.widgets.bases import Widget @@ -524,10 +524,7 @@ def _mgui_get_orientation(self) -> str: return "vertical" if isinstance(self._ipywidget, ipywdg.VBox) else "horizontal" -from ipywidgets import GridspecLayout - - -class IpyMainWindow(GridspecLayout): +class IpyMainWindow(ipywdg.GridspecLayout): IDX_MENUBAR = (0, slice(None)) IDX_STATUSBAR = (6, slice(None)) IDX_TOOLBAR_TOP = (1, slice(None)) @@ -592,34 +589,7 @@ def add_dock_widget(self, widget, area: Literal["left", "top", "right", "bottom" raise ValueError(f"Invalid area: {area!r}") -# grid = GridspecLayout(7, 5, layout=layout, width="600px", height="600px") - -# grid[0, :] = Button( -# description="Menu Bar", button_style="danger", layout=Layout(**he_vf) -# ) -# grid[1, :] = Button(description="Toolbars", button_style="info", layout=Layout(**he_vf)) -# grid[2, 1:4] = Button( -# description="Dock Widgets", button_style="success", layout=Layout(**he_vf) -# ) -# grid[2:5, 0] = Button(description="T", button_style="info", layout=Layout(**hf_ve)) -# grid[3, 1] = Button(description="D", button_style="success", layout=Layout(**hf_ve)) -# grid[3, 2] = Button( -# description="Central Widget", -# button_style="warning", -# layout=Layout(height="auto", width="auto"), -# ) -# grid[3, 3] = Button(description="D", button_style="success", layout=Layout(**hf_ve)) -# grid[2:5, 4] = Button(description="T", button_style="info", layout=Layout(**hf_ve)) -# grid[4, 1:4] = Button(description="D", button_style="success", layout=Layout(**he_vf)) -# grid[5, :] = Button(description="T", button_style="info", layout=Layout(**he_vf)) -# grid[6, :] = Button( -# description="Status Bar", button_style="danger", layout=Layout(**he_vf) -# ) -# grid.layout.grid_template_columns = "34px 34px 1fr 34px 34px" -# grid.layout.grid_template_rows = "34px 34px 34px 1fr 34px 34px 34px" - - -class MainWindow(Container): +class MainWindow(Container, protocols.MainWindowProtocol): def __init__(self, layout="horizontal", scrollable: bool = False, **kwargs): self._ipywidget = IpyMainWindow() print(self._ipywidget) @@ -633,6 +603,18 @@ def _mgui_create_menu_item( ): pass + def _mgui_add_dock_widget(self, widget: Widget, area: "protocols.Area") -> None: + ... + + def _mgui_add_tool_bar(self, widget: Widget, area: "protocols.Area") -> None: + ... + + def _mgui_set_status_bar(self, widget: Widget | None) -> None: + ... + + def _mgui_set_menu_bar(self, widget: Widget | None) -> None: + ... + def get_text_width(text): # FIXME: how to do this in ipywidgets? diff --git a/tests/test_main_window.py b/tests/test_main_window.py new file mode 100644 index 000000000..90eff72c0 --- /dev/null +++ b/tests/test_main_window.py @@ -0,0 +1,34 @@ +from magicgui import magicgui, widgets + + +def test_main_function_gui(): + """Test that main_window makes the widget a top level main window with menus.""" + + @magicgui(main_window=True) + def add(num1: int, num2: int) -> int: + """Adds the given two numbers, returning the result. + + The function assumes that the two numbers can be added and does + not perform any prior checks. + + Parameters + ---------- + num1 , num2 : int + Numbers to be added + + Returns + ------- + int + Resulting integer + """ + + assert not add.visible + add.show() + assert add.visible + + assert isinstance(add, widgets.MainFunctionGui) + add._show_docs() + assert isinstance(add._help_text_edit, widgets.TextEdit) + assert add._help_text_edit.value.startswith("Adds the given two numbers") + assert add._help_text_edit.read_only + add.close() diff --git a/tests/test_widgets.py b/tests/test_widgets.py index 99df22cf1..399c7981b 100644 --- a/tests/test_widgets.py +++ b/tests/test_widgets.py @@ -400,39 +400,6 @@ def t(pbar: widgets.ProgressBar): assert t() == 23 -def test_main_function_gui(): - """Test that main_window makes the widget a top level main window with menus.""" - - @magicgui(main_window=True) - def add(num1: int, num2: int) -> int: - """Adds the given two numbers, returning the result. - - The function assumes that the two numbers can be added and does - not perform any prior checks. - - Parameters - ---------- - num1 , num2 : int - Numbers to be added - - Returns - ------- - int - Resulting integer - """ - - assert not add.visible - add.show() - assert add.visible - - assert isinstance(add, widgets.MainFunctionGui) - add._show_docs() - assert isinstance(add._help_text_edit, widgets.TextEdit) - assert add._help_text_edit.value.startswith("Adds the given two numbers") - assert add._help_text_edit.read_only - add.close() - - def test_range_widget(): args = (-100, 1000, 2) rw = widgets.RangeEdit(*args) From c8d4c5bdf0fba9bf32ad51e7c459adb36ae61062 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 22 Oct 2023 17:37:46 +0000 Subject: [PATCH 27/32] style(pre-commit.ci): auto fixes [...] --- src/magicgui/backends/_ipynb/widgets.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/magicgui/backends/_ipynb/widgets.py b/src/magicgui/backends/_ipynb/widgets.py index 50c1d5b8b..04c4e64f1 100644 --- a/src/magicgui/backends/_ipynb/widgets.py +++ b/src/magicgui/backends/_ipynb/widgets.py @@ -2,7 +2,6 @@ from typing import Any, Callable, Iterable, Literal, Optional, Tuple, Type, Union - try: import ipywidgets from ipywidgets import widgets as ipywdg From 917196add1daa0c8609e682e6bc243c3a76cad36 Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Sun, 22 Oct 2023 14:48:46 -0400 Subject: [PATCH 28/32] fix py38 --- src/magicgui/backends/_ipynb/widgets.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/magicgui/backends/_ipynb/widgets.py b/src/magicgui/backends/_ipynb/widgets.py index 04c4e64f1..911c7cead 100644 --- a/src/magicgui/backends/_ipynb/widgets.py +++ b/src/magicgui/backends/_ipynb/widgets.py @@ -597,8 +597,8 @@ def _mgui_create_menu_item( self, menu_name: str, action_name: str, - callback: Callable | None = None, - shortcut: str | None = None, + callback: Optional[Callable] = None, + shortcut: Optional[str] = None, ): pass @@ -608,10 +608,10 @@ def _mgui_add_dock_widget(self, widget: Widget, area: "protocols.Area") -> None: def _mgui_add_tool_bar(self, widget: Widget, area: "protocols.Area") -> None: ... - def _mgui_set_status_bar(self, widget: Widget | None) -> None: + def _mgui_set_status_bar(self, widget: Optional[Widget]) -> None: ... - def _mgui_set_menu_bar(self, widget: Widget | None) -> None: + def _mgui_set_menu_bar(self, widget: Optional[Widget]) -> None: ... From 706004fa5b21c7401767d53672daddcf590d9414 Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Sun, 22 Oct 2023 15:58:35 -0400 Subject: [PATCH 29/32] starting on ipywidgets --- src/magicgui/backends/_ipynb/__init__.py | 12 ++- src/magicgui/backends/_ipynb/widgets.py | 96 ++++++++++++++++++++++-- 2 files changed, 97 insertions(+), 11 deletions(-) diff --git a/src/magicgui/backends/_ipynb/__init__.py b/src/magicgui/backends/_ipynb/__init__.py index a01a0fbd8..7e49ada2c 100644 --- a/src/magicgui/backends/_ipynb/__init__.py +++ b/src/magicgui/backends/_ipynb/__init__.py @@ -12,12 +12,15 @@ LineEdit, LiteralEvalLineEdit, MainWindow, + Menu, + MenuBar, Password, PushButton, RadioButton, Select, Slider, SpinBox, + StatusBar, TextEdit, TimeEdit, ToolBar, @@ -33,23 +36,26 @@ "ComboBox", "Container", "DateEdit", - "TimeEdit", "DateTimeEdit", "EmptyWidget", "FloatSlider", "FloatSpinBox", + "get_text_width", "Label", "LineEdit", "LiteralEvalLineEdit", "MainWindow", + "Menu", + "MenuBar", "Password", "PushButton", "RadioButton", "Select", + "show_file_dialog", "Slider", "SpinBox", + "StatusBar", "TextEdit", + "TimeEdit", "ToolBar", - "get_text_width", - "show_file_dialog", ] diff --git a/src/magicgui/backends/_ipynb/widgets.py b/src/magicgui/backends/_ipynb/widgets.py index 911c7cead..84527e5a9 100644 --- a/src/magicgui/backends/_ipynb/widgets.py +++ b/src/magicgui/backends/_ipynb/widgets.py @@ -1,5 +1,6 @@ # from __future__ import annotations # NO +import asyncio from typing import Any, Callable, Iterable, Literal, Optional, Tuple, Type, Union try: @@ -12,7 +13,7 @@ ) from e from magicgui.widgets import protocols -from magicgui.widgets.bases import Widget +from magicgui.widgets.bases import MenuWidget, Widget def _pxstr2int(pxstr: Union[int, str]) -> int: @@ -554,8 +555,8 @@ def __init__(self, **kwargs): self[self.IDX_DOCK_LEFT] = self._dwdgs_left = ipywdg.VBox(layout=vlay) self[self.IDX_DOCK_RIGHT] = self._dwdgs_right = ipywdg.VBox(layout=vlay) - self.layout.grid_template_columns = "34px 34px 1fr 34px 34px" - self.layout.grid_template_rows = "34px 34px 34px 1fr 34px 34px 34px" + # self.layout.grid_template_columns = "34px 34px 1fr 34px 34px" + # self.layout.grid_template_rows = "34px 34px 34px 1fr 34px 34px 34px" def set_menu_bar(self, widget): self[self.IDX_MENUBAR] = widget @@ -588,10 +589,89 @@ def add_dock_widget(self, widget, area: Literal["left", "top", "right", "bottom" raise ValueError(f"Invalid area: {area!r}") +class StatusBar(_IPyWidget, protocols.StatusBarProtocol): + _ipywidget: ipywdg.HBox + + def __init__(self, **kwargs): + super().__init__(ipywdg.HBox, **kwargs) + self._ipywidget.layout.width = "100%" + + self._message_label = ipywdg.Label() + self._buttons = ipywdg.HBox() + # Spacer to push buttons to the right + self._spacer = ipywdg.HBox(layout=ipywdg.Layout(flex="1")) + self._ipywidget.children = (self._message_label, self._spacer, self._buttons) + + def _mgui_get_message(self) -> str: + return self._message_label.value + + def _clear_message(self): + self._message_label.value = "" + + def _mgui_set_message(self, message: str, timeout: int = 0) -> None: + self._message_label.value = message + if timeout > 0: + asyncio.get_event_loop().call_later(timeout / 1000, self._clear_message) + + def _mgui_insert_widget(self, position: int, widget: Widget) -> None: + self._ipywidget.children = ( + *self._ipywidget.children[:position], + widget.native, + *self._ipywidget.children[position:], + ) + + def _mgui_remove_widget(self, widget: Widget) -> None: + self._ipywidget.children = tuple( + child for child in self._ipywidget.children if child != widget.native + ) + + +class MenuBar(_IPyWidget, protocols.MenuBarProtocol): + def _mgui_add_menu_widget(self, widget: MenuWidget) -> None: + ... + + def _mgui_clear(self) -> None: + ... + + +class Menu(_IPyWidget, protocols.MenuProtocol): + def _mgui_add_menu_widget(self, widget: MenuWidget) -> None: + ... + + def _mgui_add_action( + self, + text: str, + shortcut: str | None = None, + icon: str | None = None, + tooltip: str | None = None, + callback: Callable[..., Any] | None = None, + ) -> None: + ... + + def _mgui_clear(self) -> None: + ... + + def _mgui_add_separator(self) -> None: + ... + + def _mgui_get_icon(self) -> str | None: + ... + + def _mgui_set_icon(self, icon: str | None) -> None: + ... + + def _mgui_get_title(self) -> str: + ... + + def _mgui_set_title(self, title: str) -> None: + ... + + class MainWindow(Container, protocols.MainWindowProtocol): + _ipywidget: IpyMainWindow + def __init__(self, layout="horizontal", scrollable: bool = False, **kwargs): self._ipywidget = IpyMainWindow() - print(self._ipywidget) def _mgui_create_menu_item( self, @@ -603,16 +683,16 @@ def _mgui_create_menu_item( pass def _mgui_add_dock_widget(self, widget: Widget, area: "protocols.Area") -> None: - ... + self._ipywidget.add_dock_widget(widget.native, area) def _mgui_add_tool_bar(self, widget: Widget, area: "protocols.Area") -> None: - ... + self._ipywidget.add_toolbar(widget.native, area) def _mgui_set_status_bar(self, widget: Optional[Widget]) -> None: - ... + self._ipywidget.set_status_bar(widget.native) def _mgui_set_menu_bar(self, widget: Optional[Widget]) -> None: - ... + self._ipywidget.set_menu_bar(widget.native) def get_text_width(text): From dc008d6d41b7d20e7d7279edf9602b73cccdaa75 Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Sun, 22 Oct 2023 16:03:24 -0400 Subject: [PATCH 30/32] update example --- x.py => example.py | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) rename x.py => example.py (61%) diff --git a/x.py b/example.py similarity index 61% rename from x.py rename to example.py index 54d80a159..0aa11fe9f 100644 --- a/x.py +++ b/example.py @@ -1,30 +1,28 @@ from magicgui import widgets -dw = widgets.PushButton(text="Hello World!") +main = widgets.MainWindow() + +# toolbar tb = widgets.ToolBar() -tb.add_button(text="Hello", icon="mdi:folder") +tb.add_button(text="Folder", icon="mdi:folder") tb.add_spacer() -tb.add_button(text="World!", icon="mdi:square-edit-outline") - - -main = widgets.MainWindow() -main.add_dock_widget(dw) +tb.add_button(text="Edit", icon="mdi:square-edit-outline") main.add_tool_bar(tb) -# sb = widgets.StatusBar() -# sb.set_message("Hello Status!") -# main.set_status_bar(sb) -# or -main.status_bar.set_message("") +# status bar +main.status_bar.set_message("Hello Status!", timeout=5000) + +# doc widgets +main.add_dock_widget(widgets.PushButton(text="Push me."), area="right") +# menus file_menu = main.menu_bar.add_menu("File") assert file_menu is main.menu_bar["File"] # can also access like this file_menu.add_action("Open", callback=lambda: print("Open")) - subm = file_menu.add_menu("Submenu") subm.add_action("Subaction", callback=lambda: print("Subaction")) subm.add_separator() subm.add_action("Subaction2", callback=lambda: print("Subaction2")) -main.width = 800 +main.height = 400 main.show(run=True) From e1a68e4a0ea75057858497d9faa50116906ffb86 Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Tue, 24 Oct 2023 13:13:27 -0400 Subject: [PATCH 31/32] add central --- example.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/example.py b/example.py index 0aa11fe9f..48e3b2849 100644 --- a/example.py +++ b/example.py @@ -24,5 +24,8 @@ subm.add_separator() subm.add_action("Subaction2", callback=lambda: print("Subaction2")) +# central widget +main.append(widgets.Label(value="Central widget")) + main.height = 400 main.show(run=True) From 778c0ea108d57d8167905d2e533cc3c0112b1f58 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 24 Oct 2023 23:27:40 +0000 Subject: [PATCH 32/32] style(pre-commit.ci): auto fixes [...] --- src/magicgui/backends/_ipynb/widgets.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/magicgui/backends/_ipynb/widgets.py b/src/magicgui/backends/_ipynb/widgets.py index 453cb01ab..7c5d1b64b 100644 --- a/src/magicgui/backends/_ipynb/widgets.py +++ b/src/magicgui/backends/_ipynb/widgets.py @@ -7,7 +7,6 @@ Callable, Iterable, Literal, - Optional, get_type_hints, ) @@ -687,8 +686,8 @@ def _mgui_create_menu_item( self, menu_name: str, action_name: str, - callback: Optional[Callable] = None, - shortcut: Optional[str] = None, + callback: Callable | None = None, + shortcut: str | None = None, ): pass @@ -698,10 +697,10 @@ def _mgui_add_dock_widget(self, widget: Widget, area: protocols.Area) -> None: def _mgui_add_tool_bar(self, widget: Widget, area: protocols.Area) -> None: self._ipywidget.add_toolbar(widget.native, area) - def _mgui_set_status_bar(self, widget: Optional[Widget]) -> None: + def _mgui_set_status_bar(self, widget: Widget | None) -> None: self._ipywidget.set_status_bar(widget.native) - def _mgui_set_menu_bar(self, widget: Optional[Widget]) -> None: + def _mgui_set_menu_bar(self, widget: Widget | None) -> None: self._ipywidget.set_menu_bar(widget.native)