diff --git a/controller_binding_spec.yml b/controller_binding_spec.yml new file mode 100644 index 00000000000..73af1bc9bb0 --- /dev/null +++ b/controller_binding_spec.yml @@ -0,0 +1,81 @@ +enable_rumble: + type: bool + default: true +# steel_battalion_mapping: +# ... +controller_mapping: + a: + type: integer + default: 0 + b: + type: integer + default: 1 + x: + type: integer + default: 2 + y: + type: integer + default: 3 + back: + type: integer + default: 4 + guide: + type: integer + default: 5 + start: + type: integer + default: 6 + lstick_btn: + type: integer + default: 7 + rstick_btn: + type: integer + default: 8 + lshoulder: + type: integer + default: 9 + rshoulder: + type: integer + default: 10 + dpad_up: + type: integer + default: 11 + dpad_down: + type: integer + default: 12 + dpad_left: + type: integer + default: 13 + dpad_right: + type: integer + default: 14 + axis_left_x: + type: integer + default: 0 + axis_left_y: + type: integer + default: 1 + axis_right_x: + type: integer + default: 2 + axis_right_y: + type: integer + default: 3 + axis_trigger_left: + type: integer + default: 4 + axis_trigger_right: + type: integer + default: 5 + invert_axis_left_x: + type: bool + default: false + invert_axis_left_y: + type: bool + default: false + invert_axis_right_x: + type: bool + default: false + invert_axis_right_y: + type: bool + default: false diff --git a/genconfig b/genconfig index 44bab849ce8..cd3275756e6 160000 --- a/genconfig +++ b/genconfig @@ -1 +1 @@ -Subproject commit 44bab849ce87fceafd74703bfcf2b61a1a1b738f +Subproject commit cd3275756e67f3e24039d6478ad766632ba816e3 diff --git a/meson.build b/meson.build index 38127c98098..be6c9af77c4 100644 --- a/meson.build +++ b/meson.build @@ -2900,7 +2900,7 @@ qemu_version = custom_target('qemu-version.h', build_by_default: true, build_always_stale: true) -genconfig_cmd = [ +config_genconfig_cmd = [ python, files('genconfig/gen_config.py'), meson.current_source_dir() / 'config_spec.yml', 'xemu-config.h' ] @@ -2908,12 +2908,25 @@ genconfig_cmd = [ xemu_config = custom_target('xemu-config.h', output: 'xemu-config.h', input: [ files('config_spec.yml') ], - command: genconfig_cmd, + command: config_genconfig_cmd, depend_files: files('config_spec.yml')) +controller_genconfig_cmd = [ + python, files('genconfig/gen_config.py'), + meson.current_source_dir() / 'controller_binding_spec.yml', 'xemu-controller-binding.h', + '--struct', 'controller_binding' +] + +xemu_controller_binding = custom_target('xemu-controller-binding.h', + output: 'xemu-controller-binding.h', + input: [ files('controller_binding_spec.yml') ], + command: controller_genconfig_cmd, + depend_files: files('controller_binding_spec.yml')) + genh += qemu_version genh += xemu_version genh += xemu_config +genh += xemu_controller_binding hxdep = [] hx_headers = [ diff --git a/ui/meson.build b/ui/meson.build index d09be2dbc02..ac94d5928f0 100644 --- a/ui/meson.build +++ b/ui/meson.build @@ -25,6 +25,8 @@ xemu_ss.add(files( 'xemu-monitor.c', 'xemu-net.c', 'xemu-settings.cc', + 'xemu-controllers.cc', + 'xemu-toml.cc', 'xemu.c', 'xemu-data.c', diff --git a/ui/xemu-controllers.cc b/ui/xemu-controllers.cc new file mode 100644 index 00000000000..13c73924e50 --- /dev/null +++ b/ui/xemu-controllers.cc @@ -0,0 +1,189 @@ +/* + * xemu Controller Binding Management + * + * Copyright (C) 2020-2023 Matt Borgerson + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "qemu/osdep.h" +#include "ui/xemu-toml.hh" +#include "xemu-controllers.h" +#include "xemu-settings.h" +#include +#include +#include +#include + +#define DEFINE_CONFIG_TREE +#include "xemu-controller-binding.h" + +std::pair +ControllerKeyboardRebindingMap::ConsumeRebindEvent(SDL_Event *event) +{ + if (event->type == SDL_KEYDOWN) { + *(xemu_keyboard_scancode_map[table_row]) = event->key.keysym.scancode; + + return { true, true }; + } + + return { false, false }; +} +#include + +std::pair +ControllerGamepadRebindingMap::ConsumeRebindEvent(SDL_Event *event) +{ + if (event->type == SDL_CONTROLLERDEVICEREMOVED) { + if (state->sdl_joystick_id == event->cdevice.which) { + return { false, true }; + } + } else if (event->type == SDL_CONTROLLERBUTTONUP && table_row < 15 && + seen_key_down) { + // Bind on controller up ensures the UI does not immediately respond + // once the new binding is applied + + if (state->sdl_joystick_id != event->cbutton.which) { + return { false, false }; + } + + int *button_map[15] = { + &state->controller_map.controller_mapping.a, + &state->controller_map.controller_mapping.b, + &state->controller_map.controller_mapping.x, + &state->controller_map.controller_mapping.y, + &state->controller_map.controller_mapping.back, + &state->controller_map.controller_mapping.guide, + &state->controller_map.controller_mapping.start, + &state->controller_map.controller_mapping.lstick_btn, + &state->controller_map.controller_mapping.rstick_btn, + &state->controller_map.controller_mapping.lshoulder, + &state->controller_map.controller_mapping.rshoulder, + &state->controller_map.controller_mapping.dpad_up, + &state->controller_map.controller_mapping.dpad_down, + &state->controller_map.controller_mapping.dpad_left, + &state->controller_map.controller_mapping.dpad_right, + }; + + *(button_map[table_row]) = event->cbutton.button; + + return { true, true }; + } else if (event->type == SDL_CONTROLLERBUTTONDOWN && table_row < 15) { + // If we are rebinding with a controller, we should not consume the key + // up event from activating the button + seen_key_down = true; + } else if (event->type == SDL_CONTROLLERAXISMOTION && table_row >= 15) { + // FIXME: Allow face buttons to map to axes + if (state->sdl_joystick_id != event->caxis.which) { + return { false, false }; + } + + int *axis_map[6] = { + &state->controller_map.controller_mapping.axis_left_x, + &state->controller_map.controller_mapping.axis_left_y, + &state->controller_map.controller_mapping.axis_right_x, + &state->controller_map.controller_mapping.axis_right_y, + &state->controller_map.controller_mapping.axis_trigger_left, + &state->controller_map.controller_mapping.axis_trigger_right, + }; + + *(axis_map[table_row - 15]) = event->caxis.axis; + + return { true, true }; + } + + return { false, false }; +} + +static const char *get_binding_path() +{ + static const char *binding_path = nullptr; + + if (!binding_path) { + binding_path = + g_strdup_printf("%s/bindings", xemu_settings_get_base_path()); + qemu_mkdir(binding_path); + } + + return binding_path; +} + +void xemu_settings_load_controller_binding(const char *guid, + struct controller_binding *cb) +{ + const char *base_path = get_binding_path(); + controller_binding_tree.reset_to_defaults(); + + assert(strlen(guid) > 0); + char *binding_path = g_strdup_printf("%s/%s.toml", base_path, guid); + + bool success = true; + + auto err = + xemu_toml::load_toml_config(binding_path, controller_binding_tree); + g_free(binding_path); + controller_binding_tree.store_to_struct(cb); + + if (err) { + success = false; + auto error_msg = std::visit( + xemu_toml::overloaded{ + [](xemu_toml::file_not_found) { + return std::string("Binding file not found, starting with " + "default settings.\n"); + }, + [](xemu_toml::failed_to_open) { + return std::string("Failed to open binding file for " + "reading. Check permissions.\n"); + }, + [](xemu_toml::failed_to_read) { + return std::string("Failed to read binding file.\n"); + }, + [](toml::parse_error &err) { + std::ostringstream oss; + oss << "Error parsing config file at " << err.source().begin + << ":\n" + << " " << err.description() << "\n" + << "Please fix the error or delete the file to " + "continue.\n"; + return oss.str(); + }, + }, + *err); + + fprintf(stderr, "%s", error_msg.c_str()); + } + + if (!success) { + xemu_settings_save_controller_binding(guid, cb); + } +} + +void xemu_settings_save_controller_binding(const char *guid, + struct controller_binding *cb) +{ + const char *base_path = get_binding_path(); + + assert(strlen(guid) > 0); + char *binding_path = g_strdup_printf("%s/%s.toml", base_path, guid); + + auto err = + xemu_toml::save_toml_config(binding_path, controller_binding_tree, cb); + g_free(binding_path); + if (err) { + fprintf( + stderr, + "Failed to open binding file for writing. Check permissions.\n"); + } +} diff --git a/ui/xemu-controllers.h b/ui/xemu-controllers.h new file mode 100644 index 00000000000..b3cb2b2e474 --- /dev/null +++ b/ui/xemu-controllers.h @@ -0,0 +1,86 @@ +/* + * xemu Settings Management + * + * Copyright (C) 2020-2023 Matt Borgerson + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef XEMU_CONTROLLERS_H +#define XEMU_CONTROLLERS_H + +#include "xemu-controller-binding.h" +#include "xemu-input.h" +#include "xemu-settings.h" +#include + +#ifdef __cplusplus + +struct RebindingMap { + // Returns [consume, cancel]: + // consume: Whether the SDL_Event should not propagate to the UI + // cancel: Whether this rebinding map should be cancelled + virtual std::pair ConsumeRebindEvent(SDL_Event *event) = 0; + + int GetTableRow() const + { + return table_row; + } + + virtual ~RebindingMap() + { + } + +protected: + int table_row; + RebindingMap(int table_row) : table_row{ table_row } + { + } +}; + +struct ControllerKeyboardRebindingMap : public virtual RebindingMap { + std::pair ConsumeRebindEvent(SDL_Event *event) override; + + ControllerKeyboardRebindingMap(int table_row) : RebindingMap(table_row) + { + } +}; + +class ControllerGamepadRebindingMap : public virtual RebindingMap { + ControllerState *state; + bool seen_key_down; + +public: + std::pair ConsumeRebindEvent(SDL_Event *event) override; + ControllerGamepadRebindingMap(int table_row, ControllerState *state) + : RebindingMap(table_row), state{ state }, seen_key_down{ false } + { + } +}; + +extern "C" { +#endif + +extern int *xemu_keyboard_scancode_map[25]; + +void xemu_settings_load_controller_binding(const char *guid, + struct controller_binding *cb); +void xemu_settings_save_controller_binding(const char *guid, + struct controller_binding *cb); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/ui/xemu-input.c b/ui/xemu-input.c index aa932830962..024ebab5999 100644 --- a/ui/xemu-input.c +++ b/ui/xemu-input.c @@ -28,9 +28,12 @@ #include "qemu/timer.h" #include "qemu/config-file.h" +#include "xemu-controllers.h" #include "xemu-input.h" #include "xemu-notifications.h" #include "xemu-settings.h" +#include +#include // #define DEBUG_INPUT @@ -93,7 +96,104 @@ static const char **port_index_to_settings_key_map[] = { &g_config.input.bindings.port4, }; -static int sdl_kbd_scancode_map[25]; +int *xemu_keyboard_scancode_map[25] = { + &g_config.input.keyboard_controller_scancode_map.a, + &g_config.input.keyboard_controller_scancode_map.b, + &g_config.input.keyboard_controller_scancode_map.x, + &g_config.input.keyboard_controller_scancode_map.y, + &g_config.input.keyboard_controller_scancode_map.dpad_left, + &g_config.input.keyboard_controller_scancode_map.dpad_up, + &g_config.input.keyboard_controller_scancode_map.dpad_right, + &g_config.input.keyboard_controller_scancode_map.dpad_down, + &g_config.input.keyboard_controller_scancode_map.back, + &g_config.input.keyboard_controller_scancode_map.start, + &g_config.input.keyboard_controller_scancode_map.white, + &g_config.input.keyboard_controller_scancode_map.black, + &g_config.input.keyboard_controller_scancode_map.lstick_btn, + &g_config.input.keyboard_controller_scancode_map.rstick_btn, + &g_config.input.keyboard_controller_scancode_map.guide, + &g_config.input.keyboard_controller_scancode_map.lstick_up, + &g_config.input.keyboard_controller_scancode_map.lstick_left, + &g_config.input.keyboard_controller_scancode_map.lstick_right, + &g_config.input.keyboard_controller_scancode_map.lstick_down, + &g_config.input.keyboard_controller_scancode_map.ltrigger, + &g_config.input.keyboard_controller_scancode_map.rstick_up, + &g_config.input.keyboard_controller_scancode_map.rstick_left, + &g_config.input.keyboard_controller_scancode_map.rstick_right, + &g_config.input.keyboard_controller_scancode_map.rstick_down, + &g_config.input.keyboard_controller_scancode_map.rtrigger, +}; + +static void check_and_reset_in_range(int *btn, int min, int max, + const char *message) +{ + if (*btn < min || *btn >= max) { + fprintf(stderr, "%s\n", message); + *btn = min; + } +} + +static void xemu_input_bindings_reload_controller_map(ControllerState *con) +{ + assert(con->type == INPUT_DEVICE_SDL_GAMECONTROLLER); + + char guid[35] = { 0 }; + SDL_JoystickGetGUIDString(con->sdl_joystick_guid, guid, sizeof(guid)); + xemu_settings_load_controller_binding(guid, &con->controller_map); + +#define CHECK_RESET_BUTTON(btn) \ + check_and_reset_in_range(&con->controller_map.controller_mapping.btn, \ + SDL_CONTROLLER_BUTTON_INVALID, \ + SDL_CONTROLLER_BUTTON_MAX, \ + "Invalid entry for button " #btn ", resetting") + + CHECK_RESET_BUTTON(a); + CHECK_RESET_BUTTON(b); + CHECK_RESET_BUTTON(x); + CHECK_RESET_BUTTON(y); + CHECK_RESET_BUTTON(dpad_left); + CHECK_RESET_BUTTON(dpad_up); + CHECK_RESET_BUTTON(dpad_right); + CHECK_RESET_BUTTON(dpad_down); + CHECK_RESET_BUTTON(back); + CHECK_RESET_BUTTON(start); + CHECK_RESET_BUTTON(lshoulder); + CHECK_RESET_BUTTON(rshoulder); + CHECK_RESET_BUTTON(lstick_btn); + CHECK_RESET_BUTTON(rstick_btn); + CHECK_RESET_BUTTON(guide); + +#undef CHECK_RESET_BUTTON + +#define CHECK_RESET_AXIS(axis) \ + check_and_reset_in_range(&con->controller_map.controller_mapping.axis, \ + SDL_CONTROLLER_AXIS_INVALID, \ + SDL_CONTROLLER_AXIS_MAX, \ + "Invalid entry for button " #axis ", resetting") + + CHECK_RESET_AXIS(axis_trigger_left); + CHECK_RESET_AXIS(axis_trigger_right); + CHECK_RESET_AXIS(axis_left_x); + CHECK_RESET_AXIS(axis_left_y); + CHECK_RESET_AXIS(axis_right_x); + CHECK_RESET_AXIS(axis_right_y); + +#undef CHECK_RESET_AXIS +} + +static void xemu_input_bindings_flush_all(void) +{ + ControllerState *iter, *next; + char guid[35] = { 0 }; + QTAILQ_FOREACH_SAFE (iter, &available_controllers, entry, next) { + // Keyboard does not need to be flushed + if (iter->type != INPUT_DEVICE_SDL_GAMECONTROLLER) + continue; + + SDL_JoystickGetGUIDString(iter->sdl_joystick_guid, guid, sizeof(guid)); + xemu_settings_save_controller_binding(guid, &iter->controller_map); + } +} void xemu_input_init(void) { @@ -113,38 +213,14 @@ void xemu_input_init(void) new_con->name = "Keyboard"; new_con->bound = -1; - sdl_kbd_scancode_map[0] = g_config.input.keyboard_controller_scancode_map.a; - sdl_kbd_scancode_map[1] = g_config.input.keyboard_controller_scancode_map.b; - sdl_kbd_scancode_map[2] = g_config.input.keyboard_controller_scancode_map.x; - sdl_kbd_scancode_map[3] = g_config.input.keyboard_controller_scancode_map.y; - sdl_kbd_scancode_map[4] = g_config.input.keyboard_controller_scancode_map.dpad_left; - sdl_kbd_scancode_map[5] = g_config.input.keyboard_controller_scancode_map.dpad_up; - sdl_kbd_scancode_map[6] = g_config.input.keyboard_controller_scancode_map.dpad_right; - sdl_kbd_scancode_map[7] = g_config.input.keyboard_controller_scancode_map.dpad_down; - sdl_kbd_scancode_map[8] = g_config.input.keyboard_controller_scancode_map.back; - sdl_kbd_scancode_map[9] = g_config.input.keyboard_controller_scancode_map.start; - sdl_kbd_scancode_map[10] = g_config.input.keyboard_controller_scancode_map.white; - sdl_kbd_scancode_map[11] = g_config.input.keyboard_controller_scancode_map.black; - sdl_kbd_scancode_map[12] = g_config.input.keyboard_controller_scancode_map.lstick_btn; - sdl_kbd_scancode_map[13] = g_config.input.keyboard_controller_scancode_map.rstick_btn; - sdl_kbd_scancode_map[14] = g_config.input.keyboard_controller_scancode_map.guide; - sdl_kbd_scancode_map[15] = g_config.input.keyboard_controller_scancode_map.lstick_up; - sdl_kbd_scancode_map[16] = g_config.input.keyboard_controller_scancode_map.lstick_left; - sdl_kbd_scancode_map[17] = g_config.input.keyboard_controller_scancode_map.lstick_right; - sdl_kbd_scancode_map[18] = g_config.input.keyboard_controller_scancode_map.lstick_down; - sdl_kbd_scancode_map[19] = g_config.input.keyboard_controller_scancode_map.ltrigger; - sdl_kbd_scancode_map[20] = g_config.input.keyboard_controller_scancode_map.rstick_up; - sdl_kbd_scancode_map[21] = g_config.input.keyboard_controller_scancode_map.rstick_left; - sdl_kbd_scancode_map[22] = g_config.input.keyboard_controller_scancode_map.rstick_right; - sdl_kbd_scancode_map[23] = g_config.input.keyboard_controller_scancode_map.rstick_down; - sdl_kbd_scancode_map[24] = g_config.input.keyboard_controller_scancode_map.rtrigger; - for (int i = 0; i < 25; i++) { - if( (sdl_kbd_scancode_map[i] < SDL_SCANCODE_UNKNOWN) || - (sdl_kbd_scancode_map[i] >= SDL_NUM_SCANCODES) ) { - fprintf(stderr, "WARNING: Keyboard controller map scancode out of range (%d) : Disabled\n", sdl_kbd_scancode_map[i]); - sdl_kbd_scancode_map[i] = SDL_SCANCODE_UNKNOWN; - } + static const char *format_str = + "WARNING: Keyboard controller map scancode out of range " + "(%d) : Disabled\n"; + char buf[128]; + snprintf(buf, sizeof(buf), format_str, i); + check_and_reset_in_range(xemu_keyboard_scancode_map[i], + SDL_SCANCODE_UNKNOWN, SDL_NUM_SCANCODES, buf); } // Check to see if we should auto-bind the keyboard @@ -156,6 +232,7 @@ void xemu_input_init(void) xemu_queue_notification(buf); } + atexit(xemu_input_bindings_flush_all); QTAILQ_INSERT_TAIL(&available_controllers, new_con, entry); } @@ -194,8 +271,7 @@ void xemu_input_process_sdl_events(const SDL_Event *event) ControllerState *new_con = malloc(sizeof(ControllerState)); memset(new_con, 0, sizeof(ControllerState)); new_con->type = INPUT_DEVICE_SDL_GAMECONTROLLER; - new_con->name = SDL_GameControllerName(sdl_con); - new_con->rumble_enabled = true; + new_con->name = SDL_GameControllerName(sdl_con); new_con->sdl_gamecontroller = sdl_con; new_con->sdl_joystick = SDL_GameControllerGetJoystick(new_con->sdl_gamecontroller); new_con->sdl_joystick_id = SDL_JoystickInstanceID(new_con->sdl_joystick); @@ -206,6 +282,8 @@ void xemu_input_process_sdl_events(const SDL_Event *event) SDL_JoystickGetGUIDString(new_con->sdl_joystick_guid, guid_buf, sizeof(guid_buf)); DPRINTF("Opened %s (%s)\n", new_con->name, guid_buf); + xemu_input_bindings_reload_controller_map(new_con); + QTAILQ_INSERT_TAIL(&available_controllers, new_con, entry); // Do not replace binding for a currently bound device. In the case that @@ -282,6 +360,13 @@ void xemu_input_process_sdl_events(const SDL_Event *event) // Unlink QTAILQ_REMOVE(&available_controllers, iter, entry); + // Flush bindings + char guid[35] = { 0 }; + SDL_JoystickGetGUIDString(iter->sdl_joystick_guid, guid, + sizeof(guid)); + xemu_settings_save_controller_binding(guid, + &iter->controller_map); + // Deallocate if (iter->sdl_gamecontroller) { SDL_GameControllerClose(iter->sdl_gamecontroller); @@ -336,20 +421,30 @@ void xemu_input_update_sdl_kbd_controller_state(ControllerState *state) const uint8_t *kbd = SDL_GetKeyboardState(NULL); for (int i = 0; i < 15; i++) { - state->buttons |= kbd[sdl_kbd_scancode_map[i]] << i; + state->buttons |= kbd[*(xemu_keyboard_scancode_map[i])] << i; } - if (kbd[sdl_kbd_scancode_map[15]]) state->axis[CONTROLLER_AXIS_LSTICK_Y] = 32767; - if (kbd[sdl_kbd_scancode_map[16]]) state->axis[CONTROLLER_AXIS_LSTICK_X] = -32768; - if (kbd[sdl_kbd_scancode_map[17]]) state->axis[CONTROLLER_AXIS_LSTICK_X] = 32767; - if (kbd[sdl_kbd_scancode_map[18]]) state->axis[CONTROLLER_AXIS_LSTICK_Y] = -32768; - if (kbd[sdl_kbd_scancode_map[19]]) state->axis[CONTROLLER_AXIS_LTRIG] = 32767; - - if (kbd[sdl_kbd_scancode_map[20]]) state->axis[CONTROLLER_AXIS_RSTICK_Y] = 32767; - if (kbd[sdl_kbd_scancode_map[21]]) state->axis[CONTROLLER_AXIS_RSTICK_X] = -32768; - if (kbd[sdl_kbd_scancode_map[22]]) state->axis[CONTROLLER_AXIS_RSTICK_X] = 32767; - if (kbd[sdl_kbd_scancode_map[23]]) state->axis[CONTROLLER_AXIS_RSTICK_Y] = -32768; - if (kbd[sdl_kbd_scancode_map[24]]) state->axis[CONTROLLER_AXIS_RTRIG] = 32767; + if (kbd[*(xemu_keyboard_scancode_map[15])]) + state->axis[CONTROLLER_AXIS_LSTICK_Y] = 32767; + if (kbd[*(xemu_keyboard_scancode_map[16])]) + state->axis[CONTROLLER_AXIS_LSTICK_X] = -32768; + if (kbd[*(xemu_keyboard_scancode_map[17])]) + state->axis[CONTROLLER_AXIS_LSTICK_X] = 32767; + if (kbd[*(xemu_keyboard_scancode_map[18])]) + state->axis[CONTROLLER_AXIS_LSTICK_Y] = -32768; + if (kbd[*(xemu_keyboard_scancode_map[19])]) + state->axis[CONTROLLER_AXIS_LTRIG] = 32767; + + if (kbd[*(xemu_keyboard_scancode_map[20])]) + state->axis[CONTROLLER_AXIS_RSTICK_Y] = 32767; + if (kbd[*(xemu_keyboard_scancode_map[21])]) + state->axis[CONTROLLER_AXIS_RSTICK_X] = -32768; + if (kbd[*(xemu_keyboard_scancode_map[22])]) + state->axis[CONTROLLER_AXIS_RSTICK_X] = 32767; + if (kbd[*(xemu_keyboard_scancode_map[23])]) + state->axis[CONTROLLER_AXIS_RSTICK_Y] = -32768; + if (kbd[*(xemu_keyboard_scancode_map[24])]) + state->axis[CONTROLLER_AXIS_RTRIG] = 32767; } void xemu_input_update_sdl_controller_state(ControllerState *state) @@ -357,51 +452,75 @@ void xemu_input_update_sdl_controller_state(ControllerState *state) state->buttons = 0; memset(state->axis, 0, sizeof(state->axis)); - const SDL_GameControllerButton sdl_button_map[15] = { - SDL_CONTROLLER_BUTTON_A, - SDL_CONTROLLER_BUTTON_B, - SDL_CONTROLLER_BUTTON_X, - SDL_CONTROLLER_BUTTON_Y, - SDL_CONTROLLER_BUTTON_DPAD_LEFT, - SDL_CONTROLLER_BUTTON_DPAD_UP, - SDL_CONTROLLER_BUTTON_DPAD_RIGHT, - SDL_CONTROLLER_BUTTON_DPAD_DOWN, - SDL_CONTROLLER_BUTTON_BACK, - SDL_CONTROLLER_BUTTON_START, - SDL_CONTROLLER_BUTTON_LEFTSHOULDER, - SDL_CONTROLLER_BUTTON_RIGHTSHOULDER, - SDL_CONTROLLER_BUTTON_LEFTSTICK, - SDL_CONTROLLER_BUTTON_RIGHTSTICK, - SDL_CONTROLLER_BUTTON_GUIDE - }; +#define SDL_MASK_BUTTON(state, btn, idx) \ + (SDL_GameControllerGetButton( \ + (state)->sdl_gamecontroller, \ + (state)->controller_map.controller_mapping.btn) \ + << idx) + + state->buttons |= SDL_MASK_BUTTON(state, a, 0); + state->buttons |= SDL_MASK_BUTTON(state, b, 1); + state->buttons |= SDL_MASK_BUTTON(state, x, 2); + state->buttons |= SDL_MASK_BUTTON(state, y, 3); + state->buttons |= SDL_MASK_BUTTON(state, dpad_left, 4); + state->buttons |= SDL_MASK_BUTTON(state, dpad_up, 5); + state->buttons |= SDL_MASK_BUTTON(state, dpad_right, 6); + state->buttons |= SDL_MASK_BUTTON(state, dpad_down, 7); + state->buttons |= SDL_MASK_BUTTON(state, back, 8); + state->buttons |= SDL_MASK_BUTTON(state, start, 9); + state->buttons |= SDL_MASK_BUTTON(state, lshoulder, 10); + state->buttons |= SDL_MASK_BUTTON(state, rshoulder, 11); + state->buttons |= SDL_MASK_BUTTON(state, lstick_btn, 12); + state->buttons |= SDL_MASK_BUTTON(state, rstick_btn, 13); + state->buttons |= SDL_MASK_BUTTON(state, guide, 14); + +#undef SDL_MASK_BUTTON + +#define SDL_GET_AXIS(state, axis) \ + SDL_GameControllerGetAxis((state)->sdl_gamecontroller, \ + (state)->controller_map.controller_mapping.axis) + + state->axis[0] = SDL_GET_AXIS(state, axis_trigger_left); + state->axis[1] = SDL_GET_AXIS(state, axis_trigger_right); + state->axis[2] = SDL_GET_AXIS(state, axis_left_x); + state->axis[3] = SDL_GET_AXIS(state, axis_left_y); + state->axis[4] = SDL_GET_AXIS(state, axis_right_x); + state->axis[5] = SDL_GET_AXIS(state, axis_right_y); + +#undef SDL_GET_AXIS + +// FIXME: Check range +#define INVERT_AXIS(controller_axis) \ + state->axis[controller_axis] = -1 - state->axis[controller_axis] + + if (state->controller_map.controller_mapping.invert_axis_left_x) { + INVERT_AXIS(CONTROLLER_AXIS_LSTICK_X); + } - for (int i = 0; i < 15; i++) { - state->buttons |= SDL_GameControllerGetButton(state->sdl_gamecontroller, sdl_button_map[i]) << i; + if (!state->controller_map.controller_mapping.invert_axis_left_y) { + INVERT_AXIS(CONTROLLER_AXIS_LSTICK_Y); + } + + if (state->controller_map.controller_mapping.invert_axis_right_x) { + INVERT_AXIS(CONTROLLER_AXIS_RSTICK_X); } - const SDL_GameControllerAxis sdl_axis_map[6] = { - SDL_CONTROLLER_AXIS_TRIGGERLEFT, - SDL_CONTROLLER_AXIS_TRIGGERRIGHT, - SDL_CONTROLLER_AXIS_LEFTX, - SDL_CONTROLLER_AXIS_LEFTY, - SDL_CONTROLLER_AXIS_RIGHTX, - SDL_CONTROLLER_AXIS_RIGHTY, - }; - - for (int i = 0; i < 6; i++) { - state->axis[i] = SDL_GameControllerGetAxis(state->sdl_gamecontroller, sdl_axis_map[i]); + if (!state->controller_map.controller_mapping.invert_axis_right_y) { + INVERT_AXIS(CONTROLLER_AXIS_RSTICK_Y); } - // FIXME: Check range - state->axis[CONTROLLER_AXIS_LSTICK_Y] = -1 - state->axis[CONTROLLER_AXIS_LSTICK_Y]; - state->axis[CONTROLLER_AXIS_RSTICK_Y] = -1 - state->axis[CONTROLLER_AXIS_RSTICK_Y]; +#undef INVERT_AXIS // xemu_input_print_controller_state(state); } void xemu_input_update_rumble(ControllerState *state) { - if (!state->rumble_enabled) { + if (state->type != INPUT_DEVICE_SDL_GAMECONTROLLER) { + return; + } + + if (!state->controller_map.enable_rumble) { return; } diff --git a/ui/xemu-input.h b/ui/xemu-input.h index 8a8ba6544ea..7a10f7e7824 100644 --- a/ui/xemu-input.h +++ b/ui/xemu-input.h @@ -25,8 +25,9 @@ #ifndef XEMU_INPUT_H #define XEMU_INPUT_H -#include #include "qemu/queue.h" +#include "xemu-controller-binding.h" +#include enum controller_state_buttons_mask { CONTROLLER_BUTTON_A = (1 << 0), @@ -78,7 +79,6 @@ typedef struct ControllerState { uint32_t animate_trigger_end; // Rumble state - bool rumble_enabled; uint16_t rumble_l, rumble_r; enum controller_input_device_type type; @@ -88,6 +88,8 @@ typedef struct ControllerState { SDL_JoystickID sdl_joystick_id; SDL_JoystickGUID sdl_joystick_guid; + struct controller_binding controller_map; + int bound; // Which port this input device is bound to void *device; // DeviceState opaque } ControllerState; diff --git a/ui/xemu-settings.cc b/ui/xemu-settings.cc index 5237d5682b3..5ee96aae5da 100644 --- a/ui/xemu-settings.cc +++ b/ui/xemu-settings.cc @@ -18,20 +18,21 @@ */ #include "qemu/osdep.h" -#include #include -#include -#include -#include #include -#include -#include -#include -#include #include #include +#include +#include +#include +#include +#include +#include +#include #include "xemu-settings.h" +#include "xemu-toml.hh" +#include #define DEFINE_CONFIG_TREE #include "xemu-config.h" @@ -112,119 +113,52 @@ const char *xemu_settings_get_default_eeprom_path(void) return eeprom_path; } -static ssize_t get_file_size(FILE *fd) -{ - if (fseek(fd, 0, SEEK_END)) { - return -1; - } - - size_t file_size = ftell(fd); - - if (fseek(fd, 0, SEEK_SET)) { - return -1; - } - - return file_size; -} - -static const char *read_file(FILE *fd) -{ - ssize_t size = get_file_size(fd); - if (size < 0) { - return NULL; - } - - char *buf = (char *)malloc(size + 1); - if (!buf) { - return NULL; - } - - if (size > 0) { - if (fread(buf, size, 1, fd) != 1) { - free(buf); - return NULL; - } - } - - buf[size] = '\x00'; - return buf; -} - bool xemu_settings_load(void) { const char *settings_path = xemu_settings_get_path(); - bool success = false; - - if (qemu_access(settings_path, F_OK) == -1) { - fprintf(stderr, "Config file not found, starting with default settings.\n"); - success = true; - } else { - FILE *fd = qemu_fopen(settings_path, "rb"); - if (fd) { - const char *buf = read_file(fd); - if (buf) { - char *previous_numeric_locale = setlocale(LC_NUMERIC, NULL); - if (previous_numeric_locale) { - previous_numeric_locale = g_strdup(previous_numeric_locale); - } - - /* Ensure numeric values are scanned with '.' radix, no grouping */ - setlocale(LC_NUMERIC, "C"); - - try { - config_tree.update_from_table(toml::parse(buf)); - success = true; - } catch (const toml::parse_error& err) { - std::ostringstream oss; - oss << "Error parsing config file at " << err.source().begin << ":\n" - << " " << err.description() << "\n" - << "Please fix the error or delete the file to continue.\n"; - error_msg = oss.str(); - } - free((char*)buf); - - if (previous_numeric_locale) { - setlocale(LC_NUMERIC, previous_numeric_locale); - g_free(previous_numeric_locale); - } - } else { - error_msg = "Failed to read config file.\n"; - } - fclose(fd); - } else { - error_msg = "Failed to open config file for reading. Check permissions.\n"; - } + bool success = true; + + auto err = xemu_toml::load_toml_config(settings_path, config_tree); + if (err) { + success = false; + std::visit(xemu_toml::overloaded{ + [&success](xemu_toml::file_not_found) { + fprintf(stderr, "Config file not found, starting " + "with default settings.\n"); + success = true; + }, + [](xemu_toml::failed_to_open) { + error_msg = "Failed to open config file for " + "reading. Check permissions.\n"; + }, + [](xemu_toml::failed_to_read) { + error_msg = "Failed to read config file.\n"; + }, + [](toml::parse_error &err) { + std::ostringstream oss; + oss << "Error parsing config file at " + << err.source().begin << ":\n" + << " " << err.description() << "\n" + << "Please fix the error or delete the file to " + "continue.\n"; + error_msg = oss.str(); + }, + }, + *err); } config_tree.store_to_struct(&g_config); - return success; } void xemu_settings_save(void) { - FILE *fd = qemu_fopen(xemu_settings_get_path(), "wb"); - if (!fd) { - fprintf(stderr, "Failed to open config file for writing. Check permissions.\n"); + auto err = xemu_toml::save_toml_config(xemu_settings_get_path(), + config_tree, &g_config); + if (err) { + fprintf(stderr, "Failed to write to config file. Check permissions.\n"); return; } - - char *previous_numeric_locale = setlocale(LC_NUMERIC, NULL); - if (previous_numeric_locale) { - previous_numeric_locale = g_strdup(previous_numeric_locale); - } - - /* Ensure numeric values are printed with '.' radix, no grouping */ - setlocale(LC_NUMERIC, "C"); - - config_tree.update_from_struct(&g_config); - fprintf(fd, "%s", config_tree.generate_delta_toml().c_str()); - fclose(fd); - - if (previous_numeric_locale) { - setlocale(LC_NUMERIC, previous_numeric_locale); - g_free(previous_numeric_locale); - } } void add_net_nat_forward_ports(int host, int guest, CONFIG_NET_NAT_FORWARD_PORTS_PROTOCOL protocol) diff --git a/ui/xemu-toml.cc b/ui/xemu-toml.cc new file mode 100644 index 00000000000..92e7ed8d75b --- /dev/null +++ b/ui/xemu-toml.cc @@ -0,0 +1,135 @@ +/* + * xemu Toml Configuration Utilities + * + * Copyright (C) 2020-2023 Matt Borgerson + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "qemu/osdep.h" +#include "xemu-toml.hh" +#include + +namespace xemu_toml { + +static ssize_t get_file_size(FILE *fd) +{ + if (fseek(fd, 0, SEEK_END)) { + return -1; + } + + size_t file_size = ftell(fd); + + if (fseek(fd, 0, SEEK_SET)) { + return -1; + } + + return file_size; +} + +static const char *read_file(FILE *fd) +{ + ssize_t size = get_file_size(fd); + if (size < 0) { + return NULL; + } + + char *buf = (char *)malloc(size + 1); + if (!buf) { + return NULL; + } + + if (size > 0) { + if (fread(buf, size, 1, fd) != 1) { + free(buf); + return NULL; + } + } + + buf[size] = '\x00'; + return buf; +} + +std::optional load_toml_config(const char *path, CNode &tree) +{ + if (qemu_access(path, F_OK) == -1) { + return file_not_found(); + } + + FILE *fd = qemu_fopen(path, "rb"); + if (!fd) { + return failed_to_open(); + } + + const char *buf = read_file(fd); + fclose(fd); + if (!buf) { + return failed_to_read(); + } + + char *previous_numeric_locale = setlocale(LC_NUMERIC, NULL); + if (previous_numeric_locale) { + previous_numeric_locale = g_strdup(previous_numeric_locale); + } + + /* Ensure numeric values are scanned with '.' radix, no grouping */ + setlocale(LC_NUMERIC, "C"); + + try { + tree.update_from_table(toml::parse(buf)); + } catch (const toml::parse_error &err) { + free((char *)buf); + return err; + } + + free((char *)buf); + + if (previous_numeric_locale) { + setlocale(LC_NUMERIC, previous_numeric_locale); + g_free(previous_numeric_locale); + } + + return std::nullopt; +} + +// Returns true on error to match semantics of load_toml_config +bool save_toml_config(const char *path, CNode &tree, void *cfg) +{ + FILE *fd = qemu_fopen(path, "wb"); + if (!fd) { + return true; + } + + char *previous_numeric_locale = setlocale(LC_NUMERIC, NULL); + if (previous_numeric_locale) { + previous_numeric_locale = g_strdup(previous_numeric_locale); + } + + /* Ensure numeric values are printed with '.' radix, no grouping */ + setlocale(LC_NUMERIC, "C"); + + tree.update_from_struct(cfg); + fprintf(fd, "%s", tree.generate_delta_toml().c_str()); + fclose(fd); + + if (previous_numeric_locale) { + setlocale(LC_NUMERIC, previous_numeric_locale); + g_free(previous_numeric_locale); + } + + return false; +} + + +} // namespace xemu_toml diff --git a/ui/xemu-toml.hh b/ui/xemu-toml.hh new file mode 100644 index 00000000000..6650b3deff9 --- /dev/null +++ b/ui/xemu-toml.hh @@ -0,0 +1,45 @@ +/* + * xemu Toml Configuration Utilities + * + * Copyright (C) 2020-2023 Matt Borgerson + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +#pragma once + +#include +#include +#include +#include +#include + +namespace xemu_toml { + +template struct overloaded : Ts... { + using Ts::operator()...; +}; +template overloaded(Ts...) -> overloaded; + +struct file_not_found {}; +struct failed_to_open {}; +struct failed_to_read {}; + +typedef std::variant + error_variant; + +std::optional load_toml_config(const char *path, CNode &tree); +bool save_toml_config(const char *path, CNode &tree, void *cfg); + +} // namespace xemu_toml diff --git a/ui/xemu.c b/ui/xemu.c index b06e19eb1c7..4dbd779bdaf 100644 --- a/ui/xemu.c +++ b/ui/xemu.c @@ -669,8 +669,10 @@ void sdl2_poll_events(struct sdl2_console *scon) xemu_hud_should_capture_kbd_mouse(&kbd, &mouse); while (SDL_PollEvent(ev)) { - xemu_input_process_sdl_events(ev); + // HUD must process events first so that if a controller is detached, + // a latent rebind request can cancel before the state is freed xemu_hud_process_sdl_events(ev); + xemu_input_process_sdl_events(ev); switch (ev->type) { case SDL_KEYDOWN: diff --git a/ui/xui/input-manager.cc b/ui/xui/input-manager.cc index 786b54b176e..5f9de128fce 100644 --- a/ui/xui/input-manager.cc +++ b/ui/xui/input-manager.cc @@ -1,6 +1,7 @@ -#include "common.hh" +#include "ui/xui/main-menu.hh" #include "input-manager.hh" #include "../xemu-input.h" +#include "common.hh" InputManager g_input_mgr; @@ -18,14 +19,18 @@ void InputManager::Update() m_buttons = 0; int16_t axis[CONTROLLER_AXIS__COUNT] = {0}; - ControllerState *iter; - QTAILQ_FOREACH(iter, &available_controllers, entry) { - if (iter->type != INPUT_DEVICE_SDL_GAMECONTROLLER) continue; - m_buttons |= iter->buttons; - // We simply take any axis that is >10 % activation - for (int i = 0; i < CONTROLLER_AXIS__COUNT; i++) { - if ((iter->axis[i] > 3276) || (iter->axis[i] < -3276)) { - axis[i] = iter->axis[i]; + // If we are rebinding a controller, prevent navigation + if (!g_main_menu.IsInputRebinding()) { + ControllerState *iter; + QTAILQ_FOREACH (iter, &available_controllers, entry) { + if (iter->type != INPUT_DEVICE_SDL_GAMECONTROLLER) + continue; + m_buttons |= iter->buttons; + // We simply take any axis that is >10 % activation + for (int i = 0; i < CONTROLLER_AXIS__COUNT; i++) { + if ((iter->axis[i] > 3276) || (iter->axis[i] < -3276)) { + axis[i] = iter->axis[i]; + } } } } diff --git a/ui/xui/main-menu.cc b/ui/xui/main-menu.cc index f29b0139832..00c08b52ded 100644 --- a/ui/xui/main-menu.cc +++ b/ui/xui/main-menu.cc @@ -44,6 +44,9 @@ MainMenuScene g_main_menu; MainMenuTabView::~MainMenuTabView() {} void MainMenuTabView::Draw() {} +void MainMenuTabView::Hide() +{ +} void MainMenuGeneralView::Draw() { @@ -71,6 +74,24 @@ void MainMenuGeneralView::Draw() // "Limit DVD/HDD throughput to approximate Xbox load times"); } +bool MainMenuInputView::ConsumeRebindEvent(SDL_Event *event) +{ + if (!rebinding) + return false; + + auto [consume, cancel] = rebinding->ConsumeRebindEvent(event); + if (cancel) { + rebinding = nullptr; + } + + return consume; +} + +bool MainMenuInputView::IsInputRebinding() +{ + return rebinding != nullptr; +} + void MainMenuInputView::Draw() { SectionTitle("Controllers"); @@ -133,6 +154,7 @@ void MainMenuInputView::Draw() if (activated) { active = i; + rebinding = nullptr; } uint32_t port_color = 0xafafafff; @@ -260,6 +282,39 @@ void MainMenuInputView::Draw() ImGui::PopFont(); ImGui::SetCursorPos(pos); + SectionTitle("Mapping"); + + float p = ImGui::GetFrameHeight() * 0.3; + ImGui::PushStyleVar(ImGuiStyleVar_CellPadding, ImVec2(p, p)); + if (ImGui::BeginTable("input_remap_tbl", 3, + ImGuiTableFlags_RowBg | ImGuiTableFlags_Borders)) { + ImGui::TableSetupColumn("Button"); + ImGui::TableSetupColumn("Mapped to"); + ImGui::TableSetupColumn("Remap"); + ImGui::TableHeadersRow(); + + PopulateTableController(bound_state); + + ImGui::EndTable(); + } + ImGui::PopStyleVar(); + + if (bound_state && bound_state->type == INPUT_DEVICE_SDL_GAMECONTROLLER) { + Toggle("Enable Rumble", &bound_state->controller_map.enable_rumble); + Toggle( + "Invert Left X Axis", + &bound_state->controller_map.controller_mapping.invert_axis_left_x); + Toggle( + "Invert Left Y Axis", + &bound_state->controller_map.controller_mapping.invert_axis_left_y); + Toggle("Invert Right X Axis", + &bound_state->controller_map.controller_mapping + .invert_axis_right_x); + Toggle("Invert Right Y Axis", + &bound_state->controller_map.controller_mapping + .invert_axis_right_y); + } + SectionTitle("Options"); Toggle("Auto-bind controllers", &g_config.input.auto_bind, "Bind newly connected controllers to any open port"); @@ -268,6 +323,154 @@ void MainMenuInputView::Draw() "Capture even if window is unfocused (requires restart)"); } +void MainMenuInputView::Hide() +{ + rebinding = nullptr; +} + +void MainMenuInputView::PopulateTableController(ControllerState *state) +{ + if (!state) + return; + + static const char *kbd_map[25] = { + "A", + "B", + "X", + "Y", + "DPad Left", + "DPad Up", + "DPad Right", + "DPad Down", + "Back", + "Start", + "White", + "Black", + "Left Stick Button", + "Right Stick Button", + "Guide", + "Left Stick Up", + "Left Stick Left", + "Left Stick Right", + "Left Stick Down", + "Left Trigger", + "Right Stick Up", + "Right Stick Left", + "Right Stick Right", + "Right Stick Down", + "Right Trigger", + }; + + static const char *gamepad_map[21] = { + "A", + "B", + "X", + "Y", + "Back", + "Guide", + "Start", + "Left Stick Button", + "Right Stick Button", + "White", + "Black", + "DPad Up", + "DPad Down", + "DPad Left", + "DPad Right", + "Left Stick Axis+", + "Left Stick Axis-", + "Right Stick Axis+", + "Right Stick Axis-", + "Left Trigger Axis", + "Right Trigger Axis", + }; + + bool is_keyboard = state->type == INPUT_DEVICE_SDL_KEYBOARD; + int table_rows = is_keyboard ? 25 : 21; + const char **table_row_entries = is_keyboard ? kbd_map : gamepad_map; + for (int i = 0; i < table_rows; ++i) { + ImGui::TableNextRow(); + + ImGui::TableSetColumnIndex(0); + ImGui::Text("%s", table_row_entries[i]); + + ImGui::TableSetColumnIndex(1); + if (is_keyboard) { + int keycode = *(xemu_keyboard_scancode_map[i]); + if (keycode == SDL_SCANCODE_UNKNOWN) { + ImGui::Text("Invalid"); + } else { + ImGui::Text("%s", SDL_GetScancodeName( + static_cast(keycode))); + } + } else { + if (i < 15) { + int *button_map[15] = { + &state->controller_map.controller_mapping.a, + &state->controller_map.controller_mapping.b, + &state->controller_map.controller_mapping.x, + &state->controller_map.controller_mapping.y, + &state->controller_map.controller_mapping.back, + &state->controller_map.controller_mapping.guide, + &state->controller_map.controller_mapping.start, + &state->controller_map.controller_mapping.lstick_btn, + &state->controller_map.controller_mapping.rstick_btn, + &state->controller_map.controller_mapping.lshoulder, + &state->controller_map.controller_mapping.rshoulder, + &state->controller_map.controller_mapping.dpad_up, + &state->controller_map.controller_mapping.dpad_down, + &state->controller_map.controller_mapping.dpad_left, + &state->controller_map.controller_mapping.dpad_right, + }; + + int button = *(button_map[i]); + if (button == SDL_CONTROLLER_BUTTON_INVALID) { + ImGui::Text("Invalid"); + } else { + ImGui::Text("%s", SDL_GameControllerGetStringForButton( + static_cast( + button))); + } + } else { + int *axis_map[6] = { + &state->controller_map.controller_mapping.axis_left_x, + &state->controller_map.controller_mapping.axis_left_y, + &state->controller_map.controller_mapping.axis_right_x, + &state->controller_map.controller_mapping.axis_right_y, + &state->controller_map.controller_mapping.axis_trigger_left, + &state->controller_map.controller_mapping + .axis_trigger_right, + }; + int axis = *(axis_map[i - 15]); + if (axis == SDL_CONTROLLER_AXIS_INVALID) { + ImGui::Text("Invalid"); + } else { + ImGui::Text("%s", + SDL_GameControllerGetStringForAxis( + static_cast(axis))); + } + } + } + + ImGui::TableSetColumnIndex(2); + if (rebinding && rebinding->GetTableRow() == i) { + ImGui::Text("Press a key to rebind"); + } else { + ImGui::PushID(i); + if (ImGui::Button("Rebind")) { + if (is_keyboard) { + rebinding = + std::make_unique(i); + } else { + rebinding = std::make_unique( + i, state); + } + } + ImGui::PopID(); + } + } +} + void MainMenuDisplayView::Draw() { SectionTitle("Quality"); @@ -1236,6 +1439,7 @@ void MainMenuScene::Show() void MainMenuScene::Hide() { + m_views[m_current_view_index]->Hide(); m_background.Hide(); m_nav_control_view.Hide(); m_animation.EaseOut(); @@ -1248,6 +1452,7 @@ bool MainMenuScene::IsAnimating() void MainMenuScene::SetNextViewIndex(int i) { + m_views[m_current_view_index]->Hide(); m_next_view_index = i % m_tabs.size(); g_config.general.last_viewed_menu_index = i; } @@ -1286,6 +1491,16 @@ void MainMenuScene::UpdateAboutViewConfigInfo() m_about_view.UpdateConfigInfoText(); } +bool MainMenuScene::ConsumeRebindEvent(SDL_Event *event) +{ + return m_input_view.ConsumeRebindEvent(event); +} + +bool MainMenuScene::IsInputRebinding() +{ + return m_input_view.IsInputRebinding(); +} + bool MainMenuScene::Draw() { m_animation.Step(); diff --git a/ui/xui/main-menu.hh b/ui/xui/main-menu.hh index 7be564701ca..e15f534763c 100644 --- a/ui/xui/main-menu.hh +++ b/ui/xui/main-menu.hh @@ -17,14 +17,16 @@ // along with this program. If not, see . // #pragma once -#include -#include -#include +#include "ui/xemu-input.h" +#include "../xemu-controllers.h" +#include "../xemu-snapshots.h" #include "common.hh" -#include "widgets.hh" -#include "scene.hh" #include "scene-components.hh" -#include "../xemu-snapshots.h" +#include "scene.hh" +#include "widgets.hh" +#include +#include +#include extern "C" { #include "net/pcap.h" @@ -36,6 +38,7 @@ class MainMenuTabView public: virtual ~MainMenuTabView(); virtual void Draw(); + virtual void Hide(); }; class MainMenuGeneralView : public virtual MainMenuTabView @@ -47,7 +50,15 @@ public: class MainMenuInputView : public virtual MainMenuTabView { public: + std::unique_ptr rebinding; + MainMenuInputView() : rebinding{ nullptr } + { + } + bool ConsumeRebindEvent(SDL_Event *event); + bool IsInputRebinding(); void Draw() override; + void Hide() override; + void PopulateTableController(ControllerState *state); }; class MainMenuDisplayView : public virtual MainMenuTabView @@ -197,6 +208,8 @@ public: void SetNextViewIndex(int i); void HandleInput(); void UpdateAboutViewConfigInfo(); + bool ConsumeRebindEvent(SDL_Event *event); + bool IsInputRebinding(); bool Draw() override; }; diff --git a/ui/xui/main.cc b/ui/xui/main.cc index fd38aa4e7bb..70fc806689d 100644 --- a/ui/xui/main.cc +++ b/ui/xui/main.cc @@ -170,6 +170,10 @@ void xemu_hud_cleanup(void) void xemu_hud_process_sdl_events(SDL_Event *event) { + if (g_main_menu.ConsumeRebindEvent(event)) { + return; + } + ImGui_ImplSDL2_ProcessEvent(event); }