From 5ba7b11ba49ecba530612248286482f94799672c Mon Sep 17 00:00:00 2001 From: german77 Date: Sat, 27 Nov 2021 23:26:51 -0600 Subject: yuzu: Implement basic controller navigation --- src/yuzu/CMakeLists.txt | 2 + src/yuzu/applets/qt_controller.h | 4 +- src/yuzu/applets/qt_profile_select.cpp | 17 ++- src/yuzu/applets/qt_profile_select.h | 8 +- src/yuzu/game_list.cpp | 20 ++++ src/yuzu/game_list.h | 5 + src/yuzu/main.cpp | 9 +- src/yuzu/util/controller_navigation.cpp | 177 ++++++++++++++++++++++++++++++++ src/yuzu/util/controller_navigation.h | 51 +++++++++ 9 files changed, 285 insertions(+), 8 deletions(-) create mode 100644 src/yuzu/util/controller_navigation.cpp create mode 100644 src/yuzu/util/controller_navigation.h diff --git a/src/yuzu/CMakeLists.txt b/src/yuzu/CMakeLists.txt index a44815e71..732e8c276 100644 --- a/src/yuzu/CMakeLists.txt +++ b/src/yuzu/CMakeLists.txt @@ -152,6 +152,8 @@ add_executable(yuzu main.ui uisettings.cpp uisettings.h + util/controller_navigation.cpp + util/controller_navigation.h util/limitable_input_dialog.cpp util/limitable_input_dialog.h util/overlay_dialog.cpp diff --git a/src/yuzu/applets/qt_controller.h b/src/yuzu/applets/qt_controller.h index cc343e5ae..7ab9ced3d 100644 --- a/src/yuzu/applets/qt_controller.h +++ b/src/yuzu/applets/qt_controller.h @@ -7,7 +7,6 @@ #include #include #include -#include "core/core.h" #include "core/frontend/applets/controller.h" class GMainWindow; @@ -32,8 +31,9 @@ class System; } namespace Core::HID { +class HIDCore; enum class NpadStyleIndex : u8; -} +} // namespace Core::HID class QtControllerSelectorDialog final : public QDialog { Q_OBJECT diff --git a/src/yuzu/applets/qt_profile_select.cpp b/src/yuzu/applets/qt_profile_select.cpp index a56638e21..7b19f1f8d 100644 --- a/src/yuzu/applets/qt_profile_select.cpp +++ b/src/yuzu/applets/qt_profile_select.cpp @@ -3,6 +3,7 @@ // Refer to the license.txt file included. #include +#include #include #include #include @@ -16,6 +17,7 @@ #include "core/hle/lock.h" #include "yuzu/applets/qt_profile_select.h" #include "yuzu/main.h" +#include "yuzu/util/controller_navigation.h" namespace { QString FormatUserEntryText(const QString& username, Common::UUID uuid) { @@ -45,7 +47,7 @@ QPixmap GetIcon(Common::UUID uuid) { } } // Anonymous namespace -QtProfileSelectionDialog::QtProfileSelectionDialog(QWidget* parent) +QtProfileSelectionDialog::QtProfileSelectionDialog(Core::HID::HIDCore& hid_core, QWidget* parent) : QDialog(parent), profile_manager(std::make_unique()) { outer_layout = new QVBoxLayout; @@ -65,6 +67,7 @@ QtProfileSelectionDialog::QtProfileSelectionDialog(QWidget* parent) tree_view = new QTreeView; item_model = new QStandardItemModel(tree_view); tree_view->setModel(item_model); + controller_navigation = new ControllerNavigation(hid_core, this); tree_view->setAlternatingRowColors(true); tree_view->setSelectionMode(QHeaderView::SingleSelection); @@ -91,6 +94,14 @@ QtProfileSelectionDialog::QtProfileSelectionDialog(QWidget* parent) scroll_area->setLayout(layout); connect(tree_view, &QTreeView::clicked, this, &QtProfileSelectionDialog::SelectUser); + connect(controller_navigation, &ControllerNavigation::TriggerKeyboardEvent, + [this](Qt::Key key) { + if (!this->isActiveWindow()) { + return; + } + QKeyEvent* event = new QKeyEvent(QEvent::KeyPress, key, Qt::NoModifier); + QCoreApplication::postEvent(tree_view, event); + }); const auto& profiles = profile_manager->GetAllUsers(); for (const auto& user : profiles) { @@ -113,7 +124,9 @@ QtProfileSelectionDialog::QtProfileSelectionDialog(QWidget* parent) resize(550, 400); } -QtProfileSelectionDialog::~QtProfileSelectionDialog() = default; +QtProfileSelectionDialog::~QtProfileSelectionDialog() { + controller_navigation->UnloadController(); +}; int QtProfileSelectionDialog::exec() { // Skip profile selection when there's only one. diff --git a/src/yuzu/applets/qt_profile_select.h b/src/yuzu/applets/qt_profile_select.h index 4e9037488..56496ed31 100644 --- a/src/yuzu/applets/qt_profile_select.h +++ b/src/yuzu/applets/qt_profile_select.h @@ -11,6 +11,7 @@ #include "core/frontend/applets/profile_select.h" #include "core/hle/service/acc/profile_manager.h" +class ControllerNavigation; class GMainWindow; class QDialogButtonBox; class QGraphicsScene; @@ -20,11 +21,15 @@ class QStandardItem; class QStandardItemModel; class QVBoxLayout; +namespace Core::HID { +class HIDCore; +} // namespace Core::HID + class QtProfileSelectionDialog final : public QDialog { Q_OBJECT public: - explicit QtProfileSelectionDialog(QWidget* parent); + explicit QtProfileSelectionDialog(Core::HID::HIDCore& hid_core, QWidget* parent); ~QtProfileSelectionDialog() override; int exec() override; @@ -51,6 +56,7 @@ private: QDialogButtonBox* buttons; std::unique_ptr profile_manager; + ControllerNavigation* controller_navigation = nullptr; }; class QtProfileSelector final : public QObject, public Core::Frontend::ProfileSelectApplet { diff --git a/src/yuzu/game_list.cpp b/src/yuzu/game_list.cpp index 2af95dbe5..1a5e41588 100644 --- a/src/yuzu/game_list.cpp +++ b/src/yuzu/game_list.cpp @@ -17,6 +17,7 @@ #include #include "common/common_types.h" #include "common/logging/log.h" +#include "core/core.h" #include "core/file_sys/patch_manager.h" #include "core/file_sys/registered_cache.h" #include "yuzu/compatibility_list.h" @@ -25,6 +26,7 @@ #include "yuzu/game_list_worker.h" #include "yuzu/main.h" #include "yuzu/uisettings.h" +#include "yuzu/util/controller_navigation.h" GameListSearchField::KeyReleaseEater::KeyReleaseEater(GameList* gamelist, QObject* parent) : QObject(parent), gamelist{gamelist} {} @@ -312,6 +314,7 @@ GameList::GameList(FileSys::VirtualFilesystem vfs, FileSys::ManualContentProvide this->main_window = parent; layout = new QVBoxLayout; tree_view = new QTreeView; + controller_navigation = new ControllerNavigation(system.HIDCore(), this); search_field = new GameListSearchField(this); item_model = new QStandardItemModel(tree_view); tree_view->setModel(item_model); @@ -341,6 +344,18 @@ GameList::GameList(FileSys::VirtualFilesystem vfs, FileSys::ManualContentProvide connect(tree_view, &QTreeView::customContextMenuRequested, this, &GameList::PopupContextMenu); connect(tree_view, &QTreeView::expanded, this, &GameList::OnItemExpanded); connect(tree_view, &QTreeView::collapsed, this, &GameList::OnItemExpanded); + connect(controller_navigation, &ControllerNavigation::TriggerKeyboardEvent, + [this](Qt::Key key) { + // Avoid pressing buttons while playing + if (system.IsPoweredOn()) { + return; + } + if (!this->isActiveWindow()) { + return; + } + QKeyEvent* event = new QKeyEvent(QEvent::KeyPress, key, Qt::NoModifier); + QCoreApplication::postEvent(tree_view, event); + }); // We must register all custom types with the Qt Automoc system so that we are able to use // it with signals/slots. In this case, QList falls under the umbrells of custom types. @@ -353,7 +368,12 @@ GameList::GameList(FileSys::VirtualFilesystem vfs, FileSys::ManualContentProvide setLayout(layout); } +void GameList::UnloadController() { + controller_navigation->UnloadController(); +} + GameList::~GameList() { + UnloadController(); emit ShouldCancelWorker(); } diff --git a/src/yuzu/game_list.h b/src/yuzu/game_list.h index 675469e66..a94ea1477 100644 --- a/src/yuzu/game_list.h +++ b/src/yuzu/game_list.h @@ -24,6 +24,7 @@ #include "uisettings.h" #include "yuzu/compatibility_list.h" +class ControllerNavigation; class GameListWorker; class GameListSearchField; class GameListDir; @@ -88,6 +89,9 @@ public: void SaveInterfaceLayout(); void LoadInterfaceLayout(); + /// Disables events from the emulated controller + void UnloadController(); + static const QStringList supported_file_extensions; signals: @@ -143,6 +147,7 @@ private: QStandardItemModel* item_model = nullptr; GameListWorker* current_worker = nullptr; QFileSystemWatcher* watcher = nullptr; + ControllerNavigation* controller_navigation = nullptr; CompatibilityList compatibility_list; friend class GameListSearchField; diff --git a/src/yuzu/main.cpp b/src/yuzu/main.cpp index 9bd0db10a..f266fd963 100644 --- a/src/yuzu/main.cpp +++ b/src/yuzu/main.cpp @@ -449,7 +449,7 @@ void GMainWindow::ControllerSelectorReconfigureControllers( } void GMainWindow::ProfileSelectorSelectProfile() { - QtProfileSelectionDialog dialog(this); + QtProfileSelectionDialog dialog(system->HIDCore(), this); dialog.setWindowFlags(Qt::Dialog | Qt::CustomizeWindowHint | Qt::WindowStaysOnTopHint | Qt::WindowTitleHint | Qt::WindowSystemMenuHint | Qt::WindowCloseButtonHint); @@ -1346,7 +1346,7 @@ bool GMainWindow::LoadROM(const QString& filename, u64 program_id, std::size_t p } void GMainWindow::SelectAndSetCurrentUser() { - QtProfileSelectionDialog dialog(this); + QtProfileSelectionDialog dialog(system->HIDCore(), this); dialog.setWindowFlags(Qt::Dialog | Qt::CustomizeWindowHint | Qt::WindowTitleHint | Qt::WindowSystemMenuHint | Qt::WindowCloseButtonHint); dialog.setWindowModality(Qt::WindowModal); @@ -1608,7 +1608,7 @@ void GMainWindow::OnGameListOpenFolder(u64 program_id, GameListOpenTarget target if (has_user_save) { // User save data const auto select_profile = [this] { - QtProfileSelectionDialog dialog(this); + QtProfileSelectionDialog dialog(system->HIDCore(), this); dialog.setWindowFlags(Qt::Dialog | Qt::CustomizeWindowHint | Qt::WindowTitleHint | Qt::WindowSystemMenuHint | Qt::WindowCloseButtonHint); dialog.setWindowModality(Qt::WindowModal); @@ -3376,7 +3376,10 @@ void GMainWindow::closeEvent(QCloseEvent* event) { UpdateUISettings(); game_list->SaveInterfaceLayout(); hotkey_registry.SaveHotkeys(); + + // Unload controllers early controller_dialog->UnloadController(); + game_list->UnloadController(); system->HIDCore().UnloadInputDevices(); // Shutdown session if the emu thread is active... diff --git a/src/yuzu/util/controller_navigation.cpp b/src/yuzu/util/controller_navigation.cpp new file mode 100644 index 000000000..86fb28b9f --- /dev/null +++ b/src/yuzu/util/controller_navigation.cpp @@ -0,0 +1,177 @@ +// Copyright 2021 yuzu Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included + +#include "common/settings_input.h" +#include "core/hid/emulated_controller.h" +#include "core/hid/hid_core.h" +#include "yuzu/util/controller_navigation.h" + +ControllerNavigation::ControllerNavigation(Core::HID::HIDCore& hid_core, QWidget* parent) { + player1_controller = hid_core.GetEmulatedController(Core::HID::NpadIdType::Player1); + handheld_controller = hid_core.GetEmulatedController(Core::HID::NpadIdType::Handheld); + Core::HID::ControllerUpdateCallback engine_callback{ + .on_change = [this](Core::HID::ControllerTriggerType type) { ControllerUpdateEvent(type); }, + .is_npad_service = false, + }; + player1_callback_key = player1_controller->SetCallback(engine_callback); + handheld_callback_key = handheld_controller->SetCallback(engine_callback); + is_controller_set = true; +} + +ControllerNavigation::~ControllerNavigation() { + UnloadController(); +} + +void ControllerNavigation::UnloadController() { + if (is_controller_set) { + player1_controller->DeleteCallback(player1_callback_key); + handheld_controller->DeleteCallback(handheld_callback_key); + is_controller_set = false; + } +} + +void ControllerNavigation::TriggerButton(Settings::NativeButton::Values native_button, + Qt::Key key) { + if (button_values[native_button].value && !button_values[native_button].locked) { + emit TriggerKeyboardEvent(key); + } +} + +void ControllerNavigation::ControllerUpdateEvent(Core::HID::ControllerTriggerType type) { + std::lock_guard lock{mutex}; + if (type == Core::HID::ControllerTriggerType::Button) { + ControllerUpdateButton(); + return; + } + + if (type == Core::HID::ControllerTriggerType::Stick) { + ControllerUpdateStick(); + return; + } +} + +void ControllerNavigation::ControllerUpdateButton() { + const auto controller_type = player1_controller->GetNpadStyleIndex(); + const auto& player1_buttons = player1_controller->GetButtonsValues(); + const auto& handheld_buttons = handheld_controller->GetButtonsValues(); + + for (std::size_t i = 0; i < player1_buttons.size(); ++i) { + const bool button = player1_buttons[i].value || handheld_buttons[i].value; + // Trigger only once + button_values[i].locked = button == button_values[i].value; + button_values[i].value = button; + } + + switch (controller_type) { + case Core::HID::NpadStyleIndex::ProController: + case Core::HID::NpadStyleIndex::JoyconDual: + case Core::HID::NpadStyleIndex::Handheld: + case Core::HID::NpadStyleIndex::GameCube: + TriggerButton(Settings::NativeButton::A, Qt::Key_Enter); + TriggerButton(Settings::NativeButton::B, Qt::Key_Escape); + TriggerButton(Settings::NativeButton::DDown, Qt::Key_Down); + TriggerButton(Settings::NativeButton::DLeft, Qt::Key_Left); + TriggerButton(Settings::NativeButton::DRight, Qt::Key_Right); + TriggerButton(Settings::NativeButton::DUp, Qt::Key_Up); + break; + case Core::HID::NpadStyleIndex::JoyconLeft: + TriggerButton(Settings::NativeButton::DDown, Qt::Key_Enter); + TriggerButton(Settings::NativeButton::DLeft, Qt::Key_Escape); + break; + case Core::HID::NpadStyleIndex::JoyconRight: + TriggerButton(Settings::NativeButton::X, Qt::Key_Enter); + TriggerButton(Settings::NativeButton::A, Qt::Key_Escape); + break; + default: + break; + } +} + +void ControllerNavigation::ControllerUpdateStick() { + const auto controller_type = player1_controller->GetNpadStyleIndex(); + const auto& player1_sticks = player1_controller->GetSticksValues(); + const auto& handheld_sticks = player1_controller->GetSticksValues(); + bool update = false; + + for (std::size_t i = 0; i < player1_sticks.size(); ++i) { + const Common::Input::StickStatus stick{ + .left = player1_sticks[i].left || handheld_sticks[i].left, + .right = player1_sticks[i].right || handheld_sticks[i].right, + .up = player1_sticks[i].up || handheld_sticks[i].up, + .down = player1_sticks[i].down || handheld_sticks[i].down, + }; + // Trigger only once + if (stick.down != stick_values[i].down || stick.left != stick_values[i].left || + stick.right != stick_values[i].right || stick.up != stick_values[i].up) { + update = true; + } + stick_values[i] = stick; + } + + if (!update) { + return; + } + + switch (controller_type) { + case Core::HID::NpadStyleIndex::ProController: + case Core::HID::NpadStyleIndex::JoyconDual: + case Core::HID::NpadStyleIndex::Handheld: + case Core::HID::NpadStyleIndex::GameCube: + if (stick_values[Settings::NativeAnalog::LStick].down) { + emit TriggerKeyboardEvent(Qt::Key_Down); + return; + } + if (stick_values[Settings::NativeAnalog::LStick].left) { + emit TriggerKeyboardEvent(Qt::Key_Left); + return; + } + if (stick_values[Settings::NativeAnalog::LStick].right) { + emit TriggerKeyboardEvent(Qt::Key_Right); + return; + } + if (stick_values[Settings::NativeAnalog::LStick].up) { + emit TriggerKeyboardEvent(Qt::Key_Up); + return; + } + break; + case Core::HID::NpadStyleIndex::JoyconLeft: + if (stick_values[Settings::NativeAnalog::LStick].left) { + emit TriggerKeyboardEvent(Qt::Key_Down); + return; + } + if (stick_values[Settings::NativeAnalog::LStick].up) { + emit TriggerKeyboardEvent(Qt::Key_Left); + return; + } + if (stick_values[Settings::NativeAnalog::LStick].down) { + emit TriggerKeyboardEvent(Qt::Key_Right); + return; + } + if (stick_values[Settings::NativeAnalog::LStick].right) { + emit TriggerKeyboardEvent(Qt::Key_Up); + return; + } + break; + case Core::HID::NpadStyleIndex::JoyconRight: + if (stick_values[Settings::NativeAnalog::RStick].right) { + emit TriggerKeyboardEvent(Qt::Key_Down); + return; + } + if (stick_values[Settings::NativeAnalog::RStick].down) { + emit TriggerKeyboardEvent(Qt::Key_Left); + return; + } + if (stick_values[Settings::NativeAnalog::RStick].up) { + emit TriggerKeyboardEvent(Qt::Key_Right); + return; + } + if (stick_values[Settings::NativeAnalog::RStick].left) { + emit TriggerKeyboardEvent(Qt::Key_Up); + return; + } + break; + default: + break; + } +} diff --git a/src/yuzu/util/controller_navigation.h b/src/yuzu/util/controller_navigation.h new file mode 100644 index 000000000..7c616a088 --- /dev/null +++ b/src/yuzu/util/controller_navigation.h @@ -0,0 +1,51 @@ +// Copyright 2021 yuzu Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included + +#pragma once + +#include +#include + +#include "common/input.h" +#include "common/settings_input.h" + +namespace Core::HID { +using ButtonValues = std::array; +using SticksValues = std::array; +enum class ControllerTriggerType; +class EmulatedController; +class HIDCore; +} // namespace Core::HID + +class ControllerNavigation : public QObject { + Q_OBJECT + +public: + explicit ControllerNavigation(Core::HID::HIDCore& hid_core, QWidget* parent = nullptr); + ~ControllerNavigation(); + + /// Disables events from the emulated controller + void UnloadController(); + +signals: + void TriggerKeyboardEvent(Qt::Key key); + +private: + void TriggerButton(Settings::NativeButton::Values native_button, Qt::Key key); + void ControllerUpdateEvent(Core::HID::ControllerTriggerType type); + + void ControllerUpdateButton(); + + void ControllerUpdateStick(); + + Core::HID::ButtonValues button_values{}; + Core::HID::SticksValues stick_values{}; + + int player1_callback_key{}; + int handheld_callback_key{}; + bool is_controller_set{}; + mutable std::mutex mutex; + Core::HID::EmulatedController* player1_controller; + Core::HID::EmulatedController* handheld_controller; +}; -- cgit v1.2.3