diff options
Diffstat (limited to 'src/yuzu/applets')
| -rw-r--r-- | src/yuzu/applets/controller.cpp | 297 | ||||
| -rw-r--r-- | src/yuzu/applets/controller.h | 44 | ||||
| -rw-r--r-- | src/yuzu/applets/controller.ui | 49 | ||||
| -rw-r--r-- | src/yuzu/applets/error.cpp | 12 | ||||
| -rw-r--r-- | src/yuzu/applets/profile_select.cpp | 13 | ||||
| -rw-r--r-- | src/yuzu/applets/profile_select.h | 3 | ||||
| -rw-r--r-- | src/yuzu/applets/software_keyboard.cpp | 4 | ||||
| -rw-r--r-- | src/yuzu/applets/software_keyboard.h | 2 | ||||
| -rw-r--r-- | src/yuzu/applets/web_browser.cpp | 443 | ||||
| -rw-r--r-- | src/yuzu/applets/web_browser.h | 191 | ||||
| -rw-r--r-- | src/yuzu/applets/web_browser_scripts.h | 193 |
11 files changed, 999 insertions, 252 deletions
diff --git a/src/yuzu/applets/controller.cpp b/src/yuzu/applets/controller.cpp index 9d45f2a01..c680fd2c2 100644 --- a/src/yuzu/applets/controller.cpp +++ b/src/yuzu/applets/controller.cpp @@ -3,6 +3,7 @@ // Refer to the license.txt file included. #include <algorithm> +#include <thread> #include "common/assert.h" #include "common/string_util.h" @@ -13,20 +14,25 @@ #include "core/hle/service/sm/sm.h" #include "ui_controller.h" #include "yuzu/applets/controller.h" -#include "yuzu/configuration/configure_input_dialog.h" +#include "yuzu/configuration/configure_input.h" +#include "yuzu/configuration/configure_input_profile_dialog.h" +#include "yuzu/configuration/configure_vibration.h" +#include "yuzu/configuration/input_profiles.h" #include "yuzu/main.h" namespace { -constexpr std::array<std::array<bool, 4>, 8> led_patterns = {{ - {1, 0, 0, 0}, - {1, 1, 0, 0}, - {1, 1, 1, 0}, - {1, 1, 1, 1}, - {1, 0, 0, 1}, - {1, 0, 1, 0}, - {1, 0, 1, 1}, - {0, 1, 1, 0}, +constexpr std::size_t HANDHELD_INDEX = 8; + +constexpr std::array<std::array<bool, 4>, 8> led_patterns{{ + {true, false, false, false}, + {true, true, false, false}, + {true, true, true, false}, + {true, true, true, true}, + {true, false, false, true}, + {true, false, true, false}, + {true, false, true, true}, + {false, true, true, false}, }}; void UpdateController(Settings::ControllerType controller_type, std::size_t npad_index, @@ -66,47 +72,14 @@ bool IsControllerCompatible(Settings::ControllerType controller_type, } } -/// Maps the controller type combobox index to Controller Type enum -constexpr Settings::ControllerType GetControllerTypeFromIndex(int index) { - switch (index) { - case 0: - default: - return Settings::ControllerType::ProController; - case 1: - return Settings::ControllerType::DualJoyconDetached; - case 2: - return Settings::ControllerType::LeftJoycon; - case 3: - return Settings::ControllerType::RightJoycon; - case 4: - return Settings::ControllerType::Handheld; - } -} - -/// Maps the Controller Type enum to controller type combobox index -constexpr int GetIndexFromControllerType(Settings::ControllerType type) { - switch (type) { - case Settings::ControllerType::ProController: - default: - return 0; - case Settings::ControllerType::DualJoyconDetached: - return 1; - case Settings::ControllerType::LeftJoycon: - return 2; - case Settings::ControllerType::RightJoycon: - return 3; - case Settings::ControllerType::Handheld: - return 4; - } -} - } // namespace QtControllerSelectorDialog::QtControllerSelectorDialog( QWidget* parent, Core::Frontend::ControllerParameters parameters_, InputCommon::InputSubsystem* input_subsystem_) : QDialog(parent), ui(std::make_unique<Ui::QtControllerSelectorDialog>()), - parameters(std::move(parameters_)), input_subsystem(input_subsystem_) { + parameters(std::move(parameters_)), input_subsystem{input_subsystem_}, + input_profiles(std::make_unique<InputProfiles>()) { ui->setupUi(this); player_widgets = { @@ -177,6 +150,11 @@ QtControllerSelectorDialog::QtControllerSelectorDialog( // This avoids unintentionally changing the states of elements while loading them in. SetSupportedControllers(); DisableUnsupportedPlayers(); + + for (std::size_t player_index = 0; player_index < NUM_PLAYERS; ++player_index) { + SetEmulatedControllers(player_index); + } + LoadConfiguration(); for (std::size_t i = 0; i < NUM_PLAYERS; ++i) { @@ -216,19 +194,29 @@ QtControllerSelectorDialog::QtControllerSelectorDialog( if (i == 0) { connect(emulated_controllers[i], qOverload<int>(&QComboBox::currentIndexChanged), - [this](int index) { - UpdateDockedState(GetControllerTypeFromIndex(index) == + [this, i](int index) { + UpdateDockedState(GetControllerTypeFromIndex(index, i) == Settings::ControllerType::Handheld); }); } } + connect(ui->vibrationButton, &QPushButton::clicked, this, + &QtControllerSelectorDialog::CallConfigureVibrationDialog); + connect(ui->inputConfigButton, &QPushButton::clicked, this, - &QtControllerSelectorDialog::CallConfigureInputDialog); + &QtControllerSelectorDialog::CallConfigureInputProfileDialog); connect(ui->buttonBox, &QDialogButtonBox::accepted, this, &QtControllerSelectorDialog::ApplyConfiguration); + // Enhancement: Check if the parameters have already been met before disconnecting controllers. + // If all the parameters are met AND only allows a single player, + // stop the constructor here as we do not need to continue. + if (CheckIfParametersMet() && parameters.enable_single_mode) { + return; + } + // If keep_controllers_connected is false, forcefully disconnect all controllers if (!parameters.keep_controllers_connected) { for (auto player : player_groupboxes) { @@ -236,58 +224,66 @@ QtControllerSelectorDialog::QtControllerSelectorDialog( } } - CheckIfParametersMet(); - resize(0, 0); } QtControllerSelectorDialog::~QtControllerSelectorDialog() = default; -void QtControllerSelectorDialog::ApplyConfiguration() { - // Update the controller state once more, just to be sure they are properly applied. - for (std::size_t index = 0; index < NUM_PLAYERS; ++index) { - UpdateControllerState(index); +int QtControllerSelectorDialog::exec() { + if (parameters_met && parameters.enable_single_mode) { + return QDialog::Accepted; } + return QDialog::exec(); +} - const bool pre_docked_mode = Settings::values.use_docked_mode; - Settings::values.use_docked_mode = ui->radioDocked->isChecked(); - OnDockedModeChanged(pre_docked_mode, Settings::values.use_docked_mode); +void QtControllerSelectorDialog::ApplyConfiguration() { + const bool pre_docked_mode = Settings::values.use_docked_mode.GetValue(); + Settings::values.use_docked_mode.SetValue(ui->radioDocked->isChecked()); + OnDockedModeChanged(pre_docked_mode, Settings::values.use_docked_mode.GetValue()); - Settings::values.vibration_enabled = ui->vibrationGroup->isChecked(); + Settings::values.vibration_enabled.SetValue(ui->vibrationGroup->isChecked()); + Settings::values.motion_enabled.SetValue(ui->motionGroup->isChecked()); } void QtControllerSelectorDialog::LoadConfiguration() { for (std::size_t index = 0; index < NUM_PLAYERS; ++index) { - const auto connected = Settings::values.players[index].connected || - (index == 0 && Settings::values.players[8].connected); + const auto connected = + Settings::values.players.GetValue()[index].connected || + (index == 0 && Settings::values.players.GetValue()[HANDHELD_INDEX].connected); player_groupboxes[index]->setChecked(connected); connected_controller_checkboxes[index]->setChecked(connected); - emulated_controllers[index]->setCurrentIndex( - GetIndexFromControllerType(Settings::values.players[index].controller_type)); + emulated_controllers[index]->setCurrentIndex(GetIndexFromControllerType( + Settings::values.players.GetValue()[index].controller_type, index)); } - UpdateDockedState(Settings::values.players[8].connected); + UpdateDockedState(Settings::values.players.GetValue()[HANDHELD_INDEX].connected); - ui->vibrationGroup->setChecked(Settings::values.vibration_enabled); + ui->vibrationGroup->setChecked(Settings::values.vibration_enabled.GetValue()); + ui->motionGroup->setChecked(Settings::values.motion_enabled.GetValue()); } -void QtControllerSelectorDialog::CallConfigureInputDialog() { - const auto max_supported_players = parameters.enable_single_mode ? 1 : parameters.max_players; - - ConfigureInputDialog dialog(this, max_supported_players, input_subsystem); +void QtControllerSelectorDialog::CallConfigureVibrationDialog() { + ConfigureVibration dialog(this); dialog.setWindowFlags(Qt::Dialog | Qt::CustomizeWindowHint | Qt::WindowTitleHint | Qt::WindowSystemMenuHint); dialog.setWindowModality(Qt::WindowModal); - dialog.exec(); - dialog.ApplyConfiguration(); + if (dialog.exec() == QDialog::Accepted) { + dialog.ApplyConfiguration(); + } +} - LoadConfiguration(); - CheckIfParametersMet(); +void QtControllerSelectorDialog::CallConfigureInputProfileDialog() { + ConfigureInputProfileDialog dialog(this, input_subsystem, input_profiles.get()); + + dialog.setWindowFlags(Qt::Dialog | Qt::CustomizeWindowHint | Qt::WindowTitleHint | + Qt::WindowSystemMenuHint); + dialog.setWindowModality(Qt::WindowModal); + dialog.exec(); } -void QtControllerSelectorDialog::CheckIfParametersMet() { +bool QtControllerSelectorDialog::CheckIfParametersMet() { // Here, we check and validate the current configuration against all applicable parameters. const auto num_connected_players = static_cast<int>( std::count_if(player_groupboxes.begin(), player_groupboxes.end(), @@ -301,7 +297,7 @@ void QtControllerSelectorDialog::CheckIfParametersMet() { num_connected_players > max_supported_players) { parameters_met = false; ui->buttonBox->setEnabled(parameters_met); - return; + return parameters_met; } // Next, check against all connected controllers. @@ -313,7 +309,7 @@ void QtControllerSelectorDialog::CheckIfParametersMet() { } const auto compatible = IsControllerCompatible( - GetControllerTypeFromIndex(emulated_controllers[index]->currentIndex()), + GetControllerTypeFromIndex(emulated_controllers[index]->currentIndex(), index), parameters); // If any controller is found to be incompatible, return false early. @@ -326,18 +322,13 @@ void QtControllerSelectorDialog::CheckIfParametersMet() { return true; }(); - if (!all_controllers_compatible) { - parameters_met = false; - ui->buttonBox->setEnabled(parameters_met); - return; - } - - parameters_met = true; + parameters_met = all_controllers_compatible; ui->buttonBox->setEnabled(parameters_met); + return parameters_met; } void QtControllerSelectorDialog::SetSupportedControllers() { - const QString theme = [this] { + const QString theme = [] { if (QIcon::themeName().contains(QStringLiteral("dark"))) { return QStringLiteral("_dark"); } else if (QIcon::themeName().contains(QStringLiteral("midnight"))) { @@ -402,6 +393,63 @@ void QtControllerSelectorDialog::SetSupportedControllers() { } } +void QtControllerSelectorDialog::SetEmulatedControllers(std::size_t player_index) { + auto& pairs = index_controller_type_pairs[player_index]; + + pairs.clear(); + emulated_controllers[player_index]->clear(); + + pairs.emplace_back(emulated_controllers[player_index]->count(), + Settings::ControllerType::ProController); + emulated_controllers[player_index]->addItem(tr("Pro Controller")); + + pairs.emplace_back(emulated_controllers[player_index]->count(), + Settings::ControllerType::DualJoyconDetached); + emulated_controllers[player_index]->addItem(tr("Dual Joycons")); + + pairs.emplace_back(emulated_controllers[player_index]->count(), + Settings::ControllerType::LeftJoycon); + emulated_controllers[player_index]->addItem(tr("Left Joycon")); + + pairs.emplace_back(emulated_controllers[player_index]->count(), + Settings::ControllerType::RightJoycon); + emulated_controllers[player_index]->addItem(tr("Right Joycon")); + + if (player_index == 0) { + pairs.emplace_back(emulated_controllers[player_index]->count(), + Settings::ControllerType::Handheld); + emulated_controllers[player_index]->addItem(tr("Handheld")); + } +} + +Settings::ControllerType QtControllerSelectorDialog::GetControllerTypeFromIndex( + int index, std::size_t player_index) const { + const auto& pairs = index_controller_type_pairs[player_index]; + + const auto it = std::find_if(pairs.begin(), pairs.end(), + [index](const auto& pair) { return pair.first == index; }); + + if (it == pairs.end()) { + return Settings::ControllerType::ProController; + } + + return it->second; +} + +int QtControllerSelectorDialog::GetIndexFromControllerType(Settings::ControllerType type, + std::size_t player_index) const { + const auto& pairs = index_controller_type_pairs[player_index]; + + const auto it = std::find_if(pairs.begin(), pairs.end(), + [type](const auto& pair) { return pair.second == type; }); + + if (it == pairs.end()) { + return 0; + } + + return it->first; +} + void QtControllerSelectorDialog::UpdateControllerIcon(std::size_t player_index) { if (!player_groupboxes[player_index]->isChecked()) { connected_controller_icons[player_index]->setStyleSheet(QString{}); @@ -410,7 +458,8 @@ void QtControllerSelectorDialog::UpdateControllerIcon(std::size_t player_index) } const QString stylesheet = [this, player_index] { - switch (GetControllerTypeFromIndex(emulated_controllers[player_index]->currentIndex())) { + switch (GetControllerTypeFromIndex(emulated_controllers[player_index]->currentIndex(), + player_index)) { case Settings::ControllerType::ProController: return QStringLiteral("image: url(:/controller/applet_pro_controller%0); "); case Settings::ControllerType::DualJoyconDetached: @@ -426,7 +475,13 @@ void QtControllerSelectorDialog::UpdateControllerIcon(std::size_t player_index) } }(); - const QString theme = [this] { + if (stylesheet.isEmpty()) { + connected_controller_icons[player_index]->setStyleSheet(QString{}); + player_labels[player_index]->show(); + return; + } + + const QString theme = [] { if (QIcon::themeName().contains(QStringLiteral("dark"))) { return QStringLiteral("_dark"); } else if (QIcon::themeName().contains(QStringLiteral("midnight"))) { @@ -441,38 +496,54 @@ void QtControllerSelectorDialog::UpdateControllerIcon(std::size_t player_index) } void QtControllerSelectorDialog::UpdateControllerState(std::size_t player_index) { - auto& player = Settings::values.players[player_index]; + auto& player = Settings::values.players.GetValue()[player_index]; - player.controller_type = - GetControllerTypeFromIndex(emulated_controllers[player_index]->currentIndex()); - player.connected = player_groupboxes[player_index]->isChecked(); + const auto controller_type = GetControllerTypeFromIndex( + emulated_controllers[player_index]->currentIndex(), player_index); + const auto player_connected = player_groupboxes[player_index]->isChecked() && + controller_type != Settings::ControllerType::Handheld; - // Player 2-8 - if (player_index != 0) { - UpdateController(player.controller_type, player_index, player.connected); + if (player.controller_type == controller_type && player.connected == player_connected) { + // Set vibration devices in the event that the input device has changed. + ConfigureVibration::SetVibrationDevices(player_index); return; } - // Player 1 and Handheld - auto& handheld = Settings::values.players[8]; - // If Handheld is selected, copy all the settings from Player 1 to Handheld. - if (player.controller_type == Settings::ControllerType::Handheld) { - handheld = player; - handheld.connected = player_groupboxes[player_index]->isChecked(); - player.connected = false; // Disconnect Player 1 - } else { - player.connected = player_groupboxes[player_index]->isChecked(); - handheld.connected = false; // Disconnect Handheld + // Disconnect the controller first. + UpdateController(controller_type, player_index, false); + + player.controller_type = controller_type; + player.connected = player_connected; + + ConfigureVibration::SetVibrationDevices(player_index); + + // Handheld + if (player_index == 0) { + auto& handheld = Settings::values.players.GetValue()[HANDHELD_INDEX]; + if (controller_type == Settings::ControllerType::Handheld) { + handheld = player; + } + handheld.connected = player_groupboxes[player_index]->isChecked() && + controller_type == Settings::ControllerType::Handheld; + UpdateController(Settings::ControllerType::Handheld, 8, handheld.connected); } - UpdateController(player.controller_type, player_index, player.connected); - UpdateController(Settings::ControllerType::Handheld, 8, handheld.connected); + if (!player.connected) { + return; + } + + // This emulates a delay between disconnecting and reconnecting controllers as some games + // do not respond to a change in controller type if it was instantaneous. + using namespace std::chrono_literals; + std::this_thread::sleep_for(60ms); + + UpdateController(controller_type, player_index, player_connected); } void QtControllerSelectorDialog::UpdateLEDPattern(std::size_t player_index) { if (!player_groupboxes[player_index]->isChecked() || - GetControllerTypeFromIndex(emulated_controllers[player_index]->currentIndex()) == - Settings::ControllerType::Handheld) { + GetControllerTypeFromIndex(emulated_controllers[player_index]->currentIndex(), + player_index) == Settings::ControllerType::Handheld) { led_patterns_boxes[player_index][0]->setChecked(false); led_patterns_boxes[player_index][1]->setChecked(false); led_patterns_boxes[player_index][2]->setChecked(false); @@ -520,8 +591,8 @@ void QtControllerSelectorDialog::UpdateDockedState(bool is_handheld) { ui->radioDocked->setEnabled(!is_handheld); ui->radioUndocked->setEnabled(!is_handheld); - ui->radioDocked->setChecked(Settings::values.use_docked_mode); - ui->radioUndocked->setChecked(!Settings::values.use_docked_mode); + ui->radioDocked->setChecked(Settings::values.use_docked_mode.GetValue()); + ui->radioUndocked->setChecked(!Settings::values.use_docked_mode.GetValue()); // Also force into undocked mode if the controller type is handheld. if (is_handheld) { @@ -564,8 +635,8 @@ void QtControllerSelectorDialog::DisableUnsupportedPlayers() { for (std::size_t index = max_supported_players; index < NUM_PLAYERS; ++index) { // Disconnect any unsupported players here and disable or hide them if applicable. - Settings::values.players[index].connected = false; - UpdateController(Settings::values.players[index].controller_type, index, false); + Settings::values.players.GetValue()[index].connected = false; + UpdateController(Settings::values.players.GetValue()[index].controller_type, index, false); // Hide the player widgets when max_supported_controllers is less than or equal to 4. if (max_supported_players <= 4) { player_widgets[index]->hide(); @@ -589,13 +660,13 @@ QtControllerSelector::QtControllerSelector(GMainWindow& parent) { QtControllerSelector::~QtControllerSelector() = default; void QtControllerSelector::ReconfigureControllers( - std::function<void()> callback, Core::Frontend::ControllerParameters parameters) const { - this->callback = std::move(callback); + std::function<void()> callback_, const Core::Frontend::ControllerParameters& parameters) const { + callback = std::move(callback_); emit MainWindowReconfigureControllers(parameters); } void QtControllerSelector::MainWindowReconfigureFinished() { // Acquire the HLE mutex - std::lock_guard<std::recursive_mutex> lock(HLE::g_hle_lock); + std::lock_guard lock(HLE::g_hle_lock); callback(); } diff --git a/src/yuzu/applets/controller.h b/src/yuzu/applets/controller.h index 2d6d588c6..3518eed56 100644 --- a/src/yuzu/applets/controller.h +++ b/src/yuzu/applets/controller.h @@ -16,10 +16,16 @@ class QDialogButtonBox; class QGroupBox; class QLabel; +class InputProfiles; + namespace InputCommon { class InputSubsystem; } +namespace Settings { +enum class ControllerType; +} + namespace Ui { class QtControllerSelectorDialog; } @@ -33,6 +39,8 @@ public: InputCommon::InputSubsystem* input_subsystem_); ~QtControllerSelectorDialog() override; + int exec() override; + private: // Applies the current configuration. void ApplyConfiguration(); @@ -40,16 +48,28 @@ private: // Loads the current input configuration into the frontend applet. void LoadConfiguration(); - // Initializes the "Configure Input" Dialog. - void CallConfigureInputDialog(); + // Initializes the "Configure Vibration" Dialog. + void CallConfigureVibrationDialog(); + + // Initializes the "Create Input Profile" Dialog. + void CallConfigureInputProfileDialog(); - // Checks the current configuration against the given parameters and - // sets the value of parameters_met. - void CheckIfParametersMet(); + // Checks the current configuration against the given parameters. + // This sets and returns the value of parameters_met. + bool CheckIfParametersMet(); // Sets the controller icons for "Supported Controller Types". void SetSupportedControllers(); + // Sets the emulated controllers per player. + void SetEmulatedControllers(std::size_t player_index); + + // Gets the Controller Type for a given controller combobox index per player. + Settings::ControllerType GetControllerTypeFromIndex(int index, std::size_t player_index) const; + + // Gets the controller combobox index for a given Controller Type per player. + int GetIndexFromControllerType(Settings::ControllerType type, std::size_t player_index) const; + // Updates the controller icons per player. void UpdateControllerIcon(std::size_t player_index); @@ -78,6 +98,8 @@ private: InputCommon::InputSubsystem* input_subsystem; + std::unique_ptr<InputProfiles> input_profiles; + // This is true if and only if all parameters are met. Otherwise, this is false. // This determines whether the "OK" button can be clicked to exit the applet. bool parameters_met{false}; @@ -105,6 +127,10 @@ private: // Comboboxes with a list of emulated controllers per player. std::array<QComboBox*, NUM_PLAYERS> emulated_controllers; + /// Pairs of emulated controller index and Controller Type enum per player. + std::array<std::vector<std::pair<int, Settings::ControllerType>>, NUM_PLAYERS> + index_controller_type_pairs; + // Labels representing the number of connected controllers // above the "Connected Controllers" checkboxes. std::array<QLabel*, NUM_PLAYERS> connected_controller_labels; @@ -120,11 +146,13 @@ public: explicit QtControllerSelector(GMainWindow& parent); ~QtControllerSelector() override; - void ReconfigureControllers(std::function<void()> callback, - Core::Frontend::ControllerParameters parameters) const override; + void ReconfigureControllers( + std::function<void()> callback_, + const Core::Frontend::ControllerParameters& parameters) const override; signals: - void MainWindowReconfigureControllers(Core::Frontend::ControllerParameters parameters) const; + void MainWindowReconfigureControllers( + const Core::Frontend::ControllerParameters& parameters) const; private: void MainWindowReconfigureFinished(); diff --git a/src/yuzu/applets/controller.ui b/src/yuzu/applets/controller.ui index c4108a979..c8cb6bcf3 100644 --- a/src/yuzu/applets/controller.ui +++ b/src/yuzu/applets/controller.ui @@ -1217,9 +1217,6 @@ </item> <item> <widget class="QComboBox" name="comboPlayer3Emulated"> - <property name="editable"> - <bool>false</bool> - </property> <item> <property name="text"> <string>Pro Controller</string> @@ -2279,7 +2276,7 @@ <number>6</number> </property> <property name="leftMargin"> - <number>6</number> + <number>8</number> </property> <property name="topMargin"> <number>6</number> @@ -2332,30 +2329,24 @@ <number>3</number> </property> <item> - <widget class="QSpinBox" name="vibrationSpin"> + <widget class="QPushButton" name="vibrationButton"> <property name="minimumSize"> <size> - <width>65</width> + <width>68</width> <height>0</height> </size> </property> <property name="maximumSize"> <size> - <width>65</width> + <width>68</width> <height>16777215</height> </size> </property> - <property name="suffix"> - <string>%</string> - </property> - <property name="minimum"> - <number>1</number> - </property> - <property name="maximum"> - <number>200</number> + <property name="styleSheet"> + <string notr="true">min-width: 68px;</string> </property> - <property name="value"> - <number>100</number> + <property name="text"> + <string>Configure</string> </property> </widget> </item> @@ -2387,18 +2378,18 @@ <widget class="QPushButton" name="motionButton"> <property name="minimumSize"> <size> - <width>57</width> + <width>68</width> <height>0</height> </size> </property> <property name="maximumSize"> <size> - <width>55</width> + <width>68</width> <height>16777215</height> </size> </property> <property name="styleSheet"> - <string notr="true">min-width: 55px;</string> + <string notr="true">min-width: 68px;</string> </property> <property name="text"> <string>Configure</string> @@ -2411,7 +2402,7 @@ <item> <widget class="QGroupBox" name="inputConfigGroup"> <property name="title"> - <string>Input Config</string> + <string>Profiles</string> </property> <layout class="QHBoxLayout" name="horizontalLayout_7"> <property name="leftMargin"> @@ -2430,15 +2421,15 @@ <widget class="QPushButton" name="inputConfigButton"> <property name="maximumSize"> <size> - <width>65</width> + <width>68</width> <height>16777215</height> </size> </property> <property name="styleSheet"> - <string notr="true">min-width: 55px;</string> + <string notr="true">min-width: 68px;</string> </property> <property name="text"> - <string>Open</string> + <string>Create</string> </property> </widget> </item> @@ -2657,16 +2648,6 @@ <signal>accepted()</signal> <receiver>QtControllerSelectorDialog</receiver> <slot>accept()</slot> - <hints> - <hint type="sourcelabel"> - <x>20</x> - <y>20</y> - </hint> - <hint type="destinationlabel"> - <x>20</x> - <y>20</y> - </hint> - </hints> </connection> </connections> </ui> diff --git a/src/yuzu/applets/error.cpp b/src/yuzu/applets/error.cpp index 08ed57355..8ee03ddb3 100644 --- a/src/yuzu/applets/error.cpp +++ b/src/yuzu/applets/error.cpp @@ -17,9 +17,9 @@ QtErrorDisplay::QtErrorDisplay(GMainWindow& parent) { QtErrorDisplay::~QtErrorDisplay() = default; void QtErrorDisplay::ShowError(ResultCode error, std::function<void()> finished) const { - this->callback = std::move(finished); + callback = std::move(finished); emit MainWindowDisplayError( - tr("An error has occured.\nPlease try again or contact the developer of the " + tr("An error has occurred.\nPlease try again or contact the developer of the " "software.\n\nError Code: %1-%2 (0x%3)") .arg(static_cast<u32>(error.module.Value()) + 2000, 4, 10, QChar::fromLatin1('0')) .arg(error.description, 4, 10, QChar::fromLatin1('0')) @@ -28,11 +28,11 @@ void QtErrorDisplay::ShowError(ResultCode error, std::function<void()> finished) void QtErrorDisplay::ShowErrorWithTimestamp(ResultCode error, std::chrono::seconds time, std::function<void()> finished) const { - this->callback = std::move(finished); + callback = std::move(finished); const QDateTime date_time = QDateTime::fromSecsSinceEpoch(time.count()); emit MainWindowDisplayError( - tr("An error occured on %1 at %2.\nPlease try again or contact the " + tr("An error occurred on %1 at %2.\nPlease try again or contact the " "developer of the software.\n\nError Code: %3-%4 (0x%5)") .arg(date_time.toString(QStringLiteral("dddd, MMMM d, yyyy"))) .arg(date_time.toString(QStringLiteral("h:mm:ss A"))) @@ -44,9 +44,9 @@ void QtErrorDisplay::ShowErrorWithTimestamp(ResultCode error, std::chrono::secon void QtErrorDisplay::ShowCustomErrorText(ResultCode error, std::string dialog_text, std::string fullscreen_text, std::function<void()> finished) const { - this->callback = std::move(finished); + callback = std::move(finished); emit MainWindowDisplayError( - tr("An error has occured.\nError Code: %1-%2 (0x%3)\n\n%4\n\n%5") + tr("An error has occurred.\nError Code: %1-%2 (0x%3)\n\n%4\n\n%5") .arg(static_cast<u32>(error.module.Value()) + 2000, 4, 10, QChar::fromLatin1('0')) .arg(error.description, 4, 10, QChar::fromLatin1('0')) .arg(error.raw, 8, 16, QChar::fromLatin1('0')) diff --git a/src/yuzu/applets/profile_select.cpp b/src/yuzu/applets/profile_select.cpp index dca8835ed..4bf2bfd40 100644 --- a/src/yuzu/applets/profile_select.cpp +++ b/src/yuzu/applets/profile_select.cpp @@ -114,6 +114,15 @@ QtProfileSelectionDialog::QtProfileSelectionDialog(QWidget* parent) QtProfileSelectionDialog::~QtProfileSelectionDialog() = default; +int QtProfileSelectionDialog::exec() { + // Skip profile selection when there's only one. + if (profile_manager->GetUserCount() == 1) { + user_index = 0; + return QDialog::Accepted; + } + return QDialog::exec(); +} + void QtProfileSelectionDialog::accept() { QDialog::accept(); } @@ -141,8 +150,8 @@ QtProfileSelector::QtProfileSelector(GMainWindow& parent) { QtProfileSelector::~QtProfileSelector() = default; void QtProfileSelector::SelectProfile( - std::function<void(std::optional<Common::UUID>)> callback) const { - this->callback = std::move(callback); + std::function<void(std::optional<Common::UUID>)> callback_) const { + callback = std::move(callback_); emit MainWindowSelectProfile(); } diff --git a/src/yuzu/applets/profile_select.h b/src/yuzu/applets/profile_select.h index cee886a77..4e9037488 100644 --- a/src/yuzu/applets/profile_select.h +++ b/src/yuzu/applets/profile_select.h @@ -27,6 +27,7 @@ public: explicit QtProfileSelectionDialog(QWidget* parent); ~QtProfileSelectionDialog() override; + int exec() override; void accept() override; void reject() override; @@ -59,7 +60,7 @@ public: explicit QtProfileSelector(GMainWindow& parent); ~QtProfileSelector() override; - void SelectProfile(std::function<void(std::optional<Common::UUID>)> callback) const override; + void SelectProfile(std::function<void(std::optional<Common::UUID>)> callback_) const override; signals: void MainWindowSelectProfile() const; diff --git a/src/yuzu/applets/software_keyboard.cpp b/src/yuzu/applets/software_keyboard.cpp index af36f07c6..ab8cfd8ee 100644 --- a/src/yuzu/applets/software_keyboard.cpp +++ b/src/yuzu/applets/software_keyboard.cpp @@ -135,8 +135,8 @@ void QtSoftwareKeyboard::RequestText(std::function<void(std::optional<std::u16st } void QtSoftwareKeyboard::SendTextCheckDialog(std::u16string error_message, - std::function<void()> finished_check) const { - this->finished_check = std::move(finished_check); + std::function<void()> finished_check_) const { + finished_check = std::move(finished_check_); emit MainWindowTextCheckDialog(error_message); } diff --git a/src/yuzu/applets/software_keyboard.h b/src/yuzu/applets/software_keyboard.h index 44bcece75..9e1094cce 100644 --- a/src/yuzu/applets/software_keyboard.h +++ b/src/yuzu/applets/software_keyboard.h @@ -61,7 +61,7 @@ public: void RequestText(std::function<void(std::optional<std::u16string>)> out, Core::Frontend::SoftwareKeyboardParameters parameters) const override; void SendTextCheckDialog(std::u16string error_message, - std::function<void()> finished_check) const override; + std::function<void()> finished_check_) const override; signals: void MainWindowGetText(Core::Frontend::SoftwareKeyboardParameters parameters) const; diff --git a/src/yuzu/applets/web_browser.cpp b/src/yuzu/applets/web_browser.cpp index 33f1c385d..e482ba029 100644 --- a/src/yuzu/applets/web_browser.cpp +++ b/src/yuzu/applets/web_browser.cpp @@ -1,115 +1,414 @@ -// Copyright 2018 yuzu Emulator Project +// Copyright 2020 yuzu Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. -#include <mutex> - +#ifdef YUZU_USE_QT_WEB_ENGINE #include <QKeyEvent> -#include "core/hle/lock.h" +#include <QWebEngineProfile> +#include <QWebEngineScript> +#include <QWebEngineScriptCollection> +#include <QWebEngineSettings> +#include <QWebEngineUrlScheme> +#endif + +#include "common/file_util.h" +#include "core/core.h" +#include "core/frontend/input_interpreter.h" +#include "input_common/keyboard.h" +#include "input_common/main.h" #include "yuzu/applets/web_browser.h" +#include "yuzu/applets/web_browser_scripts.h" #include "yuzu/main.h" +#include "yuzu/util/url_request_interceptor.h" #ifdef YUZU_USE_QT_WEB_ENGINE -constexpr char NX_SHIM_INJECT_SCRIPT[] = R"( - window.nx = {}; - window.nx.playReport = {}; - window.nx.playReport.setCounterSetIdentifier = function () { - console.log("nx.playReport.setCounterSetIdentifier called - unimplemented"); - }; +namespace { - window.nx.playReport.incrementCounter = function () { - console.log("nx.playReport.incrementCounter called - unimplemented"); - }; +constexpr int HIDButtonToKey(HIDButton button) { + switch (button) { + case HIDButton::DLeft: + case HIDButton::LStickLeft: + return Qt::Key_Left; + case HIDButton::DUp: + case HIDButton::LStickUp: + return Qt::Key_Up; + case HIDButton::DRight: + case HIDButton::LStickRight: + return Qt::Key_Right; + case HIDButton::DDown: + case HIDButton::LStickDown: + return Qt::Key_Down; + default: + return 0; + } +} + +} // Anonymous namespace + +QtNXWebEngineView::QtNXWebEngineView(QWidget* parent, Core::System& system, + InputCommon::InputSubsystem* input_subsystem_) + : QWebEngineView(parent), input_subsystem{input_subsystem_}, + url_interceptor(std::make_unique<UrlRequestInterceptor>()), + input_interpreter(std::make_unique<InputInterpreter>(system)), + default_profile{QWebEngineProfile::defaultProfile()}, + global_settings{QWebEngineSettings::globalSettings()} { + QWebEngineScript gamepad; + QWebEngineScript window_nx; + + gamepad.setName(QStringLiteral("gamepad_script.js")); + window_nx.setName(QStringLiteral("window_nx_script.js")); + + gamepad.setSourceCode(QString::fromStdString(GAMEPAD_SCRIPT)); + window_nx.setSourceCode(QString::fromStdString(WINDOW_NX_SCRIPT)); + + gamepad.setInjectionPoint(QWebEngineScript::DocumentCreation); + window_nx.setInjectionPoint(QWebEngineScript::DocumentCreation); + + gamepad.setWorldId(QWebEngineScript::MainWorld); + window_nx.setWorldId(QWebEngineScript::MainWorld); + + gamepad.setRunsOnSubFrames(true); + window_nx.setRunsOnSubFrames(true); + + default_profile->scripts()->insert(gamepad); + default_profile->scripts()->insert(window_nx); + + default_profile->setRequestInterceptor(url_interceptor.get()); + + global_settings->setAttribute(QWebEngineSettings::LocalContentCanAccessRemoteUrls, true); + global_settings->setAttribute(QWebEngineSettings::FullScreenSupportEnabled, true); + global_settings->setAttribute(QWebEngineSettings::AllowRunningInsecureContent, true); + global_settings->setAttribute(QWebEngineSettings::FocusOnNavigationEnabled, true); + global_settings->setAttribute(QWebEngineSettings::AllowWindowActivationFromJavaScript, true); + global_settings->setAttribute(QWebEngineSettings::ShowScrollBars, false); + + global_settings->setFontFamily(QWebEngineSettings::StandardFont, QStringLiteral("Roboto")); + + connect( + page(), &QWebEnginePage::windowCloseRequested, page(), + [this] { + if (page()->url() == url_interceptor->GetRequestedURL()) { + SetFinished(true); + SetExitReason(Service::AM::Applets::WebExitReason::WindowClosed); + } + }, + Qt::QueuedConnection); +} + +QtNXWebEngineView::~QtNXWebEngineView() { + SetFinished(true); + StopInputThread(); +} + +void QtNXWebEngineView::LoadLocalWebPage(std::string_view main_url, + std::string_view additional_args) { + is_local = true; + + LoadExtractedFonts(); + SetUserAgent(UserAgent::WebApplet); + SetFinished(false); + SetExitReason(Service::AM::Applets::WebExitReason::EndButtonPressed); + SetLastURL("http://localhost/"); + StartInputThread(); + + load(QUrl(QUrl::fromLocalFile(QString::fromStdString(std::string(main_url))).toString() + + QString::fromStdString(std::string(additional_args)))); +} + +void QtNXWebEngineView::LoadExternalWebPage(std::string_view main_url, + std::string_view additional_args) { + is_local = false; + + SetUserAgent(UserAgent::WebApplet); + SetFinished(false); + SetExitReason(Service::AM::Applets::WebExitReason::EndButtonPressed); + SetLastURL("http://localhost/"); + StartInputThread(); + + load(QUrl(QString::fromStdString(std::string(main_url)) + + QString::fromStdString(std::string(additional_args)))); +} + +void QtNXWebEngineView::SetUserAgent(UserAgent user_agent) { + const QString user_agent_str = [user_agent] { + switch (user_agent) { + case UserAgent::WebApplet: + default: + return QStringLiteral("WebApplet"); + case UserAgent::ShopN: + return QStringLiteral("ShopN"); + case UserAgent::LoginApplet: + return QStringLiteral("LoginApplet"); + case UserAgent::ShareApplet: + return QStringLiteral("ShareApplet"); + case UserAgent::LobbyApplet: + return QStringLiteral("LobbyApplet"); + case UserAgent::WifiWebAuthApplet: + return QStringLiteral("WifiWebAuthApplet"); + } + }(); + + QWebEngineProfile::defaultProfile()->setHttpUserAgent( + QStringLiteral("Mozilla/5.0 (Nintendo Switch; %1) AppleWebKit/606.4 " + "(KHTML, like Gecko) NF/6.0.1.15.4 NintendoBrowser/5.1.0.20389") + .arg(user_agent_str)); +} + +bool QtNXWebEngineView::IsFinished() const { + return finished; +} + +void QtNXWebEngineView::SetFinished(bool finished_) { + finished = finished_; +} + +Service::AM::Applets::WebExitReason QtNXWebEngineView::GetExitReason() const { + return exit_reason; +} + +void QtNXWebEngineView::SetExitReason(Service::AM::Applets::WebExitReason exit_reason_) { + exit_reason = exit_reason_; +} + +const std::string& QtNXWebEngineView::GetLastURL() const { + return last_url; +} + +void QtNXWebEngineView::SetLastURL(std::string last_url_) { + last_url = std::move(last_url_); +} + +QString QtNXWebEngineView::GetCurrentURL() const { + return url_interceptor->GetRequestedURL().toString(); +} + +void QtNXWebEngineView::hide() { + SetFinished(true); + StopInputThread(); - window.nx.footer = {}; - window.nx.footer.unsetAssign = function () { - console.log("nx.footer.unsetAssign called - unimplemented"); + QWidget::hide(); +} + +void QtNXWebEngineView::keyPressEvent(QKeyEvent* event) { + if (is_local) { + input_subsystem->GetKeyboard()->PressKey(event->key()); + } +} + +void QtNXWebEngineView::keyReleaseEvent(QKeyEvent* event) { + if (is_local) { + input_subsystem->GetKeyboard()->ReleaseKey(event->key()); + } +} + +template <HIDButton... T> +void QtNXWebEngineView::HandleWindowFooterButtonPressedOnce() { + const auto f = [this](HIDButton button) { + if (input_interpreter->IsButtonPressedOnce(button)) { + page()->runJavaScript( + QStringLiteral("yuzu_key_callbacks[%1] == null;").arg(static_cast<u8>(button)), + [&](const QVariant& variant) { + if (variant.toBool()) { + switch (button) { + case HIDButton::A: + SendMultipleKeyPressEvents<Qt::Key_A, Qt::Key_Space, Qt::Key_Return>(); + break; + case HIDButton::B: + SendKeyPressEvent(Qt::Key_B); + break; + case HIDButton::X: + SendKeyPressEvent(Qt::Key_X); + break; + case HIDButton::Y: + SendKeyPressEvent(Qt::Key_Y); + break; + default: + break; + } + } + }); + + page()->runJavaScript( + QStringLiteral("if (yuzu_key_callbacks[%1] != null) { yuzu_key_callbacks[%1](); }") + .arg(static_cast<u8>(button))); + } }; - var yuzu_key_callbacks = []; - window.nx.footer.setAssign = function(key, discard1, func, discard2) { - switch (key) { - case 'A': - yuzu_key_callbacks[0] = func; - break; - case 'B': - yuzu_key_callbacks[1] = func; - break; - case 'X': - yuzu_key_callbacks[2] = func; - break; - case 'Y': - yuzu_key_callbacks[3] = func; - break; - case 'L': - yuzu_key_callbacks[6] = func; - break; - case 'R': - yuzu_key_callbacks[7] = func; - break; + (f(T), ...); +} + +template <HIDButton... T> +void QtNXWebEngineView::HandleWindowKeyButtonPressedOnce() { + const auto f = [this](HIDButton button) { + if (input_interpreter->IsButtonPressedOnce(button)) { + SendKeyPressEvent(HIDButtonToKey(button)); } }; - var applet_done = false; - window.nx.endApplet = function() { - applet_done = true; + (f(T), ...); +} + +template <HIDButton... T> +void QtNXWebEngineView::HandleWindowKeyButtonHold() { + const auto f = [this](HIDButton button) { + if (input_interpreter->IsButtonHeld(button)) { + SendKeyPressEvent(HIDButtonToKey(button)); + } }; - window.onkeypress = function(e) { if (e.keyCode === 13) { applet_done = true; } }; -)"; + (f(T), ...); +} + +void QtNXWebEngineView::SendKeyPressEvent(int key) { + if (key == 0) { + return; + } + + QCoreApplication::postEvent(focusProxy(), + new QKeyEvent(QKeyEvent::KeyPress, key, Qt::NoModifier)); + QCoreApplication::postEvent(focusProxy(), + new QKeyEvent(QKeyEvent::KeyRelease, key, Qt::NoModifier)); +} + +void QtNXWebEngineView::StartInputThread() { + if (input_thread_running) { + return; + } + + input_thread_running = true; + input_thread = std::thread(&QtNXWebEngineView::InputThread, this); +} + +void QtNXWebEngineView::StopInputThread() { + if (is_local) { + QWidget::releaseKeyboard(); + } -QString GetNXShimInjectionScript() { - return QString::fromStdString(NX_SHIM_INJECT_SCRIPT); + input_thread_running = false; + if (input_thread.joinable()) { + input_thread.join(); + } } -NXInputWebEngineView::NXInputWebEngineView(QWidget* parent) : QWebEngineView(parent) {} +void QtNXWebEngineView::InputThread() { + // Wait for 1 second before allowing any inputs to be processed. + std::this_thread::sleep_for(std::chrono::seconds(1)); + + if (is_local) { + QWidget::grabKeyboard(); + } + + while (input_thread_running) { + input_interpreter->PollInput(); + + HandleWindowFooterButtonPressedOnce<HIDButton::A, HIDButton::B, HIDButton::X, HIDButton::Y, + HIDButton::L, HIDButton::R>(); + + HandleWindowKeyButtonPressedOnce<HIDButton::DLeft, HIDButton::DUp, HIDButton::DRight, + HIDButton::DDown, HIDButton::LStickLeft, + HIDButton::LStickUp, HIDButton::LStickRight, + HIDButton::LStickDown>(); -void NXInputWebEngineView::keyPressEvent(QKeyEvent* event) { - parent()->event(event); + HandleWindowKeyButtonHold<HIDButton::DLeft, HIDButton::DUp, HIDButton::DRight, + HIDButton::DDown, HIDButton::LStickLeft, HIDButton::LStickUp, + HIDButton::LStickRight, HIDButton::LStickDown>(); + + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + } } -void NXInputWebEngineView::keyReleaseEvent(QKeyEvent* event) { - parent()->event(event); +void QtNXWebEngineView::LoadExtractedFonts() { + QWebEngineScript nx_font_css; + QWebEngineScript load_nx_font; + + const QString fonts_dir = QString::fromStdString(Common::FS::SanitizePath( + fmt::format("{}/fonts", Common::FS::GetUserPath(Common::FS::UserPath::CacheDir)))); + + nx_font_css.setName(QStringLiteral("nx_font_css.js")); + load_nx_font.setName(QStringLiteral("load_nx_font.js")); + + nx_font_css.setSourceCode( + QString::fromStdString(NX_FONT_CSS) + .arg(fonts_dir + QStringLiteral("/FontStandard.ttf")) + .arg(fonts_dir + QStringLiteral("/FontChineseSimplified.ttf")) + .arg(fonts_dir + QStringLiteral("/FontExtendedChineseSimplified.ttf")) + .arg(fonts_dir + QStringLiteral("/FontChineseTraditional.ttf")) + .arg(fonts_dir + QStringLiteral("/FontKorean.ttf")) + .arg(fonts_dir + QStringLiteral("/FontNintendoExtended.ttf")) + .arg(fonts_dir + QStringLiteral("/FontNintendoExtended2.ttf"))); + load_nx_font.setSourceCode(QString::fromStdString(LOAD_NX_FONT)); + + nx_font_css.setInjectionPoint(QWebEngineScript::DocumentReady); + load_nx_font.setInjectionPoint(QWebEngineScript::Deferred); + + nx_font_css.setWorldId(QWebEngineScript::MainWorld); + load_nx_font.setWorldId(QWebEngineScript::MainWorld); + + nx_font_css.setRunsOnSubFrames(true); + load_nx_font.setRunsOnSubFrames(true); + + default_profile->scripts()->insert(nx_font_css); + default_profile->scripts()->insert(load_nx_font); + + connect( + url_interceptor.get(), &UrlRequestInterceptor::FrameChanged, url_interceptor.get(), + [this] { + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + page()->runJavaScript(QString::fromStdString(LOAD_NX_FONT)); + }, + Qt::QueuedConnection); } #endif QtWebBrowser::QtWebBrowser(GMainWindow& main_window) { - connect(this, &QtWebBrowser::MainWindowOpenPage, &main_window, &GMainWindow::WebBrowserOpenPage, - Qt::QueuedConnection); - connect(&main_window, &GMainWindow::WebBrowserUnpackRomFS, this, - &QtWebBrowser::MainWindowUnpackRomFS, Qt::QueuedConnection); - connect(&main_window, &GMainWindow::WebBrowserFinishedBrowsing, this, - &QtWebBrowser::MainWindowFinishedBrowsing, Qt::QueuedConnection); + connect(this, &QtWebBrowser::MainWindowOpenWebPage, &main_window, + &GMainWindow::WebBrowserOpenWebPage, Qt::QueuedConnection); + connect(&main_window, &GMainWindow::WebBrowserExtractOfflineRomFS, this, + &QtWebBrowser::MainWindowExtractOfflineRomFS, Qt::QueuedConnection); + connect(&main_window, &GMainWindow::WebBrowserClosed, this, + &QtWebBrowser::MainWindowWebBrowserClosed, Qt::QueuedConnection); } QtWebBrowser::~QtWebBrowser() = default; -void QtWebBrowser::OpenPageLocal(std::string_view url, std::function<void()> unpack_romfs_callback, - std::function<void()> finished_callback) { - this->unpack_romfs_callback = std::move(unpack_romfs_callback); - this->finished_callback = std::move(finished_callback); +void QtWebBrowser::OpenLocalWebPage( + std::string_view local_url, std::function<void()> extract_romfs_callback_, + std::function<void(Service::AM::Applets::WebExitReason, std::string)> callback_) const { + extract_romfs_callback = std::move(extract_romfs_callback_); + callback = std::move(callback_); + + const auto index = local_url.find('?'); + + if (index == std::string::npos) { + emit MainWindowOpenWebPage(local_url, "", true); + } else { + emit MainWindowOpenWebPage(local_url.substr(0, index), local_url.substr(index), true); + } +} + +void QtWebBrowser::OpenExternalWebPage( + std::string_view external_url, + std::function<void(Service::AM::Applets::WebExitReason, std::string)> callback_) const { + callback = std::move(callback_); + + const auto index = external_url.find('?'); - const auto index = url.find('?'); if (index == std::string::npos) { - emit MainWindowOpenPage(url, ""); + emit MainWindowOpenWebPage(external_url, "", false); } else { - const auto front = url.substr(0, index); - const auto back = url.substr(index); - emit MainWindowOpenPage(front, back); + emit MainWindowOpenWebPage(external_url.substr(0, index), external_url.substr(index), + false); } } -void QtWebBrowser::MainWindowUnpackRomFS() { - // Acquire the HLE mutex - std::lock_guard lock{HLE::g_hle_lock}; - unpack_romfs_callback(); +void QtWebBrowser::MainWindowExtractOfflineRomFS() { + extract_romfs_callback(); } -void QtWebBrowser::MainWindowFinishedBrowsing() { - // Acquire the HLE mutex - std::lock_guard lock{HLE::g_hle_lock}; - finished_callback(); +void QtWebBrowser::MainWindowWebBrowserClosed(Service::AM::Applets::WebExitReason exit_reason, + std::string last_url) { + callback(exit_reason, last_url); } diff --git a/src/yuzu/applets/web_browser.h b/src/yuzu/applets/web_browser.h index b38437e46..47f960d69 100644 --- a/src/yuzu/applets/web_browser.h +++ b/src/yuzu/applets/web_browser.h @@ -1,10 +1,13 @@ -// Copyright 2018 yuzu Emulator Project +// Copyright 2020 yuzu Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. #pragma once -#include <functional> +#include <atomic> +#include <memory> +#include <thread> + #include <QObject> #ifdef YUZU_USE_QT_WEB_ENGINE @@ -13,19 +16,172 @@ #include "core/frontend/applets/web_browser.h" +enum class HIDButton : u8; + class GMainWindow; +class InputInterpreter; +class UrlRequestInterceptor; + +namespace Core { +class System; +} + +namespace InputCommon { +class InputSubsystem; +} #ifdef YUZU_USE_QT_WEB_ENGINE -QString GetNXShimInjectionScript(); +enum class UserAgent { + WebApplet, + ShopN, + LoginApplet, + ShareApplet, + LobbyApplet, + WifiWebAuthApplet, +}; + +class QWebEngineProfile; +class QWebEngineSettings; + +class QtNXWebEngineView : public QWebEngineView { + Q_OBJECT -class NXInputWebEngineView : public QWebEngineView { public: - explicit NXInputWebEngineView(QWidget* parent = nullptr); + explicit QtNXWebEngineView(QWidget* parent, Core::System& system, + InputCommon::InputSubsystem* input_subsystem_); + ~QtNXWebEngineView() override; + + /** + * Loads a HTML document that exists locally. Cannot be used to load external websites. + * + * @param main_url The url to the file. + * @param additional_args Additional arguments appended to the main url. + */ + void LoadLocalWebPage(std::string_view main_url, std::string_view additional_args); + + /** + * Loads an external website. Cannot be used to load local urls. + * + * @param main_url The url to the website. + * @param additional_args Additional arguments appended to the main url. + */ + void LoadExternalWebPage(std::string_view main_url, std::string_view additional_args); + + /** + * Sets the background color of the web page. + * + * @param color The color to set. + */ + void SetBackgroundColor(QColor color); + + /** + * Sets the user agent of the web browser. + * + * @param user_agent The user agent enum. + */ + void SetUserAgent(UserAgent user_agent); + + [[nodiscard]] bool IsFinished() const; + void SetFinished(bool finished_); + + [[nodiscard]] Service::AM::Applets::WebExitReason GetExitReason() const; + void SetExitReason(Service::AM::Applets::WebExitReason exit_reason_); + + [[nodiscard]] const std::string& GetLastURL() const; + void SetLastURL(std::string last_url_); + + /** + * This gets the current URL that has been requested by the webpage. + * This only applies to the main frame. Sub frames and other resources are ignored. + * + * @return Currently requested URL + */ + [[nodiscard]] QString GetCurrentURL() const; + +public slots: + void hide(); protected: void keyPressEvent(QKeyEvent* event) override; void keyReleaseEvent(QKeyEvent* event) override; + +private: + /** + * Handles button presses to execute functions assigned in yuzu_key_callbacks. + * yuzu_key_callbacks contains specialized functions for the buttons in the window footer + * that can be overriden by games to achieve desired functionality. + * + * @tparam HIDButton The list of buttons contained in yuzu_key_callbacks + */ + template <HIDButton... T> + void HandleWindowFooterButtonPressedOnce(); + + /** + * Handles button presses and converts them into keyboard input. + * This should only be used to convert D-Pad or Analog Stick input into arrow keys. + * + * @tparam HIDButton The list of buttons that can be converted into keyboard input. + */ + template <HIDButton... T> + void HandleWindowKeyButtonPressedOnce(); + + /** + * Handles button holds and converts them into keyboard input. + * This should only be used to convert D-Pad or Analog Stick input into arrow keys. + * + * @tparam HIDButton The list of buttons that can be converted into keyboard input. + */ + template <HIDButton... T> + void HandleWindowKeyButtonHold(); + + /** + * Sends a key press event to QWebEngineView. + * + * @param key Qt key code. + */ + void SendKeyPressEvent(int key); + + /** + * Sends multiple key press events to QWebEngineView. + * + * @tparam int Qt key code. + */ + template <int... T> + void SendMultipleKeyPressEvents() { + (SendKeyPressEvent(T), ...); + } + + void StartInputThread(); + void StopInputThread(); + + /// The thread where input is being polled and processed. + void InputThread(); + + /// Loads the extracted fonts using JavaScript. + void LoadExtractedFonts(); + + InputCommon::InputSubsystem* input_subsystem; + + std::unique_ptr<UrlRequestInterceptor> url_interceptor; + + std::unique_ptr<InputInterpreter> input_interpreter; + + std::thread input_thread; + + std::atomic<bool> input_thread_running{}; + + std::atomic<bool> finished{}; + + Service::AM::Applets::WebExitReason exit_reason{ + Service::AM::Applets::WebExitReason::EndButtonPressed}; + + std::string last_url{"http://localhost/"}; + + bool is_local{}; + + QWebEngineProfile* default_profile; + QWebEngineSettings* global_settings; }; #endif @@ -34,19 +190,28 @@ class QtWebBrowser final : public QObject, public Core::Frontend::WebBrowserAppl Q_OBJECT public: - explicit QtWebBrowser(GMainWindow& main_window); + explicit QtWebBrowser(GMainWindow& parent); ~QtWebBrowser() override; - void OpenPageLocal(std::string_view url, std::function<void()> unpack_romfs_callback, - std::function<void()> finished_callback) override; + void OpenLocalWebPage(std::string_view local_url, std::function<void()> extract_romfs_callback_, + std::function<void(Service::AM::Applets::WebExitReason, std::string)> + callback_) const override; + + void OpenExternalWebPage(std::string_view external_url, + std::function<void(Service::AM::Applets::WebExitReason, std::string)> + callback_) const override; signals: - void MainWindowOpenPage(std::string_view filename, std::string_view additional_args) const; + void MainWindowOpenWebPage(std::string_view main_url, std::string_view additional_args, + bool is_local) const; private: - void MainWindowUnpackRomFS(); - void MainWindowFinishedBrowsing(); + void MainWindowExtractOfflineRomFS(); + + void MainWindowWebBrowserClosed(Service::AM::Applets::WebExitReason exit_reason, + std::string last_url); + + mutable std::function<void()> extract_romfs_callback; - std::function<void()> unpack_romfs_callback; - std::function<void()> finished_callback; + mutable std::function<void(Service::AM::Applets::WebExitReason, std::string)> callback; }; diff --git a/src/yuzu/applets/web_browser_scripts.h b/src/yuzu/applets/web_browser_scripts.h new file mode 100644 index 000000000..992837a85 --- /dev/null +++ b/src/yuzu/applets/web_browser_scripts.h @@ -0,0 +1,193 @@ +// Copyright 2020 yuzu Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +constexpr char NX_FONT_CSS[] = R"( +(function() { + css = document.createElement('style'); + css.type = 'text/css'; + css.id = 'nx_font'; + css.innerText = ` +/* FontStandard */ +@font-face { + font-family: 'FontStandard'; + src: url('%1') format('truetype'); +} + +/* FontChineseSimplified */ +@font-face { + font-family: 'FontChineseSimplified'; + src: url('%2') format('truetype'); +} + +/* FontExtendedChineseSimplified */ +@font-face { + font-family: 'FontExtendedChineseSimplified'; + src: url('%3') format('truetype'); +} + +/* FontChineseTraditional */ +@font-face { + font-family: 'FontChineseTraditional'; + src: url('%4') format('truetype'); +} + +/* FontKorean */ +@font-face { + font-family: 'FontKorean'; + src: url('%5') format('truetype'); +} + +/* FontNintendoExtended */ +@font-face { + font-family: 'NintendoExt003'; + src: url('%6') format('truetype'); +} + +/* FontNintendoExtended2 */ +@font-face { + font-family: 'NintendoExt003'; + src: url('%7') format('truetype'); +} +`; + + document.head.appendChild(css); +})(); +)"; + +constexpr char LOAD_NX_FONT[] = R"( +(function() { + var elements = document.querySelectorAll("*"); + + for (var i = 0; i < elements.length; i++) { + var style = window.getComputedStyle(elements[i], null); + if (style.fontFamily.includes("Arial") || style.fontFamily.includes("Calibri") || + style.fontFamily.includes("Century") || style.fontFamily.includes("Times New Roman")) { + elements[i].style.fontFamily = "FontStandard, FontChineseSimplified, FontExtendedChineseSimplified, FontChineseTraditional, FontKorean, NintendoExt003"; + } else { + elements[i].style.fontFamily = style.fontFamily + ", FontStandard, FontChineseSimplified, FontExtendedChineseSimplified, FontChineseTraditional, FontKorean, NintendoExt003"; + } + } +})(); +)"; + +constexpr char GAMEPAD_SCRIPT[] = R"( +window.addEventListener("gamepadconnected", function(e) { + console.log("Gamepad connected at index %d: %s. %d buttons, %d axes.", + e.gamepad.index, e.gamepad.id, e.gamepad.buttons.length, e.gamepad.axes.length); +}); + +window.addEventListener("gamepaddisconnected", function(e) { + console.log("Gamepad disconnected from index %d: %s", e.gamepad.index, e.gamepad.id); +}); +)"; + +constexpr char WINDOW_NX_SCRIPT[] = R"( +var end_applet = false; +var yuzu_key_callbacks = []; + +(function() { + class WindowNX { + constructor() { + yuzu_key_callbacks[1] = function() { window.history.back(); }; + yuzu_key_callbacks[2] = function() { window.nx.endApplet(); }; + } + + addEventListener(type, listener, options) { + console.log("nx.addEventListener called, type=%s", type); + + window.addEventListener(type, listener, options); + } + + endApplet() { + console.log("nx.endApplet called"); + + end_applet = true; + } + + playSystemSe(system_se) { + console.log("nx.playSystemSe is not implemented, system_se=%s", system_se); + } + + sendMessage(message) { + console.log("nx.sendMessage is not implemented, message=%s", message); + } + + setCursorScrollSpeed(scroll_speed) { + console.log("nx.setCursorScrollSpeed is not implemented, scroll_speed=%d", scroll_speed); + } + } + + class WindowNXFooter { + setAssign(key, label, func, option) { + console.log("nx.footer.setAssign called, key=%s", key); + + switch (key) { + case "A": + yuzu_key_callbacks[0] = func; + break; + case "B": + yuzu_key_callbacks[1] = func; + break; + case "X": + yuzu_key_callbacks[2] = func; + break; + case "Y": + yuzu_key_callbacks[3] = func; + break; + case "L": + yuzu_key_callbacks[6] = func; + break; + case "R": + yuzu_key_callbacks[7] = func; + break; + } + } + + setFixed(kind) { + console.log("nx.footer.setFixed is not implemented, kind=%s", kind); + } + + unsetAssign(key) { + console.log("nx.footer.unsetAssign called, key=%s", key); + + switch (key) { + case "A": + yuzu_key_callbacks[0] = function() {}; + break; + case "B": + yuzu_key_callbacks[1] = function() {}; + break; + case "X": + yuzu_key_callbacks[2] = function() {}; + break; + case "Y": + yuzu_key_callbacks[3] = function() {}; + break; + case "L": + yuzu_key_callbacks[6] = function() {}; + break; + case "R": + yuzu_key_callbacks[7] = function() {}; + break; + } + } + } + + class WindowNXPlayReport { + incrementCounter(counter_id) { + console.log("nx.playReport.incrementCounter is not implemented, counter_id=%d", counter_id); + } + + setCounterSetIdentifier(counter_id) { + console.log("nx.playReport.setCounterSetIdentifier is not implemented, counter_id=%d", counter_id); + } + } + + window.nx = new WindowNX(); + window.nx.footer = new WindowNXFooter(); + window.nx.playReport = new WindowNXPlayReport(); +})(); +)"; |
