Add interactive key rebind tool to SDL2 frontend

This involved writing a slightly messy function that updates an .ini file
while keeping its comments and whitespace intact. It seems to do a good job.

This tool isn't *great*, right now -- but issue #2313 prevents me from
rendering any text in the SDL window that pops up to prompt the user for a
single keypress.
This commit is contained in:
Lynn 2016-12-15 17:32:22 +01:00
parent 0f28ed9ce8
commit 9a9d627eb9
8 changed files with 314 additions and 7 deletions

View File

@ -1,11 +1,13 @@
set(SRCS
emu_window/emu_window_sdl2.cpp
rebind_window/rebind_window_sdl2.cpp
citra.cpp
config.cpp
citra.rc
)
set(HEADERS
emu_window/emu_window_sdl2.h
rebind_window/rebind_window_sdl2.h
config.h
default_ini.h
resource.h
@ -17,7 +19,7 @@ include_directories(${SDL2_INCLUDE_DIR})
add_executable(citra ${SRCS} ${HEADERS})
target_link_libraries(citra core video_core audio_core common)
target_link_libraries(citra ${SDL2_LIBRARY} ${OPENGL_gl_LIBRARY} inih glad)
target_link_libraries(citra ${SDL2_LIBRARY} ${OPENGL_gl_LIBRARY} inih glad SDL_Pango)
if (MSVC)
target_link_libraries(citra getopt)
endif()

View File

@ -21,8 +21,11 @@
#include <windows.h>
#endif
#include <SDL.h>
#include "citra/config.h"
#include "citra/emu_window/emu_window_sdl2.h"
#include "citra/rebind_window/rebind_window_sdl2.h"
#include "common/logging/backend.h"
#include "common/logging/filter.h"
#include "common/logging/log.h"
@ -40,6 +43,8 @@ static void PrintHelp(const char* argv0) {
std::cout << "Usage: " << argv0
<< " [options] <filename>\n"
"-g, --gdbport=NUMBER Enable gdb stub on port NUMBER\n"
"-l, --list-bindings List the current keybindings\n"
"-r, --rebind=KEY Rebind a key (see -l for key names)\n"
"-h, --help Display this help and exit\n"
"-v, --version Output version information and exit\n";
}
@ -48,6 +53,20 @@ static void PrintVersion() {
std::cout << "Citra " << Common::g_scm_branch << " " << Common::g_scm_desc << std::endl;
}
static void ListBindings(const Config& config) {
for (int i = 0; i < Settings::NativeInput::NUM_INPUTS; ++i) {
int code = Settings::values.input_mappings[Settings::NativeInput::All[i]];
std::cout << Settings::NativeInput::Mapping[i] << ": "
<< SDL_GetScancodeName(static_cast<SDL_Scancode>(code)) << std::endl;
}
}
/// Show an SDL window to rebind the `index`-th key in Settings::NativeInput::All.
static void Rebind(int index, const std::string& config_filename) {
RebindWindow_SDL2 rebind_window(index, config_filename);
rebind_window.Run();
}
/// Application entry point
int main(int argc, char** argv) {
Config config;
@ -67,14 +86,13 @@ int main(int argc, char** argv) {
std::string boot_filename;
static struct option long_options[] = {
{"gdbport", required_argument, 0, 'g'},
{"help", no_argument, 0, 'h'},
{"version", no_argument, 0, 'v'},
{0, 0, 0, 0},
{"gdbport", required_argument, 0, 'g'}, {"list-bindings", no_argument, 0, 'l'},
{"rebind", required_argument, 0, 'r'}, {"help", no_argument, 0, 'h'},
{"version", no_argument, 0, 'v'}, {0, 0, 0, 0},
};
while (optind < argc) {
char arg = getopt_long(argc, argv, "g:hv", long_options, &option_index);
char arg = getopt_long(argc, argv, "g:lr:hv", long_options, &option_index);
if (arg != -1) {
switch (arg) {
case 'g':
@ -88,6 +106,22 @@ int main(int argc, char** argv) {
exit(1);
}
break;
case 'l':
ListBindings(config);
return 0;
case 'r':
// Try to read a key name.
for (int i = 0; i < Settings::NativeInput::NUM_INPUTS; ++i) {
if (strcmp(Settings::NativeInput::Mapping[i], optarg) == 0) {
Rebind(i, config.GetConfigPath());
return 0;
}
}
LOG_CRITICAL(Frontend, "\"%s\" is not a valid key name. "
"See `%s -l` for a list of key names.",
optarg, argv[0]);
return -1;
case 'h':
PrintHelp(argv[0]);
return 0;

View File

@ -12,7 +12,8 @@
#include "core/settings.h"
Config::Config() {
// TODO: Don't hardcode the path; let the frontend decide where to put the config files.
// TODO: Don't hardcode the path; let the frontend decide where to put the
// config files.
sdl2_config_loc = FileUtil::GetUserPath(D_CONFIG_IDX) + "sdl2-config.ini";
sdl2_config = std::make_unique<INIReader>(sdl2_config_loc);
@ -100,6 +101,10 @@ void Config::ReadValues() {
static_cast<u16>(sdl2_config->GetInteger("Debugging", "gdbstub_port", 24689));
}
const std::string& Config::GetConfigPath() {
return sdl2_config_loc;
}
void Config::Reload() {
LoadINI(DefaultINI::sdl2_config_file);
ReadValues();

View File

@ -18,5 +18,7 @@ class Config {
public:
Config();
const std::string& GetConfigPath();
void Reload();
};

View File

@ -0,0 +1,130 @@
// Copyright 2016 Citra Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
// #include <algorithm>
#include <cstdlib>
#include <iostream>
#include <string>
#define SDL_MAIN_HANDLED
#include <SDL.h>
#include "citra/rebind_window/rebind_window_sdl2.h"
// #include "common/key_map.h"
#include "common/file_util.h"
#include "common/logging/log.h"
#include "common/scm_rev.h"
#include "common/string_util.h"
// #include "core/hle/service/hid/hid.h"
#include "citra/config.h"
#include "core/settings.h"
// #include "video_core/video_core.h"
const u32 fps = 60;
const u32 minimum_frame_time = 1000 / fps;
const int REBIND_WINDOW_WIDTH = 640;
const int REBIND_WINDOW_HEIGHT = 480;
void RebindWindow_SDL2::OnKeyDown(int key) {
const char* key_name = Settings::NativeInput::Mapping[modifying_index];
std::string scancode_name = SDL_GetScancodeName(static_cast<SDL_Scancode>(key));
std::cout << "Rebound " << key_name << " to " << scancode_name << "." << std::endl;
FileUtil::SetINIKey(config_filename, "Controls", key_name, std::to_string(key));
is_open = false;
}
bool RebindWindow_SDL2::IsOpen() const {
return is_open;
}
RebindWindow_SDL2::RebindWindow_SDL2(int modifying_index, const std::string& config_filename)
: modifying_index(modifying_index), config_filename(config_filename) {
SDL_SetMainReady();
// Initialize the window
if (SDL_Init(SDL_INIT_VIDEO) < 0) {
LOG_CRITICAL(Frontend, "Failed to initialize SDL2! Exiting...");
exit(1);
}
std::string window_title = Common::StringFromFormat("Citra | %s-%s | Rebinding keys",
Common::g_scm_branch, Common::g_scm_desc);
render_window = SDL_CreateWindow(window_title.c_str(),
SDL_WINDOWPOS_UNDEFINED, // x position
SDL_WINDOWPOS_UNDEFINED, // y position
REBIND_WINDOW_WIDTH, REBIND_WINDOW_HEIGHT, SDL_WINDOW_SHOWN);
if (render_window == nullptr) {
LOG_CRITICAL(Frontend, "Failed to create SDL2 window! Exiting...");
exit(1);
}
renderer = SDL_CreateRenderer(render_window, -1, SDL_RENDERER_ACCELERATED);
if (renderer == nullptr) {
LOG_CRITICAL(Frontend, "Failed to create SDL2 renderer! Exiting...");
exit(1);
}
}
RebindWindow_SDL2::~RebindWindow_SDL2() {
SDL_DestroyRenderer(renderer);
SDL_DestroyWindow(render_window);
SDL_Quit();
}
void RebindWindow_SDL2::PollEvents() {
SDL_Event event;
while (SDL_PollEvent(&event) && is_open) {
switch (event.type) {
case SDL_WINDOWEVENT:
if (event.window.event == SDL_WINDOWEVENT_CLOSE) {
is_open = false;
}
break;
case SDL_KEYDOWN:
OnKeyDown(static_cast<int>(event.key.keysym.scancode));
break;
case SDL_QUIT:
is_open = false;
break;
}
}
}
void RebindWindow_SDL2::Render() {
SDL_SetRenderDrawColor(renderer, 0xff, 0xff, 0xff, 0xff);
SDL_RenderClear(renderer);
// TODO: Ideally, this window should be an interactive editor, but at the very least,
// display some sort of "press a key" text here. This requires a text rendering library
// though, see issue #2313.
}
void RebindWindow_SDL2::Run() {
Uint32 last_frame_time = 0;
Uint32 frame_time;
Uint32 time_delta;
int current = Settings::values.input_mappings[Settings::NativeInput::All[modifying_index]];
std::cout << "Press a key to bind to " << Settings::NativeInput::Mapping[modifying_index]
<< " (currently " << SDL_GetScancodeName(static_cast<SDL_Scancode>(current)) << ")..."
<< std::endl;
while (is_open) {
frame_time = SDL_GetTicks();
PollEvents();
time_delta = frame_time - last_frame_time;
last_frame_time = frame_time;
Render();
SDL_RenderPresent(renderer);
// Make sure each frame is at least `minimum_frame_time` milliseconds long.
Uint32 ms = std::min(minimum_frame_time - (SDL_GetTicks() - frame_time), 0U);
SDL_Delay(ms);
}
}

View File

@ -0,0 +1,50 @@
// Copyright 2016 Citra Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#pragma once
#include <utility>
#include "common/emu_window.h"
struct SDL_Window;
class RebindWindow_SDL2 {
public:
RebindWindow_SDL2(int modifying_index, const std::string& config_filename);
~RebindWindow_SDL2();
/// Polls window events
void PollEvents();
/// Run until the window is closed
void Run();
/// Render the keybind editor UI to `renderer`
void Render();
/// Whether the window is still open, and a close request hasn't yet been sent
bool IsOpen() const;
/// Load keymap from configuration
void ReloadSetKeymaps();
private:
/// Called by PollEvents when a key is pressed.
void OnKeyDown(int key);
/// Which button are we modifying?
int modifying_index;
/// Where is the config file we're modifying?
std::string config_filename;
/// Is the window still open?
bool is_open = true;
/// Internal SDL2 render window
SDL_Window* render_window;
/// Internal SDL2 renderer
SDL_Renderer* renderer;
};

View File

@ -7,6 +7,7 @@
#include "common/common_paths.h"
#include "common/file_util.h"
#include "common/logging/log.h"
#include "common/string_util.h"
#ifdef _WIN32
#include <windows.h>
@ -46,6 +47,7 @@
#endif
#include <algorithm>
#include <sstream>
#include <sys/stat.h>
#ifndef S_ISDIR
@ -836,6 +838,83 @@ void SplitFilename83(const std::string& filename, std::array<char, 9>& short_nam
}
}
bool SetINIKey(const std::string& filename, const std::string& section, const std::string& key,
const std::string& value) {
// TODO: Rewrite this to take a map<section, map<key, value>>, performing
// many updates at once.
std::string contents;
ReadFileToString(true, filename.c_str(), contents);
std::istringstream iss(contents);
std::string new_contents;
std::string this_section;
std::string target_section = Common::ToLower(section);
std::string target_key = Common::ToLower(key);
bool updated = false;
for (std::string line; std::getline(iss, line);) {
std::string original = line + "\n";
// Strip any inline comment.
size_t comment = line.find(" ;");
if (comment != std::string::npos) {
line.erase(line.begin() + comment, line.end());
}
// Strip whitespace, too.
line = Common::StripSpaces(line);
if (line.empty() || line[0] == '#' || line[0] == ';') {
new_contents += original;
continue;
}
if (line[0] == '[') {
if (line.back() == ']') {
// Valid section header.
if (this_section == target_section && !updated) {
// Create a new key.
new_contents += key + "=" + value + "\n\n";
updated = true;
}
this_section = Common::ToLower(line.substr(1, line.size() - 2));
new_contents += original;
continue;
} else {
// Invalid section header.
return false;
}
}
// Handle key-value pairs.
// We want to keep indentation, here, so work with `original`.
size_t equals = original.find("=");
if (equals != std::string::npos) {
std::string pre = original.substr(0, equals);
std::string this_key = Common::ToLower(Common::StripSpaces(pre));
if (this_section == target_section && this_key == target_key) {
new_contents += pre + "=" + value + "\n";
updated = true;
} else {
new_contents += original;
}
} else {
// Invalid key-value pair.
return false;
}
}
if (!updated) {
// Create a new section.
new_contents += "\n[" + section + "]\n" + key + "=" + value + "\n";
}
WriteStringToFile(true, new_contents, filename.c_str());
return true;
}
IOFile::IOFile() {}
IOFile::IOFile(const std::string& filename, const char openmode[]) {

View File

@ -170,6 +170,11 @@ size_t ReadFileToString(bool text_file, const char* filename, std::string& str);
void SplitFilename83(const std::string& filename, std::array<char, 9>& short_name,
std::array<char, 4>& extension);
// Set a single key in an INI file at the given path, creating a new key/section
// if necessary. Overwrites the original file. Returns true on success.
bool SetINIKey(const std::string& filename, const std::string& section, const std::string& key,
const std::string& value);
// simple wrapper for cstdlib file functions to
// hopefully will make error checking easier
// and make forgetting an fclose() harder