Repository: napari/napari

Issue:
KeyPress not detected depending on selected window area
### 🐛 Bug Report

Referencing this bug report on the [forums](https://forum.image.sc/t/napari-not-recognizing-key-press/73268/5), I'm having a similiar issue.

If I don't focus a mouse click on the lower left hand corner of the Napari viewer keypresses aren't recognised


### 💡 Steps to Reproduce

A minimal working example 

```
import napari

viewer = napari.Viewer(title='this is the viewer')

@viewer.bind_key('d')
def _print_keypress(viewer):
    print('key press detected!')
napari.run()
```

Then click on the main window (1)

![fbfae2591b6b7ac97438d007535f2197db82b24b_2_954x750](https://github.com/napari/napari/assets/1923114/bf5c56ca-09ab-40bf-b13e-71cb67513ab2)

Try to detect key presses using 'd'

Then click on the side bar area (2)

Try to detect key presses using 'd'



### 💡 Expected Behavior

I would expect keypresses to be found regardless of pressing, but they are not when the main area (1) is selected

### 🌎 Environment

napari: 0.4.18
Platform: Windows-10-10.0.22621-SP0
Python: 3.10.11 (tags/v3.10.11:7d4cc5a, Apr 5 2023, 00:38:17) [MSC v.1929 64 bit (AMD64)]
Qt: 5.15.2
PySide2: 5.15.2.1
NumPy: 1.26.1
SciPy: 1.11.3
Dask: 2023.10.0
VisPy: 0.12.2
magicgui: 0.7.3
superqt: 0.6.1
in-n-out: 0.1.9
app-model: 0.2.2
npe2: 0.7.2

OpenGL:
- GL version: 4.6.0 - Build 31.0.101.4502
- MAX_TEXTURE_SIZE: 16384

Screens:
- screen 1: resolution 1920x1080, scale 2.0
- screen 2: resolution 1920x1080, scale 2.0

Settings path:
- C:\Users\gordo\AppData\Local\napari\.venv_c05b69228fb815e80d4b7eeba47eb2b74c51cefa\settings.yaml

### 💡 Additional Context

_No response_


Discussion:
This issue has been mentioned on **Image.sc Forum**. There might be relevant details there:

https://forum.image.sc/t/napari-not-recognizing-key-press/73268/7

I can reproduce this on 0.4.18 & 0.4.19RC2 but only for the case of no image open, so the splash screen showing.
If I open an image it behaves as expected, could you try with an image open?
Or is interacting with an empty canvas a key part of your use-case?

Agreed I reproduce the behaviour you observe it works fine with an image open but not 
with no image open.

I can work with this issue as is but it would be nice to have as lots of example code doesn't mention you need an image open for these events to be detected.

https://forum.image.sc/t/napari-not-recognizing-key-press/73268/8?u=gnstevenson

Is there a rc3 candidate to test this? As suggested above? I have tried with 0.4.19RC2 and
Ok. My bad. I remember that there were three rc releases but forgot that I started numbering them from 0. Yes, rc2 is last one. 
All good! Appreciate the help is there any pointers toward what I can do to fix/debug the issue?

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
@@ -501,6 +501,32 @@ def changeEvent(self, event):
 
         super().changeEvent(event)
 
+    def keyPressEvent(self, event):
+        """Called whenever a key is pressed.
+
+        Parameters
+        ----------
+        event : qtpy.QtCore.QEvent
+            Event from the Qt context.
+        """
+        self._qt_viewer.canvas._scene_canvas._backend._keyEvent(
+            self._qt_viewer.canvas._scene_canvas.events.key_press, event
+        )
+        event.accept()
+
+    def keyReleaseEvent(self, event):
+        """Called whenever a key is released.
+
+        Parameters
+        ----------
+        event : qtpy.QtCore.QEvent
+            Event from the Qt context.
+        """
+        self._qt_viewer.canvas._scene_canvas._backend._keyEvent(
+            self._qt_viewer.canvas._scene_canvas.events.key_release, event
+        )
+        event.accept()
+
     def resizeEvent(self, event):
         """Override to handle original size before maximizing."""
         # the first resize event will have nonsense positions that we don't


Test patch:
diff --git a/napari/_qt/_tests/test_qt_viewer.py b/napari/_qt/_tests/test_qt_viewer.py
--- a/napari/_qt/_tests/test_qt_viewer.py
+++ b/napari/_qt/_tests/test_qt_viewer.py
@@ -11,7 +11,8 @@
 import pytest
 from imageio import imread
 from pytestqt.qtbot import QtBot
-from qtpy.QtGui import QGuiApplication
+from qtpy.QtCore import QEvent, Qt
+from qtpy.QtGui import QGuiApplication, QKeyEvent
 from qtpy.QtWidgets import QApplication, QMessageBox
 from scipy import ndimage as ndi
 
@@ -892,3 +893,25 @@ def test_background_color(qtbot, qt_viewer: QtViewer, dtype):
         npt.assert_array_equal(
             color_pixel, color, err_msg=f"background {background}"
         )
+
+
+def test_shortcut_passing(make_napari_viewer):
+    viewer = make_napari_viewer(ndisplay=3)
+    layer = viewer.add_labels(
+        np.zeros((2, 2, 2), dtype=np.uint8), scale=(1, 2, 4)
+    )
+    layer.mode = "fill"
+
+    qt_window = viewer.window._qt_window
+
+    qt_window.keyPressEvent(
+        QKeyEvent(
+            QEvent.Type.KeyPress, Qt.Key.Key_1, Qt.KeyboardModifier.NoModifier
+        )
+    )
+    qt_window.keyReleaseEvent(
+        QKeyEvent(
+            QEvent.Type.KeyPress, Qt.Key.Key_1, Qt.KeyboardModifier.NoModifier
+        )
+    )
+    assert layer.mode == "erase"