summaryrefslogtreecommitdiff
path: root/src/citron/applets/qt_web_browser.cpp
diff options
context:
space:
mode:
authorZephyron <zephyron@citron-emu.org>2024-12-31 16:19:25 +1000
committerZephyron <zephyron@citron-emu.org>2024-12-31 16:19:25 +1000
commit9427e27e24a7135880ee2881c3c44988e174b41a (patch)
tree83f0062a35be144f6b162eaa823c5b3c7620146e /src/citron/applets/qt_web_browser.cpp
parentb35ae725d20960411e8588b11c12a2d55f86c9d0 (diff)
chore: update project branding to citron
Diffstat (limited to 'src/citron/applets/qt_web_browser.cpp')
-rw-r--r--src/citron/applets/qt_web_browser.cpp449
1 files changed, 449 insertions, 0 deletions
diff --git a/src/citron/applets/qt_web_browser.cpp b/src/citron/applets/qt_web_browser.cpp
new file mode 100644
index 000000000..cce9b2efb
--- /dev/null
+++ b/src/citron/applets/qt_web_browser.cpp
@@ -0,0 +1,449 @@
+// 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);
+ }
+}