// SPDX-FileCopyrightText: 2002-2024 PCSX2 Dev Team // SPDX-License-Identifier: GPL-3.0+ #include "AboutDialog.h" #include "AutoUpdaterDialog.h" #include "CoverDownloadDialog.h" #include "DisplayWidget.h" #include "GameList/GameListRefreshThread.h" #include "GameList/GameListWidget.h" #include "LogWindow.h" #include "MainWindow.h" #include "QtHost.h" #include "QtUtils.h" #include "SettingWidgetBinder.h" #include "Settings/AchievementLoginDialog.h" #include "Settings/ControllerSettingsWindow.h" #include "Settings/GameListSettingsWidget.h" #include "Settings/InterfaceSettingsWidget.h" #include "Settings/MemoryCardCreateDialog.h" #include "Tools/InputRecording/InputRecordingViewer.h" #include "Tools/InputRecording/NewInputRecordingDlg.h" #include "svnrev.h" #include "pcsx2/Achievements.h" #include "pcsx2/CDVD/CDVDcommon.h" #include "pcsx2/CDVD/CDVDdiscReader.h" #include "pcsx2/GS.h" #include "pcsx2/GS/GS.h" #include "pcsx2/GSDumpReplayer.h" #include "pcsx2/GameList.h" #include "pcsx2/Host.h" #include "pcsx2/MTGS.h" #include "pcsx2/PerformanceMetrics.h" #include "pcsx2/Recording/InputRecording.h" #include "pcsx2/Recording/InputRecordingControls.h" #include "pcsx2/SIO/Sio.h" #include "common/Assertions.h" #include "common/CocoaTools.h" #include "common/FileSystem.h" #include #include #include #include #include #include #include #include #include #ifdef _WIN32 #include "common/RedtapeWindows.h" #include #endif const char* MainWindow::OPEN_FILE_FILTER = QT_TRANSLATE_NOOP("MainWindow", "All File Types (*.bin *.iso *.cue *.mdf *.chd *.cso *.zso *.gz *.elf *.irx *.gs *.gs.xz *.gs.zst *.dump);;" "Single-Track Raw Images (*.bin *.iso);;" "Cue Sheets (*.cue);;" "Media Descriptor File (*.mdf);;" "MAME CHD Images (*.chd);;" "CSO Images (*.cso);;" "ZSO Images (*.zso);;" "GZ Images (*.gz);;" "ELF Executables (*.elf);;" "IRX Executables (*.irx);;" "GS Dumps (*.gs *.gs.xz *.gs.zst);;" "Block Dumps (*.dump)"); const char* MainWindow::DISC_IMAGE_FILTER = QT_TRANSLATE_NOOP("MainWindow", "All File Types (*.bin *.iso *.cue *.mdf *.chd *.cso *.zso *.gz *.dump);;" "Single-Track Raw Images (*.bin *.iso);;" "Cue Sheets (*.cue);;" "Media Descriptor File (*.mdf);;" "MAME CHD Images (*.chd);;" "CSO Images (*.cso);;" "ZSO Images (*.zso);;" "GZ Images (*.gz);;" "Block Dumps (*.dump)"); MainWindow* g_main_window = nullptr; #if defined(_WIN32) || defined(__APPLE__) static const bool s_use_central_widget = false; #else // Qt Wayland is broken. Any sort of stacked widget usage fails to update, // leading to broken window resizes, no display rendering, etc. So, we mess // with the central widget instead. Which we can't do on xorg, because it // breaks window resizing there... static bool s_use_central_widget = false; #endif // UI thread VM validity. static bool s_vm_valid = false; static bool s_vm_paused = false; static QString s_current_title; static QString s_current_elf_override; static QString s_current_disc_path; static QString s_current_disc_serial; static quint32 s_current_disc_crc; static quint32 s_current_running_crc; MainWindow::MainWindow() { pxAssert(!g_main_window); g_main_window = this; #if !defined(_WIN32) && !defined(__APPLE__) s_use_central_widget = DisplayContainer::isRunningOnWayland(); #endif } MainWindow::~MainWindow() { // make sure the game list isn't refreshing, because it's on a separate thread cancelGameListRefresh(); destroySubWindows(); // we compare here, since recreate destroys the window later if (g_main_window == this) g_main_window = nullptr; #ifdef _WIN32 unregisterForDeviceNotifications(); #endif #ifdef __APPLE__ CocoaTools::RemoveThemeChangeHandler(this); #endif } void MainWindow::initialize() { #ifdef __APPLE__ CocoaTools::AddThemeChangeHandler(this, [](void* ctx) { // This handler is called *before* the style change has propagated far enough for Qt to see it // Use RunOnUIThread to delay until it has QtHost::RunOnUIThread([ctx = static_cast(ctx)] { ctx->updateTheme(); // Qt won't notice the style change without us touching the palette in some way }); }); #endif m_ui.setupUi(this); setupAdditionalUi(); connectSignals(); connectVMThreadSignals(g_emu_thread); restoreStateFromConfig(); switchToGameListView(); updateWindowTitle(); updateGameDependentActions(); #ifdef _WIN32 registerForDeviceNotifications(); #endif } // TODO: Figure out how to set this in the .ui file /// Marks the icons for all actions in the given menu as mask icons /// This means macOS's menubar renderer will ignore color values and use only the alpha in the image. /// The color value will instead be taken from the system theme. /// Since the menubar follows the OS's dark/light mode and not our current theme's, this prevents problems where a theme mismatch puts white icons in light mode or dark icons in dark mode. static void makeIconsMasks(QWidget* menu) { for (QAction* action : menu->actions()) { if (!action->icon().isNull()) { QIcon icon = action->icon(); icon.setIsMask(true); action->setIcon(icon); } if (action->menu()) makeIconsMasks(action->menu()); } } QWidget* MainWindow::getContentParent() { return s_use_central_widget ? static_cast(this) : static_cast(m_ui.mainContainer); } void MainWindow::setupAdditionalUi() { makeIconsMasks(menuBar()); updateAdvancedSettingsVisibility(); const bool toolbar_visible = Host::GetBaseBoolSettingValue("UI", "ShowToolbar", false); m_ui.actionViewToolbar->setChecked(toolbar_visible); m_ui.toolBar->setVisible(toolbar_visible); const bool toolbars_locked = Host::GetBaseBoolSettingValue("UI", "LockToolbar", false); m_ui.actionViewLockToolbar->setChecked(toolbars_locked); m_ui.toolBar->setMovable(!toolbars_locked); m_ui.toolBar->setContextMenuPolicy(Qt::PreventContextMenu); const bool status_bar_visible = Host::GetBaseBoolSettingValue("UI", "ShowStatusBar", true); m_ui.actionViewStatusBar->setChecked(status_bar_visible); m_ui.statusBar->setVisible(status_bar_visible); m_game_list_widget = new GameListWidget(getContentParent()); m_game_list_widget->initialize(); m_ui.actionGridViewShowTitles->setChecked(m_game_list_widget->getShowGridCoverTitles()); if (s_use_central_widget) { m_ui.mainContainer = nullptr; // setCentralWidget() will delete this setCentralWidget(m_game_list_widget); } else { m_ui.mainContainer->addWidget(m_game_list_widget); } m_status_progress_widget = new QProgressBar(m_ui.statusBar); m_status_progress_widget->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed); m_status_progress_widget->setFixedSize(140, 16); m_status_progress_widget->setMinimum(0); m_status_progress_widget->setMaximum(100); m_status_progress_widget->hide(); m_status_verbose_widget = new QLabel(m_ui.statusBar); m_status_verbose_widget->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); m_status_verbose_widget->setFixedHeight(16); m_status_verbose_widget->hide(); m_status_renderer_widget = new QLabel(m_ui.statusBar); m_status_renderer_widget->setFixedHeight(16); m_status_renderer_widget->setFixedSize(65, 16); m_status_renderer_widget->hide(); m_status_resolution_widget = new QLabel(m_ui.statusBar); m_status_resolution_widget->setFixedHeight(16); m_status_resolution_widget->setFixedSize(75, 16); m_status_resolution_widget->hide(); m_status_fps_widget = new QLabel(m_ui.statusBar); m_status_fps_widget->setFixedSize(60, 16); m_status_fps_widget->hide(); m_status_vps_widget = new QLabel(m_ui.statusBar); m_status_vps_widget->setFixedSize(60, 16); m_status_vps_widget->hide(); m_status_speed_widget = new QLabel(m_ui.statusBar); m_status_speed_widget->setFixedSize(90, 16); m_status_speed_widget->hide(); m_settings_toolbar_menu = new QMenu(m_ui.toolBar); m_settings_toolbar_menu->addAction(m_ui.actionSettings); m_settings_toolbar_menu->addAction(m_ui.actionViewGameProperties); for (u32 scale = 0; scale <= 10; scale++) { QAction* action = m_ui.menuWindowSize->addAction((scale == 0) ? tr("Internal Resolution") : tr("%1x Scale").arg(scale)); connect(action, &QAction::triggered, [scale]() { g_emu_thread->requestDisplaySize(static_cast(scale)); }); } updateEmulationActions(false, false, false); updateDisplayRelatedActions(false, false, false); #ifdef ENABLE_RAINTEGRATION if (Achievements::IsUsingRAIntegration()) { QMenu* raMenu = new QMenu(QStringLiteral("RAIntegration"), m_ui.menuTools); connect(raMenu, &QMenu::aboutToShow, this, [this, raMenu]() { raMenu->clear(); const auto items = Achievements::RAIntegration::GetMenuItems(); for (const auto& [id, title, checked] : items) { if (id == 0) { raMenu->addSeparator(); continue; } QAction* raAction = raMenu->addAction(QString::fromUtf8(title)); if (checked) { raAction->setCheckable(true); raAction->setChecked(checked); } connect(raAction, &QAction::triggered, this, [id = id]() { Host::RunOnCPUThread([id]() { Achievements::RAIntegration::ActivateMenuItem(id); }, false); }); } }); m_ui.menuTools->insertMenu(m_ui.menuInputRecording->menuAction(), raMenu); } #endif } void MainWindow::connectSignals() { connect(m_ui.actionStartFile, &QAction::triggered, this, &MainWindow::onStartFileActionTriggered); connect(m_ui.actionStartDisc, &QAction::triggered, this, &MainWindow::onStartDiscActionTriggered); connect(m_ui.actionStartBios, &QAction::triggered, this, &MainWindow::onStartBIOSActionTriggered); connect(m_ui.actionChangeDiscFromFile, &QAction::triggered, this, &MainWindow::onChangeDiscFromFileActionTriggered); connect(m_ui.actionChangeDiscFromDevice, &QAction::triggered, this, &MainWindow::onChangeDiscFromDeviceActionTriggered); connect(m_ui.actionChangeDiscFromGameList, &QAction::triggered, this, &MainWindow::onChangeDiscFromGameListActionTriggered); connect(m_ui.actionRemoveDisc, &QAction::triggered, this, &MainWindow::onRemoveDiscActionTriggered); connect(m_ui.menuChangeDisc, &QMenu::aboutToShow, this, &MainWindow::onChangeDiscMenuAboutToShow); connect(m_ui.menuChangeDisc, &QMenu::aboutToHide, this, &MainWindow::onChangeDiscMenuAboutToHide); connect(m_ui.actionPowerOff, &QAction::triggered, this, [this]() { requestShutdown(true, true, EmuConfig.SaveStateOnShutdown); }); connect(m_ui.actionPowerOffWithoutSaving, &QAction::triggered, this, [this]() { requestShutdown(false, false, false); }); connect(m_ui.actionStartFullscreenUI, &QAction::triggered, this, &MainWindow::onStartFullscreenUITriggered); connect(m_ui.actionToolbarStartFullscreenUI, &QAction::triggered, this, &MainWindow::onStartFullscreenUITriggered); connect(m_ui.actionToolbarStartFile, &QAction::triggered, this, &MainWindow::onStartFileActionTriggered); connect(m_ui.actionToolbarStartDisc, &QAction::triggered, this, &MainWindow::onStartDiscActionTriggered); connect(m_ui.actionToolbarStartBios, &QAction::triggered, this, &MainWindow::onStartBIOSActionTriggered); connect(m_ui.actionToolbarChangeDisc, &QAction::triggered, [this] { m_ui.menuChangeDisc->exec(QCursor::pos()); }); connect(m_ui.actionToolbarPowerOff, &QAction::triggered, this, [this]() { requestShutdown(true, true, EmuConfig.SaveStateOnShutdown); }); connect(m_ui.actionToolbarLoadState, &QAction::triggered, this, [this]() { m_ui.menuLoadState->exec(QCursor::pos()); }); connect(m_ui.actionToolbarSaveState, &QAction::triggered, this, [this]() { m_ui.menuSaveState->exec(QCursor::pos()); }); connect(m_ui.actionToolbarSettings, &QAction::triggered, this, &MainWindow::onSettingsTriggeredFromToolbar); connect(m_ui.actionToolbarControllerSettings, &QAction::triggered, [this]() { doControllerSettings(ControllerSettingsWindow::Category::GlobalSettings); }); connect(m_ui.actionToolbarScreenshot, &QAction::triggered, this, &MainWindow::onScreenshotActionTriggered); connect(m_ui.actionExit, &QAction::triggered, this, &MainWindow::close); connect(m_ui.actionScreenshot, &QAction::triggered, this, &MainWindow::onScreenshotActionTriggered); connect(m_ui.menuLoadState, &QMenu::aboutToShow, this, &MainWindow::onLoadStateMenuAboutToShow); connect(m_ui.menuSaveState, &QMenu::aboutToShow, this, &MainWindow::onSaveStateMenuAboutToShow); connect(m_ui.actionSettings, &QAction::triggered, [this]() { doSettings(); }); connect(m_ui.actionInterfaceSettings, &QAction::triggered, [this]() { doSettings("Interface"); }); connect(m_ui.actionGameListSettings, &QAction::triggered, [this]() { doSettings("Game List"); }); connect(m_ui.actionEmulationSettings, &QAction::triggered, [this]() { doSettings("Emulation"); }); connect(m_ui.actionBIOSSettings, &QAction::triggered, [this]() { doSettings("BIOS"); }); connect(m_ui.actionGraphicsSettings, &QAction::triggered, [this]() { doSettings("Graphics"); }); connect(m_ui.actionAudioSettings, &QAction::triggered, [this]() { doSettings("Audio"); }); connect(m_ui.actionMemoryCardSettings, &QAction::triggered, [this]() { doSettings("Memory Cards"); }); connect(m_ui.actionDEV9Settings, &QAction::triggered, [this]() { doSettings("Network & HDD"); }); connect(m_ui.actionFolderSettings, &QAction::triggered, [this]() { doSettings("Folders"); }); connect(m_ui.actionAchievementSettings, &QAction::triggered, [this]() { doSettings("Achievements"); }); connect(m_ui.actionControllerSettings, &QAction::triggered, [this]() { doControllerSettings(ControllerSettingsWindow::Category::GlobalSettings); }); connect(m_ui.actionHotkeySettings, &QAction::triggered, [this]() { doControllerSettings(ControllerSettingsWindow::Category::HotkeySettings); }); connect(m_ui.actionAddGameDirectory, &QAction::triggered, [this]() { getSettingsWindow()->getGameListSettingsWidget()->addSearchDirectory(this); }); connect(m_ui.actionScanForNewGames, &QAction::triggered, [this]() { refreshGameList(false); }); connect(m_ui.actionRescanAllGames, &QAction::triggered, [this]() { refreshGameList(true); }); connect(m_ui.actionViewToolbar, &QAction::toggled, this, &MainWindow::onViewToolbarActionToggled); connect(m_ui.actionViewLockToolbar, &QAction::toggled, this, &MainWindow::onViewLockToolbarActionToggled); connect(m_ui.actionViewStatusBar, &QAction::toggled, this, &MainWindow::onViewStatusBarActionToggled); connect(m_ui.actionViewGameList, &QAction::triggered, this, &MainWindow::onViewGameListActionTriggered); connect(m_ui.actionViewGameGrid, &QAction::triggered, this, &MainWindow::onViewGameGridActionTriggered); connect(m_ui.actionViewSystemDisplay, &QAction::triggered, this, &MainWindow::onViewSystemDisplayTriggered); connect(m_ui.actionViewGameProperties, &QAction::triggered, this, &MainWindow::onViewGamePropertiesActionTriggered); connect(m_ui.actionGitHubRepository, &QAction::triggered, this, &MainWindow::onGitHubRepositoryActionTriggered); connect(m_ui.actionSupportForums, &QAction::triggered, this, &MainWindow::onSupportForumsActionTriggered); connect(m_ui.actionDiscordServer, &QAction::triggered, this, &MainWindow::onDiscordServerActionTriggered); connect(m_ui.actionAboutQt, &QAction::triggered, qApp, &QApplication::aboutQt); connect(m_ui.actionAbout, &QAction::triggered, this, &MainWindow::onAboutActionTriggered); connect(m_ui.actionCheckForUpdates, &QAction::triggered, this, [this]() { checkForUpdates(true, true); }); connect(m_ui.actionOpenDataDirectory, &QAction::triggered, this, &MainWindow::onToolsOpenDataDirectoryTriggered); connect(m_ui.actionCoverDownloader, &QAction::triggered, this, &MainWindow::onToolsCoverDownloaderTriggered); connect(m_ui.actionGridViewShowTitles, &QAction::triggered, m_game_list_widget, &GameListWidget::setShowCoverTitles); connect(m_ui.actionGridViewZoomIn, &QAction::triggered, m_game_list_widget, [this]() { if (isShowingGameList()) m_game_list_widget->gridZoomIn(); }); connect(m_ui.actionGridViewZoomOut, &QAction::triggered, m_game_list_widget, [this]() { if (isShowingGameList()) m_game_list_widget->gridZoomOut(); }); connect(m_ui.actionGridViewRefreshCovers, &QAction::triggered, m_game_list_widget, &GameListWidget::refreshGridCovers); connect(m_game_list_widget, &GameListWidget::layoutChange, this, [this]() { QSignalBlocker sb(m_ui.actionGridViewShowTitles); m_ui.actionGridViewShowTitles->setChecked(m_game_list_widget->getShowGridCoverTitles()); }); SettingWidgetBinder::BindWidgetToBoolSetting(nullptr, m_ui.actionViewStatusBarVerbose, "UI", "VerboseStatusBar", false); SettingWidgetBinder::BindWidgetToBoolSetting(nullptr, m_ui.actionEnableSystemConsole, "Logging", "EnableSystemConsole", false); if (Log::IsDebugOutputAvailable()) { SettingWidgetBinder::BindWidgetToBoolSetting(nullptr, m_ui.actionEnableDebugConsole, "Logging", "EnableDebugConsole", false); } else { m_ui.menuTools->removeAction(m_ui.actionEnableDebugConsole); m_ui.actionEnableDebugConsole->deleteLater(); m_ui.actionEnableDebugConsole = nullptr; } #ifndef PCSX2_DEVBUILD SettingWidgetBinder::BindWidgetToBoolSetting(nullptr, m_ui.actionEnableVerboseLogging, "Logging", "EnableVerbose", false); #else // Dev builds always have verbose logging. m_ui.actionEnableVerboseLogging->setChecked(true); m_ui.actionEnableVerboseLogging->setEnabled(false); #endif SettingWidgetBinder::BindWidgetToBoolSetting(nullptr, m_ui.actionEnableEEConsoleLogging, "Logging", "EnableEEConsole", true); SettingWidgetBinder::BindWidgetToBoolSetting(nullptr, m_ui.actionEnableIOPConsoleLogging, "Logging", "EnableIOPConsole", true); SettingWidgetBinder::BindWidgetToBoolSetting(nullptr, m_ui.actionEnableLogWindow, "Logging", "EnableLogWindow", false); SettingWidgetBinder::BindWidgetToBoolSetting(nullptr, m_ui.actionEnableFileLogging, "Logging", "EnableFileLogging", false); SettingWidgetBinder::BindWidgetToBoolSetting(nullptr, m_ui.actionEnableLogTimestamps, "Logging", "EnableTimestamps", true); SettingWidgetBinder::BindWidgetToBoolSetting(nullptr, m_ui.actionEnableCDVDVerboseReads, "EmuCore", "CdvdVerboseReads", false); SettingWidgetBinder::BindWidgetToBoolSetting(nullptr, m_ui.actionSaveBlockDump, "EmuCore", "CdvdDumpBlocks", false); m_ui.actionShowAdvancedSettings->setChecked(QtHost::ShouldShowAdvancedSettings()); connect(m_ui.actionSaveBlockDump, &QAction::toggled, this, &MainWindow::onBlockDumpActionToggled); connect(m_ui.actionShowAdvancedSettings, &QAction::toggled, this, &MainWindow::onShowAdvancedSettingsToggled); connect(m_ui.actionSaveGSDump, &QAction::triggered, this, &MainWindow::onSaveGSDumpActionTriggered); connect(m_ui.actionVideoCapture, &QAction::toggled, this, &MainWindow::onVideoCaptureToggled); connect(m_ui.actionEditPatches, &QAction::triggered, this, [this]() { onToolsEditCheatsPatchesTriggered(false); }); connect(m_ui.actionEditCheats, &QAction::triggered, this, [this]() { onToolsEditCheatsPatchesTriggered(true); }); // Input Recording connect(m_ui.actionInputRecNew, &QAction::triggered, this, &MainWindow::onInputRecNewActionTriggered); connect(m_ui.actionInputRecPlay, &QAction::triggered, this, &MainWindow::onInputRecPlayActionTriggered); connect(m_ui.actionInputRecStop, &QAction::triggered, this, &MainWindow::onInputRecStopActionTriggered); SettingWidgetBinder::BindWidgetToBoolSetting(nullptr, m_ui.actionInputRecConsoleLogs, "Logging", "EnableInputRecordingLogs", false); SettingWidgetBinder::BindWidgetToBoolSetting(nullptr, m_ui.actionInputRecControllerLogs, "Logging", "EnableControllerLogs", false); connect(m_ui.actionInputRecOpenViewer, &QAction::triggered, this, &MainWindow::onInputRecOpenViewer); // These need to be queued connections to stop crashing due to menus opening/closing and switching focus. connect(m_game_list_widget, &GameListWidget::refreshProgress, this, &MainWindow::onGameListRefreshProgress); connect(m_game_list_widget, &GameListWidget::refreshComplete, this, &MainWindow::onGameListRefreshComplete); connect(m_game_list_widget, &GameListWidget::selectionChanged, this, &MainWindow::onGameListSelectionChanged, Qt::QueuedConnection); connect(m_game_list_widget, &GameListWidget::entryActivated, this, &MainWindow::onGameListEntryActivated, Qt::QueuedConnection); connect(m_game_list_widget, &GameListWidget::entryContextMenuRequested, this, &MainWindow::onGameListEntryContextMenuRequested, Qt::QueuedConnection); connect(m_game_list_widget, &GameListWidget::addGameDirectoryRequested, this, [this]() { getSettingsWindow()->getGameListSettingsWidget()->addSearchDirectory(this); }); createRendererSwitchMenu(); } void MainWindow::connectVMThreadSignals(EmuThread* thread) { connect(thread, &EmuThread::messageConfirmed, this, &MainWindow::confirmMessage, Qt::BlockingQueuedConnection); connect(thread, &EmuThread::statusMessage, this, &MainWindow::onStatusMessage); connect(thread, &EmuThread::onAcquireRenderWindowRequested, this, &MainWindow::acquireRenderWindow, Qt::BlockingQueuedConnection); connect(thread, &EmuThread::onReleaseRenderWindowRequested, this, &MainWindow::releaseRenderWindow, Qt::BlockingQueuedConnection); connect(thread, &EmuThread::onResizeRenderWindowRequested, this, &MainWindow::displayResizeRequested); connect(thread, &EmuThread::onMouseModeRequested, this, &MainWindow::mouseModeRequested); connect(thread, &EmuThread::onFullscreenUIStateChange, this, &MainWindow::onFullscreenUIStateChange); connect(thread, &EmuThread::onVMStarting, this, &MainWindow::onVMStarting); connect(thread, &EmuThread::onVMStarted, this, &MainWindow::onVMStarted); connect(thread, &EmuThread::onVMPaused, this, &MainWindow::onVMPaused); connect(thread, &EmuThread::onVMResumed, this, &MainWindow::onVMResumed); connect(thread, &EmuThread::onVMStopped, this, &MainWindow::onVMStopped); connect(thread, &EmuThread::onGameChanged, this, &MainWindow::onGameChanged); connect(thread, &EmuThread::onCaptureStarted, this, &MainWindow::onCaptureStarted); connect(thread, &EmuThread::onCaptureStopped, this, &MainWindow::onCaptureStopped); connect(thread, &EmuThread::onAchievementsLoginRequested, this, &MainWindow::onAchievementsLoginRequested); connect(thread, &EmuThread::onAchievementsHardcoreModeChanged, this, &MainWindow::onAchievementsHardcoreModeChanged); connect(thread, &EmuThread::onCoverDownloaderOpenRequested, this, &MainWindow::onToolsCoverDownloaderTriggered); connect(thread, &EmuThread::onCreateMemoryCardOpenRequested, this, &MainWindow::onCreateMemoryCardOpenRequested); connect(m_ui.actionReset, &QAction::triggered, this, &MainWindow::requestReset); connect(m_ui.actionPause, &QAction::toggled, thread, &EmuThread::setVMPaused); connect(m_ui.actionFullscreen, &QAction::triggered, thread, &EmuThread::toggleFullscreen); connect(m_ui.actionToolbarReset, &QAction::triggered, this, &MainWindow::requestReset); connect(m_ui.actionToolbarPause, &QAction::toggled, thread, &EmuThread::setVMPaused); connect(m_ui.actionToolbarFullscreen, &QAction::triggered, thread, &EmuThread::toggleFullscreen); connect(m_ui.actionToggleSoftwareRendering, &QAction::triggered, thread, &EmuThread::toggleSoftwareRendering); connect(m_ui.actionDebugger, &QAction::triggered, this, &MainWindow::openDebugger); connect(m_ui.actionReloadPatches, &QAction::triggered, thread, &EmuThread::reloadPatches); } void MainWindow::createRendererSwitchMenu() { static constexpr const GSRendererType renderers[] = { GSRendererType::Auto, #if defined(_WIN32) GSRendererType::DX11, GSRendererType::DX12, #elif defined(__APPLE__) GSRendererType::Metal, #endif #ifdef ENABLE_OPENGL GSRendererType::OGL, #endif #ifdef ENABLE_VULKAN GSRendererType::VK, #endif GSRendererType::SW, GSRendererType::Null, }; const GSRendererType current_renderer = static_cast( Host::GetBaseIntSettingValue("EmuCore/GS", "Renderer", static_cast(GSRendererType::Auto))); for (const GSRendererType renderer : renderers) { QAction* action = m_ui.menuDebugSwitchRenderer->addAction( QString::fromUtf8(Pcsx2Config::GSOptions::GetRendererName(renderer))); action->setCheckable(true); action->setChecked(current_renderer == renderer); connect(action, &QAction::triggered, [this, action, renderer] { Host::SetBaseIntSettingValue("EmuCore/GS", "Renderer", static_cast(renderer)); Host::CommitBaseSettingChanges(); g_emu_thread->applySettings(); // clear all others for (QObject* obj : m_ui.menuDebugSwitchRenderer->children()) { if (QAction* act = qobject_cast(obj); act && act != action) act->setChecked(false); } }); } } void MainWindow::recreate() { destroySubWindows(); const bool was_display_created = m_display_created; if (was_display_created) { g_emu_thread->setSurfaceless(true); while (m_display_widget || !g_emu_thread->isSurfaceless()) QApplication::processEvents(QEventLoop::ExcludeUserInputEvents, 1); m_display_created = false; } // We need to close input sources, because e.g. DInput uses our window handle. g_emu_thread->closeInputSources(); close(); g_main_window = nullptr; MainWindow* new_main_window = new MainWindow(); pxAssert(g_main_window == new_main_window); new_main_window->initialize(); new_main_window->refreshGameList(false); new_main_window->show(); deleteLater(); // Recreate log window as well. Then make sure we're still on top. LogWindow::updateSettings(); new_main_window->raise(); new_main_window->activateWindow(); // Reload the sources we just closed. g_emu_thread->applySettings(); if (was_display_created) { g_emu_thread->setSurfaceless(false); g_main_window->updateEmulationActions(false, s_vm_valid, Achievements::IsHardcoreModeActive()); g_main_window->onFullscreenUIStateChange(g_emu_thread->isRunningFullscreenUI()); } } void MainWindow::recreateSettings() { QString current_category; if (m_settings_window) { const bool was_visible = m_settings_window->isVisible(); current_category = m_settings_window->getCategory(); m_settings_window->hide(); m_settings_window->deleteLater(); m_settings_window = nullptr; if (!was_visible) return; } doSettings(current_category.toUtf8().constData()); } void MainWindow::resetSettings(bool ui) { Host::RequestResetSettings(false, true, false, false, ui); if (ui) { // UI reset includes theme (and eventually language). // Just updating the theme here, when there's no change, causes Qt to get very confused.. // So, we'll just tear down everything and recreate. We'll need to do that for language // resets eventaully anyway. recreate(); } // g_main_window here for recreate() case above. g_main_window->recreateSettings(); } void MainWindow::quit() { // Make sure VM is gone. It really should be if we're here. if (s_vm_valid) { g_emu_thread->shutdownVM(false); while (s_vm_valid) QApplication::processEvents(QEventLoop::ExcludeUserInputEvents, 1); } // Big picture might still be active. if (m_display_created) g_emu_thread->stopFullscreenUI(); // Ensure subwindows are removed before quitting. That way the log window cancelling // the close event won't cancel the quit process. destroySubWindows(); QGuiApplication::quit(); } void MainWindow::destroySubWindows() { if (m_debugger_window) { m_debugger_window->close(); m_debugger_window->deleteLater(); m_debugger_window = nullptr; } if (m_controller_settings_window) { m_controller_settings_window->close(); m_controller_settings_window->deleteLater(); m_controller_settings_window = nullptr; } if (m_settings_window) { m_settings_window->close(); m_settings_window->deleteLater(); m_settings_window = nullptr; } SettingsWindow::closeGamePropertiesDialogs(); LogWindow::destroy(); } void MainWindow::onScreenshotActionTriggered() { g_emu_thread->queueSnapshot(0); } void MainWindow::onSaveGSDumpActionTriggered() { g_emu_thread->queueSnapshot(1); } void MainWindow::onBlockDumpActionToggled(bool checked) { if (!checked) return; std::string old_directory = Host::GetBaseStringSettingValue("EmuCore", "BlockDumpSaveDirectory", ""); if (old_directory.empty()) old_directory = FileSystem::GetWorkingDirectory(); // prompt for a location to save const QString new_dir(QDir::toNativeSeparators( QFileDialog::getExistingDirectory(this, tr("Select location to save block dump:"), QString::fromStdString(old_directory)))); if (new_dir.isEmpty()) { // disable it again m_ui.actionSaveBlockDump->setChecked(false); return; } Host::SetBaseStringSettingValue("EmuCore", "BlockDumpSaveDirectory", new_dir.toUtf8().constData()); Host::CommitBaseSettingChanges(); g_emu_thread->applySettings(); } void MainWindow::onShowAdvancedSettingsToggled(bool checked) { if (checked && !Host::GetBaseBoolSettingValue("UI", "AdvancedSettingsWarningShown", false)) { QCheckBox* cb = new QCheckBox(tr("Do not show again")); QMessageBox mb(this); mb.setWindowIcon(QtHost::GetAppIcon()); mb.setWindowModality(Qt::WindowModal); mb.setWindowTitle(tr("Show Advanced Settings")); mb.setText(tr("Changing advanced settings can have unpredictable effects on games, including graphical glitches, lock-ups, and " "even corrupted save files. " "We do not recommend changing advanced settings unless you know what you are doing, and the implications of changing " "each setting.\n\n" "The PCSX2 team will not provide any support for configurations that modify these settings, you are on your own.\n\n" "Are you sure you want to continue?")); mb.setIcon(QMessageBox::Warning); mb.addButton(QMessageBox::Yes); mb.addButton(QMessageBox::No); mb.setDefaultButton(QMessageBox::No); mb.setCheckBox(cb); if (mb.exec() == QMessageBox::No) { QSignalBlocker sb(m_ui.actionShowAdvancedSettings); m_ui.actionShowAdvancedSettings->setChecked(false); return; } if (cb->isChecked()) { Host::SetBaseBoolSettingValue("UI", "AdvancedSettingsWarningShown", true); Host::CommitBaseSettingChanges(); } } Host::SetBaseBoolSettingValue("UI", "ShowAdvancedSettings", checked); Host::CommitBaseSettingChanges(); updateAdvancedSettingsVisibility(); // just recreate the entire settings window, it's easier. if (m_settings_window) recreateSettings(); } void MainWindow::updateAdvancedSettingsVisibility() { const bool enabled = QtHost::ShouldShowAdvancedSettings(); m_ui.menuDebug->menuAction()->setVisible(enabled); m_ui.actionEnableSystemConsole->setVisible(enabled); if (m_ui.actionEnableDebugConsole) m_ui.actionEnableDebugConsole->setVisible(enabled); m_ui.actionEnableVerboseLogging->setVisible(enabled); } void MainWindow::onVideoCaptureToggled(bool checked) { if (!s_vm_valid) return; // Reset the checked state, we'll get updated by the GS thread. QSignalBlocker sb(m_ui.actionVideoCapture); m_ui.actionVideoCapture->setChecked(!checked); if (!checked) { g_emu_thread->endCapture(); return; } const QString container(QString::fromStdString( Host::GetStringSettingValue("EmuCore/GS", "CaptureContainer", Pcsx2Config::GSOptions::DEFAULT_CAPTURE_CONTAINER))); const QString filter(tr("%1 Files (*.%2)").arg(container.toUpper()).arg(container)); QString path(QStringLiteral("%1.%2").arg(QString::fromStdString(GSGetBaseVideoFilename())).arg(container)); path = QDir::toNativeSeparators(QFileDialog::getSaveFileName(this, tr("Video Capture"), path, filter)); if (path.isEmpty()) return; g_emu_thread->beginCapture(path); } void MainWindow::onCaptureStarted(const QString& filename) { if (!s_vm_valid) return; QSignalBlocker sb(m_ui.actionVideoCapture); m_ui.actionVideoCapture->setChecked(true); } void MainWindow::onCaptureStopped() { if (!s_vm_valid) return; QSignalBlocker sb(m_ui.actionVideoCapture); m_ui.actionVideoCapture->setChecked(false); } void MainWindow::onAchievementsLoginRequested(Achievements::LoginRequestReason reason) { auto lock = pauseAndLockVM(); AchievementLoginDialog dlg(lock.getDialogParent(), reason); dlg.exec(); } void MainWindow::onAchievementsHardcoreModeChanged(bool enabled) { // disable debugger while hardcore mode is active m_ui.actionDebugger->setDisabled(enabled); if (enabled) { // If PauseOnEntry is enabled, we prompt the user to disable Hardcore Mode // or cancel the action later, so we should keep the debugger around if (m_debugger_window && !DebugInterface::getPauseOnEntry()) { m_debugger_window->close(); m_debugger_window->deleteLater(); m_debugger_window = nullptr; } } } void MainWindow::onSettingsTriggeredFromToolbar() { if (s_vm_valid) { m_settings_toolbar_menu->exec(QCursor::pos()); } else { doSettings(); } } void MainWindow::saveStateToConfig() { if (!isVisible()) return; bool changed = false; const QByteArray geometry(saveGeometry()); const QByteArray geometry_b64(geometry.toBase64()); const std::string old_geometry_b64(Host::GetBaseStringSettingValue("UI", "MainWindowGeometry")); if (old_geometry_b64 != geometry_b64.constData()) { Host::SetBaseStringSettingValue("UI", "MainWindowGeometry", geometry_b64.constData()); changed = true; } const QByteArray state(saveState()); const QByteArray state_b64(state.toBase64()); const std::string old_state_b64(Host::GetBaseStringSettingValue("UI", "MainWindowState")); if (old_state_b64 != state_b64.constData()) { Host::SetBaseStringSettingValue("UI", "MainWindowState", state_b64.constData()); changed = true; } if (changed) Host::CommitBaseSettingChanges(); } void MainWindow::restoreStateFromConfig() { { const std::string geometry_b64 = Host::GetBaseStringSettingValue("UI", "MainWindowGeometry"); const QByteArray geometry = QByteArray::fromBase64(QByteArray::fromStdString(geometry_b64)); if (!geometry.isEmpty()) restoreGeometry(geometry); } { const std::string state_b64 = Host::GetBaseStringSettingValue("UI", "MainWindowState"); const QByteArray state = QByteArray::fromBase64(QByteArray::fromStdString(state_b64)); if (!state.isEmpty()) restoreState(state); { QSignalBlocker sb(m_ui.actionViewToolbar); m_ui.actionViewToolbar->setChecked(!m_ui.toolBar->isHidden()); } { QSignalBlocker sb(m_ui.actionViewStatusBar); m_ui.actionViewStatusBar->setChecked(!m_ui.statusBar->isHidden()); } } } void MainWindow::updateEmulationActions(bool starting, bool running, bool stopping) { const bool starting_or_running_or_stopping = starting || running || stopping; m_ui.actionStartFile->setDisabled(starting_or_running_or_stopping); m_ui.actionStartDisc->setDisabled(starting_or_running_or_stopping); m_ui.actionStartBios->setDisabled(starting_or_running_or_stopping); m_ui.actionToolbarStartFile->setDisabled(starting_or_running_or_stopping); m_ui.actionToolbarStartDisc->setDisabled(starting_or_running_or_stopping); m_ui.actionToolbarStartBios->setDisabled(starting_or_running_or_stopping); m_ui.actionStartFullscreenUI->setDisabled(starting_or_running_or_stopping); m_ui.actionToolbarStartFullscreenUI->setDisabled(starting_or_running_or_stopping); m_ui.actionPowerOff->setEnabled(running); m_ui.actionPowerOffWithoutSaving->setEnabled(running); m_ui.actionReset->setEnabled(running); m_ui.actionPause->setEnabled(running); m_ui.actionScreenshot->setEnabled(running); m_ui.menuChangeDisc->setEnabled(running); m_ui.menuLoadState->setEnabled(running); m_ui.menuSaveState->setEnabled(running); m_ui.actionSaveGSDump->setEnabled(running); m_ui.actionToolbarPowerOff->setEnabled(running); m_ui.actionToolbarReset->setEnabled(running); m_ui.actionToolbarPause->setEnabled(running); m_ui.actionToolbarScreenshot->setEnabled(running); m_ui.actionToolbarChangeDisc->setEnabled(running); m_ui.actionToolbarLoadState->setEnabled(running); m_ui.actionToolbarSaveState->setEnabled(running); m_ui.actionViewGameProperties->setEnabled(running); m_ui.actionVideoCapture->setEnabled(running); if (!running && m_ui.actionVideoCapture->isChecked()) { QSignalBlocker sb(m_ui.actionVideoCapture); m_ui.actionVideoCapture->setChecked(false); } m_game_list_widget->setDisabled(starting && !running); if (!starting && !running) { { QSignalBlocker sb(m_ui.actionPause); m_ui.actionPause->setChecked(false); } { QSignalBlocker sb(m_ui.actionToolbarPause); m_ui.actionToolbarPause->setChecked(false); } } // scanning needs to be disabled while running m_ui.actionScanForNewGames->setDisabled(starting_or_running_or_stopping); m_ui.actionRescanAllGames->setDisabled(starting_or_running_or_stopping); } void MainWindow::updateDisplayRelatedActions(bool has_surface, bool render_to_main, bool fullscreen) { // rendering to main, or switched to gamelist/grid m_ui.actionViewSystemDisplay->setEnabled((has_surface && render_to_main) || (!has_surface && MTGS::IsOpen())); m_ui.menuWindowSize->setEnabled(has_surface && !fullscreen); m_ui.actionFullscreen->setEnabled(has_surface); m_ui.actionToolbarFullscreen->setEnabled(has_surface); { QSignalBlocker blocker(m_ui.actionFullscreen); m_ui.actionFullscreen->setChecked(fullscreen); } { QSignalBlocker blocker(m_ui.actionToolbarFullscreen); m_ui.actionToolbarFullscreen->setChecked(fullscreen); } } void MainWindow::updateStatusBarWidgetVisibility() { auto Update = [this](QWidget* widget, bool visible, int stretch) { if (widget->isVisible()) { m_ui.statusBar->removeWidget(widget); widget->hide(); } if (visible) { m_ui.statusBar->addPermanentWidget(widget, stretch); widget->show(); } }; Update(m_status_verbose_widget, s_vm_valid, 1); Update(m_status_renderer_widget, s_vm_valid, 0); Update(m_status_resolution_widget, s_vm_valid, 0); Update(m_status_fps_widget, s_vm_valid, 0); Update(m_status_vps_widget, s_vm_valid, 0); Update(m_status_speed_widget, s_vm_valid, 0); } void MainWindow::updateWindowTitle() { QString suffix(QtHost::GetAppConfigSuffix()); QString main_title(QtHost::GetAppNameAndVersion() + suffix); QString display_title(s_current_title + suffix); if (!s_vm_valid || s_current_title.isEmpty()) display_title = main_title; else if (isRenderingToMain()) main_title = display_title; if (windowTitle() != main_title) setWindowTitle(main_title); if (m_display_widget && !isRenderingToMain()) { QWidget* container = m_display_container ? static_cast(m_display_container) : static_cast(m_display_widget); if (container->windowTitle() != display_title) container->setWindowTitle(display_title); } if (g_log_window) g_log_window->updateWindowTitle(); } void MainWindow::updateWindowState(bool force_visible) { // Skip all of this when we're closing, since we don't want to make ourselves visible and cancel it. if (m_is_closing) return; const bool hide_window = !isRenderingToMain() && shouldHideMainWindow(); const bool disable_resize = Host::GetBoolSettingValue("UI", "DisableWindowResize", false); const bool has_window = s_vm_valid || m_display_widget; // Need to test both valid and display widget because of startup (vm invalid while window is created). const bool visible = force_visible || !hide_window || !has_window; if (isVisible() != visible) setVisible(visible); // No point changing realizability if we're not visible. const bool resizeable = force_visible || !disable_resize || !has_window; if (visible) QtUtils::SetWindowResizeable(this, resizeable); // Update the display widget too if rendering separately. if (m_display_widget && !isRenderingToMain()) QtUtils::SetWindowResizeable(getDisplayContainer(), resizeable); } void MainWindow::setProgressBar(int current, int total) { const int value = (total != 0) ? ((current * 100) / total) : 0; if (m_status_progress_widget->value() != value) m_status_progress_widget->setValue(value); if (m_status_progress_widget->isVisible()) return; m_status_progress_widget->show(); m_ui.statusBar->addPermanentWidget(m_status_progress_widget); } void MainWindow::clearProgressBar() { if (!m_status_progress_widget->isVisible()) return; m_status_progress_widget->hide(); m_ui.statusBar->removeWidget(m_status_progress_widget); } bool MainWindow::isShowingGameList() const { if (s_use_central_widget) return (centralWidget() == m_game_list_widget); else return (m_ui.mainContainer->currentIndex() == 0); } bool MainWindow::isRenderingFullscreen() const { if (!MTGS::IsOpen() || !m_display_widget) return false; return getDisplayContainer()->isFullScreen(); } bool MainWindow::isRenderingToMain() const { if (s_use_central_widget) return (m_display_widget && centralWidget() == m_display_widget); else return (m_display_widget && m_ui.mainContainer->indexOf(m_display_widget) == 1); } bool MainWindow::shouldHideMouseCursor() const { return ((isRenderingFullscreen() && Host::GetBoolSettingValue("UI", "HideMouseCursor", false)) || m_relative_mouse_mode || m_hide_mouse_cursor); } bool MainWindow::shouldHideMainWindow() const { // NOTE: We can't use isRenderingToMain() here, because this happens post-fullscreen-switch. return (Host::GetBoolSettingValue("UI", "HideMainWindowWhenRunning", false) && !g_emu_thread->shouldRenderToMain()) || (g_emu_thread->shouldRenderToMain() && (isRenderingFullscreen() || m_is_temporarily_windowed)) || QtHost::InNoGUIMode(); } bool MainWindow::shouldAbortForMemcardBusy(const VMLock& lock) { if (MemcardBusy::IsBusy() && !GSDumpReplayer::IsReplayingDump()) { const QMessageBox::StandardButton res = QMessageBox::critical( lock.getDialogParent(), tr("WARNING: Memory Card Busy"), tr("WARNING: Your memory card is still writing data. Shutting down now WILL IRREVERSIBLY DESTROY YOUR MEMORY CARD. It is strongly recommended to resume your game and let it finish writing to your memory card.

Do you wish to shutdown anyways and IRREVERSIBLY DESTROY YOUR MEMORY CARD?"), QMessageBox::Yes | QMessageBox::No, QMessageBox::No); if (res != QMessageBox::Yes) { return true; } } return false; } void MainWindow::switchToGameListView() { if (isShowingGameList()) { m_game_list_widget->setFocus(); return; } if (m_display_created) { m_was_paused_on_surface_loss = s_vm_paused; if (!s_vm_paused) g_emu_thread->setVMPaused(true); // switch to surfaceless. we have to wait until the display widget is gone before we swap over. g_emu_thread->setSurfaceless(true); while (m_display_widget) QApplication::processEvents(QEventLoop::ExcludeUserInputEvents, 1); } } void MainWindow::switchToEmulationView() { if (!m_display_created || !isShowingGameList()) return; // we're no longer surfaceless! this will call back to UpdateDisplay(), which will swap the widget out. g_emu_thread->setSurfaceless(false); // resume if we weren't paused at switch time if (s_vm_paused && !m_was_paused_on_surface_loss) g_emu_thread->setVMPaused(false); if (m_display_widget) m_display_widget->setFocus(); } void MainWindow::refreshGameList(bool invalidate_cache) { // can't do this while the VM is running because of CDVD if (s_vm_valid) return; m_game_list_widget->refresh(invalidate_cache); } void MainWindow::cancelGameListRefresh() { m_game_list_widget->cancelRefresh(); } void MainWindow::reportError(const QString& title, const QString& message) { QMessageBox::critical(this, title, message); } bool MainWindow::confirmMessage(const QString& title, const QString& message) { VMLock lock(pauseAndLockVM()); return (QMessageBox::question(this, title, message) == QMessageBox::Yes); } void MainWindow::onStatusMessage(const QString& message) { m_ui.statusBar->showMessage(message); } void MainWindow::runOnUIThread(const std::function& func) { func(); } void MainWindow::requestReset() { if (!s_vm_valid) return; const auto lock = pauseAndLockVM(); // Check if memcard is busy, deny request if so if (shouldAbortForMemcardBusy(lock)) { return; } g_emu_thread->resetVM(); } bool MainWindow::requestShutdown(bool allow_confirm, bool allow_save_to_state, bool default_save_to_state) { if (!s_vm_valid) return true; // If we don't have a crc, we can't save state. allow_save_to_state &= (s_current_disc_crc != 0); bool save_state = allow_save_to_state && default_save_to_state; VMLock lock(pauseAndLockVM()); // Check if memcard is busy, deny request if so. if (shouldAbortForMemcardBusy(lock)) { return false; } // Only confirm on UI thread because we need to display a msgbox. if (!m_is_closing && allow_confirm && !GSDumpReplayer::IsReplayingDump() && Host::GetBoolSettingValue("UI", "ConfirmShutdown", true)) { QMessageBox msgbox(lock.getDialogParent()); msgbox.setIcon(QMessageBox::Question); msgbox.setWindowTitle(tr("Confirm Shutdown")); msgbox.setWindowModality(Qt::WindowModal); msgbox.setWindowIcon(QtHost::GetAppIcon()); msgbox.setText(tr("Are you sure you want to shut down the virtual machine?")); QCheckBox* save_cb = new QCheckBox(tr("Save State For Resume"), &msgbox); save_cb->setChecked(save_state); save_cb->setEnabled(allow_save_to_state); msgbox.setCheckBox(save_cb); msgbox.addButton(QMessageBox::Yes); msgbox.addButton(QMessageBox::No); msgbox.setDefaultButton(QMessageBox::Yes); if (msgbox.exec() != QMessageBox::Yes) return false; save_state = save_cb->isChecked(); // Don't switch back to fullscreen when we're shutting down anyway. lock.cancelResume(); } // This is a little bit annoying. Qt will close everything down if we don't have at least one window visible, // but we might not be visible because the user is using render-to-separate and hide. We don't want to always // reshow the main window during display updates, because otherwise fullscreen transitions and renderer switches // would briefly show and then hide the main window. So instead, we do it on shutdown, here. Except if we're in // batch mode, when we're going to exit anyway. if (!isRenderingToMain() && isHidden() && !QtHost::InBatchMode() && !g_emu_thread->isRunningFullscreenUI()) updateWindowState(true); // Clear the VM valid state early. That way we can't do anything in the UI if we take a while to shut down. if (s_vm_valid) { s_vm_valid = false; updateEmulationActions(false, false, true); updateDisplayRelatedActions(false, false, false); } // Now we can actually shut down the VM. g_emu_thread->shutdownVM(save_state); return true; } void MainWindow::requestExit(bool allow_confirm) { // requestShutdown() clears this flag. const bool vm_was_valid = QtHost::IsVMValid(); // this is block, because otherwise closeEvent() will also prompt if (!requestShutdown(allow_confirm, true, EmuConfig.SaveStateOnShutdown)) return; // VM stopped signal won't have fired yet, so queue an exit if we still have one. // Otherwise, immediately exit, because there's no VM to exit us later. if (vm_was_valid) m_is_closing = true; else quit(); } void MainWindow::checkForSettingChanges() { if (m_display_widget) updateDisplayWidgetCursor(); updateWindowState(); LogWindow::updateSettings(); } std::optional MainWindow::getWindowInfo() { if (!m_display_widget || isRenderingToMain()) return QtUtils::GetWindowInfoForWidget(this); else if (QWidget* widget = getDisplayContainer()) return QtUtils::GetWindowInfoForWidget(widget); else return std::nullopt; } void MainWindow::onGameListRefreshProgress(const QString& status, int current, int total) { m_ui.statusBar->showMessage(status); setProgressBar(current, total); } void MainWindow::onGameListRefreshComplete() { m_ui.statusBar->clearMessage(); clearProgressBar(); } void MainWindow::onGameListSelectionChanged() { auto lock = GameList::GetLock(); const GameList::Entry* entry = m_game_list_widget->getSelectedEntry(); if (!entry) return; m_ui.statusBar->showMessage(QString::fromStdString(entry->path)); } void MainWindow::onGameListEntryActivated() { auto lock = GameList::GetLock(); const GameList::Entry* entry = m_game_list_widget->getSelectedEntry(); if (!entry) return; if (s_vm_valid) { // change disc on double click if (!entry->IsDisc()) { QMessageBox::critical(this, tr("Error"), tr("You must select a disc to change discs.")); return; } doDiscChange(CDVD_SourceType::Iso, QString::fromStdString(entry->path)); return; } // we might still be saving a resume state... VMManager::WaitForSaveStateFlush(); const std::optional resume = promptForResumeState(QString::fromStdString(VMManager::GetSaveStateFileName(entry->serial.c_str(), entry->crc, -1))); if (!resume.has_value()) { // cancelled return; } // only resume if the option is enabled, and we have one for this game startGameListEntry(entry, resume.value() ? std::optional(-1) : std::optional(), std::nullopt); } void MainWindow::onGameListEntryContextMenuRequested(const QPoint& point) { auto lock = GameList::GetLock(); const GameList::Entry* entry = m_game_list_widget->getSelectedEntry(); QMenu menu; if (entry) { QAction* action = menu.addAction(tr("Properties...")); action->setEnabled(!entry->serial.empty() || entry->type == GameList::EntryType::ELF); if (action->isEnabled()) { connect(action, &QAction::triggered, [entry]() { SettingsWindow::openGamePropertiesDialog(entry, entry->title, (entry->type != GameList::EntryType::ELF) ? entry->serial : std::string(), entry->crc); }); } action = menu.addAction(QtUtils::GetShowInFileExplorerMessage()); connect(action, &QAction::triggered, [this, entry]() { const QFileInfo fi(QString::fromStdString(entry->path)); QtUtils::ShowInFileExplorer(this, fi); }); action = menu.addAction(tr("Set Cover Image...")); connect(action, &QAction::triggered, [this, entry]() { setGameListEntryCoverImage(entry); }); connect(menu.addAction(tr("Exclude From List")), &QAction::triggered, [this, entry]() { getSettingsWindow()->getGameListSettingsWidget()->addExcludedPath(entry->path); }); connect(menu.addAction(tr("Reset Play Time")), &QAction::triggered, [this, entry]() { clearGameListEntryPlayTime(entry); }); if (!entry->serial.empty()) { connect(menu.addAction(tr("Check Wiki Page")), &QAction::triggered, [this, entry]() { goToWikiPage(entry); }); } menu.addSeparator(); if (!s_vm_valid) { action = menu.addAction(tr("Default Boot")); connect(action, &QAction::triggered, [this, entry]() { startGameListEntry(entry); }); // Make bold to indicate it's the default choice when double-clicking if (!VMManager::HasSaveStateInSlot(entry->serial.c_str(), entry->crc, -1)) QtUtils::MarkActionAsDefault(action); action = menu.addAction(tr("Fast Boot")); connect(action, &QAction::triggered, [this, entry]() { startGameListEntry(entry, std::nullopt, true); }); action = menu.addAction(tr("Full Boot")); connect(action, &QAction::triggered, [this, entry]() { startGameListEntry(entry, std::nullopt, false); }); if (m_ui.menuDebug->menuAction()->isVisible()) { action = menu.addAction(tr("Boot and Debug")); connect(action, &QAction::triggered, [this, entry]() { DebugInterface::setPauseOnEntry(true); startGameListEntry(entry); getDebuggerWindow()->show(); }); } menu.addSeparator(); populateLoadStateMenu(&menu, QString::fromStdString(entry->path), QString::fromStdString(entry->serial), entry->crc); } else if (entry->IsDisc()) { action = menu.addAction(tr("Change Disc")); connect(action, &QAction::triggered, [this, entry]() { g_emu_thread->changeDisc(CDVD_SourceType::Iso, QString::fromStdString(entry->path)); switchToEmulationView(); }); QtUtils::MarkActionAsDefault(action); } menu.addSeparator(); } connect(menu.addAction(tr("Add Search Directory...")), &QAction::triggered, [this]() { getSettingsWindow()->getGameListSettingsWidget()->addSearchDirectory(this); }); menu.exec(point); } void MainWindow::onStartFileActionTriggered() { const QString path( QDir::toNativeSeparators(QFileDialog::getOpenFileName(this, tr("Start File"), QString(), tr(OPEN_FILE_FILTER), nullptr))); if (path.isEmpty()) return; doStartFile(std::nullopt, path); } void MainWindow::onStartDiscActionTriggered() { QString path(getDiscDevicePath(tr("Start Disc"))); if (path.isEmpty()) return; doStartFile(CDVD_SourceType::Disc, path); } void MainWindow::onStartBIOSActionTriggered() { std::shared_ptr params = std::make_shared(); g_emu_thread->startVM(std::move(params)); } void MainWindow::onChangeDiscFromFileActionTriggered() { VMLock lock(pauseAndLockVM()); QString filename = QDir::toNativeSeparators( QFileDialog::getOpenFileName(lock.getDialogParent(), tr("Select Disc Image"), QString(), tr(DISC_IMAGE_FILTER), nullptr)); if (filename.isEmpty()) return; g_emu_thread->changeDisc(CDVD_SourceType::Iso, filename); } void MainWindow::onChangeDiscFromGameListActionTriggered() { m_was_disc_change_request = true; switchToGameListView(); } void MainWindow::onChangeDiscFromDeviceActionTriggered() { QString path(getDiscDevicePath(tr("Change Disc"))); if (path.isEmpty()) return; g_emu_thread->changeDisc(CDVD_SourceType::Disc, path); } void MainWindow::onRemoveDiscActionTriggered() { g_emu_thread->changeDisc(CDVD_SourceType::NoDisc, QString()); } void MainWindow::onChangeDiscMenuAboutToShow() { // TODO: This is where we would populate the playlist if there is one. } void MainWindow::onChangeDiscMenuAboutToHide() { } void MainWindow::onLoadStateMenuAboutToShow() { m_ui.menuLoadState->clear(); populateLoadStateMenu(m_ui.menuLoadState, s_current_disc_path, s_current_disc_serial, s_current_disc_crc); } void MainWindow::onSaveStateMenuAboutToShow() { m_ui.menuSaveState->clear(); populateSaveStateMenu(m_ui.menuSaveState, s_current_disc_serial, s_current_disc_crc); } void MainWindow::onStartFullscreenUITriggered() { if (m_display_widget) g_emu_thread->stopFullscreenUI(); else g_emu_thread->startFullscreenUI(Host::GetBaseBoolSettingValue("UI", "StartFullscreen", false)); } void MainWindow::onFullscreenUIStateChange(bool running) { m_ui.actionStartFullscreenUI->setText(running ? tr("Stop Big Picture Mode") : tr("Start Big Picture Mode")); m_ui.actionToolbarStartFullscreenUI->setText(running ? tr("Exit Big Picture", "In Toolbar") : tr("Big Picture", "In Toolbar")); } void MainWindow::onViewToolbarActionToggled(bool checked) { Host::SetBaseBoolSettingValue("UI", "ShowToolbar", checked); Host::CommitBaseSettingChanges(); m_ui.toolBar->setVisible(checked); } void MainWindow::onViewLockToolbarActionToggled(bool checked) { Host::SetBaseBoolSettingValue("UI", "LockToolbar", checked); Host::CommitBaseSettingChanges(); m_ui.toolBar->setMovable(!checked); } void MainWindow::onViewStatusBarActionToggled(bool checked) { Host::SetBaseBoolSettingValue("UI", "ShowStatusBar", checked); Host::CommitBaseSettingChanges(); m_ui.statusBar->setVisible(checked); } void MainWindow::onViewGameListActionTriggered() { switchToGameListView(); m_game_list_widget->showGameList(); } void MainWindow::onViewGameGridActionTriggered() { switchToGameListView(); m_game_list_widget->showGameGrid(); } void MainWindow::onViewSystemDisplayTriggered() { if (m_display_created) switchToEmulationView(); } void MainWindow::onViewGamePropertiesActionTriggered() { if (!s_vm_valid) return; // prefer to use a game list entry, if we have one, that way the summary is populated if (!s_current_disc_path.isEmpty() || !s_current_elf_override.isEmpty()) { auto lock = GameList::GetLock(); const QString& path = (s_current_elf_override.isEmpty() ? s_current_disc_path : s_current_elf_override); const GameList::Entry* entry = GameList::GetEntryForPath(path.toUtf8().constData()); if (entry) { SettingsWindow::openGamePropertiesDialog( entry, entry->title, s_current_elf_override.isEmpty() ? entry->serial : std::string(), entry->crc); return; } } // open properties for the current running file (isn't in the game list) if (s_current_disc_crc == 0) { QMessageBox::critical(this, tr("Game Properties"), tr("Game properties is unavailable for the current game.")); return; } // can't use serial for ELFs, because they might have a disc set if (s_current_elf_override.isEmpty()) { SettingsWindow::openGamePropertiesDialog( nullptr, s_current_title.toStdString(), s_current_disc_serial.toStdString(), s_current_disc_crc); } else { SettingsWindow::openGamePropertiesDialog( nullptr, s_current_title.toStdString(), std::string(), s_current_disc_crc); } } void MainWindow::onGitHubRepositoryActionTriggered() { QtUtils::OpenURL(this, AboutDialog::getGitHubRepositoryUrl()); } void MainWindow::onSupportForumsActionTriggered() { QtUtils::OpenURL(this, AboutDialog::getSupportForumsUrl()); } void MainWindow::onDiscordServerActionTriggered() { QtUtils::OpenURL(this, AboutDialog::getDiscordServerUrl()); } void MainWindow::onAboutActionTriggered() { AboutDialog about(this); about.exec(); } void MainWindow::checkForUpdates(bool display_message, bool force_check) { if (!AutoUpdaterDialog::isSupported()) { if (display_message) { QMessageBox mbox(this); mbox.setWindowTitle(tr("Updater Error")); mbox.setWindowIcon(QtHost::GetAppIcon()); mbox.setTextFormat(Qt::RichText); QString message; #ifdef _WIN32 message = tr("

Sorry, you are trying to update a PCSX2 version which is not an official GitHub release. To " "prevent incompatibilities, the auto-updater is only enabled on official builds.

" "

To obtain an official build, please download from the link below:

" "

https://pcsx2.net/downloads/

"); #else message = tr("Automatic updating is not supported on the current platform."); #endif mbox.setText(message); mbox.setIcon(QMessageBox::Critical); mbox.exec(); } return; } if (m_auto_updater_dialog) return; if (force_check) { // Wipe out the last version, that way it displays the update if we've previously skipped it. Host::RemoveBaseSettingValue("AutoUpdater", "LastVersion"); Host::CommitBaseSettingChanges(); } m_auto_updater_dialog = new AutoUpdaterDialog(this); connect(m_auto_updater_dialog, &AutoUpdaterDialog::updateCheckCompleted, this, &MainWindow::onUpdateCheckComplete); m_auto_updater_dialog->queueUpdateCheck(display_message); } void MainWindow::onUpdateCheckComplete() { if (!m_auto_updater_dialog) return; m_auto_updater_dialog->deleteLater(); m_auto_updater_dialog = nullptr; } void MainWindow::startupUpdateCheck() { if (!Host::GetBaseBoolSettingValue("AutoUpdater", "CheckAtStartup", true)) return; checkForUpdates(false, false); } void MainWindow::onToolsOpenDataDirectoryTriggered() { const QString path(QString::fromStdString(EmuFolders::DataRoot)); QtUtils::OpenURL(this, QUrl::fromLocalFile(path)); } void MainWindow::onToolsCoverDownloaderTriggered() { // This can be invoked via big picture, so exit fullscreen. VMLock lock(pauseAndLockVM()); CoverDownloadDialog dlg(lock.getDialogParent()); connect(&dlg, &CoverDownloadDialog::coverRefreshRequested, m_game_list_widget, &GameListWidget::refreshGridCovers); dlg.exec(); } void MainWindow::onToolsEditCheatsPatchesTriggered(bool cheats) { if (s_current_disc_serial.isEmpty() || s_current_running_crc == 0) return; const std::string path = Patch::GetPnachFilename(s_current_disc_serial.toStdString(), s_current_running_crc, cheats); if (!FileSystem::FileExists(path.c_str())) { if (QMessageBox::question(this, tr("Confirm File Creation"), tr("The pnach file '%1' does not currently exist. Do you want to create it?") .arg(QtUtils::StringViewToQString(Path::GetFileName(path))), QMessageBox::Yes, QMessageBox::No) != QMessageBox::Yes) { return; } if (!FileSystem::WriteStringToFile(path.c_str(), std::string_view())) { QMessageBox::critical(this, tr("Error"), tr("Failed to create '%1'.").arg(QString::fromStdString(path))); return; } } QtUtils::OpenURL(this, QUrl::fromLocalFile(QString::fromStdString(path))); } void MainWindow::onCreateMemoryCardOpenRequested() { // This can be invoked via big picture, so exit fullscreen. VMLock lock(pauseAndLockVM()); MemoryCardCreateDialog dlg(lock.getDialogParent()); dlg.exec(); } void MainWindow::updateTheme() { // The debugger hates theme changes. // We have unfortunately to destroy it and recreate it. const bool debugger_is_open = m_debugger_window ? m_debugger_window->isVisible() : false; const QSize debugger_size = m_debugger_window ? m_debugger_window->size() : QSize(); const QPoint debugger_pos = m_debugger_window ? m_debugger_window->pos() : QPoint(); if (m_debugger_window) { if (QMessageBox::question(this, tr("Theme Change"), tr("Changing the theme will close the debugger window. Any unsaved data will be lost. Do you want to continue?"), QMessageBox::Yes | QMessageBox::No) == QMessageBox::No) { return; } } QtHost::UpdateApplicationTheme(); reloadThemeSpecificImages(); if (m_debugger_window) { m_debugger_window->deleteLater(); m_debugger_window = nullptr; getDebuggerWindow(); // populates m_debugger_window m_debugger_window->resize(debugger_size); m_debugger_window->move(debugger_pos); if (debugger_is_open) { m_debugger_window->show(); } } } void MainWindow::reloadThemeSpecificImages() { m_game_list_widget->reloadThemeSpecificImages(); } void MainWindow::updateLanguage() { // Remove the settings window, so it doesn't mess with any popups that happen (e.g. font download). destroySubWindows(); QtHost::InstallTranslator(this); recreate(); } void MainWindow::onThemeChanged() { [[maybe_unused]] const QString old_style_name = qApp->style()->name(); updateTheme(); #ifdef _WIN32 // Work around a bug where the background colour of menus is broken when changing to/from the windowsvista theme. const QString new_style_name = qApp->style()->name(); if ((old_style_name == QStringLiteral("windowsvista")) != (new_style_name == QStringLiteral("windowsvista"))) recreate(); #endif // Reopen settings dialog after it applies. If you doSettings now, on macOS, the window will somehow end up // underneath the main window that was created above. Delay it slightly... QtHost::RunOnUIThread([] { g_main_window->doSettings("Interface"); }); } void MainWindow::onLanguageChanged() { // reopen settings dialog after it applies updateLanguage(); // If you doSettings now, on macOS, the window will somehow end up underneath the main window that was created above // Delay it slightly... QtHost::RunOnUIThread([] { g_main_window->doSettings("Interface"); }); } void MainWindow::onInputRecNewActionTriggered() { const bool wasPaused = s_vm_paused; const bool wasRunning = s_vm_valid; if (wasRunning && !wasPaused) { g_emu_thread->setVMPaused(true); } NewInputRecordingDlg dlg(this); const auto result = dlg.exec(); if (result == QDialog::Accepted) { Host::RunOnCPUThread( [&, filePath = dlg.getFilePath(), fromSavestate = dlg.getInputRecType() == InputRecording::Type::FROM_SAVESTATE, authorName = dlg.getAuthorName()]() { if (g_InputRecording.create(filePath, fromSavestate, authorName)) { QtHost::RunOnUIThread([&]() { m_ui.actionInputRecNew->setEnabled(false); m_ui.actionInputRecStop->setEnabled(true); m_ui.actionReset->setEnabled(!g_InputRecording.isTypeSavestate()); m_ui.actionToolbarReset->setEnabled(!g_InputRecording.isTypeSavestate()); }); } else { Host::ReportErrorAsync( TRANSLATE_SV("MainWindow", "Input Recording Failed"), fmt::format(TRANSLATE_FS("MainWindow", "Failed to create file: {}"), filePath)); } }); } if (wasRunning && !wasPaused) { g_emu_thread->setVMPaused(false); } } void MainWindow::onInputRecPlayActionTriggered() { const bool wasPaused = s_vm_paused; if (!wasPaused) { g_emu_thread->setVMPaused(true); } QFileDialog dialog(this); dialog.setFileMode(QFileDialog::ExistingFile); dialog.setWindowTitle("Select a File"); dialog.setNameFilter(tr("Input Recording Files (*.p2m2)")); QStringList fileNames; if (dialog.exec()) { fileNames = dialog.selectedFiles(); } else { if (!wasPaused) { g_emu_thread->setVMPaused(false); return; } } if (fileNames.length() > 0) { if (g_InputRecording.isActive()) { Host::RunOnCPUThread([]() { g_InputRecording.stop(); }); m_ui.actionInputRecStop->setEnabled(false); } Host::RunOnCPUThread([&, filename = QDir::toNativeSeparators(fileNames.first()).toStdString()]() { if (g_InputRecording.play(filename)) { QtHost::RunOnUIThread([&]() { m_ui.actionInputRecNew->setEnabled(false); m_ui.actionInputRecStop->setEnabled(true); m_ui.actionReset->setEnabled(!g_InputRecording.isTypeSavestate()); m_ui.actionToolbarReset->setEnabled(!g_InputRecording.isTypeSavestate()); }); } else { Host::ReportErrorAsync( TRANSLATE_SV("MainWindow", "Input Playback Failed"), fmt::format(TRANSLATE_FS("MainWindow", "Failed to open file: {}"), filename)); } }); } } void MainWindow::onInputRecStopActionTriggered() { if (g_InputRecording.isActive()) { Host::RunOnCPUThread([&]() { g_InputRecording.stop(); QtHost::RunOnUIThread([&]() { m_ui.actionInputRecNew->setEnabled(true); m_ui.actionInputRecStop->setEnabled(false); m_ui.actionReset->setEnabled(true); m_ui.actionToolbarReset->setEnabled(true); }); }); } } InputRecordingViewer* MainWindow::getInputRecordingViewer() { if (!m_input_recording_viewer) { m_input_recording_viewer = new InputRecordingViewer(this); } return m_input_recording_viewer; } void MainWindow::updateInputRecordingActions(bool started) { m_ui.actionInputRecNew->setEnabled(started); m_ui.actionInputRecPlay->setEnabled(started); } void MainWindow::onInputRecOpenViewer() { InputRecordingViewer* viewer = getInputRecordingViewer(); if (!viewer->isVisible()) { viewer->show(); } } void MainWindow::onVMStarting() { s_vm_valid = true; updateEmulationActions(true, false, false); updateWindowTitle(); } void MainWindow::onVMStarted() { s_vm_valid = true; m_was_disc_change_request = false; updateEmulationActions(true, true, false); updateGameDependentActions(); updateWindowTitle(); updateStatusBarWidgetVisibility(); updateInputRecordingActions(true); } void MainWindow::onVMPaused() { // update UI { QSignalBlocker sb(m_ui.actionPause); m_ui.actionPause->setChecked(true); } { QSignalBlocker sb(m_ui.actionToolbarPause); m_ui.actionToolbarPause->setChecked(true); } s_vm_paused = true; updateWindowTitle(); updateStatusBarWidgetVisibility(); m_last_fps_status = m_status_verbose_widget->text(); m_status_verbose_widget->setText(tr("Paused")); if (m_display_widget) updateDisplayWidgetCursor(); } void MainWindow::onVMResumed() { // update UI { QSignalBlocker sb(m_ui.actionPause); m_ui.actionPause->setChecked(false); } { QSignalBlocker sb(m_ui.actionToolbarPause); m_ui.actionToolbarPause->setChecked(false); } s_vm_paused = false; m_was_disc_change_request = false; updateWindowTitle(); updateStatusBarWidgetVisibility(); m_status_verbose_widget->setText(m_last_fps_status); m_last_fps_status = QString(); if (m_display_widget) { updateDisplayWidgetCursor(); m_display_widget->setFocus(); } } void MainWindow::onVMStopped() { s_vm_valid = false; s_vm_paused = false; const QString empty_string; m_last_fps_status = empty_string; m_status_renderer_widget->setText(empty_string); m_status_resolution_widget->setText(empty_string); m_status_fps_widget->setText(empty_string); m_status_vps_widget->setText(empty_string); m_status_speed_widget->setText(empty_string); m_status_verbose_widget->setText(empty_string); updateEmulationActions(false, false, false); updateGameDependentActions(); updateWindowTitle(); updateWindowState(); updateStatusBarWidgetVisibility(); updateInputRecordingActions(false); // If we're closing or in batch mode, quit the whole application now. if (m_is_closing || QtHost::InBatchMode()) { quit(); return; } if (m_display_widget) updateDisplayWidgetCursor(); else switchToGameListView(); // reload played time if (m_game_list_widget->isShowingGameList()) m_game_list_widget->refresh(false); } void MainWindow::onGameChanged(const QString& title, const QString& elf_override, const QString& disc_path, const QString& serial, quint32 disc_crc, quint32 crc) { s_current_title = title; s_current_elf_override = elf_override; s_current_disc_path = disc_path; s_current_disc_serial = serial; s_current_disc_crc = disc_crc; s_current_running_crc = crc; updateWindowTitle(); updateGameDependentActions(); } void MainWindow::showEvent(QShowEvent* event) { QMainWindow::showEvent(event); // This is a bit silly, but for some reason resizing *before* the window is shown // gives the incorrect sizes for columns, if you set the style before setting up // the rest of the window... so, instead, let's just force it to be resized on show. if (isShowingGameList()) m_game_list_widget->resizeTableViewColumnsToFit(); #ifdef ENABLE_RAINTEGRATION if (Achievements::IsUsingRAIntegration()) Achievements::RAIntegration::MainWindowChanged((void*)winId()); #endif } void MainWindow::closeEvent(QCloseEvent* event) { // If there's no VM, we can just exit as normal. if (!s_vm_valid || !m_display_created) { m_is_closing = true; saveStateToConfig(); if (m_display_created) g_emu_thread->stopFullscreenUI(); destroySubWindows(); QMainWindow::closeEvent(event); return; } // But if there is, we have to cancel the action, regardless of whether we ended exiting // or not. The window still needs to be visible while GS is shutting down. event->ignore(); // Exit cancelled? if (!requestShutdown(true, true, EmuConfig.SaveStateOnShutdown)) return; // Application will be exited in VM stopped handler. m_is_closing = true; } void MainWindow::changeEvent(QEvent* event) { QMainWindow::changeEvent(event); if (event->type() == QEvent::StyleChange) { QtHost::SetIconThemeFromStyle(); reloadThemeSpecificImages(); } } static QString getFilenameFromMimeData(const QMimeData* md) { QString filename; if (md->hasUrls()) { // only one url accepted const QList urls(md->urls()); if (urls.size() == 1) filename = QDir::toNativeSeparators(urls.front().toLocalFile()); } return filename; } void MainWindow::dragEnterEvent(QDragEnterEvent* event) { const std::string filename(getFilenameFromMimeData(event->mimeData()).toStdString()); // allow save states being dragged in if (!VMManager::IsLoadableFileName(filename) && !VMManager::IsSaveStateFileName(filename)) return; event->acceptProposedAction(); } void MainWindow::dropEvent(QDropEvent* event) { const auto mcLock = pauseAndLockVM(); // Check if memcard is busy, deny request if so if (shouldAbortForMemcardBusy(mcLock)) { return; } const QString filename(getFilenameFromMimeData(event->mimeData())); const std::string filename_str(filename.toStdString()); if (VMManager::IsSaveStateFileName(filename_str)) { event->acceptProposedAction(); // can't load a save state without a current VM if (s_vm_valid) g_emu_thread->loadState(filename); else QMessageBox::critical(this, tr("Load State Failed"), tr("Cannot load a save state without a running VM.")); return; } if (!VMManager::IsLoadableFileName(filename_str)) return; // if we're already running, do a disc change, otherwise start if (!s_vm_valid) { event->acceptProposedAction(); doStartFile(std::nullopt, filename); return; } if (VMManager::IsDiscFileName(filename_str) || VMManager::IsBlockDumpFileName(filename_str)) { event->acceptProposedAction(); doDiscChange(CDVD_SourceType::Iso, filename); } else if (VMManager::IsElfFileName(filename_str)) { const auto lock = pauseAndLockVM(); event->acceptProposedAction(); if (QMessageBox::question(this, tr("Confirm Reset"), tr("The new ELF cannot be loaded without resetting the virtual machine. Do you want to reset the virtual machine now?")) != QMessageBox::Yes) { return; } g_emu_thread->setELFOverride(filename); switchToEmulationView(); } else if (VMManager::IsGSDumpFileName(filename_str)) { event->acceptProposedAction(); if (!GSDumpReplayer::IsReplayingDump()) { QMessageBox::critical(this, tr("Error"), tr("Cannot change from game to GS dump without shutting down first.")); return; } g_emu_thread->changeGSDump(filename); switchToEmulationView(); } } void MainWindow::moveEvent(QMoveEvent* event) { QMainWindow::moveEvent(event); if (g_log_window && g_log_window->isAttachedToMainWindow()) g_log_window->reattachToMainWindow(); } void MainWindow::resizeEvent(QResizeEvent* event) { QMainWindow::resizeEvent(event); if (g_log_window && g_log_window->isAttachedToMainWindow()) g_log_window->reattachToMainWindow(); } void MainWindow::registerForDeviceNotifications() { #ifdef _WIN32 // We use these notifications to detect when a controller is connected or disconnected. DEV_BROADCAST_DEVICEINTERFACE_W filter = {sizeof(DEV_BROADCAST_DEVICEINTERFACE_W), DBT_DEVTYP_DEVICEINTERFACE}; m_device_notification_handle = RegisterDeviceNotificationW((HANDLE)winId(), &filter, DEVICE_NOTIFY_WINDOW_HANDLE | DEVICE_NOTIFY_ALL_INTERFACE_CLASSES); #endif } void MainWindow::unregisterForDeviceNotifications() { #ifdef _WIN32 if (!m_device_notification_handle) return; UnregisterDeviceNotification(static_cast(m_device_notification_handle)); m_device_notification_handle = nullptr; #endif } #ifdef _WIN32 bool MainWindow::nativeEvent(const QByteArray& eventType, void* message, qintptr* result) { static constexpr const char win_type[] = "windows_generic_MSG"; if (eventType == QByteArray(win_type, sizeof(win_type) - 1)) { const MSG* msg = static_cast(message); if (msg->message == WM_DEVICECHANGE && msg->wParam == DBT_DEVNODES_CHANGED) { g_emu_thread->reloadInputDevices(); *result = 1; return true; } } return QMainWindow::nativeEvent(eventType, message, result); } #endif std::optional MainWindow::acquireRenderWindow(bool recreate_window, bool fullscreen, bool render_to_main, bool surfaceless) { DevCon.WriteLn("acquireRenderWindow() recreate=%s fullscreen=%s render_to_main=%s surfaceless=%s", recreate_window ? "true" : "false", fullscreen ? "true" : "false", render_to_main ? "true" : "false", surfaceless ? "true" : "false"); QWidget* container = m_display_container ? static_cast(m_display_container) : static_cast(m_display_widget); const bool is_fullscreen = isRenderingFullscreen(); const bool is_rendering_to_main = isRenderingToMain(); const bool changing_surfaceless = (!m_display_widget != surfaceless); if (m_display_created && !recreate_window && fullscreen == is_fullscreen && is_rendering_to_main == render_to_main && !changing_surfaceless) { return m_display_widget ? m_display_widget->getWindowInfo() : WindowInfo(); } // Skip recreating the surface if we're just transitioning between fullscreen and windowed with render-to-main off. // .. except on Wayland, where everything tends to break if you don't recreate. const bool has_container = (m_display_container != nullptr); const bool needs_container = DisplayContainer::isNeeded(fullscreen, render_to_main); if (m_display_created && !recreate_window && !is_rendering_to_main && !render_to_main && has_container == needs_container && !needs_container && !changing_surfaceless) { DevCon.WriteLn("Toggling to %s without recreating surface", (fullscreen ? "fullscreen" : "windowed")); // since we don't destroy the display widget, we need to save it here if (!is_fullscreen && !is_rendering_to_main) saveDisplayWindowGeometryToConfig(); if (fullscreen) { container->showFullScreen(); } else { if (m_is_temporarily_windowed && g_emu_thread->shouldRenderToMain()) container->setGeometry(geometry()); else restoreDisplayWindowGeometryFromConfig(); container->showNormal(); } updateDisplayWidgetCursor(); m_display_widget->setFocus(); updateWindowState(); QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents); return m_display_widget->getWindowInfo(); } destroyDisplayWidget(surfaceless); m_display_created = true; // if we're going to surfaceless, we're done here if (surfaceless) return WindowInfo(); // very low-chance race here, if the user starts the fullscreen UI, and immediately closes the window. if (m_is_closing) { m_display_created = false; return std::nullopt; } createDisplayWidget(fullscreen, render_to_main); std::optional wi = m_display_widget->getWindowInfo(); if (!wi.has_value()) { QMessageBox::critical(this, tr("Error"), tr("Failed to get window info from widget")); destroyDisplayWidget(true); return std::nullopt; } g_emu_thread->connectDisplaySignals(m_display_widget); updateWindowTitle(); updateWindowState(); updateDisplayWidgetCursor(); m_display_widget->setFocus(); return wi; } void MainWindow::createDisplayWidget(bool fullscreen, bool render_to_main) { // If we're rendering to main and were hidden (e.g. coming back from fullscreen), // make sure we're visible before trying to add ourselves. Otherwise Wayland breaks. if (!fullscreen && render_to_main && !isVisible()) { setVisible(true); QGuiApplication::sync(); } QWidget* container; if (DisplayContainer::isNeeded(fullscreen, render_to_main)) { m_display_container = new DisplayContainer(); m_display_widget = new DisplayWidget(m_display_container); m_display_container->setDisplayWidget(m_display_widget); container = m_display_container; } else { m_display_widget = new DisplayWidget((!fullscreen && render_to_main) ? getContentParent() : nullptr); container = m_display_widget; } if (fullscreen || !render_to_main) { container->setWindowTitle(windowTitle()); container->setWindowIcon(windowIcon()); } if (fullscreen) { // Don't risk doing this on Wayland, it really doesn't like window state changes, // and positioning has no effect anyway. if (!s_use_central_widget) { if (isVisible() && g_emu_thread->shouldRenderToMain()) container->move(pos()); else restoreDisplayWindowGeometryFromConfig(); } container->showFullScreen(); } else if (!render_to_main) { if (m_is_temporarily_windowed && g_emu_thread->shouldRenderToMain()) container->setGeometry(geometry()); else restoreDisplayWindowGeometryFromConfig(); container->showNormal(); } else if (s_use_central_widget) { m_game_list_widget->setVisible(false); takeCentralWidget(); m_game_list_widget->setParent(this); // takeCentralWidget() removes parent setCentralWidget(m_display_widget); m_display_widget->setFocus(); update(); } else { pxAssertRel(m_ui.mainContainer->count() == 1, "Has no display widget"); m_ui.mainContainer->addWidget(container); m_ui.mainContainer->setCurrentIndex(1); } updateDisplayRelatedActions(true, render_to_main, fullscreen); // We need the surface visible. QGuiApplication::sync(); } void MainWindow::displayResizeRequested(qint32 width, qint32 height) { if (!m_display_widget) return; // unapply the pixel scaling factor for hidpi const float dpr = devicePixelRatioF(); width = static_cast(std::max(static_cast(std::lroundf(static_cast(width) / dpr)), 1)); height = static_cast(std::max(static_cast(std::lroundf(static_cast(height) / dpr)), 1)); if (m_display_container || !m_display_widget->parent()) { // no parent - rendering to separate window. easy. QtUtils::ResizePotentiallyFixedSizeWindow(getDisplayContainer(), width, height); return; } // we are rendering to the main window. we have to add in the extra height from the toolbar/status bar. const s32 extra_height = this->height() - m_display_widget->height(); QtUtils::ResizePotentiallyFixedSizeWindow(this, width, height + extra_height); } void MainWindow::mouseModeRequested(bool relative_mode, bool hide_cursor) { if (m_relative_mouse_mode == relative_mode && m_hide_mouse_cursor == hide_cursor) return; m_relative_mouse_mode = relative_mode; m_hide_mouse_cursor = hide_cursor; if (m_display_widget && !s_vm_paused) updateDisplayWidgetCursor(); } void MainWindow::releaseRenderWindow() { // Now we can safely destroy the display window. destroyDisplayWidget(true); m_display_created = false; m_ui.actionViewSystemDisplay->setEnabled(false); m_ui.actionFullscreen->setEnabled(false); } void MainWindow::destroyDisplayWidget(bool show_game_list) { if (!m_display_widget) return; if (!isRenderingFullscreen() && !isRenderingToMain()) saveDisplayWindowGeometryToConfig(); if (m_display_container) m_display_container->removeDisplayWidget(); if (isRenderingToMain()) { if (s_use_central_widget) { pxAssertRel(centralWidget() == m_display_widget, "Display widget is currently central"); takeCentralWidget(); if (show_game_list) { m_game_list_widget->setVisible(true); setCentralWidget(m_game_list_widget); m_game_list_widget->resizeTableViewColumnsToFit(); } } else { pxAssertRel(m_ui.mainContainer->indexOf(m_display_widget) == 1, "Display widget in stack"); m_ui.mainContainer->removeWidget(m_display_widget); if (show_game_list) { m_ui.mainContainer->setCurrentIndex(0); m_game_list_widget->resizeTableViewColumnsToFit(); } } } if (m_display_widget) { m_display_widget->destroy(); m_display_widget = nullptr; } if (m_display_container) { m_display_container->deleteLater(); m_display_container = nullptr; } updateDisplayRelatedActions(false, false, false); } void MainWindow::updateDisplayWidgetCursor() { pxAssertRel(m_display_widget, "Should have a display widget"); m_display_widget->updateRelativeMode(s_vm_valid && !s_vm_paused && m_relative_mouse_mode); m_display_widget->updateCursor(s_vm_valid && !s_vm_paused && shouldHideMouseCursor()); } void MainWindow::focusDisplayWidget() { if (!m_display_widget || centralWidget() != m_display_widget) return; m_display_widget->setFocus(); } QWidget* MainWindow::getDisplayContainer() const { return (m_display_container ? static_cast(m_display_container) : static_cast(m_display_widget)); } void MainWindow::saveDisplayWindowGeometryToConfig() { QWidget* container = getDisplayContainer(); if (container->windowState() & Qt::WindowFullScreen) { // if we somehow ended up here, don't save the fullscreen state to the config return; } const QByteArray geometry = getDisplayContainer()->saveGeometry(); const QByteArray geometry_b64 = geometry.toBase64(); const std::string old_geometry_b64 = Host::GetBaseStringSettingValue("UI", "DisplayWindowGeometry"); if (old_geometry_b64 != geometry_b64.constData()) { Host::SetBaseStringSettingValue("UI", "DisplayWindowGeometry", geometry_b64.constData()); Host::CommitBaseSettingChanges(); } } void MainWindow::restoreDisplayWindowGeometryFromConfig() { const std::string geometry_b64 = Host::GetBaseStringSettingValue("UI", "DisplayWindowGeometry"); const QByteArray geometry = QByteArray::fromBase64(QByteArray::fromStdString(geometry_b64)); QWidget* container = getDisplayContainer(); if (!geometry.isEmpty()) { container->restoreGeometry(geometry); // make sure we're not loading a dodgy config which had fullscreen set... container->setWindowState(container->windowState() & ~(Qt::WindowFullScreen | Qt::WindowActive)); } else { // default size container->resize(640, 480); } } SettingsWindow* MainWindow::getSettingsWindow() { if (!m_settings_window) { m_settings_window = new SettingsWindow(); connect(m_settings_window->getInterfaceSettingsWidget(), &InterfaceSettingsWidget::themeChanged, this, &MainWindow::onThemeChanged); connect(m_settings_window->getInterfaceSettingsWidget(), &InterfaceSettingsWidget::languageChanged, this, &MainWindow::onLanguageChanged); connect(m_settings_window->getGameListSettingsWidget(), &GameListSettingsWidget::preferEnglishGameListChanged, this, [] { g_main_window->m_game_list_widget->refreshGridCovers(); }); } return m_settings_window; } void MainWindow::doSettings(const char* category /* = nullptr */) { SettingsWindow* dlg = getSettingsWindow(); if (!dlg->isVisible()) { dlg->show(); } else { dlg->raise(); dlg->activateWindow(); dlg->setFocus(); } if (category) dlg->setCategory(category); } DebuggerWindow* MainWindow::getDebuggerWindow() { if (!m_debugger_window) // Don't pass us (this) as the parent, otherwise the window is always on top of the mainwindow (on windows at least) m_debugger_window = new DebuggerWindow(nullptr); return m_debugger_window; } void MainWindow::openDebugger() { DebuggerWindow* dwnd = getDebuggerWindow(); dwnd->isVisible() ? dwnd->activateWindow() : dwnd->show(); } void MainWindow::doControllerSettings(ControllerSettingsWindow::Category category) { if (!m_controller_settings_window) m_controller_settings_window = new ControllerSettingsWindow(); if (!m_controller_settings_window->isVisible()) { m_controller_settings_window->show(); } else { m_controller_settings_window->raise(); m_controller_settings_window->activateWindow(); m_controller_settings_window->setFocus(); } if (category != ControllerSettingsWindow::Category::Count) m_controller_settings_window->setCategory(category); } QString MainWindow::getDiscDevicePath(const QString& title) { QString ret; const std::vector devices(GetOpticalDriveList()); if (devices.empty()) { QMessageBox::critical(this, title, tr("Could not find any CD/DVD-ROM devices. Please ensure you have a drive connected and " "sufficient permissions to access it.")); return ret; } // if there's only one, select it automatically if (devices.size() == 1) { ret = QString::fromStdString(devices.front()); return ret; } QStringList input_options; for (const std::string& name : devices) input_options.append(QString::fromStdString(name)); QInputDialog input_dialog(this); input_dialog.setWindowTitle(title); input_dialog.setLabelText(tr("Select disc drive:")); input_dialog.setInputMode(QInputDialog::TextInput); input_dialog.setOptions(QInputDialog::UseListViewForComboBoxItems); input_dialog.setComboBoxEditable(false); input_dialog.setComboBoxItems(std::move(input_options)); if (input_dialog.exec() == 0) return ret; ret = input_dialog.textValue(); return ret; } void MainWindow::startGameListEntry(const GameList::Entry* entry, std::optional save_slot, std::optional fast_boot) { std::shared_ptr params = std::make_shared(); params->fast_boot = fast_boot; GameList::FillBootParametersForEntry(params.get(), entry); if (save_slot.has_value() && !entry->serial.empty()) { std::string state_filename = VMManager::GetSaveStateFileName(entry->serial.c_str(), entry->crc, save_slot.value()); if (!FileSystem::FileExists(state_filename.c_str())) { QMessageBox::critical(this, tr("Error"), tr("This save state does not exist.")); return; } params->save_state = std::move(state_filename); } g_emu_thread->startVM(std::move(params)); } void MainWindow::setGameListEntryCoverImage(const GameList::Entry* entry) { const QString filename = QDir::toNativeSeparators( QFileDialog::getOpenFileName(this, tr("Select Cover Image"), QString(), tr("All Cover Image Types (*.jpg *.jpeg *.png *.webp)"))); if (filename.isEmpty()) return; const QString old_filename = QString::fromStdString(GameList::GetCoverImagePathForEntry(entry)); const QString new_filename = QString::fromStdString(GameList::GetNewCoverImagePathForEntry(entry, filename.toUtf8().constData(), true)); if (new_filename.isEmpty()) return; if (!old_filename.isEmpty()) { if (QFileInfo(old_filename) == QFileInfo(filename)) { QMessageBox::critical(this, tr("Copy Error"), tr("You must select a different file to the current cover image.")); return; } if (QMessageBox::question(this, tr("Cover Already Exists"), tr("A cover image for this game already exists, do you wish to replace it?"), QMessageBox::Yes, QMessageBox::No) != QMessageBox::Yes) { return; } } if (QFile::exists(new_filename) && !QFile::remove(new_filename)) { QMessageBox::critical(this, tr("Copy Error"), tr("Failed to remove existing cover '%1'").arg(new_filename)); return; } if (!QFile::copy(filename, new_filename)) { QMessageBox::critical(this, tr("Copy Error"), tr("Failed to copy '%1' to '%2'").arg(filename).arg(new_filename)); return; } if (!old_filename.isEmpty() && old_filename != new_filename && !QFile::remove(old_filename)) { QMessageBox::critical(this, tr("Copy Error"), tr("Failed to remove '%1'").arg(old_filename)); return; } m_game_list_widget->refreshGridCovers(); } void MainWindow::clearGameListEntryPlayTime(const GameList::Entry* entry) { if (QMessageBox::question(this, tr("Confirm Reset"), tr("Are you sure you want to reset the play time for '%1'?\n\nThis action cannot be undone.") .arg(QString::fromStdString(entry->title))) != QMessageBox::Yes) { return; } GameList::ClearPlayedTimeForSerial(entry->serial); m_game_list_widget->refresh(false); } void MainWindow::goToWikiPage(const GameList::Entry* entry) { QtUtils::OpenURL(this, fmt::format("https://wiki.pcsx2.net/{}", entry->serial).c_str()); } std::optional MainWindow::promptForResumeState(const QString& save_state_path) { if (save_state_path.isEmpty()) return false; QFileInfo fi(save_state_path); if (!fi.exists()) return false; QMessageBox msgbox(this); msgbox.setIcon(QMessageBox::Question); msgbox.setWindowIcon(QtHost::GetAppIcon()); msgbox.setWindowTitle(tr("Load Resume State")); msgbox.setWindowModality(Qt::WindowModal); msgbox.setText( tr("A resume save state was found for this game, saved at:\n\n%1.\n\nDo you want to load this state, or start from a fresh boot?") .arg(fi.lastModified().toLocalTime().toString())); QPushButton* load = msgbox.addButton(tr("Load State"), QMessageBox::AcceptRole); QPushButton* boot = msgbox.addButton(tr("Fresh Boot"), QMessageBox::RejectRole); QPushButton* delboot = msgbox.addButton(tr("Delete And Boot"), QMessageBox::RejectRole); msgbox.addButton(QMessageBox::Cancel); msgbox.setDefaultButton(load); msgbox.exec(); QAbstractButton* clicked = msgbox.clickedButton(); if (load == clicked) { return true; } else if (boot == clicked) { return false; } else if (delboot == clicked) { if (!QFile::remove(save_state_path)) QMessageBox::critical(this, tr("Error"), tr("Failed to delete save state file '%1'.").arg(save_state_path)); return false; } return std::nullopt; } void MainWindow::loadSaveStateSlot(s32 slot) { if (s_vm_valid) { // easy when we're running g_emu_thread->loadStateFromSlot(slot); return; } else { // we're not currently running, therefore we must've right clicked in the game list const GameList::Entry* entry = m_game_list_widget->getSelectedEntry(); if (!entry) return; startGameListEntry(entry, slot, std::nullopt); } } void MainWindow::loadSaveStateFile(const QString& filename, const QString& state_filename) { if (s_vm_valid) { if (!filename.isEmpty() && s_current_disc_path != filename) g_emu_thread->changeDisc(CDVD_SourceType::Iso, s_current_disc_path); g_emu_thread->loadState(state_filename); } else { std::shared_ptr params = std::make_shared(); params->filename = filename.toStdString(); params->save_state = state_filename.toStdString(); g_emu_thread->startVM(std::move(params)); } } static QString formatTimestampForSaveStateMenu(time_t timestamp) { const QDateTime qtime(QDateTime::fromSecsSinceEpoch(static_cast(timestamp))); return qtime.toString(QLocale::system().dateTimeFormat(QLocale::ShortFormat)); } void MainWindow::populateLoadStateMenu(QMenu* menu, const QString& filename, const QString& serial, quint32 crc) { if (serial.isEmpty()) return; const bool is_right_click_menu = (menu != m_ui.menuLoadState); bool has_any_states = false; QAction* action = menu->addAction(is_right_click_menu ? tr("Load State File...") : tr("Load From File...")); connect(action, &QAction::triggered, [this, filename]() { const QString path = QDir::toNativeSeparators(QFileDialog::getOpenFileName(this, tr("Select Save State File"), QString(), tr("Save States (*.p2s *.p2s.backup)"))); if (path.isEmpty()) return; loadSaveStateFile(filename, path); }); QAction* delete_save_states_action = menu->addAction(tr("Delete Save States...")); // don't include undo in the right click menu if (!is_right_click_menu) { QAction* load_undo_state = menu->addAction(tr("Undo Load State")); load_undo_state->setEnabled(false); // CanUndoLoadState() // connect(load_undo_state, &QAction::triggered, this, &QtHostInterface::undoLoadState); menu->addSeparator(); } const QByteArray game_serial_utf8(serial.toUtf8()); std::string state_filename; FILESYSTEM_STAT_DATA sd; if (is_right_click_menu) { state_filename = VMManager::GetSaveStateFileName(game_serial_utf8.constData(), crc, -1); if (FileSystem::StatFile(state_filename.c_str(), &sd)) { action = menu->addAction(tr("Resume (%2)").arg(formatTimestampForSaveStateMenu(sd.ModificationTime))); connect(action, &QAction::triggered, [this]() { loadSaveStateSlot(-1); }); // Make bold to indicate it's the default choice when double-clicking QtUtils::MarkActionAsDefault(action); has_any_states = true; } } for (s32 i = 1; i <= VMManager::NUM_SAVE_STATE_SLOTS; i++) { FILESYSTEM_STAT_DATA sd; state_filename = VMManager::GetSaveStateFileName(game_serial_utf8.constData(), crc, i); if (!FileSystem::StatFile(state_filename.c_str(), &sd)) continue; action = menu->addAction(tr("Load Slot %1 (%2)").arg(i).arg(formatTimestampForSaveStateMenu(sd.ModificationTime))); connect(action, &QAction::triggered, [this, i]() { loadSaveStateSlot(i); }); has_any_states = true; } delete_save_states_action->setEnabled(has_any_states); if (has_any_states) { connect(delete_save_states_action, &QAction::triggered, this, [this, serial, crc] { if (QMessageBox::warning(this, tr("Delete Save States"), tr("Are you sure you want to delete all save states for %1?\n\nThe saves will not be recoverable.").arg(serial), QMessageBox::Yes, QMessageBox::No) != QMessageBox::Yes) { return; } const u32 deleted = VMManager::DeleteSaveStates(serial.toUtf8().constData(), crc, true); QMessageBox::information(this, tr("Delete Save States"), tr("%1 save states deleted.").arg(deleted)); }); } } void MainWindow::populateSaveStateMenu(QMenu* menu, const QString& serial, quint32 crc) { if (serial.isEmpty()) return; connect(menu->addAction(tr("Save To File...")), &QAction::triggered, [this]() { const QString path = QDir::toNativeSeparators(QFileDialog::getSaveFileName( this, tr("Select Save State File"), QString(), tr("Save States (*.p2s)"))); if (path.isEmpty()) return; g_emu_thread->saveState(path); }); menu->addSeparator(); const QByteArray game_serial_utf8(serial.toUtf8()); for (s32 i = 1; i <= VMManager::NUM_SAVE_STATE_SLOTS; i++) { std::string filename(VMManager::GetSaveStateFileName(game_serial_utf8.constData(), crc, i)); FILESYSTEM_STAT_DATA sd; QString timestamp; if (FileSystem::StatFile(filename.c_str(), &sd)) timestamp = formatTimestampForSaveStateMenu(sd.ModificationTime); else timestamp = tr("Empty"); QString title(tr("Save Slot %1 (%2)").arg(i).arg(timestamp)); connect(menu->addAction(title), &QAction::triggered, [i]() { g_emu_thread->saveStateToSlot(i); }); } } void MainWindow::updateGameDependentActions() { const bool can_use_pnach = (s_vm_valid && !s_current_disc_serial.isEmpty() && s_current_running_crc != 0); m_ui.actionEditCheats->setEnabled(can_use_pnach); m_ui.actionEditPatches->setEnabled(can_use_pnach); m_ui.actionReloadPatches->setEnabled(s_vm_valid); } void MainWindow::doStartFile(std::optional source, const QString& path) { if (s_vm_valid) return; std::shared_ptr params = std::make_shared(); params->source_type = source; params->filename = path.toStdString(); // we might still be saving a resume state... VMManager::WaitForSaveStateFlush(); // GetSaveStateFileName() might temporarily mount the ISO to get the serial. cancelGameListRefresh(); const std::optional resume( promptForResumeState(QString::fromStdString(VMManager::GetSaveStateFileName(params->filename.c_str(), -1)))); if (!resume.has_value()) return; else if (resume.value()) params->state_index = -1; g_emu_thread->startVM(std::move(params)); } void MainWindow::doDiscChange(CDVD_SourceType source, const QString& path) { const auto lock = pauseAndLockVM(); // Check if memcard is busy, deny request if so if (shouldAbortForMemcardBusy(lock)) { return; } bool reset_system = false; if (!m_was_disc_change_request) { QMessageBox message(QMessageBox::Question, tr("Confirm Disc Change"), tr("Do you want to swap discs or boot the new image (via system reset)?"), QMessageBox::NoButton, this); message.addButton(tr("Swap Disc"), QMessageBox::ActionRole); QPushButton* reset_button = message.addButton(tr("Reset"), QMessageBox::ActionRole); QPushButton* cancel_button = message.addButton(QMessageBox::Cancel); message.setDefaultButton(cancel_button); message.exec(); if (message.clickedButton() == cancel_button) return; reset_system = (message.clickedButton() == reset_button); } switchToEmulationView(); g_emu_thread->changeDisc(source, path); if (reset_system) { // Clearing ELF override will reset the system. if (!s_current_elf_override.isEmpty()) g_emu_thread->setELFOverride(QString()); else g_emu_thread->resetVM(); } } MainWindow::VMLock MainWindow::pauseAndLockVM() { // To switch out of fullscreen when displaying a popup, or not to? // For Windows, with driver's direct scanout, what renders behind tends to be hit and miss. // We can't draw anything over exclusive fullscreen, so get out of it in that case. // Wayland's a pain as usual, we need to recreate the window, which means there'll be a brief // period when there's no window, and Qt might shut us down. So avoid it there. // On MacOS, it forces a workspace switch, which is kinda jarring. #ifndef __APPLE__ const bool was_fullscreen = g_emu_thread->isFullscreen() && !s_use_central_widget; #else const bool was_fullscreen = false; #endif const bool was_paused = s_vm_paused; if (!was_paused) g_emu_thread->setVMPaused(true); // We need to switch out of exclusive fullscreen before we can display our popup. // However, we do not want to switch back to render-to-main, the window might have generated this event. if (was_fullscreen) { // m_is_temporarily_windowed needs to be set, so that we don't show the main window just for this popup. pxAssertRel(!g_main_window->m_is_temporarily_windowed, "Not already temporarily windowed"); g_main_window->m_is_temporarily_windowed = true; g_emu_thread->setFullscreen(false, false); // Container could change... thanks Wayland. QWidget* container; while (QtHost::IsVMValid() && (g_emu_thread->isFullscreen() || !(container = getDisplayContainer()) || container->isFullScreen())) { QApplication::processEvents(QEventLoop::ExcludeUserInputEvents); } } // Now we'll either have a borderless window, or a regular window (if we were exclusive fullscreen). QWidget* dialog_parent = getDisplayContainer(); // Ensure main window is visible. if (!g_main_window->isVisible()) g_main_window->show(); g_main_window->raise(); g_main_window->activateWindow(); return VMLock(dialog_parent, was_paused, was_fullscreen); } void MainWindow::rescanFile(const std::string& path) { m_game_list_widget->rescanFile(path); } MainWindow::VMLock::VMLock(QWidget* dialog_parent, bool was_paused, bool was_fullscreen) : m_dialog_parent(dialog_parent) , m_was_paused(was_paused) , m_was_fullscreen(was_fullscreen) { } MainWindow::VMLock::VMLock(VMLock&& lock) : m_dialog_parent(lock.m_dialog_parent) , m_was_paused(lock.m_was_paused) , m_was_fullscreen(lock.m_was_fullscreen) { lock.m_dialog_parent = nullptr; lock.m_was_paused = true; lock.m_was_fullscreen = false; } MainWindow::VMLock::~VMLock() { if (m_was_fullscreen) { g_main_window->m_is_temporarily_windowed = false; g_emu_thread->setFullscreen(true, true); } if (!m_was_paused) g_emu_thread->setVMPaused(false); } MainWindow::VMLock& MainWindow::VMLock::operator=(VMLock&& lock) { m_dialog_parent = lock.m_dialog_parent; m_was_paused = lock.m_was_paused; m_was_fullscreen = lock.m_was_fullscreen; lock.m_dialog_parent = nullptr; lock.m_was_paused = true; lock.m_was_fullscreen = false; return *this; } void MainWindow::VMLock::cancelResume() { m_was_paused = true; m_was_fullscreen = false; g_main_window->m_is_temporarily_windowed = false; } bool QtHost::IsVMValid() { return s_vm_valid; } bool QtHost::IsVMPaused() { return s_vm_paused; } const QString& QtHost::GetCurrentGameTitle() { return s_current_title; } const QString& QtHost::GetCurrentGameSerial() { return s_current_disc_serial; } const QString& QtHost::GetCurrentGamePath() { return s_current_disc_path; }