mirror of
https://github.com/citra-emu/citra.git
synced 2024-11-26 03:20: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
|
||||
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()
|
||||
|
@ -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;
|
||||
|
@ -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();
|
||||
|
@ -18,5 +18,7 @@ class Config {
|
||||
public:
|
||||
Config();
|
||||
|
||||
const std::string& GetConfigPath();
|
||||
|
||||
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/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[]) {
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user