Skip to content

Commit

Permalink
Refactor accelerator / global shortcuts handling
Browse files Browse the repository at this point in the history
* Removes wxAcceleratorEntryUnicode and assorted arrays in favor of a
  new class, `config::Shortcuts`, which handles UserInput assignment to
  commands and resolution at runtime. `config::Shortcuts` also handles the
  INI user configuration in a backwards-compatible way. Runtime
  resolution of UserInput to command is also now logarithmic rather than
  linear.
* The same shortcut can no longer be assigned to 2 different commands,
  which fixes #158.
* Moves the `AccelConfig` dialog to its own dedicated class.
  • Loading branch information
Steelskin committed May 23, 2023
1 parent fda429f commit d1f6500
Show file tree
Hide file tree
Showing 21 changed files with 1,346 additions and 1,362 deletions.
721 changes: 356 additions & 365 deletions po/wxvbam/wxvbam.pot

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions src/wx/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -762,9 +762,12 @@ set(
wxutil.cpp
config/game-control.cpp
config/internal/option-internal.cpp
config/internal/shortcuts-internal.cpp
config/option-observer.cpp
config/option.cpp
config/shortcuts.cpp
config/user-input.cpp
dialogs/accel-config.cpp
dialogs/directories-config.cpp
dialogs/display-config.cpp
dialogs/game-boy-config.cpp
Expand Down Expand Up @@ -808,11 +811,14 @@ set(
wxutil.h
config/game-control.h
config/internal/option-internal.h
config/internal/shortcuts-internal.h
config/option-id.h
config/option-observer.h
config/option-proxy.h
config/option.h
config/shortcuts.h
config/user-input.h
dialogs/accel-config.h
dialogs/directories-config.h
dialogs/display-config.h
dialogs/game-boy-config.h
Expand Down
6 changes: 4 additions & 2 deletions src/wx/cmdevents.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2710,8 +2710,10 @@ EVT_HANDLER(Customize, "Customize UI...")

if (!joy_timer) frame->StartJoyPollTimer();

if (ShowModal(dlg) == wxID_OK)
update_opts();
if (ShowModal(dlg) == wxID_OK) {
update_shortcut_opts();
ResetMenuAccelerators();
}

if (!joy_timer) frame->StopJoyPollTimer();

Expand Down
107 changes: 107 additions & 0 deletions src/wx/config/internal/shortcuts-internal.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
#include "config/shortcuts.h"

#include <wx/xrc/xmlres.h>

#define VBAM_SHORTCUTS_INTERNAL_INCLUDE
#include "config/internal/shortcuts-internal.h"
#undef VBAM_SHORTCUTS_INTERNAL_INCLUDE

namespace config {
namespace internal {

const std::unordered_map<int, UserInput>& DefaultShortcuts() {
static const std::unordered_map<int, UserInput> kDefaultShortcuts = {
{XRCID("CheatsList"), UserInput('C', wxMOD_CMD)},
{XRCID("NextFrame"), UserInput('N', wxMOD_CMD)},
// this was annoying people A LOT #334
//{wxID_EXIT, UserInput(WXK_ESCAPE, wxMOD_NONE)},
// this was annoying people #298
//{wxID_EXIT, UserInput('X', wxMOD_CMD)},

{wxID_EXIT, UserInput('Q', wxMOD_CMD)},
{wxID_CLOSE, UserInput('W', wxMOD_CMD)},
// load most recent is more commonly used than load state
// {XRCID("Load"), UserInput('L', wxMOD_CMD)},
{XRCID("LoadGameRecent"), UserInput('L', wxMOD_CMD)},
{XRCID("LoadGame01"), UserInput(WXK_F1, wxMOD_NONE)},
{XRCID("LoadGame02"), UserInput(WXK_F2, wxMOD_NONE)},
{XRCID("LoadGame03"), UserInput(WXK_F3, wxMOD_NONE)},
{XRCID("LoadGame04"), UserInput(WXK_F4, wxMOD_NONE)},
{XRCID("LoadGame05"), UserInput(WXK_F5, wxMOD_NONE)},
{XRCID("LoadGame06"), UserInput(WXK_F6, wxMOD_NONE)},
{XRCID("LoadGame07"), UserInput(WXK_F7, wxMOD_NONE)},
{XRCID("LoadGame08"), UserInput(WXK_F8, wxMOD_NONE)},
{XRCID("LoadGame09"), UserInput(WXK_F9, wxMOD_NONE)},
{XRCID("LoadGame10"), UserInput(WXK_F10, wxMOD_NONE)},
{XRCID("Pause"), UserInput(WXK_PAUSE, wxMOD_NONE)},
{XRCID("Pause"), UserInput('P', wxMOD_CMD)},
{XRCID("Reset"), UserInput('R', wxMOD_CMD)},
// add shortcuts for original size multiplier #415
{XRCID("SetSize1x"), UserInput('1', wxMOD_NONE)},
{XRCID("SetSize2x"), UserInput('2', wxMOD_NONE)},
{XRCID("SetSize3x"), UserInput('3', wxMOD_NONE)},
{XRCID("SetSize4x"), UserInput('4', wxMOD_NONE)},
{XRCID("SetSize5x"), UserInput('5', wxMOD_NONE)},
{XRCID("SetSize6x"), UserInput('6', wxMOD_NONE)},
// save oldest is more commonly used than save other
// {XRCID("Save"), UserInput('S', wxMOD_CMD)},
{XRCID("SaveGameOldest"), UserInput('S', wxMOD_CMD)},
{XRCID("SaveGame01"), UserInput(WXK_F1, wxMOD_SHIFT)},
{XRCID("SaveGame02"), UserInput(WXK_F2, wxMOD_SHIFT)},
{XRCID("SaveGame03"), UserInput(WXK_F3, wxMOD_SHIFT)},
{XRCID("SaveGame04"), UserInput(WXK_F4, wxMOD_SHIFT)},
{XRCID("SaveGame05"), UserInput(WXK_F5, wxMOD_SHIFT)},
{XRCID("SaveGame06"), UserInput(WXK_F6, wxMOD_SHIFT)},
{XRCID("SaveGame07"), UserInput(WXK_F7, wxMOD_SHIFT)},
{XRCID("SaveGame08"), UserInput(WXK_F8, wxMOD_SHIFT)},
{XRCID("SaveGame09"), UserInput(WXK_F9, wxMOD_SHIFT)},
{XRCID("SaveGame10"), UserInput(WXK_F10, wxMOD_SHIFT)},
// I prefer the SDL ESC key binding
// {XRCID("ToggleFullscreen"), UserInput(WXK_ESCAPE, wxMOD_NONE)},
// alt-enter is more standard anyway
{XRCID("ToggleFullscreen"), UserInput(WXK_RETURN, wxMOD_ALT)},
{XRCID("JoypadAutofireA"), UserInput('1', wxMOD_ALT)},
{XRCID("JoypadAutofireB"), UserInput('2', wxMOD_ALT)},
{XRCID("JoypadAutofireL"), UserInput('3', wxMOD_ALT)},
{XRCID("JoypadAutofireR"), UserInput('4', wxMOD_ALT)},
{XRCID("VideoLayersBG0"), UserInput('1', wxMOD_CMD)},
{XRCID("VideoLayersBG1"), UserInput('2', wxMOD_CMD)},
{XRCID("VideoLayersBG2"), UserInput('3', wxMOD_CMD)},
{XRCID("VideoLayersBG3"), UserInput('4', wxMOD_CMD)},
{XRCID("VideoLayersOBJ"), UserInput('5', wxMOD_CMD)},
{XRCID("VideoLayersWIN0"), UserInput('6', wxMOD_CMD)},
{XRCID("VideoLayersWIN1"), UserInput('7', wxMOD_CMD)},
{XRCID("VideoLayersOBJWIN"), UserInput('8', wxMOD_CMD)},
{XRCID("Rewind"), UserInput('B', wxMOD_CMD)},
// following are not in standard menus
// FILExx are filled in when recent menu is filled
{wxID_FILE1, UserInput(WXK_F1, wxMOD_CMD)},
{wxID_FILE2, UserInput(WXK_F2, wxMOD_CMD)},
{wxID_FILE3, UserInput(WXK_F3, wxMOD_CMD)},
{wxID_FILE4, UserInput(WXK_F4, wxMOD_CMD)},
{wxID_FILE5, UserInput(WXK_F5, wxMOD_CMD)},
{wxID_FILE6, UserInput(WXK_F6, wxMOD_CMD)},
{wxID_FILE7, UserInput(WXK_F7, wxMOD_CMD)},
{wxID_FILE8, UserInput(WXK_F8, wxMOD_CMD)},
{wxID_FILE9, UserInput(WXK_F9, wxMOD_CMD)},
{wxID_FILE10, UserInput(WXK_F10, wxMOD_CMD)},
{XRCID("VideoLayersReset"), UserInput('0', wxMOD_CMD)},
{XRCID("ChangeFilter"), UserInput('G', wxMOD_CMD)},
{XRCID("ChangeIFB"), UserInput('I', wxMOD_CMD)},
{XRCID("IncreaseVolume"), UserInput(WXK_NUMPAD_ADD, wxMOD_NONE)},
{XRCID("DecreaseVolume"), UserInput(WXK_NUMPAD_SUBTRACT, wxMOD_NONE)},
{XRCID("ToggleSound"), UserInput(WXK_NUMPAD_ENTER, wxMOD_NONE)},
};
return kDefaultShortcuts;
}

UserInput DefaultShortcutForCommand(int command) {
const auto& iter = DefaultShortcuts().find(command);
if (iter != DefaultShortcuts().end()) {
return iter->second;
}
return UserInput();
}

} // namespace internal
} // namespace config
21 changes: 21 additions & 0 deletions src/wx/config/internal/shortcuts-internal.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
#ifndef VBAM_SHORTCUTS_INTERNAL_INCLUDE
#error "Do not include "config/internal/shortcuts-internal.h" outside of the implementation."
#endif

#include <set>
#include <unordered_map>

#include "config/user-input.h"

namespace config {
namespace internal {

// Returns the map of commands to their default shortcut.
const std::unordered_map<int, UserInput>& DefaultShortcuts();

// Returns the default shortcut for the given `command`.
// Returns an Invalid UserInput if there is no default shortcut for `command`.
UserInput DefaultShortcutForCommand(int command);

} // namespace internal
} // namespace config
183 changes: 183 additions & 0 deletions src/wx/config/shortcuts.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
#include "config/shortcuts.h"

#include <wx/string.h>
#include <wx/translation.h>
#include <wx/xrc/xmlres.h>

#include "config/user-input.h"

#define VBAM_SHORTCUTS_INTERNAL_INCLUDE
#include "config/internal/shortcuts-internal.h"
#undef VBAM_SHORTCUTS_INTERNAL_INCLUDE

namespace config {

namespace {

int NoopCommand() {
static const int noop = XRCID("NOOP");
return noop;
}

} // namespace

Shortcuts::Shortcuts() {
// Set up default shortcuts.
for (const auto& iter : internal::DefaultShortcuts()) {
AssignInputToCommand(iter.second, iter.first);
}
}

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)
: 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>> config;
config.reserve(command_to_inputs_.size() + 1);

if (!disabled_defaults_.empty()) {
std::set<UserInput> noop_inputs;
for (const auto& iter : disabled_defaults_) {
noop_inputs.insert(iter.first);
}
config.push_back(std::make_pair(NoopCommand(), UserInput::SpanToConfigString(noop_inputs)));
}

for (const auto& iter : command_to_inputs_) {
std::set<UserInput> inputs;
for (const auto& input : iter.second) {
if (internal::DefaultShortcutForCommand(iter.first) != input) {
// Not a default input.
inputs.insert(input);
}
}
if (!inputs.empty()) {
config.push_back(std::make_pair(iter.first, UserInput::SpanToConfigString(inputs)));
}
}

return config;
}

std::set<UserInput> Shortcuts::InputsForCommand(int command) const {
if (command == NoopCommand()) {
std::set<UserInput> noop_inputs;
for (const auto& iter : disabled_defaults_) {
noop_inputs.insert(iter.first);
}
return noop_inputs;
}

auto iter = command_to_inputs_.find(command);
if (iter == command_to_inputs_.end()) {
return {};
}
return iter->second;
}

int Shortcuts::CommandForInput(const UserInput& input) const {
const auto iter = input_to_command_.find(input);
if (iter == input_to_command_.end()) {
return 0;
}
return iter->second;
}

std::set<wxJoystick> Shortcuts::Joysticks() const {
std::set<wxJoystick> output;
for (const auto& iter : command_to_inputs_) {
for (const UserInput& user_input : iter.second) {
output.insert(user_input.joystick());
}
}
return output;
}

Shortcuts Shortcuts::Clone() const {
return Shortcuts(this->command_to_inputs_, this->input_to_command_, this->disabled_defaults_);
}

void Shortcuts::AssignInputToCommand(const UserInput& input, int command) {
if (command == NoopCommand()) {
// "Assigning to Noop" means unassinging the default binding.
UnassignDefaultBinding(input);
return;
}

// Remove the existing binding if it exists.
auto iter = input_to_command_.find(input);
if (iter != input_to_command_.end()) {
UnassignInput(input);
}

auto disabled_iter = disabled_defaults_.find(input);
if (disabled_iter != disabled_defaults_.end()) {
int original_command = disabled_iter->second;
if (original_command == command) {
// Restoring a disabled input. Remove from the disabled set.
disabled_defaults_.erase(disabled_iter);
}
// Then, just continue normally.
}

command_to_inputs_[command].emplace(input);
input_to_command_[input] = command;
}

void Shortcuts::UnassignInput(const UserInput& input) {
assert(input);

auto iter = input_to_command_.find(input);
if (iter == input_to_command_.end()) {
// Input not found, nothing to do.
return;
}

if (internal::DefaultShortcutForCommand(iter->second) == input) {
// Unassigning a default binding has some special handling.
UnassignDefaultBinding(input);
return;
}

// Otherwise, just remove it from the 2 maps.
auto command_iter = command_to_inputs_.find(iter->second);
assert(command_iter != command_to_inputs_.end());

command_iter->second.erase(input);
if (command_iter->second.empty()) {
// Remove empty set.
command_to_inputs_.erase(command_iter);
}
input_to_command_.erase(iter);
}

void Shortcuts::UnassignDefaultBinding(const UserInput& input) {
auto input_iter = input_to_command_.find(input);
if (input_iter == input_to_command_.end()) {
// This can happen if the INI file provided by the user has an invalid
// option. In this case, just silently ignore it.
return;
}

if (internal::DefaultShortcutForCommand(input_iter->second) != input) {
// As above, we have already removed the default binding, ignore it.
return;
}

auto command_iter = command_to_inputs_.find(input_iter->second);
assert(command_iter != command_to_inputs_.end());

command_iter->second.erase(input);
if (command_iter->second.empty()) {
command_to_inputs_.erase(command_iter);
}

disabled_defaults_[input] = input_iter->second;
input_to_command_.erase(input_iter);
}

} // namespace config
Loading

0 comments on commit d1f6500

Please sign in to comment.