From f84167a7fbea991835fc2aaa1680b7b27a78b0d5 Mon Sep 17 00:00:00 2001 From: Storm Dragon Date: Thu, 21 May 2026 01:07:01 -0400 Subject: [PATCH] Of course, soon as I feel things are stable enough to merge to master bugs come crawling out of the woodwork. Fix for being sure Fenrir switches out of its modal mode completely when leaving speech history. --- .../core/speechHistoryManager.py | 19 ++++++++++++++ tests/unit/test_speech_history_manager.py | 25 +++++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/src/fenrirscreenreader/core/speechHistoryManager.py b/src/fenrirscreenreader/core/speechHistoryManager.py index 5fa82fb9..4e62ad4d 100644 --- a/src/fenrirscreenreader/core/speechHistoryManager.py +++ b/src/fenrirscreenreader/core/speechHistoryManager.py @@ -172,6 +172,7 @@ class SpeechHistoryManager: str([1, ["KEY_KPENTER"]]): [1, ["KEY_KPENTER"]], str([1, ["KEY_ESC"]]): [1, ["KEY_ESC"]], } + self._refresh_input_bindings() def _restore_bindings(self): if self.bindings_backup is not None: @@ -180,3 +181,21 @@ class SpeechHistoryManager: self.env["rawBindings"] = self.raw_bindings_backup self.bindings_backup = None self.raw_bindings_backup = None + self._reset_input_state() + self._refresh_input_bindings() + + def _reset_input_state(self): + try: + self.env["runtime"]["InputManager"].reset_input_state() + except Exception: + pass + + def _refresh_input_bindings(self): + try: + refresh_grabs = getattr( + self.env["runtime"]["InputDriver"], "refresh_grabs", None + ) + if refresh_grabs: + refresh_grabs(force=True) + except Exception: + pass diff --git a/tests/unit/test_speech_history_manager.py b/tests/unit/test_speech_history_manager.py index 18a432d8..1a570ebe 100644 --- a/tests/unit/test_speech_history_manager.py +++ b/tests/unit/test_speech_history_manager.py @@ -16,11 +16,15 @@ def build_speech_history_manager(history_size=3): settings_manager = Mock() settings_manager.get_setting_as_int.return_value = history_size memory_manager = Mock(add_value_to_first_index=Mock()) + input_manager = Mock(reset_input_state=Mock()) + input_driver = Mock(refresh_grabs=Mock()) env = { "runtime": { "OutputManager": output_manager, "SettingsManager": settings_manager, "MemoryManager": memory_manager, + "InputManager": input_manager, + "InputDriver": input_driver, }, "bindings": {"original": "COMMAND"}, "rawBindings": {"original": [1, ["KEY_FENRIR"]]}, @@ -105,6 +109,8 @@ def test_open_history_installs_modal_bindings_and_replay_is_not_recorded(): assert env["bindings"][str([1, ["KEY_UP"]])] == "SPEECH_HISTORY_PREV" assert env["bindings"][str([1, ["KEY_ENTER"]])] == "SPEECH_HISTORY_COPY" assert env["bindings"][str([1, ["KEY_ESC"]])] == "SPEECH_HISTORY_CLOSE" + input_driver = env["runtime"]["InputDriver"] + input_driver.refresh_grabs.assert_called_once_with(force=True) @pytest.mark.unit @@ -145,3 +151,22 @@ def test_copy_current_adds_clipboard_and_restores_bindings(): assert not manager.is_active() assert env["bindings"] == {"original": "COMMAND"} assert env["rawBindings"] == {"original": [1, ["KEY_FENRIR"]]} + env["runtime"]["InputManager"].reset_input_state.assert_called_once_with() + assert env["runtime"]["InputDriver"].refresh_grabs.call_count == 2 + + +@pytest.mark.unit +def test_close_history_restores_keyboard_state_and_grabs(): + manager, env, _spoken_messages, _memory_manager = ( + build_speech_history_manager() + ) + manager.add_text("first") + manager.open_history() + + manager.close_history() + + assert not manager.is_active() + assert env["bindings"] == {"original": "COMMAND"} + assert env["rawBindings"] == {"original": [1, ["KEY_FENRIR"]]} + env["runtime"]["InputManager"].reset_input_state.assert_called_once_with() + assert env["runtime"]["InputDriver"].refresh_grabs.call_count == 2