From 93cb7838539075f0d94cb08a65a25b0c05589d7d Mon Sep 17 00:00:00 2001 From: Morph <39850852+Morph1984@users.noreply.github.com> Date: Mon, 30 Nov 2020 08:31:26 -0500 Subject: [PATCH] applets/web: Implement the Qt web browser applet frontend --- src/yuzu/applets/web_browser.cpp | 337 ++++++++++++++++++++++++++++++- src/yuzu/applets/web_browser.h | 155 +++++++++++++- src/yuzu/main.cpp | 110 +++++++++- src/yuzu/main.h | 7 + 4 files changed, 602 insertions(+), 7 deletions(-) diff --git a/src/yuzu/applets/web_browser.cpp b/src/yuzu/applets/web_browser.cpp index 92b53fed0..26b9df51a 100644 --- a/src/yuzu/applets/web_browser.cpp +++ b/src/yuzu/applets/web_browser.cpp @@ -2,10 +2,339 @@ // Licensed under GPLv2 or any later version // Refer to the license.txt file included. -#include "core/hle/lock.h" -#include "yuzu/applets/web_browser.h" -#include "yuzu/main.h" +#ifdef YUZU_USE_QT_WEB_ENGINE +#include -QtWebBrowser::QtWebBrowser(GMainWindow& main_window) {} +#include +#include +#include +#include +#include +#endif + +#include "common/file_util.h" +#include "core/core.h" +#include "core/frontend/input_interpreter.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 + +namespace { + +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) + : QWebEngineView(parent), url_interceptor(std::make_unique()), + input_interpreter(std::make_unique(system)) { + QWebEngineScript nx_font_css; + QWebEngineScript load_nx_font; + QWebEngineScript gamepad; + QWebEngineScript window_nx; + + 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")); + gamepad.setName(QStringLiteral("gamepad_script.js")); + window_nx.setName(QStringLiteral("window_nx_script.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)); + gamepad.setSourceCode(QString::fromStdString(GAMEPAD_SCRIPT)); + window_nx.setSourceCode(QString::fromStdString(WINDOW_NX_SCRIPT)); + + nx_font_css.setInjectionPoint(QWebEngineScript::DocumentReady); + load_nx_font.setInjectionPoint(QWebEngineScript::Deferred); + gamepad.setInjectionPoint(QWebEngineScript::DocumentCreation); + window_nx.setInjectionPoint(QWebEngineScript::DocumentCreation); + + nx_font_css.setWorldId(QWebEngineScript::MainWorld); + load_nx_font.setWorldId(QWebEngineScript::MainWorld); + gamepad.setWorldId(QWebEngineScript::MainWorld); + window_nx.setWorldId(QWebEngineScript::MainWorld); + + nx_font_css.setRunsOnSubFrames(true); + load_nx_font.setRunsOnSubFrames(true); + gamepad.setRunsOnSubFrames(true); + window_nx.setRunsOnSubFrames(true); + + auto* default_profile = QWebEngineProfile::defaultProfile(); + + default_profile->scripts()->insert(nx_font_css); + default_profile->scripts()->insert(load_nx_font); + default_profile->scripts()->insert(gamepad); + default_profile->scripts()->insert(window_nx); + + default_profile->setRequestInterceptor(url_interceptor.get()); + + auto* global_settings = QWebEngineSettings::globalSettings(); + + 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); + + 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); + + connect( + page(), &QWebEnginePage::windowCloseRequested, page(), + [this] { + if (page()->url() == url_interceptor->GetRequestedURL()) { + SetFinished(true); + SetExitReason(WebExitReason::WindowClosed); + } + }, + Qt::QueuedConnection); +} + +QtNXWebEngineView::~QtNXWebEngineView() { + SetFinished(true); + StopInputThread(); +} + +void QtNXWebEngineView::LoadLocalWebPage(std::string_view main_url, + std::string_view additional_args) { + SetUserAgent(UserAgent::WebApplet); + SetFinished(false); + SetExitReason(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::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_; +} + +WebExitReason QtNXWebEngineView::GetExitReason() const { + return exit_reason; +} + +void QtNXWebEngineView::SetExitReason(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(); +} + +template +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(button)), + [&](const QVariant& variant) { + if (variant.toBool()) { + switch (button) { + case HIDButton::A: + SendMultipleKeyPressEvents(); + 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(button))); + } + }; + + (f(T), ...); +} + +template +void QtNXWebEngineView::HandleWindowKeyButtonPressedOnce() { + const auto f = [this](HIDButton button) { + if (input_interpreter->IsButtonPressedOnce(button)) { + SendKeyPressEvent(HIDButtonToKey(button)); + } + }; + + (f(T), ...); +} + +template +void QtNXWebEngineView::HandleWindowKeyButtonHold() { + const auto f = [this](HIDButton 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() { + 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)); + + while (input_thread_running) { + input_interpreter->PollInput(); + + HandleWindowFooterButtonPressedOnce(); + + HandleWindowKeyButtonPressedOnce(); + + HandleWindowKeyButtonHold(); + + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + } +} + +#endif + +QtWebBrowser::QtWebBrowser(GMainWindow& main_window) { + connect(this, &QtWebBrowser::MainWindowOpenLocalWebPage, &main_window, + &GMainWindow::WebBrowserOpenLocalWebPage, Qt::QueuedConnection); + connect(&main_window, &GMainWindow::WebBrowserClosed, this, + &QtWebBrowser::MainWindowWebBrowserClosed, Qt::QueuedConnection); +} QtWebBrowser::~QtWebBrowser() = default; + +void QtWebBrowser::OpenLocalWebPage( + std::string_view local_url, std::function callback) const { + this->callback = std::move(callback); + + const auto index = local_url.find('?'); + + if (index == std::string::npos) { + emit MainWindowOpenLocalWebPage(local_url, ""); + } else { + emit MainWindowOpenLocalWebPage(local_url.substr(0, index), local_url.substr(index)); + } +} + +void QtWebBrowser::MainWindowWebBrowserClosed(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 af053ace7..74f2b49d2 100644 --- a/src/yuzu/applets/web_browser.h +++ b/src/yuzu/applets/web_browser.h @@ -4,6 +4,10 @@ #pragma once +#include +#include +#include + #include #ifdef YUZU_USE_QT_WEB_ENGINE @@ -12,12 +16,161 @@ #include "core/frontend/applets/web_browser.h" +enum class HIDButton : u8; + +class InputInterpreter; class GMainWindow; +class UrlRequestInterceptor; + +namespace Core { +class System; +} + +#ifdef YUZU_USE_QT_WEB_ENGINE + +enum class UserAgent { + WebApplet, + ShopN, + LoginApplet, + ShareApplet, + LobbyApplet, + WifiWebAuthApplet, +}; + +class QtNXWebEngineView : public QWebEngineView { + Q_OBJECT + +public: + explicit QtNXWebEngineView(QWidget* parent, Core::System& system); + ~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); + + /** + * 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]] WebExitReason GetExitReason() const; + void SetExitReason(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(); + +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 + 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 + 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 + 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 + void SendMultipleKeyPressEvents() { + (SendKeyPressEvent(T), ...); + } + + void StartInputThread(); + void StopInputThread(); + + /// The thread where input is being polled and processed. + void InputThread(); + + std::unique_ptr url_interceptor; + + std::unique_ptr input_interpreter; + + std::thread input_thread; + + std::atomic input_thread_running{}; + + std::atomic finished{}; + + WebExitReason exit_reason{WebExitReason::EndButtonPressed}; + + std::string last_url{"http://localhost/"}; +}; + +#endif class QtWebBrowser final : public QObject, public Core::Frontend::WebBrowserApplet { Q_OBJECT public: - explicit QtWebBrowser(GMainWindow& main_window); + explicit QtWebBrowser(GMainWindow& parent); ~QtWebBrowser() override; + + void OpenLocalWebPage(std::string_view local_url, + std::function callback) const override; + +signals: + void MainWindowOpenLocalWebPage(std::string_view main_url, + std::string_view additional_args) const; + +private: + void MainWindowWebBrowserClosed(WebExitReason exit_reason, std::string last_url); + + mutable std::function callback; }; diff --git a/src/yuzu/main.cpp b/src/yuzu/main.cpp index 7d4bba854..bab76db1e 100644 --- a/src/yuzu/main.cpp +++ b/src/yuzu/main.cpp @@ -28,8 +28,6 @@ #include "core/hle/service/am/applet_ae.h" #include "core/hle/service/am/applet_oe.h" #include "core/hle/service/am/applets/applets.h" -#include "core/hle/service/hid/controllers/npad.h" -#include "core/hle/service/hid/hid.h" // These are wrappers to avoid the calls to CreateDirectory and CreateFile because of the Windows // defines. @@ -182,6 +180,30 @@ static void InitializeLogging() { #endif } +static void RemoveCachedContents() { + const auto offline_fonts = Common::FS::SanitizePath( + fmt::format("{}/fonts", Common::FS::GetUserPath(Common::FS::UserPath::CacheDir)), + Common::FS::DirectorySeparator::PlatformDefault); + + const auto offline_manual = Common::FS::SanitizePath( + fmt::format("{}/offline_web_applet_manual", + Common::FS::GetUserPath(Common::FS::UserPath::CacheDir)), + Common::FS::DirectorySeparator::PlatformDefault); + const auto offline_legal_information = Common::FS::SanitizePath( + fmt::format("{}/offline_web_applet_legal_information", + Common::FS::GetUserPath(Common::FS::UserPath::CacheDir)), + Common::FS::DirectorySeparator::PlatformDefault); + const auto offline_system_data = Common::FS::SanitizePath( + fmt::format("{}/offline_web_applet_system_data", + Common::FS::GetUserPath(Common::FS::UserPath::CacheDir)), + Common::FS::DirectorySeparator::PlatformDefault); + + Common::FS::DeleteDirRecursively(offline_fonts); + Common::FS::DeleteDirRecursively(offline_manual); + Common::FS::DeleteDirRecursively(offline_legal_information); + Common::FS::DeleteDirRecursively(offline_system_data); +} + GMainWindow::GMainWindow() : input_subsystem{std::make_shared()}, config{std::make_unique()}, vfs{std::make_shared()}, @@ -250,6 +272,9 @@ GMainWindow::GMainWindow() FileSys::ContentProviderUnionSlot::FrontendManual, provider.get()); Core::System::GetInstance().GetFileSystemController().CreateFactories(*vfs); + // Remove cached contents generated during the previous session + RemoveCachedContents(); + // Gen keys if necessary OnReinitializeKeys(ReinitializeKeyBehavior::NoWarning); @@ -341,6 +366,86 @@ void GMainWindow::SoftwareKeyboardInvokeCheckDialog(std::u16string error_message emit SoftwareKeyboardFinishedCheckDialog(); } +void GMainWindow::WebBrowserOpenLocalWebPage(std::string_view main_url, + std::string_view additional_args) { +#ifdef YUZU_USE_QT_WEB_ENGINE + + QtNXWebEngineView web_browser_view(this, Core::System::GetInstance()); + + web_browser_view.LoadLocalWebPage(main_url, additional_args); + + ui.action_Pause->setEnabled(false); + ui.action_Restart->setEnabled(false); + ui.action_Stop->setEnabled(false); + + if (render_window->IsLoadingComplete()) { + render_window->hide(); + } + + const auto& layout = render_window->GetFramebufferLayout(); + web_browser_view.resize(layout.screen.GetWidth(), layout.screen.GetHeight()); + web_browser_view.move(layout.screen.left, layout.screen.top + menuBar()->height()); + web_browser_view.setZoomFactor(static_cast(layout.screen.GetWidth()) / + static_cast(Layout::ScreenUndocked::Width)); + + web_browser_view.setFocus(); + web_browser_view.show(); + + bool exit_check = false; + + while (!web_browser_view.IsFinished()) { + QCoreApplication::processEvents(); + + if (!exit_check) { + web_browser_view.page()->runJavaScript( + QStringLiteral("end_applet;"), [&](const QVariant& variant) { + exit_check = false; + if (variant.toBool()) { + web_browser_view.SetFinished(true); + web_browser_view.SetExitReason(WebExitReason::EndButtonPressed); + } + }); + + exit_check = true; + } + + if (web_browser_view.GetCurrentURL().contains(QStringLiteral("localhost"))) { + if (!web_browser_view.IsFinished()) { + web_browser_view.SetFinished(true); + web_browser_view.SetExitReason(WebExitReason::CallbackURL); + } + + web_browser_view.SetLastURL(web_browser_view.GetCurrentURL().toStdString()); + } + + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + + const auto exit_reason = web_browser_view.GetExitReason(); + const auto last_url = web_browser_view.GetLastURL(); + + web_browser_view.hide(); + + render_window->setFocus(); + + if (render_window->IsLoadingComplete()) { + render_window->show(); + } + + ui.action_Pause->setEnabled(true); + ui.action_Restart->setEnabled(true); + ui.action_Stop->setEnabled(true); + + emit WebBrowserClosed(exit_reason, last_url); + +#else + + // Utilize the same fallback as the default web browser applet. + emit WebBrowserClosed(WebExitReason::WindowClosed, "http://localhost"); + +#endif +} + void GMainWindow::InitializeWidgets() { #ifdef YUZU_ENABLE_COMPATIBILITY_REPORTING ui.action_Report_Compatibility->setVisible(true); @@ -1948,6 +2053,7 @@ void GMainWindow::OnStartGame() { qRegisterMetaType("std::string"); qRegisterMetaType>("std::optional"); qRegisterMetaType("std::string_view"); + qRegisterMetaType("Service::AM::Applets::WebExitReason"); connect(emu_thread.get(), &EmuThread::ErrorThrown, this, &GMainWindow::OnCoreError); diff --git a/src/yuzu/main.h b/src/yuzu/main.h index f311f2b5b..22f64fc9c 100644 --- a/src/yuzu/main.h +++ b/src/yuzu/main.h @@ -55,6 +55,10 @@ namespace InputCommon { class InputSubsystem; } +namespace Service::AM::Applets { +enum class WebExitReason : u32; +} + enum class EmulatedDirectoryTarget { NAND, SDMC, @@ -126,6 +130,8 @@ signals: void SoftwareKeyboardFinishedText(std::optional text); void SoftwareKeyboardFinishedCheckDialog(); + void WebBrowserClosed(Service::AM::Applets::WebExitReason exit_reason, std::string last_url); + public slots: void OnLoadComplete(); void OnExecuteProgram(std::size_t program_index); @@ -135,6 +141,7 @@ public slots: void ProfileSelectorSelectProfile(); void SoftwareKeyboardGetText(const Core::Frontend::SoftwareKeyboardParameters& parameters); void SoftwareKeyboardInvokeCheckDialog(std::u16string error_message); + void WebBrowserOpenLocalWebPage(std::string_view main_url, std::string_view additional_args); void OnAppFocusStateChanged(Qt::ApplicationState state); private: