diff --git a/CMakeModules/GenerateSettingKeys.cmake b/CMakeModules/GenerateSettingKeys.cmake index 90110cd33..1d705113c 100644 --- a/CMakeModules/GenerateSettingKeys.cmake +++ b/CMakeModules/GenerateSettingKeys.cmake @@ -202,12 +202,15 @@ if (ENABLE_QT) "use_touchpad" "controller_touch_device" "use_touch_from_button" + "input_maptype" + "controller_hotkey_maptype" "touch_from_button_map" "touch_from_button_maps" # Why are these two so similar? Basically typo bait "nand_directory" "sdmc_directory" "game_id" "KeySeq" + "controller_keyseq" "gamedirs" "libvorbis" "Context" diff --git a/dist/qt_themes/default/icons/256x256/automap_face_buttons.png b/dist/qt_themes/default/icons/256x256/automap_face_buttons.png new file mode 100644 index 000000000..581192664 Binary files /dev/null and b/dist/qt_themes/default/icons/256x256/automap_face_buttons.png differ diff --git a/dist/qt_themes/default/theme_default.qrc b/dist/qt_themes/default/theme_default.qrc index 90ae777aa..4d035c33c 100644 --- a/dist/qt_themes/default/theme_default.qrc +++ b/dist/qt_themes/default/theme_default.qrc @@ -15,6 +15,7 @@ icons/48x48/sd_card.png icons/128x128/cartridge.png icons/256x256/azahar.png + icons/256x256/automap_face_buttons.png icons/48x48/star.png icons/256x256/plus_folder.png diff --git a/src/android/app/src/main/jni/default_ini.h b/src/android/app/src/main/jni/default_ini.h index 5e2eab1d1..74556eb99 100644 --- a/src/android/app/src/main/jni/default_ini.h +++ b/src/android/app/src/main/jni/default_ini.h @@ -13,30 +13,28 @@ namespace DefaultINI { // All of these setting keys are either not currently used by Android or are too niche to bother // documenting (people can contribute documentation if they care), or some other explained reason -constexpr std::array android_config_omitted_keys = { - Keys::enable_gamemode, - Keys::use_custom_storage, - Keys::init_time_offset, - Keys::physical_device, - Keys::use_gles, // Niche - Keys::dump_command_buffers, - Keys::use_display_refresh_rate_detection, - Keys::screen_top_stretch, - Keys::screen_top_leftright_padding, - Keys::screen_top_topbottom_padding, - Keys::screen_bottom_stretch, - Keys::screen_bottom_leftright_padding, - Keys::screen_bottom_topbottom_padding, - Keys::mono_render_option, - Keys::log_regex_filter, // Niche - Keys::video_encoder, - Keys::video_encoder_options, - Keys::video_bitrate, - Keys::audio_encoder, - Keys::audio_encoder_options, - Keys::audio_bitrate, - Keys::last_artic_base_addr, // On Android, this value is stored as a "preference" -}; +constexpr std::array android_config_omitted_keys = {Keys::enable_gamemode, + Keys::use_custom_storage, + Keys::init_time_offset, + Keys::physical_device, + Keys::use_gles, // Niche + Keys::dump_command_buffers, + Keys::use_display_refresh_rate_detection, + Keys::screen_top_stretch, + Keys::screen_top_leftright_padding, + Keys::screen_top_topbottom_padding, + Keys::screen_bottom_stretch, + Keys::screen_bottom_leftright_padding, + Keys::screen_bottom_topbottom_padding, + Keys::mono_render_option, + Keys::log_regex_filter, // Niche + Keys::video_encoder, + Keys::video_encoder_options, + Keys::video_bitrate, + Keys::audio_encoder, + Keys::audio_encoder_options, + Keys::audio_bitrate, + Keys::last_artic_base_addr}; // clang-format off diff --git a/src/citra_qt/CMakeLists.txt b/src/citra_qt/CMakeLists.txt index 20971828a..3732da83f 100644 --- a/src/citra_qt/CMakeLists.txt +++ b/src/citra_qt/CMakeLists.txt @@ -63,6 +63,9 @@ add_library(citra_qt STATIC EXCLUDE_FROM_ALL configuration/configure_hotkeys.cpp configuration/configure_hotkeys.h configuration/configure_hotkeys.ui + configuration/configure_hotkeys_controller.cpp + configuration/configure_hotkeys_controller.h + configuration/configure_hotkeys_controller.ui configuration/configure_input.cpp configuration/configure_input.h configuration/configure_input.ui @@ -140,6 +143,8 @@ add_library(citra_qt STATIC EXCLUDE_FROM_ALL game_list_worker.h hotkeys.cpp hotkeys.h + hotkey_monitor.cpp + hotkey_monitor.h loading_screen.cpp loading_screen.h loading_screen.ui @@ -188,6 +193,8 @@ add_library(citra_qt STATIC EXCLUDE_FROM_ALL util/graphics_device_info.h util/sequence_dialog/sequence_dialog.cpp util/sequence_dialog/sequence_dialog.h + util/sequence_dialog/controller_sequence_dialog.cpp + util/sequence_dialog/controller_sequence_dialog.h util/spinbox.cpp util/spinbox.h util/util.cpp diff --git a/src/citra_qt/citra_qt.cpp b/src/citra_qt/citra_qt.cpp index 9bb0b2e34..db3307fa4 100644 --- a/src/citra_qt/citra_qt.cpp +++ b/src/citra_qt/citra_qt.cpp @@ -778,7 +778,8 @@ void GMainWindow::InitializeSaveStateMenuActions() { void GMainWindow::InitializeHotkeys() { hotkey_registry.LoadHotkeys(); - + hotkey_registry.buttonMonitor.start(16); + LOG_DEBUG(Frontend, "Initializing hotkeys"); const QString main_window = QStringLiteral("Main Window"); const QString fullscreen = QStringLiteral("Fullscreen"); @@ -791,6 +792,7 @@ void GMainWindow::InitializeHotkeys() { this->addAction(action); if (!primary_only) secondary_window->addAction(action); + hotkey_registry.SetAction(main_window, action_name, action); }; link_action_shortcut(ui->action_Load_File, QStringLiteral("Load File")); diff --git a/src/citra_qt/configuration/config.cpp b/src/citra_qt/configuration/config.cpp index a5aa9896b..e667468cc 100644 --- a/src/citra_qt/configuration/config.cpp +++ b/src/citra_qt/configuration/config.cpp @@ -58,44 +58,44 @@ const std::array, Settings::NativeAnalog::NumAnalogs> QtConfi // UISetting::values.shortcuts, which is alphabetically ordered. // clang-format off const std::array QtConfig::default_hotkeys {{ - {QStringLiteral("Advance Frame"), QStringLiteral("Main Window"), {QStringLiteral(""), Qt::ApplicationShortcut}}, - {QStringLiteral("Audio Mute/Unmute"), QStringLiteral("Main Window"), {QStringLiteral("Ctrl+M"), Qt::WindowShortcut}}, - {QStringLiteral("Audio Volume Down"), QStringLiteral("Main Window"), {QStringLiteral(""), Qt::WindowShortcut}}, - {QStringLiteral("Audio Volume Up"), QStringLiteral("Main Window"), {QStringLiteral(""), Qt::WindowShortcut}}, - {QStringLiteral("Capture Screenshot"), QStringLiteral("Main Window"), {QStringLiteral("Ctrl+P"), Qt::WidgetWithChildrenShortcut}}, - {QStringLiteral("Continue/Pause Emulation"), QStringLiteral("Main Window"), {QStringLiteral("F4"), Qt::WindowShortcut}}, - {QStringLiteral("Decrease 3D Factor"), QStringLiteral("Main Window"), {QStringLiteral("Ctrl+-"), Qt::ApplicationShortcut}}, - {QStringLiteral("Decrease Speed Limit"), QStringLiteral("Main Window"), {QStringLiteral("-"), Qt::ApplicationShortcut}}, - {QStringLiteral("Exit Azahar"), QStringLiteral("Main Window"), {QStringLiteral("Ctrl+Q"), Qt::WindowShortcut}}, - {QStringLiteral("Exit Fullscreen"), QStringLiteral("Main Window"), {QStringLiteral("Esc"), Qt::WindowShortcut}}, - {QStringLiteral("Fullscreen"), QStringLiteral("Main Window"), {QStringLiteral("F11"), Qt::WindowShortcut}}, - {QStringLiteral("Increase 3D Factor"), QStringLiteral("Main Window"), {QStringLiteral("Ctrl++"), Qt::ApplicationShortcut}}, - {QStringLiteral("Increase Speed Limit"), QStringLiteral("Main Window"), {QStringLiteral("+"), Qt::ApplicationShortcut}}, - {QStringLiteral("Load Amiibo"), QStringLiteral("Main Window"), {QStringLiteral("F2"), Qt::WidgetWithChildrenShortcut}}, - {QStringLiteral("Load File"), QStringLiteral("Main Window"), {QStringLiteral("Ctrl+O"), Qt::WidgetWithChildrenShortcut}}, - {QStringLiteral("Load from Newest Non-Quicksave Slot"), QStringLiteral("Main Window"), {QStringLiteral("Ctrl+V"), Qt::WindowShortcut}}, - {QStringLiteral("Multiplayer Browse Public Rooms"), QStringLiteral("Main Window"), {QStringLiteral("Ctrl+B"), Qt::ApplicationShortcut}}, - {QStringLiteral("Multiplayer Create Room"), QStringLiteral("Main Window"), {QStringLiteral("Ctrl+N"), Qt::ApplicationShortcut}}, - {QStringLiteral("Multiplayer Direct Connect to Room"), QStringLiteral("Main Window"), {QStringLiteral("Ctrl+Shift"), Qt::ApplicationShortcut}}, - {QStringLiteral("Multiplayer Leave Room"), QStringLiteral("Main Window"), {QStringLiteral("Ctrl+L"), Qt::ApplicationShortcut}}, - {QStringLiteral("Multiplayer Show Current Room"), QStringLiteral("Main Window"), {QStringLiteral("Ctrl+R"), Qt::ApplicationShortcut}}, - {QStringLiteral("Quick Save"), QStringLiteral("Main Window"), {QStringLiteral(""), Qt::WindowShortcut}}, - {QStringLiteral("Quick Load"), QStringLiteral("Main Window"), {QStringLiteral(""), Qt::WindowShortcut}}, - {QStringLiteral("Remove Amiibo"), QStringLiteral("Main Window"), {QStringLiteral("F3"), Qt::ApplicationShortcut}}, - {QStringLiteral("Restart Emulation"), QStringLiteral("Main Window"), {QStringLiteral("F6"), Qt::WindowShortcut}}, - {QStringLiteral("Rotate Screens Upright"), QStringLiteral("Main Window"), {QStringLiteral("F8"), Qt::WindowShortcut}}, - {QStringLiteral("Save to Oldest Non-Quicksave Slot"), QStringLiteral("Main Window"), {QStringLiteral("Ctrl+C"), Qt::WindowShortcut}}, - {QStringLiteral("Stop Emulation"), QStringLiteral("Main Window"), {QStringLiteral("F5"), Qt::WindowShortcut}}, - {QStringLiteral("Swap Screens"), QStringLiteral("Main Window"), {QStringLiteral("F9"), Qt::WindowShortcut}}, - {QStringLiteral("Toggle 3D"), QStringLiteral("Main Window"), {QStringLiteral("Ctrl+3"), Qt::ApplicationShortcut}}, - {QStringLiteral("Toggle Custom Textures"), QStringLiteral("Main Window"), {QStringLiteral("F7"), Qt::ApplicationShortcut}}, - {QStringLiteral("Toggle Filter Bar"), QStringLiteral("Main Window"), {QStringLiteral("Ctrl+F"), Qt::WindowShortcut}}, - {QStringLiteral("Toggle Frame Advancing"), QStringLiteral("Main Window"), {QStringLiteral("Ctrl+A"), Qt::ApplicationShortcut}}, - {QStringLiteral("Toggle Per-Application Speed"), QStringLiteral("Main Window"), {QStringLiteral("Ctrl+Z"), Qt::ApplicationShortcut}}, - {QStringLiteral("Toggle Screen Layout"), QStringLiteral("Main Window"), {QStringLiteral("F10"), Qt::WindowShortcut}}, - {QStringLiteral("Toggle Status Bar"), QStringLiteral("Main Window"), {QStringLiteral("Ctrl+S"), Qt::WindowShortcut}}, - {QStringLiteral("Toggle Texture Dumping"), QStringLiteral("Main Window"), {QStringLiteral(""), Qt::ApplicationShortcut}}, - {QStringLiteral("Toggle Turbo Mode"), QStringLiteral("Main Window"), {QStringLiteral(""), Qt::ApplicationShortcut}}, + {QStringLiteral("Advance Frame"), QStringLiteral("Main Window"), {QStringLiteral(""), QStringLiteral(""), Qt::ApplicationShortcut}}, + {QStringLiteral("Audio Mute/Unmute"), QStringLiteral("Main Window"), {QStringLiteral("Ctrl+M"), QStringLiteral(""), Qt::WindowShortcut}}, + {QStringLiteral("Audio Volume Down"), QStringLiteral("Main Window"), {QStringLiteral(""), QStringLiteral(""), Qt::WindowShortcut}}, + {QStringLiteral("Audio Volume Up"), QStringLiteral("Main Window"), {QStringLiteral(""), QStringLiteral(""), Qt::WindowShortcut}}, + {QStringLiteral("Capture Screenshot"), QStringLiteral("Main Window"), {QStringLiteral("Ctrl+P"), QStringLiteral(""), Qt::WidgetWithChildrenShortcut}}, + {QStringLiteral("Continue/Pause Emulation"), QStringLiteral("Main Window"), {QStringLiteral("F4"), QStringLiteral(""), Qt::WindowShortcut}}, + {QStringLiteral("Decrease 3D Factor"), QStringLiteral("Main Window"), {QStringLiteral("Ctrl+-"), QStringLiteral(""), Qt::ApplicationShortcut}}, + {QStringLiteral("Decrease Speed Limit"), QStringLiteral("Main Window"), {QStringLiteral("-"), QStringLiteral(""), Qt::ApplicationShortcut}}, + {QStringLiteral("Exit Azahar"), QStringLiteral("Main Window"), {QStringLiteral("Ctrl+Q"), QStringLiteral(""), Qt::WindowShortcut}}, + {QStringLiteral("Exit Fullscreen"), QStringLiteral("Main Window"), {QStringLiteral("Esc"), QStringLiteral(""), Qt::WindowShortcut}}, + {QStringLiteral("Fullscreen"), QStringLiteral("Main Window"), {QStringLiteral("F11"), QStringLiteral(""), Qt::WindowShortcut}}, + {QStringLiteral("Increase 3D Factor"), QStringLiteral("Main Window"), {QStringLiteral("Ctrl++"), QStringLiteral(""), Qt::ApplicationShortcut}}, + {QStringLiteral("Increase Speed Limit"), QStringLiteral("Main Window"), {QStringLiteral("+"), QStringLiteral(""), Qt::ApplicationShortcut}}, + {QStringLiteral("Load Amiibo"), QStringLiteral("Main Window"), {QStringLiteral("F2"), QStringLiteral(""), Qt::WidgetWithChildrenShortcut}}, + {QStringLiteral("Load File"), QStringLiteral("Main Window"), {QStringLiteral("Ctrl+O"), QStringLiteral(""), Qt::WidgetWithChildrenShortcut}}, + {QStringLiteral("Load from Newest Non-Quicksave Slot"), QStringLiteral("Main Window"), {QStringLiteral("Ctrl+V"), QStringLiteral(""), Qt::WindowShortcut}}, + {QStringLiteral("Multiplayer Browse Public Rooms"), QStringLiteral("Main Window"), {QStringLiteral("Ctrl+B"), QStringLiteral(""), Qt::ApplicationShortcut}}, + {QStringLiteral("Multiplayer Create Room"), QStringLiteral("Main Window"), {QStringLiteral("Ctrl+N"), QStringLiteral(""), Qt::ApplicationShortcut}}, + {QStringLiteral("Multiplayer Direct Connect to Room"), QStringLiteral("Main Window"), {QStringLiteral("Ctrl+Shift"), QStringLiteral(""), Qt::ApplicationShortcut}}, + {QStringLiteral("Multiplayer Leave Room"), QStringLiteral("Main Window"), {QStringLiteral("Ctrl+L"), QStringLiteral(""), Qt::ApplicationShortcut}}, + {QStringLiteral("Multiplayer Show Current Room"), QStringLiteral("Main Window"), {QStringLiteral("Ctrl+R"), QStringLiteral(""), Qt::ApplicationShortcut}}, + {QStringLiteral("Quick Save"), QStringLiteral("Main Window"), {QStringLiteral(""), QStringLiteral(""), Qt::WindowShortcut}}, + {QStringLiteral("Quick Load"), QStringLiteral("Main Window"), {QStringLiteral(""), QStringLiteral(""), Qt::WindowShortcut}}, + {QStringLiteral("Remove Amiibo"), QStringLiteral("Main Window"), {QStringLiteral("F3"), QStringLiteral(""), Qt::ApplicationShortcut}}, + {QStringLiteral("Restart Emulation"), QStringLiteral("Main Window"), {QStringLiteral("F6"), QStringLiteral(""), Qt::WindowShortcut}}, + {QStringLiteral("Rotate Screens Upright"), QStringLiteral("Main Window"), {QStringLiteral("F8"), QStringLiteral(""), Qt::WindowShortcut}}, + {QStringLiteral("Save to Oldest Non-Quicksave Slot"), QStringLiteral("Main Window"), {QStringLiteral("Ctrl+C"), QStringLiteral(""), Qt::WindowShortcut}}, + {QStringLiteral("Stop Emulation"), QStringLiteral("Main Window"), {QStringLiteral("F5"), QStringLiteral(""), Qt::WindowShortcut}}, + {QStringLiteral("Swap Screens"), QStringLiteral("Main Window"), {QStringLiteral("F9"), QStringLiteral(""), Qt::WindowShortcut}}, + {QStringLiteral("Toggle 3D"), QStringLiteral("Main Window"), {QStringLiteral("Ctrl+3"), QStringLiteral(""), Qt::ApplicationShortcut}}, + {QStringLiteral("Toggle Custom Textures"), QStringLiteral("Main Window"), {QStringLiteral("F7"), QStringLiteral(""), Qt::ApplicationShortcut}}, + {QStringLiteral("Toggle Filter Bar"), QStringLiteral("Main Window"), {QStringLiteral("Ctrl+F"), QStringLiteral(""), Qt::WindowShortcut}}, + {QStringLiteral("Toggle Frame Advancing"), QStringLiteral("Main Window"), {QStringLiteral("Ctrl+A"), QStringLiteral(""), Qt::ApplicationShortcut}}, + {QStringLiteral("Toggle Per-Application Speed"), QStringLiteral("Main Window"), {QStringLiteral("Ctrl+Z"), QStringLiteral(""), Qt::ApplicationShortcut}}, + {QStringLiteral("Toggle Screen Layout"), QStringLiteral("Main Window"), {QStringLiteral("F10"), QStringLiteral(""), Qt::WindowShortcut}}, + {QStringLiteral("Toggle Status Bar"), QStringLiteral("Main Window"), {QStringLiteral("Ctrl+S"), QStringLiteral(""), Qt::WindowShortcut}}, + {QStringLiteral("Toggle Texture Dumping"), QStringLiteral("Main Window"), {QStringLiteral(""), QStringLiteral(""), Qt::ApplicationShortcut}}, + {QStringLiteral("Toggle Turbo Mode"), QStringLiteral("Main Window"), {QStringLiteral(""), QStringLiteral(""), Qt::ApplicationShortcut}}, }}; // clang-format on @@ -336,6 +336,11 @@ void QtConfig::ReadControlValues() { ReadBasicSetting(Settings::values.use_artic_base_controller); + UISettings::values.controller_hotkey_maptype = static_cast( + ReadSetting(Settings::QKeys::controller_hotkey_maptype, + static_cast(Settings::InputMappingType::AllControllers)) + .toInt()); + int num_touch_from_button_maps = qt_config->beginReadArray(Settings::QKeys::touch_from_button_maps); @@ -374,6 +379,8 @@ void QtConfig::ReadControlValues() { Settings::InputProfile profile; profile.name = ReadSetting(Settings::QKeys::name, QStringLiteral("Default")).toString().toStdString(); + profile.maptype = static_cast( + ReadSetting(Settings::QKeys::input_maptype, 2).toInt()); for (int i = 0; i < Settings::NativeButton::NumButtons; ++i) { std::string default_param = InputCommon::GenerateKeyboardParam(default_buttons[i]); profile.buttons[i] = ReadSetting(QString::fromUtf8(Settings::NativeButton::mapping[i]), @@ -741,7 +748,10 @@ void QtConfig::ReadShortcutValues() { UISettings::values.shortcuts.push_back( {name, group, - {ReadSetting(Settings::QKeys::KeySeq, shortcut.keyseq).toString(), shortcut.context}}); + {ReadSetting(Settings::QKeys::KeySeq, shortcut.keyseq).toString(), + ReadSetting(Settings::QKeys::controller_keyseq, shortcut.controller_keyseq) + .toString(), + shortcut.context}}); qt_config->endGroup(); qt_config->endGroup(); } @@ -983,7 +993,9 @@ void QtConfig::SaveControlValues() { qt_config->beginGroup(QStringLiteral("Controls")); WriteBasicSetting(Settings::values.use_artic_base_controller); - + WriteSetting(Settings::QKeys::controller_hotkey_maptype, + static_cast(UISettings::values.controller_hotkey_maptype.GetValue()), + static_cast(Settings::InputMappingType::GuidPort)); WriteSetting(Settings::QKeys::profile, Settings::values.current_input_profile_index, 0); qt_config->beginWriteArray(QStringLiteral("profiles")); for (std::size_t p = 0; p < Settings::values.input_profiles.size(); ++p) { @@ -991,6 +1003,8 @@ void QtConfig::SaveControlValues() { const auto& profile = Settings::values.input_profiles[p]; WriteSetting(Settings::QKeys::name, QString::fromStdString(profile.name), QStringLiteral("default")); + WriteSetting(Settings::QKeys::input_maptype, static_cast(profile.maptype), + static_cast(Settings::InputMappingType::GuidPort)); for (int i = 0; i < Settings::NativeButton::NumButtons; ++i) { std::string default_param = InputCommon::GenerateKeyboardParam(default_buttons[i]); WriteSetting(QString::fromStdString(Settings::NativeButton::mapping[i]), @@ -1287,6 +1301,8 @@ void QtConfig::SaveShortcutValues() { qt_config->beginGroup(name); WriteSetting(Settings::QKeys::KeySeq, shortcut.keyseq, default_hotkey.keyseq); WriteSetting(Settings::QKeys::Context, shortcut.context, default_hotkey.context); + WriteSetting(Settings::QKeys::controller_keyseq, shortcut.controller_keyseq, + default_hotkey.controller_keyseq); qt_config->endGroup(); qt_config->endGroup(); } diff --git a/src/citra_qt/configuration/configure_dialog.cpp b/src/citra_qt/configuration/configure_dialog.cpp index 0f0fe2e71..c8de31b88 100644 --- a/src/citra_qt/configuration/configure_dialog.cpp +++ b/src/citra_qt/configuration/configure_dialog.cpp @@ -12,6 +12,7 @@ #include "citra_qt/configuration/configure_general.h" #include "citra_qt/configuration/configure_graphics.h" #include "citra_qt/configuration/configure_hotkeys.h" +#include "citra_qt/configuration/configure_hotkeys_controller.h" #include "citra_qt/configuration/configure_input.h" #include "citra_qt/configuration/configure_layout.h" #include "citra_qt/configuration/configure_storage.h" @@ -32,6 +33,7 @@ ConfigureDialog::ConfigureDialog(QWidget* parent, HotkeyRegistry& registry_, Cor system_tab{std::make_unique(system, this)}, input_tab{std::make_unique(system, this)}, hotkeys_tab{std::make_unique(this)}, + hotkeys_controller_tab{std::make_unique(this)}, graphics_tab{ std::make_unique(gl_renderer, physical_devices, is_powered_on, this)}, enhancements_tab{std::make_unique(this)}, @@ -48,7 +50,8 @@ ConfigureDialog::ConfigureDialog(QWidget* parent, HotkeyRegistry& registry_, Cor ui->tabWidget->addTab(general_tab.get(), tr("General")); ui->tabWidget->addTab(system_tab.get(), tr("System")); ui->tabWidget->addTab(input_tab.get(), tr("Input")); - ui->tabWidget->addTab(hotkeys_tab.get(), tr("Hotkeys")); + ui->tabWidget->addTab(hotkeys_controller_tab.get(), tr("Controller Hotkeys")); + ui->tabWidget->addTab(hotkeys_tab.get(), tr("Keyboard Hotkeys")); ui->tabWidget->addTab(graphics_tab.get(), tr("Graphics")); ui->tabWidget->addTab(enhancements_tab.get(), tr("Enhancements")); ui->tabWidget->addTab(layout_tab.get(), tr("Layout")); @@ -60,7 +63,7 @@ ConfigureDialog::ConfigureDialog(QWidget* parent, HotkeyRegistry& registry_, Cor ui->tabWidget->addTab(ui_tab.get(), tr("UI")); hotkeys_tab->Populate(registry); - + hotkeys_controller_tab->Populate(registry); PopulateSelectionList(); connect(ui_tab.get(), &ConfigureUi::LanguageChanged, this, &ConfigureDialog::OnLanguageChanged); @@ -104,6 +107,7 @@ void ConfigureDialog::ApplyConfiguration() { input_tab->ApplyConfiguration(); input_tab->ApplyProfile(); hotkeys_tab->ApplyConfiguration(registry); + hotkeys_controller_tab->ApplyConfiguration(registry); graphics_tab->ApplyConfiguration(); enhancements_tab->ApplyConfiguration(); layout_tab->ApplyConfiguration(); @@ -127,7 +131,7 @@ void ConfigureDialog::PopulateSelectionList() { {tr("System"), {system_tab.get(), camera_tab.get(), storage_tab.get()}}, {tr("Graphics"), {enhancements_tab.get(), layout_tab.get(), graphics_tab.get()}}, {tr("Audio"), {audio_tab.get()}}, - {tr("Controls"), {input_tab.get(), hotkeys_tab.get()}}}}; + {tr("Controls"), {input_tab.get(), hotkeys_controller_tab.get(), hotkeys_tab.get()}}}}; for (const auto& entry : items) { auto* const item = new QListWidgetItem(entry.first); @@ -158,6 +162,7 @@ void ConfigureDialog::RetranslateUI() { system_tab->RetranslateUI(); input_tab->RetranslateUI(); hotkeys_tab->RetranslateUI(); + hotkeys_controller_tab->RetranslateUI(); graphics_tab->RetranslateUI(); enhancements_tab->RetranslateUI(); layout_tab->RetranslateUI(); @@ -174,19 +179,21 @@ void ConfigureDialog::UpdateVisibleTabs() { if (items.isEmpty()) return; - const std::map widgets = {{general_tab.get(), tr("General")}, - {system_tab.get(), tr("System")}, - {input_tab.get(), tr("Input")}, - {hotkeys_tab.get(), tr("Hotkeys")}, - {enhancements_tab.get(), tr("Enhancements")}, - {layout_tab.get(), tr("Layout")}, - {graphics_tab.get(), tr("Advanced")}, - {audio_tab.get(), tr("Audio")}, - {camera_tab.get(), tr("Camera")}, - {debug_tab.get(), tr("Debug")}, - {storage_tab.get(), tr("Storage")}, - {web_tab.get(), tr("Web")}, - {ui_tab.get(), tr("UI")}}; + const std::map widgets = { + {general_tab.get(), tr("General")}, + {system_tab.get(), tr("System")}, + {input_tab.get(), tr("Input")}, + {hotkeys_tab.get(), tr("Keyboard Hotkeys")}, + {hotkeys_controller_tab.get(), tr("Controller Hotkeys")}, + {enhancements_tab.get(), tr("Enhancements")}, + {layout_tab.get(), tr("Layout")}, + {graphics_tab.get(), tr("Advanced")}, + {audio_tab.get(), tr("Audio")}, + {camera_tab.get(), tr("Camera")}, + {debug_tab.get(), tr("Debug")}, + {storage_tab.get(), tr("Storage")}, + {web_tab.get(), tr("Web")}, + {ui_tab.get(), tr("UI")}}; ui->tabWidget->clear(); diff --git a/src/citra_qt/configuration/configure_dialog.h b/src/citra_qt/configuration/configure_dialog.h index ce3c6170c..ed9b8d9d0 100644 --- a/src/citra_qt/configuration/configure_dialog.h +++ b/src/citra_qt/configuration/configure_dialog.h @@ -1,4 +1,4 @@ -// Copyright 2016 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -23,6 +23,7 @@ class ConfigureGeneral; class ConfigureSystem; class ConfigureInput; class ConfigureHotkeys; +class ConfigureControllerHotkeys; class ConfigureGraphics; class ConfigureLayout; class ConfigureEnhancements; @@ -65,6 +66,7 @@ private: std::unique_ptr system_tab; std::unique_ptr input_tab; std::unique_ptr hotkeys_tab; + std::unique_ptr hotkeys_controller_tab; std::unique_ptr graphics_tab; std::unique_ptr enhancements_tab; std::unique_ptr layout_tab; diff --git a/src/citra_qt/configuration/configure_hotkeys.cpp b/src/citra_qt/configuration/configure_hotkeys.cpp index ecaed462b..16e762438 100644 --- a/src/citra_qt/configuration/configure_hotkeys.cpp +++ b/src/citra_qt/configuration/configure_hotkeys.cpp @@ -29,7 +29,7 @@ ConfigureHotkeys::ConfigureHotkeys(QWidget* parent) ui->hotkey_list->setContextMenuPolicy(Qt::CustomContextMenu); ui->hotkey_list->setModel(model); - ui->hotkey_list->setColumnWidth(0, 250); + ui->hotkey_list->setColumnWidth(0, 300); ui->hotkey_list->resizeColumnToContents(hotkey_column); connect(ui->button_restore_defaults, &QPushButton::clicked, this, @@ -63,11 +63,9 @@ void ConfigureHotkeys::Populate(const HotkeyRegistry& registry) { QStandardItem* action = new QStandardItem(hotkey.first); QStandardItem* keyseq = new QStandardItem(hotkey.second.keyseq.toString(QKeySequence::NativeText)); - QStandardItem* controller_keyseq = new QStandardItem(hotkey.second.controller_keyseq); action->setEditable(false); keyseq->setEditable(false); - controller_keyseq->setEditable(false); - parent_item->appendRow({action, keyseq, controller_keyseq}); + parent_item->appendRow({action, keyseq}); } model->appendRow(parent_item); } diff --git a/src/citra_qt/configuration/configure_hotkeys_controller.cpp b/src/citra_qt/configuration/configure_hotkeys_controller.cpp new file mode 100644 index 000000000..48259c4c5 --- /dev/null +++ b/src/citra_qt/configuration/configure_hotkeys_controller.cpp @@ -0,0 +1,166 @@ +// Copyright Citra Emulator Project / Azahar Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include +#include +#include +#include +#include "citra_qt/configuration/config.h" +#include "citra_qt/configuration/configure_hotkeys_controller.h" +#include "citra_qt/configuration/configure_input.h" +#include "citra_qt/hotkeys.h" +#include "citra_qt/util/sequence_dialog/controller_sequence_dialog.h" +#include "ui_configure_hotkeys_controller.h" + +constexpr int name_column = 0; +constexpr int readable_hotkey_column = 1; +constexpr int hotkey_column = 2; + +ConfigureControllerHotkeys::ConfigureControllerHotkeys(QWidget* parent) + : QWidget(parent), ui(std::make_unique()) { + ui->setupUi(this); + setFocusPolicy(Qt::ClickFocus); + ui->comboBoxMappingType->setCurrentIndex( + static_cast(UISettings::values.controller_hotkey_maptype.GetValue())); + model = new QStandardItemModel(this); + model->setColumnCount(2); + model->setHorizontalHeaderLabels({tr("Action"), tr("Controller Hotkey")}); + + connect(ui->hotkey_list, &QTreeView::doubleClicked, this, + &ConfigureControllerHotkeys::Configure); + connect(ui->hotkey_list, &QTreeView::customContextMenuRequested, this, + &ConfigureControllerHotkeys::PopupContextMenu); + ui->hotkey_list->setContextMenuPolicy(Qt::CustomContextMenu); + ui->hotkey_list->setModel(model); + + ui->hotkey_list->setColumnWidth(0, 300); + ui->hotkey_list->resizeColumnToContents(readable_hotkey_column); + + connect(ui->button_clear_all, &QPushButton::clicked, this, + &ConfigureControllerHotkeys::ClearAll); +} + +ConfigureControllerHotkeys::~ConfigureControllerHotkeys() = default; + +void ConfigureControllerHotkeys::Populate(const HotkeyRegistry& registry) { + for (const auto& group : registry.hotkey_groups) { + QStandardItem* parent_item = new QStandardItem(group.first); + parent_item->setEditable(false); + for (const auto& hotkey : group.second) { + QStandardItem* action = new QStandardItem(hotkey.first); + QStandardItem* controller_keyseq = new QStandardItem(hotkey.second.controller_keyseq); + QStandardItem* readable_keyseq = new QStandardItem( + HotkeyRegistry::SequenceToString(hotkey.second.controller_keyseq)); + action->setEditable(false); + controller_keyseq->setEditable(false); + parent_item->appendRow({action, readable_keyseq, controller_keyseq}); + } + model->appendRow(parent_item); + } + + ui->hotkey_list->expandAll(); +} + +void ConfigureControllerHotkeys::Configure(QModelIndex index) { + if (!index.parent().isValid()) { + return; + } + + // Swap to the hotkey column + index = index.sibling(index.row(), hotkey_column); + QModelIndex readableIndex = index.sibling(index.row(), readable_hotkey_column); + + const auto previous_key = model->data(index); + + ControllerSequenceDialog hotkey_dialog{this}; + + const int return_code = hotkey_dialog.exec(); + const auto key_sequence = hotkey_dialog.GetSequence(); + if (return_code == QDialog::Rejected || key_sequence.isEmpty()) { + return; + } + model->setData(index, key_sequence); + model->setData(readableIndex, HotkeyRegistry::SequenceToString(key_sequence)); +} + +void ConfigureControllerHotkeys::ApplyConfiguration(HotkeyRegistry& registry) { + Settings::InputMappingType maptype = UISettings::values.controller_hotkey_maptype = + static_cast(ui->comboBoxMappingType->currentIndex()); + + for (int key_id = 0; key_id < model->rowCount(); key_id++) { + QStandardItem* parent = model->item(key_id, 0); + for (int key_column_id = 0; key_column_id < parent->rowCount(); key_column_id++) { + const QStandardItem* action = parent->child(key_column_id, name_column); + const QStandardItem* controller_keyseq = parent->child(key_column_id, hotkey_column); + if (controller_keyseq->text().isEmpty()) + continue; + const QStringList sequences = controller_keyseq->text().split(QStringLiteral("||")); + std::vector params; + std::transform(sequences.begin(), sequences.end(), std::back_inserter(params), + [](const QString& s) { return Common::ParamPackage(s.toStdString()); }); + if (maptype == Settings::InputMappingType::AllControllers) { + for (auto& param : params) + param.Set("maptype", "all"); + } else if (maptype == Settings::InputMappingType::Guid) { + for (auto& param : params) + param.Set("maptype", "guid"); + } else { + for (auto& param : params) + param.Set("maptype", "guid+port"); + } + + for (auto& [group, sub_actions] : registry.hotkey_groups) { + if (group != parent->text()) + continue; + for (auto& [action_name, hotkey] : sub_actions) { + if (action_name == action->text()) { + QStringList parts; + for (const auto& param : params) { + parts.append(QString::fromStdString(param.Serialize())); + } + hotkey.controller_keyseq = parts.join(QStringLiteral("||")); + registry.UpdateControllerHotkey(action_name, hotkey); + break; + } + } + } + } + } + + registry.SaveHotkeys(); +} + +void ConfigureControllerHotkeys::ClearAll() { + for (int r = 0; r < model->rowCount(); ++r) { + const QStandardItem* parent = model->item(r, 0); + + for (int r2 = 0; r2 < parent->rowCount(); ++r2) { + model->item(r, 0)->child(r2, readable_hotkey_column)->setText(QString{}); + model->item(r, 0)->child(r2, hotkey_column)->setText(QString{}); + } + } +} + +void ConfigureControllerHotkeys::PopupContextMenu(const QPoint& menu_location) { + const auto index = ui->hotkey_list->indexAt(menu_location); + if (!index.parent().isValid()) { + return; + } + + QMenu context_menu; + QAction* clear = context_menu.addAction(tr("Clear")); + + const auto readable_hotkey_index = index.sibling(index.row(), readable_hotkey_column); + const auto hotkey_index = index.sibling(index.row(), hotkey_column); + connect(clear, &QAction::triggered, this, [this, hotkey_index, readable_hotkey_index] { + model->setData(hotkey_index, QString{}); + model->setData(readable_hotkey_index, QString{}); + }); + + context_menu.exec(ui->hotkey_list->viewport()->mapToGlobal(menu_location)); +} + +void ConfigureControllerHotkeys::RetranslateUI() { + ui->retranslateUi(this); +} diff --git a/src/citra_qt/configuration/configure_hotkeys_controller.h b/src/citra_qt/configuration/configure_hotkeys_controller.h new file mode 100644 index 000000000..115b59f91 --- /dev/null +++ b/src/citra_qt/configuration/configure_hotkeys_controller.h @@ -0,0 +1,42 @@ +// Copyright Citra Emulator Project / Azahar Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include +#include + +namespace Ui { +class ConfigureControllerHotkeys; +} + +class HotkeyRegistry; +class QStandardItemModel; + +class ConfigureControllerHotkeys : public QWidget { + Q_OBJECT + +public: + explicit ConfigureControllerHotkeys(QWidget* parent = nullptr); + ~ConfigureControllerHotkeys() override; + + void ApplyConfiguration(HotkeyRegistry& registry); + void RetranslateUI(); + + /** + * Populates the hotkey list widget using data from the provided registry. + * Called everytime the Configure dialog is opened. + * @param registry The HotkeyRegistry whose data is used to populate the list. + */ + void Populate(const HotkeyRegistry& registry); + +private: + void Configure(QModelIndex index); + void ClearAll(); + void PopupContextMenu(const QPoint& menu_location); + + std::unique_ptr ui; + + QStandardItemModel* model; +}; diff --git a/src/citra_qt/configuration/configure_hotkeys_controller.ui b/src/citra_qt/configuration/configure_hotkeys_controller.ui new file mode 100644 index 000000000..a6422caec --- /dev/null +++ b/src/citra_qt/configuration/configure_hotkeys_controller.ui @@ -0,0 +1,135 @@ + + + ConfigureControllerHotkeys + + + + 0 + 0 + 933 + 388 + + + + Controller Hotkey Settings + + + + + + + + Double-click on a binding to change it. You can use two-button chords as well. + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Clear All + + + + + + + + + 0 + + + + + + 0 + 0 + + + + + 166 + 0 + + + + + 1920192 + 16777215 + + + + Some mappings cannot be applied to all controllers, such as back buttons + + + Apply Controller Hotkeys To: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + 0 + 0 + + + + + 150 + 0 + + + + + All controllers + + + + + Controllers of the mapped type + + + + + Only the mapped controller + + + + + + + + + + + + QAbstractItemView::NoEditTriggers + + + false + + + + + + + + + + diff --git a/src/citra_qt/configuration/configure_input.cpp b/src/citra_qt/configuration/configure_input.cpp index 2cc319968..6f072a7ad 100644 --- a/src/citra_qt/configuration/configure_input.cpp +++ b/src/citra_qt/configuration/configure_input.cpp @@ -70,88 +70,15 @@ static void SetAnalogButton(const Common::ParamPackage& input_param, } static QString ButtonToText(const Common::ParamPackage& param) { - if (!param.Has("engine")) { - return QObject::tr("[not set]"); - } - const auto engine_str = param.Get("engine", ""); - if (engine_str == "keyboard") { + if (param.Get("engine", "") == "keyboard") { return GetKeyName(param.Get("code", 0)); + } else { + return QString::fromStdString(InputCommon::ButtonToText(param)); } - - if (engine_str == "sdl") { - if (param.Has("hat")) { - const QString hat_str = QString::fromStdString(param.Get("hat", "")); - const QString direction_str = QString::fromStdString(param.Get("direction", "")); - - return QObject::tr("Hat %1 %2").arg(hat_str, direction_str); - } - - if (param.Has("axis")) { - const QString axis_str = QString::fromStdString(param.Get("axis", "")); - const QString direction_str = QString::fromStdString(param.Get("direction", "")); - - return QObject::tr("Axis %1%2").arg(axis_str, direction_str); - } - - if (param.Has("button")) { - const QString button_str = QString::fromStdString(param.Get("button", "")); - - return QObject::tr("Button %1").arg(button_str); - } - - return {}; - } - - if (engine_str == "gcpad") { - if (param.Has("axis")) { - const QString axis_str = QString::fromStdString(param.Get("axis", "")); - const QString direction_str = QString::fromStdString(param.Get("direction", "")); - - return QObject::tr("GC Axis %1%2").arg(axis_str, direction_str); - } - if (param.Has("button")) { - const QString button_str = QString::number(int(std::log2(param.Get("button", 0)))); - return QObject::tr("GC Button %1").arg(button_str); - } - return GetKeyName(param.Get("code", 0)); - } - - return QObject::tr("[unknown]"); } static QString AnalogToText(const Common::ParamPackage& param, const std::string& dir) { - if (!param.Has("engine")) { - return QObject::tr("[not set]"); - } - - const auto engine_str = param.Get("engine", ""); - if (engine_str == "analog_from_button") { - return ButtonToText(Common::ParamPackage{param.Get(dir, "")}); - } - - const QString axis_x_str{QString::fromStdString(param.Get("axis_x", ""))}; - const QString axis_y_str{QString::fromStdString(param.Get("axis_y", ""))}; - static const QString plus_str{QString::fromStdString("+")}; - static const QString minus_str{QString::fromStdString("-")}; - if (engine_str == "sdl" || engine_str == "gcpad") { - if (dir == "modifier") { - return QObject::tr("[unused]"); - } - if (dir == "left") { - return QObject::tr("Axis %1%2").arg(axis_x_str, minus_str); - } - if (dir == "right") { - return QObject::tr("Axis %1%2").arg(axis_x_str, plus_str); - } - if (dir == "up") { - return QObject::tr("Axis %1%2").arg(axis_y_str, plus_str); - } - if (dir == "down") { - return QObject::tr("Axis %1%2").arg(axis_y_str, minus_str); - } - return {}; - } - return QObject::tr("[unknown]"); + return QString::fromStdString(InputCommon::AnalogToText(param, dir)); } ConfigureInput::ConfigureInput(Core::System& _system, QWidget* parent) @@ -230,7 +157,6 @@ ConfigureInput::ConfigureInput(Core::System& _system, QWidget* parent) // If the user closes the dialog, the changes are reverted in // `GMainWindow::OnConfigure()` ApplyConfiguration(); - Settings::SaveProfile(ui->profile->currentIndex()); }, InputCommon::Polling::DeviceType::Button); }); @@ -241,7 +167,6 @@ ConfigureInput::ConfigureInput(Core::System& _system, QWidget* parent) buttons_param[button_id].Clear(); button_map[button_id]->setText(tr("[not set]")); ApplyConfiguration(); - Settings::SaveProfile(ui->profile->currentIndex()); }); context_menu.addAction(tr("Restore Default"), this, [&] { buttons_param[button_id] = @@ -249,7 +174,6 @@ ConfigureInput::ConfigureInput(Core::System& _system, QWidget* parent) QtConfig::default_buttons[button_id])}; button_map[button_id]->setText(ButtonToText(buttons_param[button_id])); ApplyConfiguration(); - Settings::SaveProfile(ui->profile->currentIndex()); }); context_menu.exec(button_map[button_id]->mapToGlobal(menu_location)); }); @@ -269,7 +193,6 @@ ConfigureInput::ConfigureInput(Core::System& _system, QWidget* parent) SetAnalogButton(params, analogs_param[analog_id], analog_sub_buttons[sub_button_id]); ApplyConfiguration(); - Settings::SaveProfile(ui->profile->currentIndex()); }, InputCommon::Polling::DeviceType::Button); }); @@ -281,7 +204,6 @@ ConfigureInput::ConfigureInput(Core::System& _system, QWidget* parent) analogs_param[analog_id].Erase(analog_sub_buttons[sub_button_id]); analog_map_buttons[analog_id][sub_button_id]->setText(tr("[not set]")); ApplyConfiguration(); - Settings::SaveProfile(ui->profile->currentIndex()); }); context_menu.addAction(tr("Restore Default"), this, [&] { Common::ParamPackage params{InputCommon::GenerateKeyboardParam( @@ -291,7 +213,6 @@ ConfigureInput::ConfigureInput(Core::System& _system, QWidget* parent) analog_map_buttons[analog_id][sub_button_id]->setText(AnalogToText( analogs_param[analog_id], analog_sub_buttons[sub_button_id])); ApplyConfiguration(); - Settings::SaveProfile(ui->profile->currentIndex()); }); context_menu.exec(analog_map_buttons[analog_id][sub_button_id]->mapToGlobal( menu_location)); @@ -308,7 +229,6 @@ ConfigureInput::ConfigureInput(Core::System& _system, QWidget* parent) [this, analog_id](const Common::ParamPackage& params) { analogs_param[analog_id] = params; ApplyConfiguration(); - Settings::SaveProfile(ui->profile->currentIndex()); }, InputCommon::Polling::DeviceType::Analog); } @@ -328,7 +248,6 @@ ConfigureInput::ConfigureInput(Core::System& _system, QWidget* parent) analogs_param[analog_id].Set("modifier_scale", slider_value / 100.0f); } ApplyConfiguration(); - Settings::SaveProfile(ui->profile->currentIndex()); }); } @@ -343,7 +262,6 @@ ConfigureInput::ConfigureInput(Core::System& _system, QWidget* parent) SetAnalogButton(params, analogs_param[analog_id], "modifier"); } ApplyConfiguration(); - Settings::SaveProfile(ui->profile->currentIndex()); }, InputCommon::Polling::DeviceType::Button); }); @@ -357,7 +275,6 @@ ConfigureInput::ConfigureInput(Core::System& _system, QWidget* parent) } ui->buttonCircleMod->setText(tr("[not set]")); ApplyConfiguration(); - Settings::SaveProfile(ui->profile->currentIndex()); }); context_menu.addAction(tr("Restore Default"), this, [&] { @@ -371,7 +288,6 @@ ConfigureInput::ConfigureInput(Core::System& _system, QWidget* parent) AnalogToText(analogs_param[analog_id], "modifier")); } ApplyConfiguration(); - Settings::SaveProfile(ui->profile->currentIndex()); }); context_menu.exec(ui->buttonCircleMod->mapToGlobal(menu_location)); }); @@ -395,7 +311,6 @@ ConfigureInput::ConfigureInput(Core::System& _system, QWidget* parent) connect(ui->profile, qOverload(&QComboBox::currentIndexChanged), this, [this](int i) { ApplyConfiguration(); - Settings::SaveProfile(Settings::values.current_input_profile_index); Settings::LoadProfile(i); LoadConfiguration(); }); @@ -407,7 +322,8 @@ ConfigureInput::ConfigureInput(Core::System& _system, QWidget* parent) Common::ParamPackage params; for (auto& poller : device_pollers) { params = poller->GetNextInput(); - if (params.Has("engine")) { + // skip button downs and only process button ups to maintain former behavior + if (params.Has("engine") && !params.Has("down")) { SetPollingResult(params, false); return; } @@ -423,12 +339,44 @@ void ConfigureInput::ApplyConfiguration() { Settings::values.use_artic_base_controller = ui->use_artic_controller->isChecked(); + Settings::values.current_input_profile.maptype = + static_cast(ui->comboBoxMappingType->currentIndex()); + std::transform(buttons_param.begin(), buttons_param.end(), Settings::values.current_input_profile.buttons.begin(), - [](const Common::ParamPackage& param) { return param.Serialize(); }); + [](Common::ParamPackage& param) { + if (param.Get("engine", "keyboard") == "sdl") { + if (Settings::values.current_input_profile.maptype == + Settings::InputMappingType::AllControllers) + param.Set("maptype", "all"); + else if (Settings::values.current_input_profile.maptype == + Settings::InputMappingType::Guid) + param.Set("maptype", "guid"); + else + param.Set("maptype", "guid+port"); + } else { + param.Erase("maptype"); + } + return param.Serialize(); + }); std::transform(analogs_param.begin(), analogs_param.end(), Settings::values.current_input_profile.analogs.begin(), - [](const Common::ParamPackage& param) { return param.Serialize(); }); + [](Common::ParamPackage& param) { + if (param.Get("engine", "keyboard") == "sdl") { + if (Settings::values.current_input_profile.maptype == + Settings::InputMappingType::AllControllers) + param.Set("maptype", "all"); + else if (Settings::values.current_input_profile.maptype == + Settings::InputMappingType::Guid) + param.Set("maptype", "guid"); + else + param.Set("maptype", "guid+port"); + } else { + param.Erase("maptype"); + } + return param.Serialize(); + }); + Settings::SaveProfile(ui->profile->currentIndex()); } void ConfigureInput::ApplyProfile() { @@ -469,6 +417,8 @@ QList ConfigureInput::GetUsedKeyboardKeys() { void ConfigureInput::LoadConfiguration() { ui->use_artic_controller->setChecked(Settings::values.use_artic_base_controller.GetValue()); + ui->comboBoxMappingType->setCurrentIndex( + static_cast(Settings::values.current_input_profile.maptype)); ui->use_artic_controller->setEnabled(!system.IsPoweredOn()); std::transform(Settings::values.current_input_profile.buttons.begin(), @@ -495,7 +445,6 @@ void ConfigureInput::RestoreDefaults() { UpdateButtonLabels(); ApplyConfiguration(); - Settings::SaveProfile(Settings::values.current_input_profile_index); } void ConfigureInput::ClearAll() { @@ -509,7 +458,6 @@ void ConfigureInput::ClearAll() { UpdateButtonLabels(); ApplyConfiguration(); - Settings::SaveProfile(Settings::values.current_input_profile_index); } void ConfigureInput::UpdateButtonLabels() { @@ -583,16 +531,21 @@ void ConfigureInput::MapFromButton(const Common::ParamPackage& params) { void ConfigureInput::AutoMap() { ui->buttonAutoMap->setEnabled(false); - if (QMessageBox::information(this, tr("Information"), - tr("After pressing OK, press any button on your joystick"), - QMessageBox::Ok | QMessageBox::Cancel) == QMessageBox::Cancel) { + QMessageBox box(this); + box.setWindowTitle(tr("Auto map Controller")); + box.setText(tr("After pressing OK, press the A (right) button on your gamepad")); + box.setStandardButtons(QMessageBox::Ok | QMessageBox::Cancel); + QPixmap pixmap(QStringLiteral(":/icons/default/256x256/automap_face_buttons.png")); + pixmap = pixmap.scaled(64, 64, Qt::KeepAspectRatio, Qt::SmoothTransformation); + box.setIconPixmap(pixmap); + int result = box.exec(); + if (result == QMessageBox::Cancel) { ui->buttonAutoMap->setEnabled(true); return; } input_setter = [this](const Common::ParamPackage& params) { MapFromButton(params); ApplyConfiguration(); - Settings::SaveProfile(ui->profile->currentIndex()); }; device_pollers = InputCommon::Polling::GetPollers(InputCommon::Polling::DeviceType::Button); want_keyboard_keys = false; @@ -690,7 +643,6 @@ void ConfigureInput::NewProfile() { } ApplyConfiguration(); - Settings::SaveProfile(ui->profile->currentIndex()); Settings::CreateProfile(name.toStdString()); ui->profile->addItem(name); ui->profile->setCurrentIndex(Settings::values.current_input_profile_index); diff --git a/src/citra_qt/configuration/configure_input.h b/src/citra_qt/configuration/configure_input.h index f70b32240..a984811ba 100644 --- a/src/citra_qt/configuration/configure_input.h +++ b/src/citra_qt/configuration/configure_input.h @@ -1,4 +1,4 @@ -// Copyright Citra Emulator Project / Lime3DS Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -43,6 +43,7 @@ public: /// Save the current input profile index void ApplyProfile(); + public slots: void OnHotkeysChanged(QList new_key_list); diff --git a/src/citra_qt/configuration/configure_input.ui b/src/citra_qt/configuration/configure_input.ui index 67cc7688f..b1e743c5e 100644 --- a/src/citra_qt/configuration/configure_input.ui +++ b/src/citra_qt/configuration/configure_input.ui @@ -6,61 +6,163 @@ 0 0 - 441 - 727 + 716 + 694 ConfigureInput + + 2 + + + 12 + - - - - - Profile - - - - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - New - - - - - - - Delete - - - - - - - Rename - - - - + + + QFrame::StyledPanel + + + QFrame::Sunken + + + + 4 + + + 12 + + + 8 + + + 12 + + + 8 + + + + + + + Profile + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + New + + + + + + + Delete + + + + + + + Rename + + + + + + + + + 0 + + + + + + 0 + 0 + + + + + 166 + 0 + + + + + 1920192 + 16777215 + + + + Some mappings cannot be applied to all controllers, such as back buttons + + + Apply Game Controller Maps To: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + 0 + 0 + + + + + 150 + 0 + + + + + All controllers + + + + + Controllers of the mapped type + + + + + Only the mapped controller + + + + + + + + @@ -68,6 +170,14 @@ true + + + 0 + 0 + 675 + 996 + + diff --git a/src/citra_qt/hotkey_monitor.cpp b/src/citra_qt/hotkey_monitor.cpp new file mode 100644 index 000000000..7ffce7df1 --- /dev/null +++ b/src/citra_qt/hotkey_monitor.cpp @@ -0,0 +1,92 @@ +// Copyright Citra Emulator Project / Azahar Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include +#include +#include +#include +#include +#include "core/frontend/input.h" +#include "hotkey_monitor.h" +#include "hotkeys.h" + +struct ControllerHotkeyMonitor::ButtonState { + Hotkey* hk; + bool lastStatus = false; + bool lastStatus2 = false; +}; + +ControllerHotkeyMonitor::ControllerHotkeyMonitor() { + m_buttons = std::make_unique>(); + m_timer = new QTimer(); + QObject::connect(m_timer, &QTimer::timeout, [this]() { checkAllButtons(); }); +} + +void ControllerHotkeyMonitor::start(const int msec) { + m_timer->start(msec); +} + +ControllerHotkeyMonitor::~ControllerHotkeyMonitor() { + delete m_timer; +} + +void ControllerHotkeyMonitor::addButton(const QString& name, Hotkey* hk) { + (*m_buttons)[name] = {hk, false}; +} + +void ControllerHotkeyMonitor::removeButton(const QString& name) { + m_buttons->erase(name); +} + +void ControllerHotkeyMonitor::checkAllButtons() { + // Controller Hotkeys + for (auto& [name, it] : *m_buttons) { + bool trigger = false; + if (!it.hk || !it.hk->button_device) + continue; + bool currentStatus = it.hk->button_device->GetStatus(); + if (it.hk->button_device2) { + // two buttons, need both pressed and one *just now* pressed + bool currentStatus2 = it.hk->button_device2->GetStatus(); + trigger = currentStatus && currentStatus2 && (!it.lastStatus || !it.lastStatus2); + it.lastStatus = currentStatus; + it.lastStatus2 = currentStatus2; + } else { + // if only one button, trigger as soon as pressed + trigger = currentStatus && !it.lastStatus; + it.lastStatus = currentStatus; + } + if (trigger) { + if (it.hk->action) { + it.hk->action->trigger(); + } + for (auto const& [name, hotkey_shortcut] : it.hk->shortcuts) { + if (hotkey_shortcut && hotkey_shortcut->isEnabled()) { + QWidget* parent = qobject_cast(hotkey_shortcut->parent()); + if (!parent) + continue; + if (name == QStringLiteral("move down")) { + std::cout << "move down triggered before context check" << std::endl; + } + bool shouldFire = true; + // Code to honor context, so we can set different contexts and parents + // appropriately + if (hotkey_shortcut->context() == Qt::WidgetShortcut) { + shouldFire = parent == QApplication::focusWidget(); + } else if (hotkey_shortcut->context() == Qt::WidgetWithChildrenShortcut) { + shouldFire = parent == QApplication::focusWidget() || + parent->isAncestorOf(QApplication::focusWidget()); + } else if (hotkey_shortcut->context() == Qt::WindowShortcut) { + shouldFire = parent->window()->isActiveWindow(); + } + + if (shouldFire) { + hotkey_shortcut->activated(); + break; + } + } + } + } + } +} \ No newline at end of file diff --git a/src/citra_qt/hotkey_monitor.h b/src/citra_qt/hotkey_monitor.h new file mode 100644 index 000000000..ecd4e3296 --- /dev/null +++ b/src/citra_qt/hotkey_monitor.h @@ -0,0 +1,28 @@ +// Copyright Citra Emulator Project / Azahar Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include +#include +#include + +class QTimer; +struct Hotkey; + +class ControllerHotkeyMonitor { +public: + explicit ControllerHotkeyMonitor(); + ~ControllerHotkeyMonitor(); + void addButton(const QString& name, Hotkey* hk); + void removeButton(const QString& name); + void start(const int msec); + +private: + void checkAllButtons(); + struct ButtonState; + + std::unique_ptr> m_buttons; + QTimer* m_timer; +}; \ No newline at end of file diff --git a/src/citra_qt/hotkeys.cpp b/src/citra_qt/hotkeys.cpp index 9b0fe4598..039653fd7 100644 --- a/src/citra_qt/hotkeys.cpp +++ b/src/citra_qt/hotkeys.cpp @@ -1,11 +1,13 @@ -// Copyright 2014 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. +#include #include #include #include "citra_qt/hotkeys.h" #include "citra_qt/uisettings.h" +#include "input_common/main.h" HotkeyRegistry::HotkeyRegistry() = default; @@ -17,22 +19,41 @@ void HotkeyRegistry::SaveHotkeys() { for (const auto& hotkey : group.second) { UISettings::values.shortcuts.push_back( {hotkey.first, group.first, - UISettings::ContextualShortcut( - {hotkey.second.keyseq.toString(), hotkey.second.context})}); + UISettings::ContextualShortcut({hotkey.second.keyseq.toString(), + hotkey.second.controller_keyseq, + hotkey.second.context})}); + } + } +} +void HotkeyRegistry::UpdateControllerHotkey(QString name, Hotkey& hk) { + if (hk.controller_keyseq.isEmpty()) { + buttonMonitor.removeButton(name); + } else { + QStringList paramList = hk.controller_keyseq.split(QStringLiteral("||")); + if (paramList.length() > 0) { + hk.button_device = + Input::CreateDevice(paramList.value(0).toStdString()); + if (paramList.length() > 1) { + hk.button_device2 = + Input::CreateDevice(paramList.value(1).toStdString()); + } + buttonMonitor.addButton(name, &hk); } } } - void HotkeyRegistry::LoadHotkeys() { // Make sure NOT to use a reference here because it would become invalid once we call // beginGroup() for (auto shortcut : UISettings::values.shortcuts) { Hotkey& hk = hotkey_groups[shortcut.group][shortcut.name]; - if (!shortcut.shortcut.keyseq.isEmpty()) { + if (!shortcut.shortcut.keyseq.isEmpty() || !shortcut.shortcut.controller_keyseq.isEmpty()) { hk.keyseq = QKeySequence::fromString(shortcut.shortcut.keyseq, QKeySequence::NativeText); hk.context = static_cast(shortcut.shortcut.context); + hk.controller_keyseq = shortcut.shortcut.controller_keyseq; } + UpdateControllerHotkey(shortcut.name, hk); + for (auto const& [_, hotkey_shortcut] : hk.shortcuts) { if (hotkey_shortcut) { hotkey_shortcut->disconnect(); @@ -63,3 +84,23 @@ Qt::ShortcutContext HotkeyRegistry::GetShortcutContext(const QString& group, Hotkey& hk = hotkey_groups[group][action]; return hk.context; } + +void HotkeyRegistry::SetAction(const QString& group, const QString& action_name, QAction* action) { + Hotkey& hk = hotkey_groups[group][action_name]; + hk.action = action; +} + +QString HotkeyRegistry::SequenceToString(QString controller_keyseq) { + if (controller_keyseq.isEmpty()) + return controller_keyseq; + QStringList keys = controller_keyseq.split(QStringLiteral("||")); + Common::ParamPackage p1 = Common::ParamPackage(keys.value(0).toStdString()); + QString output = QString::fromStdString(InputCommon::ButtonToText(p1)); + + if (keys.length() > 1) { + output.append(QStringLiteral(" + ")); + p1 = Common::ParamPackage(keys.value(1).toStdString()); + output.append(QString::fromStdString(InputCommon::ButtonToText(p1))); + } + return output; +} diff --git a/src/citra_qt/hotkeys.h b/src/citra_qt/hotkeys.h index 639d5db51..b597d7193 100644 --- a/src/citra_qt/hotkeys.h +++ b/src/citra_qt/hotkeys.h @@ -1,25 +1,41 @@ -// Copyright 2014 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. #pragma once #include +#include #include #include +#include "core/frontend/input.h" +#include "hotkey_monitor.h" class QDialog; class QSettings; class QShortcut; class QWidget; +struct Hotkey { + QKeySequence keyseq; + QString controller_keyseq; + std::map shortcuts; + Qt::ShortcutContext context = Qt::ApplicationShortcut; + std::unique_ptr button_device = nullptr; + std::unique_ptr button_device2 = nullptr; + QAction* action = nullptr; +}; + class HotkeyRegistry final { public: friend class ConfigureHotkeys; + friend class ConfigureControllerHotkeys; explicit HotkeyRegistry(); ~HotkeyRegistry(); + ControllerHotkeyMonitor buttonMonitor; + /** * Loads hotkeys from the settings file. * @@ -36,6 +52,11 @@ public: */ void SaveHotkeys(); + /** + * Updates the button devices for a hotkey based on the controller_keyseq value + */ + void UpdateControllerHotkey(QString name, Hotkey& hk); + /** * Returns a QShortcut object whose activated() signal can be connected to other QObjects' * slots. @@ -43,8 +64,8 @@ public: * @param group General group this hotkey belongs to (e.g. "Main Window", "Debugger"). * @param action Name of the action (e.g. "Start Emulation", "Load Image"). * @param widget Parent widget of the returned QShortcut. - * @warning If multiple QWidgets' call this function for the same action, the returned QShortcut - * will be the same. Thus, you shouldn't rely on the caller really being the + * @warning If multiple QWidgets' call this function for the same action, the returned + * QShortcut will be the same. Thus, you shouldn't rely on the caller really being the * QShortcut's parent. */ QShortcut* GetHotkey(const QString& group, const QString& action, QObject* widget); @@ -67,14 +88,23 @@ public: */ Qt::ShortcutContext GetShortcutContext(const QString& group, const QString& action); -private: - struct Hotkey { - QKeySequence keyseq; - QString controller_keyseq; - std::map shortcuts; - Qt::ShortcutContext context = Qt::WindowShortcut; - }; + /** + * Stores a QAction into the appropriate hotkey, for triggering by controller + * + * @param group General group this shortcut context belongs to + * @param action_name Name of the action + * @param action The QAction to store + */ + void SetAction(const QString& group, const QString& action_name, QAction* action); + /** + * Takes a controller keysequene for a hotkey and returns a readable string + * + * + */ + static QString SequenceToString(QString controller_keyseq); + +private: using HotkeyMap = std::map; using HotkeyGroupMap = std::map; diff --git a/src/citra_qt/uisettings.h b/src/citra_qt/uisettings.h index ef87b3fe3..1be719eb6 100644 --- a/src/citra_qt/uisettings.h +++ b/src/citra_qt/uisettings.h @@ -13,12 +13,14 @@ #include #include #include +#include "citra_qt/setting_qkeys.h" #include "common/settings.h" namespace UISettings { struct ContextualShortcut { QString keyseq; + QString controller_keyseq; int context; }; @@ -163,6 +165,11 @@ struct Values { Settings::Setting show_console{false, "showConsole"}; bool shortcut_already_warned = false; + + // this isn't really a UI setting, but it's a citra_qt exclusive setting so here we are + Settings::Setting controller_hotkey_maptype{ + Settings::InputMappingType::AllControllers, + Settings::QKeys::controller_hotkey_maptype.toStdString()}; }; extern Values values; diff --git a/src/citra_qt/util/sequence_dialog/controller_sequence_dialog.cpp b/src/citra_qt/util/sequence_dialog/controller_sequence_dialog.cpp new file mode 100644 index 000000000..8bd056979 --- /dev/null +++ b/src/citra_qt/util/sequence_dialog/controller_sequence_dialog.cpp @@ -0,0 +1,93 @@ +// Copyright Citra Emulator Project / Azahar Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include +#include +#include +#include +#include +#include "citra_qt/hotkeys.h" +#include "common/param_package.h" +#include "configuration/configure_hotkeys_controller.h" +#include "configuration/configure_input.h" +#include "controller_sequence_dialog.h" +#include "util/sequence_dialog/controller_sequence_dialog.h" + +ControllerSequenceDialog::ControllerSequenceDialog(QWidget* parent) + : QDialog(parent), poll_timer(std::make_unique()) { + setWindowTitle(tr("Press then release one or two controller buttons")); + + auto* const buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); + buttons->setCenterButtons(true); + + textBox = new QLabel(QStringLiteral("Waiting..."), this); + auto* const layout = new QVBoxLayout(this); + layout->addWidget(textBox); + layout->addWidget(buttons); + + connect(buttons, &QDialogButtonBox::accepted, this, &QDialog::accept); + connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject); + + LaunchPollers(); +} + +ControllerSequenceDialog::~ControllerSequenceDialog() = default; + +QString ControllerSequenceDialog::GetSequence() { + return key_sequence; +} + +void ControllerSequenceDialog::closeEvent(QCloseEvent*) { + reject(); +} + +bool ControllerSequenceDialog::focusNextPrevChild(bool next) { + return false; +} + +void ControllerSequenceDialog::LaunchPollers() { + device_pollers = InputCommon::Polling::GetPollers(InputCommon::Polling::DeviceType::Button); + + for (auto& poller : device_pollers) { + poller->Start(); + } + + connect(poll_timer.get(), &QTimer::timeout, this, [this, downCount = 0]() mutable { + Common::ParamPackage params; + for (auto& poller : device_pollers) { + params = poller->GetNextInput(); + if (params.Has("engine")) { + if (params.Has("down")) { + downCount++; + if (downCount == 1) { + // either the first press, or the first new press + key_sequence = QStringLiteral(""); + params1 = params; + params2 = Common::ParamPackage(); + key_sequence = QString::fromStdString(params1.Serialize()); + textBox->setText(HotkeyRegistry::SequenceToString(key_sequence) + + QStringLiteral("...")); + } else if (downCount == 2 && !params2.Has("engine")) { + // this is a second button, currently only one button saved, so save it + params2 = params; + key_sequence = QString::fromStdString(params1.Serialize() + "||" + + params2.Serialize()); + textBox->setText(HotkeyRegistry::SequenceToString(key_sequence)); + } + // if downCount == 3 or more, just ignore them - we have saved the first two + // presses + } else { // button release + downCount--; + if (downCount <= 0) { + // buttons all released, show the saved sequence and prepare to start again + textBox->setText(HotkeyRegistry::SequenceToString(key_sequence)); + params1 = Common::ParamPackage(); + params2 = Common::ParamPackage(); + } + } + } + } + }); + poll_timer->start(100); +} \ No newline at end of file diff --git a/src/citra_qt/util/sequence_dialog/controller_sequence_dialog.h b/src/citra_qt/util/sequence_dialog/controller_sequence_dialog.h new file mode 100644 index 000000000..20a28ff1e --- /dev/null +++ b/src/citra_qt/util/sequence_dialog/controller_sequence_dialog.h @@ -0,0 +1,31 @@ +// Copyright Citra Emulator Project / Azahar Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include +#include +#include +#include "common/param_package.h" +#include "input_common/main.h" + +class ControllerSequenceDialog : public QDialog { + Q_OBJECT + +public: + explicit ControllerSequenceDialog(QWidget* parent = nullptr); + ~ControllerSequenceDialog(); + + QString GetSequence(); + void closeEvent(QCloseEvent*) override; + +private: + void LaunchPollers(); + QLabel* textBox; + QString key_sequence; + Common::ParamPackage params1, params2; + bool focusNextPrevChild(bool next) override; + std::vector> device_pollers; + std::unique_ptr poll_timer; +}; diff --git a/src/citra_qt/util/sequence_dialog/sequence_dialog.cpp b/src/citra_qt/util/sequence_dialog/sequence_dialog.cpp index 72d99b6bb..1c510b91d 100644 --- a/src/citra_qt/util/sequence_dialog/sequence_dialog.cpp +++ b/src/citra_qt/util/sequence_dialog/sequence_dialog.cpp @@ -1,4 +1,4 @@ -// Copyright 2018 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. diff --git a/src/citra_qt/util/sequence_dialog/sequence_dialog.h b/src/citra_qt/util/sequence_dialog/sequence_dialog.h index ba8843d92..3e09f2212 100644 --- a/src/citra_qt/util/sequence_dialog/sequence_dialog.h +++ b/src/citra_qt/util/sequence_dialog/sequence_dialog.h @@ -1,4 +1,4 @@ -// Copyright 2018 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. diff --git a/src/common/param_package.h b/src/common/param_package.h index 7db597b42..ed9631f92 100644 --- a/src/common/param_package.h +++ b/src/common/param_package.h @@ -1,7 +1,6 @@ -// Copyright 2017 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. - #pragma once #include @@ -23,7 +22,6 @@ public: ParamPackage& operator=(const ParamPackage& other) = default; ParamPackage& operator=(ParamPackage&& other) = default; - [[nodiscard]] std::string Serialize() const; [[nodiscard]] std::string Get(const std::string& key, const std::string& default_value) const; [[nodiscard]] int Get(const std::string& key, int default_value) const; diff --git a/src/common/settings.h b/src/common/settings.h index 90922bcff..ec1b2bad8 100644 --- a/src/common/settings.h +++ b/src/common/settings.h @@ -49,6 +49,8 @@ enum class LayoutOption : u32 { // Shouldn't these have set numbers to prevent l CustomLayout, }; +enum class InputMappingType : u8 { AllControllers, Guid, GuidPort }; + /** Defines the layout option for mobile portrait */ enum class PortraitLayoutOption : u32 { // formerly mobile portrait @@ -449,6 +451,7 @@ struct InputProfile { std::string udp_input_address; u16 udp_input_port; u8 udp_pad_index; + InputMappingType maptype = Settings::InputMappingType::GuidPort; }; struct TouchFromButtonMap { diff --git a/src/input_common/CMakeLists.txt b/src/input_common/CMakeLists.txt index 70d36cbfd..39ac39f79 100644 --- a/src/input_common/CMakeLists.txt +++ b/src/input_common/CMakeLists.txt @@ -23,6 +23,7 @@ add_library(input_common STATIC if(ENABLE_SDL2) target_sources(input_common PRIVATE sdl/sdl_impl.cpp + sdl/sdl_joystick.cpp sdl/sdl_impl.h ) target_link_libraries(input_common PRIVATE SDL2::SDL2) diff --git a/src/input_common/main.cpp b/src/input_common/main.cpp index bcff11d20..3288e6979 100644 --- a/src/input_common/main.cpp +++ b/src/input_common/main.cpp @@ -1,4 +1,4 @@ -// Copyright 2017 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -70,6 +70,104 @@ void Shutdown() { udp.reset(); } +std::string ButtonToText(const Common::ParamPackage& param) { + if (!param.Has("engine")) { + return "[not set]"; + } + const auto engine_str = param.Get("engine", ""); + // keyboard should be handled by the frontend + if (engine_str == "keyboard") { + return "keyboard code " + param.Get("code", 0); + } + + if (engine_str == "sdl") { + if (param.Has("hat")) { + return "Hat " + param.Get("hat", "") + " " + param.Get("direction", ""); + } else if (param.Has("button")) { + if (param.Get("name", "") != "") + return param.Get("name", ""); + else + return "Button " + param.Get("button", ""); + + } else if (param.Has("axis")) { + auto name = param.Get("name", ""); + if (name == "LT" || name == "RT") + return name; + else if (name != "") + return name + param.Get("direction", ""); + else + return "Axis " + param.Get("axis", "") + param.Get("direction", ""); + } + + return {}; + } + + if (engine_str == "gcpad") { + if (param.Has("axis")) { + const auto axis_str = param.Get("axis", ""); + const auto direction_str = param.Get("direction", ""); + + return "GC Axis " + axis_str + direction_str; + } + if (param.Has("button")) { + const auto button = int(std::log2(param.Get("button", 0))); + return "GC Button " + button; + } + return "keyboard code " + param.Get("code", 0); + } + + return "[unknown]"; +} + +std::string AnalogToText(const Common::ParamPackage& param, const std::string& dir) { + if (!param.Has("engine")) { + return "[not set]"; + } + + const auto engine_str = param.Get("engine", ""); + if (engine_str == "analog_from_button") { + return ButtonToText(Common::ParamPackage{param.Get(dir, "")}); + } + + const auto axis_x_str = param.Get("axis_x", ""); + const auto axis_y_str = param.Get("axis_y", ""); + const auto name_x_str = param.Get("name_x", ""); + const auto name_y_str = param.Get("name_y", ""); + static const auto plus_str = "+"; + static const auto minus_str = "-"; + if (engine_str == "sdl" || engine_str == "gcpad") { + if (dir == "modifier") { + return "[unused]"; + } + if (dir == "left") { + if (name_x_str == "") + return "Axis " + axis_x_str + minus_str; + else + return name_x_str + minus_str; + } + if (dir == "right") { + if (name_x_str == "") + return "Axis " + axis_x_str + plus_str; + else + return name_x_str + plus_str; + } + if (dir == "up") { + if (name_y_str == "") + return "Axis " + axis_y_str + plus_str; + else + return name_y_str + plus_str; + } + if (dir == "down") { + if (name_y_str == "") + return "Axis " + axis_y_str + minus_str; + else + return name_y_str + plus_str; + } + return {}; + } + return "[unknown]"; +} + Keyboard* GetKeyboard() { return keyboard.get(); } @@ -104,8 +202,8 @@ Common::ParamPackage GetControllerButtonBinds(const Common::ParamPackage& params const auto native_button{static_cast(button)}; const auto engine{params.Get("engine", "")}; if (engine == "sdl") { - return dynamic_cast(sdl.get())->GetSDLControllerButtonBindByGUID( - params.Get("guid", "0"), params.Get("port", 0), native_button); + return dynamic_cast(sdl.get())->GetSDLControllerButtonBind(params, + native_button); } #ifdef ENABLE_GCADAPTER if (engine == "gcpad") { diff --git a/src/input_common/main.h b/src/input_common/main.h index bbc7669e7..b7a3b056e 100644 --- a/src/input_common/main.h +++ b/src/input_common/main.h @@ -37,6 +37,9 @@ std::string GenerateKeyboardParam(int key_code); std::string GenerateAnalogParamFromKeys(int key_up, int key_down, int key_left, int key_right, int key_modifier, float modifier_scale); +std::string AnalogToText(const Common::ParamPackage& param, const std::string& dir); +std::string ButtonToText(const Common::ParamPackage& param); + Common::ParamPackage GetControllerButtonBinds(const Common::ParamPackage& params, int button); Common::ParamPackage GetControllerAnalogBinds(const Common::ParamPackage& params, int analog); diff --git a/src/input_common/sdl/sdl_impl.cpp b/src/input_common/sdl/sdl_impl.cpp index 71f2be379..29a972f74 100644 --- a/src/input_common/sdl/sdl_impl.cpp +++ b/src/input_common/sdl/sdl_impl.cpp @@ -111,7 +111,35 @@ static Common::ParamPackage SDLEventToButtonParamPackage(SDLState& state, const static int SDLEventWatcher(void* userdata, SDL_Event* event) { SDLState* sdl_state = reinterpret_cast(userdata); - // Don't handle the event if we are configuring + switch (event->type) { + case SDL_JOYAXISMOTION: + case SDL_JOYHATMOTION: + case SDL_JOYDEVICEREMOVED: { + auto joystick = sdl_state->GetSDLJoystickBySDLID(event->jdevice.which); + if (joystick && joystick->GetSDLGameController()) { + return 0; + } + break; + } + + case SDL_JOYBUTTONDOWN: + case SDL_JOYBUTTONUP: { + // only let these through if they are nonstandard buttons, like back buttons + auto joystick = sdl_state->GetSDLJoystickBySDLID(event->jdevice.which); + if (joystick && joystick->GetSDLGameController() && + joystick->IsButtonMappedToController(event->jbutton.button)) { + return 0; + } + break; + } + + case SDL_JOYDEVICEADDED: + if (SDL_IsGameController(event->jdevice.which)) { + return 0; + } + break; + } + // deal if (sdl_state->polling) { sdl_state->event_queue.Push(*event); } else { @@ -141,330 +169,148 @@ constexpr std::array + nintendo_to_3ds_mapping = {{ + SDL_CONTROLLER_BUTTON_A, + SDL_CONTROLLER_BUTTON_B, + SDL_CONTROLLER_BUTTON_X, + SDL_CONTROLLER_BUTTON_Y, + SDL_CONTROLLER_BUTTON_DPAD_UP, + SDL_CONTROLLER_BUTTON_DPAD_DOWN, + SDL_CONTROLLER_BUTTON_DPAD_LEFT, + SDL_CONTROLLER_BUTTON_DPAD_RIGHT, + SDL_CONTROLLER_BUTTON_LEFTSHOULDER, + SDL_CONTROLLER_BUTTON_RIGHTSHOULDER, + SDL_CONTROLLER_BUTTON_START, + SDL_CONTROLLER_BUTTON_BACK, + SDL_CONTROLLER_BUTTON_INVALID, + SDL_CONTROLLER_BUTTON_INVALID, + SDL_CONTROLLER_BUTTON_INVALID, + SDL_CONTROLLER_BUTTON_INVALID, + SDL_CONTROLLER_BUTTON_GUIDE, + SDL_CONTROLLER_BUTTON_INVALID, + }}; +const std::map axis_names = { + {SDL_CONTROLLER_AXIS_LEFTX, "Left Stick X"}, + {SDL_CONTROLLER_AXIS_LEFTY, "Left Stick Y"}, + {SDL_CONTROLLER_AXIS_RIGHTX, "Right Stick X"}, + {SDL_CONTROLLER_AXIS_RIGHTY, "Right Stick Y"}, + {SDL_CONTROLLER_AXIS_INVALID, ""}, + {SDL_CONTROLLER_AXIS_TRIGGERLEFT, "Left Trigger"}, + {SDL_CONTROLLER_AXIS_TRIGGERRIGHT, "Right Trigger"}}; + +const std::map button_names = { + {SDL_CONTROLLER_BUTTON_A, "A / ✖"}, + {SDL_CONTROLLER_BUTTON_B, "B / ●"}, + {SDL_CONTROLLER_BUTTON_X, "X / ■"}, + {SDL_CONTROLLER_BUTTON_Y, "Y / ▲"}, + {SDL_CONTROLLER_BUTTON_BACK, "Back/Select"}, + {SDL_CONTROLLER_BUTTON_GUIDE, "Guide/Home"}, + {SDL_CONTROLLER_BUTTON_START, "Start"}, + {SDL_CONTROLLER_BUTTON_LEFTSTICK, "LS Click"}, + {SDL_CONTROLLER_BUTTON_RIGHTSTICK, "RS Click"}, + {SDL_CONTROLLER_BUTTON_LEFTSHOULDER, "LB"}, + {SDL_CONTROLLER_BUTTON_RIGHTSHOULDER, "RB"}, + {SDL_CONTROLLER_BUTTON_DPAD_UP, "D-Pad Up"}, + {SDL_CONTROLLER_BUTTON_DPAD_DOWN, "D-Pad Down"}, + {SDL_CONTROLLER_BUTTON_DPAD_LEFT, "D-Pad Left"}, + {SDL_CONTROLLER_BUTTON_DPAD_RIGHT, "D-Pad Right"}, + {SDL_CONTROLLER_BUTTON_MISC1, "Misc (Share/Mute)"}, + {SDL_CONTROLLER_BUTTON_PADDLE1, "Paddle 1"}, + {SDL_CONTROLLER_BUTTON_PADDLE2, "Paddle 2"}, + {SDL_CONTROLLER_BUTTON_PADDLE3, "Paddle 3"}, + {SDL_CONTROLLER_BUTTON_PADDLE4, "Paddle 4"}, + {SDL_CONTROLLER_BUTTON_TOUCHPAD, "Touchpad"}, + {SDL_CONTROLLER_BUTTON_INVALID, ""}}; struct SDLJoystickDeleter { void operator()(SDL_Joystick* object) { SDL_JoystickClose(object); } }; -class SDLJoystick { -public: - SDLJoystick(std::string guid_, int port_, SDL_Joystick* joystick, - SDL_GameController* game_controller) - : guid{std::move(guid_)}, port{port_}, sdl_joystick{joystick, &SDL_JoystickClose}, - sdl_controller{game_controller, &SDL_GameControllerClose} { - EnableMotion(); - } - - void EnableMotion() { - if (!sdl_controller) { - return; - } -#if SDL_VERSION_ATLEAST(2, 0, 14) - SDL_GameController* controller = sdl_controller.get(); - - if (HasMotion()) { - SDL_GameControllerSetSensorEnabled(controller, SDL_SENSOR_ACCEL, SDL_FALSE); - SDL_GameControllerSetSensorEnabled(controller, SDL_SENSOR_GYRO, SDL_FALSE); - } - has_accel = SDL_GameControllerHasSensor(controller, SDL_SENSOR_ACCEL) == SDL_TRUE; - has_gyro = SDL_GameControllerHasSensor(controller, SDL_SENSOR_GYRO) == SDL_TRUE; - if (has_accel) { - SDL_GameControllerSetSensorEnabled(controller, SDL_SENSOR_ACCEL, SDL_TRUE); - } - if (has_gyro) { - SDL_GameControllerSetSensorEnabled(controller, SDL_SENSOR_GYRO, SDL_TRUE); - } -#endif - } - - bool HasMotion() const { - return has_gyro || has_accel; - } - - void SetTouchpad(float x, float y, int touchpad, bool down) { - std::lock_guard lock{mutex}; - state.touchpad[touchpad] = std::make_tuple(x, y, down); - } - - void SetButton(int button, bool value) { - std::lock_guard lock{mutex}; - state.buttons[button] = value; - } - - bool GetButton(int button) const { - std::lock_guard lock{mutex}; - return state.buttons.at(button); - } - - void SetAxis(int axis, Sint16 value) { - std::lock_guard lock{mutex}; - state.axes[axis] = value; - } - - float GetAxis(int axis) const { - std::lock_guard lock{mutex}; - return state.axes.at(axis) / 32767.0f; - } - - std::tuple GetAnalog(int axis_x, int axis_y) const { - float x = GetAxis(axis_x); - float y = GetAxis(axis_y); - y = -y; // 3DS uses an y-axis inverse from SDL - - // Make sure the coordinates are in the unit circle, - // otherwise normalize it. - float r = x * x + y * y; - if (r > 1.0f) { - r = std::sqrt(r); - x /= r; - y /= r; - } - - return std::make_tuple(x, y); - } - - void SetHat(int hat, Uint8 direction) { - std::lock_guard lock{mutex}; - state.hats[hat] = direction; - } - - bool GetHatDirection(int hat, Uint8 direction) const { - std::lock_guard lock{mutex}; - return (state.hats.at(hat) & direction) != 0; - } - - void SetAccel(const float x, const float y, const float z) { - std::lock_guard lock{mutex}; - state.accel.x = x; - state.accel.y = y; - state.accel.z = z; - } - void SetGyro(const float pitch, const float yaw, const float roll) { - std::lock_guard lock{mutex}; - state.gyro.x = pitch; - state.gyro.y = yaw; - state.gyro.z = roll; - } - std::tuple, Common::Vec3> GetMotion() const { - std::lock_guard lock{mutex}; - return std::make_tuple(state.accel, state.gyro); - } - - std::tuple GetTouch(int pad) { - std::lock_guard lock{mutex}; - return state.touchpad[pad]; - } - - /** - * The guid of the joystick - */ - const std::string& GetGUID() const { - return guid; - } - - /** - * The number of joystick from the same type that were connected before this joystick - */ - int GetPort() const { - return port; - } - - SDL_Joystick* GetSDLJoystick() const { - return sdl_joystick.get(); - } - - SDL_GameController* GetSDLGameController() const { - return sdl_controller.get(); - } - - void SetSDLJoystick(SDL_Joystick* joystick, SDL_GameController* controller) { - sdl_joystick.reset(joystick); - sdl_controller.reset(controller); - } - -private: - struct State { - std::unordered_map buttons; - std::unordered_map axes; - std::unordered_map hats; - Common::Vec3 accel; - Common::Vec3 gyro; - std::unordered_map> touchpad; - } state; - std::string guid; - int port; - bool has_gyro{false}; - bool has_accel{false}; - std::unique_ptr sdl_joystick; - std::unique_ptr sdl_controller; - mutable std::mutex mutex; -}; - -struct SDLGameControllerDeleter { - void operator()(SDL_GameController* object) { - SDL_GameControllerClose(object); - } -}; -class SDLGameController { -public: - SDLGameController(std::string guid_, int port_, SDL_GameController* controller) - : guid{std::move(guid_)}, port{port_}, sdl_controller{controller} {} - - /** - * The guid of the joystick/controller - */ - const std::string& GetGUID() const { - return guid; - } - - /** - * The number of joystick from the same type that were connected before this joystick - */ - int GetPort() const { - return port; - } - - SDL_GameController* GetSDLGameController() const { - return sdl_controller.get(); - } - - void SetSDLGameController(SDL_GameController* controller) { - sdl_controller = std::unique_ptr(controller); - } - -private: - std::string guid; - int port; - std::unique_ptr sdl_controller; -}; - /** - * Get the nth joystick with the corresponding GUID + * Return a list of matching joysticks. All joysticks with GUID are reported, port will be handled + * elsewhere. */ -std::shared_ptr SDLState::GetSDLJoystickByGUID(const std::string& guid, int port) { +std::shared_ptr>> SDLState::GetJoysticksByGUID( + const std::string& guid) { std::lock_guard lock{joystick_map_mutex}; - const auto it = joystick_map.find(guid); - if (it != joystick_map.end()) { - while (it->second.size() <= static_cast(port)) { - auto joystick = std::make_shared(guid, static_cast(it->second.size()), - nullptr, nullptr); - it->second.emplace_back(std::move(joystick)); + if (guid.empty() || guid == "0") { + return joystick_vector; + } else { + const auto it = joystick_map.find(guid); + if (it != joystick_map.end()) { + return it->second; } - return it->second[static_cast(port)]; + // if no joysticks with this GUID exists, add a fake one to avoid crashes (yuck) + auto vec = std::make_shared>>(); + auto joystick = std::make_shared(guid, 0, nullptr, nullptr); + vec->emplace_back(joystick); + joystick_map[guid] = vec; + return vec; } - auto joystick = std::make_shared(guid, 0, nullptr, nullptr); - return joystick_map[guid].emplace_back(std::move(joystick)); } /** - * Check how many identical joysticks (by guid) were connected before the one with sdl_id and so tie - * it to a SDLJoystick with the same guid and that port + * Find the most up-to-date SDLJoystick object from an instance id */ std::shared_ptr SDLState::GetSDLJoystickBySDLID(SDL_JoystickID sdl_id) { - auto sdl_joystick = SDL_JoystickFromInstanceID(sdl_id); - const std::string guid = GetGUID(sdl_joystick); - + // rewriting this method, the old version was more fragile std::lock_guard lock{joystick_map_mutex}; - auto map_it = joystick_map.find(guid); - if (map_it == joystick_map.end()) { - return nullptr; + for (auto& [guid, joystick_list] : joystick_map) { + for (auto& joystick : *joystick_list) { + SDL_Joystick* sdl_joy = joystick->GetSDLJoystick(); + if (sdl_joy && SDL_JoystickInstanceID(sdl_joy) == sdl_id) { + return joystick; + } + } } - const auto vec_it = std::find_if(map_it->second.begin(), map_it->second.end(), - [&sdl_joystick](const auto& joystick) { - return joystick->GetSDLJoystick() == sdl_joystick; - }); - - if (vec_it == map_it->second.end()) { - return nullptr; - } - - return *vec_it; + return nullptr; } -Common::ParamPackage SDLState::GetSDLControllerButtonBindByGUID( - const std::string& guid, int port, Settings::NativeButton::Values button) { +/** + * Return the button binds param package for the button assuming the params passed in is for the + * n3ds "A" button to determine whether this is xbox or nintendo layout. If the passed in button + * is neither A nor B, default to xbox layout as the most common on desktop. + */ +Common::ParamPackage SDLState::GetSDLControllerButtonBind( + const Common::ParamPackage a_button_params, Settings::NativeButton::Values button) { + auto guid = a_button_params.Get("guid", 0); + auto port = a_button_params.Get("port", 0); + auto a_button = a_button_params.Get("button", -1); + auto api = a_button_params.Get("api", "joystick"); + // for xinputs, the "A" or right button would normally register as the B button. But if the + // user is either using a nintendo-layout controller or using xinput but would rather the labels + // match than the icons, they can press the button that appears as "A" and trigger nintendo + // automap + bool is_nintendo_layout = + api == "controller" && a_button == SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_A; + Common::ParamPackage params({{"engine", "sdl"}}); params.Set("guid", guid); params.Set("port", port); - SDL_GameController* controller = GetSDLJoystickByGUID(guid, port)->GetSDLGameController(); - SDL_GameControllerButtonBind button_bind; - if (!controller) { - LOG_WARNING(Input, "failed to open controller {}", guid); + auto mapped_button = is_nintendo_layout ? nintendo_to_3ds_mapping[static_cast(button)] + : xinput_to_3ds_mapping[static_cast(button)]; + + params.Set("api", "controller"); + if (button == Settings::NativeButton::Values::ZL) { + params.Set("axis", SDL_CONTROLLER_AXIS_TRIGGERLEFT); + params.Set("name", axis_names.at(SDL_CONTROLLER_AXIS_TRIGGERLEFT)); + params.Set("direction", "+"); + params.Set("threshold", 0.5f); + } else if (button == Settings::NativeButton::Values::ZR) { + params.Set("axis", SDL_CONTROLLER_AXIS_TRIGGERRIGHT); + params.Set("name", axis_names.at(SDL_CONTROLLER_AXIS_TRIGGERRIGHT)); + params.Set("direction", "+"); + params.Set("threshold", 0.5f); + } else if (mapped_button == SDL_CONTROLLER_BUTTON_INVALID) { return {{}}; - } - - auto mapped_button = xinput_to_3ds_mapping[static_cast(button)]; - if (mapped_button == SDL_CONTROLLER_BUTTON_INVALID) { - if (button == Settings::NativeButton::Values::ZL) { - button_bind = - SDL_GameControllerGetBindForAxis(controller, SDL_CONTROLLER_AXIS_TRIGGERLEFT); - } else if (button == Settings::NativeButton::Values::ZR) { - button_bind = - SDL_GameControllerGetBindForAxis(controller, SDL_CONTROLLER_AXIS_TRIGGERRIGHT); - } else { - return {{}}; - } } else { - button_bind = SDL_GameControllerGetBindForButton(controller, mapped_button); - } - - switch (button_bind.bindType) { - case SDL_CONTROLLER_BINDTYPE_BUTTON: - params.Set("button", button_bind.value.button); - break; - case SDL_CONTROLLER_BINDTYPE_HAT: - params.Set("hat", button_bind.value.hat.hat); - switch (button_bind.value.hat.hat_mask) { - case SDL_HAT_UP: - params.Set("direction", "up"); - break; - case SDL_HAT_DOWN: - params.Set("direction", "down"); - break; - case SDL_HAT_LEFT: - params.Set("direction", "left"); - break; - case SDL_HAT_RIGHT: - params.Set("direction", "right"); - break; - default: - return {{}}; - } - break; - case SDL_CONTROLLER_BINDTYPE_AXIS: - params.Set("axis", button_bind.value.axis); - -#if SDL_VERSION_ATLEAST(2, 0, 6) - { - if (mapped_button != SDL_CONTROLLER_BUTTON_INVALID) { - const SDL_ExtendedGameControllerBind extended_bind = - controller->bindings[mapped_button]; - if (extended_bind.input.axis.axis_max < extended_bind.input.axis.axis_min) { - params.Set("direction", "-"); - } else { - params.Set("direction", "+"); - } - params.Set("threshold", (extended_bind.input.axis.axis_min + - (extended_bind.input.axis.axis_max - - extended_bind.input.axis.axis_min) / - 2.0f) / - SDL_JOYSTICK_AXIS_MAX); - } - } -#else - params.Set("direction", "+"); // lacks extended_bind, so just a guess -#endif - break; - case SDL_CONTROLLER_BINDTYPE_NONE: - LOG_WARNING(Input, "Button not bound: {}", Settings::NativeButton::mapping[button]); - return {{}}; - default: - LOG_WARNING(Input, "unknown SDL bind type {}", button_bind.bindType); - return {{}}; + params.Set("button", mapped_button); + params.Set("name", button_names.at(mapped_button)); } return params; @@ -475,41 +321,39 @@ Common::ParamPackage SDLState::GetSDLControllerAnalogBindByGUID( Common::ParamPackage params({{"engine", "sdl"}}); params.Set("guid", guid); params.Set("port", port); - SDL_GameController* controller = GetSDLJoystickByGUID(guid, port)->GetSDLGameController(); - SDL_GameControllerButtonBind button_bind_x; - SDL_GameControllerButtonBind button_bind_y; - if (!controller) { - LOG_WARNING(Input, "failed to open controller {}", guid); - return {{}}; - } + SDL_GameControllerAxis button_bind_x; + SDL_GameControllerAxis button_bind_y; if (analog == Settings::NativeAnalog::Values::CirclePad) { - button_bind_x = SDL_GameControllerGetBindForAxis(controller, SDL_CONTROLLER_AXIS_LEFTX); - button_bind_y = SDL_GameControllerGetBindForAxis(controller, SDL_CONTROLLER_AXIS_LEFTY); + button_bind_x = SDL_CONTROLLER_AXIS_LEFTX; + button_bind_y = SDL_CONTROLLER_AXIS_LEFTY; } else if (analog == Settings::NativeAnalog::Values::CStick) { - button_bind_x = SDL_GameControllerGetBindForAxis(controller, SDL_CONTROLLER_AXIS_RIGHTX); - button_bind_y = SDL_GameControllerGetBindForAxis(controller, SDL_CONTROLLER_AXIS_RIGHTY); + button_bind_x = SDL_CONTROLLER_AXIS_RIGHTX; + button_bind_y = SDL_CONTROLLER_AXIS_RIGHTY; } else { LOG_WARNING(Input, "analog value out of range {}", analog); return {{}}; } - - if (button_bind_x.bindType != SDL_CONTROLLER_BINDTYPE_AXIS || - button_bind_y.bindType != SDL_CONTROLLER_BINDTYPE_AXIS) { - return {{}}; - } - params.Set("axis_x", button_bind_x.value.axis); - params.Set("axis_y", button_bind_y.value.axis); + params.Set("api", "controller"); + params.Set("axis_x", button_bind_x); + params.Set("name_x", axis_names.at(button_bind_x)); + params.Set("axis_y", button_bind_y); + params.Set("name_y", axis_names.at(button_bind_y)); return params; } void SDLState::InitJoystick(int joystick_index) { - SDL_Joystick* sdl_joystick = SDL_JoystickOpen(joystick_index); + SDL_Joystick* sdl_joystick = nullptr; SDL_GameController* sdl_gamecontroller = nullptr; if (SDL_IsGameController(joystick_index)) { sdl_gamecontroller = SDL_GameControllerOpen(joystick_index); + if (sdl_gamecontroller) { + sdl_joystick = SDL_GameControllerGetJoystick(sdl_gamecontroller); + } + } else { + sdl_joystick = SDL_JoystickOpen(joystick_index); } if (!sdl_joystick) { @@ -523,66 +367,42 @@ void SDLState::InitJoystick(int joystick_index) { if (joystick_map.find(guid) == joystick_map.end()) { auto joystick = std::make_shared(guid, 0, sdl_joystick, sdl_gamecontroller); joystick->EnableMotion(); - joystick_map[guid].emplace_back(std::move(joystick)); + auto vec = std::make_shared>>(); + vec->emplace_back(joystick); + joystick_map[guid] = vec; + joystick_vector->emplace_back(joystick); return; } auto& joystick_guid_list = joystick_map[guid]; - const auto it = std::find_if(joystick_guid_list.begin(), joystick_guid_list.end(), + const auto it = std::find_if(joystick_guid_list->begin(), joystick_guid_list->end(), [](const auto& joystick) { return !joystick->GetSDLJoystick(); }); - if (it != joystick_guid_list.end()) { + if (it != joystick_guid_list->end()) { (*it)->SetSDLJoystick(sdl_joystick, sdl_gamecontroller); (*it)->EnableMotion(); + joystick_vector->emplace_back(*it); return; } - const int port = static_cast(joystick_guid_list.size()); + const int port = static_cast(joystick_guid_list->size()); auto joystick = std::make_shared(guid, port, sdl_joystick, sdl_gamecontroller); joystick->EnableMotion(); - joystick_guid_list.emplace_back(std::move(joystick)); + joystick_guid_list->emplace_back(joystick); + joystick_vector->emplace_back(joystick); } -void SDLState::CloseJoystick(SDL_Joystick* sdl_joystick) { - const auto guid = GetGUID(sdl_joystick); - - std::scoped_lock lock{joystick_map_mutex}; - // This call to guid is safe since the joystick is guaranteed to be in the map - const auto& joystick_guid_list = joystick_map[guid]; - const auto joystick_it = std::find_if(joystick_guid_list.begin(), joystick_guid_list.end(), - [&sdl_joystick](const auto& joystick) { - return joystick->GetSDLJoystick() == sdl_joystick; - }); - - if (joystick_it != joystick_guid_list.end()) { - (*joystick_it)->SetSDLJoystick(nullptr, nullptr); +void SDLState::CloseJoystick(SDL_JoystickID instance_id) { + auto joystick = GetSDLJoystickBySDLID(instance_id); + if (joystick) { + LOG_DEBUG(Input, "Closing joystick with instance ID {}", instance_id); + joystick->SetSDLJoystick(nullptr, nullptr); + } else { + LOG_DEBUG(Input, "Joystick with instance ID {} already closed or not found", instance_id); } } void SDLState::HandleGameControllerEvent(const SDL_Event& event) { switch (event.type) { - case SDL_JOYBUTTONUP: { - if (auto joystick = GetSDLJoystickBySDLID(event.jbutton.which)) { - joystick->SetButton(event.jbutton.button, false); - } - break; - } - case SDL_JOYBUTTONDOWN: { - if (auto joystick = GetSDLJoystickBySDLID(event.jbutton.which)) { - joystick->SetButton(event.jbutton.button, true); - } - break; - } - case SDL_JOYHATMOTION: { - if (auto joystick = GetSDLJoystickBySDLID(event.jhat.which)) { - joystick->SetHat(event.jhat.hat, event.jhat.value); - } - break; - } - case SDL_JOYAXISMOTION: { - if (auto joystick = GetSDLJoystickBySDLID(event.jaxis.which)) { - joystick->SetAxis(event.jaxis.axis, event.jaxis.value); - } - break; - } + #if SDL_VERSION_ATLEAST(2, 0, 14) case SDL_CONTROLLERSENSORUPDATE: { if (auto joystick = GetSDLJoystickBySDLID(event.csensor.which)) { @@ -615,12 +435,16 @@ void SDLState::HandleGameControllerEvent(const SDL_Event& event) { } break; #endif + // this event will get called twice + case SDL_CONTROLLERDEVICEREMOVED: case SDL_JOYDEVICEREMOVED: - LOG_DEBUG(Input, "Joystick removed with Instance_ID {}", event.jdevice.which); - CloseJoystick(SDL_JoystickFromInstanceID(event.jdevice.which)); + LOG_DEBUG(Input, "Device removed with Instance_ID {}", event.jdevice.which); + CloseJoystick(event.jdevice.which); break; + + case SDL_CONTROLLERDEVICEADDED: case SDL_JOYDEVICEADDED: - LOG_DEBUG(Input, "Joystick connected with device index {}", event.jdevice.which); + LOG_DEBUG(Input, "Device added with index {}", event.jdevice.which); InitJoystick(event.jdevice.which); break; } @@ -629,78 +453,136 @@ void SDLState::HandleGameControllerEvent(const SDL_Event& event) { void SDLState::CloseJoysticks() { std::lock_guard lock{joystick_map_mutex}; joystick_map.clear(); + joystick_vector->clear(); } class SDLButton final : public Input::ButtonDevice { public: - explicit SDLButton(std::shared_ptr joystick_, int button_) - : joystick(std::move(joystick_)), button(button_) {} + explicit SDLButton(std::shared_ptr>> joysticks_, + int button_, int port_ = -1, bool isController_ = true) + : joysticks(joysticks_), button(button_), isController(isController_), port(port_) {} bool GetStatus() const override { - return joystick->GetButton(button); + if (port >= 0 && joysticks && joysticks->size() > port && joysticks->at(port)) { + return joysticks->at(port)->GetButton(button, isController); + } + for (const auto& joystick : *joysticks) { + if (joystick && joystick->GetButton(button, isController)) { + return true; + } + } + return false; } private: - std::shared_ptr joystick; + std::shared_ptr>> joysticks; int button; + bool isController = true; + int port; }; class SDLDirectionButton final : public Input::ButtonDevice { public: - explicit SDLDirectionButton(std::shared_ptr joystick_, int hat_, Uint8 direction_) - : joystick(std::move(joystick_)), hat(hat_), direction(direction_) {} + explicit SDLDirectionButton( + std::shared_ptr>> joysticks_, int hat_, + Uint8 direction_, int port_ = -1) + : joysticks(joysticks_), hat(hat_), direction(direction_), port(port_) {} bool GetStatus() const override { - return joystick->GetHatDirection(hat, direction); + if (port >= 0 && joysticks && joysticks->size() > port && joysticks->at(port)) { + return joysticks->at(port)->GetHatDirection(hat, direction); + } + for (const auto& joystick : *joysticks) { + if (joystick && joystick->GetHatDirection(hat, direction)) + return true; + } + return false; } private: - std::shared_ptr joystick; + std::shared_ptr>> joysticks; int hat; Uint8 direction; + int port; }; class SDLAxisButton final : public Input::ButtonDevice { public: - explicit SDLAxisButton(std::shared_ptr joystick_, int axis_, float threshold_, - bool trigger_if_greater_) - : joystick(std::move(joystick_)), axis(axis_), threshold(threshold_), - trigger_if_greater(trigger_if_greater_) {} + explicit SDLAxisButton(std::shared_ptr>> joysticks_, + int axis_, float threshold_, bool trigger_if_greater_, int port_ = -1, + bool isController_ = true) + : joysticks(joysticks_), axis(axis_), threshold(threshold_), + trigger_if_greater(trigger_if_greater_), isController(isController_), port(port_) {} bool GetStatus() const override { - float axis_value = joystick->GetAxis(axis); - if (trigger_if_greater) - return axis_value > threshold; - return axis_value < threshold; + if (port >= 0 && joysticks && joysticks->size() > port && joysticks->at(port)) { + return joysticks->at(port)->GetAxis(axis, isController); + } + for (const auto& joystick : *joysticks) { + if (!joystick) + continue; + float axis_value = joystick->GetAxis(axis, isController); + if (trigger_if_greater && axis_value > threshold) + return true; + else if (!trigger_if_greater && axis_value < threshold) + return true; + } + return false; } private: - std::shared_ptr joystick; + std::shared_ptr>> joysticks; int axis; float threshold; bool trigger_if_greater; + bool isController = true; + int port; }; class SDLAnalog final : public Input::AnalogDevice { public: - SDLAnalog(std::shared_ptr joystick_, int axis_x_, int axis_y_, float deadzone_) - : joystick(std::move(joystick_)), axis_x(axis_x_), axis_y(axis_y_), deadzone(deadzone_) {} + SDLAnalog(std::shared_ptr>> joysticks_, int axis_x_, + int axis_y_, float deadzone_, int port_ = -1, bool isController_ = true) + : joysticks(joysticks_), axis_x(axis_x_), axis_y(axis_y_), deadzone(deadzone_), + isController(isController_), port(port_) {} std::tuple GetStatus() const override { - const auto [x, y] = joystick->GetAnalog(axis_x, axis_y); - const float r = std::sqrt((x * x) + (y * y)); - if (r > deadzone) { - return std::make_tuple(x / r * (r - deadzone) / (1 - deadzone), - y / r * (r - deadzone) / (1 - deadzone)); + float rMax = 0.0f, xMax = 0.0f, yMax = 0.0f; + if (port >= 0 && joysticks && joysticks->size() > port && joysticks->at(port)) { + const auto [x, y] = joysticks->at(port)->GetAnalog(axis_x, axis_y, isController); + const float r = std::sqrt((x * x) + (y * y)); + if (r > deadzone) { + return std::make_tuple(x / r * (r - deadzone) / (1 - deadzone), + y / r * (r - deadzone) / (1 - deadzone)); + } + return std::make_tuple(0.0f, 0.0f); + } + // if more than 1, return the value of whichever joystick is greatest + for (const auto& joystick : *joysticks) { + if (!joystick) + continue; + const auto [x, y] = joystick->GetAnalog(axis_x, axis_y, isController); + const float r = std::sqrt((x * x) + (y * y)); + if (r > rMax) { + xMax = x; + yMax = y; + rMax = r; + } + } + if (rMax > deadzone) { + return std::make_tuple(xMax / rMax * (rMax - deadzone) / (1 - deadzone), + yMax / rMax * (rMax - deadzone) / (1 - deadzone)); } return std::make_tuple(0.0f, 0.0f); } private: - std::shared_ptr joystick; + std::shared_ptr>> joysticks; const int axis_x; const int axis_y; const float deadzone; + bool isController; + int port; }; class SDLMotion final : public Input::MotionDevice { @@ -708,6 +590,8 @@ public: explicit SDLMotion(std::shared_ptr joystick_) : joystick(std::move(joystick_)) {} std::tuple, Common::Vec3> GetStatus() const override { + if (!joystick) + return std::make_tuple(Common::Vec3(0, 0, 0), Common::Vec3(0, 0, 0)); return joystick->GetMotion(); } @@ -737,25 +621,29 @@ public: /** * Creates a button device from a joystick button * @param params contains parameters for creating the device: + * - "api": either "controller" or "joystick" depending on API used * - "guid": the guid of the joystick to bind * - "port": the nth joystick of the same type to bind - * - "button"(optional): the index of the button to bind + * - "button"(optional): the index of the joystick button to bind + * - "maptype" (optional): can be "guid+port, "guid", "all" - which components should be + * used for binding * - "hat"(optional): the index of the hat to bind as direction buttons - * - "axis"(optional): the index of the axis to bind + * - "axis"(optional): the index of the joystick or controller axis to bind * - "direction"(only used for hat): the direction name of the hat to bind. Can be "up", * "down", "left" or "right" * - "threshold"(only used for axis): a float value in (-1.0, 1.0) which the button is * triggered if the axis value crosses * - "direction"(only used for axis): "+" means the button is triggered when the axis - * value is greater than the threshold; "-" means the button is triggered when the axis - * value is smaller than the threshold + * value is greater than the threshold; "-" means the button is triggered when the axis + * value is smaller than the threshold */ std::unique_ptr Create(const Common::ParamPackage& params) override { - const std::string guid = params.Get("guid", "0"); - const int port = params.Get("port", 0); - - auto joystick = state.GetSDLJoystickByGUID(guid, port); + const std::string maptype = params.Get("maptype", "guid+port"); + const int port = maptype == "guid+port" ? params.Get("port", -1) : -1; + const bool controller = params.Get("api", "joystick") == "controller"; + const std::string guid = (controller && maptype == "all") ? "" : params.Get("guid", ""); + auto joysticks = state.GetJoysticksByGUID(guid); if (params.Has("hat")) { const int hat = params.Get("hat", 0); const std::string direction_name = params.Get("direction", ""); @@ -771,12 +659,11 @@ public: } else { direction = 0; } - // This is necessary so accessing GetHat with hat won't crash - joystick->SetHat(hat, SDL_HAT_CENTERED); - return std::make_unique(joystick, hat, direction); + return std::make_unique(joysticks, hat, direction, port); } if (params.Has("axis")) { + bool controller = params.Get("api", "joystick") == "controller"; const int axis = params.Get("axis", 0); const float threshold = params.Get("threshold", 0.5f); const std::string direction_name = params.Get("direction", ""); @@ -789,15 +676,11 @@ public: trigger_if_greater = true; LOG_ERROR(Input, "Unknown direction {}", direction_name); } - // This is necessary so accessing GetAxis with axis won't crash - joystick->SetAxis(axis, 0); - return std::make_unique(joystick, axis, threshold, trigger_if_greater); + return std::make_unique(joysticks, axis, threshold, trigger_if_greater, + port, controller); } - const int button = params.Get("button", 0); - // This is necessary so accessing GetButton with button won't crash - joystick->SetButton(button, false); - return std::make_unique(joystick, button); + return std::make_unique(joysticks, button, port, controller); } private: @@ -811,24 +694,26 @@ public: /** * Creates analog device from joystick axes * @param params contains parameters for creating the device: - * - "guid": the guid of the joystick to bind + * - "api": either "controller" or "joystick" based on API used + * - "guid": the guid of the joystick to bind or all controllers (when possible)) * - "port": the nth joystick of the same type + * - "maptype": could be "guid+port", "guid", or "all" * - "axis_x": the index of the axis to be bind as x-axis * - "axis_y": the index of the axis to be bind as y-axis */ std::unique_ptr Create(const Common::ParamPackage& params) override { - const std::string guid = params.Get("guid", "0"); - const int port = params.Get("port", 0); + const std::string maptype = params.Get("maptype", "guid+port"); + const bool controller = params.Get("api", "joystick") == "controller"; + const std::string guid = (controller && maptype == "all") ? "" : params.Get("guid", "0"); const int axis_x = params.Get("axis_x", 0); const int axis_y = params.Get("axis_y", 1); + const int port = maptype == "guid+port" ? params.Get("port", -1) : -1; + float deadzone = std::clamp(params.Get("deadzone", 0.0f), 0.0f, .99f); - auto joystick = state.GetSDLJoystickByGUID(guid, port); + auto joysticks = state.GetJoysticksByGUID(guid); - // This is necessary so accessing GetAxis with axis_x and axis_y won't crash - joystick->SetAxis(axis_x, 0); - joystick->SetAxis(axis_y, 0); - return std::make_unique(joystick, axis_x, axis_y, deadzone); + return std::make_unique(joysticks, axis_x, axis_y, deadzone, port, controller); } private: @@ -842,9 +727,10 @@ public: std::unique_ptr Create(const Common::ParamPackage& params) override { const std::string guid = params.Get("guid", "0"); const int port = params.Get("port", 0); - - auto joystick = state.GetSDLJoystickByGUID(guid, port); - + auto joysticks = state.GetJoysticksByGUID(guid); + if (joysticks->empty()) + return std::make_unique(nullptr); + auto joystick = joysticks->size() > port ? joysticks->at(port) : joysticks->at(0); return std::make_unique(joystick); } @@ -868,7 +754,8 @@ public: const std::string guid = params.Get("guid", "0"); const int port = params.Get("port", 0); const int touchpad = params.Get("touchpad", 0); - auto joystick = state.GetSDLJoystickByGUID(guid, port); + auto joysticks = state.GetJoysticksByGUID(guid); + auto joystick = joysticks->size() > port ? joysticks->at(port) : joysticks->at(0); return std::make_unique(joystick, touchpad); } @@ -924,8 +811,8 @@ SDLState::SDLState() { } }); } - // Because the events for joystick connection happens before we have our event watcher added, we - // can just open all the joysticks right here + // Because the events for joystick connection happens before we have our event watcher + // added, we can just open all the joysticks right here for (int i = 0; i < SDL_NumJoysticks(); ++i) { InitJoystick(i); } @@ -947,14 +834,37 @@ SDLState::~SDLState() { } } -Common::ParamPackage SDLEventToButtonParamPackage(SDLState& state, const SDL_Event& event) { +Common::ParamPackage SDLEventToButtonParamPackage(SDLState& state, const SDL_Event& event, + const bool down = false) { Common::ParamPackage params({{"engine", "sdl"}}); + if (down) { + params.Set("down", "1"); + } + // is it safe to always use event.jhat.which here regardless of event type? + auto joystick = state.GetSDLJoystickBySDLID(event.jhat.which); + if (!joystick) + return {}; + params.Set("port", joystick->GetPort()); + params.Set("guid", joystick->GetGUID()); switch (event.type) { + case SDL_CONTROLLERAXISMOTION: { + params.Set("api", "controller"); + if (axis_names.contains(event.caxis.axis)) { + params.Set("name", axis_names.at(event.caxis.axis)); + } + params.Set("axis", event.caxis.axis); + if (event.caxis.value > 0) { + params.Set("direction", "+"); + params.Set("threshold", "0.5"); + } else { + params.Set("direction", "-"); + params.Set("threshold", "-0.5"); + } + break; + } case SDL_JOYAXISMOTION: { - auto joystick = state.GetSDLJoystickBySDLID(event.jaxis.which); - params.Set("port", joystick->GetPort()); - params.Set("guid", joystick->GetGUID()); + params.Set("api", "joystick"); params.Set("axis", event.jaxis.axis); if (event.jaxis.value > 0) { params.Set("direction", "+"); @@ -965,15 +875,22 @@ Common::ParamPackage SDLEventToButtonParamPackage(SDLState& state, const SDL_Eve } break; } - case SDL_JOYBUTTONUP: { - auto joystick = state.GetSDLJoystickBySDLID(event.jbutton.which); - params.Set("port", joystick->GetPort()); - params.Set("guid", joystick->GetGUID()); + case SDL_CONTROLLERBUTTONUP: + case SDL_CONTROLLERBUTTONDOWN: { + if (button_names.contains(event.cbutton.button)) { + params.Set("name", button_names.at(event.cbutton.button)); + } + params.Set("api", "controller"); + params.Set("button", event.cbutton.button); + break; + } + case SDL_JOYBUTTONUP: + case SDL_JOYBUTTONDOWN: { + params.Set("api", "joystick"); params.Set("button", event.jbutton.button); break; } case SDL_JOYHATMOTION: { - auto joystick = state.GetSDLJoystickBySDLID(event.jhat.which); params.Set("port", joystick->GetPort()); params.Set("guid", joystick->GetGUID()); params.Set("hat", event.jhat.hat); @@ -990,12 +907,16 @@ Common::ParamPackage SDLEventToButtonParamPackage(SDLState& state, const SDL_Eve case SDL_HAT_RIGHT: params.Set("direction", "right"); break; + case SDL_HAT_CENTERED: + params.Set("direction", "centered"); + break; default: return {}; } break; } } + return params; } @@ -1007,6 +928,7 @@ public: void Start() override { state.event_queue.Clear(); + state.polling = true; } @@ -1049,61 +971,148 @@ public: Common::ParamPackage GetNextInput() override { SDL_Event event; while (state.event_queue.Pop(event)) { + auto axis = event.jaxis.axis; + auto id = event.jaxis.which; + auto value = event.jaxis.value; + auto timestamp = event.jaxis.timestamp; + auto button = event.jbutton.button; + bool controller = false; switch (event.type) { - case SDL_JOYAXISMOTION: - if (!axis_memory.count(event.jaxis.which) || - !axis_memory[event.jaxis.which].count(event.jaxis.axis)) { - axis_memory[event.jaxis.which][event.jaxis.axis] = event.jaxis.value; - axis_event_count[event.jaxis.which][event.jaxis.axis] = 1; + case SDL_CONTROLLERAXISMOTION: { + axis = event.caxis.axis; + value = event.caxis.value; + timestamp = event.caxis.timestamp; + value = event.caxis.value; + controller = true; + [[fallthrough]]; + } + case SDL_JOYAXISMOTION: { + // if a button has been pressed down within 50ms of this axis movement, + // assume they are actually the same thing and skip this axis + if (buttonDownTimestamp && + ((timestamp >= buttonDownTimestamp && timestamp - buttonDownTimestamp <= 50) || + (timestamp < buttonDownTimestamp && buttonDownTimestamp - timestamp <= 50))) { + axis_skip[id][axis] = true; + break; + } + + // skipping this axis + if (axis_skip[id][axis]) + break; + if (!axis_memory.count(id) || !axis_memory[id].count(axis)) { + // starting a new movement. + axisStartTimestamps[id][axis] = event.jaxis.timestamp; + axis_event_count[id][axis] = 1; + if (IsAxisAtExtreme(value)) { + // a single event with a value right at the extreme. + // Assume this is a digital "axis" and send the down + // signal with center set to 0. + if (controller) { + event.caxis.value = std::copysign(32767, value); + } else { + event.jaxis.value = std::copysign(32767, value); + } + axis_center_value[id][axis] = 0; + return SDLEventToButtonParamPackage(state, event, true); + } + // otherwise, this is our first event, identify the center + if (value < -28000) + axis_center_value[id][axis] = -32768; + else if (value > 28000) + axis_center_value[id][axis] = 32767; + else + axis_center_value[id][axis] = 0; + + axis_memory[id][axis] = axis_center_value[id][axis]; break; } else { - axis_event_count[event.jaxis.which][event.jaxis.axis]++; - // The joystick and axis exist in our map if we take this branch, so no checks - // needed - if (std::abs( - (event.jaxis.value - axis_memory[event.jaxis.which][event.jaxis.axis]) / - 32767.0) < 0.5) { - break; - } else { - if (axis_event_count[event.jaxis.which][event.jaxis.axis] == 2 && - IsAxisAtPole(event.jaxis.value) && - IsAxisAtPole(axis_memory[event.jaxis.which][event.jaxis.axis])) { - // If we have exactly two events and both are near a pole, this is - // likely a digital input masquerading as an analog axis; Instead of - // trying to look at the direction the axis travelled, assume the first - // event was press and the second was release; This should handle most - // digital axes while deferring to the direction of travel for analog - // axes - event.jaxis.value = static_cast(std::copysign( - 32767, axis_memory[event.jaxis.which][event.jaxis.axis])); + axis_event_count[id][axis]++; + // only two events, second one at center, means this is a digital release + if (axis_event_count[id][axis] == 2 && IsAxisAtCenter(value, id, axis) && + IsAxisAtExtreme(axis_memory[id][axis])) { + // send the up signal for this digital axis, and clear. + axis_event_count[id][axis] = 0; + axis_memory[id][axis] = 0; + return SDLEventToButtonParamPackage(state, event, false); + } + if (IsAxisAtCenter(value, id, axis) && + IsAxisPastThreshold(axis_memory[id][axis], id, axis)) { + // returned to center, send the up signal + if (controller) { + event.caxis.value = static_cast(std::copysign( + 32767, axis_memory[id][axis] - axis_center_value[id][axis])); } else { - // There are more than two events, so this is likely a true analog axis, - // check the direction it travelled event.jaxis.value = static_cast(std::copysign( - 32767, event.jaxis.value - - axis_memory[event.jaxis.which][event.jaxis.axis])); + 32767, axis_memory[id][axis] - axis_center_value[id][axis])); } - axis_memory.clear(); - axis_event_count.clear(); + axis_memory[id][axis] = 0; + axis_event_count[id][axis] = 0; + return SDLEventToButtonParamPackage(state, event, false); + } else if (IsAxisAtCenter(axis_memory[id][axis], id, axis) && + IsAxisPastThreshold(event.jaxis.value, id, axis)) { + if (controller) { + event.caxis.value = static_cast( + std::copysign(32767, value - axis_center_value[id][axis])); + axis_memory[id][axis] = event.caxis.value; + } else { + event.jaxis.value = static_cast( + std::copysign(32767, value - axis_center_value[id][axis])); + axis_memory[id][axis] = event.jaxis.value; + } + return SDLEventToButtonParamPackage(state, event, true); } } - case SDL_JOYBUTTONUP: - case SDL_JOYHATMOTION: - return SDLEventToButtonParamPackage(state, event); + break; + } + case SDL_CONTROLLERBUTTONDOWN: { + buttonDownTimestamp = event.cbutton.timestamp; + return SDLEventToButtonParamPackage(state, event, true); + } + case SDL_JOYBUTTONDOWN: { + buttonDownTimestamp = event.jbutton.timestamp; + return SDLEventToButtonParamPackage(state, event, true); + break; + } + case SDL_CONTROLLERBUTTONUP: { + return SDLEventToButtonParamPackage(state, event, false); + break; + } + case SDL_JOYBUTTONUP: { + return SDLEventToButtonParamPackage(state, event, false); + break; + } + case SDL_JOYHATMOTION: { + return SDLEventToButtonParamPackage(state, event, + event.jhat.value != SDL_HAT_CENTERED); + break; + } } } return {}; } private: - // Determine whether an axis value is close to an extreme or center - // Some controllers have a digital D-Pad as a pair of analog sticks, with 3 possible values per - // axis, which is why the center must be considered a pole - bool IsAxisAtPole(int16_t value) { - return std::abs(value) >= 32767 || std::abs(value) < 327; + bool IsAxisAtCenter(int16_t value, SDL_JoystickID id, uint8_t axis) { + return std::abs(value - axis_center_value[id][axis]) < 367; } + + bool IsAxisPastThreshold(int16_t value, SDL_JoystickID id, uint8_t axis) { + return std::abs(value - axis_center_value[id][axis]) > 32767 / 2; + } + + bool IsAxisAtExtreme(int16_t value) { + return std::abs(value) > 32766; + } + + /** Holds the first received value for the axis. Used to + * identify situations where "released" is -32768 (some triggers) + */ + std::unordered_map> axis_center_value; std::unordered_map> axis_memory; std::unordered_map> axis_event_count; + std::unordered_map> axis_skip; + int buttonDownTimestamp = 0; + std::unordered_map> axisStartTimestamps; }; class SDLAnalogPoller final : public SDLPoller { @@ -1121,29 +1130,43 @@ public: Common::ParamPackage GetNextInput() override { SDL_Event event{}; + SDL_JoystickID which = -1; while (state.event_queue.Pop(event)) { - if (event.type != SDL_JOYAXISMOTION || std::abs(event.jaxis.value / 32767.0) < 0.5) { + if ((event.type != SDL_JOYAXISMOTION && event.type != SDL_CONTROLLERAXISMOTION)) { continue; } - // An analog device needs two axes, so we need to store the axis for later and wait for - // a second SDL event. The axes also must be from the same joystick. - int axis = event.jaxis.axis; + which = event.type == SDL_JOYAXISMOTION ? event.jaxis.which : event.caxis.which; + auto value = event.type == SDL_JOYAXISMOTION ? event.jaxis.value : event.caxis.value; + + if (std::abs(value / 32767.0) < 0.5) + continue; + // An analog device needs two axes, so we need to store the axis for later and wait + // for a second SDL event. The axes also must be from the same joystick. + int axis = event.type == SDL_JOYAXISMOTION ? event.jaxis.axis : event.caxis.axis; + if (analog_xaxis == -1) { analog_xaxis = axis; - analog_axes_joystick = event.jaxis.which; + analog_axes_joystick = which; } else if (analog_yaxis == -1 && analog_xaxis != axis && - analog_axes_joystick == event.jaxis.which) { + analog_axes_joystick == which) { analog_yaxis = axis; } } Common::ParamPackage params; - if (analog_xaxis != -1 && analog_yaxis != -1) { - auto joystick = state.GetSDLJoystickBySDLID(event.jaxis.which); + if (which != -1 && analog_xaxis != -1 && analog_yaxis != -1) { + auto joystick = state.GetSDLJoystickBySDLID(which); params.Set("engine", "sdl"); params.Set("port", joystick->GetPort()); params.Set("guid", joystick->GetGUID()); params.Set("axis_x", analog_xaxis); params.Set("axis_y", analog_yaxis); + if (event.type == SDL_JOYAXISMOTION) { + params.Set("api", "joystick"); + } else { + params.Set("api", "controller"); + params.Set("name_x", axis_names.at(analog_xaxis)); + params.Set("name_y", axis_names.at(analog_yaxis)); + } analog_xaxis = -1; analog_yaxis = -1; analog_axes_joystick = -1; diff --git a/src/input_common/sdl/sdl_impl.h b/src/input_common/sdl/sdl_impl.h index 4106572d9..3331312ce 100644 --- a/src/input_common/sdl/sdl_impl.h +++ b/src/input_common/sdl/sdl_impl.h @@ -10,17 +10,10 @@ #include #include "common/settings.h" #include "common/threadsafe_queue.h" -#include "input_common/sdl/sdl.h" - -union SDL_Event; -using SDL_Joystick = struct _SDL_Joystick; -using SDL_JoystickID = s32; -using SDL_GameController = struct _SDL_GameController; +#include "input_common/sdl/sdl_joystick.h" namespace InputCommon::SDL { -class SDLJoystick; -class SDLGameController; class SDLButtonFactory; class SDLAnalogFactory; class SDLMotionFactory; @@ -37,11 +30,14 @@ public: /// Handle SDL_Events for joysticks from SDL_PollEvent void HandleGameControllerEvent(const SDL_Event& event); + // returns a pointer to a vector of joysticks that match the needs of this device. + // will be a pointer because it will be updated when new joysticks are added + std::shared_ptr>> GetJoysticksByGUID( + const std::string& guid); std::shared_ptr GetSDLJoystickBySDLID(SDL_JoystickID sdl_id); - std::shared_ptr GetSDLJoystickByGUID(const std::string& guid, int port); - Common::ParamPackage GetSDLControllerButtonBindByGUID(const std::string& guid, int port, - Settings::NativeButton::Values button); + Common::ParamPackage GetSDLControllerButtonBind(const Common::ParamPackage a_button_params, + Settings::NativeButton::Values button); Common::ParamPackage GetSDLControllerAnalogBindByGUID(const std::string& guid, int port, Settings::NativeAnalog::Values analog); @@ -54,13 +50,17 @@ public: private: void InitJoystick(int joystick_index); - void CloseJoystick(SDL_Joystick* sdl_joystick); + void CloseJoystick(SDL_JoystickID instance_id); /// Needs to be called before SDL_QuitSubSystem. void CloseJoysticks(); /// Map of GUID of a list of corresponding virtual Joysticks - std::unordered_map>> joystick_map; + std::unordered_map>>> + joystick_map; + // This vector keeps a list of all joysticks, ignoring guid + std::shared_ptr>> joystick_vector = + std::make_shared>>(); std::mutex joystick_map_mutex; std::shared_ptr touch_factory; diff --git a/src/input_common/sdl/sdl_joystick.cpp b/src/input_common/sdl/sdl_joystick.cpp new file mode 100644 index 000000000..7e0dcef1c --- /dev/null +++ b/src/input_common/sdl/sdl_joystick.cpp @@ -0,0 +1,175 @@ +// Copyright Citra Emulator Project / Azahar Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include +#include "sdl_joystick.h" + +namespace InputCommon::SDL { + +SDLJoystick::SDLJoystick(std::string guid_, int port_, SDL_Joystick* joystick, + SDL_GameController* game_controller) + : guid{std::move(guid_)}, port{port_}, sdl_joystick{joystick, &SDL_JoystickClose}, + sdl_controller{game_controller, &SDL_GameControllerClose} { + EnableMotion(); + CreateControllerButtonMap(); +} + +bool SDLJoystick::IsButtonMappedToController(int button) const { + return mapped_joystick_buttons.count(button) > 0; +} + +void SDLJoystick::EnableMotion() { + if (!sdl_controller) { + return; + } +#if SDL_VERSION_ATLEAST(2, 0, 14) + SDL_GameController* controller = sdl_controller.get(); + + if (HasMotion()) { + SDL_GameControllerSetSensorEnabled(controller, SDL_SENSOR_ACCEL, SDL_FALSE); + SDL_GameControllerSetSensorEnabled(controller, SDL_SENSOR_GYRO, SDL_FALSE); + } + has_accel = SDL_GameControllerHasSensor(controller, SDL_SENSOR_ACCEL) == SDL_TRUE; + has_gyro = SDL_GameControllerHasSensor(controller, SDL_SENSOR_GYRO) == SDL_TRUE; + if (has_accel) { + SDL_GameControllerSetSensorEnabled(controller, SDL_SENSOR_ACCEL, SDL_TRUE); + } + if (has_gyro) { + SDL_GameControllerSetSensorEnabled(controller, SDL_SENSOR_GYRO, SDL_TRUE); + } +#endif +} + +bool SDLJoystick::HasMotion() const { + return has_gyro || has_accel; +} + +bool SDLJoystick::GetButton(int button, bool isController) const { + if (!sdl_joystick) + return false; + if (isController) + return SDL_GameControllerGetButton(sdl_controller.get(), + static_cast(button)); + return SDL_JoystickGetButton(sdl_joystick.get(), button) != 0; +} + +float SDLJoystick::GetAxis(int axis, bool isController) const { + if (!sdl_joystick) + return 0.0; + if (isController) + return SDL_GameControllerGetAxis(sdl_controller.get(), + static_cast(axis)) / + 32767.0f; + return SDL_JoystickGetAxis(sdl_joystick.get(), axis) / 32767.0f; +} + +std::tuple SDLJoystick::GetAnalog(int axis_x, int axis_y, bool isController) const { + float x = GetAxis(axis_x, isController); + float y = GetAxis(axis_y, isController); + y = -y; // 3DS uses an y-axis inverse from SDL + + // Make sure the coordinates are in the unit circle, + // otherwise normalize it. + float r = x * x + y * y; + if (r > 1.0f) { + r = std::sqrt(r); + x /= r; + y /= r; + } + + return std::make_tuple(x, y); +} + +bool SDLJoystick::GetHatDirection(int hat, Uint8 direction) const { + // no need to worry about gamecontroller here - that api treats hats as buttons + if (!sdl_joystick) + return false; + return SDL_JoystickGetHat(sdl_joystick.get(), hat) == direction; +} + +void SDLJoystick::SetTouchpad(float x, float y, int touchpad, bool down) { + std::lock_guard lock{mutex}; + state.touchpad[touchpad] = std::make_tuple(x, y, down); +} + +void SDLJoystick::SetAccel(const float x, const float y, const float z) { + std::lock_guard lock{mutex}; + state.accel.x = x; + state.accel.y = y; + state.accel.z = z; +} +void SDLJoystick::SetGyro(const float pitch, const float yaw, const float roll) { + std::lock_guard lock{mutex}; + state.gyro.x = pitch; + state.gyro.y = yaw; + state.gyro.z = roll; +} +std::tuple, Common::Vec3> SDLJoystick::GetMotion() const { + std::lock_guard lock{mutex}; + return std::make_tuple(state.accel, state.gyro); +} + +/** + * The guid of the joystick + */ +const std::string& SDLJoystick::GetGUID() const { + return guid; +} + +/** + * The number of joystick from the same type that were connected before this joystick + */ +int SDLJoystick::GetPort() const { + return port; +} + +std::tuple SDLJoystick::GetTouch(int pad) const { + return state.touchpad.at(pad); +} + +SDL_Joystick* SDLJoystick::GetSDLJoystick() const { + return sdl_joystick.get(); +} + +SDL_GameController* SDLJoystick::GetSDLGameController() const { + return sdl_controller.get(); +} + +void SDLJoystick::SetSDLJoystick(SDL_Joystick* joystick, SDL_GameController* controller) { + sdl_joystick.reset(joystick); + sdl_controller.reset(controller); +} + +void SDLJoystick::CreateControllerButtonMap() { + mapped_joystick_buttons.clear(); + + if (!sdl_controller) { + return; // Not a controller, no mapped buttons + } + + // Check all controller buttons + for (int i = 0; i < SDL_CONTROLLER_BUTTON_MAX; i++) { + auto bind = SDL_GameControllerGetBindForButton(sdl_controller.get(), + static_cast(i)); + + if (bind.bindType == SDL_CONTROLLER_BINDTYPE_BUTTON) { + mapped_joystick_buttons.insert(bind.value.button); + } + } + + // Also check trigger axes that might be buttons + auto lt_bind = + SDL_GameControllerGetBindForAxis(sdl_controller.get(), SDL_CONTROLLER_AXIS_TRIGGERLEFT); + if (lt_bind.bindType == SDL_CONTROLLER_BINDTYPE_BUTTON) { + mapped_joystick_buttons.insert(lt_bind.value.button); + } + + auto rt_bind = + SDL_GameControllerGetBindForAxis(sdl_controller.get(), SDL_CONTROLLER_AXIS_TRIGGERRIGHT); + if (rt_bind.bindType == SDL_CONTROLLER_BINDTYPE_BUTTON) { + mapped_joystick_buttons.insert(rt_bind.value.button); + } +} + +} // namespace InputCommon::SDL \ No newline at end of file diff --git a/src/input_common/sdl/sdl_joystick.h b/src/input_common/sdl/sdl_joystick.h new file mode 100644 index 000000000..812daefb3 --- /dev/null +++ b/src/input_common/sdl/sdl_joystick.h @@ -0,0 +1,76 @@ +// Copyright Citra Emulator Project / Azahar Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include +#include +#include +#include +#include +#include +#include "common/vector_math.h" +#include "input_common/sdl/sdl.h" + +union SDL_Event; +using SDL_Joystick = struct _SDL_Joystick; +using SDL_JoystickID = s32; +using SDL_GameController = struct _SDL_GameController; + +namespace InputCommon::SDL { +class SDLJoystick { +public: + SDLJoystick(std::string guid_, int port_, SDL_Joystick* joystick, + SDL_GameController* game_controller); + + bool IsButtonMappedToController(int button) const; + + void EnableMotion(); + bool HasMotion() const; + + bool GetButton(int button, bool isController) const; + float GetAxis(int axis, bool isController) const; + std::tuple GetAnalog(int axis_x, int axis_y, bool isController) const; + bool GetHatDirection(int hat, uint8_t direction) const; + + void SetTouchpad(float x, float y, int touchpad, bool down); + void SetAccel(const float x, const float y, const float z); + void SetGyro(const float pitch, const float yaw, const float roll); + + std::tuple, Common::Vec3> GetMotion() const; + + /** + * The guid of the joystick + */ + const std::string& GetGUID() const; + + /** + * The number of joystick from the same type that were connected before this joystick + */ + int GetPort() const; + + std::tuple GetTouch(int pad) const; + + SDL_Joystick* GetSDLJoystick() const; + SDL_GameController* GetSDLGameController() const; + + void SetSDLJoystick(SDL_Joystick* joystick, SDL_GameController* controller); + +private: + struct State { + Common::Vec3 accel; + Common::Vec3 gyro; + std::unordered_map> touchpad; + } state; + std::string guid; + int port; + bool has_gyro{false}; + bool has_accel{false}; + std::unique_ptr sdl_joystick; + std::unique_ptr sdl_controller; + mutable std::mutex mutex; + std::unordered_set mapped_joystick_buttons; + void CreateControllerButtonMap(); +}; +} // namespace InputCommon::SDL \ No newline at end of file