| |
| |
|
|
| #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; |
| } |
| } |
|
|
| } |
|
|
| 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() { |
| |
| 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); |
| } |
| } |
|
|