[UserInput] Only process shortcut commands once

This modifies the UserInputEvent class to fire a vector of events at
once, rather than individual down/up events for each UserInput. This
simplifies handling of the global event filter and prevents the firing
of spurious events.

This also fixes a bug when pressing "Ctrl+1" would trigger the command
for both the command assigned to "Ctrl+1" and to "1". Now, only the
"Ctrl+1" command will fire.
This commit is contained in:
Fabrice de Gans 2024-05-04 13:06:20 -07:00 committed by Fabrice de Gans
parent d32be9ddbe
commit 902c6c8e4b
8 changed files with 201 additions and 123 deletions

View File

@ -1177,8 +1177,7 @@ void GameArea::OnIdle(wxIdleEvent& event)
wxWindow* w = panel->GetWindow();
// set up event handlers
w->Bind(VBAM_EVT_USER_INPUT_DOWN, &GameArea::OnUserInputDown, this);
w->Bind(VBAM_EVT_USER_INPUT_UP, &GameArea::OnUserInputUp, this);
w->Bind(VBAM_EVT_USER_INPUT, &GameArea::OnUserInput, this);
w->Bind(wxEVT_PAINT, &GameArea::PaintEv, this);
w->Bind(wxEVT_ERASE_BACKGROUND, &GameArea::EraseBackground, this);
@ -1330,26 +1329,33 @@ static Display* GetX11Display() {
}
#endif // __WXGTK__
void GameArea::OnUserInputDown(widgets::UserInputEvent& event) {
if (wxGetApp().emulated_gamepad()->OnInputPressed(event.input())) {
wxWakeUpIdle();
}
}
void GameArea::OnUserInputUp(widgets::UserInputEvent& event) {
if (wxGetApp().emulated_gamepad()->OnInputReleased(event.input())) {
wxWakeUpIdle();
void GameArea::OnUserInput(widgets::UserInputEvent& event) {
bool emulated_key_pressed = false;
for (const auto& event_data : event.data()) {
if (event_data.pressed) {
if (wxGetApp().emulated_gamepad()->OnInputPressed(event_data.input)) {
emulated_key_pressed = true;
}
} else {
if (wxGetApp().emulated_gamepad()->OnInputReleased(event_data.input)) {
emulated_key_pressed = true;
}
}
}
// tell Linux to turn off the screensaver/screen-blank if joystick button was pressed
// this shouldn't be necessary of course
if (emulated_key_pressed) {
wxWakeUpIdle();
#if defined(__WXGTK__) && defined(HAVE_X11) && !defined(HAVE_XSS)
if (event.input().is_joystick() && !wxGetApp().UsingWayland()) {
auto display = GetX11Display();
XResetScreenSaver(display);
XFlush(display);
}
// Tell X11 to turn off the screensaver/screen-blank if a button was
// was pressed. This shouldn't be necessary.
if (!wxGetApp().UsingWayland()) {
auto display = GetX11Display();
XResetScreenSaver(display);
XFlush(display);
}
#endif
}
}
// these three are forwarded to the DrawingPanel instance

View File

@ -3,7 +3,6 @@
#include <cassert>
#include <map>
#include <SDL_events.h>
#include <wx/timer.h>
#include <wx/toplevel.h>
@ -86,9 +85,10 @@ public:
bool is_game_controller() const { return !!game_controller_; }
// Processes the corresponding events.
std::vector<UserInputEvent> ProcessAxisEvent(const uint8_t index, const JoyAxisStatus status);
std::vector<UserInputEvent> ProcessButtonEvent(const uint8_t index, const bool pressed);
std::vector<UserInputEvent> ProcessHatEvent(const uint8_t index, const uint8_t status);
std::vector<UserInputEvent::Data> ProcessAxisEvent(const uint8_t index,
const JoyAxisStatus status);
std::vector<UserInputEvent::Data> ProcessButtonEvent(const uint8_t index, const bool pressed);
std::vector<UserInputEvent::Data> ProcessHatEvent(const uint8_t index, const uint8_t status);
// Activates or deactivates rumble.
void SetRumble(bool activate_rumble);
@ -192,14 +192,14 @@ bool JoyState::IsValid() const {
return sdl_joystick_;
}
std::vector<UserInputEvent> JoyState::ProcessAxisEvent(const uint8_t index,
const JoyAxisStatus status) {
std::vector<UserInputEvent::Data> JoyState::ProcessAxisEvent(const uint8_t index,
const JoyAxisStatus status) {
const JoyAxisStatus previous_status = axis_[index];
std::vector<UserInputEvent> events;
std::vector<UserInputEvent::Data> event_data;
// Nothing to do if no-op.
if (status == previous_status) {
return events;
return event_data;
}
// Update the value.
@ -207,48 +207,50 @@ std::vector<UserInputEvent> JoyState::ProcessAxisEvent(const uint8_t index,
if (previous_status != JoyAxisStatus::Neutral) {
// Send the "unpressed" event.
events.push_back(UserInputEvent(
config::JoyInput(wx_joystick_, AxisStatusToJoyControl(previous_status), index), false));
event_data.emplace_back(
config::JoyInput(wx_joystick_, AxisStatusToJoyControl(previous_status), index), false);
}
// We already sent the "unpressed" event so nothing more to do.
if (status == JoyAxisStatus::Neutral) {
return events;
return event_data;
}
// Send the "pressed" event.
events.push_back(UserInputEvent(
config::JoyInput(wx_joystick_, AxisStatusToJoyControl(status), index), true));
event_data.emplace_back(config::JoyInput(wx_joystick_, AxisStatusToJoyControl(status), index),
true);
return events;
return event_data;
}
std::vector<UserInputEvent> JoyState::ProcessButtonEvent(const uint8_t index, const bool status) {
std::vector<UserInputEvent::Data> JoyState::ProcessButtonEvent(const uint8_t index,
const bool status) {
const bool previous_status = buttons_[index];
std::vector<UserInputEvent> events;
std::vector<UserInputEvent::Data> event_data;
// Nothing to do if no-op.
if (status == previous_status) {
return events;
return event_data;
}
// Update the value.
buttons_[index] = status;
// Send the event.
events.push_back(
UserInputEvent(config::JoyInput(wx_joystick_, config::JoyControl::Button, index), status));
event_data.emplace_back(config::JoyInput(wx_joystick_, config::JoyControl::Button, index),
status);
return events;
return event_data;
}
std::vector<UserInputEvent> JoyState::ProcessHatEvent(const uint8_t index, const uint8_t status) {
std::vector<UserInputEvent::Data> JoyState::ProcessHatEvent(const uint8_t index,
const uint8_t status) {
const uint16_t previous_status = hats_[index];
std::vector<UserInputEvent> events;
std::vector<UserInputEvent::Data> event_data;
// Nothing to do if no-op.
if (status == previous_status) {
return events;
return event_data;
}
// Update the value.
@ -262,17 +264,17 @@ std::vector<UserInputEvent> JoyState::ProcessHatEvent(const uint8_t index, const
const bool new_control_pressed = (status & bit) != 0;
if (old_control_pressed && !new_control_pressed) {
// Send the "unpressed" event.
events.push_back(UserInputEvent(
config::JoyInput(wx_joystick_, HatStatusToJoyControl(bit), index), false));
event_data.emplace_back(
config::JoyInput(wx_joystick_, HatStatusToJoyControl(bit), index), false);
}
if (!old_control_pressed && new_control_pressed) {
// Send the "pressed" event.
events.push_back(UserInputEvent(
config::JoyInput(wx_joystick_, HatStatusToJoyControl(bit), index), true));
event_data.emplace_back(
config::JoyInput(wx_joystick_, HatStatusToJoyControl(bit), index), true);
}
}
return events;
return event_data;
}
void JoyState::SetRumble(bool activate_rumble) {
@ -341,23 +343,23 @@ void SdlPoller::Notify() {
SDL_Event sdl_event;
while (SDL_PollEvent(&sdl_event)) {
std::vector<UserInputEvent> events;
std::vector<UserInputEvent::Data> event_data;
JoyState* joy_state = nullptr;
switch (sdl_event.type) {
case SDL_CONTROLLERBUTTONDOWN:
case SDL_CONTROLLERBUTTONUP:
joy_state = FindJoyState(sdl_event.cbutton.which);
if (joy_state) {
events = joy_state->ProcessButtonEvent(sdl_event.cbutton.button,
sdl_event.cbutton.state);
event_data = joy_state->ProcessButtonEvent(sdl_event.cbutton.button,
sdl_event.cbutton.state);
}
break;
case SDL_CONTROLLERAXISMOTION:
joy_state = FindJoyState(sdl_event.caxis.which);
if (joy_state) {
events = joy_state->ProcessAxisEvent(sdl_event.caxis.axis,
AxisValueToStatus(sdl_event.caxis.value));
event_data = joy_state->ProcessAxisEvent(
sdl_event.caxis.axis, AxisValueToStatus(sdl_event.caxis.value));
}
break;
@ -372,23 +374,24 @@ void SdlPoller::Notify() {
case SDL_JOYBUTTONUP:
joy_state = FindJoyState(sdl_event.jbutton.which);
if (joy_state && !joy_state->is_game_controller()) {
events = joy_state->ProcessButtonEvent(sdl_event.jbutton.button,
sdl_event.jbutton.state);
event_data = joy_state->ProcessButtonEvent(sdl_event.jbutton.button,
sdl_event.jbutton.state);
}
break;
case SDL_JOYAXISMOTION:
joy_state = FindJoyState(sdl_event.jaxis.which);
if (joy_state && !joy_state->is_game_controller()) {
events = joy_state->ProcessAxisEvent(sdl_event.jaxis.axis,
AxisValueToStatus(sdl_event.jaxis.value));
event_data = joy_state->ProcessAxisEvent(
sdl_event.jaxis.axis, AxisValueToStatus(sdl_event.jaxis.value));
}
break;
case SDL_JOYHATMOTION:
joy_state = FindJoyState(sdl_event.jhat.which);
if (joy_state && !joy_state->is_game_controller()) {
events = joy_state->ProcessHatEvent(sdl_event.jhat.hat, sdl_event.jhat.value);
event_data =
joy_state->ProcessHatEvent(sdl_event.jhat.hat, sdl_event.jhat.value);
}
break;
@ -402,12 +405,10 @@ void SdlPoller::Notify() {
break;
}
if (!events.empty()) {
if (!event_data.empty()) {
wxEvtHandler* handler = handler_provider_();
if (handler) {
for (const auto& user_input_event : events) {
handler->QueueEvent(user_input_event.Clone());
}
handler->QueueEvent(new UserInputEvent(std::move(event_data)));
}
}
}

View File

@ -31,9 +31,8 @@ bool UserInputCtrl::Create(wxWindow* parent,
long style,
const wxString& name) {
this->SetClientObject(new UserInputEventSender(this));
this->Bind(VBAM_EVT_USER_INPUT_UP, &UserInputCtrl::OnUserInputUp, this);
this->Bind(VBAM_EVT_USER_INPUT, &UserInputCtrl::OnUserInput, this);
this->Bind(wxEVT_SET_FOCUS, [this](wxFocusEvent& event) {
is_navigating_away_ = false;
last_focus_time_ = wxGetUTCTimeMillis();
event.Skip();
});
@ -66,7 +65,15 @@ void UserInputCtrl::Clear() {
wxIMPLEMENT_DYNAMIC_CLASS(UserInputCtrl, wxTextCtrl);
void UserInputCtrl::OnUserInputUp(UserInputEvent& event) {
void UserInputCtrl::OnUserInput(UserInputEvent& event) {
// Find the first pressed input.
nonstd::optional<config::UserInput> input = event.FirstReleasedInput();
if (input == nonstd::nullopt) {
// No pressed inputs.
return;
}
static const wxLongLong kInterval = 100;
if (wxGetUTCTimeMillis() - last_focus_time_ < kInterval) {
// Ignore events sent very shortly after focus. This is used to ignore
@ -75,20 +82,13 @@ void UserInputCtrl::OnUserInputUp(UserInputEvent& event) {
return;
}
if (is_navigating_away_) {
// Ignore events sent after the control has been navigated away from.
event.Skip();
return;
}
if (!is_multikey_) {
inputs_.clear();
}
inputs_.insert(event.input());
inputs_.insert(std::move(input.value()));
UpdateText();
Navigate();
is_navigating_away_ = true;
}
void UserInputCtrl::UpdateText() {

View File

@ -61,7 +61,7 @@ public:
private:
// Event handler.
void OnUserInputUp(widgets::UserInputEvent& event);
void OnUserInput(widgets::UserInputEvent& event);
// Updates the text in the control to reflect the current inputs.
void UpdateText();
@ -72,10 +72,6 @@ private:
// very shortly after activation.
wxLongLong last_focus_time_ = 0;
// Set to true after one input has been received. This is used to ignore
// subsequent events until the control is focused again.
bool is_navigating_away_ = false;
std::unordered_set<config::UserInput> inputs_;
};

View File

@ -1,8 +1,10 @@
#include "wx/widgets/user-input-event.h"
#include <algorithm>
#include <vector>
#include <wx/event.h>
#include <wx/eventfilter.h>
#include <wx/window.h>
#include "wx/config/user-input.h"
@ -109,11 +111,44 @@ wxKeyCode KeyFromModifier(const wxKeyModifier mod) {
} // namespace
UserInputEvent::UserInputEvent(const config::UserInput& input, bool pressed)
: wxEvent(0, pressed ? VBAM_EVT_USER_INPUT_DOWN : VBAM_EVT_USER_INPUT_UP), input_(input) {}
UserInputEvent::UserInputEvent(std::vector<Data> event_data)
: wxEvent(0, VBAM_EVT_USER_INPUT), data_(std::move(event_data)) {}
nonstd::optional<config::UserInput> UserInputEvent::FirstReleasedInput() const {
const auto iter =
std::find_if(data_.begin(), data_.end(), [](const auto& data) { return !data.pressed; });
if (iter == data_.end()) {
// No pressed inputs.
return nonstd::nullopt;
}
return iter->input;
}
int UserInputEvent::FilterProcessedInput(const config::UserInput& user_input) {
// Keep all data not using `user_input`.
std::vector<Data> new_data;
for (const auto& data : data_) {
if (data.input != user_input) {
new_data.push_back(data);
}
}
// Update the internal data.
data_ = std::move(new_data);
if (data_.empty()) {
// All data was removed, the event was fully processed.
return wxEventFilter::Event_Processed;
} else {
// Some data remains, let the event propagate.
return wxEventFilter::Event_Skip;
}
}
wxEvent* UserInputEvent::Clone() const {
return new UserInputEvent(*this);
return new UserInputEvent(this->data_);
}
UserInputEventSender::UserInputEventSender(wxWindow* const window)
@ -161,26 +196,25 @@ void UserInputEventSender::OnKeyDown(wxKeyEvent& event) {
}
const wxKeyModifier active_mods = GetModifiersFromSet(active_mods_);
std::vector<config::KeyboardInput> new_inputs;
std::vector<UserInputEvent::Data> event_data;
if (key_pressed == WXK_NONE) {
// A new standalone modifier was pressed, send the event.
new_inputs.emplace_back(KeyFromModifier(mod_pressed), mod_pressed);
event_data.emplace_back(config::KeyboardInput(KeyFromModifier(mod_pressed), mod_pressed),
true);
} else {
// A new key was pressed, send the event with modifiers, first.
new_inputs.emplace_back(key, active_mods);
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.
new_inputs.emplace_back(key, wxMOD_NONE);
event_data.emplace_back(config::KeyboardInput(key, wxMOD_NONE), true);
}
}
for (const config::KeyboardInput& input : new_inputs) {
wxQueueEvent(window_, new UserInputEvent(input, true));
}
wxQueueEvent(window_, new UserInputEvent(std::move(event_data)));
}
void UserInputEventSender::OnKeyUp(wxKeyEvent& event) {
@ -220,31 +254,32 @@ void UserInputEventSender::OnKeyUp(wxKeyEvent& event) {
return;
}
std::vector<config::KeyboardInput> released_inputs;
std::vector<UserInputEvent::Data> event_data;
if (key_released == WXK_NONE) {
// A standalone modifier was released, send it.
released_inputs.emplace_back(KeyFromModifier(mod_released), mod_released);
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.
released_inputs.emplace_back(key, wxMOD_NONE);
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.
wxQueueEvent(window_, new UserInputEvent(input_with_modifiers, true));
event_data.emplace_back(input_with_modifiers, true);
} else {
active_mod_inputs_.erase(iter);
}
// Send the key release event with the active modifiers.
released_inputs.emplace_back(key, previous_mods);
event_data.emplace_back(config::KeyboardInput(key, previous_mods), false);
// Also send the key release event without modifiers.
released_inputs.emplace_back(key, wxMOD_NONE);
event_data.emplace_back(config::KeyboardInput(key, wxMOD_NONE), false);
}
}
@ -255,15 +290,14 @@ void UserInputEventSender::OnKeyUp(wxKeyEvent& event) {
auto iter = active_mod_inputs_.find(input);
if (iter != active_mod_inputs_.end()) {
active_mod_inputs_.erase(iter);
released_inputs.push_back(std::move(input));
event_data.emplace_back(std::move(input), false);
}
}
for (const config::KeyboardInput& input : released_inputs) {
active_mod_inputs_.erase(input);
wxQueueEvent(window_, new UserInputEvent(input, false));
for (const auto& data : event_data) {
active_mod_inputs_.erase(data.input.keyboard_input());
}
wxQueueEvent(window_, new UserInputEvent(std::move(event_data)));
}
void UserInputEventSender::Reset(wxFocusEvent& event) {
@ -278,5 +312,4 @@ void UserInputEventSender::Reset(wxFocusEvent& event) {
} // namespace widgets
wxDEFINE_EVENT(VBAM_EVT_USER_INPUT_DOWN, widgets::UserInputEvent);
wxDEFINE_EVENT(VBAM_EVT_USER_INPUT_UP, widgets::UserInputEvent);
wxDEFINE_EVENT(VBAM_EVT_USER_INPUT, widgets::UserInputEvent);

View File

@ -2,6 +2,9 @@
#define WX_WIDGETS_USER_INPUT_EVENT_H_
#include <unordered_set>
#include <vector>
#include <optional.hpp>
#include <wx/clntdata.h>
#include <wx/event.h>
@ -10,20 +13,43 @@
namespace widgets {
// Event fired when a user input is pressed or released. The event contains the
// user input that was pressed or released.
// Event fired when a set of user input are pressed or released. The event
// 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.
class UserInputEvent final : public wxEvent {
public:
UserInputEvent(const config::UserInput& input, bool pressed);
// Data for the event. Contains the user input and whether it was pressed or
// released.
struct Data {
const config::UserInput input;
const bool pressed;
Data(config::UserInput input, bool pressed) : input(input), pressed(pressed){};
};
UserInputEvent(std::vector<Data> event_data);
virtual ~UserInputEvent() override = default;
// Disable copy and copy assignment.
UserInputEvent(const UserInputEvent&) = delete;
UserInputEvent& operator=(const UserInputEvent&) = delete;
// Returns the first pressed input, if any.
nonstd::optional<config::UserInput> FirstReleasedInput() const;
// Mark `event_data` as processed and returns the new event filter. This is
// meant to be used with FilterEvent() to process global shortcuts before
// sending the event to the next handler.
int FilterProcessedInput(const config::UserInput& user_input);
// wxEvent implementation.
wxEvent* Clone() const override;
const config::UserInput& input() const { return input_; }
const std::vector<Data>& data() const { return data_; }
private:
const config::UserInput input_;
std::vector<Data> data_;
};
// Object that is used to fire user input events when a key is pressed or
@ -59,9 +85,7 @@ private:
} // namespace widgets
// Fired when a user input is pressed.
wxDECLARE_EVENT(VBAM_EVT_USER_INPUT_DOWN, widgets::UserInputEvent);
// Fired when a user input is released.
wxDECLARE_EVENT(VBAM_EVT_USER_INPUT_UP, widgets::UserInputEvent);
// Fired when a set of user inputs are pressed or released.
wxDECLARE_EVENT(VBAM_EVT_USER_INPUT, widgets::UserInputEvent);
#endif // WX_WIDGETS_USER_INPUT_EVENT_H_

View File

@ -1,5 +1,4 @@
#include "wx/wxvbam.h"
#include "wx/config/command.h"
#ifdef __WXMSW__
#include <windows.h>
@ -35,6 +34,7 @@
#include "wx/builtin-over.h"
#include "wx/builtin-xrc.h"
#include "wx/config/cmdtab.h"
#include "wx/config/command.h"
#include "wx/config/emulated-gamepad.h"
#include "wx/config/option-proxy.h"
#include "wx/config/option.h"
@ -995,27 +995,46 @@ int MainFrame::FilterEvent(wxEvent& event) {
return wxEventFilter::Event_Skip;
}
if (event.GetEventType() != VBAM_EVT_USER_INPUT_DOWN) {
// We only treat "VBAM_EVT_USER_INPUT_DOWN" events here.
if (event.GetEventType() != VBAM_EVT_USER_INPUT) {
// We only treat "VBAM_EVT_USER_INPUT" events here.
return wxEventFilter::Event_Skip;
}
const widgets::UserInputEvent& user_input_event = static_cast<widgets::UserInputEvent&>(event);
nonstd::optional<config::Command> command =
wxGetApp().bindings()->CommandForInput(user_input_event.input());
if (command == nonstd::nullopt) {
widgets::UserInputEvent& user_input_event = static_cast<widgets::UserInputEvent&>(event);
const config::Bindings* bindings = wxGetApp().bindings();
int command_id = wxID_NONE;
nonstd::optional<config::UserInput> user_input;
for (const auto& event_data : user_input_event.data()) {
if (!event_data.pressed) {
// We only treat key press events here.
continue;
}
const nonstd::optional<config::Command> command =
bindings->CommandForInput(event_data.input);
if (command != nonstd::nullopt && command->is_shortcut()) {
// Associated shortcut command found.
command_id = command->shortcut().id();
user_input.emplace(event_data.input);
break;
}
}
if (command_id == wxID_NONE) {
// No associated command found.
return wxEventFilter::Event_Skip;
}
if (!command->is_shortcut()) {
return wxEventFilter::Event_Skip;
}
wxCommandEvent command_event(wxEVT_COMMAND_MENU_SELECTED, command->shortcut().id());
// Execute the associated shortcut command.
wxCommandEvent command_event(wxEVT_COMMAND_MENU_SELECTED, command_id);
command_event.SetEventObject(this);
this->GetEventHandler()->ProcessEvent(command_event);
return wxEventFilter::Event_Processed;
// Filter out the processed input so it is not processed again. This also
// prevents us from firing 2 commands if the user presses "Ctrl+1" and has
// a shortcut for both "Ctrl+1" and "1". Only one will be handled here.
return user_input_event.FilterProcessedInput(user_input.value());
}
wxString MainFrame::GetGamePath(wxString path)

View File

@ -530,8 +530,7 @@ protected:
bool paused;
void OnIdle(wxIdleEvent&);
void OnUserInputDown(widgets::UserInputEvent& event);
void OnUserInputUp(widgets::UserInputEvent& event);
void OnUserInput(widgets::UserInputEvent& event);
void PaintEv(wxPaintEvent& ev);
void EraseBackground(wxEraseEvent& ev);
void OnSize(wxSizeEvent& ev);