From 9d20ce9b597164a2f90d84843b5764ec92e1676f Mon Sep 17 00:00:00 2001 From: Fabrice de Gans Date: Tue, 8 Oct 2024 18:22:17 -0700 Subject: [PATCH] [Input] Reset keyboard tracking on focus loss (#1357) When pressing Alt+Tab, the "Alt" and "Tab" keys were considered in the "pressed" state until the user pressed them again because the window is no longer receiving keyboard events. This resulted in some shortcuts no longer working, since "Alt" was always in the pressed state. This changes the keyboard tracking to be reset when the application loses focus, fixing the issue. This change also adds tests for the keyboard tracking. --- src/wx/config/copy-events.cmake | 1 - src/wx/widgets/CMakeLists.txt | 5 + .../widgets/keyboard-input-handler-test.cpp | 197 +++++++++++++ src/wx/widgets/keyboard-input-handler.cpp | 273 ++++++++++++++++++ src/wx/widgets/keyboard-input-handler.h | 49 ++++ src/wx/widgets/user-input-event-test.cpp | 53 ++++ src/wx/widgets/user-input-event.cpp | 264 +---------------- src/wx/widgets/user-input-event.h | 51 +--- src/wx/wxvbam.cpp | 32 +- src/wx/wxvbam.h | 9 +- 10 files changed, 618 insertions(+), 316 deletions(-) create mode 100644 src/wx/widgets/keyboard-input-handler-test.cpp create mode 100644 src/wx/widgets/keyboard-input-handler.cpp create mode 100644 src/wx/widgets/keyboard-input-handler.h create mode 100644 src/wx/widgets/user-input-event-test.cpp diff --git a/src/wx/config/copy-events.cmake b/src/wx/config/copy-events.cmake index eeedbf52..95ce472a 100644 --- a/src/wx/config/copy-events.cmake +++ b/src/wx/config/copy-events.cmake @@ -31,7 +31,6 @@ LIST(SORT EVLINES) STRING(REGEX REPLACE ",\n\$" "\n" EVLINES "${EVLINES}") FILE(APPEND "${CMDTAB}" ${EVLINES}) FILE(APPEND "${CMDTAB}" "};\n") -FILE(APPEND "${CMDTAB}" "const int ncmds = sizeof(cmdtab) / sizeof(cmdtab[0]);\n") # cmdhandlers.h contains prototypes for all handlers FILE(WRITE "${EVPROTO}" "// Generated from cmdevents.cpp; do not edit\n") diff --git a/src/wx/widgets/CMakeLists.txt b/src/wx/widgets/CMakeLists.txt index e160cc6b..d1e86dc1 100644 --- a/src/wx/widgets/CMakeLists.txt +++ b/src/wx/widgets/CMakeLists.txt @@ -11,6 +11,7 @@ target_sources(vbam-wx-widgets $,dpi-support-mac.mm,dpi-support.cpp> group-check-box.cpp keep-on-top-styler.cpp + keyboard-input-handler.cpp option-validator.cpp render-plugin.cpp user-input-ctrl.cpp @@ -28,6 +29,7 @@ target_sources(vbam-wx-widgets event-handler-provider.h group-check-box.h keep-on-top-styler.h + keyboard-input-handler.h option-validator.h render-plugin.h user-input-ctrl.h @@ -46,8 +48,10 @@ if(BUILD_TESTING) client-data-test.cpp group-check-box-test.cpp keep-on-top-styler-test.cpp + keyboard-input-handler-test.cpp option-validator-test.cpp user-input-ctrl-test.cpp + user-input-event-test.cpp ) target_link_libraries(vbam-wx-widgets-tests @@ -61,6 +65,7 @@ if(BUILD_TESTING) vbam-wx-config vbam-wx-widgets GTest::gtest_main + GTest::gmock_main ) configure_wx_target(vbam-wx-widgets-tests) diff --git a/src/wx/widgets/keyboard-input-handler-test.cpp b/src/wx/widgets/keyboard-input-handler-test.cpp new file mode 100644 index 00000000..8dbd47a0 --- /dev/null +++ b/src/wx/widgets/keyboard-input-handler-test.cpp @@ -0,0 +1,197 @@ +#include "wx/widgets/keyboard-input-handler.h" + +#include + +#include +#include + +#include + +#include "wx/config/user-input.h" +#include "wx/widgets/event-handler-provider.h" +#include "wx/widgets/test/widgets-test.h" +#include "wx/widgets/user-input-event.h" + +namespace widgets { + +namespace { + +class KeyboardInputHandlerTest : public WidgetsTest, + public wxEvtHandler, + public EventHandlerProvider { +public: + KeyboardInputHandlerTest() : handler_(this) { + Bind(VBAM_EVT_USER_INPUT, &KeyboardInputHandlerTest::OnUserInputEvent, this); + } + +protected: + // Processes `key_event` and returns the data from the generated events. + std::vector ProcessKeyEvent(wxKeyEvent key_event) { + handler_.ProcessKeyEvent(key_event); + ProcessPendingEvents(); + std::vector data; + std::swap(data, pending_data_); + return data; + } + +private: + void OnUserInputEvent(UserInputEvent& event) { + // Do not let the event propagate. + event.Skip(false); + for (const auto& data : event.data()) { + pending_data_.emplace_back(data); + } + } + + // EventHandlerProvider implementation. + wxEvtHandler* event_handler() override { return this; } + + KeyboardInputHandler handler_; + std::vector pending_data_; +}; + +static constexpr config::KeyboardInput kF1(wxKeyCode::WXK_F1); +static constexpr config::KeyboardInput kCtrlF1(wxKeyCode::WXK_F1, wxMOD_CONTROL); +static constexpr config::KeyboardInput kCtrlShiftF1(wxKeyCode::WXK_F1, + static_cast(wxMOD_CONTROL | + wxMOD_SHIFT)); +static constexpr config::KeyboardInput kCtrl(wxKeyCode::WXK_CONTROL, wxMOD_CONTROL); +static constexpr config::KeyboardInput kShift(wxKeyCode::WXK_SHIFT, wxMOD_SHIFT); +static const UserInputEvent::Data kF1Pressed(kF1, true); +static const UserInputEvent::Data kF1Released(kF1, false); +static const UserInputEvent::Data kCtrlF1Pressed(kCtrlF1, true); +static const UserInputEvent::Data kCtrlF1Released(kCtrlF1, false); +static const UserInputEvent::Data kCtrlShiftF1Pressed(kCtrlShiftF1, true); +static const UserInputEvent::Data kCtrlShiftF1Released(kCtrlShiftF1, false); +static const UserInputEvent::Data kCtrlPressed(kCtrl, true); +static const UserInputEvent::Data kCtrlReleased(kCtrl, false); +static const UserInputEvent::Data kShiftPressed(kShift, true); +static const UserInputEvent::Data kShiftReleased(kShift, false); + +wxKeyEvent F1DownEvent() { + wxKeyEvent event(wxEVT_KEY_DOWN); + event.m_keyCode = WXK_F1; + return event; +} + +wxKeyEvent F1UpEvent() { + wxKeyEvent event(wxEVT_KEY_UP); + event.m_keyCode = WXK_F1; + return event; +} + +wxKeyEvent CtrlF1DownEvent() { + wxKeyEvent event(wxEVT_KEY_DOWN); + event.m_keyCode = WXK_F1; + event.m_controlDown = true; + return event; +} + +wxKeyEvent CtrlF1UpEvent() { + wxKeyEvent event(wxEVT_KEY_UP); + event.m_keyCode = WXK_F1; + event.m_controlDown = true; + return event; +} + +wxKeyEvent CtrlShiftF1DownEvent() { + wxKeyEvent event(wxEVT_KEY_DOWN); + event.m_keyCode = WXK_F1; + event.m_controlDown = true; + event.m_shiftDown = true; + return event; +} + +wxKeyEvent CtrlShiftF1UpEvent() { + wxKeyEvent event(wxEVT_KEY_UP); + event.m_keyCode = WXK_F1; + event.m_controlDown = true; + event.m_shiftDown = true; + return event; +} + +wxKeyEvent CtrlDownEvent() { + wxKeyEvent event(wxEVT_KEY_DOWN); + event.m_keyCode = WXK_CONTROL; + return event; +} + +wxKeyEvent CtrlUpEvent() { + wxKeyEvent event(wxEVT_KEY_UP); + event.m_keyCode = WXK_CONTROL; + return event; +} + +wxKeyEvent ShiftDownEvent() { + wxKeyEvent event(wxEVT_KEY_DOWN); + event.m_keyCode = WXK_SHIFT; + return event; +} + +wxKeyEvent ShiftUpEvent() { + wxKeyEvent event(wxEVT_KEY_UP); + event.m_keyCode = WXK_SHIFT; + return event; +} + +} // namespace + +TEST_F(KeyboardInputHandlerTest, SimpleKeyDownUp) { + // Send F1 down event. + ASSERT_THAT(ProcessKeyEvent(F1DownEvent()), testing::ElementsAre(kF1Pressed)); + + // Send F1 up event. + ASSERT_THAT(ProcessKeyEvent(F1UpEvent()), testing::ElementsAre(kF1Released)); +} + +TEST_F(KeyboardInputHandlerTest, ModifierThenKey) { + // Ctrl Down -> F1 Down -> F1 Up -> Ctrl Up + ASSERT_THAT(ProcessKeyEvent(CtrlDownEvent()), testing::ElementsAre(kCtrlPressed)); + ASSERT_THAT(ProcessKeyEvent(CtrlF1DownEvent()), + testing::ElementsAre(kCtrlF1Pressed, kF1Pressed)); + ASSERT_THAT(ProcessKeyEvent(CtrlF1UpEvent()), + testing::ElementsAre(kCtrlF1Released, kF1Released)); + ASSERT_THAT(ProcessKeyEvent(CtrlUpEvent()), testing::ElementsAre(kCtrlReleased)); +} + +TEST_F(KeyboardInputHandlerTest, KeyThenModifier) { + // F1 Down -> Ctrl Down -> Ctrl Up -> F1 Up + // In this case, no Ctrl+F1 event should be generated. + ASSERT_THAT(ProcessKeyEvent(F1DownEvent()), testing::ElementsAre(kF1Pressed)); + ASSERT_THAT(ProcessKeyEvent(CtrlDownEvent()), testing::ElementsAre(kCtrlPressed)); + ASSERT_THAT(ProcessKeyEvent(CtrlUpEvent()), testing::ElementsAre(kCtrlReleased)); + ASSERT_THAT(ProcessKeyEvent(F1UpEvent()), testing::ElementsAre(kF1Released)); + + // F1 Down -> Ctrl Down -> F1 Up -> Ctrl Up + // In this case, a Ctrl+F1 event should be generated when F1 is released. + ASSERT_THAT(ProcessKeyEvent(F1DownEvent()), testing::ElementsAre(kF1Pressed)); + ASSERT_THAT(ProcessKeyEvent(CtrlDownEvent()), testing::ElementsAre(kCtrlPressed)); + ASSERT_THAT(ProcessKeyEvent(F1UpEvent()), + testing::ElementsAre(kCtrlF1Pressed, kCtrlF1Released, kF1Released)); + ASSERT_THAT(ProcessKeyEvent(CtrlUpEvent()), testing::ElementsAre(kCtrlReleased)); +} + +TEST_F(KeyboardInputHandlerTest, Multiplemodifiers) { + // F1 Down -> Ctrl Down -> Shift Down -> F1 Up -> Ctrl Up -> Shift Up + // In this case, a Ctrl+Shift+F1 event should be generated when F1 is released. + ASSERT_THAT(ProcessKeyEvent(F1DownEvent()), testing::ElementsAre(kF1Pressed)); + ASSERT_THAT(ProcessKeyEvent(CtrlDownEvent()), testing::ElementsAre(kCtrlPressed)); + ASSERT_THAT(ProcessKeyEvent(ShiftDownEvent()), testing::ElementsAre(kShiftPressed)); + ASSERT_THAT(ProcessKeyEvent(F1UpEvent()), + testing::ElementsAre(kCtrlShiftF1Pressed, kCtrlShiftF1Released, kF1Released)); + ASSERT_THAT(ProcessKeyEvent(CtrlUpEvent()), testing::ElementsAre(kCtrlReleased)); + ASSERT_THAT(ProcessKeyEvent(ShiftUpEvent()), testing::ElementsAre(kShiftReleased)); + + // Ctrl Down -> Shift Down -> F1 Down -> F1 Up -> Shift Up -> Ctrl Up + // In this case, a Ctrl+Shift+F1 event should be generated when F1 is pressed. + ASSERT_THAT(ProcessKeyEvent(CtrlDownEvent()), testing::ElementsAre(kCtrlPressed)); + ASSERT_THAT(ProcessKeyEvent(ShiftDownEvent()), testing::ElementsAre(kShiftPressed)); + ASSERT_THAT(ProcessKeyEvent(CtrlShiftF1DownEvent()), + testing::ElementsAre(kCtrlShiftF1Pressed, kF1Pressed)); + ASSERT_THAT(ProcessKeyEvent(CtrlShiftF1UpEvent()), + testing::ElementsAre(kCtrlShiftF1Released, kF1Released)); + ASSERT_THAT(ProcessKeyEvent(ShiftUpEvent()), testing::ElementsAre(kShiftReleased)); + ASSERT_THAT(ProcessKeyEvent(CtrlUpEvent()), testing::ElementsAre(kCtrlReleased)); +} + +} // namespace widgets diff --git a/src/wx/widgets/keyboard-input-handler.cpp b/src/wx/widgets/keyboard-input-handler.cpp new file mode 100644 index 00000000..80cb25fe --- /dev/null +++ b/src/wx/widgets/keyboard-input-handler.cpp @@ -0,0 +1,273 @@ +#include "wx/widgets/keyboard-input-handler.h" + +#include "wx/config/user-input.h" +#include "wx/widgets/user-input-event.h" + +#include + +namespace widgets { + +namespace { + +// Filters the received key code in the key event for something we can use. +wxKeyCode FilterKeyCode(const wxKeyEvent& event) { + const wxKeyCode unicode_key = static_cast(event.GetUnicodeKey()); + if (unicode_key == WXK_NONE) { + // We need to filter out modifier keys here so we can differentiate + // between a key press and a modifier press. + const wxKeyCode keycode = static_cast(event.GetKeyCode()); + switch (keycode) { + case WXK_CONTROL: + case WXK_ALT: + case WXK_SHIFT: +#ifdef __WXMAC__ + case WXK_RAW_CONTROL: +#endif + return WXK_NONE; + default: + return keycode; + } + } + + if (unicode_key < 32) { + switch (unicode_key) { + case WXK_BACK: + case WXK_TAB: + case WXK_RETURN: + case WXK_ESCAPE: + return unicode_key; + default: + return WXK_NONE; + } + } + + return unicode_key; +} + +// Returns the set of modifiers for the given key event. +std::unordered_set GetModifiers(const wxKeyEvent& event) { + // Standalone modifier are treated as keys and do not set the keyboard modifiers. + switch (event.GetKeyCode()) { + case WXK_CONTROL: + return {wxMOD_CONTROL}; + case WXK_ALT: + return {wxMOD_ALT}; + case WXK_SHIFT: + return {wxMOD_SHIFT}; +#ifdef __WXMAC__ + case WXK_RAW_CONTROL: + return {wxMOD_RAW_CONTROL}; +#endif + } + + std::unordered_set mods; + if (event.ControlDown()) { + mods.insert(wxMOD_CONTROL); + } + if (event.AltDown()) { + mods.insert(wxMOD_ALT); + } + if (event.ShiftDown()) { + mods.insert(wxMOD_SHIFT); + } +#ifdef __WXMAC__ + if (event.RawControlDown()) { + mods.insert(wxMOD_RAW_CONTROL); + } +#endif + return mods; +} + +// Builds a wxKeyModifier from a set of modifiers. +wxKeyModifier GetModifiersFromSet(const std::unordered_set& mods) { + int mod = wxMOD_NONE; + for (const wxKeyModifier m : mods) { + mod |= m; + } + return static_cast(mod); +} + +// Returns the key code for a standalone modifier. +wxKeyCode KeyFromModifier(const wxKeyModifier mod) { + switch (mod) { + case wxMOD_CONTROL: + return WXK_CONTROL; + case wxMOD_ALT: + return WXK_ALT; + case wxMOD_SHIFT: + return WXK_SHIFT; +#ifdef __WXMAC__ + case wxMOD_RAW_CONTROL: + return WXK_RAW_CONTROL; +#endif + default: + return WXK_NONE; + } +} + +} // namespace + +KeyboardInputHandler::KeyboardInputHandler(EventHandlerProvider* const handler_provider) + : handler_provider_(handler_provider) { + VBAM_CHECK(handler_provider_); +} + +KeyboardInputHandler::~KeyboardInputHandler() = default; + +void KeyboardInputHandler::ProcessKeyEvent(wxKeyEvent& event) { + if (!handler_provider_->event_handler()) { + // No event handler to send the event to. + return; + } + + if (event.GetEventType() == wxEVT_KEY_DOWN) { + OnKeyDown(event); + } else if (event.GetEventType() == wxEVT_KEY_UP) { + OnKeyUp(event); + } +} + +void KeyboardInputHandler::Reset() { + active_keys_.clear(); + active_mods_.clear(); + active_mod_inputs_.clear(); +} + +void KeyboardInputHandler::OnKeyDown(wxKeyEvent& event) { + // Stop propagation of the event. + event.Skip(false); + + const wxKeyCode key = FilterKeyCode(event); + const std::unordered_set mods = GetModifiers(event); + + wxKeyCode key_pressed = WXK_NONE; + if (key != WXK_NONE) { + if (active_keys_.find(key) == active_keys_.end()) { + // Key was not pressed before. + key_pressed = key; + active_keys_.insert(key); + } + } + + wxKeyModifier mod_pressed = wxMOD_NONE; + for (const wxKeyModifier mod : mods) { + if (active_mods_.find(mod) == active_mods_.end()) { + // Mod was not pressed before. + active_mods_.insert(mod); + mod_pressed = mod; + break; + } + } + + if (key_pressed == WXK_NONE && mod_pressed == wxMOD_NONE) { + // No new keys or mods were pressed. + return; + } + + const wxKeyModifier active_mods = GetModifiersFromSet(active_mods_); + std::vector event_data; + if (key_pressed == WXK_NONE) { + // A new standalone modifier was pressed, send the event. + event_data.emplace_back(config::KeyboardInput(KeyFromModifier(mod_pressed), mod_pressed), + true); + } else { + // A new key was pressed, send the event with modifiers, first. + event_data.emplace_back(config::KeyboardInput(key, active_mods), true); + + if (active_mods != wxMOD_NONE) { + // Keep track of the key pressed with the active modifiers. + active_mod_inputs_.emplace(key, active_mods); + + // Also send the key press event without modifiers. + event_data.emplace_back(config::KeyboardInput(key, wxMOD_NONE), true); + } + } + + wxQueueEvent(handler_provider_->event_handler(), new UserInputEvent(std::move(event_data))); +} + +void KeyboardInputHandler::OnKeyUp(wxKeyEvent& event) { + // Stop propagation of the event. + event.Skip(false); + + const wxKeyCode key = FilterKeyCode(event); + const std::unordered_set mods = GetModifiers(event); + const wxKeyModifier previous_mods = GetModifiersFromSet(active_mods_); + + wxKeyCode key_released = WXK_NONE; + if (key != WXK_NONE) { + auto iter = active_keys_.find(key); + if (iter != active_keys_.end()) { + // Key was pressed before. + key_released = key; + active_keys_.erase(iter); + } + } + + wxKeyModifier mod_released = wxMOD_NONE; + if (key_released == WXK_NONE) { + // Only look for a standalone modifier if no key was released. + for (const wxKeyModifier mod : mods) { + auto iter = active_mods_.find(mod); + if (iter != active_mods_.end()) { + // Mod was pressed before. + mod_released = mod; + active_mods_.erase(iter); + break; + } + } + } + + if (key_released == WXK_NONE && mod_released == wxMOD_NONE) { + // No keys or mods were released. + return; + } + + std::vector event_data; + if (key_released == WXK_NONE) { + // A standalone modifier was released, send it. + event_data.emplace_back(config::KeyboardInput(KeyFromModifier(mod_released), mod_released), + false); + } else { + // A key was released. + if (previous_mods == wxMOD_NONE) { + // The key was pressed without modifiers, just send the key release event. + event_data.emplace_back(config::KeyboardInput(key, wxMOD_NONE), false); + } else { + // Check if the key was pressed with the active modifiers. + const config::KeyboardInput input_with_modifiers(key, previous_mods); + auto iter = active_mod_inputs_.find(input_with_modifiers); + if (iter == active_mod_inputs_.end()) { + // The key press event was never sent, so do it now. + event_data.emplace_back(input_with_modifiers, true); + } else { + active_mod_inputs_.erase(iter); + } + + // Send the key release event with the active modifiers. + event_data.emplace_back(input_with_modifiers, false); + + // Also send the key release event without modifiers. + event_data.emplace_back(config::KeyboardInput(key, wxMOD_NONE), false); + } + } + + // Also check for any key that were pressed with the previously active + // modifiers and release them. + for (const wxKeyCode active_key : active_keys_) { + const config::KeyboardInput input(active_key, previous_mods); + auto iter = active_mod_inputs_.find(input); + if (iter != active_mod_inputs_.end()) { + active_mod_inputs_.erase(iter); + event_data.emplace_back(std::move(input), false); + } + } + + for (const auto& data : event_data) { + active_mod_inputs_.erase(data.input.keyboard_input()); + } + + wxQueueEvent(handler_provider_->event_handler(), new UserInputEvent(std::move(event_data))); +} + +} // namespace widgets diff --git a/src/wx/widgets/keyboard-input-handler.h b/src/wx/widgets/keyboard-input-handler.h new file mode 100644 index 00000000..7d33c088 --- /dev/null +++ b/src/wx/widgets/keyboard-input-handler.h @@ -0,0 +1,49 @@ +#ifndef VBAM_WX_WIDGETS_KEYBOARD_INPUT_HANDLER_H_ +#define VBAM_WX_WIDGETS_KEYBOARD_INPUT_HANDLER_H_ + +#include + +#include + +#include "wx/config/user-input.h" +#include "wx/widgets/event-handler-provider.h" + +namespace widgets { + +// Object that is used to fire user input events when a keyboard key is pressed +// or released. This class should be kept as a singleton owned by the +// application object. It is meant to be used in the FilterEvent() method of the +// app to create user input events globally whenever the keyboard is used. +class KeyboardInputHandler final { +public: + explicit KeyboardInputHandler(EventHandlerProvider* const handler_provider); + ~KeyboardInputHandler(); + + // Disable copy and copy assignment. + KeyboardInputHandler(const KeyboardInputHandler&) = delete; + KeyboardInputHandler& operator=(const KeyboardInputHandler&) = delete; + + // Processes the provided key event and sends the appropriate user input + // event to the current event handler. + void ProcessKeyEvent(wxKeyEvent& event); + + // Resets the state of the sender. This should be called when the main frame + // loses focus to prevent stuck keys. + void Reset(); + +private: + // Keyboard event handlers. + void OnKeyDown(wxKeyEvent& event); + void OnKeyUp(wxKeyEvent& event); + + std::unordered_set active_keys_; + std::unordered_set active_mods_; + std::unordered_set active_mod_inputs_; + + // The provider of event handlers to send the events to. + EventHandlerProvider* const handler_provider_; +}; + +} // namespace widgets + +#endif // VBAM_WX_WIDGETS_KEYBOARD_INPUT_HANDLER_H_ diff --git a/src/wx/widgets/user-input-event-test.cpp b/src/wx/widgets/user-input-event-test.cpp new file mode 100644 index 00000000..c590a82b --- /dev/null +++ b/src/wx/widgets/user-input-event-test.cpp @@ -0,0 +1,53 @@ +#include "wx/widgets/user-input-event.h" + +#include + +#include + +#include "wx/config/user-input.h" + +namespace widgets { + +namespace { + +static constexpr config::KeyboardInput kF1(wxKeyCode::WXK_F1); +static constexpr config::KeyboardInput kCtrlF1(wxKeyCode::WXK_F1, wxMOD_CONTROL); +static constexpr config::JoyInput kButton0(config::JoyId(0), config::JoyControl::Button, 0); + +} // namespace + +TEST(UserInputEventTest, KeyboardInputEvent) { + // Press Ctrl+F1. + UserInputEvent pressed_event({{kCtrlF1, true}}); + EXPECT_EQ(pressed_event.FirstReleasedInput(), nonstd::nullopt); + + // Process Ctrl+F1. + EXPECT_EQ(pressed_event.FilterProcessedInput(kCtrlF1), wxEventFilter::Event_Processed); + EXPECT_EQ(pressed_event.data().size(), 0); +} + +TEST(UserInputEventTest, JoystickInputEvent) { + // Press button 0. + UserInputEvent pressed_event({{kButton0, true}}); + EXPECT_EQ(pressed_event.FirstReleasedInput(), nonstd::nullopt); + + // Process button 0. + EXPECT_EQ(pressed_event.FilterProcessedInput(kButton0), wxEventFilter::Event_Processed); + EXPECT_EQ(pressed_event.data().size(), 0); +} + +TEST(UserInputeventTest, MultipleInput) { + // Release F1 and Ctrl+F1. + UserInputEvent pressed_event({{kCtrlF1, false}, {kF1, false}}); + EXPECT_EQ(pressed_event.FirstReleasedInput(), kCtrlF1); + + // Process Ctrl+F1. + EXPECT_EQ(pressed_event.FilterProcessedInput(kCtrlF1), wxEventFilter::Event_Skip); + EXPECT_EQ(pressed_event.data().size(), 1); + + // Process button 0. + EXPECT_EQ(pressed_event.FilterProcessedInput(kF1), wxEventFilter::Event_Processed); + EXPECT_EQ(pressed_event.data().size(), 0); +} + +} // namespace widgets diff --git a/src/wx/widgets/user-input-event.cpp b/src/wx/widgets/user-input-event.cpp index fba8f056..04531d28 100644 --- a/src/wx/widgets/user-input-event.cpp +++ b/src/wx/widgets/user-input-event.cpp @@ -1,116 +1,15 @@ #include "wx/widgets/user-input-event.h" -#include +#include #include #include #include -#include #include "wx/config/user-input.h" namespace widgets { -namespace { - -// Filters the received key code in the key event for something we can use. -wxKeyCode FilterKeyCode(const wxKeyEvent& event) { - const wxKeyCode unicode_key = static_cast(event.GetUnicodeKey()); - if (unicode_key == WXK_NONE) { - // We need to filter out modifier keys here so we can differentiate - // between a key press and a modifier press. - const wxKeyCode keycode = static_cast(event.GetKeyCode()); - switch (keycode) { - case WXK_CONTROL: - case WXK_ALT: - case WXK_SHIFT: -#ifdef __WXMAC__ - case WXK_RAW_CONTROL: -#endif - return WXK_NONE; - default: - return keycode; - } - } - - if (unicode_key < 32) { - switch (unicode_key) { - case WXK_BACK: - case WXK_TAB: - case WXK_RETURN: - case WXK_ESCAPE: - return unicode_key; - default: - return WXK_NONE; - } - } - - return unicode_key; -} - -// Returns the set of modifiers for the given key event. -std::unordered_set GetModifiers(const wxKeyEvent& event) { - // Standalone modifier are treated as keys and do not set the keyboard modifiers. - switch (event.GetKeyCode()) { - case WXK_CONTROL: - return {wxMOD_CONTROL}; - case WXK_ALT: - return {wxMOD_ALT}; - case WXK_SHIFT: - return {wxMOD_SHIFT}; -#ifdef __WXMAC__ - case WXK_RAW_CONTROL: - return {wxMOD_RAW_CONTROL}; -#endif - } - - std::unordered_set mods; - if (event.ControlDown()) { - mods.insert(wxMOD_CONTROL); - } - if (event.AltDown()) { - mods.insert(wxMOD_ALT); - } - if (event.ShiftDown()) { - mods.insert(wxMOD_SHIFT); - } -#ifdef __WXMAC__ - if (event.RawControlDown()) { - mods.insert(wxMOD_RAW_CONTROL); - } -#endif - return mods; -} - -// Builds a wxKeyModifier from a set of modifiers. -wxKeyModifier GetModifiersFromSet(const std::unordered_set& mods) { - int mod = wxMOD_NONE; - for (const wxKeyModifier m : mods) { - mod |= m; - } - return static_cast(mod); -} - -// Returns the key code for a standalone modifier. -wxKeyCode KeyFromModifier(const wxKeyModifier mod) { - switch (mod) { - case wxMOD_CONTROL: - return WXK_CONTROL; - case wxMOD_ALT: - return WXK_ALT; - case wxMOD_SHIFT: - return WXK_SHIFT; -#ifdef __WXMAC__ - case wxMOD_RAW_CONTROL: - return WXK_RAW_CONTROL; -#endif - default: - return WXK_NONE; - } -} - -} // namespace - UserInputEvent::UserInputEvent(std::vector event_data) : wxEvent(0, VBAM_EVT_USER_INPUT), data_(std::move(event_data)) {} @@ -119,7 +18,7 @@ nonstd::optional UserInputEvent::FirstReleasedInput() const { std::find_if(data_.begin(), data_.end(), [](const auto& data) { return !data.pressed; }); if (iter == data_.end()) { - // No pressed inputs. + // No released inputs. return nonstd::nullopt; } @@ -151,163 +50,6 @@ wxEvent* UserInputEvent::Clone() const { return new UserInputEvent(this->data_); } -KeyboardInputSender::KeyboardInputSender(EventHandlerProvider* const handler_provider) - : handler_provider_(handler_provider) { - VBAM_CHECK(handler_provider_); -} - -KeyboardInputSender::~KeyboardInputSender() = default; - -void KeyboardInputSender::ProcessKeyEvent(wxKeyEvent& event) { - if (!handler_provider_->event_handler()) { - // No event handler to send the event to. - return; - } - - if (event.GetEventType() == wxEVT_KEY_DOWN) { - OnKeyDown(event); - } else if (event.GetEventType() == wxEVT_KEY_UP) { - OnKeyUp(event); - } -} - -void KeyboardInputSender::OnKeyDown(wxKeyEvent& event) { - // Stop propagation of the event. - event.Skip(false); - - const wxKeyCode key = FilterKeyCode(event); - const std::unordered_set mods = GetModifiers(event); - - wxKeyCode key_pressed = WXK_NONE; - if (key != WXK_NONE) { - if (active_keys_.find(key) == active_keys_.end()) { - // Key was not pressed before. - key_pressed = key; - active_keys_.insert(key); - } - } - - wxKeyModifier mod_pressed = wxMOD_NONE; - for (const wxKeyModifier mod : mods) { - if (active_mods_.find(mod) == active_mods_.end()) { - // Mod was not pressed before. - active_mods_.insert(mod); - mod_pressed = mod; - break; - } - } - - if (key_pressed == WXK_NONE && mod_pressed == wxMOD_NONE) { - // No new keys or mods were pressed. - return; - } - - const wxKeyModifier active_mods = GetModifiersFromSet(active_mods_); - std::vector event_data; - if (key_pressed == WXK_NONE) { - // A new standalone modifier was pressed, send the event. - event_data.emplace_back(config::KeyboardInput(KeyFromModifier(mod_pressed), mod_pressed), - true); - } else { - // A new key was pressed, send the event with modifiers, first. - event_data.emplace_back(config::KeyboardInput(key, active_mods), true); - - if (active_mods != wxMOD_NONE) { - // Keep track of the key pressed with the active modifiers. - active_mod_inputs_.emplace(key, active_mods); - - // Also send the key press event without modifiers. - event_data.emplace_back(config::KeyboardInput(key, wxMOD_NONE), true); - } - } - - wxQueueEvent(handler_provider_->event_handler(), new UserInputEvent(std::move(event_data))); -} - -void KeyboardInputSender::OnKeyUp(wxKeyEvent& event) { - // Stop propagation of the event. - event.Skip(false); - - const wxKeyCode key = FilterKeyCode(event); - const std::unordered_set mods = GetModifiers(event); - const wxKeyModifier previous_mods = GetModifiersFromSet(active_mods_); - - wxKeyCode key_released = WXK_NONE; - if (key != WXK_NONE) { - auto iter = active_keys_.find(key); - if (iter != active_keys_.end()) { - // Key was pressed before. - key_released = key; - active_keys_.erase(iter); - } - } - - wxKeyModifier mod_released = wxMOD_NONE; - if (key_released == WXK_NONE) { - // Only look for a standalone modifier if no key was released. - for (const wxKeyModifier mod : mods) { - auto iter = active_mods_.find(mod); - if (iter != active_mods_.end()) { - // Mod was pressed before. - mod_released = mod; - active_mods_.erase(iter); - break; - } - } - } - - if (key_released == WXK_NONE && mod_released == wxMOD_NONE) { - // No keys or mods were released. - return; - } - - std::vector event_data; - if (key_released == WXK_NONE) { - // A standalone modifier was released, send it. - event_data.emplace_back(config::KeyboardInput(KeyFromModifier(mod_released), mod_released), - false); - } else { - // A key was released. - if (previous_mods == wxMOD_NONE) { - // The key was pressed without modifiers, just send the key release event. - event_data.emplace_back(config::KeyboardInput(key, wxMOD_NONE), false); - } else { - // Check if the key was pressed with the active modifiers. - const config::KeyboardInput input_with_modifiers(key, previous_mods); - auto iter = active_mod_inputs_.find(input_with_modifiers); - if (iter == active_mod_inputs_.end()) { - // The key press event was never sent, so do it now. - event_data.emplace_back(input_with_modifiers, true); - } else { - active_mod_inputs_.erase(iter); - } - - // Send the key release event with the active modifiers. - event_data.emplace_back(config::KeyboardInput(key, previous_mods), false); - - // Also send the key release event without modifiers. - event_data.emplace_back(config::KeyboardInput(key, wxMOD_NONE), false); - } - } - - // Also check for any key that were pressed with the previously active - // modifiers and release them. - for (const wxKeyCode active_key : active_keys_) { - const config::KeyboardInput input(active_key, previous_mods); - auto iter = active_mod_inputs_.find(input); - if (iter != active_mod_inputs_.end()) { - active_mod_inputs_.erase(iter); - event_data.emplace_back(std::move(input), false); - } - } - - for (const auto& data : event_data) { - active_mod_inputs_.erase(data.input.keyboard_input()); - } - - wxQueueEvent(handler_provider_->event_handler(), new UserInputEvent(std::move(event_data))); -} - } // namespace widgets -wxDEFINE_EVENT(VBAM_EVT_USER_INPUT, widgets::UserInputEvent); +wxDEFINE_EVENT(VBAM_EVT_USER_INPUT, ::widgets::UserInputEvent); diff --git a/src/wx/widgets/user-input-event.h b/src/wx/widgets/user-input-event.h index 0e88c432..17e2bf5e 100644 --- a/src/wx/widgets/user-input-event.h +++ b/src/wx/widgets/user-input-event.h @@ -1,7 +1,6 @@ -#ifndef WX_WIDGETS_USER_INPUT_EVENT_H_ -#define WX_WIDGETS_USER_INPUT_EVENT_H_ +#ifndef VBAM_WX_WIDGETS_USER_INPUT_EVENT_H_ +#define VBAM_WX_WIDGETS_USER_INPUT_EVENT_H_ -#include #include #include @@ -10,7 +9,6 @@ #include #include "wx/config/user-input.h" -#include "wx/widgets/event-handler-provider.h" namespace widgets { @@ -18,6 +16,8 @@ namespace widgets { // contains the set of user input that were pressed or released. The order // in the vector matters, this is the order in which the inputs were pressed or // released. +// Note that a single event can contain multiple inputs pressed and/or released. +// These should be processed in the same order as they are in the vector. class UserInputEvent final : public wxEvent { public: // Data for the event. Contains the user input and whether it was pressed or @@ -26,7 +26,13 @@ public: const config::UserInput input; const bool pressed; - Data(config::UserInput input, bool pressed) : input(input), pressed(pressed){}; + Data(config::UserInput input, bool pressed) : input(input), pressed(pressed) {}; + + // Equality operators. + bool operator==(const Data& other) const { + return input == other.input && pressed == other.pressed; + } + bool operator!=(const Data& other) const { return !(*this == other); } }; UserInputEvent(std::vector event_data); @@ -36,7 +42,7 @@ public: UserInputEvent(const UserInputEvent&) = delete; UserInputEvent& operator=(const UserInputEvent&) = delete; - // Returns the first pressed input, if any. + // Returns the first released input, if any. nonstd::optional FirstReleasedInput() const; // Marks `user_input` as processed and returns the new event filter. This is @@ -53,38 +59,9 @@ private: std::vector data_; }; -// Object that is used to fire user input events when a key is pressed or -// released. This class should be kept as a singleton owned by the application -// object. It is meant to be used in the FilterEvent() method of the app. -class KeyboardInputSender final : public wxClientData { -public: - explicit KeyboardInputSender(EventHandlerProvider* const handler_provider); - ~KeyboardInputSender() override; - - // Disable copy and copy assignment. - KeyboardInputSender(const KeyboardInputSender&) = delete; - KeyboardInputSender& operator=(const KeyboardInputSender&) = delete; - - // Processes the provided key event and sends the appropriate user input - // event to the current event handler. - void ProcessKeyEvent(wxKeyEvent& event); - -private: - // Keyboard event handlers. - void OnKeyDown(wxKeyEvent& event); - void OnKeyUp(wxKeyEvent& event); - - std::unordered_set active_keys_; - std::unordered_set active_mods_; - std::unordered_set active_mod_inputs_; - - // The provider of event handlers to send the events to. - EventHandlerProvider* const handler_provider_; -}; - } // namespace widgets // Fired when a set of user inputs are pressed or released. -wxDECLARE_EVENT(VBAM_EVT_USER_INPUT, widgets::UserInputEvent); +wxDECLARE_EVENT(VBAM_EVT_USER_INPUT, ::widgets::UserInputEvent); -#endif // WX_WIDGETS_USER_INPUT_EVENT_H_ +#endif // VBAM_WX_WIDGETS_USER_INPUT_EVENT_H_ diff --git a/src/wx/wxvbam.cpp b/src/wx/wxvbam.cpp index 950970a7..3e55f2be 100644 --- a/src/wx/wxvbam.cpp +++ b/src/wx/wxvbam.cpp @@ -248,7 +248,14 @@ wxvbamApp::wxvbamApp() using_wayland(false), emulated_gamepad_(std::bind(&wxvbamApp::bindings, this)), sdl_poller_(this), - keyboard_input_sender_(this) {} + keyboard_input_handler_(this) { + Bind(wxEVT_ACTIVATE_APP, [this](wxActivateEvent& event) { + if (!event.GetActive()) { + keyboard_input_handler_.Reset(); + } + event.Skip(); + }); +} const wxString wxvbamApp::GetPluginsDir() { @@ -851,7 +858,6 @@ MainFrame::MainFrame() paused(false), menus_opened(0), dialog_opened(0), - focused(false), #ifndef NO_LINK gba_link_observer_(config::OptionID::kGBALinkHost, std::bind(&MainFrame::EnableNetworkMenu, this)), @@ -905,18 +911,24 @@ EVT_MENU_HIGHLIGHT_ALL(MainFrame::MenuPopped) END_EVENT_TABLE() -void MainFrame::OnActivate(wxActivateEvent& event) -{ - focused = event.GetActive(); +void MainFrame::OnActivate(wxActivateEvent& event) { + const bool focused = event.GetActive(); - if (panel && focused) + if (!panel) { + // Nothing more to do if no game is active. + return; + } + + if (focused) { + // Set the focus to the game panel. panel->SetFocus(); + } if (OPTION(kPrefPauseWhenInactive)) { - if (panel && focused && !paused) { + // Handle user preferences for pausing the game when the window is inactive. + if (focused && !paused) { panel->Resume(); - } - else if (panel && !focused) { + } else if (!focused) { panel->Pause(); } } @@ -1337,7 +1349,7 @@ int wxvbamApp::FilterEvent(wxEvent& event) if (event.GetEventType() == wxEVT_KEY_DOWN || event.GetEventType() == wxEVT_KEY_UP) { // Handle keyboard input events here to generate user input events. - keyboard_input_sender_.ProcessKeyEvent(static_cast(event)); + keyboard_input_handler_.ProcessKeyEvent(static_cast(event)); return wxEventFilter::Event_Skip; } diff --git a/src/wx/wxvbam.h b/src/wx/wxvbam.h index c1ab6a67..93c35e57 100644 --- a/src/wx/wxvbam.h +++ b/src/wx/wxvbam.h @@ -3,7 +3,6 @@ #include #include -#include #include #include #include @@ -20,6 +19,7 @@ #include "wx/widgets/dpi-support.h" #include "wx/widgets/event-handler-provider.h" #include "wx/widgets/keep-on-top-styler.h" +#include "wx/widgets/keyboard-input-handler.h" #include "wx/widgets/sdl-poller.h" #include "wx/widgets/user-input-event.h" #include "wx/widgets/wxmisc.h" @@ -144,7 +144,7 @@ private: char* home = nullptr; widgets::SdlPoller sdl_poller_; - widgets::KeyboardInputSender keyboard_input_sender_; + widgets::KeyboardInputHandler keyboard_input_handler_; // Main configuration file. wxFileName config_file_; @@ -251,9 +251,6 @@ public: // Resets all menu accelerators. void ResetMenuAccelerators(); - // 2.8 has no HasFocus(), and FindFocus() doesn't work right - bool HasFocus() const override { return focused; } - #ifndef NO_LINK // Returns the link mode to set according to the options LinkMode GetConfiguredLinkMode(); @@ -326,8 +323,6 @@ private: checkable_mi_array_t checkable_mi; // recent menu item accels wxMenu* recent; - // quicker & more accurate than FindFocus() != NULL - bool focused; // One-time toggle to indicate that this object is fully initialized. This // used to filter events that are sent during initialization. bool init_complete_ = false;