Repository: ppizarror/pygame-menu

Issue:
Navigation is delayed sometimes but mostly not moving
**Environment information**
Describe your environment information, such as:
- SO: linux
- python version: v3.7.3
- pygame version: v1.9.6
- pygame-menu version: v4.1.1

**Describe the bug**
The navigation to Menu options is delayed or not moving sometimes. I am using PyAutoGUI to emulate some keyboard press event from GPIO. When a GPIO pin is HIGH it calls the function press from PyAutoGUI. Also, Im trying to map WASD button to arrow keys. The keyboard event is catched by the pygame.events but the selector is sometimes moves but mostly it doesn't move.

**To Reproduce**

    from pyautogui import press

    while True:
        main_menu = pygame_menu.Menu(
            title="Main Test Menu",
            width=800,
            height=600,
            theme=pygame_menu.themes.THEME_DARK,
            joystick_enabled=False,
            mouse_enabled=False,
            mouse_motion_selection=False,
            touchscreen=False,
            touchscreen_motion_selection=False,
            mouse_visible=False,
        )
    
        main_menu.add.button("Input Test", input_menu)
        main_menu.add.button("Steering Wheel Test", steer_test_menu)
        main_menu.add.button("Motor Test", motor_menu)
        main_menu.add.button("Coin Chute Test", coin_menu)
        main_menu.add.button("Time Settings", time_menu)

        if button1.is_pressed:
            press("up")
        elif button2.is_pressed:
            press("down")
        elif button3.is_pressed:
            press("left")
        elif button4.is_pressed:
            press("right")
    
        events = pygame.event.get()
        for event in events:
            if event.type == pygame.QUIT:
                gameExit = True
    
            if event.type == pygame.KEYDOWN:
                if event.key == pygame.K_LEFT:
                    print("Left")
                elif event.key == pygame.K_RIGHT:
                    print("Right")
                elif event.key == pygame.K_UP:
                    print("Up")
                elif event.key == pygame.K_DOWN:
                    print("Down")
                elif event.key == pygame.K_d:
                    press("right")
                elif event.key == pygame.K_a:
                    press("left")
                elif event.key == pygame.K_w:
                    press("up")
                elif event.key == pygame.K_s:
                    press("down")
    
        if main_menu.is_enabled():
            main_menu.update(events)
            main_menu.draw(gameDisplay)
    
        pygame.display.flip()



Discussion:

Patch:
diff --git a/.travis.yml b/.travis.yml
index ae34e056..be1bac71 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -9,9 +9,7 @@ cache: pip
 
 # Pip installs
 install:
-  - pip install codecov
-  - pip install -r requirements.txt
-  - pip install -e .
+  - pip install -e .[test]
 env:
   global:
     - SDL_VIDEODRIVER=dummy
diff --git a/pygame_menu/_widgetmanager.py b/pygame_menu/_widgetmanager.py
index 8bd3d92b..ec2ef207 100644
--- a/pygame_menu/_widgetmanager.py
+++ b/pygame_menu/_widgetmanager.py
@@ -336,6 +336,7 @@ class WidgetManager(Base):
             widget.background_inflate_to_selection_effect()
 
         widget._update__repr___(self)
+        widget._keyboard_ignore_nonphysical = self._menu._keyboard_ignore_nonphysical
 
         widget.configured = True
         widget._configure()
diff --git a/pygame_menu/menu.py b/pygame_menu/menu.py
index 21a49a6d..0d5f589f 100644
--- a/pygame_menu/menu.py
+++ b/pygame_menu/menu.py
@@ -142,6 +142,7 @@ class Menu(Base):
     _joy_event_repeat: int
     _joystick: bool
     _keyboard: bool
+    _keyboard_ignore_nonphysical: bool
     _last_scroll_thickness: List[Union[Tuple2IntType, int]]
     _last_selected_type: str
     _mainloop: bool
@@ -532,6 +533,7 @@ class Menu(Base):
 
         # Init keyboard
         self._keyboard = keyboard_enabled
+        self._keyboard_ignore_nonphysical = True  # Ignores non physical keyboard buttons pressed
 
         # Init mouse
         if mouse_motion_selection:
@@ -2355,7 +2357,7 @@ class Menu(Base):
                 elif event.type == pygame.KEYDOWN and self._current._keyboard:
 
                     # Check key event is valid
-                    if not check_key_pressed_valid(event):
+                    if self._keyboard_ignore_nonphysical and not check_key_pressed_valid(event):
                         continue
 
                     if event.key == ctrl.KEY_MOVE_DOWN:
diff --git a/pygame_menu/version.py b/pygame_menu/version.py
index 3cff547a..7323020d 100644
--- a/pygame_menu/version.py
+++ b/pygame_menu/version.py
@@ -57,6 +57,6 @@ class Version(tuple):
     patch = property(lambda self: self[2])
 
 
-vernum = Version(4, 1, 1)
+vernum = Version(4, 1, 2)
 ver = str(vernum)
 rev = ''
diff --git a/pygame_menu/widgets/core/widget.py b/pygame_menu/widgets/core/widget.py
index 50964ff6..f5c9fa7d 100644
--- a/pygame_menu/widgets/core/widget.py
+++ b/pygame_menu/widgets/core/widget.py
@@ -269,6 +269,8 @@ class Widget(Base):
     _font_size: int
     _frame: Optional['pygame_menu.widgets.Frame']
     _joystick_enabled: bool
+    _keyboard_enabled: bool
+    _keyboard_ignore_nonphysical: bool
     _kwargs: Dict[Any, Any]
     _last_render_hash: int
     _margin: Tuple2IntType
@@ -443,6 +445,7 @@ class Widget(Base):
 
         # Inputs
         self._keyboard_enabled = True
+        self._keyboard_ignore_nonphysical = True  # Ignores non physical keyboard buttons pressed
         self._joystick_enabled = True
         self._mouse_enabled = True  # Accept mouse interaction
         self._touchscreen_enabled = True
@@ -459,6 +462,16 @@ class Widget(Base):
         self.readonly = False  # If True, widget ignores all input
         self.selection_expand_background = False  # If True, the widget background will inflate to match selection margin if selected
 
+    def _ignores_keyboard_nonphysical(self) -> bool:
+        """
+        Ignores the keyboard non-physical button events.
+
+        :return: True if ignored
+        """
+        if self._menu is None:
+            return self._keyboard_ignore_nonphysical
+        return self._keyboard_ignore_nonphysical and self._menu._keyboard_ignore_nonphysical
+
     def set_onchange(self, onchange: CallbackType) -> 'Widget':
         """
         Set ``onchange`` callback. This method is executed in
diff --git a/pygame_menu/widgets/widget/colorinput.py b/pygame_menu/widgets/widget/colorinput.py
index 3ff6f1e8..dbc76595 100644
--- a/pygame_menu/widgets/widget/colorinput.py
+++ b/pygame_menu/widgets/widget/colorinput.py
@@ -442,9 +442,8 @@ class ColorInput(TextInput):  # lgtm [py/missing-call-to-init]
 
                 # User writes
                 if event.type == pygame.KEYDOWN and self._keyboard_enabled:
-
                     # Check if any key is pressed
-                    if not check_key_pressed_valid(event):
+                    if self._ignores_keyboard_nonphysical() and not check_key_pressed_valid(event):
                         continue
 
                     if disable_remove_separator and len(input_str) > 0 and \
@@ -524,7 +523,7 @@ class ColorInput(TextInput):  # lgtm [py/missing-call-to-init]
                 # User writes
                 if event.type == pygame.KEYDOWN and self._keyboard_enabled:
                     # Check if any key is pressed
-                    if not check_key_pressed_valid(event):
+                    if self._ignores_keyboard_nonphysical() and not check_key_pressed_valid(event):
                         continue
 
                     # Backspace button, delete text from right
diff --git a/pygame_menu/widgets/widget/dropselect.py b/pygame_menu/widgets/widget/dropselect.py
index c76430e3..8abc6b99 100644
--- a/pygame_menu/widgets/widget/dropselect.py
+++ b/pygame_menu/widgets/widget/dropselect.py
@@ -1154,7 +1154,7 @@ class DropSelect(Widget):
         for event in events:
 
             if event.type == pygame.KEYDOWN:  # Check key is valid
-                if not check_key_pressed_valid(event):
+                if self._ignores_keyboard_nonphysical() and not check_key_pressed_valid(event):
                     continue
 
             # Check mouse over
diff --git a/pygame_menu/widgets/widget/rangeslider.py b/pygame_menu/widgets/widget/rangeslider.py
index 9d9fa4a1..08e82061 100644
--- a/pygame_menu/widgets/widget/rangeslider.py
+++ b/pygame_menu/widgets/widget/rangeslider.py
@@ -964,7 +964,7 @@ class RangeSlider(Widget):
         for event in events:
 
             if event.type == pygame.KEYDOWN:  # Check key is valid
-                if not check_key_pressed_valid(event):
+                if self._ignores_keyboard_nonphysical() and not check_key_pressed_valid(event):
                     continue
 
             # Check mouse over
diff --git a/pygame_menu/widgets/widget/selector.py b/pygame_menu/widgets/widget/selector.py
index 906af05e..379e35b3 100644
--- a/pygame_menu/widgets/widget/selector.py
+++ b/pygame_menu/widgets/widget/selector.py
@@ -467,7 +467,7 @@ class Selector(Widget):
         for event in events:
 
             if event.type == pygame.KEYDOWN:  # Check key is valid
-                if not check_key_pressed_valid(event):
+                if self._ignores_keyboard_nonphysical() and not check_key_pressed_valid(event):
                     continue
 
             # Check mouse over
diff --git a/pygame_menu/widgets/widget/textinput.py b/pygame_menu/widgets/widget/textinput.py
index 61485b0e..cd93f2b7 100644
--- a/pygame_menu/widgets/widget/textinput.py
+++ b/pygame_menu/widgets/widget/textinput.py
@@ -1588,9 +1588,8 @@ class TextInput(Widget):
 
             # User press a key
             if event.type == pygame.KEYDOWN and self._keyboard_enabled:
-
                 # Check if any key is pressed, if True the event is invalid
-                if not check_key_pressed_valid(event):
+                if self._ignores_keyboard_nonphysical() and not check_key_pressed_valid(event):
                     continue
 
                 self._cursor_visible = True  # So the user sees where he writes
diff --git a/pygame_menu/widgets/widget/toggleswitch.py b/pygame_menu/widgets/widget/toggleswitch.py
index 1ae053ec..771ace71 100644
--- a/pygame_menu/widgets/widget/toggleswitch.py
+++ b/pygame_menu/widgets/widget/toggleswitch.py
@@ -426,7 +426,7 @@ class ToggleSwitch(Widget):
         for event in events:
 
             if event.type == pygame.KEYDOWN:  # Check key is valid
-                if not check_key_pressed_valid(event):
+                if self._ignores_keyboard_nonphysical() and not check_key_pressed_valid(event):
                     continue
 
             # Check mouse over
diff --git a/setup.py b/setup.py
index 11fb01c1..a863f251 100644
--- a/setup.py
+++ b/setup.py
@@ -49,6 +49,12 @@ requirements_docs.extend([
     'sphinx-rtd-theme'
 ])
 
+requirements_tests = requirements.copy()
+requirements_tests.extend([
+    'codecov',
+    # 'pyautogui'
+])
+
 # Setup library
 setup(
     name=pygame_menu.__module_name__,
@@ -85,7 +91,8 @@ setup(
     python_requires='>=3.6, <4',
     install_requires=requirements,
     extras_require={
-        'docs': requirements_docs
+        'docs': requirements_docs,
+        'test': requirements_tests
     },
     setup_requires=[
         'setuptools',


Test patch:
diff --git a/test/test_controls.py b/test/test_controls.py
index 76a1cc53..46a79405 100644
--- a/test/test_controls.py
+++ b/test/test_controls.py
@@ -33,7 +33,27 @@ __all__ = ['ControlsTest']
 
 from test._utils import MenuUtils, PygameEventUtils, BaseTest
 
-from pygame import K_RETURN, K_END
+import pygame
+import os
+
+
+# Fix pyautogui travis tests
+def press(key: str) -> None:
+    """
+    Press a key.
+    """
+    print(key)
+
+
+PY_AUTO_GUI = False
+if 'DISPLAY' in os.environ.keys():
+    try:
+        # noinspection PyPackageRequirements
+        from pyautogui import press
+
+        PY_AUTO_GUI = True
+    except (ImportError, ModuleNotFoundError, FileNotFoundError, KeyError):
+        pass
 
 import pygame_menu.controls as ctrl
 
@@ -44,11 +64,11 @@ class ControlsTest(BaseTest):
         """
         Configure controls.
         """
-        self.assertEqual(ctrl.KEY_APPLY, K_RETURN)
+        self.assertEqual(ctrl.KEY_APPLY, pygame.K_RETURN)
 
         # Change apply to new key
-        ctrl.KEY_APPLY = K_END
-        self.assertEqual(ctrl.KEY_APPLY, K_END)
+        ctrl.KEY_APPLY = pygame.K_END
+        self.assertEqual(ctrl.KEY_APPLY, pygame.K_END)
 
         # Create new button
         menu = MenuUtils.generic_menu()
@@ -69,13 +89,56 @@ class ControlsTest(BaseTest):
         self.assertFalse(test[0])
 
         # Now test new apply button
-        button.update(PygameEventUtils.key(K_END, keydown=True))
+        button.update(PygameEventUtils.key(pygame.K_END, keydown=True))
         self.assertTrue(test[0])
 
         # Rollback change
-        ctrl.KEY_APPLY = K_RETURN
+        ctrl.KEY_APPLY = pygame.K_RETURN
 
-        button.update(PygameEventUtils.key(K_END, keydown=True))
+        button.update(PygameEventUtils.key(pygame.K_END, keydown=True))
         self.assertTrue(test[0])
         button.update(PygameEventUtils.key(ctrl.KEY_APPLY, keydown=True))
         self.assertFalse(test[0])
+
+    def test_pyautogui(self) -> None:
+        """
+        Test pyautogui support.
+        """
+        main_menu = MenuUtils.generic_menu()
+
+        b0 = main_menu.add.button('Input Test')
+        main_menu.add.button('Steering Wheel Test')
+        main_menu.add.button('Motor Test')
+        main_menu.add.button('Coin Chute Test')
+        main_menu.add.button('Time Settings')
+
+        # Menu ignores non physical events
+        press('down')  # This emulates the arrow down key being pressed
+        events = pygame.event.get()  # The press down is captured here
+        down = False
+        for event in events:
+            if event.type == pygame.KEYDOWN:
+                # if event.key == pygame.K_LEFT:
+                #     print('Left')
+                # elif event.key == pygame.K_RIGHT:
+                #     print('Right')
+                # elif event.key == pygame.K_UP:
+                #     print('Up')
+                if event.key == pygame.K_DOWN:
+                    down = True
+        if PY_AUTO_GUI:
+            self.assertTrue(down)
+        else:
+            self.assertFalse(down)
+        self.assertEqual(main_menu.get_index(), 0)
+
+        main_menu.update(events)
+        self.assertEqual(main_menu.get_index(), 0)  # Does not changed
+
+        main_menu._keyboard_ignore_nonphysical = False
+        main_menu.update(events)
+        self.assertEqual(main_menu.get_index(), 1 if PY_AUTO_GUI else 0)  # Does not changed
+
+        # Ignore only applies to menus, currently appended widgets does not change
+        self.assertTrue(b0._keyboard_ignore_nonphysical)
+        self.assertFalse(b0._ignores_keyboard_nonphysical())
diff --git a/test/test_widget_button.py b/test/test_widget_button.py
index 2c3f8f31..7631d278 100644
--- a/test/test_widget_button.py
+++ b/test/test_widget_button.py
@@ -138,6 +138,7 @@ class ButtonWidgetTest(BaseTest):
             self.assertTrue(kwargs.get('key', False))
 
         btn = Button('epic', onreturn=callback, key=True)
+        self.assertTrue(btn._ignores_keyboard_nonphysical())
         btn.apply()
         btn = menu.add.button('epic', callback, accept_kwargs=True, key=True)
         btn.apply()