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