feat: more complete main window implementation
This establishes the protocols and the qt backend for the main window elaboration discussed in #601.
This includes #597
it's turning into a large PR, so I might break it out into smaller ones, but this PR will serves as the cumulative progress.
cc @larsoner
for ease of review, here are the APIs this adds:
class MainWindowWidget(ContainerWidget):
"""Top level Application widget that can contain other widgets."""
def add_dock_widget(
self, widget: Widget, *, area: protocols.Area = "right"
) -> None:
"""Add a dock widget to the main window."""
def add_tool_bar(self, widget: Widget, *, area: protocols.Area = "top") -> None:
"""Add a toolbar to the main window."""
@property
def menu_bar(self) -> MenuBarWidget:
"""Return the menu bar widget."""
@menu_bar.setter
def menu_bar(self, widget: MenuBarWidget | None) -> None:
"""Set the menu bar widget."""
@property
def status_bar(self) -> StatusBarWidget:
"""Return the status bar widget."""
@status_bar.setter
def status_bar(self, widget: StatusBarWidget | None) -> None:
"""Set the status bar widget."""
@typing.deprecated
def create_menu_item(
self,
menu_name: str,
item_name: str,
callback: Callable | None = None,
shortcut: str | None = None,
) -> None:
...
class StatusBarWidget(Widget):
def add_widget(self, widget: Widget) -> None:
"""Add a widget to the statusbar."""
def insert_widget(self, position: int, widget: Widget) -> None:
"""Insert a widget at the given position."""
def remove_widget(self, widget: Widget) -> None:
"""Remove a widget from the statusbar."""
@property
def message(self) -> str:
"""Return currently shown message, or empty string if None."""
@message.setter
def message(self, message: str) -> None:
"""Return the message timeout in milliseconds."""
def set_message(self, message: str, timeout: int = 0) -> None:
"""Show a message in the status bar for a given timeout."""
class MenuBarWidget(Widget):
"""Menu bar containing menus. Can be added to a MainWindowWidget."""
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."""
def clear(self) -> None:
"""Clear the menu bar."""
class MenuWidget(Widget):
"""Menu widget. Can be added to a MenuBarWidget or another 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."""
def add_separator(self) -> None:
"""Add a separator line to the menu."""
def add_menu(self, title: str, icon: str | None = None) -> MenuWidget:
"""Add a menu to the menu."""
def clear(self) -> None:
"""Clear the menu bar."""
class ToolBarWidget(Widget):
def add_button(
self, text: str = "", icon: str = "", callback: Callable | None = None
) -> None:
"""Add an action to the toolbar."""
def add_separator(self) -> None:
"""Add a separator line to the toolbar."""
def add_spacer(self) -> None:
"""Add a spacer to the toolbar."""
def add_widget(self, widget: Widget) -> None:
"""Add a widget to the toolbar."""
@property
def icon_size(self) -> tuple[int, int] | None:
"""Return the icon size of the toolbar."""
@icon_size.setter
def icon_size(self, size: int | tuple[int, int] | None) -> None:
"""Set the icon size of the toolbar."""
def clear(self) -> None:
"""Clear the toolbar."""
Codecov Report
Attention: 167 lines in your changes are missing coverage. Please review.
Comparison is base (
aa38630) 87.75% compared to head (778c0ea) 85.42%.
Additional details and impacted files
@@ Coverage Diff @@
## main #604 +/- ##
==========================================
- Coverage 87.75% 85.42% -2.34%
==========================================
Files 40 43 +3
Lines 4705 5091 +386
==========================================
+ Hits 4129 4349 +220
- Misses 576 742 +166
| Files | Coverage Δ | |
|---|---|---|
| src/magicgui/backends/_ipynb/__init__.py | 100.00% <ø> (ø) |
|
| src/magicgui/backends/_qtpy/__init__.py | 100.00% <ø> (ø) |
|
| src/magicgui/widgets/__init__.py | 100.00% <ø> (ø) |
|
| src/magicgui/widgets/_concrete.py | 89.64% <100.00%> (+0.13%) |
:arrow_up: |
| src/magicgui/widgets/bases/__init__.py | 100.00% <100.00%> (ø) |
|
| src/magicgui/widgets/bases/_container_widget.py | 90.95% <ø> (-0.18%) |
:arrow_down: |
| src/magicgui/widgets/bases/_toolbar.py | 95.65% <100.00%> (-0.35%) |
:arrow_down: |
| src/magicgui/widgets/protocols.py | 100.00% <100.00%> (ø) |
|
| src/magicgui/application.py | 83.87% <90.00%> (+0.15%) |
:arrow_up: |
| src/magicgui/widgets/bases/_statusbar.py | 71.42% <71.42%> (ø) |
|
| ... and 4 more |
:umbrella: View full report in Codecov by Sentry.
:loudspeaker: Have feedback on the report? Share it here.
it's turning into a large PR, so I might break it out into smaller ones, but this PR will serves as the cumulative progress.
+963 −89 is large but not insurmountable. Let me know when it would help for me to look / try / whatever!
Thanks for checking in @larsoner! I was actually just gonna ping you today. I think the protocol is in good shape, and the qt backend is working pretty well. So it actually would be a great time for you to play with it and see if you can improve the ipywidgest backend.
I have a simple example.py in the root of this PR that I've been playing with. the ipywidgets backend loaded at one point, it just (mostly) does nothing for now. I implemented a barebones version of the GridSpec pattern you proposed.
I think you know ipywidgets better than I do, so if you had time and wanted to tinker a bit, i think just running example.py in jupyter and working on styles and implementing methods in the ipynb backend would be super useful! 🙏
+963 −89 is large but not insurmountable
it's true, and most of this PR is really just boilerplate adding the new protocols
I think you know ipywidgets better than I do
I am actually a bit of an ipywidgets newcomer but happy to give it a shot! Should be able to look next week