[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.
This commit is contained in:
Fabrice de Gans 2024-10-08 18:22:17 -07:00 committed by GitHub
parent 709a322337
commit 9d20ce9b59
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 618 additions and 316 deletions

View File

@ -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")

View File

@ -11,6 +11,7 @@ target_sources(vbam-wx-widgets
$<IF:$<BOOL:${APPLE}>,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)

View File

@ -0,0 +1,197 @@
#include "wx/widgets/keyboard-input-handler.h"
#include <algorithm>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <wx/event.h>
#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<UserInputEvent::Data> ProcessKeyEvent(wxKeyEvent key_event) {
handler_.ProcessKeyEvent(key_event);
ProcessPendingEvents();
std::vector<UserInputEvent::Data> 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<UserInputEvent::Data> 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<wxKeyModifier>(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

View File

@ -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 <wx/log.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<wxKeyCode>(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<wxKeyCode>(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<wxKeyModifier> 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<wxKeyModifier> 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<wxKeyModifier>& mods) {
int mod = wxMOD_NONE;
for (const wxKeyModifier m : mods) {
mod |= m;
}
return static_cast<wxKeyModifier>(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<wxKeyModifier> 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<UserInputEvent::Data> 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<wxKeyModifier> 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<UserInputEvent::Data> 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

View File

@ -0,0 +1,49 @@
#ifndef VBAM_WX_WIDGETS_KEYBOARD_INPUT_HANDLER_H_
#define VBAM_WX_WIDGETS_KEYBOARD_INPUT_HANDLER_H_
#include <unordered_set>
#include <wx/event.h>
#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<wxKeyCode> active_keys_;
std::unordered_set<wxKeyModifier> active_mods_;
std::unordered_set<config::KeyboardInput> 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_

View File

@ -0,0 +1,53 @@
#include "wx/widgets/user-input-event.h"
#include <gtest/gtest.h>
#include <wx/eventfilter.h>
#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

View File

@ -1,116 +1,15 @@
#include "wx/widgets/user-input-event.h"
#include <algorithm>
#include <utility>
#include <vector>
#include <wx/event.h>
#include <wx/eventfilter.h>
#include <wx/window.h>
#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<wxKeyCode>(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<wxKeyCode>(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<wxKeyModifier> 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<wxKeyModifier> 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<wxKeyModifier>& mods) {
int mod = wxMOD_NONE;
for (const wxKeyModifier m : mods) {
mod |= m;
}
return static_cast<wxKeyModifier>(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<Data> event_data)
: wxEvent(0, VBAM_EVT_USER_INPUT), data_(std::move(event_data)) {}
@ -119,7 +18,7 @@ nonstd::optional<config::UserInput> 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<wxKeyModifier> 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<UserInputEvent::Data> 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<wxKeyModifier> 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<UserInputEvent::Data> 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);

View File

@ -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 <unordered_set>
#include <vector>
#include <optional.hpp>
@ -10,7 +9,6 @@
#include <wx/event.h>
#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<Data> 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<config::UserInput> FirstReleasedInput() const;
// Marks `user_input` as processed and returns the new event filter. This is
@ -53,38 +59,9 @@ private:
std::vector<Data> 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<wxKeyCode> active_keys_;
std::unordered_set<wxKeyModifier> active_mods_;
std::unordered_set<config::KeyboardInput> 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_

View File

@ -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<wxKeyEvent&>(event));
keyboard_input_handler_.ProcessKeyEvent(static_cast<wxKeyEvent&>(event));
return wxEventFilter::Event_Skip;
}

View File

@ -3,7 +3,6 @@
#include <list>
#include <stdexcept>
#include <typeinfo>
#include <iostream>
#include <stdio.h>
#include <time.h>
@ -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;