[UserInput] Filter key events globally

This changes key events to be filtered globally by the application,
preventing any widget from accessing them. Widgets should instead use
the new VBAM_USER_INPUT_EVENT event.

In addition, this explicitly removes accelerator handling from wxWidgets
main menu, allowing us to populate joystick options in the menu without
firing wxWidgets assertions.
This commit is contained in:
Fabrice de Gans 2024-05-04 15:49:12 -07:00 committed by Fabrice de Gans
parent 902c6c8e4b
commit d543784a3d
17 changed files with 228 additions and 171 deletions

View File

@ -73,6 +73,7 @@ set(VBAM_WX_COMMON
widgets/checkedlistctrl.h widgets/checkedlistctrl.h
widgets/client-data.h widgets/client-data.h
widgets/dpi-support.h widgets/dpi-support.h
widgets/event-handler-provider.h
widgets/group-check-box.cpp widgets/group-check-box.cpp
widgets/group-check-box.h widgets/group-check-box.h
widgets/keep-on-top-styler.cpp widgets/keep-on-top-styler.cpp
@ -87,6 +88,8 @@ set(VBAM_WX_COMMON
widgets/user-input-event.h widgets/user-input-event.h
widgets/sdl-poller.cpp widgets/sdl-poller.cpp
widgets/sdl-poller.h widgets/sdl-poller.h
widgets/shortcut-menu-bar.cpp
widgets/shortcut-menu-bar.h
widgets/utils.cpp widgets/utils.cpp
widgets/utils.h widgets/utils.h
widgets/webupdatedef.h widgets/webupdatedef.h

View File

@ -230,12 +230,16 @@ JoyId JoyId::Invalid() {
return JoyId(kInvalidSdlIndex); return JoyId(kInvalidSdlIndex);
} }
wxString JoyId::ToString() const { wxString JoyId::ToConfigString() const {
return wxString::Format("Joy%d", sdl_index_ + 1); return wxString::Format("Joy%d", sdl_index_ + 1);
} }
wxString JoyInput::ToString() const { wxString JoyId::ToLocalizedString() const {
const wxString joy_string = joy_.ToString(); return wxString::Format(_("Joystick %d"), sdl_index_ + 1);
}
wxString JoyInput::ToConfigString() const {
const wxString joy_string = joy_.ToConfigString();
switch (control_) { switch (control_) {
case JoyControl::AxisPlus: case JoyControl::AxisPlus:
return wxString::Format("%s-Axis%d+", joy_string, control_index_); return wxString::Format("%s-Axis%d+", joy_string, control_index_);
@ -258,6 +262,30 @@ wxString JoyInput::ToString() const {
return wxEmptyString; return wxEmptyString;
} }
wxString JoyInput::ToLocalizedString() const {
const wxString joy_string = joy_.ToLocalizedString();
switch (control_) {
case JoyControl::AxisPlus:
return wxString::Format(_("%s: Axis %d+"), joy_string, control_index_);
case JoyControl::AxisMinus:
return wxString::Format(_("%s: Axis %d-"), joy_string, control_index_);
case JoyControl::Button:
return wxString::Format(_("%s: Button %d"), joy_string, control_index_);
case JoyControl::HatNorth:
return wxString::Format(_("%s: Hat %d North"), joy_string, control_index_);
case JoyControl::HatSouth:
return wxString::Format(_("%s: Hat %d South"), joy_string, control_index_);
case JoyControl::HatWest:
return wxString::Format(_("%s: Hat %d West"), joy_string, control_index_);
case JoyControl::HatEast:
return wxString::Format(_("%s: Hat %d East"), joy_string, control_index_);
}
// Unreachable.
assert(false);
return wxEmptyString;
}
wxString KeyboardInput::ToConfigString() const { wxString KeyboardInput::ToConfigString() const {
// Handle the modifier case separately. // Handle the modifier case separately.
if (KeyIsModifier(key_)) { if (KeyIsModifier(key_)) {
@ -291,10 +319,6 @@ wxString KeyboardInput::ToLocalizedString() const {
} }
const wxString accel_string = wxAcceleratorEntry(mod_, key_).ToRawString().MakeUpper(); const wxString accel_string = wxAcceleratorEntry(mod_, key_).ToRawString().MakeUpper();
if (!accel_string.IsAscii()) {
// Unicode handling.
return wxString::Format("%d:%d", key_, mod_);
}
return accel_string; return accel_string;
} }
@ -336,7 +360,7 @@ wxString UserInput::ToConfigString() const {
case Device::Keyboard: case Device::Keyboard:
return keyboard_input().ToConfigString(); return keyboard_input().ToConfigString();
case Device::Joystick: case Device::Joystick:
return joy_input().ToString(); return joy_input().ToConfigString();
} }
// Unreachable. // Unreachable.
@ -351,7 +375,7 @@ wxString UserInput::ToLocalizedString() const {
case Device::Keyboard: case Device::Keyboard:
return keyboard_input().ToLocalizedString(); return keyboard_input().ToLocalizedString();
case Device::Joystick: case Device::Joystick:
return joy_input().ToString(); return joy_input().ToLocalizedString();
} }
// Unreachable. // Unreachable.

View File

@ -70,7 +70,8 @@ public:
constexpr explicit JoyId(int sdl_index) : sdl_index_(sdl_index){}; constexpr explicit JoyId(int sdl_index) : sdl_index_(sdl_index){};
~JoyId() = default; ~JoyId() = default;
wxString ToString() const; wxString ToConfigString() const;
wxString ToLocalizedString() const;
constexpr bool operator==(const JoyId& other) const { return sdl_index_ == other.sdl_index_; } constexpr bool operator==(const JoyId& other) const { return sdl_index_ == other.sdl_index_; }
constexpr bool operator!=(const JoyId& other) const { return sdl_index_ != other.sdl_index_; } constexpr bool operator!=(const JoyId& other) const { return sdl_index_ != other.sdl_index_; }
@ -100,7 +101,8 @@ public:
constexpr JoyControl control() const { return control_; } constexpr JoyControl control() const { return control_; }
constexpr uint8_t control_index() const { return control_index_; } constexpr uint8_t control_index() const { return control_index_; }
wxString ToString() const; wxString ToConfigString() const;
wxString ToLocalizedString() const;
constexpr bool operator==(const JoyInput& other) const { constexpr bool operator==(const JoyInput& other) const {
return joy_ == other.joy_ && control_ == other.control_ && return joy_ == other.joy_ && control_ == other.control_ &&

View File

@ -165,7 +165,6 @@ GameArea::GameArea()
config::OptionID::kSoundBuffers, config::OptionID::kSoundDSoundHWAccel, config::OptionID::kSoundBuffers, config::OptionID::kSoundDSoundHWAccel,
config::OptionID::kSoundUpmix}, config::OptionID::kSoundUpmix},
[&](config::Option*) { schedule_audio_restart_ = true; }) { [&](config::Option*) { schedule_audio_restart_ = true; }) {
this->SetClientObject(new widgets::UserInputEventSender(this));
SetSizer(new wxBoxSizer(wxVERTICAL)); SetSizer(new wxBoxSizer(wxVERTICAL));
// all renderers prefer 32-bit // all renderers prefer 32-bit
// well, "simple" prefers 24-bit, but that's not available for filters // well, "simple" prefers 24-bit, but that's not available for filters
@ -1461,7 +1460,6 @@ DrawingPanel::DrawingPanel(wxWindow* parent, int _width, int _height)
, wxPanel(parent, wxID_ANY, wxPoint(0, 0), parent->GetClientSize(), , wxPanel(parent, wxID_ANY, wxPoint(0, 0), parent->GetClientSize(),
wxFULL_REPAINT_ON_RESIZE | wxWANTS_CHARS) wxFULL_REPAINT_ON_RESIZE | wxWANTS_CHARS)
{ {
this->SetClientObject(new widgets::UserInputEventSender(this));
} }
void DrawingPanelBase::DrawingPanelInit() void DrawingPanelBase::DrawingPanelInit()
@ -2193,7 +2191,6 @@ GLDrawingPanel::GLDrawingPanel(wxWindow* parent, int _width, int _height)
, wxglc(parent, wxID_ANY, glopts, wxPoint(0, 0), parent->GetClientSize(), , wxglc(parent, wxID_ANY, glopts, wxPoint(0, 0), parent->GetClientSize(),
wxFULL_REPAINT_ON_RESIZE | wxWANTS_CHARS) wxFULL_REPAINT_ON_RESIZE | wxWANTS_CHARS)
{ {
this->SetClientObject(new widgets::UserInputEventSender(this));
widgets::RequestHighResolutionOpenGlSurfaceForWindow(this); widgets::RequestHighResolutionOpenGlSurfaceForWindow(this);
SetContext(); SetContext();
} }

View File

@ -1,38 +1,11 @@
#include "wx/viewsupt.h" #include "wx/viewsupt.h"
#include "wx/config/option-proxy.h" #include "wx/config/option-proxy.h"
#include "wx/config/user-input.h"
#include "wx/wxvbam.h" #include "wx/wxvbam.h"
namespace Viewers { namespace Viewers {
namespace {
int getKeyboardKeyCode(const wxKeyEvent& event) {
const int key_code = event.GetKeyCode();
if (key_code > WXK_START) {
return key_code;
}
int uc = event.GetUnicodeKey();
if (uc != WXK_NONE) {
if (uc < 32) { // not all control chars
switch (uc) {
case WXK_BACK:
case WXK_TAB:
case WXK_RETURN:
case WXK_ESCAPE:
return uc;
default:
return WXK_NONE;
}
}
return uc;
} else {
return event.GetKeyCode();
}
}
} // namespace
void Viewer::CloseDlg(wxCloseEvent& ev) void Viewer::CloseDlg(wxCloseEvent& ev)
{ {
(void)ev; // unused params (void)ev; // unused params
@ -339,12 +312,11 @@ void MemView::Refit()
NULL, this); NULL, this);
disp.Connect(wxEVT_LEFT_UP, wxMouseEventHandler(MemView::MouseEvent), disp.Connect(wxEVT_LEFT_UP, wxMouseEventHandler(MemView::MouseEvent),
NULL, this); NULL, this);
disp.Connect(wxEVT_CHAR, wxKeyEventHandler(MemView::KeyEvent),
NULL, this);
disp.SetBackgroundColour(wxSystemSettings::GetColour(wxSYS_COLOUR_LISTBOX)); disp.SetBackgroundColour(wxSystemSettings::GetColour(wxSYS_COLOUR_LISTBOX));
sb.Create(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, sb.Create(this, wxID_ANY, wxDefaultPosition, wxDefaultSize,
wxSB_VERTICAL); wxSB_VERTICAL);
sb.SetScrollbar(0, 15, 500, 15); sb.SetScrollbar(0, 15, 500, 15);
disp.Bind(VBAM_EVT_USER_INPUT, &MemView::KeyEvent, this);
} }
wxClientDC dc(&disp); wxClientDC dc(&disp);
@ -443,17 +415,28 @@ void MemView::ShowCaret()
disp.SetFocus(); disp.SetFocus();
} }
void MemView::KeyEvent(wxKeyEvent& ev) void MemView::KeyEvent(widgets::UserInputEvent& ev)
{ {
uint32_t k = getKeyboardKeyCode(ev); const nonstd::optional<config::UserInput> opt_user_input = ev.FirstReleasedInput();
int nnib = 2 << fmt; if (opt_user_input == nonstd::nullopt) {
return;
}
switch (k) { const config::UserInput& user_input = opt_user_input.value();
if (!user_input.is_keyboard()) {
return;
}
const wxKeyCode key = user_input.keyboard_input().key();
const wxKeyModifier mod = user_input.keyboard_input().mod();
int nnib = 2 << fmt;
switch (key) {
case WXK_RIGHT: case WXK_RIGHT:
case WXK_NUMPAD_RIGHT: case WXK_NUMPAD_RIGHT:
if (isasc) if (isasc)
selnib += 2; selnib += 2;
else if (ev.GetModifiers() == wxMOD_SHIFT) else if (mod == wxMOD_SHIFT)
selnib += 2 << fmt; selnib += 2 << fmt;
else if (!(selnib % nnib)) else if (!(selnib % nnib))
selnib += nnib + nnib - 1; selnib += nnib + nnib - 1;
@ -475,7 +458,7 @@ void MemView::KeyEvent(wxKeyEvent& ev)
case WXK_NUMPAD_LEFT: case WXK_NUMPAD_LEFT:
if (isasc) if (isasc)
selnib -= 2; selnib -= 2;
else if (ev.GetModifiers() == wxMOD_SHIFT) else if (mod == wxMOD_SHIFT)
selnib -= 2 << fmt; selnib -= 2 << fmt;
else if (!(++selnib % nnib)) else if (!(++selnib % nnib))
selnib -= nnib * 2; selnib -= nnib * 2;
@ -506,7 +489,7 @@ void MemView::KeyEvent(wxKeyEvent& ev)
break; break;
default: default:
if (k > 0x7f || (isasc && !isprint(k)) || (!isasc && !isxdigit(k))) { if (key > 0x7f || (isasc && !isprint(key)) || (!isasc && !isxdigit(key))) {
ev.Skip(); ev.Skip();
return; return;
} }
@ -537,10 +520,10 @@ void MemView::KeyEvent(wxKeyEvent& ev)
if (isasc) { if (isasc) {
mask = 0xff << bno * 8; mask = 0xff << bno * 8;
val = k << bno * 8; val = key << bno * 8;
} else { } else {
mask = 8 * (0xf << bno) + 4 * nibno; mask = 8 * (0xf << bno) + 4 * nibno;
val = isdigit(k) ? k - '0' : tolower(k) + 10 - 'a'; val = isdigit(key) ? key - '0' : tolower(key) + 10 - 'a';
val <<= bno * 8 + nibno * 4; val <<= bno * 8 + nibno * 4;
} }

View File

@ -1,6 +1,8 @@
#ifndef VBAM_WX_VIEWSUPT_H_ #ifndef VBAM_WX_VIEWSUPT_H_
#define VBAM_WX_VIEWSUPT_H_ #define VBAM_WX_VIEWSUPT_H_
#include <cstdint>
#include <wx/wx.h> #include <wx/wx.h>
#include <wx/window.h> #include <wx/window.h>
#include <wx/image.h> #include <wx/image.h>
@ -14,7 +16,7 @@
#include <wx/stattext.h> #include <wx/stattext.h>
#include <wx/checkbox.h> #include <wx/checkbox.h>
#include <stdint.h> // for uint32_t #include "wx/widgets/user-input-event.h"
// avoid exporting too much stuff // avoid exporting too much stuff
namespace Viewers { namespace Viewers {
@ -218,7 +220,7 @@ protected:
int addrlen; int addrlen;
void MouseEvent(wxMouseEvent& ev); void MouseEvent(wxMouseEvent& ev);
void KeyEvent(wxKeyEvent& ev); void KeyEvent(widgets::UserInputEvent& ev);
// the subwidgets // the subwidgets
wxPanel disp; wxPanel disp;
wxScrollBar sb; wxScrollBar sb;

View File

@ -0,0 +1,21 @@
#ifndef VBAM_WX_WIDGETS_EVENT_HANDLER_PROVIDER_H_
#define VBAM_WX_WIDGETS_EVENT_HANDLER_PROVIDER_H_
// Forward declaration.
class wxEvtHandler;
namespace widgets {
// Abstract interface to provide the currently active event handler to use for
// user input events.
class EventHandlerProvider {
public:
virtual ~EventHandlerProvider() = default;
// Returns the currently active event handler to use for user input events.
virtual wxEvtHandler* event_handler() = 0;
};
} // namespace widgets
#endif // VBAM_WX_WIDGETS_EVENT_HANDLER_PROVIDER_H_

View File

@ -300,12 +300,14 @@ void JoyState::Notify() {
SetRumble(rumbling_); SetRumble(rumbling_);
} }
SdlPoller::SdlPoller(const EventHandlerProvider handler_provider) SdlPoller::SdlPoller(EventHandlerProvider* const handler_provider)
: enable_game_controller_(OPTION(kSDLGameControllerMode)), : enable_game_controller_(OPTION(kSDLGameControllerMode)),
handler_provider_(handler_provider), handler_provider_(handler_provider),
game_controller_enabled_observer_( game_controller_enabled_observer_(
config::OptionID::kSDLGameControllerMode, config::OptionID::kSDLGameControllerMode,
[this](config::Option* option) { ReconnectControllers(option->GetBool()); }) { [this](config::Option* option) { ReconnectControllers(option->GetBool()); }) {
assert(handler_provider);
wxTimer::Start(50); wxTimer::Start(50);
SDL_Init(SDL_INIT_JOYSTICK | SDL_INIT_GAMECONTROLLER | SDL_INIT_EVENTS); SDL_Init(SDL_INIT_JOYSTICK | SDL_INIT_GAMECONTROLLER | SDL_INIT_EVENTS);
SDL_GameControllerEventState(SDL_ENABLE); SDL_GameControllerEventState(SDL_ENABLE);
@ -406,7 +408,7 @@ void SdlPoller::Notify() {
} }
if (!event_data.empty()) { if (!event_data.empty()) {
wxEvtHandler* handler = handler_provider_(); wxEvtHandler* handler = handler_provider_->event_handler();
if (handler) { if (handler) {
handler->QueueEvent(new UserInputEvent(std::move(event_data))); handler->QueueEvent(new UserInputEvent(std::move(event_data)));
} }

View File

@ -1,7 +1,6 @@
#ifndef WX_WIDGETS_SDL_POLLER_H_ #ifndef WX_WIDGETS_SDL_POLLER_H_
#define WX_WIDGETS_SDL_POLLER_H_ #define WX_WIDGETS_SDL_POLLER_H_
#include <functional>
#include <map> #include <map>
#include <wx/timer.h> #include <wx/timer.h>
@ -9,6 +8,7 @@
#include <SDL.h> #include <SDL.h>
#include "wx/config/option-observer.h" #include "wx/config/option-observer.h"
#include "wx/widgets/event-handler-provider.h"
// Forward declarations. // Forward declarations.
class wxEvtHandler; class wxEvtHandler;
@ -18,16 +18,13 @@ namespace widgets {
// Forward declaration. // Forward declaration.
class JoyState; class JoyState;
// Provider for the event handler to send the events to.
using EventHandlerProvider = std::function<wxEvtHandler*()>;
// The SDL worker is responsible for handling SDL events and firing the // The SDL worker is responsible for handling SDL events and firing the
// appropriate `UserInputEvent` for joysticks. // appropriate `UserInputEvent` for joysticks.
// It is used to fire `UserInputEvent` for joysticks. This class should be kept // It is used to fire `UserInputEvent` for joysticks. This class should be kept
// as a singleton owned by the application object. // as a singleton owned by the application object.
class SdlPoller final : public wxTimer { class SdlPoller final : public wxTimer {
public: public:
explicit SdlPoller(const EventHandlerProvider handler_provider); explicit SdlPoller(EventHandlerProvider* const handler_provider);
~SdlPoller() final; ~SdlPoller() final;
// Disable copy and copy assignment. // Disable copy and copy assignment.
@ -59,7 +56,7 @@ private:
bool enable_game_controller_ = false; bool enable_game_controller_ = false;
// The provider of event handlers to send the events to. // The provider of event handlers to send the events to.
const EventHandlerProvider handler_provider_; EventHandlerProvider* const handler_provider_;
// Observer for the game controller enabled option. // Observer for the game controller enabled option.
const config::OptionsObserver game_controller_enabled_observer_; const config::OptionsObserver game_controller_enabled_observer_;

View File

@ -0,0 +1,14 @@
#include "wx/widgets/shortcut-menu-bar.h"
#include <wx/accel.h>
namespace widgets {
void ShortcutMenuBar::SetAcceleratorTable(const wxAcceleratorTable& /*accel*/) {
// Do nothing. We don't want to set up accelerators on the menu bar.
wxMenuBar::SetAcceleratorTable(wxNullAcceleratorTable);
}
wxIMPLEMENT_DYNAMIC_CLASS(ShortcutMenuBar, wxMenuBar);
} // namespace widgets

View File

@ -0,0 +1,23 @@
#ifndef VBAM_WIDGETS_SHORTCUT_MENU_BAR_H_
#define VBAM_WIDGETS_SHORTCUT_MENU_BAR_H_
#include <wx/menu.h>
namespace widgets {
// A menu bar with no accelerator table. This is used to prevent the menu bar
// from stealing keyboard shortcuts from the main window.
class ShortcutMenuBar : public wxMenuBar {
public:
ShortcutMenuBar() = default;
~ShortcutMenuBar() override = default;
// wxMenuBar implementation.
void SetAcceleratorTable(const wxAcceleratorTable& accel) override;
wxDECLARE_DYNAMIC_CLASS(ShortcutMenuBar);
};
} // namespace widgets
#endif // VBAM_WIDGETS_SHORTCUT_MENU_BAR_H_

View File

@ -30,7 +30,6 @@ bool UserInputCtrl::Create(wxWindow* parent,
const wxSize& size, const wxSize& size,
long style, long style,
const wxString& name) { const wxString& name) {
this->SetClientObject(new UserInputEventSender(this));
this->Bind(VBAM_EVT_USER_INPUT, &UserInputCtrl::OnUserInput, this); this->Bind(VBAM_EVT_USER_INPUT, &UserInputCtrl::OnUserInput, this);
this->Bind(wxEVT_SET_FOCUS, [this](wxFocusEvent& event) { this->Bind(wxEVT_SET_FOCUS, [this](wxFocusEvent& event) {
last_focus_time_ = wxGetUTCTimeMillis(); last_focus_time_ = wxGetUTCTimeMillis();

View File

@ -151,20 +151,27 @@ wxEvent* UserInputEvent::Clone() const {
return new UserInputEvent(this->data_); return new UserInputEvent(this->data_);
} }
UserInputEventSender::UserInputEventSender(wxWindow* const window) KeyboardInputSender::KeyboardInputSender(EventHandlerProvider* const handler_provider)
: window_(window) { : handler_provider_(handler_provider) {
assert(window); assert(handler_provider_);
// Attach the event handlers.
window_->Bind(wxEVT_KEY_DOWN, &UserInputEventSender::OnKeyDown, this);
window_->Bind(wxEVT_KEY_UP, &UserInputEventSender::OnKeyUp, this);
window_->Bind(wxEVT_SET_FOCUS, &UserInputEventSender::Reset, this, window_->GetId());
window_->Bind(wxEVT_KILL_FOCUS, &UserInputEventSender::Reset, this, window_->GetId());
} }
UserInputEventSender::~UserInputEventSender() = default; KeyboardInputSender::~KeyboardInputSender() = default;
void UserInputEventSender::OnKeyDown(wxKeyEvent& event) { 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. // Stop propagation of the event.
event.Skip(false); event.Skip(false);
@ -214,10 +221,10 @@ void UserInputEventSender::OnKeyDown(wxKeyEvent& event) {
} }
} }
wxQueueEvent(window_, new UserInputEvent(std::move(event_data))); wxQueueEvent(handler_provider_->event_handler(), new UserInputEvent(std::move(event_data)));
} }
void UserInputEventSender::OnKeyUp(wxKeyEvent& event) { void KeyboardInputSender::OnKeyUp(wxKeyEvent& event) {
// Stop propagation of the event. // Stop propagation of the event.
event.Skip(false); event.Skip(false);
@ -297,17 +304,8 @@ void UserInputEventSender::OnKeyUp(wxKeyEvent& event) {
for (const auto& data : event_data) { for (const auto& data : event_data) {
active_mod_inputs_.erase(data.input.keyboard_input()); active_mod_inputs_.erase(data.input.keyboard_input());
} }
wxQueueEvent(window_, new UserInputEvent(std::move(event_data)));
}
void UserInputEventSender::Reset(wxFocusEvent& event) { wxQueueEvent(handler_provider_->event_handler(), new UserInputEvent(std::move(event_data)));
// Reset the internal state.
active_keys_.clear();
active_mods_.clear();
active_mod_inputs_.clear();
// Let the event propagate.
event.Skip();
} }
} // namespace widgets } // namespace widgets

View File

@ -10,6 +10,7 @@
#include <wx/event.h> #include <wx/event.h>
#include "wx/config/user-input.h" #include "wx/config/user-input.h"
#include "wx/widgets/event-handler-provider.h"
namespace widgets { namespace widgets {
@ -53,34 +54,32 @@ private:
}; };
// Object that is used to fire user input events when a key is pressed or // Object that is used to fire user input events when a key is pressed or
// released. To use this object, attach it to a wxWindow and listen for // released. This class should be kept as a singleton owned by the application
// the VBAM_EVT_USER_INPUT_DOWN and VBAM_EVT_USER_INPUT_UP events. // object. It is meant to be used in the FilterEvent() method of the app.
class UserInputEventSender final : public wxClientData { class KeyboardInputSender final : public wxClientData {
public: public:
// `window` must not be nullptr. Will assert otherwise. explicit KeyboardInputSender(EventHandlerProvider* const handler_provider);
// `window` must outlive this object. ~KeyboardInputSender() override;
explicit UserInputEventSender(wxWindow* const window);
~UserInputEventSender() override;
// Disable copy and copy assignment. // Disable copy and copy assignment.
UserInputEventSender(const UserInputEventSender&) = delete; KeyboardInputSender(const KeyboardInputSender&) = delete;
UserInputEventSender& operator=(const UserInputEventSender&) = 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: private:
// Keyboard event handlers. // Keyboard event handlers.
void OnKeyDown(wxKeyEvent& event); void OnKeyDown(wxKeyEvent& event);
void OnKeyUp(wxKeyEvent& event); void OnKeyUp(wxKeyEvent& event);
// Resets the internal state. Called on focus in/out.
void Reset(wxFocusEvent& event);
std::unordered_set<wxKeyCode> active_keys_; std::unordered_set<wxKeyCode> active_keys_;
std::unordered_set<wxKeyModifier> active_mods_; std::unordered_set<wxKeyModifier> active_mods_;
std::unordered_set<config::KeyboardInput> active_mod_inputs_; std::unordered_set<config::KeyboardInput> active_mod_inputs_;
// The wxWindow this object is attached to. // The provider of event handlers to send the events to.
// Must outlive this object. EventHandlerProvider* const handler_provider_;
wxWindow* const window_;
}; };
} // namespace widgets } // namespace widgets

View File

@ -64,11 +64,6 @@ void ResetMenuItemAccelerator(wxMenuItem* menu_item) {
wxGetApp().bindings()->InputsForCommand( wxGetApp().bindings()->InputsForCommand(
config::ShortcutCommand(menu_item->GetId())); config::ShortcutCommand(menu_item->GetId()));
for (const config::UserInput& user_input : user_inputs) { for (const config::UserInput& user_input : user_inputs) {
if (user_input.device() != config::UserInput::Device::Keyboard) {
// Cannot use joystick keybinding as text without wx assertion error.
continue;
}
new_label.append('\t'); new_label.append('\t');
new_label.append(user_input.ToLocalizedString()); new_label.append(user_input.ToLocalizedString());
break; break;
@ -252,7 +247,8 @@ wxvbamApp::wxvbamApp()
frame(nullptr), frame(nullptr),
using_wayland(false), using_wayland(false),
emulated_gamepad_(std::bind(&wxvbamApp::bindings, this)), emulated_gamepad_(std::bind(&wxvbamApp::bindings, this)),
sdl_poller_(std::bind(&wxvbamApp::GetJoyEventHandler, this)) {} sdl_poller_(this),
keyboard_input_sender_(this) {}
const wxString wxvbamApp::GetPluginsDir() const wxString wxvbamApp::GetPluginsDir()
{ {
@ -826,7 +822,7 @@ wxvbamApp::~wxvbamApp() {
#endif #endif
} }
wxEvtHandler* wxvbamApp::GetJoyEventHandler() { wxEvtHandler* wxvbamApp::event_handler() {
// Use the active window, if any. // Use the active window, if any.
wxWindow* focused_window = wxWindow::FindFocus(); wxWindow* focused_window = wxWindow::FindFocus();
if (focused_window) { if (focused_window) {
@ -842,7 +838,7 @@ wxEvtHandler* wxvbamApp::GetJoyEventHandler() {
return nullptr; return nullptr;
} }
if (OPTION(kUIAllowJoystickBackgroundInput)) { if (OPTION(kUIAllowJoystickBackgroundInput) || OPTION(kUIAllowKeyboardBackgroundInput)) {
// Use the game panel, if the background polling option is enabled. // Use the game panel, if the background polling option is enabled.
return panel->panel->GetWindow()->GetEventHandler(); return panel->panel->GetWindow()->GetEventHandler();
} }
@ -990,53 +986,6 @@ void MainFrame::OnSize(wxSizeEvent& event)
OPTION(kGeomFullScreen) = IsFullScreen(); OPTION(kGeomFullScreen) = IsFullScreen();
} }
int MainFrame::FilterEvent(wxEvent& event) {
if (menus_opened || dialog_opened) {
return wxEventFilter::Event_Skip;
}
if (event.GetEventType() != VBAM_EVT_USER_INPUT) {
// We only treat "VBAM_EVT_USER_INPUT" events here.
return wxEventFilter::Event_Skip;
}
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;
}
// Execute the associated shortcut command.
wxCommandEvent command_event(wxEVT_COMMAND_MENU_SELECTED, command_id);
command_event.SetEventObject(this);
this->GetEventHandler()->ProcessEvent(command_event);
// 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) wxString MainFrame::GetGamePath(wxString path)
{ {
wxString game_path = path; wxString game_path = path;
@ -1376,15 +1325,57 @@ void MainFrame::IdentifyRom()
} }
} }
// global event filter
// apparently required for win32; just setting accel table still misses
// a few keys (e.g. only ctrl-x works for exit, but not esc & ctrl-q;
// ctrl-w does not work for close). It's possible another entity is
// grabbing those keys, but I can't track it down.
int wxvbamApp::FilterEvent(wxEvent& event) int wxvbamApp::FilterEvent(wxEvent& event)
{ {
if (frame) { if (!frame) {
return frame->FilterEvent(event); // Ignore early events.
return wxEventFilter::Event_Skip;
} }
return wxApp::FilterEvent(event);
if (event.GetEventType() == wxEVT_KEY_DOWN || event.GetEventType() == wxEVT_KEY_UP) {
// Handle keyboard input events here. No control will receive them.
keyboard_input_sender_.ProcessKeyEvent(static_cast<wxKeyEvent&>(event));
return wxEventFilter::Event_Processed;
}
if (!frame->CanProcessShortcuts()) {
return wxEventFilter::Event_Skip;
}
if (event.GetEventType() != VBAM_EVT_USER_INPUT) {
// We only treat "VBAM_EVT_USER_INPUT" events here.
return wxEventFilter::Event_Skip;
}
widgets::UserInputEvent& user_input_event = static_cast<widgets::UserInputEvent&>(event);
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;
}
// Queue the associated shortcut command.
wxCommandEvent* command_event = new wxCommandEvent(wxEVT_COMMAND_MENU_SELECTED, command_id);
command_event->SetEventObject(this);
frame->GetEventHandler()->QueueEvent(command_event);
return user_input_event.FilterProcessedInput(user_input.value());
} }

View File

@ -18,6 +18,7 @@
#include "wx/config/option.h" #include "wx/config/option.h"
#include "wx/dialogs/base-dialog.h" #include "wx/dialogs/base-dialog.h"
#include "wx/widgets/dpi-support.h" #include "wx/widgets/dpi-support.h"
#include "wx/widgets/event-handler-provider.h"
#include "wx/widgets/keep-on-top-styler.h" #include "wx/widgets/keep-on-top-styler.h"
#include "wx/widgets/sdl-poller.h" #include "wx/widgets/sdl-poller.h"
#include "wx/widgets/user-input-event.h" #include "wx/widgets/user-input-event.h"
@ -59,7 +60,7 @@ inline std::string ToString(const wxChar* aString)
class MainFrame; class MainFrame;
class wxvbamApp final : public wxApp { class wxvbamApp final : public wxApp, public widgets::EventHandlerProvider {
public: public:
wxvbamApp(); wxvbamApp();
@ -133,8 +134,8 @@ protected:
int console_status = 0; int console_status = 0;
private: private:
// Returns the currently active event handler to use for user input events. // EventHandlerProvider implementation.
wxEvtHandler* GetJoyEventHandler(); wxEvtHandler* event_handler() override;
config::Bindings bindings_; config::Bindings bindings_;
config::EmulatedGamepad emulated_gamepad_; config::EmulatedGamepad emulated_gamepad_;
@ -143,6 +144,7 @@ private:
char* home = nullptr; char* home = nullptr;
widgets::SdlPoller sdl_poller_; widgets::SdlPoller sdl_poller_;
widgets::KeyboardInputSender keyboard_input_sender_;
// Main configuration file. // Main configuration file.
wxFileName config_file_; wxFileName config_file_;
@ -200,8 +202,6 @@ public:
void GetMenuOptionBool(const wxString& menuName, bool* field); void GetMenuOptionBool(const wxString& menuName, bool* field);
void SetMenuOption(const wxString& menuName, bool value); void SetMenuOption(const wxString& menuName, bool value);
int FilterEvent(wxEvent& event);
GameArea* GetPanel() GameArea* GetPanel()
{ {
return panel; return panel;
@ -209,6 +209,8 @@ public:
wxString GetGamePath(wxString path); wxString GetGamePath(wxString path);
bool CanProcessShortcuts() const { return !menus_opened && !dialog_opened; }
// wxMSW pauses the game for menu popups and modal dialogs, but wxGTK // wxMSW pauses the game for menu popups and modal dialogs, but wxGTK
// does not. It's probably desirable to pause the game. To do this for // does not. It's probably desirable to pause the game. To do this for
// dialogs, use this function instead of dlg->ShowModal() // dialogs, use this function instead of dlg->ShowModal()

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes" ?> <?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
<resource xmlns="http://www.wxwidgets.org/wxxrc" version="2.5.3.0"> <resource xmlns="http://www.wxwidgets.org/wxxrc" version="2.5.3.0">
<object class="wxMenuBar" name="MainMenu"> <object class="wxMenuBar" name="MainMenu" subclass="ShortcutMenuBar">
<object class="wxMenu"> <object class="wxMenu">
<label>_File</label> <label>_File</label>
<object class="wxMenuItem" name="wxID_OPEN"> <object class="wxMenuItem" name="wxID_OPEN">