Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/magicgui/backends/_ipynb/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
SpinBox,
TextEdit,
TimeEdit,
ToolBar,
get_text_width,
)

Expand Down Expand Up @@ -46,6 +47,7 @@
"Slider",
"SpinBox",
"TextEdit",
"ToolBar",
"get_text_width",
"show_file_dialog",
]
61 changes: 61 additions & 0 deletions src/magicgui/backends/_ipynb/widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,67 @@ class TimeEdit(_IPyValueWidget):
_ipywidget: ipywdg.TimePicker


class ToolBar(_IPyWidget):
_ipywidget: ipywidgets.HBox

def __init__(self, **kwargs):
super().__init__(ipywidgets.HBox, **kwargs)
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."""
btn = ipywdg.Button(
description=text, icon=icon, layout={"width": "auto", "height": "auto"}
)
if callback:
btn.on_click(lambda e: callback())
self._add_ipywidget(btn)

def _add_ipywidget(self, widget: "ipywidgets.Widget") -> None:
children = list(self._ipywidget.children)
children.append(widget)
self._ipywidget.children = children

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."""
self._add_ipywidget(widget.native)

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: Union[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."""
self._ipywidget.children = ()


class PushButton(_IPyButtonWidget):
_ipywidget: ipywdg.Button

Expand Down
2 changes: 2 additions & 0 deletions src/magicgui/backends/_qtpy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
Table,
TextEdit,
TimeEdit,
ToolBar,
get_text_width,
show_file_dialog,
)
Expand Down Expand Up @@ -64,4 +65,5 @@
"Table",
"TextEdit",
"TimeEdit",
"ToolBar",
]
59 changes: 58 additions & 1 deletion src/magicgui/backends/_qtpy/widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -1217,6 +1217,63 @@ 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)
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()):
act.setIcon(qicon)
act.setData(icon)

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) -> tuple[int, int] | None:
"""Return the icon size of the toolbar."""
sz = self._qwidget.iconSize()
return None if sz.isNull() else (sz.width(), sz.height())

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 = 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."""
self._qwidget.clear()


class Dialog(QBaseWidget, protocols.ContainerProtocol):
def __init__(
self, layout="vertical", scrollable: bool = False, **kwargs: Any
Expand Down
2 changes: 2 additions & 0 deletions src/magicgui/widgets/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
SpinBox,
TextEdit,
TimeEdit,
ToolBar,
TupleEdit,
)
from ._dialogs import request_values, show_file_dialog
Expand Down Expand Up @@ -107,6 +108,7 @@
"Table",
"TextEdit",
"TimeEdit",
"ToolBar",
"TupleEdit",
"Widget",
"show_file_dialog",
Expand Down
6 changes: 6 additions & 0 deletions src/magicgui/widgets/_concrete.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
MultiValuedSliderWidget,
RangedWidget,
SliderWidget,
ToolBarWidget,
TransformedRangedWidget,
ValueWidget,
Widget,
Expand Down Expand Up @@ -969,6 +970,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."""

Expand Down
4 changes: 3 additions & 1 deletion src/magicgui/widgets/bases/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,20 +48,22 @@ 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

__all__ = [
"ButtonWidget",
"CategoricalWidget",
"ContainerWidget",
"create_widget",
"DialogWidget",
"MainWindowWidget",
"MultiValuedSliderWidget",
"RangedWidget",
"SliderWidget",
"ToolBarWidget",
"TransformedRangedWidget",
"ValueWidget",
"Widget",
"create_widget",
]
60 changes: 60 additions & 0 deletions src/magicgui/widgets/bases/_toolbar.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
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)

@property
def icon_size(self) -> tuple[int, int] | None:
"""Return the icon size of the toolbar."""
return self._widget._mgui_get_icon_size()

@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."""
self._widget._mgui_clear()
35 changes: 35 additions & 0 deletions src/magicgui/widgets/protocols.py
Original file line number Diff line number Diff line change
Expand Up @@ -515,6 +515,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) -> tuple[int, int] | None:
"""Return the icon size of the toolbar."""

@abstractmethod
def _mgui_set_icon_size(self, size: int | tuple[int, int] | None) -> 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."""

Expand Down
11 changes: 11 additions & 0 deletions tests/test_widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -1056,3 +1056,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()