mirror of
https://github.com/citra-emu/citra.git
synced 2024-11-29 17:10:05 +00:00
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:
parent
0f28ed9ce8
commit
9a9d627eb9
@ -1,11 +1,13 @@
|
|||||||
set(SRCS
|
set(SRCS
|
||||||
emu_window/emu_window_sdl2.cpp
|
emu_window/emu_window_sdl2.cpp
|
||||||
|
rebind_window/rebind_window_sdl2.cpp
|
||||||
citra.cpp
|
citra.cpp
|
||||||
config.cpp
|
config.cpp
|
||||||
citra.rc
|
citra.rc
|
||||||
)
|
)
|
||||||
set(HEADERS
|
set(HEADERS
|
||||||
emu_window/emu_window_sdl2.h
|
emu_window/emu_window_sdl2.h
|
||||||
|
rebind_window/rebind_window_sdl2.h
|
||||||
config.h
|
config.h
|
||||||
default_ini.h
|
default_ini.h
|
||||||
resource.h
|
resource.h
|
||||||
@ -17,7 +19,7 @@ include_directories(${SDL2_INCLUDE_DIR})
|
|||||||
|
|
||||||
add_executable(citra ${SRCS} ${HEADERS})
|
add_executable(citra ${SRCS} ${HEADERS})
|
||||||
target_link_libraries(citra core video_core audio_core common)
|
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)
|
if (MSVC)
|
||||||
target_link_libraries(citra getopt)
|
target_link_libraries(citra getopt)
|
||||||
endif()
|
endif()
|
||||||
|
@ -21,8 +21,11 @@
|
|||||||
#include <windows.h>
|
#include <windows.h>
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#include <SDL.h>
|
||||||
|
|
||||||
#include "citra/config.h"
|
#include "citra/config.h"
|
||||||
#include "citra/emu_window/emu_window_sdl2.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/backend.h"
|
||||||
#include "common/logging/filter.h"
|
#include "common/logging/filter.h"
|
||||||
#include "common/logging/log.h"
|
#include "common/logging/log.h"
|
||||||
@ -40,6 +43,8 @@ static void PrintHelp(const char* argv0) {
|
|||||||
std::cout << "Usage: " << argv0
|
std::cout << "Usage: " << argv0
|
||||||
<< " [options] <filename>\n"
|
<< " [options] <filename>\n"
|
||||||
"-g, --gdbport=NUMBER Enable gdb stub on port NUMBER\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"
|
"-h, --help Display this help and exit\n"
|
||||||
"-v, --version Output version information 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;
|
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
|
/// Application entry point
|
||||||
int main(int argc, char** argv) {
|
int main(int argc, char** argv) {
|
||||||
Config config;
|
Config config;
|
||||||
@ -67,14 +86,13 @@ int main(int argc, char** argv) {
|
|||||||
std::string boot_filename;
|
std::string boot_filename;
|
||||||
|
|
||||||
static struct option long_options[] = {
|
static struct option long_options[] = {
|
||||||
{"gdbport", required_argument, 0, 'g'},
|
{"gdbport", required_argument, 0, 'g'}, {"list-bindings", no_argument, 0, 'l'},
|
||||||
{"help", no_argument, 0, 'h'},
|
{"rebind", required_argument, 0, 'r'}, {"help", no_argument, 0, 'h'},
|
||||||
{"version", no_argument, 0, 'v'},
|
{"version", no_argument, 0, 'v'}, {0, 0, 0, 0},
|
||||||
{0, 0, 0, 0},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
while (optind < argc) {
|
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) {
|
if (arg != -1) {
|
||||||
switch (arg) {
|
switch (arg) {
|
||||||
case 'g':
|
case 'g':
|
||||||
@ -88,6 +106,22 @@ int main(int argc, char** argv) {
|
|||||||
exit(1);
|
exit(1);
|
||||||
}
|
}
|
||||||
break;
|
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':
|
case 'h':
|
||||||
PrintHelp(argv[0]);
|
PrintHelp(argv[0]);
|
||||||
return 0;
|
return 0;
|
||||||
|
@ -12,7 +12,8 @@
|
|||||||
#include "core/settings.h"
|
#include "core/settings.h"
|
||||||
|
|
||||||
Config::Config() {
|
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_loc = FileUtil::GetUserPath(D_CONFIG_IDX) + "sdl2-config.ini";
|
||||||
sdl2_config = std::make_unique<INIReader>(sdl2_config_loc);
|
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));
|
static_cast<u16>(sdl2_config->GetInteger("Debugging", "gdbstub_port", 24689));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const std::string& Config::GetConfigPath() {
|
||||||
|
return sdl2_config_loc;
|
||||||
|
}
|
||||||
|
|
||||||
void Config::Reload() {
|
void Config::Reload() {
|
||||||
LoadINI(DefaultINI::sdl2_config_file);
|
LoadINI(DefaultINI::sdl2_config_file);
|
||||||
ReadValues();
|
ReadValues();
|
||||||
|
@ -18,5 +18,7 @@ class Config {
|
|||||||
public:
|
public:
|
||||||
Config();
|
Config();
|
||||||
|
|
||||||
|
const std::string& GetConfigPath();
|
||||||
|
|
||||||
void Reload();
|
void Reload();
|
||||||
};
|
};
|
||||||
|
130
src/citra/rebind_window/rebind_window_sdl2.cpp
Normal file
130
src/citra/rebind_window/rebind_window_sdl2.cpp
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
50
src/citra/rebind_window/rebind_window_sdl2.h
Normal file
50
src/citra/rebind_window/rebind_window_sdl2.h
Normal 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;
|
||||||
|
};
|
@ -7,6 +7,7 @@
|
|||||||
#include "common/common_paths.h"
|
#include "common/common_paths.h"
|
||||||
#include "common/file_util.h"
|
#include "common/file_util.h"
|
||||||
#include "common/logging/log.h"
|
#include "common/logging/log.h"
|
||||||
|
#include "common/string_util.h"
|
||||||
|
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
#include <windows.h>
|
#include <windows.h>
|
||||||
@ -46,6 +47,7 @@
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
#include <sstream>
|
||||||
#include <sys/stat.h>
|
#include <sys/stat.h>
|
||||||
|
|
||||||
#ifndef S_ISDIR
|
#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() {}
|
||||||
|
|
||||||
IOFile::IOFile(const std::string& filename, const char openmode[]) {
|
IOFile::IOFile(const std::string& filename, const char openmode[]) {
|
||||||
|
@ -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,
|
void SplitFilename83(const std::string& filename, std::array<char, 9>& short_name,
|
||||||
std::array<char, 4>& extension);
|
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
|
// simple wrapper for cstdlib file functions to
|
||||||
// hopefully will make error checking easier
|
// hopefully will make error checking easier
|
||||||
// and make forgetting an fclose() harder
|
// and make forgetting an fclose() harder
|
||||||
|
Loading…
Reference in New Issue
Block a user