// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef __APPLE__ #include // for chdir #endif #ifdef _WIN32 #include #include #else #include #endif #ifdef __unix__ #include #include #include #include "common/linux/gamemode.h" #endif #include "citra_meta/common_strings.h" #include "citra_qt/aboutdialog.h" #include "citra_qt/applets/mii_selector.h" #include "citra_qt/applets/swkbd.h" #include "citra_qt/bootmanager.h" #include "citra_qt/camera/qt_multimedia_camera.h" #include "citra_qt/camera/still_image_camera.h" #include "citra_qt/citra_qt.h" #include "citra_qt/compatibility_list.h" #include "citra_qt/configuration/config.h" #include "citra_qt/configuration/configure_dialog.h" #include "citra_qt/configuration/configure_per_game.h" #include "citra_qt/debugger/console.h" #include "citra_qt/debugger/graphics/graphics.h" #include "citra_qt/debugger/graphics/graphics_breakpoints.h" #include "citra_qt/debugger/graphics/graphics_cmdlists.h" #include "citra_qt/debugger/graphics/graphics_surface.h" #include "citra_qt/debugger/graphics/graphics_tracing.h" #include "citra_qt/debugger/graphics/graphics_vertex_shader.h" #include "citra_qt/debugger/ipc/recorder.h" #include "citra_qt/debugger/lle_service_modules.h" #include "citra_qt/debugger/profiler.h" #include "citra_qt/debugger/registers.h" #include "citra_qt/debugger/wait_tree.h" #include "citra_qt/discord.h" #include "citra_qt/dumping/dumping_dialog.h" #include "citra_qt/game_list.h" #include "citra_qt/hotkeys.h" #include "citra_qt/loading_screen.h" #include "citra_qt/movie/movie_play_dialog.h" #include "citra_qt/movie/movie_record_dialog.h" #include "citra_qt/multiplayer/state.h" #include "citra_qt/play_time_manager.h" #include "citra_qt/qt_image_interface.h" #include "citra_qt/uisettings.h" #include "citra_qt/util/clickable_label.h" #include "citra_qt/util/graphics_device_info.h" #include "citra_qt/util/util.h" #include "common/arch.h" #include "common/common_paths.h" #include "common/detached_tasks.h" #include "common/dynamic_library/dynamic_library.h" #include "common/file_util.h" #include "common/literals.h" #include "common/logging/backend.h" #include "common/logging/log.h" #include "common/memory_detect.h" #include "common/scm_rev.h" #include "common/scope_exit.h" #if CITRA_ARCH(x86_64) #include "common/x64/cpu_detect.h" #endif #include "common/settings.h" #include "common/string_util.h" #include "core/core.h" #include "core/dumping/backend.h" #include "core/file_sys/archive_extsavedata.h" #include "core/file_sys/archive_source_sd_savedata.h" #include "core/frontend/applets/default_applets.h" #include "core/hle/service/am/am.h" #include "core/hle/service/fs/archive.h" #include "core/hle/service/nfc/nfc.h" #include "core/loader/loader.h" #include "core/movie.h" #include "core/savestate.h" #include "core/system_titles.h" #include "input_common/main.h" #include "network/network_settings.h" #include "ui_main.h" #include "video_core/gpu.h" #include "video_core/renderer_base.h" #ifdef __APPLE__ #include "common/apple_authorization.h" #endif #ifdef USE_DISCORD_PRESENCE #include "citra_qt/discord_impl.h" #endif #ifdef QT_STATICPLUGIN Q_IMPORT_PLUGIN(QWindowsIntegrationPlugin); #endif #ifdef HAVE_SDL2 #include #endif constexpr int default_mouse_timeout = 2500; /** * "Callouts" are one-time instructional messages shown to the user. In the config settings, there * is a bitfield "callout_flags" options, used to track if a message has already been shown to the * user. This is 32-bits - if we have more than 32 callouts, we should retire and recycle old ones. */ const int GMainWindow::max_recent_files_item; static QString PrettyProductName() { #ifdef _WIN32 // After Windows 10 Version 2004, Microsoft decided to switch to a different notation: 20H2 // With that notation change they changed the registry key used to denote the current version QSettings windows_registry( QStringLiteral("HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion"), QSettings::NativeFormat); const QString release_id = windows_registry.value(QStringLiteral("ReleaseId")).toString(); if (release_id == QStringLiteral("2009")) { const u32 current_build = windows_registry.value(QStringLiteral("CurrentBuild")).toUInt(); const QString display_version = windows_registry.value(QStringLiteral("DisplayVersion")).toString(); const u32 ubr = windows_registry.value(QStringLiteral("UBR")).toUInt(); const u32 version = current_build >= 22000 ? 11 : 10; return QStringLiteral("Windows %1 Version %2 (Build %3.%4)") .arg(QString::number(version), display_version, QString::number(current_build), QString::number(ubr)); } #endif return QSysInfo::prettyProductName(); } void GMainWindow::ShowCommandOutput(std::string title, std::string message) { #ifdef _WIN32 boost::replace_all(message, " ", "\u00a0"); // Non-breaking space boost::replace_all(message, "-", "\u2011"); // Non-breaking hyphen QMessageBox::information(this, QString::fromStdString(title), QString::fromStdString(message)); #else std::cout << message << std::endl; #endif } GMainWindow::GMainWindow(Core::System& system_) : ui{std::make_unique()}, system{system_}, movie{system.Movie()}, user_data_migrator{this}, config{std::make_unique()}, emu_thread{nullptr} { Common::Log::Initialize(); Common::Log::Start(); Debugger::ToggleConsole(); QStringList args = QApplication::arguments(); QString game_path; std::optional fullscreen_override; for (int i = 1; i < args.size(); ++i) { // Preserves drag/drop functionality if (args.size() == 2 && !args[1].startsWith(QChar::fromLatin1('-'))) { game_path = args[1]; break; } // Dump video if (args[i] == QStringLiteral("--dump-video") || args[i] == QStringLiteral("-d")) { if (i >= args.size() - 1 || args[i + 1].startsWith(QChar::fromLatin1('-'))) { continue; } if (!DynamicLibrary::FFmpeg::LoadFFmpeg()) { ShowFFmpegErrorMessage(); continue; } video_dumping_path = args[++i]; video_dumping_on_start = true; continue; } // Launch game in fullscreen mode if (args[i] == QStringLiteral("--fullscreen") || args[i] == QStringLiteral("-f")) { fullscreen_override = true; continue; } // Enable GDB stub if (args[i] == QStringLiteral("--gdbport") || args[i] == QStringLiteral("-g")) { if (i >= args.size() - 1 || args[i + 1].startsWith(QChar::fromLatin1('-'))) { continue; } Settings::values.use_gdbstub = true; Settings::values.gdbstub_port = strtoul(args[++i].toLatin1(), NULL, 0); continue; } if (args[i] == QStringLiteral("--help") || args[i] == QStringLiteral("-h")) { ShowCommandOutput("Help", fmt::format(Common::help_string, args[0].toStdString())); exit(0); } if (args[i] == QStringLiteral("--install") || args[i] == QStringLiteral("-i")) { if (i >= args.size() - 1 || args[i + 1].startsWith(QChar::fromLatin1('-'))) { continue; } Service::AM::InstallStatus result = Service::AM::InstallCIA(args[++i].toStdString()); if (result != Service::AM::InstallStatus::Success) { std::string failure_reason; if (result == Service::AM::InstallStatus::ErrorFailedToOpenFile) failure_reason = "Unable to open file."; if (result == Service::AM::InstallStatus::ErrorFileNotFound) failure_reason = "File not found."; if (result == Service::AM::InstallStatus::ErrorAborted) failure_reason = "Install was aborted."; if (result == Service::AM::InstallStatus::ErrorInvalid) failure_reason = "CIA is invalid."; if (result == Service::AM::InstallStatus::ErrorEncrypted) failure_reason = "CIA is encrypted."; std::string failure_string = "Failed to install CIA: " + failure_reason; ShowCommandOutput("Failure", failure_string); exit((int)result + 2); // 2 is added here to avoid stepping on the toes of // exit codes 1 and 2 which have pre-established conventional meanings } ShowCommandOutput("Success", "Installed CIA successfully."); exit(0); } if (args[i] == QStringLiteral("--movie-play") || args[i] == QStringLiteral("-p")) { if (i >= args.size() - 1 || args[i + 1].startsWith(QChar::fromLatin1('-'))) { continue; } movie_playback_path = args[++i]; movie_playback_on_start = true; continue; } if (args[i] == QStringLiteral("--movie-record") || args[i] == QStringLiteral("-r")) { if (i >= args.size() - 1 || args[i + 1].startsWith(QChar::fromLatin1('-'))) { continue; } movie_record_path = args[++i]; movie_record_on_start = true; continue; } if (args[i] == QStringLiteral("--movie-record-author") || args[i] == QStringLiteral("-a")) { if (i >= args.size() - 1 || args[i + 1].startsWith(QChar::fromLatin1('-'))) { continue; } movie_record_author = args[++i]; continue; } if (args[i] == QStringLiteral("--multiplayer") || args[i] == QStringLiteral("-m")) { std::cout << "Warning: The --multiplayer option is not yet implemented for the Qt " "frontend; Ignoring." << std::endl; if (i < args.size() - 1 && !args[i + 1].startsWith(QChar::fromLatin1('-'))) { i++; } continue; } if (args[i] == QStringLiteral("--version") || args[i] == QStringLiteral("-v")) { const std::string version_string = std::string("Azahar ") + Common::g_scm_branch + " " + Common::g_scm_desc; ShowCommandOutput("Version", version_string); exit(0); } // Launch game in windowed mode if (args[i] == QStringLiteral("--windowed") || args[i] == QStringLiteral("-w")) { fullscreen_override = false; continue; } // Launch game at path if (i == args.size() - 1 && !args[i].startsWith(QChar::fromLatin1('-'))) { game_path = args[i]; continue; } } #ifdef __unix__ SetGamemodeEnabled(Settings::values.enable_gamemode.GetValue()); #endif // register types to use in slots and signals qRegisterMetaType("std::size_t"); qRegisterMetaType("Service::AM::InstallStatus"); // Register CameraFactory qt_cameras = std::make_shared(); Camera::RegisterFactory("image", std::make_unique()); Camera::RegisterFactory("qt", std::make_unique(qt_cameras)); LoadTranslation(); Pica::g_debug_context = Pica::DebugContext::Construct(); setAcceptDrops(true); ui->setupUi(this); statusBar()->hide(); default_theme_paths = QIcon::themeSearchPaths(); UpdateUITheme(); SetDiscordEnabled(UISettings::values.enable_discord_presence.GetValue()); discord_rpc->Update(); play_time_manager = std::make_unique(); Network::Init(); movie.SetPlaybackCompletionCallback([this] { QMetaObject::invokeMethod(this, "OnMoviePlaybackCompleted", Qt::BlockingQueuedConnection); }); InitializeWidgets(); InitializeDebugWidgets(); InitializeRecentFileMenuActions(); InitializeSaveStateMenuActions(); InitializeHotkeys(); SetDefaultUIGeometry(); RestoreUIState(); ui->action_Dump_Video->setChecked(video_dumping_on_start); if (fullscreen_override) { ui->action_Fullscreen->setChecked(*fullscreen_override); } ui->action_Close_Movie->setEnabled(movie_playback_on_start || movie_record_on_start); ConnectAppEvents(); ConnectMenuEvents(); ConnectWidgetEvents(); LOG_INFO(Frontend, "Azahar Version: {} | {}-{}", Common::g_build_fullname, Common::g_scm_branch, Common::g_scm_desc); #if CITRA_ARCH(x86_64) const auto& caps = Common::GetCPUCaps(); std::string cpu_string = caps.cpu_string; if (caps.avx || caps.avx2 || caps.avx512) { cpu_string += " | AVX"; if (caps.avx512) { cpu_string += "512"; } else if (caps.avx2) { cpu_string += '2'; } if (caps.fma || caps.fma4) { cpu_string += " | FMA"; } } LOG_INFO(Frontend, "Host CPU: {}", cpu_string); #endif LOG_INFO(Frontend, "Host OS: {}", PrettyProductName().toStdString()); const auto& mem_info = Common::GetMemInfo(); using namespace Common::Literals; LOG_INFO(Frontend, "Host RAM: {:.2f} GiB", mem_info.total_physical_memory / f64{1_GiB}); LOG_INFO(Frontend, "Host Swap: {:.2f} GiB", mem_info.total_swap_memory / f64{1_GiB}); UpdateWindowTitle(); show(); game_list->LoadCompatibilityList(); game_list->PopulateAsync(UISettings::values.game_dirs); mouse_hide_timer.setInterval(default_mouse_timeout); connect(&mouse_hide_timer, &QTimer::timeout, this, &GMainWindow::HideMouseCursor); connect(ui->menubar, &QMenuBar::hovered, this, &GMainWindow::OnMouseActivity); #ifdef ENABLE_OPENGL gl_renderer = GetOpenGLRenderer(); #if defined(_WIN32) if (gl_renderer.startsWith(QStringLiteral("D3D12"))) { // OpenGLOn12 supports but does not yet advertise OpenGL 4.0+ // We can override the version here to allow Citra to work. // TODO: Remove this when OpenGL 4.0+ is advertised. qputenv("MESA_GL_VERSION_OVERRIDE", "4.6"); } #endif #endif #ifdef ENABLE_VULKAN physical_devices = GetVulkanPhysicalDevices(); if (physical_devices.empty()) { QMessageBox::warning(this, tr("No Suitable Vulkan Devices Detected"), tr("Vulkan initialization failed during boot.
" "Your GPU may not support Vulkan 1.1, or you do not " "have the latest graphics driver.")); } #endif if (!game_path.isEmpty()) { BootGame(game_path); } } GMainWindow::~GMainWindow() { // Will get automatically deleted otherwise if (!render_window->parent()) { delete render_window; } Pica::g_debug_context.reset(); Network::Shutdown(); } void GMainWindow::InitializeWidgets() { render_window = new GRenderWindow(this, emu_thread.get(), system, false); secondary_window = new GRenderWindow(this, emu_thread.get(), system, true); render_window->hide(); secondary_window->hide(); secondary_window->setParent(nullptr); action_secondary_fullscreen = new QAction(secondary_window); action_secondary_toggle_screen = new QAction(secondary_window); action_secondary_swap_screen = new QAction(secondary_window); action_secondary_rotate_screen = new QAction(secondary_window); game_list = new GameList(*play_time_manager, this); ui->horizontalLayout->addWidget(game_list); game_list_placeholder = new GameListPlaceholder(this); ui->horizontalLayout->addWidget(game_list_placeholder); game_list_placeholder->setVisible(false); loading_screen = new LoadingScreen(this); loading_screen->hide(); ui->horizontalLayout->addWidget(loading_screen); connect(loading_screen, &LoadingScreen::Hidden, this, [&] { loading_screen->Clear(); if (emulation_running) { render_window->show(); render_window->setFocus(); render_window->activateWindow(); } }); InputCommon::Init(); multiplayer_state = new MultiplayerState(system, this, game_list->GetModel(), ui->action_Leave_Room, ui->action_Show_Room); multiplayer_state->setVisible(false); UpdateBootHomeMenuState(); // Create status bar message_label = new QLabel(); // Configured separately for left alignment message_label->setFrameStyle(QFrame::NoFrame); message_label->setContentsMargins(4, 0, 4, 0); message_label->setAlignment(Qt::AlignLeft); statusBar()->addPermanentWidget(message_label, 1); progress_bar = new QProgressBar(); progress_bar->hide(); statusBar()->addPermanentWidget(progress_bar); artic_traffic_label = new QLabel(); artic_traffic_label->setToolTip( tr("Current Artic traffic speed. Higher values indicate bigger transfer loads.")); emu_speed_label = new QLabel(); emu_speed_label->setToolTip(tr("Current emulation speed. Values higher or lower than 100% " "indicate emulation is running faster or slower than a 3DS.")); game_fps_label = new QLabel(); game_fps_label->setToolTip(tr("How many frames per second the app is currently displaying. " "This will vary from app to app and scene to scene.")); emu_frametime_label = new QLabel(); emu_frametime_label->setToolTip( tr("Time taken to emulate a 3DS frame, not counting framelimiting or v-sync. For " "full-speed emulation this should be at most 16.67 ms.")); for (auto& label : {artic_traffic_label, emu_speed_label, game_fps_label, emu_frametime_label}) { label->setVisible(false); label->setFrameStyle(QFrame::NoFrame); label->setContentsMargins(4, 0, 4, 0); statusBar()->addPermanentWidget(label); } // Setup Graphics API button graphics_api_button = new QPushButton(); graphics_api_button->setObjectName(QStringLiteral("GraphicsAPIStatusBarButton")); graphics_api_button->setFocusPolicy(Qt::NoFocus); UpdateAPIIndicator(); connect(graphics_api_button, &QPushButton::clicked, this, [this] { UpdateAPIIndicator(true); }); statusBar()->insertPermanentWidget(0, graphics_api_button); volume_popup = new QWidget(this); volume_popup->setWindowFlags(Qt::FramelessWindowHint | Qt::NoDropShadowWindowHint | Qt::Popup); volume_popup->setLayout(new QVBoxLayout()); volume_popup->setMinimumWidth(200); volume_slider = new QSlider(Qt::Horizontal); volume_slider->setObjectName(QStringLiteral("volume_slider")); volume_slider->setMaximum(100); volume_slider->setPageStep(5); connect(volume_slider, &QSlider::valueChanged, this, [this](int percentage) { Settings::values.audio_muted = false; const auto value = static_cast(percentage) / volume_slider->maximum(); Settings::values.volume.SetValue(value); UpdateVolumeUI(); }); volume_popup->layout()->addWidget(volume_slider); volume_button = new QPushButton(); volume_button->setObjectName(QStringLiteral("TogglableStatusBarButton")); volume_button->setFocusPolicy(Qt::NoFocus); volume_button->setCheckable(true); UpdateVolumeUI(); connect(volume_button, &QPushButton::clicked, this, [&] { UpdateVolumeUI(); volume_popup->setVisible(!volume_popup->isVisible()); QRect rect = volume_button->geometry(); QPoint bottomLeft = statusBar()->mapToGlobal(rect.topLeft()); bottomLeft.setY(bottomLeft.y() - volume_popup->geometry().height()); volume_popup->setGeometry(QRect(bottomLeft, QSize(rect.width(), rect.height()))); }); statusBar()->insertPermanentWidget(1, volume_button); statusBar()->addPermanentWidget(multiplayer_state->GetStatusText()); statusBar()->addPermanentWidget(multiplayer_state->GetStatusIcon()); statusBar()->setVisible(true); // Removes an ugly inner border from the status bar widgets under Linux setStyleSheet(QStringLiteral("QStatusBar::item{border: none;}")); QActionGroup* actionGroup_ScreenLayouts = new QActionGroup(this); actionGroup_ScreenLayouts->addAction(ui->action_Screen_Layout_Default); actionGroup_ScreenLayouts->addAction(ui->action_Screen_Layout_Single_Screen); actionGroup_ScreenLayouts->addAction(ui->action_Screen_Layout_Large_Screen); actionGroup_ScreenLayouts->addAction(ui->action_Screen_Layout_Side_by_Side); actionGroup_ScreenLayouts->addAction(ui->action_Screen_Layout_Separate_Windows); actionGroup_ScreenLayouts->addAction(ui->action_Screen_Layout_Hybrid_Screen); actionGroup_ScreenLayouts->addAction(ui->action_Screen_Layout_Custom_Layout); QActionGroup* actionGroup_SmallPositions = new QActionGroup(this); actionGroup_SmallPositions->addAction(ui->action_Small_Screen_TopRight); actionGroup_SmallPositions->addAction(ui->action_Small_Screen_MiddleRight); actionGroup_SmallPositions->addAction(ui->action_Small_Screen_BottomRight); actionGroup_SmallPositions->addAction(ui->action_Small_Screen_TopLeft); actionGroup_SmallPositions->addAction(ui->action_Small_Screen_MiddleLeft); actionGroup_SmallPositions->addAction(ui->action_Small_Screen_BottomLeft); actionGroup_SmallPositions->addAction(ui->action_Small_Screen_Above); actionGroup_SmallPositions->addAction(ui->action_Small_Screen_Below); } void GMainWindow::InitializeDebugWidgets() { connect(ui->action_Create_Pica_Surface_Viewer, &QAction::triggered, this, &GMainWindow::OnCreateGraphicsSurfaceViewer); QMenu* debug_menu = ui->menu_View_Debugging; #if MICROPROFILE_ENABLED microProfileDialog = new MicroProfileDialog(this); microProfileDialog->hide(); debug_menu->addAction(microProfileDialog->toggleViewAction()); #else auto micro_profile_stub = new QAction(tr("MicroProfile (unavailable)"), this); micro_profile_stub->setEnabled(false); micro_profile_stub->setChecked(false); debug_menu->addAction(micro_profile_stub); #endif registersWidget = new RegistersWidget(system, this); addDockWidget(Qt::RightDockWidgetArea, registersWidget); registersWidget->hide(); debug_menu->addAction(registersWidget->toggleViewAction()); connect(this, &GMainWindow::EmulationStarting, registersWidget, &RegistersWidget::OnEmulationStarting); connect(this, &GMainWindow::EmulationStopping, registersWidget, &RegistersWidget::OnEmulationStopping); graphicsWidget = new GPUCommandStreamWidget(system, this); addDockWidget(Qt::RightDockWidgetArea, graphicsWidget); graphicsWidget->hide(); debug_menu->addAction(graphicsWidget->toggleViewAction()); graphicsCommandsWidget = new GPUCommandListWidget(system, this); addDockWidget(Qt::RightDockWidgetArea, graphicsCommandsWidget); graphicsCommandsWidget->hide(); debug_menu->addAction(graphicsCommandsWidget->toggleViewAction()); graphicsBreakpointsWidget = new GraphicsBreakPointsWidget(Pica::g_debug_context, this); addDockWidget(Qt::RightDockWidgetArea, graphicsBreakpointsWidget); graphicsBreakpointsWidget->hide(); debug_menu->addAction(graphicsBreakpointsWidget->toggleViewAction()); graphicsVertexShaderWidget = new GraphicsVertexShaderWidget(system, Pica::g_debug_context, this); addDockWidget(Qt::RightDockWidgetArea, graphicsVertexShaderWidget); graphicsVertexShaderWidget->hide(); debug_menu->addAction(graphicsVertexShaderWidget->toggleViewAction()); graphicsTracingWidget = new GraphicsTracingWidget(system, Pica::g_debug_context, this); addDockWidget(Qt::RightDockWidgetArea, graphicsTracingWidget); graphicsTracingWidget->hide(); debug_menu->addAction(graphicsTracingWidget->toggleViewAction()); connect(this, &GMainWindow::EmulationStarting, graphicsTracingWidget, &GraphicsTracingWidget::OnEmulationStarting); connect(this, &GMainWindow::EmulationStopping, graphicsTracingWidget, &GraphicsTracingWidget::OnEmulationStopping); waitTreeWidget = new WaitTreeWidget(system, this); addDockWidget(Qt::LeftDockWidgetArea, waitTreeWidget); waitTreeWidget->hide(); debug_menu->addAction(waitTreeWidget->toggleViewAction()); connect(this, &GMainWindow::EmulationStarting, waitTreeWidget, &WaitTreeWidget::OnEmulationStarting); connect(this, &GMainWindow::EmulationStopping, waitTreeWidget, &WaitTreeWidget::OnEmulationStopping); lleServiceModulesWidget = new LLEServiceModulesWidget(this); addDockWidget(Qt::RightDockWidgetArea, lleServiceModulesWidget); lleServiceModulesWidget->hide(); debug_menu->addAction(lleServiceModulesWidget->toggleViewAction()); connect(this, &GMainWindow::EmulationStarting, [this] { lleServiceModulesWidget->setDisabled(true); }); connect(this, &GMainWindow::EmulationStopping, waitTreeWidget, [this] { lleServiceModulesWidget->setDisabled(false); }); ipcRecorderWidget = new IPCRecorderWidget(system, this); addDockWidget(Qt::RightDockWidgetArea, ipcRecorderWidget); ipcRecorderWidget->hide(); debug_menu->addAction(ipcRecorderWidget->toggleViewAction()); connect(this, &GMainWindow::EmulationStarting, ipcRecorderWidget, &IPCRecorderWidget::OnEmulationStarting); } void GMainWindow::InitializeRecentFileMenuActions() { for (int i = 0; i < max_recent_files_item; ++i) { actions_recent_files[i] = new QAction(this); actions_recent_files[i]->setVisible(false); connect(actions_recent_files[i], &QAction::triggered, this, &GMainWindow::OnMenuRecentFile); ui->menu_recent_files->addAction(actions_recent_files[i]); } ui->menu_recent_files->addSeparator(); QAction* action_clear_recent_files = new QAction(this); action_clear_recent_files->setText(tr("Clear Recent Files")); connect(action_clear_recent_files, &QAction::triggered, this, [this] { UISettings::values.recent_files.clear(); UpdateRecentFiles(); }); ui->menu_recent_files->addAction(action_clear_recent_files); UpdateRecentFiles(); } void GMainWindow::InitializeSaveStateMenuActions() { for (u32 i = 0; i < Core::SaveStateSlotCount; ++i) { actions_load_state[i] = new QAction(this); actions_load_state[i]->setData(i + 1); connect(actions_load_state[i], &QAction::triggered, this, &GMainWindow::OnLoadState); ui->menu_Load_State->addAction(actions_load_state[i]); actions_save_state[i] = new QAction(this); actions_save_state[i]->setData(i + 1); connect(actions_save_state[i], &QAction::triggered, this, &GMainWindow::OnSaveState); ui->menu_Save_State->addAction(actions_save_state[i]); } connect(ui->action_Load_from_Newest_Slot, &QAction::triggered, this, [this] { UpdateSaveStates(); if (newest_slot != 0) { actions_load_state[newest_slot - 1]->trigger(); } }); connect(ui->action_Save_to_Oldest_Slot, &QAction::triggered, this, [this] { UpdateSaveStates(); actions_save_state[oldest_slot - 1]->trigger(); }); connect(ui->menu_Load_State->menuAction(), &QAction::hovered, this, &GMainWindow::UpdateSaveStates); connect(ui->menu_Save_State->menuAction(), &QAction::hovered, this, &GMainWindow::UpdateSaveStates); UpdateSaveStates(); } void GMainWindow::InitializeHotkeys() { hotkey_registry.LoadHotkeys(); const QString main_window = QStringLiteral("Main Window"); const QString fullscreen = QStringLiteral("Fullscreen"); // QAction Hotkeys const auto link_action_shortcut = [&](QAction* action, const QString& action_name, const bool primary_only = false) { static const QString main_window = QStringLiteral("Main Window"); action->setShortcut(hotkey_registry.GetKeySequence(main_window, action_name)); action->setAutoRepeat(false); this->addAction(action); if (!primary_only) secondary_window->addAction(action); }; link_action_shortcut(ui->action_Load_File, QStringLiteral("Load File")); link_action_shortcut(ui->action_Load_Amiibo, QStringLiteral("Load Amiibo")); link_action_shortcut(ui->action_Remove_Amiibo, QStringLiteral("Remove Amiibo")); link_action_shortcut(ui->action_Exit, QStringLiteral("Exit Azahar")); link_action_shortcut(ui->action_Restart, QStringLiteral("Restart Emulation")); link_action_shortcut(ui->action_Pause, QStringLiteral("Continue/Pause Emulation")); link_action_shortcut(ui->action_Stop, QStringLiteral("Stop Emulation")); link_action_shortcut(ui->action_Show_Filter_Bar, QStringLiteral("Toggle Filter Bar")); link_action_shortcut(ui->action_Show_Status_Bar, QStringLiteral("Toggle Status Bar")); link_action_shortcut(ui->action_Fullscreen, fullscreen, true); link_action_shortcut(ui->action_Capture_Screenshot, QStringLiteral("Capture Screenshot")); link_action_shortcut(ui->action_Screen_Layout_Swap_Screens, QStringLiteral("Swap Screens")); link_action_shortcut(ui->action_Screen_Layout_Upright_Screens, QStringLiteral("Rotate Screens Upright")); link_action_shortcut(ui->action_Advance_Frame, QStringLiteral("Advance Frame")); link_action_shortcut(ui->action_Load_from_Newest_Slot, QStringLiteral("Load from Newest Slot")); link_action_shortcut(ui->action_Save_to_Oldest_Slot, QStringLiteral("Save to Oldest Slot")); link_action_shortcut(ui->action_View_Lobby, QStringLiteral("Multiplayer Browse Public Rooms")); link_action_shortcut(ui->action_Start_Room, QStringLiteral("Multiplayer Create Room")); link_action_shortcut(ui->action_Connect_To_Room, QStringLiteral("Multiplayer Direct Connect to Room")); link_action_shortcut(ui->action_Show_Room, QStringLiteral("Multiplayer Show Current Room")); link_action_shortcut(ui->action_Leave_Room, QStringLiteral("Multiplayer Leave Room")); // QShortcut Hotkeys const auto connect_shortcut = [&](const QString& action_name, const auto& function) { const auto* hotkey = hotkey_registry.GetHotkey(main_window, action_name, this); const auto* secondary_hotkey = hotkey_registry.GetHotkey(main_window, action_name, secondary_window); connect(hotkey, &QShortcut::activated, this, function); connect(secondary_hotkey, &QShortcut::activated, this, function); }; connect_shortcut(QStringLiteral("Toggle Screen Layout"), &GMainWindow::ToggleScreenLayout); connect_shortcut(QStringLiteral("Exit Fullscreen"), [&] { if (emulation_running) { ui->action_Fullscreen->setChecked(false); ToggleFullscreen(); } }); connect_shortcut(QStringLiteral("Toggle Per-Application Speed"), [&] { Settings::values.frame_limit.SetGlobal(!Settings::values.frame_limit.UsingGlobal()); UpdateStatusBar(); }); connect_shortcut(QStringLiteral("Toggle Texture Dumping"), [&] { Settings::values.dump_textures = !Settings::values.dump_textures; }); connect_shortcut(QStringLiteral("Toggle Custom Textures"), [&] { Settings::values.custom_textures = !Settings::values.custom_textures; }); // We use "static" here in order to avoid capturing by lambda due to a MSVC bug, which makes // the variable hold a garbage value after this function exits static constexpr u16 SPEED_LIMIT_STEP = 5; connect_shortcut(QStringLiteral("Increase Speed Limit"), [&] { if (Settings::values.frame_limit.GetValue() == 0) { return; } if (Settings::values.frame_limit.GetValue() < 995 - SPEED_LIMIT_STEP) { Settings::values.frame_limit.SetValue(Settings::values.frame_limit.GetValue() + SPEED_LIMIT_STEP); } else { Settings::values.frame_limit = 0; } UpdateStatusBar(); }); connect_shortcut(QStringLiteral("Decrease Speed Limit"), [&] { if (Settings::values.frame_limit.GetValue() == 0) { Settings::values.frame_limit = 995; } else if (Settings::values.frame_limit.GetValue() > SPEED_LIMIT_STEP) { Settings::values.frame_limit.SetValue(Settings::values.frame_limit.GetValue() - SPEED_LIMIT_STEP); UpdateStatusBar(); } UpdateStatusBar(); }); connect_shortcut(QStringLiteral("Audio Mute/Unmute"), &GMainWindow::OnMute); connect_shortcut(QStringLiteral("Audio Volume Down"), &GMainWindow::OnDecreaseVolume); connect_shortcut(QStringLiteral("Audio Volume Up"), &GMainWindow::OnIncreaseVolume); // We use "static" here in order to avoid capturing by lambda due to a MSVC bug, which makes the // variable hold a garbage value after this function exits static constexpr u16 FACTOR_3D_STEP = 5; connect_shortcut(QStringLiteral("Decrease 3D Factor"), [this] { const auto factor_3d = Settings::values.factor_3d.GetValue(); if (factor_3d > 0) { if (factor_3d % FACTOR_3D_STEP != 0) { Settings::values.factor_3d = factor_3d - (factor_3d % FACTOR_3D_STEP); } else { Settings::values.factor_3d = factor_3d - FACTOR_3D_STEP; } UpdateStatusBar(); } }); connect_shortcut(QStringLiteral("Increase 3D Factor"), [this] { const auto factor_3d = Settings::values.factor_3d.GetValue(); if (factor_3d < 100) { if (factor_3d % FACTOR_3D_STEP != 0) { Settings::values.factor_3d = factor_3d + FACTOR_3D_STEP - (factor_3d % FACTOR_3D_STEP); } else { Settings::values.factor_3d = factor_3d + FACTOR_3D_STEP; } UpdateStatusBar(); } }); // Secondary Window QAction Hotkeys const auto add_secondary_window_hotkey = [this](QAction* action, QKeySequence hotkey, const char* slot) { // This action will fire specifically when secondary_window is in focus action->setShortcut(hotkey); disconnect(action, SIGNAL(triggered()), this, slot); connect(action, SIGNAL(triggered()), this, slot); secondary_window->addAction(action); }; // Use the same fullscreen hotkey as the main window const auto fullscreen_hotkey = hotkey_registry.GetKeySequence(main_window, fullscreen); add_secondary_window_hotkey(action_secondary_fullscreen, fullscreen_hotkey, SLOT(ToggleSecondaryFullscreen())); } void GMainWindow::SetDefaultUIGeometry() { // geometry: 55% of the window contents are in the upper screen half, 45% in the lower half const QRect screenRect = screen()->geometry(); const int w = screenRect.width() * 2 / 3; const int h = screenRect.height() / 2; const int x = (screenRect.x() + screenRect.width()) / 2 - w / 2; const int y = (screenRect.y() + screenRect.height()) / 2 - h * 55 / 100; setGeometry(x, y, w, h); } void GMainWindow::RestoreUIState() { restoreGeometry(UISettings::values.geometry); restoreState(UISettings::values.state); render_window->restoreGeometry(UISettings::values.renderwindow_geometry); #if MICROPROFILE_ENABLED microProfileDialog->restoreGeometry(UISettings::values.microprofile_geometry); microProfileDialog->setVisible(UISettings::values.microprofile_visible.GetValue()); #endif game_list->LoadInterfaceLayout(); ui->action_Single_Window_Mode->setChecked(UISettings::values.single_window_mode.GetValue()); ToggleWindowMode(); ui->action_Fullscreen->setChecked(UISettings::values.fullscreen.GetValue()); SyncMenuUISettings(); ui->action_Display_Dock_Widget_Headers->setChecked( UISettings::values.display_titlebar.GetValue()); OnDisplayTitleBars(ui->action_Display_Dock_Widget_Headers->isChecked()); ui->action_Show_Filter_Bar->setChecked(UISettings::values.show_filter_bar.GetValue()); game_list->SetFilterVisible(ui->action_Show_Filter_Bar->isChecked()); ui->action_Show_Status_Bar->setChecked(UISettings::values.show_status_bar.GetValue()); statusBar()->setVisible(ui->action_Show_Status_Bar->isChecked()); } void GMainWindow::OnAppFocusStateChanged(Qt::ApplicationState state) { if (state != Qt::ApplicationHidden && state != Qt::ApplicationInactive && state != Qt::ApplicationActive) { LOG_DEBUG(Frontend, "ApplicationState unusual flag: {} ", state); } if (!emulation_running) { return; } if (UISettings::values.pause_when_in_background) { if (emu_thread->IsRunning() && (state & (Qt::ApplicationHidden | Qt::ApplicationInactive))) { auto_paused = true; OnPauseGame(); } else if (!emu_thread->IsRunning() && auto_paused && state == Qt::ApplicationActive) { auto_paused = false; OnStartGame(); } } if (UISettings::values.mute_when_in_background) { if (!Settings::values.audio_muted && (state & (Qt::ApplicationHidden | Qt::ApplicationInactive))) { Settings::values.audio_muted = true; auto_muted = true; } else if (auto_muted && state == Qt::ApplicationActive) { Settings::values.audio_muted = false; auto_muted = false; } UpdateVolumeUI(); } } bool GApplicationEventFilter::eventFilter(QObject* object, QEvent* event) { if (event->type() == QEvent::FileOpen) { emit FileOpen(static_cast(event)); return true; } return false; } void GMainWindow::ConnectAppEvents() { const auto filter = new GApplicationEventFilter(); QGuiApplication::instance()->installEventFilter(filter); connect(filter, &GApplicationEventFilter::FileOpen, this, &GMainWindow::OnFileOpen); } void GMainWindow::ConnectWidgetEvents() { connect(game_list, &GameList::GameChosen, this, &GMainWindow::OnGameListLoadFile); connect(game_list, &GameList::OpenDirectory, this, &GMainWindow::OnGameListOpenDirectory); connect(game_list, &GameList::OpenFolderRequested, this, &GMainWindow::OnGameListOpenFolder); connect(game_list, &GameList::RemovePlayTimeRequested, this, &GMainWindow::OnGameListRemovePlayTimeData); connect(game_list, &GameList::CreateShortcut, this, &GMainWindow::OnGameListCreateShortcut); connect(game_list, &GameList::DumpRomFSRequested, this, &GMainWindow::OnGameListDumpRomFS); connect(game_list, &GameList::AddDirectory, this, &GMainWindow::OnGameListAddDirectory); connect(game_list_placeholder, &GameListPlaceholder::AddDirectory, this, &GMainWindow::OnGameListAddDirectory); connect(game_list, &GameList::ShowList, this, &GMainWindow::OnGameListShowList); connect(game_list, &GameList::PopulatingCompleted, this, [this] { multiplayer_state->UpdateGameList(game_list->GetModel()); }); connect(game_list, &GameList::OpenPerGameGeneralRequested, this, &GMainWindow::OnGameListOpenPerGameProperties); connect(this, &GMainWindow::EmulationStarting, render_window, &GRenderWindow::OnEmulationStarting); connect(this, &GMainWindow::EmulationStopping, render_window, &GRenderWindow::OnEmulationStopping); connect(this, &GMainWindow::EmulationStarting, secondary_window, &GRenderWindow::OnEmulationStarting); connect(this, &GMainWindow::EmulationStopping, secondary_window, &GRenderWindow::OnEmulationStopping); connect(&status_bar_update_timer, &QTimer::timeout, this, &GMainWindow::UpdateStatusBar); connect(this, &GMainWindow::UpdateProgress, this, &GMainWindow::OnUpdateProgress); connect(this, &GMainWindow::CIAInstallReport, this, &GMainWindow::OnCIAInstallReport); connect(this, &GMainWindow::CIAInstallFinished, this, &GMainWindow::OnCIAInstallFinished); connect(this, &GMainWindow::UpdateThemedIcons, multiplayer_state, &MultiplayerState::UpdateThemedIcons); } void GMainWindow::ConnectMenuEvents() { const auto connect_menu = [&](QAction* action, const auto& event_fn) { connect(action, &QAction::triggered, this, event_fn); // Add actions to this window so that hiding menus in fullscreen won't disable them addAction(action); // Add actions to the render window so that they work outside of single window mode render_window->addAction(action); }; // File connect_menu(ui->action_Load_File, &GMainWindow::OnMenuLoadFile); connect_menu(ui->action_Install_CIA, &GMainWindow::OnMenuInstallCIA); connect_menu(ui->action_Connect_Artic, &GMainWindow::OnMenuConnectArticBase); connect_menu(ui->action_Setup_System_Files, &GMainWindow::OnMenuSetUpSystemFiles); for (u32 region = 0; region < Core::NUM_SYSTEM_TITLE_REGIONS; region++) { connect_menu(ui->menu_Boot_Home_Menu->actions().at(region), [this, region] { OnMenuBootHomeMenu(region); }); } connect_menu(ui->action_Exit, &QMainWindow::close); connect_menu(ui->action_Load_Amiibo, &GMainWindow::OnLoadAmiibo); connect_menu(ui->action_Remove_Amiibo, &GMainWindow::OnRemoveAmiibo); // Emulation connect_menu(ui->action_Pause, &GMainWindow::OnPauseContinueGame); connect_menu(ui->action_Stop, &GMainWindow::OnStopGame); connect_menu(ui->action_Restart, [this] { BootGame(QString(game_path)); }); connect_menu(ui->action_Report_Compatibility, &GMainWindow::OnMenuReportCompatibility); connect_menu(ui->action_Configure, &GMainWindow::OnConfigure); connect_menu(ui->action_Configure_Current_Game, &GMainWindow::OnConfigurePerGame); // View connect_menu(ui->action_Single_Window_Mode, &GMainWindow::ToggleWindowMode); connect_menu(ui->action_Display_Dock_Widget_Headers, &GMainWindow::OnDisplayTitleBars); connect_menu(ui->action_Show_Filter_Bar, &GMainWindow::OnToggleFilterBar); connect(ui->action_Show_Status_Bar, &QAction::triggered, statusBar(), &QStatusBar::setVisible); // Multiplayer connect(ui->action_View_Lobby, &QAction::triggered, multiplayer_state, &MultiplayerState::OnViewLobby); connect(ui->action_Start_Room, &QAction::triggered, multiplayer_state, &MultiplayerState::OnCreateRoom); connect(ui->action_Leave_Room, &QAction::triggered, multiplayer_state, &MultiplayerState::OnCloseRoom); connect(ui->action_Connect_To_Room, &QAction::triggered, multiplayer_state, &MultiplayerState::OnDirectConnectToRoom); connect(ui->action_Show_Room, &QAction::triggered, multiplayer_state, &MultiplayerState::OnOpenNetworkRoom); connect_menu(ui->action_Fullscreen, &GMainWindow::ToggleFullscreen); connect_menu(ui->action_Screen_Layout_Default, &GMainWindow::ChangeScreenLayout); connect_menu(ui->action_Screen_Layout_Single_Screen, &GMainWindow::ChangeScreenLayout); connect_menu(ui->action_Screen_Layout_Large_Screen, &GMainWindow::ChangeScreenLayout); connect_menu(ui->action_Screen_Layout_Hybrid_Screen, &GMainWindow::ChangeScreenLayout); connect_menu(ui->action_Screen_Layout_Side_by_Side, &GMainWindow::ChangeScreenLayout); connect_menu(ui->action_Screen_Layout_Separate_Windows, &GMainWindow::ChangeScreenLayout); connect_menu(ui->action_Screen_Layout_Custom_Layout, &GMainWindow::ChangeScreenLayout); connect_menu(ui->action_Screen_Layout_Swap_Screens, &GMainWindow::OnSwapScreens); connect_menu(ui->action_Screen_Layout_Upright_Screens, &GMainWindow::OnRotateScreens); connect_menu(ui->action_Small_Screen_TopRight, &GMainWindow::ChangeSmallScreenPosition); connect_menu(ui->action_Small_Screen_MiddleRight, &GMainWindow::ChangeSmallScreenPosition); connect_menu(ui->action_Small_Screen_BottomRight, &GMainWindow::ChangeSmallScreenPosition); connect_menu(ui->action_Small_Screen_TopLeft, &GMainWindow::ChangeSmallScreenPosition); connect_menu(ui->action_Small_Screen_MiddleLeft, &GMainWindow::ChangeSmallScreenPosition); connect_menu(ui->action_Small_Screen_BottomLeft, &GMainWindow::ChangeSmallScreenPosition); connect_menu(ui->action_Small_Screen_Above, &GMainWindow::ChangeSmallScreenPosition); connect_menu(ui->action_Small_Screen_Below, &GMainWindow::ChangeSmallScreenPosition); // Movie connect_menu(ui->action_Record_Movie, &GMainWindow::OnRecordMovie); connect_menu(ui->action_Play_Movie, &GMainWindow::OnPlayMovie); connect_menu(ui->action_Close_Movie, &GMainWindow::OnCloseMovie); connect_menu(ui->action_Save_Movie, &GMainWindow::OnSaveMovie); connect_menu(ui->action_Movie_Read_Only_Mode, [this](bool checked) { movie.SetReadOnly(checked); }); connect_menu(ui->action_Advance_Frame, [this] { if (emulation_running && system.frame_limiter.IsFrameAdvancing()) { system.frame_limiter.AdvanceFrame(); } }); connect_menu(ui->action_Capture_Screenshot, &GMainWindow::OnCaptureScreenshot); connect_menu(ui->action_Dump_Video, &GMainWindow::OnDumpVideo); // Help connect_menu(ui->action_Open_Citra_Folder, &GMainWindow::OnOpenCitraFolder); connect_menu(ui->action_Open_Log_Folder, []() { QString path = QString::fromStdString(FileUtil::GetUserPath(FileUtil::UserPath::LogDir)); QDesktopServices::openUrl(QUrl::fromLocalFile(path)); }); connect_menu(ui->action_FAQ, []() { QDesktopServices::openUrl(QUrl(QStringLiteral("https://azahar-emu.org/pages/faq/"))); }); connect_menu(ui->action_About, &GMainWindow::OnMenuAboutCitra); } void GMainWindow::UpdateMenuState() { const bool is_paused = !emu_thread || !emu_thread->IsRunning() || system.frame_limiter.IsFrameAdvancing(); const std::array running_actions{ ui->action_Stop, ui->action_Restart, ui->action_Configure_Current_Game, ui->action_Report_Compatibility, ui->action_Load_Amiibo, ui->action_Remove_Amiibo, ui->action_Pause, ui->action_Advance_Frame, }; for (QAction* action : running_actions) { action->setEnabled(emulation_running); } ui->action_Capture_Screenshot->setEnabled(emulation_running); ui->action_Advance_Frame->setEnabled(emulation_running && is_paused); if (emulation_running && is_paused) { ui->action_Pause->setText(tr("&Continue")); } else { ui->action_Pause->setText(tr("&Pause")); } } void GMainWindow::OnDisplayTitleBars(bool show) { QList widgets = findChildren(); if (show) { for (QDockWidget* widget : widgets) { QWidget* old = widget->titleBarWidget(); widget->setTitleBarWidget(nullptr); if (old) { delete old; } } } else { for (QDockWidget* widget : widgets) { QWidget* old = widget->titleBarWidget(); widget->setTitleBarWidget(new QWidget()); if (old) { delete old; } } } } #if defined(HAVE_SDL2) && defined(__unix__) && !defined(__APPLE__) static std::optional HoldWakeLockLinux(u32 window_id = 0) { if (!QDBusConnection::sessionBus().isConnected()) { return {}; } // reference: https://flatpak.github.io/xdg-desktop-portal/#gdbus-org.freedesktop.portal.Inhibit QDBusInterface xdp(QStringLiteral("org.freedesktop.portal.Desktop"), QStringLiteral("/org/freedesktop/portal/desktop"), QStringLiteral("org.freedesktop.portal.Inhibit")); if (!xdp.isValid()) { LOG_WARNING(Frontend, "Couldn't connect to XDP D-Bus endpoint"); return {}; } QVariantMap options = {}; //: TRANSLATORS: This string is shown to the user to explain why Citra needs to prevent the //: computer from sleeping options.insert(QString::fromLatin1("reason"), QCoreApplication::translate("GMainWindow", "Azahar is running an application")); // 0x4: Suspend lock; 0x8: Idle lock QDBusReply reply = xdp.call(QString::fromLatin1("Inhibit"), QString::fromLatin1("x11:") + QString::number(window_id, 16), 12U, options); if (reply.isValid()) { return reply.value(); } LOG_WARNING(Frontend, "Couldn't read Inhibit reply from XDP: {}", reply.error().message().toStdString()); return {}; } static void ReleaseWakeLockLinux(const QDBusObjectPath& lock) { if (!QDBusConnection::sessionBus().isConnected()) { return; } QDBusInterface unlocker(QString::fromLatin1("org.freedesktop.portal.Desktop"), lock.path(), QString::fromLatin1("org.freedesktop.portal.Request")); unlocker.call(QString::fromLatin1("Close")); } #endif // __unix__ void GMainWindow::PreventOSSleep() { #ifdef _WIN32 SetThreadExecutionState(ES_CONTINUOUS | ES_SYSTEM_REQUIRED | ES_DISPLAY_REQUIRED); #elif defined(HAVE_SDL2) SDL_DisableScreenSaver(); #if defined(__unix__) && !defined(__APPLE__) auto reply = HoldWakeLockLinux(winId()); if (reply) { wake_lock = std::move(reply.value()); } #endif // defined(__unix__) && !defined(__APPLE__) #endif // _WIN32 } void GMainWindow::AllowOSSleep() { #ifdef _WIN32 SetThreadExecutionState(ES_CONTINUOUS); #elif defined(HAVE_SDL2) SDL_EnableScreenSaver(); #if defined(__unix__) && !defined(__APPLE__) if (!wake_lock.path().isEmpty()) { ReleaseWakeLockLinux(wake_lock); } #endif // defined(__unix__) && !defined(__APPLE__) #endif // _WIN32 } bool GMainWindow::LoadROM(const QString& filename) { // Shutdown previous session if the emu thread is still active... if (emu_thread) { ShutdownGame(); } if (!render_window->InitRenderTarget() || !secondary_window->InitRenderTarget()) { LOG_CRITICAL(Frontend, "Failed to initialize render targets!"); return false; } const auto scope = render_window->Acquire(); const Core::System::ResultStatus result{ system.Load(*render_window, filename.toStdString(), secondary_window)}; if (result != Core::System::ResultStatus::Success) { switch (result) { case Core::System::ResultStatus::ErrorGetLoader: LOG_CRITICAL(Frontend, "Failed to obtain loader for {}!", filename.toStdString()); QMessageBox::critical( this, tr("Invalid App Format"), tr("Your app format is not supported.
Please follow the guides to redump your " "game " "cartridges or " "installed " "titles.")); break; case Core::System::ResultStatus::ErrorSystemMode: LOG_CRITICAL(Frontend, "Failed to load App!"); QMessageBox::critical( this, tr("App Corrupted"), tr("Your app is corrupted.
Please follow the guides to redump your " "game " "cartridges or " "installed " "titles.")); break; case Core::System::ResultStatus::ErrorLoader_ErrorEncrypted: { QMessageBox::critical(this, tr("App Encrypted"), tr("Your app is encrypted.
" "" "Please check our blog for more info.")); break; } case Core::System::ResultStatus::ErrorLoader_ErrorInvalidFormat: QMessageBox::critical( this, tr("Invalid App Format"), tr("Your app format is not supported.
Please follow the guides to redump your " "game " "cartridges or " "installed " "titles.")); break; case Core::System::ResultStatus::ErrorLoader_ErrorGbaTitle: QMessageBox::critical(this, tr("Unsupported App"), tr("GBA Virtual Console is not supported by Azahar.")); break; case Core::System::ResultStatus::ErrorArticDisconnected: QMessageBox::critical( this, tr("Artic Server"), tr(fmt::format( "An error has occurred whilst communicating with the Artic Server.\n{}", system.GetStatusDetails()) .c_str())); break; default: QMessageBox::critical( this, tr("Error while loading App!"), tr("An unknown error occurred. Please see the log for more details.")); break; } return false; } std::string title; system.GetAppLoader().ReadTitle(title); game_title = QString::fromStdString(title); UpdateWindowTitle(); u64 title_id; system.GetAppLoader().ReadProgramId(title_id); game_path = filename; game_title_id = title_id; return true; } void GMainWindow::BootGame(const QString& filename) { if (emu_thread) { ShutdownGame(); } const bool is_artic = filename.startsWith(QString::fromStdString("articbase:/")) || filename.startsWith(QString::fromStdString("articinio:/")) || filename.startsWith(QString::fromStdString("articinin:/")); if (!is_artic && filename.endsWith(QStringLiteral(".cia"))) { const auto answer = QMessageBox::question( this, tr("CIA must be installed before usage"), tr("Before using this CIA, you must install it. Do you want to install it now?"), QMessageBox::Yes | QMessageBox::No); if (answer == QMessageBox::Yes) InstallCIA(QStringList(filename)); return; } show_artic_label = is_artic; LOG_INFO(Frontend, "Azahar starting..."); if (!is_artic) { StoreRecentFile(filename); // Put the filename on top of the list } if (movie_record_on_start) { movie.PrepareForRecording(); } if (movie_playback_on_start) { movie.PrepareForPlayback(movie_playback_path.toStdString()); } const std::string path = filename.toStdString(); auto loader = Loader::GetLoader(path); u64 title_id{0}; Loader::ResultStatus res = loader->ReadProgramId(title_id); if (Loader::ResultStatus::Success == res) { // Load per game settings const std::string name{is_artic ? "" : FileUtil::GetFilename(filename.toStdString())}; const std::string config_file_name = title_id == 0 ? name : fmt::format("{:016X}", title_id); LOG_INFO(Frontend, "Loading per application config file for title {}", config_file_name); QtConfig per_game_config(config_file_name, QtConfig::ConfigType::PerGameConfig); } // Artic Server cannot accept a client multiple times, so multiple loaders are not // possible. Instead register the app loader early and do not create it again on system load. if (!loader->SupportsMultipleInstancesForSameFile()) { system.RegisterAppLoaderEarly(loader); } system.ApplySettings(); Settings::LogSettings(); // Save configurations UpdateUISettings(); game_list->SaveInterfaceLayout(); config->Save(); if (!LoadROM(filename)) { render_window->ReleaseRenderTarget(); secondary_window->ReleaseRenderTarget(); return; } // Set everything up if (movie_record_on_start) { movie.StartRecording(movie_record_path.toStdString(), movie_record_author.toStdString()); movie_record_on_start = false; movie_record_path.clear(); movie_record_author.clear(); } if (movie_playback_on_start) { movie.StartPlayback(movie_playback_path.toStdString()); movie_playback_on_start = false; movie_playback_path.clear(); } ui->action_Advance_Frame->setEnabled(false); if (video_dumping_on_start) { StartVideoDumping(video_dumping_path); video_dumping_on_start = false; video_dumping_path.clear(); } // Register debug widgets if (graphicsWidget->isVisible()) { graphicsWidget->Register(); } // Create and start the emulation thread emu_thread = std::make_unique(system, *render_window); emit EmulationStarting(emu_thread.get()); emu_thread->start(); connect(render_window, &GRenderWindow::Closed, this, &GMainWindow::OnStopGame); connect(render_window, &GRenderWindow::MouseActivity, this, &GMainWindow::OnMouseActivity); connect(secondary_window, &GRenderWindow::Closed, this, &GMainWindow::OnStopGame); connect(secondary_window, &GRenderWindow::MouseActivity, this, &GMainWindow::OnMouseActivity); // BlockingQueuedConnection is important here, it makes sure we've finished refreshing our views // before the CPU continues connect(emu_thread.get(), &EmuThread::DebugModeEntered, registersWidget, &RegistersWidget::OnDebugModeEntered, Qt::BlockingQueuedConnection); connect(emu_thread.get(), &EmuThread::DebugModeEntered, waitTreeWidget, &WaitTreeWidget::OnDebugModeEntered, Qt::BlockingQueuedConnection); connect(emu_thread.get(), &EmuThread::DebugModeLeft, registersWidget, &RegistersWidget::OnDebugModeLeft, Qt::BlockingQueuedConnection); connect(emu_thread.get(), &EmuThread::DebugModeLeft, waitTreeWidget, &WaitTreeWidget::OnDebugModeLeft, Qt::BlockingQueuedConnection); connect(emu_thread.get(), &EmuThread::LoadProgress, loading_screen, &LoadingScreen::OnLoadProgress, Qt::QueuedConnection); connect(emu_thread.get(), &EmuThread::HideLoadingScreen, loading_screen, &LoadingScreen::OnLoadComplete); // Update the GUI registersWidget->OnDebugModeEntered(); if (ui->action_Single_Window_Mode->isChecked()) { game_list->hide(); game_list_placeholder->hide(); } status_bar_update_timer.start(1000); if (UISettings::values.hide_mouse) { mouse_hide_timer.start(); setMouseTracking(true); } loading_screen->Prepare(system.GetAppLoader()); loading_screen->show(); emulation_running = true; if (ui->action_Fullscreen->isChecked()) { ShowFullscreen(); } OnStartGame(); } void GMainWindow::ShutdownGame() { if (!emulation_running) { return; } if (ui->action_Fullscreen->isChecked()) { HideFullscreen(); } auto video_dumper = system.GetVideoDumper(); if (video_dumper && video_dumper->IsDumping()) { game_shutdown_delayed = true; OnStopVideoDumping(); return; } AllowOSSleep(); discord_rpc->Pause(); emu_thread->RequestStop(); // Release emu threads from any breakpoints // This belongs after RequestStop() and before wait() because if emulation stops on a GPU // breakpoint after (or before) RequestStop() is called, the emulation would never be able // to continue out to the main loop and terminate. Thus wait() would hang forever. // TODO(bunnei): This function is not thread safe, but it's being used as if it were Pica::g_debug_context->ClearBreakpoints(); // Unregister debug widgets if (graphicsWidget->isVisible()) { graphicsWidget->Unregister(); } // Frame advancing must be cancelled in order to release the emu thread from waiting system.frame_limiter.SetFrameAdvancing(false); emit EmulationStopping(); // Wait for emulation thread to complete and delete it emu_thread->wait(); emu_thread = nullptr; OnCloseMovie(); discord_rpc->Update(); #ifdef __unix__ Common::Linux::StopGamemode(); #endif // The emulation is stopped, so closing the window or not does not matter anymore disconnect(render_window, &GRenderWindow::Closed, this, &GMainWindow::OnStopGame); disconnect(secondary_window, &GRenderWindow::Closed, this, &GMainWindow::OnStopGame); render_window->hide(); secondary_window->hide(); loading_screen->hide(); loading_screen->Clear(); if (game_list->IsEmpty()) { game_list_placeholder->show(); } else { game_list->show(); } game_list->SetFilterFocus(); setMouseTracking(false); // Disable status bar updates status_bar_update_timer.stop(); message_label_used_for_movie = false; show_artic_label = false; artic_traffic_label->setVisible(false); emu_speed_label->setVisible(false); game_fps_label->setVisible(false); emu_frametime_label->setVisible(false); UpdateSaveStates(); emulation_running = false; game_title.clear(); UpdateWindowTitle(); game_path.clear(); game_title_id = 0; // Update the GUI UpdateMenuState(); // When closing the game, destroy the GLWindow to clear the context after the game is closed render_window->ReleaseRenderTarget(); secondary_window->ReleaseRenderTarget(); } void GMainWindow::StoreRecentFile(const QString& filename) { UISettings::values.recent_files.prepend(filename); UISettings::values.recent_files.removeDuplicates(); while (UISettings::values.recent_files.size() > max_recent_files_item) { UISettings::values.recent_files.removeLast(); } UpdateRecentFiles(); } void GMainWindow::UpdateRecentFiles() { const int num_recent_files = std::min(static_cast(UISettings::values.recent_files.size()), max_recent_files_item); for (int i = 0; i < num_recent_files; i++) { const QString text = QStringLiteral("&%1. %2").arg(i + 1).arg( QFileInfo(UISettings::values.recent_files[i]).fileName()); actions_recent_files[i]->setText(text); actions_recent_files[i]->setData(UISettings::values.recent_files[i]); actions_recent_files[i]->setToolTip(UISettings::values.recent_files[i]); actions_recent_files[i]->setVisible(true); } for (int j = num_recent_files; j < max_recent_files_item; ++j) { actions_recent_files[j]->setVisible(false); } // Enable the recent files menu if the list isn't empty ui->menu_recent_files->setEnabled(num_recent_files != 0); } void GMainWindow::UpdateSaveStates() { if (!system.IsPoweredOn()) { ui->menu_Load_State->setEnabled(false); ui->menu_Save_State->setEnabled(false); return; } ui->menu_Load_State->setEnabled(true); ui->menu_Save_State->setEnabled(true); ui->action_Load_from_Newest_Slot->setEnabled(false); oldest_slot = newest_slot = 0; oldest_slot_time = std::numeric_limits::max(); newest_slot_time = 0; u64 title_id; if (system.GetAppLoader().ReadProgramId(title_id) != Loader::ResultStatus::Success) { return; } auto savestates = Core::ListSaveStates(title_id, movie.GetCurrentMovieID()); for (u32 i = 0; i < Core::SaveStateSlotCount; ++i) { actions_load_state[i]->setEnabled(false); actions_load_state[i]->setText(tr("Slot %1").arg(i + 1)); actions_save_state[i]->setText(tr("Slot %1").arg(i + 1)); } for (const auto& savestate : savestates) { const bool display_name = savestate.status == Core::SaveStateInfo::ValidationStatus::RevisionDismatch && !savestate.build_name.empty(); const auto text = tr("Slot %1 - %2 %3") .arg(savestate.slot) .arg(QDateTime::fromSecsSinceEpoch(savestate.time) .toString(QStringLiteral("yyyy-MM-dd hh:mm:ss"))) .arg(display_name ? QString::fromStdString(savestate.build_name) : QLatin1String()) .trimmed(); actions_load_state[savestate.slot - 1]->setEnabled(true); actions_load_state[savestate.slot - 1]->setText(text); actions_save_state[savestate.slot - 1]->setText(text); ui->action_Load_from_Newest_Slot->setEnabled(true); if (savestate.time > newest_slot_time) { newest_slot = savestate.slot; newest_slot_time = savestate.time; } if (savestate.time < oldest_slot_time) { oldest_slot = savestate.slot; oldest_slot_time = savestate.time; } } for (u32 i = 0; i < Core::SaveStateSlotCount; ++i) { if (!actions_load_state[i]->isEnabled()) { // Prefer empty slot oldest_slot = i + 1; oldest_slot_time = 0; break; } } } void GMainWindow::OnGameListLoadFile(QString game_path) { if (ConfirmChangeGame()) { BootGame(game_path); } } void GMainWindow::OnGameListOpenFolder(u64 data_id, GameListOpenTarget target) { std::string path; std::string open_target; switch (target) { case GameListOpenTarget::SAVE_DATA: { open_target = "Save Data"; std::string sdmc_dir = FileUtil::GetUserPath(FileUtil::UserPath::SDMCDir); path = FileSys::ArchiveSource_SDSaveData::GetSaveDataPathFor(sdmc_dir, data_id); break; } case GameListOpenTarget::EXT_DATA: { open_target = "Extra Data"; std::string sdmc_dir = FileUtil::GetUserPath(FileUtil::UserPath::SDMCDir); path = FileSys::GetExtDataPathFromId(sdmc_dir, data_id); break; } case GameListOpenTarget::APPLICATION: { open_target = "Application"; auto media_type = Service::AM::GetTitleMediaType(data_id); path = Service::AM::GetTitlePath(media_type, data_id) + "content/"; break; } case GameListOpenTarget::UPDATE_DATA: { open_target = "Update Data"; path = Service::AM::GetTitlePath(Service::FS::MediaType::SDMC, data_id + 0xe00000000) + "content/"; break; } case GameListOpenTarget::TEXTURE_DUMP: { open_target = "Dumped Textures"; path = fmt::format("{}textures/{:016X}/", FileUtil::GetUserPath(FileUtil::UserPath::DumpDir), data_id); break; } case GameListOpenTarget::TEXTURE_LOAD: { open_target = "Custom Textures"; path = fmt::format("{}textures/{:016X}/", FileUtil::GetUserPath(FileUtil::UserPath::LoadDir), data_id); break; } case GameListOpenTarget::MODS: { open_target = "Mods"; path = fmt::format("{}mods/{:016X}/", FileUtil::GetUserPath(FileUtil::UserPath::LoadDir), data_id); break; } case GameListOpenTarget::DLC_DATA: { open_target = "DLC Data"; path = fmt::format("{}Nintendo 3DS/00000000000000000000000000000000/" "00000000000000000000000000000000/title/0004008c/{:08x}/content/", FileUtil::GetUserPath(FileUtil::UserPath::SDMCDir), data_id); break; } case GameListOpenTarget::SHADER_CACHE: { open_target = "Shader Cache"; path = FileUtil::GetUserPath(FileUtil::UserPath::ShaderDir); break; } default: LOG_ERROR(Frontend, "Unexpected target {}", static_cast(target)); return; } QString qpath = QString::fromStdString(path); QDir dir(qpath); if (!dir.exists()) { QMessageBox::critical( this, tr("Error Opening %1 Folder").arg(QString::fromStdString(open_target)), tr("Folder does not exist!")); return; } LOG_INFO(Frontend, "Opening {} path for data_id={:016x}", open_target, data_id); QDesktopServices::openUrl(QUrl::fromLocalFile(qpath)); } void GMainWindow::OnGameListRemovePlayTimeData(u64 program_id) { if (QMessageBox::question(this, tr("Remove Play Time Data"), tr("Reset play time?"), QMessageBox::Yes | QMessageBox::No, QMessageBox::No) != QMessageBox::Yes) { return; } play_time_manager->ResetProgramPlayTime(program_id); game_list->PopulateAsync(UISettings::values.game_dirs); } bool GMainWindow::CreateShortcutLink(const std::filesystem::path& shortcut_path, const std::string& comment, const std::filesystem::path& icon_path, const std::string& command, const std::string& arguments, const std::string& categories, const std::string& keywords, const std::string& name, const bool& skip_tryexec) try { #if defined(__linux__) || defined(__FreeBSD__) // Linux and FreeBSD std::filesystem::path shortcut_path_full = shortcut_path / (name + ".desktop"); std::ofstream shortcut_stream(shortcut_path_full, std::ios::binary | std::ios::trunc); if (!shortcut_stream.is_open()) { LOG_ERROR(Frontend, "Failed to create shortcut"); return false; } // TODO: Migrate fmt::print to std::print in futures STD C++ 23. fmt::print(shortcut_stream, "[Desktop Entry]\n"); fmt::print(shortcut_stream, "Type=Application\n"); fmt::print(shortcut_stream, "Version=1.0\n"); fmt::print(shortcut_stream, "Name={}\n", name); if (!comment.empty()) { fmt::print(shortcut_stream, "Comment={}\n", comment); } if (std::filesystem::is_regular_file(icon_path)) { fmt::print(shortcut_stream, "Icon={}\n", icon_path.string()); } if (!skip_tryexec) { fmt::print(shortcut_stream, "TryExec={}\n", command); } fmt::print(shortcut_stream, "Exec={} {}\n", command, arguments); if (!categories.empty()) { fmt::print(shortcut_stream, "Categories={}\n", categories); } if (!keywords.empty()) { fmt::print(shortcut_stream, "Keywords={}\n", keywords); } return true; #elif defined(_WIN32) // Windows HRESULT hr = CoInitialize(nullptr); if (FAILED(hr)) { LOG_ERROR(Frontend, "CoInitialize failed"); return false; } SCOPE_EXIT({ CoUninitialize(); }); IShellLinkW* ps1 = nullptr; IPersistFile* persist_file = nullptr; SCOPE_EXIT({ if (persist_file != nullptr) { persist_file->Release(); } if (ps1 != nullptr) { ps1->Release(); } }); HRESULT hres = CoCreateInstance(CLSID_ShellLink, nullptr, CLSCTX_INPROC_SERVER, IID_IShellLinkW, reinterpret_cast(&ps1)); if (FAILED(hres)) { LOG_ERROR(Frontend, "Failed to create IShellLinkW instance"); return false; } hres = ps1->SetPath(Common::UTF8ToUTF16W(command).data()); if (FAILED(hres)) { LOG_ERROR(Frontend, "Failed to set path"); return false; } if (!arguments.empty()) { hres = ps1->SetArguments(Common::UTF8ToUTF16W(arguments).data()); if (FAILED(hres)) { LOG_ERROR(Frontend, "Failed to set arguments"); return false; } } if (!comment.empty()) { hres = ps1->SetDescription(Common::UTF8ToUTF16W(comment).data()); if (FAILED(hres)) { LOG_ERROR(Frontend, "Failed to set description"); return false; } } if (std::filesystem::is_regular_file(icon_path)) { hres = ps1->SetIconLocation(icon_path.c_str(), 0); if (FAILED(hres)) { LOG_ERROR(Frontend, "Failed to set icon location"); return false; } } hres = ps1->QueryInterface(IID_IPersistFile, reinterpret_cast(&persist_file)); if (FAILED(hres)) { LOG_ERROR(Frontend, "Failed to get IPersistFile interface"); return false; } hres = persist_file->Save(std::filesystem::path{shortcut_path / (name + ".lnk")}.c_str(), TRUE); if (FAILED(hres)) { LOG_ERROR(Frontend, "Failed to save shortcut"); return false; } return true; #else // Unsupported platform return false; #endif } catch (const std::exception& e) { LOG_ERROR(Frontend, "Failed to create shortcut: {}", e.what()); return false; } // Messages in pre-defined message boxes for less code spaghetti bool GMainWindow::CreateShortcutMessagesGUI(QWidget* parent, int message, const QString& game_title) { int result = 0; QMessageBox::StandardButtons buttons; switch (message) { case GMainWindow::CREATE_SHORTCUT_MSGBOX_FULLSCREEN_PROMPT: buttons = QMessageBox::Yes | QMessageBox::No; result = QMessageBox::information( parent, tr("Create Shortcut"), tr("Do you want to launch the application in fullscreen?"), buttons); return result == QMessageBox::Yes; case GMainWindow::CREATE_SHORTCUT_MSGBOX_SUCCESS: QMessageBox::information(parent, tr("Create Shortcut"), tr("Successfully created a shortcut to %1").arg(game_title)); return false; case GMainWindow::CREATE_SHORTCUT_MSGBOX_APPIMAGE_VOLATILE_WARNING: buttons = QMessageBox::StandardButton::Ok | QMessageBox::StandardButton::Cancel; result = QMessageBox::warning(this, tr("Create Shortcut"), tr("This will create a shortcut to the current AppImage. This may " "not work well if you update. Continue?"), buttons); return result == QMessageBox::Ok; default: buttons = QMessageBox::Ok; QMessageBox::critical(parent, tr("Create Shortcut"), tr("Failed to create a shortcut to %1").arg(game_title), buttons); return false; } } bool GMainWindow::MakeShortcutIcoPath(const u64 program_id, const std::string_view game_file_name, std::filesystem::path& out_icon_path) { // Get path to Citra icons directory & icon extension std::string ico_extension = "png"; #if defined(_WIN32) out_icon_path = FileUtil::GetUserPath(FileUtil::UserPath::IconsDir); ico_extension = "ico"; #elif defined(__linux__) || defined(__FreeBSD__) out_icon_path = FileUtil::GetUserDirectory("XDG_DATA_HOME") + "/icons/hicolor/256x256/"; #endif // Create icons directory if it doesn't exist if (!FileUtil::CreateFullPath(out_icon_path.string())) { QMessageBox::critical( this, tr("Create Icon"), tr("Cannot create icon file. Path \"%1\" does not exist and cannot be created.") .arg(QString::fromStdString(out_icon_path.string())), QMessageBox::StandardButton::Ok); out_icon_path.clear(); return false; } // Create icon file path out_icon_path /= (program_id == 0 ? fmt::format("citra-{}.{}", game_file_name, ico_extension) : fmt::format("citra-{:016X}.{}", program_id, ico_extension)); return true; } void GMainWindow::OnGameListCreateShortcut(u64 program_id, const std::string& game_path, GameListShortcutTarget target) { std::string citra_command{}; bool skip_tryexec = false; const char* env_flatpak_id = getenv("FLATPAK_ID"); if (env_flatpak_id) { citra_command = fmt::format("flatpak run {}", env_flatpak_id); skip_tryexec = true; } else { // Get path to Citra executable const QStringList args = QApplication::arguments(); citra_command = args[0].toStdString(); // If relative path, make it an absolute path if (citra_command.c_str()[0] == '.') { citra_command = FileUtil::GetCurrentDir().value_or("") + DIR_SEP + citra_command; } } // Shortcut path std::filesystem::path shortcut_path{}; if (target == GameListShortcutTarget::Desktop) { shortcut_path = QStandardPaths::writableLocation(QStandardPaths::DesktopLocation).toStdString(); } else if (target == GameListShortcutTarget::Applications) { shortcut_path = GetApplicationsDirectory(); } // Icon path and title if (!std::filesystem::exists(shortcut_path)) { CreateShortcutMessagesGUI(this, CREATE_SHORTCUT_MSGBOX_ERROR, {}); LOG_ERROR(Frontend, "Invalid shortcut target"); return; } // Get title from game file const auto loader = Loader::GetLoader(game_path); std::string game_title = fmt::format("{:016X}", program_id); if (loader->ReadTitle(game_title) != Loader::ResultStatus::Success) { game_title = fmt::format("{:016x}", program_id); } // Delete illegal characters from title const std::string illegal_chars = "<>:\"/\\|?*."; for (auto it = game_title.rbegin(); it != game_title.rend(); ++it) { if (illegal_chars.find(*it) != std::string::npos) { game_title.erase(it.base() - 1); } } // Get icon from game file std::vector icon_image_file; if (loader->ReadIcon(icon_image_file) != Loader::ResultStatus::Success) { LOG_WARNING(Frontend, "Could not read icon from {:s}", game_path); } const QPixmap pixmap = GetQPixmapFromSMDH(icon_image_file); const QImage icon_data = pixmap.toImage(); std::filesystem::path out_icon_path; if (MakeShortcutIcoPath(program_id, game_title, out_icon_path)) { if (!SaveIconToFile(out_icon_path, icon_data)) { LOG_ERROR(Frontend, "Could not write icon to file"); } } const auto qt_game_title = QString::fromStdString(game_title); #if defined(__linux__) // Special case for AppImages // Warn once if we are making a shortcut to a volatile AppImage const std::string appimage_ending = std::string(Common::g_scm_rev).substr(0, 9).append(".AppImage"); if (citra_command.ends_with(appimage_ending) && !UISettings::values.shortcut_already_warned) { if (CreateShortcutMessagesGUI(this, CREATE_SHORTCUT_MSGBOX_APPIMAGE_VOLATILE_WARNING, qt_game_title)) { return; } UISettings::values.shortcut_already_warned = true; } #endif // __linux__ // Create shortcut std::string arguments = fmt::format("\"{:s}\"", game_path); if (CreateShortcutMessagesGUI(this, CREATE_SHORTCUT_MSGBOX_FULLSCREEN_PROMPT, qt_game_title)) { arguments = "-f " + arguments; } const std::string comment = fmt::format("Start {:s} with the Azahar Emulator", game_title); const std::string categories = "Game;Emulator;Qt;"; const std::string keywords = "3ds;Nintendo;"; if (CreateShortcutLink(shortcut_path, comment, out_icon_path, citra_command, arguments, categories, keywords, game_title, skip_tryexec)) { CreateShortcutMessagesGUI(this, CREATE_SHORTCUT_MSGBOX_SUCCESS, qt_game_title); return; } CreateShortcutMessagesGUI(this, CREATE_SHORTCUT_MSGBOX_ERROR, qt_game_title); } void GMainWindow::OnGameListDumpRomFS(QString game_path, u64 program_id) { auto* dialog = new QProgressDialog(tr("Dumping..."), tr("Cancel"), 0, 0, this); dialog->setWindowModality(Qt::WindowModal); dialog->setWindowFlags(dialog->windowFlags() & ~(Qt::WindowCloseButtonHint | Qt::WindowContextHelpButtonHint)); dialog->setCancelButton(nullptr); dialog->setMinimumDuration(0); dialog->setValue(0); const auto base_path = fmt::format( "{}romfs/{:016X}", FileUtil::GetUserPath(FileUtil::UserPath::DumpDir), program_id); const auto update_path = fmt::format("{}romfs/{:016X}", FileUtil::GetUserPath(FileUtil::UserPath::DumpDir), program_id | 0x0004000e00000000); using FutureWatcher = QFutureWatcher>; auto* future_watcher = new FutureWatcher(this); connect(future_watcher, &FutureWatcher::finished, this, [this, dialog, base_path, update_path, future_watcher] { dialog->hide(); const auto& [base, update] = future_watcher->result(); if (base != Loader::ResultStatus::Success) { QMessageBox::critical( this, tr("Azahar"), tr("Could not dump base RomFS.\nRefer to the log for details.")); return; } QDesktopServices::openUrl(QUrl::fromLocalFile(QString::fromStdString(base_path))); if (update == Loader::ResultStatus::Success) { QDesktopServices::openUrl( QUrl::fromLocalFile(QString::fromStdString(update_path))); } }); auto future = QtConcurrent::run([game_path, base_path, update_path] { std::unique_ptr loader = Loader::GetLoader(game_path.toStdString()); return std::make_pair(loader->DumpRomFS(base_path), loader->DumpUpdateRomFS(update_path)); }); future_watcher->setFuture(future); } void GMainWindow::OnGameListOpenDirectory(const QString& directory) { QString path; if (directory == QStringLiteral("INSTALLED")) { path = QString::fromStdString(FileUtil::GetUserPath(FileUtil::UserPath::SDMCDir) + "Nintendo " "3DS/00000000000000000000000000000000/" "00000000000000000000000000000000/title/00040000"); } else if (directory == QStringLiteral("SYSTEM")) { path = QString::fromStdString(FileUtil::GetUserPath(FileUtil::UserPath::NANDDir) + "00000000000000000000000000000000/title/00040010"); } else { path = directory; } if (!QFileInfo::exists(path)) { QMessageBox::critical(this, tr("Error Opening %1").arg(path), tr("Folder does not exist!")); return; } QDesktopServices::openUrl(QUrl::fromLocalFile(path)); } void GMainWindow::OnGameListAddDirectory() { const QString dir_path = QFileDialog::getExistingDirectory(this, tr("Select Directory")); if (dir_path.isEmpty()) return; UISettings::GameDir game_dir{dir_path, false, true}; if (!UISettings::values.game_dirs.contains(game_dir)) { UISettings::values.game_dirs.append(game_dir); game_list->PopulateAsync(UISettings::values.game_dirs); } else { LOG_WARNING(Frontend, "Selected directory is already in the application list"); } } void GMainWindow::OnGameListShowList(bool show) { if (emulation_running && ui->action_Single_Window_Mode->isChecked()) return; game_list->setVisible(show); game_list_placeholder->setVisible(!show); }; void GMainWindow::OnGameListOpenPerGameProperties(const QString& file) { const auto loader = Loader::GetLoader(file.toStdString()); u64 title_id{}; if (!loader || loader->ReadProgramId(title_id) != Loader::ResultStatus::Success) { QMessageBox::information(this, tr("Properties"), tr("The application properties could not be loaded.")); return; } OpenPerGameConfiguration(title_id, file); } void GMainWindow::OnMenuLoadFile() { const QString extensions = QStringLiteral("*.").append( GameList::supported_file_extensions.join(QStringLiteral(" *."))); const QString file_filter = tr("3DS Executable (%1);;All Files (*.*)", "%1 is an identifier for the 3DS executable file extensions.") .arg(extensions); const QString filename = QFileDialog::getOpenFileName( this, tr("Load File"), UISettings::values.roms_path, file_filter); if (filename.isEmpty()) { return; } UISettings::values.roms_path = QFileInfo(filename).path(); BootGame(filename); } void GMainWindow::OnMenuSetUpSystemFiles() { QDialog dialog(this); dialog.setWindowTitle(tr("Set Up System Files")); QVBoxLayout layout(&dialog); QLabel label_description( tr("

Azahar needs files from a real console to be able to use some of its features.
" "You can get such files with the Azahar " "Artic Setup Tool
Notes:

  • This operation will install console unique " "files " "to Azahar, do not share your user or nand folders
    after performing the setup " "process!
  • Old 3DS setup is needed for the New 3DS setup to " "work.
  • Both setup modes will work regardless of the model of the console " "running the setup tool.

"), &dialog); label_description.setOpenExternalLinks(true); layout.addWidget(&label_description); QHBoxLayout layout_h(&dialog); layout.addLayout(&layout_h); QLabel label_enter(tr("Enter Azahar Artic Setup Tool address:"), &dialog); layout_h.addWidget(&label_enter); QLineEdit textInput(UISettings::values.last_artic_base_addr, &dialog); layout_h.addWidget(&textInput); QLabel label_select(tr("
Choose setup mode:"), &dialog); layout.addWidget(&label_select); std::pair install_state = Core::AreSystemTitlesInstalled(); QRadioButton radio1(&dialog); QRadioButton radio2(&dialog); if (!install_state.first) { radio1.setText(tr("(\u2139\uFE0F) Old 3DS setup")); radio1.setToolTip(tr("Setup is possible.")); radio2.setText(tr("(\u26A0) New 3DS setup")); radio2.setToolTip(tr("Old 3DS setup is required first.")); radio2.setEnabled(false); } else { radio1.setText(tr("(\u2705) Old 3DS setup")); radio1.setToolTip(tr("Setup completed.")); if (!install_state.second) { radio2.setText(tr("(\u2139\uFE0F) New 3DS setup")); radio2.setToolTip(tr("Setup is possible.")); } else { radio2.setText(tr("(\u2705) New 3DS setup")); radio2.setToolTip(tr("Setup completed.")); } } radio1.setChecked(true); layout.addWidget(&radio1); layout.addWidget(&radio2); QDialogButtonBox buttonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, &dialog); connect(&buttonBox, &QDialogButtonBox::accepted, &dialog, &QDialog::accept); connect(&buttonBox, &QDialogButtonBox::rejected, &dialog, &QDialog::reject); layout.addWidget(&buttonBox); int res = dialog.exec(); if (res == QDialog::Accepted) { bool is_o3ds = radio1.isChecked(); if ((is_o3ds && install_state.first) || (!is_o3ds && install_state.second)) { QMessageBox::StandardButton answer = QMessageBox::question(this, tr("Set Up System Files"), tr("The system files for the selected mode are already set " "up.\nReinstall the files anyway?"), QMessageBox::Yes | QMessageBox::No, QMessageBox::No); if (answer != QMessageBox::Yes) { return; } } Core::UninstallSystemFiles(is_o3ds ? Core::SystemTitleSet::Old3ds : Core::SystemTitleSet::New3ds); QString addr = textInput.text(); UISettings::values.last_artic_base_addr = addr; BootGame(QString::fromStdString(is_o3ds ? "articinio://" : "articinin://").append(addr)); } } void GMainWindow::OnMenuInstallCIA() { QStringList filepaths = QFileDialog::getOpenFileNames( this, tr("Load Files"), UISettings::values.roms_path, tr("3DS Installation File (*.CIA*)") + QStringLiteral(";;") + tr("All Files (*.*)")); if (filepaths.isEmpty()) { return; } UISettings::values.roms_path = QFileInfo(filepaths[0]).path(); InstallCIA(filepaths); } void GMainWindow::OnMenuConnectArticBase() { bool ok = false; auto res = QInputDialog::getText(this, tr("Connect to Artic Base"), tr("Enter Artic Base server address:"), QLineEdit::Normal, UISettings::values.last_artic_base_addr, &ok); if (ok) { UISettings::values.last_artic_base_addr = res; BootGame(QString::fromStdString("articbase://").append(res)); } } void GMainWindow::OnMenuBootHomeMenu(u32 region) { BootGame(QString::fromStdString(Core::GetHomeMenuNcchPath(region))); } void GMainWindow::InstallCIA(QStringList filepaths) { ui->action_Install_CIA->setEnabled(false); game_list->SetDirectoryWatcherEnabled(false); progress_bar->show(); progress_bar->setMaximum(INT_MAX); (void)QtConcurrent::run([&, filepaths] { Service::AM::InstallStatus status; const auto cia_progress = [&](std::size_t written, std::size_t total) { emit UpdateProgress(written, total); }; for (const auto& current_path : filepaths) { status = Service::AM::InstallCIA(current_path.toStdString(), cia_progress); emit CIAInstallReport(status, current_path); } emit CIAInstallFinished(); }); } void GMainWindow::OnUpdateProgress(std::size_t written, std::size_t total) { progress_bar->setValue( static_cast(INT_MAX * (static_cast(written) / static_cast(total)))); } void GMainWindow::OnCIAInstallReport(Service::AM::InstallStatus status, QString filepath) { QString filename = QFileInfo(filepath).fileName(); switch (status) { case Service::AM::InstallStatus::Success: this->statusBar()->showMessage(tr("%1 has been installed successfully.").arg(filename)); break; case Service::AM::InstallStatus::ErrorFailedToOpenFile: QMessageBox::critical(this, tr("Unable to open File"), tr("Could not open %1").arg(filename)); break; case Service::AM::InstallStatus::ErrorAborted: QMessageBox::critical( this, tr("Installation aborted"), tr("The installation of %1 was aborted. Please see the log for more details") .arg(filename)); break; case Service::AM::InstallStatus::ErrorInvalid: QMessageBox::critical(this, tr("Invalid File"), tr("%1 is not a valid CIA").arg(filename)); break; case Service::AM::InstallStatus::ErrorEncrypted: QMessageBox::critical(this, tr("CIA Encrypted"), tr("Your CIA file is encrypted.
" "" "Please check our blog for more info.")); break; case Service::AM::InstallStatus::ErrorFileNotFound: QMessageBox::critical(this, tr("Unable to find File"), tr("Could not find %1").arg(filename)); break; } } void GMainWindow::OnCIAInstallFinished() { progress_bar->hide(); progress_bar->setValue(0); game_list->SetDirectoryWatcherEnabled(true); ui->action_Install_CIA->setEnabled(true); game_list->PopulateAsync(UISettings::values.game_dirs); } void GMainWindow::UninstallTitles( const std::vector>& titles) { if (titles.empty()) { return; } // Select the first title in the list as representative. const auto first_name = std::get(titles[0]); QProgressDialog progress(tr("Uninstalling '%1'...").arg(first_name), tr("Cancel"), 0, static_cast(titles.size()), this); progress.setWindowModality(Qt::WindowModal); QFutureWatcher future_watcher; QObject::connect(&future_watcher, &QFutureWatcher::finished, &progress, &QProgressDialog::reset); QObject::connect(&progress, &QProgressDialog::canceled, &future_watcher, &QFutureWatcher::cancel); QObject::connect(&future_watcher, &QFutureWatcher::progressValueChanged, &progress, &QProgressDialog::setValue); auto failed = false; QString failed_name; const auto uninstall_title = [&future_watcher, &failed, &failed_name](const auto& title) { const auto name = std::get(title); const auto media_type = std::get(title); const auto program_id = std::get(title); const auto result = Service::AM::UninstallProgram(media_type, program_id); if (result.IsError()) { LOG_ERROR(Frontend, "Failed to uninstall '{}': 0x{:08X}", name.toStdString(), result.raw); failed = true; failed_name = name; future_watcher.cancel(); } }; future_watcher.setFuture(QtConcurrent::map(titles, uninstall_title)); progress.exec(); future_watcher.waitForFinished(); if (failed) { QMessageBox::critical(this, tr("Azahar"), tr("Failed to uninstall '%1'.").arg(failed_name)); } else if (!future_watcher.isCanceled()) { QMessageBox::information(this, tr("Azahar"), tr("Successfully uninstalled '%1'.").arg(first_name)); } } void GMainWindow::OnMenuRecentFile() { QAction* action = qobject_cast(sender()); ASSERT(action); const QString filename = action->data().toString(); if (QFileInfo::exists(filename)) { BootGame(filename); } else { // Display an error message and remove the file from the list. QMessageBox::information(this, tr("File not found"), tr("File \"%1\" not found").arg(filename)); UISettings::values.recent_files.removeOne(filename); UpdateRecentFiles(); } } void GMainWindow::OnStartGame() { qt_cameras->ResumeCameras(); PreventOSSleep(); emu_thread->SetRunning(true); system.frame_limiter.SetFrameAdvancing(false); graphics_api_button->setEnabled(false); qRegisterMetaType("Core::System::ResultStatus"); qRegisterMetaType("std::string"); connect(emu_thread.get(), &EmuThread::ErrorThrown, this, &GMainWindow::OnCoreError); UpdateMenuState(); play_time_manager->SetProgramId(game_title_id); play_time_manager->Start(); discord_rpc->Update(); #ifdef __unix__ Common::Linux::StartGamemode(); #endif UpdateSaveStates(); UpdateStatusButtons(); } void GMainWindow::OnRestartGame() { if (!system.IsPoweredOn()) { return; } // Make a copy since BootGame edits game_path BootGame(QString(game_path)); } void GMainWindow::OnPauseGame() { system.frame_limiter.SetFrameAdvancing(true); qt_cameras->PauseCameras(); play_time_manager->Stop(); UpdateMenuState(); AllowOSSleep(); #ifdef __unix__ Common::Linux::StopGamemode(); #endif } void GMainWindow::OnPauseContinueGame() { if (emulation_running) { if (emu_thread->IsRunning() && !system.frame_limiter.IsFrameAdvancing()) { OnPauseGame(); } else { OnStartGame(); } } } void GMainWindow::OnStopGame() { play_time_manager->Stop(); // Update game list to show new play time game_list->PopulateAsync(UISettings::values.game_dirs); ShutdownGame(); graphics_api_button->setEnabled(true); Settings::RestoreGlobalState(false); UpdateStatusButtons(); } void GMainWindow::OnLoadComplete() { loading_screen->OnLoadComplete(); UpdateSecondaryWindowVisibility(); } void GMainWindow::OnMenuReportCompatibility() { // NoOp } void GMainWindow::ToggleFullscreen() { if (!emulation_running) { return; } if (ui->action_Fullscreen->isChecked()) { ShowFullscreen(); } else { HideFullscreen(); } } void GMainWindow::ToggleSecondaryFullscreen() { if (!emulation_running) { return; } if (secondary_window->isFullScreen()) { secondary_window->showNormal(); } else { secondary_window->showFullScreen(); } } void GMainWindow::ShowFullscreen() { if (ui->action_Single_Window_Mode->isChecked()) { UISettings::values.geometry = saveGeometry(); ui->menubar->hide(); statusBar()->hide(); showFullScreen(); } else { UISettings::values.renderwindow_geometry = render_window->saveGeometry(); render_window->showFullScreen(); } } void GMainWindow::HideFullscreen() { if (ui->action_Single_Window_Mode->isChecked()) { statusBar()->setVisible(ui->action_Show_Status_Bar->isChecked()); ui->menubar->show(); showNormal(); restoreGeometry(UISettings::values.geometry); } else { render_window->showNormal(); render_window->restoreGeometry(UISettings::values.renderwindow_geometry); } } void GMainWindow::ToggleWindowMode() { if (ui->action_Single_Window_Mode->isChecked()) { // Render in the main window... render_window->BackupGeometry(); ui->horizontalLayout->addWidget(render_window); render_window->setFocusPolicy(Qt::StrongFocus); if (emulation_running) { render_window->setVisible(true); render_window->setFocus(); game_list->hide(); } } else { // Render in a separate window... ui->horizontalLayout->removeWidget(render_window); render_window->setParent(nullptr); render_window->setFocusPolicy(Qt::NoFocus); if (emulation_running) { render_window->setVisible(true); render_window->RestoreGeometry(); game_list->show(); } } } void GMainWindow::UpdateSecondaryWindowVisibility() { if (!emulation_running) { return; } if (Settings::values.layout_option.GetValue() == Settings::LayoutOption::SeparateWindows) { secondary_window->RestoreGeometry(); secondary_window->show(); } else { secondary_window->BackupGeometry(); secondary_window->hide(); } } void GMainWindow::ChangeScreenLayout() { Settings::LayoutOption new_layout = Settings::LayoutOption::Default; if (ui->action_Screen_Layout_Default->isChecked()) { new_layout = Settings::LayoutOption::Default; } else if (ui->action_Screen_Layout_Single_Screen->isChecked()) { new_layout = Settings::LayoutOption::SingleScreen; } else if (ui->action_Screen_Layout_Large_Screen->isChecked()) { new_layout = Settings::LayoutOption::LargeScreen; ui->menu_Small_Screen_Position->setEnabled(true); } else if (ui->action_Screen_Layout_Hybrid_Screen->isChecked()) { new_layout = Settings::LayoutOption::HybridScreen; } else if (ui->action_Screen_Layout_Side_by_Side->isChecked()) { new_layout = Settings::LayoutOption::SideScreen; } else if (ui->action_Screen_Layout_Separate_Windows->isChecked()) { new_layout = Settings::LayoutOption::SeparateWindows; } else if (ui->action_Screen_Layout_Custom_Layout->isChecked()) { new_layout = Settings::LayoutOption::CustomLayout; } Settings::values.layout_option = new_layout; SyncMenuUISettings(); system.ApplySettings(); UpdateSecondaryWindowVisibility(); } void GMainWindow::ChangeSmallScreenPosition() { Settings::SmallScreenPosition new_position = Settings::SmallScreenPosition::BottomRight; if (ui->action_Small_Screen_TopRight->isChecked()) { new_position = Settings::SmallScreenPosition::TopRight; } else if (ui->action_Small_Screen_MiddleRight->isChecked()) { new_position = Settings::SmallScreenPosition::MiddleRight; } else if (ui->action_Small_Screen_BottomRight->isChecked()) { new_position = Settings::SmallScreenPosition::BottomRight; } else if (ui->action_Small_Screen_TopLeft->isChecked()) { new_position = Settings::SmallScreenPosition::TopLeft; } else if (ui->action_Small_Screen_MiddleLeft->isChecked()) { new_position = Settings::SmallScreenPosition::MiddleLeft; } else if (ui->action_Small_Screen_BottomLeft->isChecked()) { new_position = Settings::SmallScreenPosition::BottomLeft; } else if (ui->action_Small_Screen_Above->isChecked()) { new_position = Settings::SmallScreenPosition::AboveLarge; } else if (ui->action_Small_Screen_Below->isChecked()) { new_position = Settings::SmallScreenPosition::BelowLarge; } Settings::values.small_screen_position = new_position; SyncMenuUISettings(); system.ApplySettings(); UpdateSecondaryWindowVisibility(); } void GMainWindow::ToggleScreenLayout() { const Settings::LayoutOption new_layout = []() { switch (Settings::values.layout_option.GetValue()) { case Settings::LayoutOption::Default: return Settings::LayoutOption::SingleScreen; case Settings::LayoutOption::SingleScreen: return Settings::LayoutOption::LargeScreen; case Settings::LayoutOption::LargeScreen: return Settings::LayoutOption::HybridScreen; case Settings::LayoutOption::HybridScreen: return Settings::LayoutOption::SideScreen; case Settings::LayoutOption::SideScreen: return Settings::LayoutOption::SeparateWindows; case Settings::LayoutOption::SeparateWindows: return Settings::LayoutOption::CustomLayout; case Settings::LayoutOption::CustomLayout: return Settings::LayoutOption::Default; default: LOG_ERROR(Frontend, "Unknown layout option {}", Settings::values.layout_option.GetValue()); return Settings::LayoutOption::Default; } }(); Settings::values.layout_option = new_layout; SyncMenuUISettings(); system.ApplySettings(); UpdateSecondaryWindowVisibility(); } void GMainWindow::OnSwapScreens() { Settings::values.swap_screen = ui->action_Screen_Layout_Swap_Screens->isChecked(); system.ApplySettings(); } void GMainWindow::OnRotateScreens() { Settings::values.upright_screen = ui->action_Screen_Layout_Upright_Screens->isChecked(); system.ApplySettings(); } void GMainWindow::TriggerSwapScreens() { ui->action_Screen_Layout_Swap_Screens->trigger(); } void GMainWindow::TriggerRotateScreens() { ui->action_Screen_Layout_Upright_Screens->trigger(); } void GMainWindow::OnSaveState() { QAction* action = qobject_cast(sender()); ASSERT(action); system.SendSignal(Core::System::Signal::Save, action->data().toUInt()); system.frame_limiter.AdvanceFrame(); newest_slot = action->data().toUInt(); } void GMainWindow::OnLoadState() { QAction* action = qobject_cast(sender()); ASSERT(action); if (UISettings::values.save_state_warning) { QMessageBox::warning( this, tr("Savestates"), tr("Warning: Savestates are NOT a replacement for in-application saves, " "and are not meant to be reliable.\n\nUse at your own risk!")); UISettings::values.save_state_warning = false; config->Save(); } system.SendSignal(Core::System::Signal::Load, action->data().toUInt()); system.frame_limiter.AdvanceFrame(); } void GMainWindow::OnConfigure() { game_list->SetDirectoryWatcherEnabled(false); Settings::SetConfiguringGlobal(true); ConfigureDialog configureDialog(this, hotkey_registry, system, gl_renderer, physical_devices, !multiplayer_state->IsHostingPublicRoom()); connect(&configureDialog, &ConfigureDialog::LanguageChanged, this, &GMainWindow::OnLanguageChanged); auto old_theme = UISettings::values.theme; const int old_input_profile_index = Settings::values.current_input_profile_index; const auto old_input_profiles = Settings::values.input_profiles; const auto old_touch_from_button_maps = Settings::values.touch_from_button_maps; const bool old_discord_presence = UISettings::values.enable_discord_presence.GetValue(); #ifdef __unix__ const bool old_gamemode = Settings::values.enable_gamemode.GetValue(); #endif auto result = configureDialog.exec(); game_list->SetDirectoryWatcherEnabled(true); if (result == QDialog::Accepted) { configureDialog.ApplyConfiguration(); InitializeHotkeys(); if (UISettings::values.theme != old_theme) { UpdateUITheme(); } if (UISettings::values.enable_discord_presence.GetValue() != old_discord_presence) { SetDiscordEnabled(UISettings::values.enable_discord_presence.GetValue()); } #ifdef __unix__ if (Settings::values.enable_gamemode.GetValue() != old_gamemode) { SetGamemodeEnabled(Settings::values.enable_gamemode.GetValue()); } #endif if (!multiplayer_state->IsHostingPublicRoom()) multiplayer_state->UpdateCredentials(); emit UpdateThemedIcons(); SyncMenuUISettings(); game_list->RefreshGameDirectory(); config->Save(); if (UISettings::values.hide_mouse && emulation_running) { setMouseTracking(true); mouse_hide_timer.start(); } else { setMouseTracking(false); } UpdateSecondaryWindowVisibility(); UpdateBootHomeMenuState(); UpdateStatusButtons(); } else { Settings::values.input_profiles = old_input_profiles; Settings::values.touch_from_button_maps = old_touch_from_button_maps; Settings::LoadProfile(old_input_profile_index); } } void GMainWindow::OnLoadAmiibo() { if (!emu_thread || !emu_thread->IsRunning()) [[unlikely]] { return; } Service::SM::ServiceManager& sm = system.ServiceManager(); auto nfc = sm.GetService("nfc:u"); if (nfc == nullptr) { return; } std::scoped_lock lock{system.Kernel().GetHLELock()}; if (nfc->IsTagActive()) { QMessageBox::warning(this, tr("Error opening amiibo data file"), tr("A tag is already in use.")); return; } if (!nfc->IsSearchingForAmiibos()) { QMessageBox::warning(this, tr("Error opening amiibo data file"), tr("Application is not looking for amiibos.")); return; } const QString extensions{QStringLiteral("*.bin")}; const QString file_filter = tr("Amiibo File (%1);; All Files (*.*)").arg(extensions); const QString filename = QFileDialog::getOpenFileName(this, tr("Load Amiibo"), {}, file_filter); if (filename.isEmpty()) { return; } LoadAmiibo(filename); } void GMainWindow::LoadAmiibo(const QString& filename) { Service::SM::ServiceManager& sm = system.ServiceManager(); auto nfc = sm.GetService("nfc:u"); if (!nfc) [[unlikely]] { return; } std::scoped_lock lock{system.Kernel().GetHLELock()}; if (!nfc->LoadAmiibo(filename.toStdString())) { QMessageBox::warning(this, tr("Error opening amiibo data file"), tr("Unable to open amiibo file \"%1\" for reading.").arg(filename)); return; } ui->action_Remove_Amiibo->setEnabled(true); } void GMainWindow::OnRemoveAmiibo() { Service::SM::ServiceManager& sm = system.ServiceManager(); auto nfc = sm.GetService("nfc:u"); if (!nfc) [[unlikely]] { return; } std::scoped_lock lock{system.Kernel().GetHLELock()}; nfc->RemoveAmiibo(); ui->action_Remove_Amiibo->setEnabled(false); } void GMainWindow::OnOpenCitraFolder() { QDesktopServices::openUrl(QUrl::fromLocalFile( QString::fromStdString(FileUtil::GetUserPath(FileUtil::UserPath::UserDir)))); } void GMainWindow::OnToggleFilterBar() { game_list->SetFilterVisible(ui->action_Show_Filter_Bar->isChecked()); if (ui->action_Show_Filter_Bar->isChecked()) { game_list->SetFilterFocus(); } else { game_list->ClearFilter(); } } void GMainWindow::OnCreateGraphicsSurfaceViewer() { auto graphicsSurfaceViewerWidget = new GraphicsSurfaceWidget(system, Pica::g_debug_context, this); addDockWidget(Qt::RightDockWidgetArea, graphicsSurfaceViewerWidget); // TODO: Maybe graphicsSurfaceViewerWidget->setFloating(true); graphicsSurfaceViewerWidget->show(); } void GMainWindow::OnRecordMovie() { MovieRecordDialog dialog(this, system); if (dialog.exec() != QDialog::Accepted) { return; } movie_record_on_start = true; movie_record_path = dialog.GetPath(); movie_record_author = dialog.GetAuthor(); if (emulation_running) { // Restart game BootGame(QString(game_path)); } ui->action_Close_Movie->setEnabled(true); ui->action_Save_Movie->setEnabled(true); } void GMainWindow::OnPlayMovie() { MoviePlayDialog dialog(this, game_list, system); if (dialog.exec() != QDialog::Accepted) { return; } movie_playback_on_start = true; movie_playback_path = dialog.GetMoviePath(); BootGame(dialog.GetGamePath()); ui->action_Close_Movie->setEnabled(true); ui->action_Save_Movie->setEnabled(false); } void GMainWindow::OnCloseMovie() { if (movie_record_on_start) { QMessageBox::information(this, tr("Record Movie"), tr("Movie recording cancelled.")); movie_record_on_start = false; movie_record_path.clear(); movie_record_author.clear(); } else { const bool was_running = emu_thread && emu_thread->IsRunning(); if (was_running) { OnPauseGame(); } const bool was_recording = movie.GetPlayMode() == Core::Movie::PlayMode::Recording; movie.Shutdown(); if (was_recording) { QMessageBox::information(this, tr("Movie Saved"), tr("The movie is successfully saved.")); } if (was_running) { OnStartGame(); } } ui->action_Close_Movie->setEnabled(false); ui->action_Save_Movie->setEnabled(false); } void GMainWindow::OnSaveMovie() { const bool was_running = emu_thread && emu_thread->IsRunning(); if (was_running) { OnPauseGame(); } if (movie.GetPlayMode() == Core::Movie::PlayMode::Recording) { movie.SaveMovie(); QMessageBox::information(this, tr("Movie Saved"), tr("The movie is successfully saved.")); } else { LOG_ERROR(Frontend, "Tried to save movie while movie is not being recorded"); } if (was_running) { OnStartGame(); } } void GMainWindow::OnCaptureScreenshot() { if (!emu_thread) [[unlikely]] { return; } const bool was_running = emu_thread->IsRunning(); if (was_running || (QMessageBox::question(this, tr("Application will unpause"), tr("The application will be unpaused, and the next " "frame will be captured. Is this okay?"), QMessageBox::Yes | QMessageBox::No, QMessageBox::No) == QMessageBox::Yes)) { if (was_running) { OnPauseGame(); } std::string path = UISettings::values.screenshot_path.GetValue(); if (!FileUtil::IsDirectory(path)) { if (!FileUtil::CreateFullPath(path)) { QMessageBox::information( this, tr("Invalid Screenshot Directory"), tr("Cannot create specified screenshot directory. Screenshot " "path is set back to its default value.")); path = FileUtil::GetUserPath(FileUtil::UserPath::UserDir); path.append("screenshots/"); UISettings::values.screenshot_path = path; }; } static QRegularExpression expr(QStringLiteral("[\\/:?\"<>|]")); const std::string filename = game_title.remove(expr).toStdString(); const std::string timestamp = QDateTime::currentDateTime() .toString(QStringLiteral("dd.MM.yy_hh.mm.ss.z")) .toStdString(); path.append(fmt::format("/{}_{}.png", filename, timestamp)); auto* const screenshot_window = secondary_window->HasFocus() ? secondary_window : render_window; screenshot_window->CaptureScreenshot( UISettings::values.screenshot_resolution_factor.GetValue(), QString::fromStdString(path)); OnStartGame(); } } void GMainWindow::ShowFFmpegErrorMessage() { QMessageBox message_box; message_box.setWindowTitle(tr("Could not load video dumper")); message_box.setText( tr("FFmpeg could not be loaded. Make sure you have a compatible version installed." #ifdef _WIN32 "\n\nTo install FFmpeg to Lime, press Open and select your FFmpeg directory." #endif "\n\nTo view a guide on how to install FFmpeg, press Help.")); message_box.setStandardButtons(QMessageBox::Ok | QMessageBox::Help #ifdef _WIN32 | QMessageBox::Open #endif ); auto result = message_box.exec(); if (result == QMessageBox::Help) { QDesktopServices::openUrl( QUrl(QStringLiteral("https://web.archive.org/web/20240301121456/https://" "citra-emu.org/wiki/installing-ffmpeg-for-the-video-dumper/"))); #ifdef _WIN32 } else if (result == QMessageBox::Open) { OnOpenFFmpeg(); #endif } } void GMainWindow::OnDumpVideo() { if (DynamicLibrary::FFmpeg::LoadFFmpeg()) { if (ui->action_Dump_Video->isChecked()) { OnStartVideoDumping(); } else { OnStopVideoDumping(); } } else { ui->action_Dump_Video->setChecked(false); ShowFFmpegErrorMessage(); } } #ifdef _WIN32 void GMainWindow::OnOpenFFmpeg() { auto filename = QFileDialog::getExistingDirectory(this, tr("Select FFmpeg Directory")).toStdString(); if (filename.empty()) { return; } // Check for a bin directory if they chose the FFmpeg root directory. auto bin_dir = filename + DIR_SEP + "bin"; if (!FileUtil::Exists(bin_dir)) { // Otherwise, assume the user directly selected the directory containing the DLLs. bin_dir = filename; } static const std::array library_names = { Common::DynamicLibrary::GetLibraryName("avcodec", LIBAVCODEC_VERSION_MAJOR), Common::DynamicLibrary::GetLibraryName("avfilter", LIBAVFILTER_VERSION_MAJOR), Common::DynamicLibrary::GetLibraryName("avformat", LIBAVFORMAT_VERSION_MAJOR), Common::DynamicLibrary::GetLibraryName("avutil", LIBAVUTIL_VERSION_MAJOR), Common::DynamicLibrary::GetLibraryName("swresample", LIBSWRESAMPLE_VERSION_MAJOR), }; for (auto& library_name : library_names) { if (!FileUtil::Exists(bin_dir + DIR_SEP + library_name)) { QMessageBox::critical(this, tr("Azahar"), tr("The provided FFmpeg directory is missing %1. Please make " "sure the correct directory was selected.") .arg(QString::fromStdString(library_name))); return; } } std::atomic success(true); auto process_file = [&success](u64* num_entries_out, const std::string& directory, const std::string& virtual_name) -> bool { auto file_path = directory + DIR_SEP + virtual_name; if (file_path.ends_with(".dll")) { auto destination_path = FileUtil::GetExeDirectory() + DIR_SEP + virtual_name; if (!FileUtil::Copy(file_path, destination_path)) { success.store(false); return false; } } return true; }; FileUtil::ForeachDirectoryEntry(nullptr, bin_dir, process_file); if (success.load()) { QMessageBox::information(this, tr("Azahar"), tr("FFmpeg has been sucessfully installed.")); } else { QMessageBox::critical(this, tr("Azahar"), tr("Installation of FFmpeg failed. Check the log file for details.")); } } #endif void GMainWindow::OnStartVideoDumping() { DumpingDialog dialog(this, system); if (dialog.exec() != QDialog::DialogCode::Accepted) { ui->action_Dump_Video->setChecked(false); return; } const auto path = dialog.GetFilePath(); if (emulation_running) { StartVideoDumping(path); } else { video_dumping_on_start = true; video_dumping_path = path; } } void GMainWindow::StartVideoDumping(const QString& path) { auto& renderer = system.GPU().Renderer(); const auto layout{Layout::FrameLayoutFromResolutionScale(renderer.GetResolutionScaleFactor())}; auto dumper = std::make_shared(renderer); if (dumper->StartDumping(path.toStdString(), layout)) { system.RegisterVideoDumper(dumper); } else { QMessageBox::critical( this, tr("Azahar"), tr("Could not start video dumping.
Please ensure that the video encoder is " "configured correctly.
Refer to the log for details.")); ui->action_Dump_Video->setChecked(false); } } void GMainWindow::OnStopVideoDumping() { ui->action_Dump_Video->setChecked(false); if (video_dumping_on_start) { video_dumping_on_start = false; video_dumping_path.clear(); } else { auto dumper = system.GetVideoDumper(); if (!dumper || !dumper->IsDumping()) { return; } game_paused_for_dumping = emu_thread->IsRunning(); OnPauseGame(); auto future = QtConcurrent::run([dumper] { dumper->StopDumping(); }); auto* future_watcher = new QFutureWatcher(this); connect(future_watcher, &QFutureWatcher::finished, this, [this] { if (game_shutdown_delayed) { game_shutdown_delayed = false; ShutdownGame(); } else if (game_paused_for_dumping) { game_paused_for_dumping = false; OnStartGame(); } }); future_watcher->setFuture(future); } } void GMainWindow::UpdateStatusBar() { if (!emu_thread) [[unlikely]] { status_bar_update_timer.stop(); return; } // Update movie status const u64 current = movie.GetCurrentInputIndex(); const u64 total = movie.GetTotalInputCount(); const auto play_mode = movie.GetPlayMode(); if (play_mode == Core::Movie::PlayMode::Recording) { message_label->setText(tr("Recording %1").arg(current)); message_label_used_for_movie = true; ui->action_Save_Movie->setEnabled(true); } else if (play_mode == Core::Movie::PlayMode::Playing) { message_label->setText(tr("Playing %1 / %2").arg(current).arg(total)); message_label_used_for_movie = true; ui->action_Save_Movie->setEnabled(false); } else if (play_mode == Core::Movie::PlayMode::MovieFinished) { message_label->setText(tr("Movie Finished")); message_label_used_for_movie = true; ui->action_Save_Movie->setEnabled(false); } else if (message_label_used_for_movie) { // Clear the label if movie was just closed message_label->setText(QString{}); message_label_used_for_movie = false; ui->action_Save_Movie->setEnabled(false); } auto results = system.GetAndResetPerfStats(); if (show_artic_label) { const bool do_mb = results.artic_transmitted >= (1000.0 * 1000.0); const double value = do_mb ? (results.artic_transmitted / (1000.0 * 1000.0)) : (results.artic_transmitted / 1000.0); static const std::array, 5> perf_events = { std::make_pair(Core::PerfStats::PerfArticEventBits::ARTIC_SHARED_EXT_DATA, tr("(Accessing SharedExtData)")), std::make_pair(Core::PerfStats::PerfArticEventBits::ARTIC_SYSTEM_SAVE_DATA, tr("(Accessing SystemSaveData)")), std::make_pair(Core::PerfStats::PerfArticEventBits::ARTIC_BOSS_EXT_DATA, tr("(Accessing BossExtData)")), std::make_pair(Core::PerfStats::PerfArticEventBits::ARTIC_EXT_DATA, tr("(Accessing ExtData)")), std::make_pair(Core::PerfStats::PerfArticEventBits::ARTIC_SAVE_DATA, tr("(Accessing SaveData)")), }; const QString unit = do_mb ? tr("MB/s") : tr("KB/s"); QString event{}; for (auto p : perf_events) { if (results.artic_events.Get(p.first)) { event = QString::fromStdString(" ") + p.second; break; } } static const std::array label_color = {QStringLiteral("#ffffff"), QStringLiteral("#eed202"), QStringLiteral("#ff3333")}; int style_index; if (value > 200.0) { style_index = 2; } else if (value > 125.0) { style_index = 1; } else { style_index = 0; } const QString style_sheet = QStringLiteral("QLabel { color: %0; }").arg(label_color[style_index]); artic_traffic_label->setText( tr("Artic Traffic: %1 %2%3").arg(value, 0, 'f', 0).arg(unit).arg(event)); artic_traffic_label->setStyleSheet(style_sheet); } if (Settings::GetFrameLimit() == 0) { emu_speed_label->setText(tr("Speed: %1%").arg(results.emulation_speed * 100.0, 0, 'f', 0)); } else { emu_speed_label->setText(tr("Speed: %1% / %2%") .arg(results.emulation_speed * 100.0, 0, 'f', 0) .arg(Settings::GetFrameLimit())); } game_fps_label->setText(tr("App: %1 FPS").arg(results.game_fps, 0, 'f', 0)); emu_frametime_label->setText(tr("Frame: %1 ms").arg(results.frametime * 1000.0, 0, 'f', 2)); if (show_artic_label) { artic_traffic_label->setVisible(true); } emu_speed_label->setVisible(true); game_fps_label->setVisible(true); emu_frametime_label->setVisible(true); } void GMainWindow::UpdateBootHomeMenuState() { const auto current_region = Settings::values.region_value.GetValue(); for (u32 region = 0; region < Core::NUM_SYSTEM_TITLE_REGIONS; region++) { const auto path = Core::GetHomeMenuNcchPath(region); ui->menu_Boot_Home_Menu->actions().at(region)->setEnabled( (current_region == Settings::REGION_VALUE_AUTO_SELECT || current_region == static_cast(region)) && !path.empty() && FileUtil::Exists(path)); } } void GMainWindow::HideMouseCursor() { if (!emu_thread || !UISettings::values.hide_mouse.GetValue()) { mouse_hide_timer.stop(); ShowMouseCursor(); return; } render_window->setCursor(QCursor(Qt::BlankCursor)); secondary_window->setCursor(QCursor(Qt::BlankCursor)); if (UISettings::values.single_window_mode.GetValue()) { setCursor(QCursor(Qt::BlankCursor)); } } void GMainWindow::ShowMouseCursor() { unsetCursor(); render_window->unsetCursor(); secondary_window->unsetCursor(); if (emu_thread && UISettings::values.hide_mouse) { mouse_hide_timer.start(); } } void GMainWindow::OnMute() { Settings::values.audio_muted = !Settings::values.audio_muted; UpdateVolumeUI(); } void GMainWindow::OnDecreaseVolume() { Settings::values.audio_muted = false; const auto current_volume = static_cast(Settings::values.volume.GetValue() * volume_slider->maximum()); int step = 5; if (current_volume <= 30) { step = 2; } if (current_volume <= 6) { step = 1; } const auto value = static_cast(std::max(current_volume - step, 0)) / volume_slider->maximum(); Settings::values.volume.SetValue(value); UpdateVolumeUI(); } void GMainWindow::OnIncreaseVolume() { Settings::values.audio_muted = false; const auto current_volume = static_cast(Settings::values.volume.GetValue() * volume_slider->maximum()); int step = 5; if (current_volume < 30) { step = 2; } if (current_volume < 6) { step = 1; } const auto value = static_cast(current_volume + step) / volume_slider->maximum(); Settings::values.volume.SetValue(value); UpdateVolumeUI(); } void GMainWindow::UpdateVolumeUI() { const auto volume_value = static_cast(Settings::values.volume.GetValue() * volume_slider->maximum()); volume_slider->setValue(volume_value); if (Settings::values.audio_muted) { volume_button->setChecked(false); volume_button->setText(tr("VOLUME: MUTE")); } else { volume_button->setChecked(true); volume_button->setText(tr("VOLUME: %1%", "Volume percentage (e.g. 50%)").arg(volume_value)); } } void GMainWindow::UpdateAPIIndicator(bool update) { static std::array graphics_apis = {QStringLiteral("SOFTWARE"), QStringLiteral("OPENGL"), QStringLiteral("VULKAN")}; static std::array graphics_api_colors = {QStringLiteral("#3ae400"), QStringLiteral("#00ccdd"), QStringLiteral("#91242a")}; u32 api_index = static_cast(Settings::values.graphics_api.GetValue()); if (update) { api_index = (api_index + 1) % graphics_apis.size(); // Skip past any disabled renderers. #ifndef ENABLE_SOFTWARE_RENDERER if (api_index == static_cast(Settings::GraphicsAPI::Software)) { api_index = (api_index + 1) % graphics_apis.size(); } #endif #ifndef ENABLE_OPENGL if (api_index == static_cast(Settings::GraphicsAPI::OpenGL)) { api_index = (api_index + 1) % graphics_apis.size(); } #endif #ifndef ENABLE_VULKAN if (api_index == static_cast(Settings::GraphicsAPI::Vulkan)) { api_index = (api_index + 1) % graphics_apis.size(); } #endif Settings::values.graphics_api = static_cast(api_index); } const QString style_sheet = QStringLiteral("QPushButton { font-weight: bold; color: %0; }") .arg(graphics_api_colors[api_index]); graphics_api_button->setText(graphics_apis[api_index]); graphics_api_button->setStyleSheet(style_sheet); } void GMainWindow::UpdateStatusButtons() { UpdateAPIIndicator(); UpdateVolumeUI(); } void GMainWindow::OnMouseActivity() { ShowMouseCursor(); } void GMainWindow::mouseMoveEvent([[maybe_unused]] QMouseEvent* event) { OnMouseActivity(); } void GMainWindow::mousePressEvent([[maybe_unused]] QMouseEvent* event) { OnMouseActivity(); } void GMainWindow::mouseReleaseEvent([[maybe_unused]] QMouseEvent* event) { OnMouseActivity(); } void GMainWindow::OnCoreError(Core::System::ResultStatus result, std::string details) { QString status_message; QString title, message; QMessageBox::Icon error_severity_icon; bool can_continue = true; if (result == Core::System::ResultStatus::ErrorSystemFiles) { const QString common_message = tr("%1 is missing. Please dump your " "system archives.
Continuing emulation may result in crashes and bugs."); if (!details.empty()) { message = common_message.arg(QString::fromStdString(details)); } else { message = common_message.arg(tr("A system archive")); } title = tr("System Archive Not Found"); status_message = tr("System Archive Missing"); error_severity_icon = QMessageBox::Icon::Critical; } else if (result == Core::System::ResultStatus::ErrorSavestate) { title = tr("Save/load Error"); message = QString::fromStdString(details); error_severity_icon = QMessageBox::Icon::Warning; } else if (result == Core::System::ResultStatus::ErrorArticDisconnected) { title = tr("Artic Server"); message = tr(fmt::format("A communication error has occurred. The game will quit.\n{}", details) .c_str()); error_severity_icon = QMessageBox::Icon::Critical; can_continue = false; } else { title = tr("Fatal Error"); message = tr("A fatal error occurred. " "Check " "the log for details." "
Continuing emulation may result in crashes and bugs."); status_message = tr("Fatal Error encountered"); error_severity_icon = QMessageBox::Icon::Critical; } QMessageBox message_box; message_box.setWindowTitle(title); message_box.setText(message); message_box.setIcon(error_severity_icon); if (error_severity_icon == QMessageBox::Icon::Critical) { if (can_continue) { message_box.addButton(tr("Continue"), QMessageBox::RejectRole); } QPushButton* abort_button = message_box.addButton(tr("Quit Application"), QMessageBox::AcceptRole); if (result != Core::System::ResultStatus::ShutdownRequested) message_box.exec(); if (!can_continue || result == Core::System::ResultStatus::ShutdownRequested || message_box.clickedButton() == abort_button) { if (emu_thread) { ShutdownGame(); return; } } } else { // This block should run when the error isn't too big of a deal // e.g. when a save state can't be saved or loaded message_box.addButton(tr("OK"), QMessageBox::RejectRole); message_box.exec(); } // Only show the message if the game is still running. if (emu_thread) { emu_thread->SetRunning(true); message_label->setText(status_message); message_label_used_for_movie = false; } } void GMainWindow::OnMenuAboutCitra() { AboutDialog about{this}; about.exec(); } bool GMainWindow::ConfirmClose() { if (!emu_thread || !UISettings::values.confirm_before_closing) { return true; } QMessageBox::StandardButton answer = QMessageBox::question(this, tr("Azahar"), tr("Would you like to exit now?"), QMessageBox::Yes | QMessageBox::No, QMessageBox::No); return answer != QMessageBox::No; } void GMainWindow::closeEvent(QCloseEvent* event) { if (!ConfirmClose()) { event->ignore(); return; } UpdateUISettings(); game_list->SaveInterfaceLayout(); hotkey_registry.SaveHotkeys(); // Shutdown session if the emu thread is active... if (emu_thread) { ShutdownGame(); } render_window->close(); secondary_window->close(); multiplayer_state->Close(); InputCommon::Shutdown(); QWidget::closeEvent(event); } static bool IsSingleFileDropEvent(const QMimeData* mime) { return mime->hasUrls() && mime->urls().length() == 1; } static const std::array AcceptedExtensions = {"cci", "cxi", "bin", "3dsx", "app", "elf", "axf"}; static bool IsCorrectFileExtension(const QMimeData* mime) { const QString& filename = mime->urls().at(0).toLocalFile(); return std::find(AcceptedExtensions.begin(), AcceptedExtensions.end(), QFileInfo(filename).suffix().toStdString()) != AcceptedExtensions.end(); } static bool IsAcceptableDropEvent(QDropEvent* event) { return IsSingleFileDropEvent(event->mimeData()) && IsCorrectFileExtension(event->mimeData()); } void GMainWindow::AcceptDropEvent(QDropEvent* event) { if (IsAcceptableDropEvent(event)) { event->setDropAction(Qt::DropAction::LinkAction); event->accept(); } } bool GMainWindow::DropAction(QDropEvent* event) { if (!IsAcceptableDropEvent(event)) { return false; } const QMimeData* mime_data = event->mimeData(); const QString& filename = mime_data->urls().at(0).toLocalFile(); if (emulation_running && QFileInfo(filename).suffix() == QStringLiteral("bin")) { // Amiibo LoadAmiibo(filename); } else { // Game if (ConfirmChangeGame()) { BootGame(filename); } } return true; } void GMainWindow::OnFileOpen(const QFileOpenEvent* event) { BootGame(event->file()); } void GMainWindow::dropEvent(QDropEvent* event) { DropAction(event); } void GMainWindow::dragEnterEvent(QDragEnterEvent* event) { AcceptDropEvent(event); } void GMainWindow::dragMoveEvent(QDragMoveEvent* event) { AcceptDropEvent(event); } bool GMainWindow::ConfirmChangeGame() { if (!emu_thread) [[unlikely]] { return true; } auto answer = QMessageBox::question( this, tr("Azahar"), tr("The application is still running. Would you like to stop emulation?"), QMessageBox::Yes | QMessageBox::No, QMessageBox::No); return answer != QMessageBox::No; } void GMainWindow::filterBarSetChecked(bool state) { ui->action_Show_Filter_Bar->setChecked(state); emit(OnToggleFilterBar()); } void GMainWindow::UpdateUITheme() { const QString icons_base_path = QStringLiteral(":/icons/"); const QString default_theme = QStringLiteral("default"); const QString default_theme_path = icons_base_path + default_theme; const QString& current_theme = UISettings::values.theme; const bool is_default_theme = current_theme == QString::fromUtf8(UISettings::themes[0].second); QStringList theme_paths(default_theme_paths); if (is_default_theme || current_theme.isEmpty()) { const QString theme_uri(QStringLiteral(":default/style.qss")); QFile f(theme_uri); if (f.open(QFile::ReadOnly | QFile::Text)) { QTextStream ts(&f); qApp->setStyleSheet(ts.readAll()); setStyleSheet(ts.readAll()); } else { LOG_ERROR(Frontend, "Unable to open default stylesheet, falling back to empty stylesheet"); qApp->setStyleSheet({}); setStyleSheet({}); } theme_paths.append(default_theme_path); QIcon::setThemeName(default_theme); } else { const QString theme_uri(QLatin1Char{':'} + current_theme + QStringLiteral("/style.qss")); QFile f(theme_uri); if (f.open(QFile::ReadOnly | QFile::Text)) { QTextStream ts(&f); qApp->setStyleSheet(ts.readAll()); setStyleSheet(ts.readAll()); } else { LOG_ERROR(Frontend, "Unable to set style, stylesheet file not found"); } const QString current_theme_path = icons_base_path + current_theme; theme_paths.append({default_theme_path, current_theme_path}); QIcon::setThemeName(current_theme); } QIcon::setThemeSearchPaths(theme_paths); } void GMainWindow::LoadTranslation() { // If the selected language is English, no need to install any translation if (UISettings::values.language == QStringLiteral("en")) { return; } bool loaded; if (UISettings::values.language.isEmpty()) { // Use the system's default locale loaded = translator.load(QLocale::system(), {}, {}, QStringLiteral(":/languages/")); } else { // Otherwise load from the specified file loaded = translator.load(UISettings::values.language, QStringLiteral(":/languages/")); } if (loaded) { qApp->installTranslator(&translator); } else { UISettings::values.language = QStringLiteral("en"); } } void GMainWindow::OnLanguageChanged(const QString& locale) { if (UISettings::values.language != QStringLiteral("en")) { qApp->removeTranslator(&translator); } UISettings::values.language = locale; LoadTranslation(); ui->retranslateUi(this); RetranslateStatusBar(); UpdateWindowTitle(); } void GMainWindow::OnConfigurePerGame() { u64 title_id{}; system.GetAppLoader().ReadProgramId(title_id); OpenPerGameConfiguration(title_id, game_path); } void GMainWindow::OpenPerGameConfiguration(u64 title_id, const QString& file_name) { Settings::SetConfiguringGlobal(false); ConfigurePerGame dialog(this, title_id, file_name, gl_renderer, physical_devices, system); const auto result = dialog.exec(); if (result != QDialog::Accepted) { Settings::RestoreGlobalState(system.IsPoweredOn()); return; } else if (result == QDialog::Accepted) { dialog.ApplyConfiguration(); } // Do not cause the global config to write local settings into the config file const bool is_powered_on = system.IsPoweredOn(); Settings::RestoreGlobalState(system.IsPoweredOn()); if (!is_powered_on) { config->Save(); } UpdateStatusButtons(); } void GMainWindow::OnMoviePlaybackCompleted() { OnPauseGame(); QMessageBox::information(this, tr("Playback Completed"), tr("Movie playback completed.")); } void GMainWindow::UpdateWindowTitle() { const QString full_name = QString::fromUtf8(Common::g_build_fullname); if (game_title.isEmpty()) { setWindowTitle(QStringLiteral("Azahar %1").arg(full_name)); } else { setWindowTitle(QStringLiteral("Azahar %1 | %2").arg(full_name, game_title)); render_window->setWindowTitle( QStringLiteral("Azahar %1 | %2 | %3").arg(full_name, game_title, tr("Primary Window"))); secondary_window->setWindowTitle(QStringLiteral("Azahar %1 | %2 | %3") .arg(full_name, game_title, tr("Secondary Window"))); } } void GMainWindow::UpdateUISettings() { if (!ui->action_Fullscreen->isChecked()) { UISettings::values.geometry = saveGeometry(); UISettings::values.renderwindow_geometry = render_window->saveGeometry(); } UISettings::values.state = saveState(); #if MICROPROFILE_ENABLED UISettings::values.microprofile_geometry = microProfileDialog->saveGeometry(); UISettings::values.microprofile_visible = microProfileDialog->isVisible(); #endif UISettings::values.single_window_mode = ui->action_Single_Window_Mode->isChecked(); UISettings::values.fullscreen = ui->action_Fullscreen->isChecked(); UISettings::values.display_titlebar = ui->action_Display_Dock_Widget_Headers->isChecked(); UISettings::values.show_filter_bar = ui->action_Show_Filter_Bar->isChecked(); UISettings::values.show_status_bar = ui->action_Show_Status_Bar->isChecked(); UISettings::values.first_start = false; } void GMainWindow::SyncMenuUISettings() { ui->action_Screen_Layout_Default->setChecked(Settings::values.layout_option.GetValue() == Settings::LayoutOption::Default); ui->action_Screen_Layout_Single_Screen->setChecked(Settings::values.layout_option.GetValue() == Settings::LayoutOption::SingleScreen); ui->action_Screen_Layout_Large_Screen->setChecked(Settings::values.layout_option.GetValue() == Settings::LayoutOption::LargeScreen); ui->action_Screen_Layout_Hybrid_Screen->setChecked(Settings::values.layout_option.GetValue() == Settings::LayoutOption::HybridScreen); ui->action_Screen_Layout_Side_by_Side->setChecked(Settings::values.layout_option.GetValue() == Settings::LayoutOption::SideScreen); ui->action_Screen_Layout_Separate_Windows->setChecked( Settings::values.layout_option.GetValue() == Settings::LayoutOption::SeparateWindows); ui->action_Screen_Layout_Custom_Layout->setChecked(Settings::values.layout_option.GetValue() == Settings::LayoutOption::CustomLayout); ui->action_Screen_Layout_Swap_Screens->setChecked(Settings::values.swap_screen.GetValue()); ui->action_Screen_Layout_Upright_Screens->setChecked( Settings::values.upright_screen.GetValue()); ui->menu_Small_Screen_Position->setEnabled(Settings::values.layout_option.GetValue() == Settings::LayoutOption::LargeScreen); ui->action_Small_Screen_TopRight->setChecked( Settings::values.small_screen_position.GetValue() == Settings::SmallScreenPosition::TopRight); ui->action_Small_Screen_MiddleRight->setChecked( Settings::values.small_screen_position.GetValue() == Settings::SmallScreenPosition::MiddleRight); ui->action_Small_Screen_BottomRight->setChecked( Settings::values.small_screen_position.GetValue() == Settings::SmallScreenPosition::BottomRight); ui->action_Small_Screen_TopLeft->setChecked(Settings::values.small_screen_position.GetValue() == Settings::SmallScreenPosition::TopLeft); ui->action_Small_Screen_MiddleLeft->setChecked( Settings::values.small_screen_position.GetValue() == Settings::SmallScreenPosition::MiddleLeft); ui->action_Small_Screen_BottomLeft->setChecked( Settings::values.small_screen_position.GetValue() == Settings::SmallScreenPosition::BottomLeft); ui->action_Small_Screen_Above->setChecked(Settings::values.small_screen_position.GetValue() == Settings::SmallScreenPosition::AboveLarge); ui->action_Small_Screen_Below->setChecked(Settings::values.small_screen_position.GetValue() == Settings::SmallScreenPosition::BelowLarge); } void GMainWindow::RetranslateStatusBar() { if (emu_thread) UpdateStatusBar(); emu_speed_label->setToolTip(tr("Current emulation speed. Values higher or lower than 100% " "indicate emulation is running faster or slower than a 3DS.")); game_fps_label->setToolTip(tr("How many frames per second the app is currently displaying. " "This will vary from app to app and scene to scene.")); emu_frametime_label->setToolTip( tr("Time taken to emulate a 3DS frame, not counting framelimiting or v-sync. For " "full-speed emulation this should be at most 16.67 ms.")); multiplayer_state->retranslateUi(); } void GMainWindow::SetDiscordEnabled([[maybe_unused]] bool state) { #ifdef USE_DISCORD_PRESENCE if (state) { discord_rpc = std::make_unique(system); } else { discord_rpc = std::make_unique(); } #else discord_rpc = std::make_unique(); #endif discord_rpc->Update(); } #ifdef __unix__ void GMainWindow::SetGamemodeEnabled(bool state) { if (emulation_running) { Common::Linux::SetGamemodeState(state); } } #endif #ifdef main #undef main #endif static Qt::HighDpiScaleFactorRoundingPolicy GetHighDpiRoundingPolicy() { #ifdef _WIN32 // For Windows, we want to avoid scaling artifacts on fractional scaling ratios. // This is done by setting the optimal scaling policy for the primary screen. // Create a temporary QApplication. int temp_argc = 0; char** temp_argv = nullptr; QApplication temp{temp_argc, temp_argv}; // Get the current screen geometry. const QScreen* primary_screen = QGuiApplication::primaryScreen(); if (!primary_screen) { return Qt::HighDpiScaleFactorRoundingPolicy::PassThrough; } const QRect screen_rect = primary_screen->geometry(); const qreal real_ratio = primary_screen->devicePixelRatio(); const qreal real_width = std::trunc(screen_rect.width() * real_ratio); const qreal real_height = std::trunc(screen_rect.height() * real_ratio); // Recommended minimum width and height for proper window fit. // Any screen with a lower resolution than this will still have a scale of 1. constexpr qreal minimum_width = 1350.0; constexpr qreal minimum_height = 900.0; const qreal width_ratio = std::max(1.0, real_width / minimum_width); const qreal height_ratio = std::max(1.0, real_height / minimum_height); // Get the lower of the 2 ratios and truncate, this is the maximum integer scale. const qreal max_ratio = std::trunc(std::min(width_ratio, height_ratio)); if (max_ratio > real_ratio) { return Qt::HighDpiScaleFactorRoundingPolicy::Round; } else { return Qt::HighDpiScaleFactorRoundingPolicy::Floor; } #else // Other OSes should be better than Windows at fractional scaling. return Qt::HighDpiScaleFactorRoundingPolicy::PassThrough; #endif } void LaunchQtFrontend(int argc, char* argv[]) { Common::DetachedTasks detached_tasks; #if MICROPROFILE_ENABLED MicroProfileOnThreadCreate("Frontend"); SCOPE_EXIT({ MicroProfileShutdown(); }); #endif // Init settings params QCoreApplication::setOrganizationName(QStringLiteral("Azahar Developers")); QCoreApplication::setApplicationName(QStringLiteral("Azahar")); auto rounding_policy = GetHighDpiRoundingPolicy(); QApplication::setHighDpiScaleFactorRoundingPolicy(rounding_policy); #ifdef __APPLE__ auto bundle_dir = FileUtil::GetBundleDirectory(); if (bundle_dir) { FileUtil::SetCurrentDir(bundle_dir.value() + ".."); } #endif #ifdef ENABLE_OPENGL QCoreApplication::setAttribute(Qt::AA_DontCheckOpenGLContextThreadAffinity); QCoreApplication::setAttribute(Qt::AA_ShareOpenGLContexts); #endif QApplication app(argc, argv); // Required when using .qrc resources from within a static library. // See https://doc.qt.io/qt-5/resources.html#using-resources-in-a-library Q_INIT_RESOURCE(compatibility_list); Q_INIT_RESOURCE(theme_colorful); Q_INIT_RESOURCE(theme_colorful_dark); Q_INIT_RESOURCE(theme_colorful_midnight_blue); Q_INIT_RESOURCE(theme_default); Q_INIT_RESOURCE(theme_qdarkstyle); Q_INIT_RESOURCE(theme_qdarkstyle_midnight_blue); #ifdef ENABLE_QT_TRANSLATION Q_INIT_RESOURCE(languages); #endif // Qt changes the locale and causes issues in float conversion using std::to_string() when // generating shaders setlocale(LC_ALL, "C"); auto& system{Core::System::GetInstance()}; // Register Qt image interface system.RegisterImageInterface(std::make_shared()); GMainWindow main_window(system); // Register frontend applets Frontend::RegisterDefaultApplets(system); system.RegisterMiiSelector(std::make_shared(main_window)); system.RegisterSoftwareKeyboard(std::make_shared(main_window)); #ifdef __APPLE__ // Register microphone permission check. system.RegisterMicPermissionCheck(&AppleAuthorization::CheckAuthorizationForMicrophone); #endif main_window.show(); QObject::connect(&app, &QGuiApplication::applicationStateChanged, &main_window, &GMainWindow::OnAppFocusStateChanged); int result = app.exec(); detached_tasks.WaitForAllTasks(); exit(result); }