Repository: napari/napari

Issue:
"Toggle menubar visibility" trap
## 🐛 Bug

After turning off the menubar by selecting "toggle menubar visibility" from the "view" dropdown menu, there are no longer any GUI elements to reverse this change. This feels like a trap (the only way out is is to memorize a keyboard shortcut). Users who do not memorize the shortcut will be forced to kill + restart napari if they want the menubar back.

## Environment

 - Please copy and paste the information at napari info option in help menubar here:
```
napari: 0.3.1+2.g96e9722
Platform: Linux-5.3.0-51-generic-x86_64-with-glibc2.10
Python: 3.8.2 (default, Mar 26 2020, 15:53:00) [GCC 7.3.0]
Qt: 5.12.5
PyQt5: 5.12.3
NumPy: 1.18.1
SciPy: 1.4.1
Dask: 2.16.0
VisPy: 0.6.4

GL version: 4.6.0 NVIDIA 435.21
MAX_TEXTURE_SIZE: 32768

Plugins:
- napari-plugin-engine: 0.1.5
- svg: 0.1.2
```

What is the use case is for the "toggle menubar visibility" option anyway? I would think that by definition the menubar contains actions / settings that affect the entire napari app, and thus this is the one UI element that should always be accessible. But maybe there is some usage I'm not considering.


Discussion:
![source](https://user-images.githubusercontent.com/1609449/81972087-1548be80-95f0-11ea-81bc-146b4147cc1e.gif)

I managed to do this during the Neubias presentation this morning . :)
> I managed to do this during the Neubias presentation this morning . :)

😬 

> What is the use case is for the "toggle menubar visibility" option anyway? I would think that by definition the menubar contains actions / settings that affect the entire napari app, and thus this is the one UI element that should always be accessible. But maybe there is some usage I'm not considering.

This dates from the first time we added a menubar. A menubar seemed redundant and an annoying waste of screen real-estate for those that are only using the viewer from Python (such as myself). So I wanted a way to get rid of it. Originally I detected whether napari was being run from the command-line and only put a menubar then, but @sofroniewn worried about the consistency of the experience. I *still* think the menubar is redundant and annoying on non-Mac platforms when not using the app, but I've gotten used to it and don't compulsively press Ctrl-M anymore. Additionally, the amount of useful stuff in the menubar keeps growing. So one option is to just remove it.

*But* I wouldn't mind if we thought a bit about solutions that involved not having the menubar there when using Python. OR, maybe we don't show the remove menubar option if launching from the command-line or an eventual bundle. Or maybe even add a visual way to get the menubar back...!
I would say something like a small, but very obviously bright Plus (+) symbol you could click on in one of the corners would work (Act as an undo from the time of the toggle?). Give it a small square background of opposing color just in case of weird image coloration issues? I know nothing about Python UI coding so I don't know how hard that would be.
I use macOS so this bug is pre-prevented for me, but I'd be a strong proponent of just removing this "feature". The utility is what, 10–20 pixel saved?—I don't say this lightly as I use a 13" monitor—while the consequences are 😬
I've asked around Windows users and apps without menus are not the norm and rather a troubling symptom of non-native UIs.
Not sure the situation on Linux, so we could just hide this on windows in addition to the current macOS protection.
Firefox on Linux and Windows hide the menu (press Alt to show it). But provides a hamburger menu to provide access to the most important functions. 
What @Czaki said. 😃 It's a "hidden" feature of many apps but it's not non-standard behaviour. tbh the rationale here is that in some situations, we don't want a menu bar. Just like a matplotlib plot doesn't have a menubar, there's an argument for an "as full-screen as possible" viewer in many situations, especially when working from Python. So, one idea might be to disable this in the bundle? Or, preferably, to add a more obvious way to bring it back, e.g. with a hamburger menu.
Or remove it from view menu and left only as keyboard shortcut?
> Or remove it from view menu and left only as keyboard shortcut?

That would work right? someone needs to know the command to trigger it, so then they'd know how to undo, avoiding the trap?
and if they accidentally do it by keybind, then they can't undo it anyways, so nothing is harmed vs. current state.

Resolving patch:
diff --git a/napari/_qt/qt_main_window.py b/napari/_qt/qt_main_window.py
--- a/napari/_qt/qt_main_window.py
+++ b/napari/_qt/qt_main_window.py
@@ -131,6 +131,7 @@ def __init__(
         self._window_pos = None
         self._old_size = None
         self._positions = []
+        self._toggle_menubar_visibility = False
 
         self._is_close_dialog = {False: True, True: True}
         # this ia sa workaround for #5335 issue. The dict is used to not
@@ -277,6 +278,32 @@ def showFullScreen(self):
         else:
             super().showFullScreen()
 
+    def eventFilter(self, source, event):
+        # Handle showing hidden menubar on mouse move event.
+        # We do not hide menubar when a menu is being shown or
+        # we are not in menubar toggled state
+        if (
+            QApplication.activePopupWidget() is None
+            and self._toggle_menubar_visibility
+        ):
+            if event.type() == QEvent.MouseMove:
+                if self.menuBar().isHidden():
+                    rect = self.geometry()
+                    # set mouse-sensitive zone to trigger showing the menubar
+                    rect.setHeight(25)
+                    if rect.contains(event.globalPos()):
+                        self.menuBar().show()
+                else:
+                    rect = QRect(
+                        self.menuBar().mapToGlobal(QPoint(0, 0)),
+                        self.menuBar().size(),
+                    )
+                    if not rect.contains(event.globalPos()):
+                        self.menuBar().hide()
+            elif event.type() == QEvent.Leave and source is self:
+                self.menuBar().hide()
+        return QMainWindow.eventFilter(self, source, event)
+
     def _load_window_settings(self):
         """
         Load window layout settings from configuration.
@@ -526,6 +553,16 @@ def restart(self):
         process.startDetached()
         self.close(quit_app=True)
 
+    def toggle_menubar_visibility(self):
+        """
+        Change menubar to be shown or to be hidden and shown on mouse movement.
+
+        For the mouse movement functionality see the `eventFilter` implementation.
+        """
+        self._toggle_menubar_visibility = not self._toggle_menubar_visibility
+        self.menuBar().setVisible(not self._toggle_menubar_visibility)
+        return self._toggle_menubar_visibility
+
     @staticmethod
     @Slot(Notification)
     def show_notification(notification: Notification):
@@ -558,7 +595,7 @@ class Window:
 
     def __init__(self, viewer: 'Viewer', *, show: bool = True) -> None:
         # create QApplication if it doesn't already exist
-        get_app()
+        qapp = get_app()
 
         # Dictionary holding dock widgets
         self._dock_widgets: Dict[
@@ -568,6 +605,7 @@ def __init__(self, viewer: 'Viewer', *, show: bool = True) -> None:
 
         # Connect the Viewer and create the Main Window
         self._qt_window = _QtMainWindow(viewer, self)
+        qapp.installEventFilter(self._qt_window)
 
         # connect theme events before collecting plugin-provided themes
         # to ensure icons from the plugins are generated correctly.
@@ -758,8 +796,8 @@ def _toggle_menubar_visible(self):
         show the menubar, since menubar shortcuts are only available while the
         menubar is visible.
         """
-        self.main_menu.setVisible(not self.main_menu.isVisible())
-        self._main_menu_shortcut.setEnabled(not self.main_menu.isVisible())
+        toggle_menubar_visibility = self._qt_window.toggle_menubar_visibility()
+        self._main_menu_shortcut.setEnabled(toggle_menubar_visibility)
 
     def _toggle_fullscreen(self):
         """Toggle fullscreen mode."""


Test patch: