Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: more complete main window implementation #604

Open
wants to merge 43 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
b19e898
wip
tlambert03 Oct 9, 2023
a2244de
update ipywidgets implementation
tlambert03 Oct 9, 2023
bad9c5e
fix hints
tlambert03 Oct 9, 2023
5f75b46
add test
tlambert03 Oct 9, 2023
804da76
feat: support button icons
tlambert03 Oct 9, 2023
c991ce0
adding iconbtn
tlambert03 Oct 10, 2023
b3a29ec
match color to palette in qt
tlambert03 Oct 10, 2023
d48b10d
update ipywidgets
tlambert03 Oct 10, 2023
a665276
bump superqt
tlambert03 Oct 10, 2023
2c1a201
add pytest-pretty
tlambert03 Oct 10, 2023
5d1b227
test: add tests
tlambert03 Oct 10, 2023
1b907d1
fix icon
tlambert03 Oct 10, 2023
6933b32
extract logic, fix 3.8
tlambert03 Oct 10, 2023
721a0b9
Merge branch 'icons' into toolbars
tlambert03 Oct 10, 2023
d27c283
update color
tlambert03 Oct 10, 2023
62daa67
change with palette
tlambert03 Oct 10, 2023
1e89504
Merge branch 'main' into toolbars
tlambert03 Oct 11, 2023
7f39313
unions
tlambert03 Oct 11, 2023
ff3b001
wip
tlambert03 Oct 18, 2023
9385303
Merge remote-tracking branch 'origin/toolbars' into main-window
tlambert03 Oct 18, 2023
8242141
wpi
tlambert03 Oct 19, 2023
db0e68f
adding menus
tlambert03 Oct 20, 2023
c48e9d0
Merge branch 'main' into main-window
tlambert03 Oct 20, 2023
db81067
style(pre-commit.ci): auto fixes [...]
pre-commit-ci[bot] Oct 20, 2023
b100bda
extend menu
tlambert03 Oct 20, 2023
3b9f785
Merge branch 'main-window' of https://github.com/tlambert03/magicgui …
tlambert03 Oct 20, 2023
025323d
menu stuff
tlambert03 Oct 20, 2023
b2584d9
style(pre-commit.ci): auto fixes [...]
pre-commit-ci[bot] Oct 20, 2023
5704bee
update menus
tlambert03 Oct 20, 2023
fd0a329
Merge branch 'main-window' of https://github.com/tlambert03/magicgui …
tlambert03 Oct 20, 2023
277e76e
Merge branch 'main' into main-window
tlambert03 Oct 20, 2023
f984046
fix main window sizing
tlambert03 Oct 22, 2023
e88d3c0
fix test
tlambert03 Oct 22, 2023
c8d4c5b
style(pre-commit.ci): auto fixes [...]
pre-commit-ci[bot] Oct 22, 2023
5654b56
Merge branch 'main' into main-window
tlambert03 Oct 22, 2023
7dd4da8
Merge branch 'main-window' of https://github.com/tlambert03/magicgui …
tlambert03 Oct 22, 2023
917196a
fix py38
tlambert03 Oct 22, 2023
706004f
starting on ipywidgets
tlambert03 Oct 22, 2023
dc008d6
update example
tlambert03 Oct 22, 2023
e1a68e4
add central
tlambert03 Oct 24, 2023
300c79a
Merge branch 'main' into main-window
tlambert03 Oct 24, 2023
511a174
Merge branch 'main-window' of https://github.com/tlambert03/magicgui …
tlambert03 Oct 24, 2023
778c0ea
style(pre-commit.ci): auto fixes [...]
pre-commit-ci[bot] Oct 24, 2023
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
31 changes: 31 additions & 0 deletions example.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
from magicgui import widgets

main = widgets.MainWindow()

# toolbar
tb = widgets.ToolBar()
tb.add_button(text="Folder", icon="mdi:folder")
tb.add_spacer()
tb.add_button(text="Edit", icon="mdi:square-edit-outline")
main.add_tool_bar(tb)

# 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"))

# central widget
main.append(widgets.Label(value="Central widget"))

main.height = 400
main.show(run=True)
19 changes: 16 additions & 3 deletions src/magicgui/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -12,10 +12,23 @@
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

ipy_class = get_ipython().__class__.__name__
return bool(ipy_class == "ZMQInteractiveShell")
return False

Check warning on line 25 in src/magicgui/application.py

View check run for this annotation

Codecov / codecov/patch

src/magicgui/application.py#L25

Added line #L25 was not covered by tests


def _choose_backend() -> str:
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."""
Expand Down Expand Up @@ -49,7 +62,7 @@
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}, "
Expand Down
14 changes: 11 additions & 3 deletions src/magicgui/backends/_ipynb/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,16 @@
Label,
LineEdit,
LiteralEvalLineEdit,
MainWindow,
Menu,
MenuBar,
Password,
PushButton,
RadioButton,
Select,
Slider,
SpinBox,
StatusBar,
TextEdit,
TimeEdit,
ToolBar,
Expand All @@ -32,22 +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",
]
184 changes: 181 additions & 3 deletions src/magicgui/backends/_ipynb/widgets.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
from __future__ import annotations

from typing import TYPE_CHECKING, Any, Callable, Iterable, get_type_hints
import asyncio
from typing import (
TYPE_CHECKING,
Any,
Callable,
Iterable,
Literal,
get_type_hints,
)

try:
import ipywidgets
Expand All @@ -11,11 +19,10 @@
"Please run `pip install ipywidgets`"
) from e


from magicgui.widgets import protocols

if TYPE_CHECKING:
from magicgui.widgets.bases import Widget
from magicgui.widgets.bases import MenuWidget, Widget


def _pxstr2int(pxstr: int | str) -> int:
Expand Down Expand Up @@ -526,6 +533,177 @@
return "vertical" if isinstance(self._ipywidget, ipywdg.VBox) else "horizontal"


class IpyMainWindow(ipywdg.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

Check warning on line 571 in src/magicgui/backends/_ipynb/widgets.py

View check run for this annotation

Codecov / codecov/patch

src/magicgui/backends/_ipynb/widgets.py#L571

Added line #L571 was not covered by tests

def set_status_bar(self, widget):
self[self.IDX_STATUSBAR] = widget

Check warning on line 574 in src/magicgui/backends/_ipynb/widgets.py

View check run for this annotation

Codecov / codecov/patch

src/magicgui/backends/_ipynb/widgets.py#L574

Added line #L574 was not covered by tests

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,)

Check warning on line 584 in src/magicgui/backends/_ipynb/widgets.py

View check run for this annotation

Codecov / codecov/patch

src/magicgui/backends/_ipynb/widgets.py#L577-L584

Added lines #L577 - L584 were not covered by tests
else:
raise ValueError(f"Invalid area: {area!r}")

Check warning on line 586 in src/magicgui/backends/_ipynb/widgets.py

View check run for this annotation

Codecov / codecov/patch

src/magicgui/backends/_ipynb/widgets.py#L586

Added line #L586 was not covered by tests

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,)

Check warning on line 596 in src/magicgui/backends/_ipynb/widgets.py

View check run for this annotation

Codecov / codecov/patch

src/magicgui/backends/_ipynb/widgets.py#L589-L596

Added lines #L589 - L596 were not covered by tests
else:
raise ValueError(f"Invalid area: {area!r}")

Check warning on line 598 in src/magicgui/backends/_ipynb/widgets.py

View check run for this annotation

Codecov / codecov/patch

src/magicgui/backends/_ipynb/widgets.py#L598

Added line #L598 was not covered by tests


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

Check warning on line 615 in src/magicgui/backends/_ipynb/widgets.py

View check run for this annotation

Codecov / codecov/patch

src/magicgui/backends/_ipynb/widgets.py#L615

Added line #L615 was not covered by tests

def _clear_message(self):
self._message_label.value = ""

Check warning on line 618 in src/magicgui/backends/_ipynb/widgets.py

View check run for this annotation

Codecov / codecov/patch

src/magicgui/backends/_ipynb/widgets.py#L618

Added line #L618 was not covered by tests

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)

Check warning on line 623 in src/magicgui/backends/_ipynb/widgets.py

View check run for this annotation

Codecov / codecov/patch

src/magicgui/backends/_ipynb/widgets.py#L621-L623

Added lines #L621 - L623 were not covered by tests

def _mgui_insert_widget(self, position: int, widget: Widget) -> None:
self._ipywidget.children = (

Check warning on line 626 in src/magicgui/backends/_ipynb/widgets.py

View check run for this annotation

Codecov / codecov/patch

src/magicgui/backends/_ipynb/widgets.py#L626

Added line #L626 was not covered by tests
*self._ipywidget.children[:position],
widget.native,
*self._ipywidget.children[position:],
)

def _mgui_remove_widget(self, widget: Widget) -> None:
self._ipywidget.children = tuple(

Check warning on line 633 in src/magicgui/backends/_ipynb/widgets.py

View check run for this annotation

Codecov / codecov/patch

src/magicgui/backends/_ipynb/widgets.py#L633

Added line #L633 was not covered by tests
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()

def _mgui_create_menu_item(
self,
menu_name: str,
action_name: str,
callback: Callable | None = None,
shortcut: str | None = None,
):
pass

def _mgui_add_dock_widget(self, widget: Widget, area: protocols.Area) -> None:
self._ipywidget.add_dock_widget(widget.native, area)

Check warning on line 695 in src/magicgui/backends/_ipynb/widgets.py

View check run for this annotation

Codecov / codecov/patch

src/magicgui/backends/_ipynb/widgets.py#L695

Added line #L695 was not covered by tests

def _mgui_add_tool_bar(self, widget: Widget, area: protocols.Area) -> None:
self._ipywidget.add_toolbar(widget.native, area)

Check warning on line 698 in src/magicgui/backends/_ipynb/widgets.py

View check run for this annotation

Codecov / codecov/patch

src/magicgui/backends/_ipynb/widgets.py#L698

Added line #L698 was not covered by tests

def _mgui_set_status_bar(self, widget: Widget | None) -> None:
self._ipywidget.set_status_bar(widget.native)

Check warning on line 701 in src/magicgui/backends/_ipynb/widgets.py

View check run for this annotation

Codecov / codecov/patch

src/magicgui/backends/_ipynb/widgets.py#L701

Added line #L701 was not covered by tests

def _mgui_set_menu_bar(self, widget: Widget | None) -> None:
self._ipywidget.set_menu_bar(widget.native)

Check warning on line 704 in src/magicgui/backends/_ipynb/widgets.py

View check run for this annotation

Codecov / codecov/patch

src/magicgui/backends/_ipynb/widgets.py#L704

Added line #L704 was not covered by tests


def get_text_width(text):
# FIXME: how to do this in ipywidgets?
return 40
6 changes: 6 additions & 0 deletions src/magicgui/backends/_qtpy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
LineEdit,
LiteralEvalLineEdit,
MainWindow,
Menu,
MenuBar,
Password,
ProgressBar,
PushButton,
Expand All @@ -25,6 +27,7 @@
Select,
Slider,
SpinBox,
StatusBar,
Table,
TextEdit,
TimeEdit,
Expand All @@ -51,6 +54,8 @@
"LineEdit",
"LiteralEvalLineEdit",
"MainWindow",
"MenuBar",
"Menu",
"Password",
"ProgressBar",
"PushButton",
Expand All @@ -62,6 +67,7 @@
"show_file_dialog",
"Slider",
"SpinBox",
"StatusBar",
"Table",
"TextEdit",
"TimeEdit",
Expand Down
Loading
Loading