nxemu/src/nxemu/user_interface/sciter_main_window.cpp

2771 lines
91 KiB
C++

#include "sciter_main_window.h"
#include "settings/game_config.h"
#include "settings/input_config.h"
#include "settings/system_config.h"
#include "settings/ui_settings.h"
#include <cstring>
#include "user_interface/about_dialog.h"
#include "user_interface/app_events.h"
#include "user_interface/file_dialogs.h"
#include "user_interface/html_utils.h"
#include "user_interface/key_mappings.h"
#include "user_interface/notification.h"
#include "user_interface/volume_slider.h"
#ifdef __APPLE__
#include "user_interface/render_window_macos.h"
#endif
#include <common/path.h>
#include <common/shell_open.h>
#include <common/std_string.h>
#include <nxemu-core/notification.h>
#include <nxemu-core/settings/identifiers.h>
#include <nxemu-core/settings/settings.h>
#include <nxemu-core/version.h>
#include <nxemu-loader/loader_settings_identifiers.h>
#include <nxemu-module-spec/operating_system.h>
#include <nxemu-module-spec/system_loader.h>
#include <nxemu-module-spec/video.h>
#include <nxemu-os/os_settings_identifiers.h>
#include <nxemu-video/video_settings_identifiers.h>
#include <nxemu/settings/ui_identifiers.h>
#include <sciter_element.h>
#include <widgets/menubar.h>
#include <yuzu_common/fs/filesystem_interfaces.h>
#include <yuzu_common/fs/fs.h>
#include <yuzu_common/fs/path_util.h>
#include <yuzu_common/settings.h>
#include <yuzu_common/uuid.h>
#ifdef _WIN32
#include <Windows.h>
#else
#include <strings.h>
#define _stricmp strcasecmp
#endif
#include <array>
#include <chrono>
#include <cmath>
#include <string>
#include <vector>
namespace
{
constexpr const char * discordUrl = "https://discord.gg/hEa4hNyFWU";
constexpr const char * reportUrl = "https://report.nxemu.com/";
constexpr int kDefaultMouseHideTimeoutMs = 2500;
const char * RendererBackendLabel(RendererBackend backend)
{
switch (backend)
{
case RendererBackend::OpenGL: return "OpenGL";
case RendererBackend::Vulkan: return "Vulkan";
case RendererBackend::Null: return "Null";
}
return "OpenGL";
}
const char * DockedModeLabel(DockedMode mode)
{
switch (mode)
{
case DockedMode::Handheld: return "Handheld";
case DockedMode::Docked: return "Docked";
}
return "Docked";
}
uint64_t NowMs()
{
#ifdef _WIN32
return GetTickCount64();
#else
using namespace std::chrono;
return (uint64_t)(duration_cast<milliseconds>(steady_clock::now().time_since_epoch()).count());
#endif
}
} // namespace
#ifdef _WIN32
struct Win32FullscreenState
{
bool active = false;
uint32_t pendingSwallowKeyUp = 0;
WINDOWPLACEMENT placement{};
LONG_PTR savedStyle = 0;
LONG_PTR savedExStyle = 0;
};
#endif
namespace
{
const uint32_t kKeyboardStateControl = 0x0040u | 0x0080u;
const uint32_t kKeyboardStateAlt = 0x0100u | 0x0200u;
const uint32_t kKeyboardStateShift = 0x0001u | 0x0002u;
bool AcceleratorMatchesKey(const MenuBarAccelerator & accel, uint32_t keyCode, uint32_t keyboardState)
{
if (accel.IsNone())
{
return false;
}
if (keyCode != accel.key)
{
return false;
}
bool ctrl = (keyboardState & kKeyboardStateControl) != 0;
bool alt = (keyboardState & kKeyboardStateAlt) != 0;
bool shift = (keyboardState & kKeyboardStateShift) != 0;
return ctrl == accel.ctrl && alt == accel.alt && shift == accel.shift;
}
void LoadImageToElement(SciterElement elem, const std::vector<uint8_t> & data)
{
if (!elem.IsValid())
{
return;
}
if (!data.empty())
{
elem.SetAttribute("src", ImageDataUri(data.data(), data.size()).c_str());
}
elem.SetStyleAttribute("display", data.empty() ? "none" : "block");
}
std::string GetInstalledFirmwareDisplayVersion(ISystemloader & loader)
{
char buffer[32]{};
const uint32_t length = loader.GetInstalledFirmwareDisplayVersion(buffer, sizeof(buffer));
if (length == 0)
{
return {};
}
return std::string(buffer, length);
}
std::string BuildSwitchOpenFileFilter(ISystemloader & loader)
{
const uint32_t count = loader.GetSupportedGameExtensions(nullptr, 0);
std::vector<const char *> extensions(count);
loader.GetSupportedGameExtensions(extensions.data(), count);
stdstr wildcards, patterns;
for (uint32_t i = 0; i < count; ++i)
{
wildcards += stdstr_f("%s*.%s", wildcards.empty() ? "" : ", ", extensions[i]);
patterns += stdstr_f("%s*.%s", patterns.empty() ? "" : ";", extensions[i]);
}
stdstr filter = stdstr_f("Switch Files (%s)", wildcards.c_str());
filter += '\0';
filter += patterns.empty() ? "*.*" : patterns;
filter += '\0';
filter += "All files (*.*)";
filter += '\0';
filter += "*.*";
filter += '\0';
return filter;
}
void UpdateLoadingProgressBar(SciterElement & fillEl, bool indeterminate, int widthPercent, bool shaderBuilding)
{
if (!fillEl.IsValid())
{
return;
}
if (indeterminate)
{
fillEl.AddClassName("indeterminate");
fillEl.RemoveClassName("building");
fillEl.SetStyleAttribute("width", "42%");
}
else
{
fillEl.RemoveClassName("indeterminate");
if (shaderBuilding)
{
fillEl.AddClassName("building");
}
else
{
fillEl.RemoveClassName("building");
}
const int w = (std::max)(0, (std::min)(100, widthPercent));
fillEl.SetStyleAttribute("width", stdstr_f("%d%%", w).c_str());
}
}
std::string GetGameTitleLoadingHtml(ISystemloader & loader, const char * verb)
{
IRomInfoPtr info(loader.LoadedRomInfo());
if (!info)
{
return stdstr_f("<span class=\"loading-verb\">%s</span> <span class=\"loading-game-name\">...</span>", verb);
}
uint32_t sz = 0;
if (info->ReadTitle(nullptr, &sz) != LoaderResultStatus::Success || sz == 0)
{
return stdstr_f("<span class=\"loading-verb\">%s</span> <span class=\"loading-game-name\">...</span>", verb);
}
std::vector<char> buf(static_cast<size_t>(sz) + 1, 0);
if (info->ReadTitle(buf.data(), &sz) != LoaderResultStatus::Success)
{
return stdstr_f("<span class=\"loading-verb\">%s</span> <span class=\"loading-game-name\">...</span>", verb);
}
const std::string titleEsc = HtmlEscape(std::string(buf.data()));
return stdstr_f("<span class=\"loading-verb\">%s</span> <span class=\"loading-game-name\">%s</span>", verb, titleEsc.c_str());
}
uint64_t ReadProgramIdForGame(SystemModules & modules, const char * gamePath)
{
if (gamePath == nullptr || gamePath[0] == '\0' || !modules.IsValid())
{
return 0;
}
ISystemloader & loader = modules.Modules().Systemloader();
IRomInfoPtr romInfo(loader.RomInfo(gamePath, 0, 0));
if (!romInfo)
{
return 0;
}
uint64_t programId = 0;
romInfo->ReadProgramId(programId);
return programId;
}
bool OpenFolderPath(const std::filesystem::path & path, void * ownerWindow)
{
if (!Common::FS::CreateDirs(path))
{
return false;
}
const std::string utf8 = Common::FS::PathToUTF8String(path);
return !utf8.empty() && ShellOpen(utf8.c_str(), ownerWindow);
}
} // namespace
SciterMainWindow::SciterMainWindow(ISciterUI & sciterUI, const char * windowTitle) :
m_sciterUI(sciterUI),
m_window(nullptr),
m_renderWindow(nullptr),
m_windowTitle(windowTitle),
m_pendingSaveDataProgramId(0),
m_emulationRunning(false),
m_pendingStartInFullscreen(false),
m_hideUi(false),
m_lastDiskCacheStatusPostMs(0),
m_lastPostedDiskCacheStage(0),
m_shownFirstFrame(false),
m_win32Fullscreen(std::make_unique<Win32FullscreenState>()),
m_firmwareInstallInProgress(false),
m_firmwareInstallUiActive(false),
m_firmwareInstallLastTotal(0),
m_mouseCursorHidden(false),
m_lastMouseActivityTick(0),
m_lastTrackedMouseX(0),
m_lastTrackedMouseY(0),
m_reloadingGame(false),
m_currentProgramIndex(0),
m_previousProgramIndex(-1),
m_pendingReloadProgramIndex(-1),
m_pendingReloadLaunchType(ApplicationLaunchType::FrontendInitiated)
{
SettingsStore & settings = SettingsStore::GetInstance();
settings.RegisterCallback(NXCoreSetting::EmulationRunning, SciterMainWindow::EmulationRunning, this);
settings.RegisterCallback(NXCoreSetting::EmulationState, SciterMainWindow::EmulationStateChanged, this);
settings.RegisterCallback(NXCoreSetting::GameFile, SciterMainWindow::GameFileChanged, this);
settings.RegisterCallback(NXCoreSetting::GameName, SciterMainWindow::GameNameChanged, this);
settings.RegisterCallback(NXCoreSetting::DisplayedFrames, SciterMainWindow::DisplayedFramesChanged, this);
settings.RegisterCallback(NXCoreSetting::DiskCacheLoadTick, SciterMainWindow::DiskCacheLoadChanged, this);
settings.RegisterCallback(NXLoaderSetting::FirmwareInstallTotal, SciterMainWindow::FirmwareInstallTotalChanged, this);
settings.RegisterCallback(NXOsSetting::AudioMuted, SciterMainWindow::SettingChanged, this);
settings.RegisterCallback(NXOsSetting::AudioVolume, SciterMainWindow::SettingChanged, this);
settings.RegisterCallback(NXOsSetting::SpeedLimit, SciterMainWindow::SettingChanged, this);
settings.RegisterCallback(NXOsSetting::UseMultiCore, SciterMainWindow::SettingChanged, this);
settings.RegisterCallback(NXOsSetting::UseSpeedLimit, SciterMainWindow::SettingChanged, this);
settings.RegisterCallback(NXOsSetting::DockedMode, SciterMainWindow::SettingChanged, this);
settings.RegisterCallback(NXUISetting::Hotkeys, SciterMainWindow::HotKeysChanged, this);
settings.RegisterCallback(NXVideoSetting::ResolutionUpFactor, SciterMainWindow::SettingChanged, this);
settings.RegisterCallback(NXVideoSetting::GpuCommandSynchronization, SciterMainWindow::SettingChanged, this);
settings.RegisterCallback(NXUISetting::HideMouseOnInactivity, SciterMainWindow::SettingChanged, this);
settings.RegisterCallback(NXUISetting::EnableDiscordPresence, SciterMainWindow::SettingChanged, this);
m_useMultiCore = settings.GetBool(NXOsSetting::UseMultiCore);
m_useSpeedLimit = settings.GetBool(NXOsSetting::UseSpeedLimit);
m_speedLimit = settings.GetInt(NXOsSetting::SpeedLimit);
m_resolutionUpFactor = settings.GetFloat(NXVideoSetting::ResolutionUpFactor);
}
const char * SciterMainWindow::MenuIconResource(GuiAction action) const
{
switch (action)
{
case GuiAction::LoadFile:
return "open-file.svg";
case GuiAction::ExitApplication:
return "close.svg";
case GuiAction::InstallFirmwareFromFile:
case GuiAction::InstallFirmwareFromFolder:
return "install.svg";
case GuiAction::ToggleFullscreen:
return "fullscreen.svg";
case GuiAction::OpenControllersDialog:
return "controller.svg";
case GuiAction::OpenSystemConfiguration:
return "settings.svg";
case GuiAction::PauseOrContinueEmulation:
return "pause.svg";
case GuiAction::StopEmulation:
return "stop.svg";
case GuiAction::ToggleHideUi:
return "hide-ui.svg";
case GuiAction::ResetWindowSize720p:
case GuiAction::ResetWindowSize900p:
case GuiAction::ResetWindowSize1080p:
return "resize.svg";
case GuiAction::OpenAboutDialog:
return "about.svg";
case GuiAction::OpenDiscord:
return "discord.svg";
case GuiAction::OpenReportIssues:
return "report.svg";
default:
return nullptr;
}
}
const std::string * SciterMainWindow::MenuIconSvgForResource(const char * resource)
{
if (resource == nullptr)
{
return nullptr;
}
const auto found = m_menuIconSvgs.find(resource);
if (found != m_menuIconSvgs.end())
{
return &found->second;
}
std::vector<uint8_t> data;
if (!m_sciterUI.LoadResource(resource, data) || data.empty())
{
return nullptr;
}
std::string & cached = m_menuIconSvgs[resource];
cached.assign(reinterpret_cast<const char *>(data.data()), data.size());
return &cached;
}
const std::string * SciterMainWindow::MenuIconSvg(GuiAction action)
{
const char * resource = MenuIconResource(action);
return MenuIconSvgForResource(resource);
}
SciterMainWindow::~SciterMainWindow()
{
if (m_window != nullptr)
{
m_window->OnCloseSinkRemove(this);
m_window->OnDestroySinkRemove(this);
if (m_menuBar)
{
m_menuBar->RemoveSink(this);
}
}
Notification::GetInstance().ClearSciterContext();
m_ProfileSelect.Detach();
m_WebBrowser.DetachWindow();
SettingsStore & settings = SettingsStore::GetInstance();
settings.UnregisterCallback(NXCoreSetting::EmulationRunning, SciterMainWindow::EmulationRunning, this);
settings.UnregisterCallback(NXCoreSetting::EmulationState, SciterMainWindow::EmulationStateChanged, this);
settings.UnregisterCallback(NXCoreSetting::GameFile, SciterMainWindow::GameFileChanged, this);
settings.UnregisterCallback(NXCoreSetting::GameName, SciterMainWindow::GameNameChanged, this);
settings.UnregisterCallback(NXCoreSetting::DisplayedFrames, SciterMainWindow::DisplayedFramesChanged, this);
settings.UnregisterCallback(NXCoreSetting::DiskCacheLoadTick, SciterMainWindow::DiskCacheLoadChanged, this);
settings.UnregisterCallback(NXLoaderSetting::FirmwareInstallTotal, SciterMainWindow::FirmwareInstallTotalChanged, this);
settings.UnregisterCallback(NXOsSetting::AudioMuted, SciterMainWindow::SettingChanged, this);
settings.UnregisterCallback(NXOsSetting::AudioVolume, SciterMainWindow::SettingChanged, this);
settings.UnregisterCallback(NXOsSetting::SpeedLimit, SciterMainWindow::SettingChanged, this);
settings.UnregisterCallback(NXOsSetting::UseMultiCore, SciterMainWindow::SettingChanged, this);
settings.UnregisterCallback(NXOsSetting::UseSpeedLimit, SciterMainWindow::SettingChanged, this);
settings.UnregisterCallback(NXOsSetting::DockedMode, SciterMainWindow::SettingChanged, this);
settings.UnregisterCallback(NXUISetting::Hotkeys, SciterMainWindow::HotKeysChanged, this);
settings.UnregisterCallback(NXVideoSetting::ResolutionUpFactor, SciterMainWindow::SettingChanged, this);
settings.UnregisterCallback(NXVideoSetting::GpuCommandSynchronization, SciterMainWindow::SettingChanged, this);
settings.UnregisterCallback(NXUISetting::HideMouseOnInactivity, SciterMainWindow::SettingChanged, this);
settings.UnregisterCallback(NXUISetting::EnableDiscordPresence, SciterMainWindow::SettingChanged, this);
if (m_rootElement.IsValid())
{
m_rootElement.SetTimer(0, (uint32_t *)TIMER_UPDATE_INSTALL_FIRMWARE);
}
if (m_firmwareInstallThread.joinable())
{
m_firmwareInstallThread.join();
}
m_systemConfig.reset(nullptr);
m_inputConfig.reset(nullptr);
m_gameConfig.reset(nullptr);
m_aboutDialog.reset(nullptr);
settings.SetBool(NXCoreSetting::ShuttingDown, true);
if (m_romBrowser)
{
// Stop launches a fresh library scan. A native close has already
// cleared m_rootElement, but that worker must still be joined before
// its loader module is destroyed.
m_romBrowser->StopScan();
m_romBrowser->SetMainWindow(nullptr, nullptr);
}
if (m_romBrowser && m_rootElement.IsValid())
{
m_romBrowser->ClearItems();
}
m_modules.ShutDown();
#ifdef __APPLE__
NxEmuMacOSDestroyRenderView(m_renderWindow);
m_renderWindow = nullptr;
#endif
if (m_window != nullptr)
{
// File -> Exit stops the loop without a native close notification. Close
// while this owner is alive, so shutdown cannot dispatch into dead sinks.
m_window->Destroy();
m_window = nullptr;
m_rootElement = {};
}
}
void SciterMainWindow::RegisterApplets()
{
if (!m_modules.IsValid())
{
return;
}
IOperatingSystem & os = m_modules.Modules().OperatingSystem();
m_ProfileSelect.Attach(m_sciterUI, m_modules, m_rootElement, (void *)m_window->GetHandle());
m_WebBrowser.AttachToWindow(m_window->GetHandle());
os.SetFrontendApplets(nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, &m_ProfileSelect, nullptr, &m_WebBrowser);
}
void SciterMainWindow::RegisterSystemCallbacks()
{
if (!m_modules.IsValid())
{
return;
}
IOperatingSystem & os = m_modules.Modules().OperatingSystem();
os.RegisterExecuteProgramCallback(&SciterMainWindow::ExecuteProgramCallbackThunk, this);
os.RegisterExitCallback(&SciterMainWindow::ExitCallbackThunk, this);
}
void SciterMainWindow::CollectUserChannelEntry(const uint8_t * data, uint32_t size, void * userData)
{
SciterMainWindow * impl = (SciterMainWindow *)userData;
if (data == nullptr || size == 0)
{
impl->m_pendingUserChannel.emplace_back();
return;
}
impl->m_pendingUserChannel.emplace_back(data, data + size);
}
void SciterMainWindow::ExecuteProgramCallbackThunk(uint32_t program_index, void * userData)
{
SciterMainWindow * impl = (SciterMainWindow *)userData;
if (!impl->m_modules.IsValid())
{
return;
}
impl->m_pendingUserChannel.clear();
impl->m_modules.Modules().OperatingSystem().ExportUserChannel(&SciterMainWindow::CollectUserChannelEntry, impl);
impl->m_rootElement.PostEvent(EVENT_EXECUTE_PROGRAM, program_index);
}
void SciterMainWindow::ExitCallbackThunk(void * userData)
{
SciterMainWindow * impl = static_cast<SciterMainWindow *>(userData);
impl->m_rootElement.PostEvent(EVENT_EXIT_PROGRAM);
}
void SciterMainWindow::OnExecuteProgram(uint64_t program_index)
{
const std::string path = SettingsStore::GetInstance().GetString(NXCoreSetting::GameFile);
if (path.empty())
{
m_pendingUserChannel.clear();
return;
}
LoadGame(path.c_str(), (int32_t)program_index, ApplicationLaunchType::ApplicationInitiated);
}
void SciterMainWindow::OnExitProgram()
{
if (m_reloadingGame)
{
return;
}
if (!m_emulationRunning)
{
return;
}
if (m_currentProgramIndex != 0)
{
const std::string path = SettingsStore::GetInstance().GetString(NXCoreSetting::GameFile);
if (!path.empty())
{
const int32_t previous_index = m_previousProgramIndex >= 0 ? m_previousProgramIndex : 0;
LoadGame(path.c_str(), previous_index, ApplicationLaunchType::ApplicationInitiated);
return;
}
}
AllowOSSleep();
SettingsStore::GetInstance().SetBool(NXCoreSetting::EmulationRunning, false);
}
void SciterMainWindow::OnReloadProgram()
{
if (m_pendingReloadPath.empty())
{
m_reloadingGame = false;
return;
}
const std::string path = std::move(m_pendingReloadPath);
const int32_t program_index = m_pendingReloadProgramIndex;
const ApplicationLaunchType launch_type = m_pendingReloadLaunchType;
m_pendingReloadPath.clear();
m_pendingReloadProgramIndex = -1;
m_pendingReloadLaunchType = ApplicationLaunchType::FrontendInitiated;
LoadGame(path.c_str(), program_index, launch_type);
}
void SciterMainWindow::ResetMenu()
{
if (m_menuBar == nullptr)
{
return;
}
MenuBarItemList mainTitleMenu;
MenuBarItemList fileMenu;
fileMenu.push_back(MenuBarItem(static_cast<int32_t>(GuiAction::LoadFile), "&Load File...", nullptr, HotkeyAccelerator(Hotkey::LoadFile), MenuBarItem::CheckState::None, MenuIconSvg(GuiAction::LoadFile)));
Stringlist & recentFiles = uiSettings.recentFiles;
MenuBarItemList RecentFileMenu;
if (recentFiles.size() > 0)
{
int32_t recentFileIndex = 0;
for (Stringlist::const_iterator itr = recentFiles.begin(); itr != recentFiles.end(); itr++)
{
stdstr_f MenuString("%d %s", recentFileIndex + 1, itr->c_str());
RecentFileMenu.push_back(MenuBarItem(static_cast<int32_t>(GuiAction::RecentFileMenuFirst) + recentFileIndex, MenuString.c_str()));
recentFileIndex += 1;
}
fileMenu.push_back(MenuBarItem(MenuBarItem::SUB_MENU, "&Recent File", &RecentFileMenu, nullptr, MenuBarItem::CheckState::None, MenuIconSvgForResource("recent.svg")));
}
fileMenu.push_back(MenuBarItem(MenuBarItem::SPLITER));
MenuBarItemList openFoldersMenu;
openFoldersMenu.push_back(MenuBarItem(static_cast<int32_t>(GuiAction::OpenAppDirectory), "&App Directory", nullptr, nullptr, MenuBarItem::CheckState::None, nullptr));
openFoldersMenu.push_back(MenuBarItem(static_cast<int32_t>(GuiAction::OpenLogDirectory), "&Log Folder", nullptr, nullptr, MenuBarItem::CheckState::None, nullptr));
fileMenu.push_back(MenuBarItem(MenuBarItem::SUB_MENU, "Open &NXEmu Folders", &openFoldersMenu));
fileMenu.push_back(MenuBarItem(MenuBarItem::SPLITER));
fileMenu.push_back(MenuBarItem(static_cast<int32_t>(GuiAction::ExitApplication), "E&xit", nullptr, HotkeyAccelerator(Hotkey::Exit), MenuBarItem::CheckState::None, MenuIconSvg(GuiAction::ExitApplication)));
mainTitleMenu.push_back(MenuBarItem(MenuBarItem::SUB_MENU, "&File", &fileMenu));
MenuBarItemList systemMenu;
if (m_emulationRunning)
{
bool paused = false;
if (m_modules.IsValid())
{
paused = m_modules.Modules().OperatingSystem().IsEmulationPaused();
}
systemMenu.push_back(MenuBarItem(static_cast<int32_t>(GuiAction::PauseOrContinueEmulation), paused ? "Continue" : "Pause", nullptr, HotkeyAccelerator(Hotkey::PauseContinue), MenuBarItem::CheckState::None, MenuIconSvg(GuiAction::PauseOrContinueEmulation)));
systemMenu.push_back(MenuBarItem(static_cast<int32_t>(GuiAction::StopEmulation), "&Stop", nullptr, HotkeyAccelerator(Hotkey::StopEmulation), MenuBarItem::CheckState::None, MenuIconSvg(GuiAction::StopEmulation)));
systemMenu.push_back(MenuBarItem(MenuBarItem::SPLITER));
systemMenu.push_back(MenuBarItem(static_cast<int32_t>(GuiAction::ToggleSpeedLimit), "Limit &Speed", nullptr, HotkeyAccelerator(Hotkey::ToggleSpeedLimit), m_useSpeedLimit ? MenuBarItem::CheckState::Checked : MenuBarItem::CheckState::Unchecked));
mainTitleMenu.push_back(MenuBarItem(MenuBarItem::SUB_MENU, "&System", &systemMenu));
}
MenuBarItemList viewMenu;
if (!m_emulationRunning)
{
viewMenu.push_back(MenuBarItem(static_cast<int32_t>(GuiAction::ToggleStartGamesInFullscreen), "&Start games in fullscreen", nullptr, nullptr, uiSettings.startGamesInFullscreen ? MenuBarItem::CheckState::Checked : MenuBarItem::CheckState::Unchecked));
viewMenu.push_back(MenuBarItem(static_cast<int32_t>(GuiAction::ToggleStartGamesWithUiHidden), "Start games with &UI hidden", nullptr, nullptr, uiSettings.startGamesWithUiHidden ? MenuBarItem::CheckState::Checked : MenuBarItem::CheckState::Unchecked));
viewMenu.push_back(MenuBarItem(MenuBarItem::SPLITER));
}
if (m_emulationRunning)
{
viewMenu.push_back(MenuBarItem(static_cast<int32_t>(GuiAction::ToggleFullscreen), "&Fullscreen", nullptr, HotkeyAccelerator(Hotkey::Fullscreen), MenuBarItem::CheckState::None, MenuIconSvg(GuiAction::ToggleFullscreen)));
}
if (m_emulationRunning)
{
viewMenu.push_back(MenuBarItem(static_cast<int32_t>(GuiAction::ToggleHideUi), m_hideUi ? "Show &UI" : "Hide &UI", nullptr, HotkeyAccelerator(Hotkey::HideUi), MenuBarItem::CheckState::None, MenuIconSvg(GuiAction::ToggleHideUi)));
}
MenuBarItemList resetWindowSizeMenu;
const std::string * resizeMenuSvg = MenuIconSvg(GuiAction::ResetWindowSize720p);
resetWindowSizeMenu.push_back(MenuBarItem(static_cast<int32_t>(GuiAction::ResetWindowSize720p), "Reset Window Size to 720p", nullptr, nullptr, MenuBarItem::CheckState::None, resizeMenuSvg));
resetWindowSizeMenu.push_back(MenuBarItem(static_cast<int32_t>(GuiAction::ResetWindowSize900p), "Reset Window Size to 900p", nullptr, nullptr, MenuBarItem::CheckState::None, resizeMenuSvg));
resetWindowSizeMenu.push_back(MenuBarItem(static_cast<int32_t>(GuiAction::ResetWindowSize1080p), "Reset Window Size to 1080p", nullptr, nullptr, MenuBarItem::CheckState::None, resizeMenuSvg));
viewMenu.push_back(MenuBarItem(MenuBarItem::SUB_MENU, "Reset Window Size", &resetWindowSizeMenu, nullptr, MenuBarItem::CheckState::None, resizeMenuSvg));
mainTitleMenu.push_back(MenuBarItem(MenuBarItem::SUB_MENU, "&View", &viewMenu));
MenuBarItemList optionsMenu;
optionsMenu.push_back(MenuBarItem(static_cast<int32_t>(GuiAction::OpenControllersDialog), "&Controllers...", nullptr, HotkeyAccelerator(Hotkey::Controllers), MenuBarItem::CheckState::None, MenuIconSvg(GuiAction::OpenControllersDialog)));
optionsMenu.push_back(MenuBarItem(static_cast<int32_t>(GuiAction::OpenSystemConfiguration), "Confi&gure...", nullptr, HotkeyAccelerator(Hotkey::Configure), MenuBarItem::CheckState::None, MenuIconSvg(GuiAction::OpenSystemConfiguration)));
MenuBarItemList installFirmwareMenu;
if (!m_emulationRunning && !m_firmwareInstallInProgress && m_modules.IsValid())
{
optionsMenu.push_back(MenuBarItem(MenuBarItem::SPLITER));
const std::string * installFirmwareSvg = MenuIconSvg(GuiAction::InstallFirmwareFromFile);
installFirmwareMenu.push_back(MenuBarItem(static_cast<int32_t>(GuiAction::InstallFirmwareFromFile), "Install from &DXCI or ZIP...", nullptr, nullptr, MenuBarItem::CheckState::None, installFirmwareSvg));
installFirmwareMenu.push_back(MenuBarItem(static_cast<int32_t>(GuiAction::InstallFirmwareFromFolder), "Install from &folder...", nullptr, nullptr, MenuBarItem::CheckState::None, installFirmwareSvg));
optionsMenu.push_back(MenuBarItem(MenuBarItem::SUB_MENU, "Install &Firmware", &installFirmwareMenu));
}
mainTitleMenu.push_back(MenuBarItem(MenuBarItem::SUB_MENU, "&Options", &optionsMenu));
MenuBarItemList helpMenu;
helpMenu.push_back(MenuBarItem(static_cast<int32_t>(GuiAction::OpenDiscord), "&Discord", nullptr, nullptr, MenuBarItem::CheckState::None, MenuIconSvg(GuiAction::OpenDiscord)));
helpMenu.push_back(MenuBarItem(static_cast<int32_t>(GuiAction::OpenReportIssues), "&Report Issues", nullptr, nullptr, MenuBarItem::CheckState::None, MenuIconSvg(GuiAction::OpenReportIssues)));
helpMenu.push_back(MenuBarItem(MenuBarItem::SPLITER));
helpMenu.push_back(MenuBarItem(static_cast<int32_t>(GuiAction::OpenAboutDialog), "&About NXEmu...", nullptr, nullptr, MenuBarItem::CheckState::None, MenuIconSvg(GuiAction::OpenAboutDialog)));
mainTitleMenu.push_back(MenuBarItem(MenuBarItem::SUB_MENU, "&Help", &helpMenu));
m_menuBar->AddSink(this);
m_menuBar->SetMenuContent(mainTitleMenu);
}
bool SciterMainWindow::Show()
{
enum
{
WINDOW_HEIGHT = 507,
WINDOW_WIDTH = 650,
};
#ifdef __APPLE__
constexpr const char * mainDocument = "main_window_macos.html";
#else
constexpr const char * mainDocument = "main_window.html";
#endif
if (!m_sciterUI.WindowCreate(nullptr, mainDocument, 0, 0, WINDOW_WIDTH, WINDOW_HEIGHT, SUIW_MAIN | SUIW_HIDDEN, m_window))
{
return false;
}
m_rootElement = m_window->GetRootElement();
#ifdef __APPLE__
NxEmuMacOSApplyWindowStyle(m_window->GetHandle());
m_sciterUI.UpdateWindow(m_rootElement.GetElementHwnd(true));
#endif
m_sciterUI.AttachHandler(m_rootElement, IID_IKEYSINK, (IKeySink *)this);
m_sciterUI.AttachHandler(m_rootElement, IID_EVENTSINK, (IEventSink *)this);
m_window->OnCloseSinkAdd(this);
m_window->OnDestroySinkAdd(this);
m_window->CenterWindow();
SetCaption(m_windowTitle);
SciterElement menuElement(m_rootElement.GetElementByID("MainMenu"));
std::shared_ptr<void> interfacePtr = menuElement.IsValid() ? m_sciterUI.GetElementInterface(menuElement, IID_IMENUBAR) : nullptr;
if (interfacePtr)
{
m_menuBar = std::static_pointer_cast<IMenuBar>(interfacePtr);
ResetMenu();
}
m_sciterUI.UpdateWindow(m_rootElement.GetElementHwnd(true));
SciterElement::RECT rect = {20, 20, 660, 500};
SciterElement mainContents(m_rootElement.GetElementByID("MainContents"));
if (mainContents.IsValid())
{
m_sciterUI.AttachHandler(mainContents, IID_IRESIZESINK, (IResizeSink *)this);
rect = mainContents.GetLocation();
}
CreateRenderWindow();
m_modules.Setup(*this);
RegisterApplets();
RegisterSystemCallbacks();
ResetMenu();
UpdateStatusWidgets();
UpdateEmulationStatusText();
m_discordPresence.SetEnabled(uiSettings.enableDiscordPresence);
UpdateDiscordPresence();
m_sciterUI.AttachHandler(m_rootElement.GetElementByID("dockedMode"), IID_ICLICKSINK, (IClickSink *)this);
#ifdef _WIN32
// NXEmu's OpenGL context implementation uses WGL. Other platforms
// currently provide Vulkan contexts only.
SciterElement(m_rootElement.GetElementByID("renderer")).AddClassName("interactive");
m_sciterUI.AttachHandler(m_rootElement.GetElementByID("renderer"), IID_ICLICKSINK, (IClickSink *)this);
#endif
m_sciterUI.AttachHandler(m_rootElement.GetElementByID("gpuCommandSynchronization"), IID_ICLICKSINK, (IClickSink *)this);
m_sciterUI.AttachHandler(m_rootElement.GetElementByID("volume"), IID_ICLICKSINK, (IClickSink *)this);
m_sciterUI.AttachHandler(m_rootElement.GetElementByID("volumeIcon"), IID_ICLICKSINK, (IClickSink *)this);
InitializeVolumeSlider(m_sciterUI, m_rootElement.GetElementByID("audioVolume"));
m_sciterUI.AttachHandler(m_rootElement, IID_ITIMERSINK, (ITimerSink *)this);
// Sciter has one timer slot per element. Keep input on the persistent
// contents element so status and deferred root timers cannot replace it.
m_inputTimerElement = mainContents;
m_sciterUI.AttachHandler(m_inputTimerElement, IID_ITIMERSINK, (ITimerSink *)this);
m_inputTimerElement.SetTimer(25, (uint32_t *)TIMER_UPDATE_INPUT);
ShowPanel(Panel::RomBrowser);
SciterElement rombrowser = m_rootElement.FindFirst("rombrowser");
interfacePtr = rombrowser.IsValid() ? m_sciterUI.GetElementInterface(rombrowser, IID_ROMBROWSER) : nullptr;
if (interfacePtr)
{
m_romBrowser = std::static_pointer_cast<IRomBrowser>(interfacePtr);
if (m_modules.IsValid())
{
m_romBrowser->SetMainWindow(this, &m_modules.Modules());
m_romBrowser->PopulateAsync();
}
else
{
m_romBrowser->SetMainWindow(this, nullptr);
}
}
if (!uiSettings.hasBrokenVulkan)
{
PopulateVulkanRecords(m_vkDeviceRecords, RenderSurface());
}
Notification::GetInstance().SetSciterContext(&m_sciterUI, (void *)m_window->GetHandle());
m_window->Show();
return true;
}
void SciterMainWindow::ShowConfig(const char * startPage)
{
m_systemConfig.reset(new SystemConfig(m_sciterUI, m_modules, m_vkDeviceRecords));
m_systemConfig->Display((void *)m_window->GetHandle(), startPage);
}
void SciterMainWindow::ShowGameConfig(const char * gamePath)
{
if (gamePath == nullptr || gamePath[0] == '\0' || !m_rootElement.IsValid())
{
return;
}
m_pendingGameConfigPath = gamePath;
m_rootElement.SetTimer(1, (uint32_t *)TIMER_OPEN_GAME_CONFIG);
}
void SciterMainWindow::LoadGame(const char * path, int32_t program_index, ApplicationLaunchType launch_type)
{
SettingsStore & settings = SettingsStore::GetInstance();
const int32_t previous_program_index = launch_type == ApplicationLaunchType::ApplicationInitiated ? m_currentProgramIndex : -1;
if (settings.GetBool(NXCoreSetting::EmulationRunning) && launch_type == ApplicationLaunchType::ApplicationInitiated)
{
m_pendingReloadPath = path != nullptr ? path : "";
m_pendingReloadProgramIndex = program_index;
m_pendingReloadLaunchType = launch_type;
m_reloadingGame = true;
settings.SetBool(NXCoreSetting::EmulationRunning, false);
return;
}
std::deque<std::vector<uint8_t>> pendingUserChannel = std::move(m_pendingUserChannel);
m_pendingUserChannel.clear();
if (m_romBrowser)
{
m_romBrowser->StopScan();
m_romBrowser->SetMainWindow(this, nullptr);
}
// Configuration pages retain controller and loader interfaces from the
// current modules. Retire them before Setup replaces those modules, even
// when a game is launched while a child window is still open.
m_inputConfig.reset();
m_systemConfig.reset();
m_gameConfig.reset();
m_modules.Setup(*this);
if (!m_modules.IsValid())
{
Notification::GetInstance().DisplayError("Required modules failed to load.", "Error loading file");
return;
}
if (m_romBrowser)
{
m_romBrowser->SetMainWindow(this, &m_modules.Modules());
}
RegisterApplets();
RegisterSystemCallbacks();
IOperatingSystem & operatingSystem = m_modules.Modules().OperatingSystem();
for (const auto & entry : pendingUserChannel)
{
operatingSystem.PushUserChannelEntry(entry.data(), static_cast<uint32_t>(entry.size()));
}
ISystemloader & loader = m_modules.Modules().Systemloader();
loader.LoadRom(path, program_index, previous_program_index, launch_type);
m_currentProgramIndex = program_index;
m_previousProgramIndex = previous_program_index;
m_reloadingGame = false;
UpdateEmulationStatusText();
}
void SciterMainWindow::OpenGameSaveDataLocation(const char * gamePath)
{
const uint64_t programId = ReadProgramIdForGame(m_modules, gamePath);
if (programId == 0)
{
Notification::GetInstance().DisplayError("Unable to determine the title ID for this game.", "Open Save Data Location");
return;
}
IOperatingSystem & operatingSystem = m_modules.Modules().OperatingSystem();
if (operatingSystem.GetProfileCount() == 0)
{
Notification::GetInstance().DisplayError("No user profiles are available. Create a profile first.", "Open Save Data Location");
return;
}
m_pendingSaveDataProgramId = programId;
ProfileSelectHostParameters parameters{};
parameters.mode = ProfileUiMode::UserSelector;
parameters.purpose = UserSelectionPurposeHost::General;
parameters.display_options.show_user_selector = true;
m_ProfileSelect.SelectProfile(this, &SciterMainWindow::OnSaveDataProfileSelected, &parameters);
}
void SciterMainWindow::OnSaveDataProfileSelected(void * user_data, bool has_uuid, const uint8_t uuid_bytes[16])
{
SciterMainWindow * window = (SciterMainWindow *)user_data;
if (window == nullptr)
{
return;
}
const uint64_t programId = window->m_pendingSaveDataProgramId;
window->m_pendingSaveDataProgramId = 0;
if (!has_uuid || uuid_bytes == nullptr || programId == 0)
{
return;
}
window->OpenSaveDataFolderForUser(programId, uuid_bytes);
}
void SciterMainWindow::OpenSaveDataFolderForUser(uint64_t programId, const uint8_t uuidBytes[HOST_PROFILE_UUID_SIZE])
{
std::array<uint8_t, HOST_PROFILE_UUID_SIZE> uuidArray{};
std::memcpy(uuidArray.data(), uuidBytes, HOST_PROFILE_UUID_SIZE);
const Common::UUID uuid{uuidArray};
const u128 userId = uuid.AsU128();
const std::filesystem::path nandDir = Common::FS::GetYuzuPath(Common::FS::YuzuPath::NANDDir);
const std::string userFolder = stdstr_f("%016llX%016llX", (unsigned long long)userId[1], (unsigned long long)userId[0]);
const std::string titleFolder = stdstr_f("%016llX", (unsigned long long)programId);
const std::filesystem::path savePath = nandDir / "user" / "save" / "0000000000000000" / userFolder / titleFolder;
void * owner = m_window != nullptr ? (void *)m_window->GetHandle() : nullptr;
if (!OpenFolderPath(savePath, owner))
{
Notification::GetInstance().DisplayError("Unable to open the save data location.", "Open Save Data Location");
}
}
void SciterMainWindow::OpenGameModDataLocation(const char * gamePath)
{
const uint64_t programId = ReadProgramIdForGame(m_modules, gamePath);
if (programId == 0)
{
Notification::GetInstance().DisplayError("Unable to determine the title ID for this game.", "Open Mod Data Location");
return;
}
const std::filesystem::path modPath = Common::FS::GetYuzuPath(Common::FS::YuzuPath::LoadDir) / std::string(stdstr_f("%016llX", (unsigned long long)programId));
void * owner = m_window != nullptr ? (void *)m_window->GetHandle() : nullptr;
if (!OpenFolderPath(modPath, owner))
{
Notification::GetInstance().DisplayError("Unable to open the mod data location.", "Open Mod Data Location");
}
}
void SciterMainWindow::UpdateStatusWidgets()
{
SettingsStore & settings = SettingsStore::GetInstance();
SciterElement dockedMode(m_rootElement.GetElementByID("dockedMode"));
if (dockedMode.IsValid())
{
stdstr_f text("%s", DockedModeLabel((DockedMode)settings.GetInt(NXOsSetting::DockedMode)));
dockedMode.SetHTML((const uint8_t *)text.c_str(), text.size());
}
SciterElement renderer(m_rootElement.GetElementByID("renderer"));
if (renderer.IsValid())
{
stdstr_f text("%s", RendererBackendLabel((RendererBackend)settings.GetInt(NXVideoSetting::GraphicsAPI)));
renderer.SetHTML((const uint8_t *)text.c_str(), text.size());
}
SciterElement gpuCommandSynchronization(m_rootElement.GetElementByID("gpuCommandSynchronization"));
if (gpuCommandSynchronization.IsValid())
{
const bool enabled = settings.GetBool(NXVideoSetting::GpuCommandSynchronization);
stdstr_f text("GPU Sync: %s", enabled ? "ON" : "OFF");
gpuCommandSynchronization.SetHTML((const uint8_t *)text.c_str(), text.size());
}
SciterElement volume(m_rootElement.GetElementByID("volume"));
if (volume.IsValid())
{
bool muted = settings.GetBool(NXOsSetting::AudioMuted);
stdstr_f text("%d%%", settings.GetInt(NXOsSetting::AudioVolume));
volume.SetHTML((const uint8_t *)text.c_str(), text.size());
SciterElement volumeIcon(m_rootElement.GetElementByID("volumeIcon"));
if (muted)
{
volumeIcon.AddClassName("muted");
}
else
{
volumeIcon.RemoveClassName("muted");
}
}
SciterElement audioVolume(m_rootElement.GetElementByID("audioVolume"));
if (audioVolume.IsValid())
{
const int32_t configuredVolume = settings.GetInt(NXOsSetting::AudioVolume);
const SciterValue displayedVolume = audioVolume.GetValue();
if (!displayedVolume.isInt() || displayedVolume.GetValueInt() != configuredVolume)
{
audioVolume.SetValue(SciterValue(configuredVolume));
}
}
}
void SciterMainWindow::UpdateInputDrivers()
{
if (m_modules.IsValid())
{
IOperatingSystem & operatingSystem = m_modules.Modules().OperatingSystem();
operatingSystem.PumpInputEvents();
}
UpdateMouseCursorHiding();
}
void SciterMainWindow::CreateRenderWindow()
{
SciterElement mainContents(m_rootElement.GetElementByID("MainContents"));
SciterElement::RECT rect = mainContents.GetLocation();
uint32_t width = rect.right - rect.left;
uint32_t height = rect.bottom - rect.top;
#ifdef _WIN32
m_renderWindow = CreateWindowExW(0, L"Static", L"", WS_CHILD | WS_VISIBLE | WS_CLIPSIBLINGS,
rect.left, rect.top, width, height, (HWND)m_window->GetHandle(), nullptr, GetModuleHandle(nullptr), nullptr);
ShowWindow((HWND)m_renderWindow, SW_HIDE);
#elif defined(__APPLE__)
m_renderWindow = NxEmuMacOSCreateRenderView(m_window->GetHandle());
NxEmuMacOSLayoutRenderView(m_renderWindow, rect.left, rect.top, width, height);
#else
(void)width;
(void)height;
#endif
if (m_modules.IsValid())
{
IVideo & video = m_modules.Modules().Video();
video.UpdateFramebufferLayout(rect.right - rect.left, rect.bottom - rect.top);
}
}
void SciterMainWindow::ResetMouseCursorHiding()
{
m_mouseCursorHidden = false;
m_lastMouseActivityTick = 0;
#ifdef _WIN32
SetCursor(LoadCursor(nullptr, IDC_ARROW));
#endif
}
void SciterMainWindow::UpdateDiscordPresence()
{
const std::string gameName = SettingsStore::GetInstance().GetString(NXCoreSetting::GameName);
m_discordPresence.Update(m_emulationRunning, gameName);
}
void SciterMainWindow::UpdateMouseCursorHiding()
{
if (!m_emulationRunning || !uiSettings.hideMouseOnInactivity)
{
if (m_mouseCursorHidden || m_lastMouseActivityTick != 0)
{
ResetMouseCursorHiding();
}
return;
}
#ifdef _WIN32
HWND hwnd = (HWND)m_renderWindow;
if (hwnd == nullptr)
{
return;
}
POINT cursor_pos{};
if (!GetCursorPos(&cursor_pos))
{
return;
}
RECT window_rect{};
if (!GetWindowRect(hwnd, &window_rect))
{
return;
}
const bool over_render_window = PtInRect(&window_rect, cursor_pos) != 0;
const uint64_t now = NowMs();
if (!over_render_window)
{
if (m_mouseCursorHidden)
{
ResetMouseCursorHiding();
}
return;
}
if (m_lastMouseActivityTick == 0 || cursor_pos.x != m_lastTrackedMouseX ||
cursor_pos.y != m_lastTrackedMouseY)
{
m_lastTrackedMouseX = cursor_pos.x;
m_lastTrackedMouseY = cursor_pos.y;
m_lastMouseActivityTick = now;
if (m_mouseCursorHidden)
{
m_mouseCursorHidden = false;
SetCursor(LoadCursor(nullptr, IDC_ARROW));
}
return;
}
if (!m_mouseCursorHidden && now - m_lastMouseActivityTick >= static_cast<uint64_t>(kDefaultMouseHideTimeoutMs))
{
m_mouseCursorHidden = true;
}
if (m_mouseCursorHidden)
{
SetCursor(nullptr);
}
#endif
}
void SciterMainWindow::SetCaption(const std::string & caption)
{
m_rootElement.Eval(stdstr_f("Window.this.caption = \"%s\";", caption.c_str()).c_str());
SciterElement captionElement(m_rootElement.FindFirst("[role='window-caption'] > span"));
if (captionElement.IsValid())
{
captionElement.SetHTML((uint8_t *)caption.data(), caption.size());
}
}
void SciterMainWindow::EmulationRunning(const char * /*setting*/, void * userData)
{
SciterMainWindow * impl = (SciterMainWindow *)userData;
SettingsStore & settings = SettingsStore::GetInstance();
impl->m_emulationRunning = settings.GetBool(NXCoreSetting::EmulationRunning);
if (!impl->m_emulationRunning && impl->m_pendingReloadProgramIndex != -1)
{
impl->m_rootElement.PostEvent(EVENT_RELOAD_PROGRAM);
return;
}
impl->m_pendingStartInFullscreen = impl->m_emulationRunning && uiSettings.startGamesInFullscreen;
impl->m_pendingStartWithUiHidden = impl->m_emulationRunning && uiSettings.startGamesWithUiHidden;
#ifdef _WIN32
if (!impl->m_emulationRunning && impl->m_win32Fullscreen && impl->m_win32Fullscreen->active)
{
impl->ExitFullscreen();
}
#endif
if (!impl->m_emulationRunning)
{
impl->m_hideUi = false;
impl->ResetMouseCursorHiding();
}
if (settings.GetBool(NXCoreSetting::ShuttingDown))
{
return;
}
if (impl->m_emulationRunning)
{
if (impl->m_renderWindow != nullptr)
{
#ifdef _WIN32
DestroyWindow((HWND)impl->m_renderWindow);
#elif defined(__APPLE__)
NxEmuMacOSDestroyRenderView(impl->m_renderWindow);
#endif
impl->m_renderWindow = nullptr;
}
impl->CreateRenderWindow();
impl->m_lastMouseActivityTick = 0;
}
SciterElement renderer(impl->m_rootElement.GetElementByID("renderer"));
if (renderer)
{
renderer.SetState(impl->m_emulationRunning ? SciterElement::STATE_DISABLED : 0, impl->m_emulationRunning ? 0 : SciterElement::STATE_DISABLED, true);
}
impl->ResetMenu();
impl->UpdateUIVisibility();
impl->UpdateDiscordPresence();
}
void SciterMainWindow::ShowPanel(Panel panel)
{
struct PanelEntry
{
Panel panel;
const char * id;
};
static const PanelEntry panels[] = {
{Panel::RomBrowser, "RomBrowserPanel"},
{Panel::Loading, "LoadingPanel"},
{Panel::Pause, "PausePanel"},
};
for (const PanelEntry & entry : panels)
{
SciterElement elem(m_rootElement.GetElementByID(entry.id));
if (!elem.IsValid())
{
continue;
}
elem.SetStyleAttribute("display", panel == entry.panel ? "block" : "none");
}
#ifdef _WIN32
ShowWindow((HWND)m_renderWindow, panel == Panel::Renderer ? SW_SHOW : SW_HIDE);
#elif defined(__APPLE__)
NxEmuMacOSSetRenderViewVisible(m_renderWindow, panel == Panel::Renderer);
#endif
m_sciterUI.UpdateWindow(m_rootElement.GetElementHwnd(true));
}
void SciterMainWindow::EmulationStateChanged(const char * /*setting*/, void * userData)
{
SciterMainWindow * impl = (SciterMainWindow *)userData;
const auto state = SettingsStore::GetInstance().GetInt(NXCoreSetting::EmulationState);
// Capture the reload disposition before deferring the notification: the
// next LoadGame may clear the flag before the UI timer drains the queue.
if (state == static_cast<int32_t>(EmulationState::Stopped) &&
impl->m_reloadingGame.load(std::memory_order_relaxed))
{
return;
}
// Notifications also originate on the emulation control thread. DOM and
// window updates must run on the UI thread, which may be waiting for that
// control thread to finish pausing or stopping.
std::scoped_lock lock{impl->m_emulationStateMutex};
impl->m_emulationStateUpdates.push_back(state);
}
void SciterMainWindow::OnEmulationStateChanged(int32_t value)
{
SciterMainWindow * impl = this;
const auto state = static_cast<EmulationState>(value);
if (state == EmulationState::RomLoaded)
{
if (impl->m_firmwareInstallUiActive && !impl->m_firmwareInstallInProgress)
{
impl->StopFirmwareInstallUi();
}
impl->UpdateLoadingScreenDetails();
SciterElement loadingMain(impl->m_rootElement.FindFirst("#LoadingPanel .loading-main"));
if (loadingMain.IsValid())
{
loadingMain.RemoveClassName("no-game-icon");
}
impl->RefreshDiskCacheLoadingText();
impl->m_shownFirstFrame = false;
impl->ShowPanel(Panel::Loading);
}
else if (state == EmulationState::Running)
{
if (impl->m_pendingStartInFullscreen && uiSettings.startGamesInFullscreen)
{
impl->m_pendingStartInFullscreen = false;
#ifdef _WIN32
impl->EnterFullscreen();
#endif
}
if (impl->m_pendingStartWithUiHidden && uiSettings.startGamesWithUiHidden)
{
impl->m_pendingStartWithUiHidden = false;
impl->m_hideUi = true;
impl->UpdateUIVisibility();
impl->m_sciterUI.UpdateWindow(impl->m_rootElement.GetElementHwnd(true));
SciterElement mainContents(impl->m_rootElement.GetElementByID("MainContents"));
if (mainContents.IsValid())
{
mainContents.Update(true);
}
impl->LayoutRenderWindow();
}
impl->m_rootElement.PostEvent(EVENT_EMULATION_RUNNING);
impl->ResetMenu();
impl->m_lastMouseActivityTick = 0;
if (impl->m_shownFirstFrame)
{
impl->ShowPanel(Panel::Renderer);
}
}
else if (state == EmulationState::Paused)
{
impl->ResetMenu();
impl->ShowPanel(Panel::Pause);
impl->UpdateEmulationStatusText();
}
else if (state == EmulationState::Stopped)
{
if (impl->m_reloadingGame)
{
return;
}
if (impl->m_romBrowser && impl->m_modules.IsValid())
{
impl->m_romBrowser->PopulateAsync();
}
impl->m_rootElement.PostEvent(EVENT_EMULATION_STOPPED);
}
}
void SciterMainWindow::GameFileChanged(const char * /*setting*/, void * userData)
{
SciterMainWindow * impl = (SciterMainWindow *)userData;
enum
{
maxRememberedFiles = 10
};
Stringlist & recentFiles = uiSettings.recentFiles;
std::string gameFile = SettingsStore::GetInstance().GetString(NXCoreSetting::GameFile);
for (Stringlist::const_iterator itr = recentFiles.begin(); itr != recentFiles.end(); itr++)
{
if (_stricmp(gameFile.c_str(), itr->c_str()) != 0)
{
continue;
}
recentFiles.erase(itr);
break;
}
recentFiles.insert(recentFiles.begin(), gameFile);
if (recentFiles.size() > maxRememberedFiles)
{
recentFiles.resize(maxRememberedFiles);
}
impl->ResetMenu();
SaveUISetting();
}
void SciterMainWindow::GameNameChanged(const char * /*setting*/, void * userData)
{
SciterMainWindow * impl = (SciterMainWindow *)userData;
std::string gameName = SettingsStore::GetInstance().GetString(NXCoreSetting::GameName);
if (gameName.length() > 0)
{
std::string caption;
caption += gameName;
caption += " | ";
caption += impl->m_windowTitle;
impl->SetCaption(caption);
}
impl->UpdateDiscordPresence();
}
void SciterMainWindow::DisplayedFramesChanged(const char * /*setting*/, void * userData)
{
SettingsStore & settings = SettingsStore::GetInstance();
SciterMainWindow * impl = (SciterMainWindow *)userData;
impl->m_shownFirstFrame = settings.GetBool(NXCoreSetting::DisplayedFrames);
if (impl->m_shownFirstFrame)
{
impl->m_rootElement.PostEvent(EVENT_EMULATION_FIRST_FRAME);
}
}
void SciterMainWindow::DiskCacheLoadChanged(const char * /*setting*/, void * userData)
{
SciterMainWindow * impl = static_cast<SciterMainWindow *>(userData);
SettingsStore & settings = SettingsStore::GetInstance();
const int stage = settings.GetInt(NXCoreSetting::DiskCacheLoadStage);
constexpr uint64_t intervalMs = 50;
const uint64_t now = NowMs();
const bool neverPosted = (impl->m_lastDiskCacheStatusPostMs == 0);
const bool stageChanged = (stage != impl->m_lastPostedDiskCacheStage);
const uint64_t elapsed = neverPosted ? intervalMs : (now - impl->m_lastDiskCacheStatusPostMs);
if (!neverPosted && !stageChanged && elapsed < intervalMs)
{
return;
}
impl->m_lastPostedDiskCacheStage = stage;
impl->m_lastDiskCacheStatusPostMs = now;
impl->m_rootElement.PostEvent(EVENT_DISK_CACHE_STATUS);
}
void SciterMainWindow::RefreshDiskCacheLoadingText()
{
SettingsStore & settings = SettingsStore::GetInstance();
const int stage = settings.GetInt(NXCoreSetting::DiskCacheLoadStage);
const int current = settings.GetInt(NXCoreSetting::DiskCacheLoadCurrent);
const int total = settings.GetInt(NXCoreSetting::DiskCacheLoadTotal);
SciterElement fillEl(m_rootElement.GetElementByID("LoadingProgressFill"));
std::string text;
switch (stage)
{
case 0:
text = GetGameTitleLoadingHtml(m_modules.Modules().Systemloader(), "Loading");
UpdateLoadingProgressBar(fillEl, true, 0, false);
break;
case 1:
text = stdstr_f(
"<span class=\"loading-verb\">Loading shaders</span> <span class=\"loading-shader-count\">%d / %d</span>",
current, total);
if (total > 0)
{
const int pct = static_cast<int>((100.0 * static_cast<double>(current)) / static_cast<double>(total));
UpdateLoadingProgressBar(fillEl, false, pct, true);
}
else
{
UpdateLoadingProgressBar(fillEl, true, 0, false);
}
break;
case 2:
text = GetGameTitleLoadingHtml(m_modules.Modules().Systemloader(), "Launching");
UpdateLoadingProgressBar(fillEl, false, 100, false);
break;
default:
UpdateLoadingProgressBar(fillEl, true, 0, false);
return;
}
SciterElement status(m_rootElement.GetElementByID("loadingText"));
if (status.IsValid())
{
status.SetHTML(reinterpret_cast<const uint8_t *>(text.c_str()), text.size());
}
m_sciterUI.UpdateWindow(m_rootElement.GetElementHwnd(true));
}
void SciterMainWindow::HotKeysChanged(const char * /*setting*/, void * userData)
{
SciterMainWindow * impl = (SciterMainWindow *)userData;
impl->ResetMenu();
}
void SciterMainWindow::PreventOSSleep()
{
#ifdef _WIN32
SetThreadExecutionState(ES_CONTINUOUS | ES_SYSTEM_REQUIRED | ES_DISPLAY_REQUIRED);
#elif defined(HAVE_SDL2)
SDL_DisableScreenSaver();
#endif
}
void SciterMainWindow::AllowOSSleep()
{
#ifdef _WIN32
SetThreadExecutionState(ES_CONTINUOUS);
#elif defined(HAVE_SDL2)
SDL_EnableScreenSaver();
#endif
}
void SciterMainWindow::OnOpenFile()
{
if (m_window == nullptr || !m_modules.IsValid())
{
return;
}
ISystemloader & loader = m_modules.Modules().Systemloader();
const std::string filter = BuildSwitchOpenFileFilter(loader);
Path fileToOpen;
if (!FileSelect((void *)m_window->GetHandle(), Path(Path::MODULE_DIRECTORY), filter.c_str(), true, fileToOpen))
{
return;
}
LoadGame(fileToOpen, 0, ApplicationLaunchType::FrontendInitiated);
}
void SciterMainWindow::OnInstallFirmwareFromFile()
{
if (m_window == nullptr || m_emulationRunning || !m_modules.IsValid() || m_firmwareInstallInProgress)
{
return;
}
Path file;
const char * filter = "Firmware Package (*.dxci, *.zip)\0*.dxci;*.zip\0DXCI (*.dxci)\0*.dxci\0ZIP (*.zip)\0*.zip\0All files (*.*)\0*.*\0";
if (!FileSelect((void *)m_window->GetHandle(), Path(Path::CURRENT_DIRECTORY), filter, true, file))
{
return;
}
BeginFirmwareInstall(file);
}
void SciterMainWindow::OnInstallFirmwareFromFolder()
{
if (m_window == nullptr || m_emulationRunning || !m_modules.IsValid() || m_firmwareInstallInProgress)
{
return;
}
Path folder = BrowseForDirectory((void *)m_window->GetHandle(), "Select folder containing firmware .dnca files");
if (!folder.DirectoryExists())
{
return;
}
BeginFirmwareInstall((const char *)folder);
}
void SciterMainWindow::RefreshFirmwareInstallLoading()
{
SettingsStore & settings = SettingsStore::GetInstance();
const uint32_t current = static_cast<uint32_t>(settings.GetInt(NXLoaderSetting::FirmwareInstallCurrent));
const uint32_t total = static_cast<uint32_t>(settings.GetInt(NXLoaderSetting::FirmwareInstallTotal));
SciterElement fillEl(m_rootElement.GetElementByID("LoadingProgressFill"));
SciterElement status(m_rootElement.GetElementByID("loadingText"));
std::string detail;
if (total > 0)
{
detail = stdstr_f("%u / %u files", current, total);
if (fillEl.IsValid())
{
const int pct = static_cast<int>((100.0 * static_cast<double>(current)) / static_cast<double>(total));
UpdateLoadingProgressBar(fillEl, false, pct, false);
}
}
else if (fillEl.IsValid())
{
UpdateLoadingProgressBar(fillEl, true, 0, false);
}
if (status.IsValid())
{
const std::string text = stdstr_f("<span class=\"loading-verb\">Installing firmware</span> <span class=\"loading-game-name\">%s</span>", HtmlEscape(detail).c_str());
status.SetHTML(reinterpret_cast<const uint8_t *>(text.c_str()), text.size());
}
m_sciterUI.UpdateWindow(m_rootElement.GetElementHwnd(true));
}
void SciterMainWindow::StartFirmwareInstallUi()
{
if (m_firmwareInstallUiActive)
{
return;
}
const int32_t total = SettingsStore::GetInstance().GetInt(NXLoaderSetting::FirmwareInstallTotal);
if (total <= 0 && !m_firmwareInstallInProgress)
{
return;
}
m_firmwareInstallUiActive = true;
SciterElement fillEl(m_rootElement.GetElementByID("LoadingProgressFill"));
SciterElement status(m_rootElement.GetElementByID("loadingText"));
if (fillEl.IsValid())
{
UpdateLoadingProgressBar(fillEl, true, 0, false);
}
if (status.IsValid())
{
const std::string text = "<span class=\"loading-verb\">Installing firmware</span>";
status.SetHTML(reinterpret_cast<const uint8_t *>(text.c_str()), text.size());
}
LoadImageToElement(m_rootElement.GetElementByID("LoadingCornerLogo"), {});
LoadImageToElement(m_rootElement.GetElementByID("LoadingCornerBanner"), {});
LoadImageToElement(m_rootElement.GetElementByID("LoadingGameIcon"), {});
SciterElement loadingMain(m_rootElement.FindFirst("#LoadingPanel .loading-main"));
if (loadingMain.IsValid())
{
loadingMain.AddClassName("no-game-icon");
}
ShowPanel(Panel::Loading);
m_sciterUI.UpdateWindow(m_rootElement.GetElementHwnd(true));
if (!m_emulationRunning)
{
ResetMenu();
}
m_rootElement.SetTimer(25, (uint32_t *)TIMER_UPDATE_INSTALL_FIRMWARE);
RefreshFirmwareInstallLoading();
}
void SciterMainWindow::StopFirmwareInstallUi()
{
if (!m_firmwareInstallUiActive)
{
return;
}
m_firmwareInstallUiActive = false;
m_rootElement.SetTimer(0, (uint32_t *)TIMER_UPDATE_INSTALL_FIRMWARE);
SciterElement loadingMain(m_rootElement.FindFirst("#LoadingPanel .loading-main"));
if (loadingMain.IsValid())
{
loadingMain.RemoveClassName("no-game-icon");
}
const EmulationState state = static_cast<EmulationState>(SettingsStore::GetInstance().GetInt(NXCoreSetting::EmulationState));
if (state == EmulationState::Running && m_shownFirstFrame)
{
ShowPanel(Panel::Renderer);
}
else if (state == EmulationState::Paused)
{
ShowPanel(Panel::Pause);
}
else if (state == EmulationState::RomLoaded)
{
UpdateLoadingScreenDetails();
ShowPanel(Panel::Loading);
RefreshDiskCacheLoadingText();
}
else
{
ShowPanel(Panel::RomBrowser);
}
UpdateEmulationStatusText();
if (!m_emulationRunning)
{
ResetMenu();
}
}
void SciterMainWindow::FinishFirmwareInstall()
{
StopFirmwareInstallUi();
if (m_firmwareInstallThread.joinable() && !m_firmwareInstallInProgress)
{
m_firmwareInstallThread.join();
}
if (!m_firmwareInstallThread.joinable())
{
m_firmwareInstallInProgress = false;
UpdateEmulationStatusText();
if (!m_emulationRunning)
{
ResetMenu();
}
}
}
void SciterMainWindow::BeginFirmwareInstall(const char * utf8_path)
{
if (utf8_path == nullptr || utf8_path[0] == '\0' || m_firmwareInstallInProgress || !m_modules.IsValid())
{
return;
}
m_firmwareInstallInProgress = true;
m_rootElement.PostEvent(EVENT_FIRMWARE_INSTALL_ACTIVE);
const std::string path = utf8_path;
m_firmwareInstallThread = std::thread([this, path]() {
m_modules.Modules().Systemloader().InstallFirmwarePackage(path.c_str());
m_firmwareInstallInProgress = false;
m_rootElement.PostEvent(EVENT_FIRMWARE_INSTALL_DONE);
});
}
void SciterMainWindow::FirmwareInstallTotalChanged(const char * /*setting*/, void * userData)
{
SciterMainWindow * impl = static_cast<SciterMainWindow *>(userData);
const int32_t total = SettingsStore::GetInstance().GetInt(NXLoaderSetting::FirmwareInstallTotal);
const int32_t previous = impl->m_firmwareInstallLastTotal;
impl->m_firmwareInstallLastTotal = total;
if (total > 0)
{
impl->m_rootElement.PostEvent(EVENT_FIRMWARE_INSTALL_ACTIVE);
}
else if (previous > 0)
{
impl->m_rootElement.PostEvent(EVENT_FIRMWARE_INSTALL_DONE);
}
}
bool SciterMainWindow::ConfirmCloseEmulator()
{
if (!m_emulationRunning || !uiSettings.confirmBeforeStopping)
{
return true;
}
const char * msg = "Are you sure you want to exit the emulator?\n\nAny unsaved progress will be lost.";
bool exitLocked = m_modules.IsValid() && m_modules.Modules().OperatingSystem().GetExitLocked();
if (exitLocked)
{
msg = "The currently running application has requested NxEmu to not exit.\n\nWould you like to bypass this and close the emulator anyway?";
}
return g_notify->Query(msg, "NxEmu - Exit Emulator?") == NotificationResponse::Yes;
}
void SciterMainWindow::OnFileExit()
{
if (!m_rootElement.IsValid())
{
DoFileExit();
return;
}
m_rootElement.SetTimer(1, (uint32_t *)TIMER_DEFERRED_FILE_EXIT);
}
void SciterMainWindow::DoFileExit()
{
if (!ConfirmCloseEmulator())
{
return;
}
m_sciterUI.Stop();
}
void SciterMainWindow::OnStopGame()
{
if (!m_emulationRunning)
{
return;
}
if (!m_rootElement.IsValid())
{
DoStopGame();
return;
}
m_rootElement.SetTimer(1, (uint32_t *)TIMER_DEFERRED_STOP_GAME);
}
void SciterMainWindow::DoStopGame()
{
if (!m_emulationRunning)
{
return;
}
if (uiSettings.confirmBeforeStopping)
{
const char * msg = "Are you sure you want to stop the emulation?\n\nAny unsaved progress will be lost.";
bool exitLocked = m_modules.IsValid() && m_modules.Modules().OperatingSystem().GetExitLocked();
if (exitLocked)
{
msg = "The currently running application has requested NxEmu to not exit.\n\nWould you like to bypass this and stop emulation anyway?";
}
if (g_notify->Query(msg, "NxEmu - Stop Emulation") == NotificationResponse::No)
{
return;
}
}
AllowOSSleep();
SettingsStore & settings = SettingsStore::GetInstance();
settings.SetBool(NXCoreSetting::EmulationRunning, false);
}
void SciterMainWindow::OnPauseContinueGame()
{
if (!m_emulationRunning || !m_modules.IsValid())
{
return;
}
IOperatingSystem & os = m_modules.Modules().OperatingSystem();
if (os.IsEmulationPaused())
{
PreventOSSleep();
os.SetEmulationPaused(false);
}
else
{
os.SetEmulationPaused(true);
AllowOSSleep();
}
ResetMenu();
}
void SciterMainWindow::OnSystemConfig()
{
ShowConfig(nullptr);
}
void SciterMainWindow::OnInputConfig()
{
m_inputConfig.reset(new InputConfig(m_sciterUI, m_modules));
m_inputConfig->Display((void *)m_window->GetHandle());
}
void SciterMainWindow::OnAbout()
{
m_aboutDialog.reset(new AboutDialog(m_sciterUI));
m_aboutDialog->Display((void *)m_window->GetHandle());
}
void SciterMainWindow::OnOpenDiscord()
{
ShellOpen(discordUrl);
}
void SciterMainWindow::OnOpenReportIssues()
{
ShellOpen(reportUrl);
}
void SciterMainWindow::OnOpenAppDirectory()
{
const std::string path = Common::FS::GetYuzuPathString(Common::FS::YuzuPath::YuzuDir);
if (!path.empty())
{
ShellOpen(path.c_str(), m_window != nullptr ? (void *)m_window->GetHandle() : nullptr);
}
}
void SciterMainWindow::OnOpenLogDirectory()
{
const std::string path = Common::FS::GetYuzuPathString(Common::FS::YuzuPath::LogDir);
if (!path.empty())
{
ShellOpen(path.c_str(), m_window != nullptr ? (void *)m_window->GetHandle() : nullptr);
}
}
void SciterMainWindow::UpdateEmulationStatusText()
{
SciterElement statusTextEl(m_rootElement.GetElementByID("StatusText"));
if (!statusTextEl.IsValid())
{
return;
}
if (!m_modules.IsValid())
{
return;
}
IOperatingSystem & operatingSystem = m_modules.Modules().OperatingSystem();
IVideo & video = m_modules.Modules().Video();
ISystemloader & loader = m_modules.Modules().Systemloader();
std::vector<std::string> parts;
if (m_emulationRunning)
{
if (operatingSystem.IsEmulationPaused())
{
parts.push_back("Paused");
}
const int shaders_building = video.ShadersBuilding();
if (shaders_building > 0)
{
parts.push_back(stdstr_f("Building: %d shader(s)", shaders_building));
}
if (m_resolutionUpFactor != 1.0)
{
parts.push_back(stdstr_f("Scale: %.0fx", m_resolutionUpFactor));
}
PerfStatsResults results = operatingSystem.GetAndResetPerfStats();
if (!m_useMultiCore)
{
if (m_useSpeedLimit)
{
if (results.emulation_speed > 0.999 && results.emulation_speed < 1.01)
{
results.emulation_speed = 100.0;
}
parts.push_back(stdstr_f("Speed: %.0f / %d", results.emulation_speed * 100.0, m_speedLimit));
}
else
{
parts.push_back(stdstr_f("Speed: %f", results.emulation_speed * 100.0));
}
}
if (results.average_game_fps != 0)
{
parts.push_back(stdstr_f("%.0f FPS (%.2f ms)%s", std::round(results.average_game_fps), std::isnan(results.frametime) ? 0.0 : (results.frametime * 1000.0), m_useSpeedLimit ? "" : " Unlocked"));
}
}
else
{
m_rootElement.SetTimer(0, (uint32_t *)TIMER_UPDATE_STATUS);
}
const std::string firmware_version = GetInstalledFirmwareDisplayVersion(loader);
if (!firmware_version.empty())
{
parts.push_back("Firmware: " + firmware_version);
}
std::string status;
for (size_t i = 0; i < parts.size(); i++)
{
if (i > 0) status += " | ";
status += parts[i];
}
statusTextEl.SetText(status.c_str());
}
const MenuBarAccelerator * SciterMainWindow::HotkeyAccelerator(const char * name)
{
if (name == nullptr)
{
return nullptr;
}
HotkeyMap::iterator itr = uiSettings.hotkeys.find(name);
return itr != uiSettings.hotkeys.end() ? &itr->second : nullptr;
}
const char * SciterMainWindow::IsMenuBarAccelerator(uint32_t keyCode, uint32_t keyboardState)
{
for (const HotkeyMap::value_type & entry : uiSettings.hotkeys)
{
if (AcceleratorMatchesKey(entry.second, keyCode, keyboardState))
{
return entry.first.c_str();
}
}
return nullptr;
}
SciterMainWindow::GuiAction SciterMainWindow::HotkeyToGuiAction(const char * hotkeyId)
{
if (hotkeyId == nullptr)
{
return GuiAction::Invalid;
}
if (strcmp(hotkeyId, Hotkey::LoadFile) == 0)
{
return GuiAction::LoadFile;
}
if (strcmp(hotkeyId, Hotkey::Exit) == 0)
{
return GuiAction::ExitApplication;
}
if (strcmp(hotkeyId, Hotkey::PauseContinue) == 0)
{
return GuiAction::PauseOrContinueEmulation;
}
if (strcmp(hotkeyId, Hotkey::ToggleDockedMode) == 0)
{
return GuiAction::ToggleDockedMode;
}
if (strcmp(hotkeyId, Hotkey::ToggleSpeedLimit) == 0)
{
return GuiAction::ToggleSpeedLimit;
}
if (strcmp(hotkeyId, Hotkey::StopEmulation) == 0)
{
return GuiAction::StopEmulation;
}
if (strcmp(hotkeyId, Hotkey::Configure) == 0)
{
return GuiAction::OpenSystemConfiguration;
}
if (strcmp(hotkeyId, Hotkey::Controllers) == 0)
{
return GuiAction::OpenControllersDialog;
}
if (strcmp(hotkeyId, Hotkey::HideUi) == 0)
{
return GuiAction::ToggleHideUi;
}
return GuiAction::Invalid;
}
void SciterMainWindow::OnToggleDockedMode()
{
SettingsStore & store = SettingsStore::GetInstance();
const bool docked = store.GetInt(NXOsSetting::DockedMode) == static_cast<int32_t>(DockedMode::Docked);
store.SetInt(NXOsSetting::DockedMode, static_cast<int32_t>(docked ? DockedMode::Handheld : DockedMode::Docked));
}
void SciterMainWindow::OnToggleSpeedLimit()
{
SettingsStore & store = SettingsStore::GetInstance();
store.SetBool(NXOsSetting::UseSpeedLimit, !store.GetBool(NXOsSetting::UseSpeedLimit));
}
void SciterMainWindow::OnToggleStartGamesInFullscreen()
{
if (m_emulationRunning)
{
return;
}
uiSettings.startGamesInFullscreen = !uiSettings.startGamesInFullscreen;
SaveUISetting();
ResetMenu();
}
void SciterMainWindow::OnToggleStartGamesWithUiHidden()
{
if (m_emulationRunning)
{
return;
}
uiSettings.startGamesWithUiHidden = !uiSettings.startGamesWithUiHidden;
SaveUISetting();
ResetMenu();
}
void SciterMainWindow::OnRecetGame(uint32_t fileIndex)
{
Stringlist & recentFiles = uiSettings.recentFiles;
if (m_modules.IsValid() && fileIndex < recentFiles.size())
{
LoadGame(recentFiles[fileIndex].c_str());
}
}
void SciterMainWindow::OnWindowDestroy(HWINDOW /*hWnd*/)
{
m_ProfileSelect.DetachAfterWindowDestroyed();
m_WebBrowser.DetachWindow();
if (m_inputTimerElement.IsValid())
{
m_inputTimerElement.SetTimer(0, (uint32_t *)TIMER_UPDATE_INPUT);
m_inputTimerElement = {};
}
if (m_rootElement.IsValid())
{
m_rootElement.SetTimer(0, (uint32_t *)TIMER_UPDATE_INSTALL_FIRMWARE);
}
m_window = nullptr;
m_rootElement = {};
m_sciterUI.Stop();
}
bool SciterMainWindow::OnWindowCloseRequest(HWINDOW /*hWnd*/)
{
return ConfirmCloseEmulator();
}
void SciterMainWindow::OnMenuItem(int32_t id, SCITER_ELEMENT /*item*/)
{
OnGuiAction(static_cast<GuiAction>(id));
}
void SciterMainWindow::OnGuiAction(GuiAction action)
{
if (action >= GuiAction::RecentFileMenuFirst && action <= GuiAction::RecentFileMenuLast)
{
OnRecetGame(static_cast<uint32_t>(action) - static_cast<uint32_t>(GuiAction::RecentFileMenuFirst));
return;
}
switch (action)
{
case GuiAction::LoadFile:
OnOpenFile();
break;
case GuiAction::InstallFirmwareFromFile:
OnInstallFirmwareFromFile();
break;
case GuiAction::InstallFirmwareFromFolder:
OnInstallFirmwareFromFolder();
break;
case GuiAction::ExitApplication:
OnFileExit();
break;
case GuiAction::PauseOrContinueEmulation:
OnPauseContinueGame();
break;
case GuiAction::StopEmulation:
OnStopGame();
break;
case GuiAction::OpenControllersDialog:
OnInputConfig();
break;
case GuiAction::OpenSystemConfiguration:
OnSystemConfig();
break;
case GuiAction::OpenAboutDialog:
OnAbout();
break;
case GuiAction::OpenDiscord:
OnOpenDiscord();
break;
case GuiAction::OpenReportIssues:
OnOpenReportIssues();
break;
case GuiAction::OpenAppDirectory:
OnOpenAppDirectory();
break;
case GuiAction::OpenLogDirectory:
OnOpenLogDirectory();
break;
case GuiAction::ToggleFullscreen:
#ifdef _WIN32
ToggleFullscreen();
#endif
break;
case GuiAction::ToggleStartGamesInFullscreen:
OnToggleStartGamesInFullscreen();
break;
case GuiAction::ToggleStartGamesWithUiHidden:
OnToggleStartGamesWithUiHidden();
break;
case GuiAction::ToggleHideUi:
ToggleHideUi();
break;
case GuiAction::ToggleDockedMode:
OnToggleDockedMode();
break;
case GuiAction::ToggleSpeedLimit:
OnToggleSpeedLimit();
break;
case GuiAction::ResetWindowSize720p:
#ifdef _WIN32
ResetWindowSize(1280U, 720U);
#endif
break;
case GuiAction::ResetWindowSize900p:
#ifdef _WIN32
ResetWindowSize(1600U, 900U);
#endif
break;
case GuiAction::ResetWindowSize1080p:
#ifdef _WIN32
ResetWindowSize(1920U, 1080U);
#endif
break;
case GuiAction::Invalid:
default:
break;
}
}
void * SciterMainWindow::RenderSurface() const
{
#ifdef __APPLE__
return NxEmuMacOSGetRenderSurface(m_renderWindow);
#else
return m_renderWindow;
#endif
}
void * SciterMainWindow::DisplayConnection() const
{
return nullptr;
}
RenderWindowSystem SciterMainWindow::WindowSystem() const
{
#ifdef _WIN32
return RenderWindowSystem::Windows;
#elif defined(__APPLE__)
return RenderWindowSystem::Cocoa;
#else
return RenderWindowSystem::Default;
#endif
}
float SciterMainWindow::PixelRatio() const
{
#ifdef _WIN32
HWND hwnd = nullptr;
if (m_renderWindow != nullptr)
{
hwnd = (HWND)m_renderWindow;
}
else if (m_window != nullptr)
{
hwnd = (HWND)m_window->GetHandle();
}
typedef UINT(WINAPI * PFN_GetDpiForWindow)(HWND);
static PFN_GetDpiForWindow pGetDpiForWindow = reinterpret_cast<PFN_GetDpiForWindow>(
::GetProcAddress(::GetModuleHandleW(L"user32.dll"), "GetDpiForWindow"));
if (hwnd != nullptr && pGetDpiForWindow != nullptr)
{
UINT dpi = pGetDpiForWindow(hwnd);
if (dpi != 0)
{
return static_cast<float>(dpi) / static_cast<float>(USER_DEFAULT_SCREEN_DPI);
}
}
HDC hdc = ::GetDC(hwnd);
if (hdc != nullptr)
{
int dpi = ::GetDeviceCaps(hdc, LOGPIXELSX);
::ReleaseDC(hwnd, hdc);
if (dpi > 0)
{
return static_cast<float>(dpi) / static_cast<float>(USER_DEFAULT_SCREEN_DPI);
}
}
#endif
#ifdef __APPLE__
return NxEmuMacOSWindowScale(m_window != nullptr ? m_window->GetHandle() : nullptr);
#endif
return 1.0f;
}
bool SciterMainWindow::OnKeyDown(SCITER_ELEMENT /*element*/, SCITER_ELEMENT /*item*/, SciterKeys keyCode, uint32_t keyboardState)
{
const char * hotkeyId = IsMenuBarAccelerator((uint32_t)keyCode, keyboardState);
if (hotkeyId != nullptr)
{
if (strcmp(hotkeyId, Hotkey::ExitFullscreen) == 0 && m_win32Fullscreen->active)
{
m_win32Fullscreen->pendingSwallowKeyUp = (uint32_t)keyCode;
OnGuiAction(GuiAction::ToggleFullscreen);
return true;
}
if (strcmp(hotkeyId, Hotkey::Fullscreen) == 0)
{
const bool allowFullscreen = m_emulationRunning || (m_win32Fullscreen != nullptr && m_win32Fullscreen->active);
if (allowFullscreen)
{
m_win32Fullscreen->pendingSwallowKeyUp = (uint32_t)keyCode;
OnGuiAction(GuiAction::ToggleFullscreen);
return true;
}
}
if (strcmp(hotkeyId, Hotkey::HideUi) == 0)
{
m_win32Fullscreen->pendingSwallowKeyUp = (uint32_t)keyCode;
OnGuiAction(GuiAction::ToggleHideUi);
return true;
}
const GuiAction fromKey = HotkeyToGuiAction(hotkeyId);
if (fromKey != GuiAction::Invalid)
{
OnGuiAction(fromKey);
return true;
}
}
if (m_modules.IsValid())
{
IOperatingSystem & operatingSystem = m_modules.Modules().OperatingSystem();
int keyIndex = SciterKeyToSwitchKey(keyCode);
if (keyIndex != 0)
{
operatingSystem.KeyboardKeyPress(0, keyIndex, SciterKeyToVKCode(keyCode));
}
}
return false;
}
bool SciterMainWindow::OnKeyUp(SCITER_ELEMENT /*element*/, SCITER_ELEMENT /*item*/, SciterKeys keyCode, uint32_t keyboardState)
{
if (m_win32Fullscreen != nullptr && m_win32Fullscreen->pendingSwallowKeyUp != 0 && (uint32_t)keyCode == m_win32Fullscreen->pendingSwallowKeyUp)
{
m_win32Fullscreen->pendingSwallowKeyUp = 0;
return true;
}
if (IsMenuBarAccelerator((uint32_t)keyCode, keyboardState) != nullptr)
{
return true;
}
if (m_modules.IsValid())
{
IOperatingSystem & operatingSystem = m_modules.Modules().OperatingSystem();
int keyIndex = SciterKeyToSwitchKey(keyCode);
if (keyIndex != 0)
{
operatingSystem.KeyboardKeyRelease(0, keyIndex, SciterKeyToVKCode(keyCode));
}
}
return false;
}
bool SciterMainWindow::OnKeyChar(SCITER_ELEMENT /*element*/, SCITER_ELEMENT /*item*/, SciterKeys /*keyCode*/, uint32_t /*keyboardState*/)
{
return false;
}
bool SciterMainWindow::OnSizeChanged(SCITER_ELEMENT elem)
{
SciterElement rootElement(m_window->GetRootElement());
if (elem == rootElement.GetElementByID("MainContents"))
{
LayoutRenderWindow();
}
return false;
}
void SciterMainWindow::LayoutRenderWindow()
{
if (m_renderWindow == nullptr)
{
return;
}
SciterElement mainContents(m_rootElement.GetElementByID("MainContents"));
if (!mainContents.IsValid())
{
return;
}
SciterElement::RECT rect = mainContents.GetLocation(SciterElement::ROOT_RELATIVE | SciterElement::BORDER_BOX);
const bool uiHidden = m_hideUi || (m_win32Fullscreen != nullptr && m_win32Fullscreen->active);
SciterElement mainMenu(m_rootElement.GetElementByID("MainMenu"));
if (!uiHidden && mainMenu.IsValid())
{
const SciterElement::RECT menuRect = mainMenu.GetLocation(SciterElement::ROOT_RELATIVE | SciterElement::BORDER_BOX);
if (rect.top < menuRect.bottom)
{
rect.top = menuRect.bottom;
}
}
if (rect.right <= rect.left || rect.bottom <= rect.top)
{
return;
}
uint32_t width = rect.right - rect.left;
uint32_t height = rect.bottom - rect.top;
#ifdef _WIN32
MoveWindow((HWND)m_renderWindow, rect.left, rect.top, width, height, false);
#elif defined(__APPLE__)
NxEmuMacOSLayoutRenderView(m_renderWindow, rect.left, rect.top, width, height);
#else
(void)width;
(void)height;
#endif
if (m_modules.IsValid())
{
IVideo & video = m_modules.Modules().Video();
video.UpdateFramebufferLayout(width, height);
}
}
void SciterMainWindow::UpdateLoadingScreenDetails()
{
IRomInfoPtr info(m_modules.Modules().Systemloader().LoadedRomInfo());
if (!info)
{
return;
}
std::vector<uint8_t> logoData, bannerData, iconData;
uint32_t sz = 0;
if (info->ReadLogo(nullptr, &sz) == LoaderResultStatus::Success && sz > 0)
{
logoData.resize(sz);
if (info->ReadLogo(logoData.data(), &sz) != LoaderResultStatus::Success)
{
logoData.clear();
}
}
sz = 0;
if (info->ReadBanner(nullptr, &sz) == LoaderResultStatus::Success && sz > 0)
{
bannerData.resize(sz);
if (info->ReadBanner(bannerData.data(), &sz) != LoaderResultStatus::Success)
{
bannerData.clear();
}
}
sz = 0;
if (info->ReadIcon(nullptr, &sz) == LoaderResultStatus::Success && sz > 0)
{
iconData.resize(sz);
if (info->ReadIcon(iconData.data(), &sz) != LoaderResultStatus::Success)
{
iconData.clear();
}
}
LoadImageToElement(m_rootElement.GetElementByID("LoadingCornerLogo"), logoData);
LoadImageToElement(m_rootElement.GetElementByID("LoadingCornerBanner"), bannerData);
LoadImageToElement(m_rootElement.GetElementByID("LoadingGameIcon"), iconData);
SciterElement loadingMain(m_rootElement.FindFirst("#LoadingPanel .loading-main"));
if (loadingMain.IsValid())
{
loadingMain.RemoveClassName("no-game-icon");
}
}
void SciterMainWindow::UpdateUIVisibility()
{
const bool hide = m_hideUi || (m_win32Fullscreen != nullptr && m_win32Fullscreen->active);
if (m_rootElement.IsValid())
{
if (hide)
{
m_rootElement.AddClassName("nx-ui-hidden");
}
else
{
m_rootElement.RemoveClassName("nx-ui-hidden");
}
}
std::array<SciterElement, 3> shellPanels = {{
m_rootElement.FindFirst("header"),
m_rootElement.GetElementByID("MainMenu"),
m_rootElement.GetElementByID("StatusBar"),
}};
for (SciterElement & panel : shellPanels)
{
if (!panel.IsValid())
{
continue;
}
if (hide)
{
panel.AddClassName("nx-fullscreen-hide");
}
else
{
panel.RemoveClassName("nx-fullscreen-hide");
}
}
}
void SciterMainWindow::ToggleHideUi()
{
if (!m_emulationRunning)
{
return;
}
m_hideUi = !m_hideUi;
UpdateUIVisibility();
m_sciterUI.UpdateWindow(m_rootElement.GetElementHwnd(true));
SciterElement mainContents(m_rootElement.GetElementByID("MainContents"));
if (mainContents.IsValid())
{
mainContents.Update(true);
}
LayoutRenderWindow();
ResetMenu();
}
#ifdef _WIN32
void SciterMainWindow::ToggleFullscreen()
{
if (!m_win32Fullscreen)
{
return;
}
if (m_win32Fullscreen->active)
{
ExitFullscreen();
}
else if (m_emulationRunning)
{
EnterFullscreen();
}
}
void SciterMainWindow::EnterFullscreen()
{
if (m_window == nullptr || !m_win32Fullscreen || m_win32Fullscreen->active || !m_emulationRunning)
{
return;
}
HWND hwnd = (HWND)m_window->GetHandle();
Win32FullscreenState & fs = *m_win32Fullscreen;
fs.placement.length = sizeof(WINDOWPLACEMENT);
if (!GetWindowPlacement(hwnd, &fs.placement))
{
return;
}
fs.savedStyle = GetWindowLongPtr(hwnd, GWL_STYLE);
fs.savedExStyle = GetWindowLongPtr(hwnd, GWL_EXSTYLE);
HMONITOR hMon = MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST);
MONITORINFO mi = {sizeof(MONITORINFO)};
if (!GetMonitorInfo(hMon, &mi))
{
return;
}
const int w = mi.rcMonitor.right - mi.rcMonitor.left;
const int h = mi.rcMonitor.bottom - mi.rcMonitor.top;
LONG_PTR style = fs.savedStyle;
style &= ~(WS_CAPTION | WS_THICKFRAME | WS_SYSMENU | WS_MINIMIZEBOX | WS_MAXIMIZEBOX | WS_BORDER);
SetWindowLongPtr(hwnd, GWL_STYLE, style);
SetWindowPos(hwnd, HWND_TOP, mi.rcMonitor.left, mi.rcMonitor.top, w, h, SWP_FRAMECHANGED | SWP_SHOWWINDOW);
fs.active = true;
UpdateUIVisibility();
m_sciterUI.UpdateWindow(m_rootElement.GetElementHwnd(true));
SciterElement mainContents(m_rootElement.GetElementByID("MainContents"));
if (mainContents.IsValid())
{
mainContents.Update(true);
}
LayoutRenderWindow();
}
void SciterMainWindow::ExitFullscreen()
{
if (m_window == nullptr || !m_win32Fullscreen || !m_win32Fullscreen->active)
{
return;
}
HWND hwnd = (HWND)m_window->GetHandle();
Win32FullscreenState & fs = *m_win32Fullscreen;
SetWindowLongPtr(hwnd, GWL_STYLE, fs.savedStyle);
SetWindowLongPtr(hwnd, GWL_EXSTYLE, fs.savedExStyle);
SetWindowPlacement(hwnd, &fs.placement);
fs.active = false;
UpdateUIVisibility();
m_sciterUI.UpdateWindow(m_rootElement.GetElementHwnd(true));
SciterElement mainContents(m_rootElement.GetElementByID("MainContents"));
if (mainContents.IsValid())
{
mainContents.Update(true);
}
LayoutRenderWindow();
}
void SciterMainWindow::ResetWindowSize(uint32_t nominal_width, uint32_t nominal_height)
{
if (m_window == nullptr || nominal_width == 0 || nominal_height == 0)
{
return;
}
if (m_win32Fullscreen && m_win32Fullscreen->active)
{
ExitFullscreen();
}
HWND hwnd = (HWND)m_window->GetHandle();
SciterElement mainContents(m_rootElement.GetElementByID("MainContents"));
if (!mainContents.IsValid())
{
return;
}
SciterElement::RECT contentRect = mainContents.GetLocation();
const int32_t curW = contentRect.right - contentRect.left;
const int32_t curH = contentRect.bottom - contentRect.top;
if (curW <= 0 || curH <= 0)
{
return;
}
const float window_aspect_ratio = (float)nominal_height / (float)nominal_width;
float emulation_aspect_ratio = 720.0f / 1280.0f;
switch ((AspectRatio)SettingsStore::GetInstance().GetInt(NXVideoSetting::AspectRatio))
{
case AspectRatio::R16_9:
emulation_aspect_ratio = 720.0f / 1280.0f;
break;
case AspectRatio::R4_3:
emulation_aspect_ratio = 3.0f / 4.0f;
break;
case AspectRatio::R21_9:
emulation_aspect_ratio = 9.0f / 21.0f;
break;
case AspectRatio::R16_10:
emulation_aspect_ratio = 10.0f / 16.0f;
break;
case AspectRatio::Stretch:
emulation_aspect_ratio = window_aspect_ratio;
break;
default:
break;
}
const uint32_t targetH = nominal_height;
const uint32_t targetW = (uint32_t)(std::lround((double)nominal_height / (double)emulation_aspect_ratio));
RECT wr{};
if (!GetWindowRect(hwnd, &wr))
{
return;
}
const int outerW = wr.right - wr.left;
const int outerH = wr.bottom - wr.top;
const int deltaW = static_cast<int>(targetW) - curW;
const int deltaH = static_cast<int>(targetH) - curH;
SetWindowPos(hwnd, nullptr, 0, 0, outerW + deltaW, outerH + deltaH, SWP_NOMOVE | SWP_NOZORDER);
m_sciterUI.UpdateWindow(m_rootElement.GetElementHwnd(true));
SciterElement mc(m_rootElement.GetElementByID("MainContents"));
if (mc.IsValid())
{
mc.Update(true);
}
LayoutRenderWindow();
}
#endif
bool SciterMainWindow::OnClick(SCITER_ELEMENT element, SCITER_ELEMENT source, uint32_t /*reason*/)
{
SciterElement rootElement(m_window->GetRootElement());
if (source == rootElement.GetElementByID("dockedMode"))
{
OnToggleDockedMode();
}
else if (element == rootElement.GetElementByID("renderer"))
{
#ifdef _WIN32
SettingsStore & settings = SettingsStore::GetInstance();
RendererBackend graphicsAPI = (RendererBackend)settings.GetInt(NXVideoSetting::GraphicsAPI);
if (graphicsAPI == RendererBackend::Vulkan)
{
graphicsAPI = RendererBackend::OpenGL;
}
else if (graphicsAPI == RendererBackend::OpenGL)
{
graphicsAPI = RendererBackend::Vulkan;
}
settings.SetInt(NXVideoSetting::GraphicsAPI, (int32_t)graphicsAPI);
stdstr_f text("%s", RendererBackendLabel((RendererBackend)settings.GetInt(NXVideoSetting::GraphicsAPI)));
SciterElement(source).SetHTML((const uint8_t *)text.c_str(), text.size());
if (m_modules.IsValid())
{
m_modules.FlushSettings();
}
#endif
}
else if (source == rootElement.GetElementByID("gpuCommandSynchronization"))
{
SettingsStore & settings = SettingsStore::GetInstance();
const bool enabled = settings.GetBool(NXVideoSetting::GpuCommandSynchronization);
settings.SetBool(NXVideoSetting::GpuCommandSynchronization, !enabled);
UpdateStatusWidgets();
}
else if (element == rootElement.GetElementByID("volume") || element == rootElement.GetElementByID("volumeIcon"))
{
SettingsStore & settings = SettingsStore::GetInstance();
settings.SetBool(NXOsSetting::AudioMuted, !settings.GetBool(NXOsSetting::AudioMuted));
}
return true;
}
void SciterMainWindow::SettingChanged(const char * setting, void * userData)
{
SciterMainWindow * impl = (SciterMainWindow *)userData;
if (strcmp(setting, NXOsSetting::AudioMuted) == 0 || strcmp(setting, NXOsSetting::AudioVolume) == 0)
{
impl->UpdateStatusWidgets();
}
else if (strcmp(setting, NXVideoSetting::ResolutionUpFactor) == 0)
{
impl->m_resolutionUpFactor = SettingsStore::GetInstance().GetFloat(NXVideoSetting::ResolutionUpFactor);
}
else if (strcmp(setting, NXVideoSetting::GpuCommandSynchronization) == 0)
{
impl->UpdateStatusWidgets();
}
else if (strcmp(setting, NXOsSetting::UseMultiCore) == 0)
{
impl->m_useMultiCore = SettingsStore::GetInstance().GetBool(NXOsSetting::UseMultiCore);
}
else if (strcmp(setting, NXOsSetting::UseSpeedLimit) == 0)
{
impl->m_useSpeedLimit = SettingsStore::GetInstance().GetBool(NXOsSetting::UseSpeedLimit);
if (impl->m_emulationRunning)
{
impl->ResetMenu();
}
}
else if (strcmp(setting, NXOsSetting::SpeedLimit) == 0)
{
impl->m_speedLimit = SettingsStore::GetInstance().GetInt(NXOsSetting::SpeedLimit);
}
else if (strcmp(setting, NXOsSetting::DockedMode) == 0)
{
impl->UpdateStatusWidgets();
impl->LayoutRenderWindow();
}
else if (strcmp(setting, NXUISetting::HideMouseOnInactivity) == 0)
{
if (!uiSettings.hideMouseOnInactivity)
{
impl->ResetMouseCursorHiding();
}
else
{
impl->m_lastMouseActivityTick = 0;
}
}
else if (strcmp(setting, NXUISetting::EnableDiscordPresence) == 0)
{
const bool enabled = SettingsStore::GetInstance().GetBool(NXUISetting::EnableDiscordPresence);
impl->m_discordPresence.SetEnabled(enabled);
if (enabled)
{
impl->UpdateDiscordPresence();
}
}
}
bool SciterMainWindow::OnTimer(SCITER_ELEMENT /*element*/, uint32_t * timerId)
{
// The input timer keeps running while loading, paused and stopped. Drain
// notifications in order without holding the queue lock during UI calls.
std::deque<int32_t> state_updates;
{
std::scoped_lock lock{m_emulationStateMutex};
state_updates.swap(m_emulationStateUpdates);
}
for (const auto state : state_updates)
{
OnEmulationStateChanged(state);
}
if (timerId == (uint32_t *)TIMER_UPDATE_UI)
{
SciterElement mainContents(m_rootElement.GetElementByID("MainContents"));
if (mainContents.IsValid())
{
mainContents.Update(false);
m_sciterUI.UpdateWindow(mainContents.GetElementHwnd(true));
}
return false;
}
else if (timerId == (uint32_t *)TIMER_UPDATE_INPUT)
{
UpdateInputDrivers();
}
else if (timerId == (uint32_t *)TIMER_UPDATE_STATUS)
{
UpdateEmulationStatusText();
}
else if (timerId == (uint32_t *)TIMER_UPDATE_INSTALL_FIRMWARE)
{
if (m_firmwareInstallUiActive)
{
const int32_t total = SettingsStore::GetInstance().GetInt(NXLoaderSetting::FirmwareInstallTotal);
if (total <= 0 && !m_firmwareInstallInProgress)
{
StopFirmwareInstallUi();
}
else
{
RefreshFirmwareInstallLoading();
}
}
}
else if (timerId == (uint32_t *)TIMER_OPEN_GAME_CONFIG)
{
const std::string path = m_pendingGameConfigPath;
m_pendingGameConfigPath.clear();
if (!path.empty())
{
m_gameConfig.reset(new GameConfig(m_sciterUI, m_modules));
m_gameConfig->Display((void *)m_window->GetHandle(), path.c_str());
}
return false;
}
else if (timerId == (uint32_t *)TIMER_DEFERRED_STOP_GAME)
{
DoStopGame();
return false;
}
else if (timerId == (uint32_t *)TIMER_DEFERRED_FILE_EXIT)
{
DoFileExit();
return false;
}
return true;
}
bool SciterMainWindow::OnEvent(SCITER_ELEMENT element, SCITER_ELEMENT source, uint32_t event_code, uint64_t reason)
{
if (event_code == static_cast<uint32_t>(SciterBehaviorEvent::ValueChanged))
{
SciterElement slider(m_rootElement.GetElementByID("audioVolume"));
if (slider == element || slider == source)
{
SnapVolumeSliderToNormal(slider, reason);
const SciterValue value = slider.GetValue();
if (value.isInt())
{
SettingsStore & settings = SettingsStore::GetInstance();
settings.SetBool(NXOsSetting::AudioMuted, false);
settings.SetInt(NXOsSetting::AudioVolume, value.GetValueInt());
if (m_modules.IsValid())
{
m_modules.FlushSettings();
}
}
return true;
}
}
if (event_code == EVENT_EMULATION_LOADING)
{
ShowPanel(Panel::Loading);
}
else if (event_code == EVENT_EMULATION_RUNNING)
{
PreventOSSleep();
m_rootElement.SetTimer(500, (uint32_t *)TIMER_UPDATE_STATUS);
}
else if (event_code == EVENT_EMULATION_STOPPED)
{
m_shownFirstFrame = false;
ResetMouseCursorHiding();
ShowPanel(Panel::RomBrowser);
}
else if (event_code == EVENT_EXECUTE_PROGRAM)
{
OnExecuteProgram(reason);
}
else if (event_code == EVENT_RELOAD_PROGRAM)
{
OnReloadProgram();
}
else if (event_code == EVENT_EXIT_PROGRAM)
{
OnExitProgram();
}
else if (event_code == EVENT_EMULATION_FIRST_FRAME)
{
SettingsStore & settings = SettingsStore::GetInstance();
if (settings.GetBool(NXCoreSetting::DisplayedFrames))
{
ShowPanel(Panel::Renderer);
}
}
else if (event_code == EVENT_DISK_CACHE_STATUS)
{
RefreshDiskCacheLoadingText();
}
else if (event_code == EVENT_FIRMWARE_INSTALL_ACTIVE)
{
StartFirmwareInstallUi();
}
else if (event_code == EVENT_FIRMWARE_INSTALL_DONE)
{
FinishFirmwareInstall();
}
return false;
}