[Input] Move input configuration objects to app

Previously, some input-related configuration objects were either owned
by `gopts` or global values. This moves these objects to be owned by the
app object instead.

Rather than directly accessing the app object, other objects (like
dialogs) that need to access the input-related configuration objects are
passed a `ConfigurationObjectProvider` function. This will make it
easier to test these objects independently down the line.

Bug: #745
This commit is contained in:
Fabrice de Gans 2024-04-23 14:46:45 -07:00 committed by Fabrice de Gans
parent 32ca2ae42f
commit cfdbdc4ec2
15 changed files with 115 additions and 88 deletions

View File

@ -1,6 +1,5 @@
#include "wx/config/game-control.h"
#include "wx/opts.h"
#include "wx/strutils.h"
#include "wx/wxlogdebug.h"
@ -195,13 +194,8 @@ bool GameControl::operator>=(const GameControl& other) const {
return !(*this < other);
}
GameControlState& GameControlState::Instance() {
static GameControlState g_game_control_state;
return g_game_control_state;
}
GameControlState::GameControlState() : joypads_({0, 0, 0, 0}) {}
GameControlState::~GameControlState() = default;
GameControlState::GameControlState(const GameControlBindingsProvider bindings_provider)
: joypads_({0, 0, 0, 0}), bindings_provider_(bindings_provider) {}
bool GameControlState::OnInputPressed(const config::UserInput& user_input) {
assert(user_input);
@ -282,7 +276,7 @@ void GameControlState::OnGameBindingsChanged() {
Reset();
input_bindings_.clear();
for (const auto& iter : gopts.game_control_bindings) {
for (const auto& iter : *bindings_provider_()) {
for (const auto& user_input : iter.second) {
input_bindings_[user_input].emplace(iter.first);
}

View File

@ -5,6 +5,7 @@
#include <array>
#include <map>
#include <set>
#include <unordered_set>
#include <optional.hpp>
@ -107,12 +108,16 @@ private:
friend class GameControlState;
};
using GameControlBindings = std::map<GameControl, std::unordered_set<UserInput>>;
using GameControlBindingsProvider = std::function<GameControlBindings*()>;
// Tracks in-game input and computes the joypad value used to send control input
// data to the emulator.
class GameControlState {
// data to the emulator. This class should be kept as a singleton owned by the
// application.
class GameControlState final {
public:
// This is a global singleton.
static GameControlState& Instance();
explicit GameControlState(const GameControlBindingsProvider bindings_provider);
~GameControlState() = default;
// Disable copy constructor and assignment operator.
GameControlState(const GameControlState&) = delete;
@ -120,8 +125,8 @@ public:
// Processes `user_input` and updates the internal tracking state.
// Returns true if `user_input` corresponds to a game input.
bool OnInputPressed(const config::UserInput& user_input);
bool OnInputReleased(const config::UserInput& user_input);
bool OnInputPressed(const UserInput& user_input);
bool OnInputReleased(const UserInput& user_input);
// Clears all input.
void Reset();
@ -133,15 +138,16 @@ public:
uint32_t GetJoypad(int joypad) const;
private:
GameControlState();
~GameControlState();
std::map<config::UserInput, std::set<GameControl>> input_bindings_;
std::map<GameControl, std::set<config::UserInput>> active_controls_;
std::set<config::UserInput> keys_pressed_;
std::map<GameControl, std::unordered_set<UserInput>> active_controls_;
std::unordered_set<config::UserInput> keys_pressed_;
std::array<uint32_t, kNbJoypads> joypads_;
const GameControlBindingsProvider bindings_provider_;
};
using GameControlStateProvider = std::function<GameControlState*()>;
} // namespace config
#endif // VBAM_WX_CONFIG_GAME_CONTROL_H_

View File

@ -6,6 +6,7 @@
#include <algorithm>
#include <limits>
#include <map>
#include <wx/log.h>
#include <wx/translation.h>

View File

@ -3,6 +3,7 @@
#include <wx/string.h>
#include <wx/translation.h>
#include <wx/xrc/xmlres.h>
#include <unordered_set>
#include "wx/config/user-input.h"
@ -28,14 +29,14 @@ Shortcuts::Shortcuts() {
}
}
Shortcuts::Shortcuts(const std::unordered_map<int, std::set<UserInput>>& command_to_inputs,
const std::map<UserInput, int>& input_to_command,
const std::map<UserInput, int>& disabled_defaults)
Shortcuts::Shortcuts(const std::unordered_map<int, std::unordered_set<UserInput>>& command_to_inputs,
const std::unordered_map<UserInput, int>& input_to_command,
const std::unordered_map<UserInput, int>& disabled_defaults)
: command_to_inputs_(command_to_inputs.begin(), command_to_inputs.end()),
input_to_command_(input_to_command.begin(), input_to_command.end()),
disabled_defaults_(disabled_defaults.begin(), disabled_defaults.end()) {}
std::vector<std::pair<int, wxString>> Shortcuts::GetConfiguration() const {
std::vector<std::pair<int, wxString>> Shortcuts::GetKeyboardConfiguration() const {
std::vector<std::pair<int, wxString>> config;
config.reserve(command_to_inputs_.size() + 1);
@ -63,9 +64,9 @@ std::vector<std::pair<int, wxString>> Shortcuts::GetConfiguration() const {
return config;
}
std::set<UserInput> Shortcuts::InputsForCommand(int command) const {
std::unordered_set<UserInput> Shortcuts::InputsForCommand(int command) const {
if (command == NoopCommand()) {
std::set<UserInput> noop_inputs;
std::unordered_set<UserInput> noop_inputs;
for (const auto& iter : disabled_defaults_) {
noop_inputs.insert(iter.first);
}

View File

@ -1,11 +1,10 @@
#ifndef VBAM_WX_CONFIG_SHORTCUTS_H_
#define VBAM_WX_CONFIG_SHORTCUTS_H_
#include <map>
#include <set>
#include <vector>
#include <unordered_map>
#include <unordered_set>
#include <utility>
#include <vector>
#include "wx/config/user-input.h"
@ -27,20 +26,23 @@ public:
// Disable copy and copy assignment operator.
// Clone() is provided only for the configuration window, this class
// should otherwise be treated as move-only.
// should otherwise be treated as move-only. If you wish to access the
// Shortcuts configuration, do it from wxGetApp()->GameControlState().
Shortcuts(const Shortcuts&) = delete;
Shortcuts& operator=(const Shortcuts&) = delete;
// Returns the configuration for the INI file.
// Returns the shortcuts configuration for the INI file.
// Internally, there are global default system inputs that are immediately
// available on first run. For the configuration saved in the [Keyboard]
// section of the vbam.ini file, we only keep track of the following:
// - Disabled default input. These appear under [Keyboard/NOOP].
// - User-added custom bindings. These appear under [Keyboard/CommandName].
std::vector<std::pair<int, wxString>> GetConfiguration() const;
// Essentially, this is a diff between the default shortcuts and the user
// configuration.
std::vector<std::pair<int, wxString>> GetKeyboardConfiguration() const;
// Returns the list of input currently configured for `command`.
std::set<UserInput> InputsForCommand(int command) const;
std::unordered_set<UserInput> InputsForCommand(int command) const;
// Returns the command currently assigned to `input` or nullptr if none.
int CommandForInput(const UserInput& input) const;
@ -52,17 +54,17 @@ public:
// Assigns `input` to `command`. Silently unassigns `input` if it is already
// assigned to another command.
void AssignInputToCommand(const UserInput& input, int command);
void AssignInputToCommand(const UserInput& input, const int command);
// Removes `input` assignment. No-op if `input` is not assigned. `input`
// must be a valid UserInput.
// must be a valid UserInput. Call will assert otherwise.
void UnassignInput(const UserInput& input);
private:
// Faster constructor for explitit copy.
Shortcuts(const std::unordered_map<int, std::set<UserInput>>& command_to_inputs,
const std::map<UserInput, int>& input_to_command,
const std::map<UserInput, int>& disabled_defaults);
// Faster constructor for explicit copy.
Shortcuts(const std::unordered_map<int, std::unordered_set<UserInput>>& command_to_inputs,
const std::unordered_map<UserInput, int>& input_to_command,
const std::unordered_map<UserInput, int>& disabled_defaults);
// Helper method to unassign a binding used by the default configuration.
// This requires special handling since the INI configuration is a diff
@ -70,14 +72,16 @@ private:
void UnassignDefaultBinding(const UserInput& input);
// Map of command to their associated input set.
std::unordered_map<int, std::set<UserInput>> command_to_inputs_;
std::unordered_map<int, std::unordered_set<UserInput>> command_to_inputs_;
// Reverse map of the above. An input can only map to a single command.
std::map<UserInput, int> input_to_command_;
// Disabled default inputs. This is used to easily retrieve the
std::unordered_map<UserInput, int> input_to_command_;
// Disabled default shortcuts. This is used to easily retrieve the
// configuration to save in the INI file.
std::map<UserInput, int> disabled_defaults_;
std::unordered_map<UserInput, int> disabled_defaults_;
};
using ShortcutsProvider = std::function<Shortcuts*()>;
} // namespace config
#endif // VBAM_WX_CONFIG_SHORTCUTS_H_

View File

@ -122,15 +122,21 @@ void PopulateTreeWithMenu(std::unordered_map<int, wxTreeItemId>* command_to_item
} // namespace
// static
AccelConfig* AccelConfig::NewInstance(wxWindow* parent, wxMenuBar* menu, wxMenu* recents) {
AccelConfig* AccelConfig::NewInstance(wxWindow* parent,
wxMenuBar* menu,
wxMenu* recents,
const config::ShortcutsProvider shortcuts_provider) {
assert(parent);
assert(menu);
assert(recents);
return new AccelConfig(parent, menu, recents);
return new AccelConfig(parent, menu, recents, shortcuts_provider);
}
AccelConfig::AccelConfig(wxWindow* parent, wxMenuBar* menu, wxMenu* recents)
: BaseDialog(parent, "AccelConfig") {
AccelConfig::AccelConfig(wxWindow* parent,
wxMenuBar* menu,
wxMenu* recents,
const config::ShortcutsProvider shortcuts_provider)
: BaseDialog(parent, "AccelConfig"), shortcuts_provider_(shortcuts_provider) {
assert(menu);
// Loads the various dialog elements.
@ -213,11 +219,11 @@ void AccelConfig::OnDialogShown(wxShowEvent& ev) {
remove_button_->Enable(false);
currently_assigned_label_->SetLabel("");
config_shortcuts_ = gopts.shortcuts.Clone();
config_shortcuts_ = shortcuts_provider_()->Clone();
}
void AccelConfig::OnValidate(wxCommandEvent& ev) {
gopts.shortcuts = std::move(config_shortcuts_);
*shortcuts_provider_() = std::move(config_shortcuts_);
ev.Skip();
}

View File

@ -24,7 +24,10 @@ namespace dialogs {
// Manages the shortcuts editor dialog.
class AccelConfig : public BaseDialog {
public:
static AccelConfig* NewInstance(wxWindow* parent, wxMenuBar* menu_bar, wxMenu* recents);
static AccelConfig* NewInstance(wxWindow* parent,
wxMenuBar* menu_bar,
wxMenu* recents,
const config::ShortcutsProvider shortcuts_provider);
~AccelConfig() override = default;
@ -32,7 +35,10 @@ private:
// The constructor is private so initialization has to be done via the
// static method. This is because this class is destroyed when its
// owner, `parent` is destroyed. This prevents accidental deletion.
AccelConfig(wxWindow* parent, wxMenuBar* menu_bar, wxMenu* recents);
AccelConfig(wxWindow* parent,
wxMenuBar* menu_bar,
wxMenu* recents,
const config::ShortcutsProvider shortcuts_provider);
// Re-initializes the configuration.
void OnDialogShown(wxShowEvent& ev);
@ -73,6 +79,8 @@ private:
config::Shortcuts config_shortcuts_;
int selected_command_ = 0;
const config::ShortcutsProvider shortcuts_provider_;
};
} // namespace dialogs

View File

@ -2480,7 +2480,8 @@ bool MainFrame::BindControls()
d->Fit();
}
#endif
dialogs::AccelConfig::NewInstance(this, menubar, recent);
dialogs::AccelConfig::NewInstance(this, menubar, recent,
std::bind(&wxvbamApp::shortcuts, &wxGetApp()));
} catch (std::exception& e) {
wxLogError(wxString::FromUTF8(e.what()));
return false;

View File

@ -13,6 +13,7 @@
#include "wx/config/option-observer.h"
#include "wx/config/option-proxy.h"
#include "wx/config/option.h"
#include "wx/config/shortcuts.h"
#include "wx/config/user-input.h"
#include "wx/strutils.h"
#include "wx/wxvbam.h"
@ -107,7 +108,7 @@ uint32_t LoadUnsignedOption(wxConfigBase* cfg,
opts_t gopts;
const std::map<config::GameControl, std::unordered_set<config::UserInput>> kDefaultBindings = {
const config::GameControlBindings kDefaultBindings = {
{config::GameControl(0, config::GameKey::Up),
{
config::KeyboardInput('W'),
@ -469,10 +470,10 @@ void load_opts(bool first_time_launch) {
}
// Initialize game control bindings to populate the configuration map.
gopts.game_control_bindings.insert(kDefaultBindings.begin(), kDefaultBindings.end());
wxGetApp().game_control_bindings()->insert(kDefaultBindings.begin(), kDefaultBindings.end());
// joypad is special
for (auto& iter : gopts.game_control_bindings) {
for (auto& iter : *wxGetApp().game_control_bindings()) {
const wxString optname = iter.first.ToString();
if (cfg->Read(optname, &s)) {
iter.second = config::UserInput::FromConfigString(s);
@ -487,8 +488,9 @@ void load_opts(bool first_time_launch) {
// keyboard is special
// Keyboard does not get written with defaults
wxString kbopt(wxT("Keyboard/"));
wxString kbopt("Keyboard/");
int kboff = kbopt.size();
config::Shortcuts* shortcuts = wxGetApp().shortcuts();
for (int i = 0; i < ncmds; i++) {
kbopt.resize(kboff);
@ -500,7 +502,7 @@ void load_opts(bool first_time_launch) {
wxLogWarning(_("Invalid key binding %s for %s"), s.c_str(), kbopt.c_str());
} else {
for (const auto& input : inputs) {
gopts.shortcuts.AssignInputToCommand(input, cmdtab[i].cmd_id);
shortcuts->AssignInputToCommand(input, cmdtab[i].cmd_id);
}
}
}
@ -564,7 +566,7 @@ void update_joypad_opts() {
// For joypad, compare the UserInput sets.
bool game_bindings_changed = false;
for (const auto &iter : gopts.game_control_bindings) {
for (const auto& iter : *wxvbamApp().game_control_bindings()) {
wxString option_name = iter.first.ToString();
std::unordered_set<config::UserInput> saved_config =
config::UserInput::FromConfigString(cfg->Read(option_name, ""));
@ -575,7 +577,7 @@ void update_joypad_opts() {
}
if (game_bindings_changed) {
config::GameControlState::Instance().OnGameBindingsChanged();
wxvbamApp().game_control_state()->OnGameBindingsChanged();
}
cfg->SetPath("/");
@ -588,7 +590,7 @@ void update_shortcut_opts() {
// For shortcuts, it's easier to delete everything and start over.
cfg->DeleteGroup("/Keyboard");
cfg->SetPath("/Keyboard");
for (const auto& iter : gopts.shortcuts.GetConfiguration()) {
for (const auto& iter : wxGetApp().shortcuts()->GetKeyboardConfiguration()) {
int cmd = 0;
for (cmd = 0; cmd < ncmds; cmd++)
if (cmdtab[cmd].cmd_id == iter.first)
@ -685,7 +687,7 @@ void opt_set(const wxString& name, const wxString& val) {
wxLogWarning(_("Invalid key binding %s for %s"), val.c_str(), name.c_str());
}
for (const auto& input : inputs) {
gopts.shortcuts.AssignInputToCommand(input, cmd->cmd_id);
wxGetApp().shortcuts()->AssignInputToCommand(input, cmd->cmd_id);
}
}
@ -694,11 +696,12 @@ void opt_set(const wxString& name, const wxString& val) {
const nonstd::optional<config::GameControl> game_control =
config::GameControl::FromString(name);
auto game_control_bindings = wxGetApp().game_control_bindings();
if (game_control) {
if (val.empty()) {
gopts.game_control_bindings[game_control.value()].clear();
(*game_control_bindings)[game_control.value()].clear();
} else {
gopts.game_control_bindings[game_control.value()] =
(*game_control_bindings)[game_control.value()] =
config::UserInput::FromConfigString(val);
}
return;

View File

@ -1,21 +1,18 @@
#ifndef VBAM_WX_OPTS_H_
#define VBAM_WX_OPTS_H_
#include <map>
#include <cstdint>
#include <wx/string.h>
#include <wx/vidmode.h>
#include "wx/config/game-control.h"
#include "wx/config/shortcuts.h"
#include "wx/config/user-input.h"
// Forward declaration.
class wxFileHistory;
// Default joystick bindings.
extern const std::map<config::GameControl, std::unordered_set<config::UserInput>>
kDefaultBindings;
extern const config::GameControlBindings kDefaultBindings;
extern struct opts_t {
opts_t();
@ -36,13 +33,8 @@ extern struct opts_t {
int rewind_interval = 0;
/// Joypad
std::map<config::GameControl, std::unordered_set<config::UserInput>>
game_control_bindings;
int autofire_rate = 1;
/// Keyboard
config::Shortcuts shortcuts;
/// Core
int gdb_port = 55555;
int link_num_players = 2;

View File

@ -1058,7 +1058,7 @@ GameArea::~GameArea()
void GameArea::OnKillFocus(wxFocusEvent& ev)
{
config::GameControlState::Instance().Reset();
wxGetApp().game_control_state()->Reset();
ev.Skip();
}
@ -1079,7 +1079,7 @@ void GameArea::Pause()
// when the game is paused like this, we should not allow any
// input to remain pressed, because they could be released
// outside of the game zone and we would not know about it.
config::GameControlState::Instance().Reset();
wxGetApp().game_control_state()->Reset();
if (loaded != IMAGE_UNKNOWN)
soundPause();
@ -1330,13 +1330,13 @@ static Display* GetX11Display() {
#endif // __WXGTK__
void GameArea::OnUserInputDown(widgets::UserInputEvent& event) {
if (config::GameControlState::Instance().OnInputPressed(event.input())) {
if (wxGetApp().game_control_state()->OnInputPressed(event.input())) {
wxWakeUpIdle();
}
}
void GameArea::OnUserInputUp(widgets::UserInputEvent& event) {
if (config::GameControlState::Instance().OnInputReleased(event.input())) {
if (wxGetApp().game_control_state()->OnInputReleased(event.input())) {
wxWakeUpIdle();
}

View File

@ -340,7 +340,7 @@ uint32_t systemReadJoypad(int joy)
if (joy < 0 || joy > 3)
joy = OPTION(kJoyDefault) - 1;
uint32_t ret = config::GameControlState::Instance().GetJoypad(joy);
uint32_t ret = wxGetApp().game_control_state()->GetJoypad(joy);
if (turbo)
ret |= KEYM_SPEED;
@ -662,8 +662,7 @@ void systemUpdateSolarSensor()
void systemUpdateMotionSensor()
{
for (int i = 0; i < 4; i++) {
const uint32_t joy_value =
config::GameControlState::Instance().GetJoypad(i);
const uint32_t joy_value = wxGetApp().game_control_state()->GetJoypad(i);
if (!sensorx[i])
sensorx[i] = 2047;

View File

@ -3,8 +3,8 @@
#include <wx/time.h>
#include "wx/config/user-input.h"
#include "wx/opts.h"
#include "wx/widgets/user-input-event.h"
#include "wx/wxvbam.h"
namespace widgets {
@ -119,7 +119,7 @@ bool UserInputCtrlValidator::TransferToWindow() {
UserInputCtrl* control = wxDynamicCast(GetWindow(), UserInputCtrl);
assert(control);
control->SetInputs(gopts.game_control_bindings[game_control_]);
control->SetInputs((*wxGetApp().game_control_bindings())[game_control_]);
return true;
}
@ -127,7 +127,7 @@ bool UserInputCtrlValidator::TransferFromWindow() {
UserInputCtrl* control = wxDynamicCast(GetWindow(), UserInputCtrl);
assert(control);
gopts.game_control_bindings.insert({game_control_, control->inputs()});
wxGetApp().game_control_bindings()->insert({game_control_, control->inputs()});
return true;
}

View File

@ -58,8 +58,8 @@ void ResetMenuItemAccelerator(wxMenuItem* menu_item) {
if (tab_index != wxString::npos) {
new_label.resize(tab_index);
}
std::set<config::UserInput> user_inputs =
gopts.shortcuts.InputsForCommand(menu_item->GetId());
std::unordered_set<config::UserInput> user_inputs =
wxGetApp().shortcuts()->InputsForCommand(menu_item->GetId());
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.
@ -256,7 +256,8 @@ wxvbamApp::wxvbamApp()
pending_fullscreen(false),
frame(nullptr),
using_wayland(false),
sdl_poller_(widgets::SdlPoller(std::bind(&wxvbamApp::GetJoyEventHandler, this))) {}
game_control_state_(std::bind(&wxvbamApp::game_control_bindings, this)),
sdl_poller_(std::bind(&wxvbamApp::GetJoyEventHandler, this)) {}
const wxString wxvbamApp::GetPluginsDir()
{
@ -541,7 +542,7 @@ bool wxvbamApp::OnInit() {
// Initialize game bindings here, after defaults bindings, vbam.ini bindings
// and command line overrides have been applied.
config::GameControlState::Instance().OnGameBindingsChanged();
game_control_state()->OnGameBindingsChanged();
// We need to gather this information before crating the MainFrame as the
// OnSize / OnMove event handlers can fire during construction.
@ -1008,7 +1009,7 @@ int MainFrame::FilterEvent(wxEvent& event) {
}
const widgets::UserInputEvent& user_input_event = static_cast<widgets::UserInputEvent&>(event);
const int command = gopts.shortcuts.CommandForInput(user_input_event.input());
const int command = wxGetApp().shortcuts()->CommandForInput(user_input_event.input());
if (command == 0) {
// No associated command found.
return wxEventFilter::Event_Skip;

View File

@ -12,8 +12,10 @@
#include <wx/datetime.h>
#include "core/base/system.h"
#include "wx/config/game-control.h"
#include "wx/config/option-observer.h"
#include "wx/config/option.h"
#include "wx/config/shortcuts.h"
#include "wx/dialogs/base-dialog.h"
#include "wx/widgets/dpi-support.h"
#include "wx/widgets/keep-on-top-styler.h"
@ -118,6 +120,11 @@ public:
}
}
// Accessors for configuration data.
config::Shortcuts* shortcuts() { return &shortcuts_; }
config::GameControlState* game_control_state() { return &game_control_state_; }
config::GameControlBindings* game_control_bindings() { return &game_control_bindings_; }
virtual ~wxvbamApp();
protected:
@ -129,6 +136,10 @@ private:
// Returns the currently active event handler to use for user input events.
wxEvtHandler* GetJoyEventHandler();
config::Shortcuts shortcuts_;
config::GameControlState game_control_state_;
config::GameControlBindings game_control_bindings_;
wxPathList config_path;
char* home = nullptr;