diff --git a/src/citra/CMakeLists.txt b/src/citra/CMakeLists.txt index f9c488a1a..719d32181 100644 --- a/src/citra/CMakeLists.txt +++ b/src/citra/CMakeLists.txt @@ -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() diff --git a/src/citra/citra.cpp b/src/citra/citra.cpp index 3114a71db..f0dacd0b4 100644 --- a/src/citra/citra.cpp +++ b/src/citra/citra.cpp @@ -21,8 +21,11 @@ #include #endif +#include + #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] \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(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; diff --git a/src/citra/config.cpp b/src/citra/config.cpp index 29462c982..7f9658728 100644 --- a/src/citra/config.cpp +++ b/src/citra/config.cpp @@ -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(sdl2_config_loc); @@ -100,6 +101,10 @@ void Config::ReadValues() { static_cast(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(); diff --git a/src/citra/config.h b/src/citra/config.h index b1c31f59c..ad71e3e6f 100644 --- a/src/citra/config.h +++ b/src/citra/config.h @@ -18,5 +18,7 @@ class Config { public: Config(); + const std::string& GetConfigPath(); + void Reload(); }; diff --git a/src/citra/rebind_window/rebind_window_sdl2.cpp b/src/citra/rebind_window/rebind_window_sdl2.cpp new file mode 100644 index 000000000..a31b06097 --- /dev/null +++ b/src/citra/rebind_window/rebind_window_sdl2.cpp @@ -0,0 +1,130 @@ +// Copyright 2016 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +// #include +#include +#include +#include +#define SDL_MAIN_HANDLED +#include + +#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(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(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(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); + } +} \ No newline at end of file diff --git a/src/citra/rebind_window/rebind_window_sdl2.h b/src/citra/rebind_window/rebind_window_sdl2.h new file mode 100644 index 000000000..e58fa719c --- /dev/null +++ b/src/citra/rebind_window/rebind_window_sdl2.h @@ -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 +#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; +}; diff --git a/src/common/file_util.cpp b/src/common/file_util.cpp index b6161f2dc..8fc4ea0b9 100644 --- a/src/common/file_util.cpp +++ b/src/common/file_util.cpp @@ -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 @@ -46,6 +47,7 @@ #endif #include +#include #include #ifndef S_ISDIR @@ -836,6 +838,83 @@ void SplitFilename83(const std::string& filename, std::array& 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>, 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[]) { diff --git a/src/common/file_util.h b/src/common/file_util.h index ac58607c5..ef52d35d4 100644 --- a/src/common/file_util.h +++ b/src/common/file_util.h @@ -170,6 +170,11 @@ size_t ReadFileToString(bool text_file, const char* filename, std::string& str); void SplitFilename83(const std::string& filename, std::array& short_name, std::array& 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