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;