// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #ifdef YUZU_USE_QT_WEB_ENGINE #include <bit> #include <QApplication> #include <QKeyEvent> #include <QWebEngineProfile> #include <QWebEngineScript> #include <QWebEngineScriptCollection> #include <QWebEngineSettings> #include <QWebEngineUrlScheme> #include "hid_core/frontend/input_interpreter.h" #include "yuzu/applets/qt_web_browser_scripts.h" #endif #include "common/fs/path_util.h" #include "core/core.h" #include "input_common/drivers/keyboard.h" #include "yuzu/applets/qt_web_browser.h" #include "yuzu/main.h" #include "yuzu/util/url_request_interceptor.h" #ifdef YUZU_USE_QT_WEB_ENGINE namespace { constexpr int HIDButtonToKey(Core::HID::NpadButton button) { switch (button) { case Core::HID::NpadButton::Left: case Core::HID::NpadButton::StickLLeft: return Qt::Key_Left; case Core::HID::NpadButton::Up: case Core::HID::NpadButton::StickLUp: return Qt::Key_Up; case Core::HID::NpadButton::Right: case Core::HID::NpadButton::StickLRight: return Qt::Key_Right; case Core::HID::NpadButton::Down: case Core::HID::NpadButton::StickLDown: 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{ default_profile->settings()} { default_profile->setPersistentStoragePath(QString::fromStdString(Common::FS::PathToUTF8String( Common::FS::GetYuzuPath(Common::FS::YuzuPath::YuzuDir) / "qtwebengine"))); 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->setUrlRequestInterceptor(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::Frontend::WebExitReason::WindowClosed); } }, Qt::QueuedConnection); } QtNXWebEngineView::~QtNXWebEngineView() { SetFinished(true); StopInputThread(); } void QtNXWebEngineView::LoadLocalWebPage(const std::string& main_url, const std::string& additional_args) { is_local = true; LoadExtractedFonts(); FocusFirstLinkElement(); SetUserAgent(UserAgent::WebApplet); SetFinished(false); SetExitReason(Service::AM::Frontend::WebExitReason::EndButtonPressed); SetLastURL("http://localhost/"); StartInputThread(); load(QUrl(QUrl::fromLocalFile(QString::fromStdString(main_url)).toString() + QString::fromStdString(additional_args))); } void QtNXWebEngineView::LoadExternalWebPage(const std::string& main_url, const std::string& additional_args) { is_local = false; FocusFirstLinkElement(); SetUserAgent(UserAgent::WebApplet); SetFinished(false); SetExitReason(Service::AM::Frontend::WebExitReason::EndButtonPressed); SetLastURL("http://localhost/"); StartInputThread(); load(QUrl(QString::fromStdString(main_url) + QString::fromStdString(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::Frontend::WebExitReason QtNXWebEngineView::GetExitReason() const { return exit_reason; } void QtNXWebEngineView::SetExitReason(Service::AM::Frontend::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(); 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 <Core::HID::NpadButton... T> void QtNXWebEngineView::HandleWindowFooterButtonPressedOnce() { const auto f = [this](Core::HID::NpadButton button) { if (input_interpreter->IsButtonPressedOnce(button)) { const auto button_index = std::countr_zero(static_cast<u64>(button)); page()->runJavaScript( QStringLiteral("yuzu_key_callbacks[%1] == null;").arg(button_index), [this, button](const QVariant& variant) { if (variant.toBool()) { switch (button) { case Core::HID::NpadButton::A: SendMultipleKeyPressEvents<Qt::Key_A, Qt::Key_Space, Qt::Key_Return>(); break; case Core::HID::NpadButton::B: SendKeyPressEvent(Qt::Key_B); break; case Core::HID::NpadButton::X: SendKeyPressEvent(Qt::Key_X); break; case Core::HID::NpadButton::Y: SendKeyPressEvent(Qt::Key_Y); break; default: break; } } }); page()->runJavaScript( QStringLiteral("if (yuzu_key_callbacks[%1] != null) { yuzu_key_callbacks[%1](); }") .arg(button_index)); } }; (f(T), ...); } template <Core::HID::NpadButton... T> void QtNXWebEngineView::HandleWindowKeyButtonPressedOnce() { const auto f = [this](Core::HID::NpadButton button) { if (input_interpreter->IsButtonPressedOnce(button)) { SendKeyPressEvent(HIDButtonToKey(button)); } }; (f(T), ...); } template <Core::HID::NpadButton... T> void QtNXWebEngineView::HandleWindowKeyButtonHold() { const auto f = [this](Core::HID::NpadButton button) { if (input_interpreter->IsButtonHeld(button)) { SendKeyPressEvent(HIDButtonToKey(button)); } }; (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(); } input_thread_running = false; if (input_thread.joinable()) { input_thread.join(); } } 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<Core::HID::NpadButton::A, Core::HID::NpadButton::B, Core::HID::NpadButton::X, Core::HID::NpadButton::Y, Core::HID::NpadButton::L, Core::HID::NpadButton::R>(); HandleWindowKeyButtonPressedOnce< Core::HID::NpadButton::Left, Core::HID::NpadButton::Up, Core::HID::NpadButton::Right, Core::HID::NpadButton::Down, Core::HID::NpadButton::StickLLeft, Core::HID::NpadButton::StickLUp, Core::HID::NpadButton::StickLRight, Core::HID::NpadButton::StickLDown>(); HandleWindowKeyButtonHold< Core::HID::NpadButton::Left, Core::HID::NpadButton::Up, Core::HID::NpadButton::Right, Core::HID::NpadButton::Down, Core::HID::NpadButton::StickLLeft, Core::HID::NpadButton::StickLUp, Core::HID::NpadButton::StickLRight, Core::HID::NpadButton::StickLDown>(); std::this_thread::sleep_for(std::chrono::milliseconds(50)); } } void QtNXWebEngineView::LoadExtractedFonts() { QWebEngineScript nx_font_css; QWebEngineScript load_nx_font; auto fonts_dir_str = Common::FS::PathToUTF8String( Common::FS::GetYuzuPath(Common::FS::YuzuPath::CacheDir) / "fonts/"); std::replace(fonts_dir_str.begin(), fonts_dir_str.end(), '\\', '/'); const auto fonts_dir = QString::fromStdString(fonts_dir_str); 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); } void QtNXWebEngineView::FocusFirstLinkElement() { QWebEngineScript focus_link_element; focus_link_element.setName(QStringLiteral("focus_link_element.js")); focus_link_element.setSourceCode(QString::fromStdString(FOCUS_LINK_ELEMENT_SCRIPT)); focus_link_element.setWorldId(QWebEngineScript::MainWorld); focus_link_element.setInjectionPoint(QWebEngineScript::Deferred); focus_link_element.setRunsOnSubFrames(true); default_profile->scripts()->insert(focus_link_element); } #endif QtWebBrowser::QtWebBrowser(GMainWindow& main_window) { connect(this, &QtWebBrowser::MainWindowOpenWebPage, &main_window, &GMainWindow::WebBrowserOpenWebPage, Qt::QueuedConnection); connect(this, &QtWebBrowser::MainWindowRequestExit, &main_window, &GMainWindow::WebBrowserRequestExit, 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::Close() const { callback = {}; emit MainWindowRequestExit(); } void QtWebBrowser::OpenLocalWebPage(const std::string& local_url, ExtractROMFSCallback extract_romfs_callback_, OpenWebPageCallback 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(const std::string& external_url, OpenWebPageCallback callback_) const { callback = std::move(callback_); const auto index = external_url.find('?'); if (index == std::string::npos) { emit MainWindowOpenWebPage(external_url, "", false); } else { emit MainWindowOpenWebPage(external_url.substr(0, index), external_url.substr(index), false); } } void QtWebBrowser::MainWindowExtractOfflineRomFS() { extract_romfs_callback(); } void QtWebBrowser::MainWindowWebBrowserClosed(Service::AM::Frontend::WebExitReason exit_reason, std::string last_url) { if (callback) { callback(exit_reason, last_url); } }