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

MainWindow for ipywidgets #601

Open
tlambert03 opened this issue Oct 11, 2023 · 5 comments
Open

MainWindow for ipywidgets #601

tlambert03 opened this issue Oct 11, 2023 · 5 comments

Comments

@tlambert03
Copy link
Member

the Qt backend has a MainWindow but the ipywidgets backend doesn't. As @larsoner has pointed out, the closest ipywidgets analog is probably AppLayout

In Qt, a main window is characterized by the ability to add a menu bar (top), status bar (bottom), dock widgets, and toolbars:

mainwindowlayout

In ipywidgets, it's more a layout issue, with a header, a footer, two sidebars and a central pane:

Screen Shot 2023-10-11 at 5 24 57 PM

Currently, our MainWindow protocol is a minimal subclass of Container that can add menus:

class MainWindowProtocol(ContainerProtocol, Protocol):
    def _mgui_create_menu_item(
        self,
        menu_name: str,
        action_name: str,
        callback: Callable | None = None,
        shortcut: str | None = None,
    ) -> None:

but will likely gain _mgui_add_toolbar after #597...

This raises the question of how represent these somewhat different concepts of "main window" in our API. If we use AppLayout behind the scenes, how do we assign stuff to left/middle/right? Or do we just ignore the left and right (and allow people to manually edit using widget.native if they choose.

fwiw, it looks like mne-python doesn't use AppLayout directly anywhere?

@larsoner, any insights/opinions from your mne applications?

@larsoner
Copy link

fwiw, it looks like mne-python doesn't use AppLayout directly anywhere?

Correct, currently we don't. I'm planning to rework all that stuff from scratch with magicgui. My issue / WIP gist sandbox uses AppLayout, though, since we use the left/center/right scheme in the two GUIs that support Qt and notebooks.

I think Qt MainWindow is (much?) more widely used than ipywidgets AppLayout and also looks more feature-complete, so it might make the most sense to roughly follow the Qt naming scheme / model and squeeze this stuff into ipywidgets somehow. One possible way to unify Qt and notebook is to map the notebook AppLayout onto the Qt model by doing:

ipywidgets AppLayout Qt MainWindow
Header split into 3 rows Menu Bar, Toolbars, Dock widgets (top)
Left Dock Widgets (left)
Center CentralWidget
Right Dock Widgets (right)
Footer split into 2 rows Dock Widgets (bottom) and Status Bar

This allows only toolbars at the top, but that seems to be by far the most widely used mode so I think this is okay.

Another option beyond AppLayout could be ipywidgets GridSpecLayout. TBD if that's easier to get to work as a MainWindow-like "thing". I can play around with it a bit if it would help @tlambert03 !

FWIW In MNE we currently use all of the entries in the second column above except Dock Widgets (top) and Dock Widgets (Bottom).

@larsoner
Copy link

Here is at least some proof of concept for GridspecLayout being able to act like QMainWindow:

ipywidgets code
from ipywidgets import Button, Layout, jslink, IntText, IntSlider, GridspecLayout
grid = GridspecLayout(7, 5, layout=layout, width="600px", height="600px")
he_vf = dict(height='30px', width='auto')
hf_ve = dict(height="auto", width="30px")
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"
grid

Screenshot from 2023-10-13 09-30-09

@tlambert03
Copy link
Member Author

thanks so much @larsoner ... that proposal seems as good as any to me!

it might make the most sense to roughly follow the Qt naming scheme / model and squeeze this stuff into ipywidgets somehow

I agree with this too and i like your proposed model. I'm sure we'll encounter some challenges with it at some point (related to mismatched user expectations)... but since the two models don't map exactly onto each other it does seem like we just have to make an opinion.

Another option beyond AppLayout could be ipywidgets GridSpecLayout. TBD if that's easier to get to work as a MainWindow-like "thing". I can play around with it a bit if it would help @tlambert03 !

Your help on that would be awesome. I like what you've started with. I'd be curious to hear what you would propose for methods and their behavior on the magicgui.widgets.MainWindow protocol. that is, how exactly does a magicgui gui user put something in the each of the corresponding places? would you go for add_dock_widget, add_toolbar , set_menu_bar, set_status_bar like methods? And those methods must be passed the corresponding widget.ToolBar, widget.MenuBar, etc... just like the Qt API?

@larsoner
Copy link

would you go for add_dock_widget, add_toolbar , set_menu_bar, set_status_bar like methods? And those methods must be passed the corresponding widget.ToolBar, widget.MenuBar, etc... just like the Qt API?

Yeah that sounds good! magicgui.widgets.StatusBar can be QStatusBar in Qt and just a Container with a single Label or so in ipywidgets. Similar for the ToolBar I think. TBD how MenuBar could be implemented -- maybe that would need to be at the ipywidgets end first (or a DropDown with some clever CSS and on_change event handling)? I think having ToolBar, StatusBar, and MenuBar classes should allow magicgui to adapt the behavior of these classes so they do what users expect. For example, adding a ToolBar to the top area should have the icons/buttons arranged horizontally, whereas adding it to the left area they should be arranged vertically, etc.

To keep things simple to start we could allow just a single widget per area: one ToolBar per toolbar area top/bottom/left/right (Qt allows multiple per area I think), a single Widget per dock area top/bottom/left/right, a single Widget as the central widget, etc. Someday if we want to allow multiple per area we can add a append=False or replace=True default kwarg or something but hopefully people can just use Containers if they need extra stuff.

@tlambert03
Copy link
Member Author

Love it!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants