Files
Brian Degenhardt 3e56da7f86 Merge upstream PCSX2 (2026-07-15 .. 2026-08-10)
71 commits from 474ad59818 to 2cf8dabe6b, triaged rather than taken wholesale.

Declined, resolved to ours:

- AGENTS.md: upstream's AI-agent instructions; we carry our own and do not
  want a second, conflicting policy file.
- CI deps bump (setup-node, labeler): both target workflows are absent here,
  and the labeler job is gated on the repository being PCSX2/pcsx2.
- KDDockWidgets 2.4.1: two of the six files do not exist here; we already
  build 2.4.0 against a 2.3.0 floor, so there is nothing to gain.
- The FullscreenUI Achievements-layout realignment: our section already
  carries the same settings, and ours is the branded copy.
- The GS draw/vertex-buffer cluster (7887919e74, b2fa00844e, 99cfbb49c1,
  5c611f85e1, 9945046a49, af48193ebb, d88510e3a6, 8c1bb5742e). Our vertex
  kick is an ARM64 rewrite of the same hot path -- register-resident cursor,
  fused min/max with a rewind watermark, and a scalar cull mirror that
  dual-issues against the NEON parse -- so upstream's generic pointer-logic
  optimisation is a variant of work already banked here, and their growth
  restructure replaces per-buffer capacity with a single global value, which
  the pooled draw-node model cannot express. Two of the four August commits
  in that cluster repair regressions the July rewrite introduced, and the
  third's genuine fix (staging arrays sized from an unrelated buffer) we had
  already made independently.

Taken with adjustment:

- EATAN coefficients (aae9438f98). Upstream relabelled mVU_Globals so the
  names match the powers; we had fixed the same defect by ordering the arm64
  call sites by power instead. Both fixes are correct alone and CANCEL when
  combined, so the arm64 call sites move to plain ascending order in the same
  commit. The values never moved, so this emits an identical instruction
  sequence. Their fix also repairs the x86 mVU we still carry.
- Shader cache version: upstream numbered their tfx.glsl change 109, which is
  below our 110. Taking their value would hand every user a stale blob, so
  this lands as 111.
- FullscreenUI: took the two readback-spin toggles, placed outside our
  non-Apple guard rather than inside upstream's unguarded run.
- Restored tools/generate_fullscreen_ui_translation_strings.py, dropped by
  431ca0c063, and regenerated both string areas. That also registers the Big
  Picture setup-wizard strings, which had never been extractable.

GameDB: the three serials upstream gave gsHWFixes (SLES-53869, PAPX-90020,
SCPS-15064) are absent from the mobile overlay, so no fix is silently erased
on handhelds.
2026-08-10 18:24:24 -07:00

3913 lines
128 KiB
C++

// SPDX-FileCopyrightText: 2002-2026 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"
#ifdef ENABLE_QT_DEBUGGER
#include "Debugger/Docking/DockManager.h"
#endif
#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"
#if !defined(__APPLE__)
#include "ShortcutCreationDialog.h"
#endif
#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/ImGui/FullscreenUI.h"
#include "pcsx2/MTGS.h"
#include "pcsx2/PerformanceMetrics.h"
#include "pcsx2/VMManager.h"
#include "pcsx2/ImGui/ImGuiOverlays.h"
#include "pcsx2/SPU2/spu2.h"
#include "pcsx2/Recording/InputRecording.h"
#include "pcsx2/Recording/InputRecordingControls.h"
#include "pcsx2/SaveState.h"
#include "pcsx2/SIO/Sio.h"
#include "pcsx2/GS/GSExtra.h"
#include "common/Assertions.h"
#include "common/CocoaTools.h"
#include "common/FileSystem.h"
#include "common/Path.h"
#include <QtCore/QDateTime>
#include <QtCore/QDir>
#include <QtGui/QCloseEvent>
#include <QtWidgets/QFileDialog>
#include <QtWidgets/QInputDialog>
#include <QtWidgets/QMessageBox>
#include <QtWidgets/QProgressBar>
#include <QtWidgets/QSlider>
#include <QtWidgets/QStyle>
#include <QtWidgets/QStyleFactory>
#include <QtWidgets/QWidgetAction>
#include <QtWidgets/QHBoxLayout>
#ifdef _WIN32
#include "common/RedtapeWindows.h"
#include <Dbt.h>
#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;
// 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;
static bool s_record_on_start = false;
static QString s_path_to_recording_for_record_on_start;
// DX cannot fullscreen when the display surface is in a container.
// QWindow, however, seems to lack CSD under wayland, so needs the container.
// MAC is unknown
#ifdef _WIN32
#define DISPLAY_SURFACE_WINDOW
#endif
MainWindow::MainWindow()
{
pxAssert(!g_main_window);
g_main_window = this;
}
MainWindow::~MainWindow()
{
// make sure the game list isn't refreshing, because it's on a separate thread
cancelGameListRefresh();
destroySubWindows();
Common::DetachMousePositionCb();
// we compare here, since recreate destroys the window later
if (g_main_window == this)
g_main_window = nullptr;
#ifdef _WIN32
unregisterForDeviceNotifications();
#endif
}
void MainWindow::initialize()
{
#ifdef __APPLE__
// The cocoa backing isn't initialized yet, delay this until stuff is set up with a `RunOnUIThread` call
QtHost::RunOnUIThread([this] {
CocoaTools::MarkHelpMenu(m_ui.menuHelp->toNSMenu());
});
#endif
m_ui.setupUi(this);
setupAdditionalUi();
connectSignals();
connectVMThreadSignals(g_emu_thread);
restoreStateFromConfig();
switchToGameListView();
updateWindowTitle();
updateGameDependentActions();
updateEmulationActions(false, s_vm_valid, false);
updateStatusBarWidgetVisibility();
if (s_vm_paused)
{
m_ui.actionPause->setChecked(true);
m_ui.actionToolbarPause->setChecked(true);
}
#ifdef _WIN32
registerForDeviceNotifications();
#endif
if (Host::GetBoolSettingValue("EmuCore", "EnableMouseLock", false))
setupMouseMoveHandler();
}
// 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 static_cast<QWidget*>(m_ui.mainContainer);
}
void MainWindow::setupAdditionalUi()
{
makeIconsMasks(menuBar());
updateAdvancedSettingsVisibility();
setupStatusBarWidgets();
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);
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<float>(scale)); });
}
const bool show_game_grid = Host::GetBaseBoolSettingValue("UI", "GameListGridView", false);
const bool show_grid_cover_titles = Host::GetBaseBoolSettingValue("UI", "ShowGridCoverTitles", false);
updateGameGridActions(show_game_grid, show_grid_cover_titles);
m_game_list_widget = new GameListWidget(getContentParent());
m_game_list_widget->initialize();
m_ui.actionGridViewShowTitles->setChecked(m_game_list_widget->getShowGridCoverTitles());
m_ui.actionGridViewShowFullTitles->setChecked(m_game_list_widget->getShowGridFullCoverTitles());
m_ui.mainContainer->addWidget(m_game_list_widget);
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::setupStatusBarWidgets()
{
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_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->setContentsMargins(10, 0, 10, 0);
m_status_verbose_widget->setFixedHeight(20);
m_status_verbose_widget->setMinimumWidth(20);
m_status_verbose_widget->hide();
m_status_renderer_widget = new QLabel(m_ui.statusBar);
m_status_renderer_widget->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Fixed);
m_status_renderer_widget->setContentsMargins(10, 0, 10, 0);
m_status_renderer_widget->setAlignment(Qt::AlignCenter);
m_status_renderer_widget->setFixedHeight(20);
m_status_renderer_widget->hide();
m_status_resolution_widget = new QLabel(m_ui.statusBar);
m_status_resolution_widget->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Fixed);
m_status_resolution_widget->setContentsMargins(10, 0, 10, 0);
m_status_resolution_widget->setAlignment(Qt::AlignCenter);
m_status_resolution_widget->setFixedHeight(20);
m_status_resolution_widget->hide();
m_status_volume_widget = new QToolButton(m_ui.statusBar);
m_status_volume_widget->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Fixed);
m_status_volume_widget->setContentsMargins(10, 0, 10, 0);
m_status_volume_widget->setAutoRaise(true);
m_status_volume_widget->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
m_status_volume_widget->setIcon(QIcon::fromTheme(QStringLiteral("volume-up-line")));
m_status_volume_widget->setPopupMode(QToolButton::InstantPopup);
m_status_volume_widget->setFixedHeight(20);
m_status_volume_menu = new QMenu(m_status_volume_widget);
m_status_volume_per_game_action = new QAction(tr("Adjust Per-Game"), m_status_volume_menu);
m_status_volume_per_game_action->setCheckable(true);
connect(m_status_volume_menu, &QMenu::aboutToShow, this, [this]() {
auto lock = Host::GetSettingsLock();
SettingsInterface* game_layer = Host::Internal::GetGameSettingsLayer();
m_status_volume_per_game_action->setEnabled(game_layer != nullptr && QtHost::IsVMValid());
m_status_volume_per_game_action->setChecked(game_layer &&
(game_layer->ContainsValue("SPU2/Output", "StandardVolume") || game_layer->ContainsValue("SPU2/Output", "OutputMuted")));
});
connect(m_status_volume_per_game_action, &QAction::triggered, this, [this](bool checked) {
m_status_volume_slider_applied = true;
applyStatusBarVolumeChanges(std::nullopt, false, checked);
});
m_status_volume_toggle_mute_action = new QAction(tr("Toggle Mute"), m_status_volume_menu);
m_status_volume_toggle_mute_action->setIcon(QIcon::fromTheme(QStringLiteral("volume-mute-line")));
connect(m_status_volume_toggle_mute_action, &QAction::triggered, this, [this]() {
m_status_volume_slider_applied = true;
applyStatusBarVolumeChanges(std::nullopt, true, std::nullopt);
});
m_status_volume_menu->addAction(m_status_volume_per_game_action);
m_status_volume_menu->addAction(m_status_volume_toggle_mute_action);
m_status_volume_menu->addSeparator();
m_status_volume_slider = new QSlider(Qt::Horizontal, m_status_volume_menu);
m_status_volume_slider->setRange(0, 100);
m_status_volume_slider->setFixedWidth(120);
m_status_volume_slider->setValue(Host::GetIntSettingValue("SPU2/Output", "StandardVolume", 100));
connect(m_status_volume_slider, &QSlider::valueChanged, this, [this](int value) {
Host::RunOnCPUThread([value]() {
if (!VMManager::HasValidVM())
return;
EmuConfig.SPU2.StandardVolume = static_cast<u32>(value);
SPU2::SetOutputVolume(static_cast<u32>(value));
});
if (m_status_volume_slider->isSliderDown())
setStatusVolumeText(m_status_volume_muted ? tr("Volume: Muted") : tr("Volume: %1%").arg(value), value, m_status_volume_muted);
});
connect(m_status_volume_slider, &QSlider::sliderReleased, this, [this]() {
m_status_volume_slider_applied = true;
applyStatusBarVolumeChanges(m_status_volume_slider->value(), false);
});
connect(m_status_volume_menu, &QMenu::aboutToHide, this, [this]() {
if (!m_status_volume_slider_applied)
applyStatusBarVolumeChanges(m_status_volume_slider->value(), false);
m_status_volume_slider_applied = false;
});
QWidget* container = new QWidget(m_status_volume_menu);
QHBoxLayout* layout = new QHBoxLayout(container);
layout->setContentsMargins(8, 4, 8, 4);
layout->addWidget(m_status_volume_slider);
QWidgetAction* slider_action = new QWidgetAction(m_status_volume_menu);
slider_action->setDefaultWidget(container);
m_status_volume_menu->addAction(slider_action);
m_status_volume_widget->setMenu(m_status_volume_menu);
m_status_volume_widget->hide();
m_status_speed_widget = new QToolButton(m_ui.statusBar);
m_status_speed_widget->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Fixed);
m_status_speed_widget->setContentsMargins(10, 0, 10, 0);
m_status_speed_widget->setAutoRaise(true);
m_status_speed_widget->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
m_status_speed_widget->setPopupMode(QToolButton::InstantPopup);
m_status_speed_widget->setFixedHeight(20);
m_status_speed_widget->hide();
m_status_speed_menu = new QMenu(m_status_speed_widget);
m_status_speed_menu->addAction(tr("Unlimited"), []() { Host::RunOnCPUThread([]() { VMManager::SetLimiterMode(LimiterModeType::Unlimited); }); });
m_status_speed_menu->addAction(tr("Turbo"), []() { Host::RunOnCPUThread([]() { VMManager::SetLimiterMode(LimiterModeType::Turbo); }); });
m_status_speed_menu->addAction(tr("Slow-Motion"), []() { Host::RunOnCPUThread([]() { VMManager::SetLimiterMode(LimiterModeType::Slomo); }); });
m_status_speed_menu->addAction(tr("Normal"), []() { Host::RunOnCPUThread([]() { VMManager::SetLimiterMode(LimiterModeType::Nominal); }); });
m_status_speed_widget->setMenu(m_status_speed_menu);
m_status_speed_widget->hide();
m_status_gpu_widget = new QLabel(m_ui.statusBar);
m_status_gpu_widget->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Fixed);
m_status_gpu_widget->setContentsMargins(10, 0, 10, 0);
m_status_gpu_widget->setAlignment(Qt::AlignCenter);
m_status_gpu_widget->setFixedHeight(20);
m_status_gpu_widget->hide();
m_status_fps_widget = new QLabel(m_ui.statusBar);
m_status_fps_widget->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Fixed);
m_status_fps_widget->setContentsMargins(10, 0, 10, 0);
m_status_fps_widget->setAlignment(Qt::AlignCenter);
m_status_fps_widget->setFixedHeight(20);
m_status_fps_widget->hide();
m_status_vps_widget = new QLabel(m_ui.statusBar);
m_status_vps_widget->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Fixed);
m_status_vps_widget->setContentsMargins(10, 0, 10, 0);
m_status_vps_widget->setAlignment(Qt::AlignCenter);
m_status_vps_widget->setFixedHeight(20);
m_status_vps_widget->hide();
}
void MainWindow::applyStatusBarVolumeChanges(std::optional<int> volume, bool toggle_mute, std::optional<bool> override_per_game)
{
const bool use_per_game = override_per_game.value_or(m_status_volume_per_game_action->isChecked());
Host::RunOnCPUThread([this, volume, toggle_mute, use_per_game]() {
int target_vol = 100;
bool target_mute = false;
{
auto lock = Host::GetSettingsLock();
SettingsInterface* per_game_settings = Host::Internal::GetGameSettingsLayer();
SettingsInterface* global_settings = Host::Internal::GetBaseSettingsLayer();
SettingsInterface* active_setting = (use_per_game && per_game_settings) ? per_game_settings : global_settings;
if (active_setting)
{
if (!active_setting->GetIntValue("SPU2/Output", "StandardVolume", &target_vol) && global_settings)
global_settings->GetIntValue("SPU2/Output", "StandardVolume", &target_vol);
if (!active_setting->GetBoolValue("SPU2/Output", "OutputMuted", &target_mute) && global_settings)
global_settings->GetBoolValue("SPU2/Output", "OutputMuted", &target_mute);
}
target_vol = volume.value_or(target_vol);
if (toggle_mute)
target_mute = !(VMManager::HasValidVM() ? SPU2::IsOutputMuted() : target_mute);
if (active_setting)
{
active_setting->SetIntValue("SPU2/Output", "StandardVolume", target_vol);
active_setting->SetBoolValue("SPU2/Output", "OutputMuted", target_mute);
}
if (per_game_settings && active_setting == per_game_settings)
QtHost::SaveGameSettings(per_game_settings, true);
else
{
if (per_game_settings)
{
const bool had_per_game_volume =
per_game_settings->ContainsValue("SPU2/Output", "StandardVolume") ||
per_game_settings->ContainsValue("SPU2/Output", "OutputMuted");
per_game_settings->DeleteValue("SPU2/Output", "StandardVolume");
per_game_settings->DeleteValue("SPU2/Output", "OutputMuted");
if (had_per_game_volume)
QtHost::SaveGameSettings(per_game_settings, true);
}
Host::CommitBaseSettingChanges();
}
}
if (VMManager::HasValidVM())
{
EmuConfig.SPU2.StandardVolume = static_cast<u32>(target_vol);
EmuConfig.SPU2.OutputMuted = target_mute;
SPU2::SetOutputMuted(target_mute);
SPU2::SetOutputVolume(static_cast<u32>(target_vol));
}
QTimer::singleShot(0, this, [this, target_vol, target_mute]() {
setStatusVolumeText(target_mute ? tr("Volume: Muted") : tr("Volume: %1%").arg(target_vol), target_vol, target_mute);
});
});
}
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.actionToolbarHotkeySettings, &QAction::triggered,
[this]() { doControllerSettings(ControllerSettingsWindow::Category::HotkeySettings); });
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.actionOSDSettings, &QAction::triggered, [this]() { doSettings("On-Screen Display"); });
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, true); });
connect(m_ui.actionRescanAllGames, &QAction::triggered, [this]() { refreshGameList(true, 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.actionWebsite, &QAction::triggered, this, &MainWindow::onWebsiteActionTriggered);
connect(m_ui.actionAbout, &QAction::triggered, this, &MainWindow::onAboutActionTriggered);
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.actionGridViewShowFullTitles, &QAction::triggered, m_game_list_widget, &GameListWidget::setShowFullCoverTitles);
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);
QSignalBlocker sb2(m_ui.actionGridViewShowFullTitles);
m_ui.actionGridViewShowTitles->setChecked(m_game_list_widget->getShowGridCoverTitles());
m_ui.actionGridViewShowFullTitles->setChecked(m_game_list_widget->getShowGridFullCoverTitles());
updateGameGridActions(m_game_list_widget->isShowingGameGrid(), 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", false);
SettingWidgetBinder::BindWidgetToBoolSetting(nullptr, m_ui.actionEnableIOPConsoleLogging, "Logging", "EnableIOPConsole", false);
SettingWidgetBinder::BindWidgetToBoolSetting(nullptr, m_ui.actionEnableLogWindow, "Logging", "EnableLogWindow", false);
SettingWidgetBinder::BindWidgetToBoolSetting(nullptr, m_ui.actionEnableFileLogging, "Logging", "EnableFileLogging", true);
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::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::onMouseLockRequested, this, &MainWindow::mouseLockRequested);
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(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);
#ifdef ENABLE_QT_DEBUGGER
connect(m_ui.actionDebugger, &QAction::triggered, this, &MainWindow::openDebugger);
#else
m_ui.actionDebugger->setVisible(false);
#endif
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
#if defined(ENABLE_OPENGL) && !defined(__APPLE__)
GSRendererType::OGL,
#endif
#if defined(ENABLE_VULKAN) && !defined(__APPLE__)
GSRendererType::VK,
#endif
GSRendererType::SW,
GSRendererType::Null,
};
const GSRendererType current_renderer = static_cast<GSRendererType>(
Host::GetBaseIntSettingValue("EmuCore/GS", "Renderer", static_cast<int>(GSRendererType::Auto)));
QActionGroup* switch_renderer_group = new QActionGroup(m_ui.menuDebugSwitchRenderer);
for (const GSRendererType renderer : renderers)
{
QAction* action = new QAction(
QString::fromUtf8(Pcsx2Config::GSOptions::GetRendererName(renderer)), switch_renderer_group);
action->setCheckable(true);
action->setChecked(current_renderer == renderer);
connect(action, &QAction::triggered, [renderer] {
Host::SetBaseIntSettingValue("EmuCore/GS", "Renderer", static_cast<int>(renderer));
Host::CommitBaseSettingChanges();
g_emu_thread->applySettings();
});
}
m_ui.menuDebugSwitchRenderer->addActions(switch_renderer_group->actions());
}
void MainWindow::recreate()
{
destroySubWindows();
const bool was_display_created = m_display_created;
if (was_display_created)
{
g_emu_thread->setSurfaceless(true);
while (m_display_surface || !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, 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, false);
g_main_window->onFullscreenUIStateChange(g_emu_thread->isRunningFullscreenUI());
}
if (s_vm_valid)
g_emu_thread->updatePerformanceMetrics(true);
}
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()
{
#ifdef ENABLE_QT_DEBUGGER
DebuggerWindow::destroyInstance();
#endif
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 ARMSX2 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)
{
QMessageBox msgbox(this);
msgbox.setIcon(QMessageBox::Question);
msgbox.setWindowIcon(QtHost::GetAppIcon());
msgbox.setWindowTitle(tr("Record On Boot"));
msgbox.setWindowModality(Qt::WindowModal);
msgbox.addButton(QMessageBox::Yes);
msgbox.addButton(QMessageBox::No);
msgbox.setDefaultButton(QMessageBox::Yes);
if (!s_record_on_start)
{
msgbox.setText(tr("Did you want to start recording on boot?"));
if (msgbox.exec() == QMessageBox::Yes)
{
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));
const QString base_video_filename(QStringLiteral("%1.%2").arg(QString::fromStdString(GSGetBaseVideoFilename())).arg(container));
s_path_to_recording_for_record_on_start = QDir::toNativeSeparators(QFileDialog::getSaveFileName(this, tr("Video Capture"), base_video_filename, filter));
s_record_on_start = !s_path_to_recording_for_record_on_start.isEmpty();
}
}
else
{
msgbox.setText(tr("Did you want to cancel recording on boot?"));
if (msgbox.exec() == QMessageBox::Yes)
s_record_on_start = false;
}
QSignalBlocker sb(m_ui.actionVideoCapture);
m_ui.actionVideoCapture->setChecked(s_record_on_start);
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;
}
if (s_record_on_start && !s_path_to_recording_for_record_on_start.isEmpty())
{
// We can't start recording immediately, this is called before full GS init (specifically the fps amount)
// and GSCapture ends up unhappy.
// TODO: Pass some sort of flag or callback to the GS thread to start recording on frame 0.
Host::AddOSDMessage(tr("Recording will start in a moment").toStdString(), 3.0f);
QTimer::singleShot(2000, []() { g_emu_thread->beginCapture(s_path_to_recording_for_record_on_start); });
}
else
{
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);
// refresh emulation actions to show/hide load state buttons based on hardcore mode
updateEmulationActions(s_vm_valid, s_vm_valid, false);
if (enabled)
{
#ifdef ENABLE_QT_DEBUGGER
// 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 (g_debugger_window && !DebugInterface::getPauseOnEntry())
DebuggerWindow::destroyInstance();
#endif
}
}
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 (Host::ContainsBaseSettingValue("UI", "MainWindowMaximized"))
{
const bool maximized = Host::GetBaseBoolSettingValue("UI", "MainWindowMaximized");
if (maximized != isMaximized())
{
Host::SetBaseBoolSettingValue("UI", "MainWindowMaximized", isMaximized());
changed = true;
}
}
if (Host::ContainsBaseSettingValue("UI", "MainWindowFullscreen"))
{
const bool fullscreen = Host::GetBaseBoolSettingValue("UI", "MainWindowFullscreen");
if (fullscreen != isFullScreen())
{
Host::SetBaseBoolSettingValue("UI", "MainWindowFullscreen", isFullScreen());
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());
}
}
{
if (Host::ContainsBaseSettingValue("UI", "MainWindowMaximized"))
{
const bool maximized = Host::GetBaseBoolSettingValue("UI", "MainWindowMaximized");
if (maximized)
showMaximized();
}
}
{
if (Host::ContainsBaseSettingValue("UI", "MainWindowFullscreen"))
{
const bool fullscreen = Host::GetBaseBoolSettingValue("UI", "MainWindowFullscreen");
if (fullscreen)
showFullScreen();
}
}
}
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 && !Achievements::IsHardcoreModeActive());
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 && !Achievements::IsHardcoreModeActive());
m_ui.actionToolbarSaveState->setEnabled(running);
m_ui.actionViewGameProperties->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() == visible)
return;
if (widget->isVisible())
{
m_ui.statusBar->removeWidget(widget);
widget->hide();
}
else
{
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_gpu_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);
Update(m_status_volume_widget, s_vm_valid, 0);
}
void MainWindow::setStatusVerboseText(const QString& text)
{
m_status_verbose_widget->setText(text);
}
void MainWindow::setStatusRendererText(const QString& text)
{
m_status_renderer_widget->setText(text);
}
void MainWindow::setStatusResolutionText(const QString& text)
{
m_status_resolution_widget->setText(text);
}
void MainWindow::setStatusVolumeText(const QString& text, int volume, bool muted)
{
m_status_volume_widget->setText(text);
if (muted != m_status_volume_muted)
{
m_status_volume_muted = muted;
m_status_volume_widget->setIcon(QIcon::fromTheme(muted ? QStringLiteral("volume-mute-line") : QStringLiteral("volume-up-line")));
m_status_volume_toggle_mute_action->setIcon(QIcon::fromTheme(muted ? QStringLiteral("volume-up-line") : QStringLiteral("volume-mute-line")));
}
if (!muted && !m_status_volume_slider->isSliderDown())
{
QSignalBlocker blocker(m_status_volume_slider);
m_status_volume_slider->setValue(volume);
}
}
void MainWindow::setStatusGPUText(const QString& text)
{
m_status_gpu_widget->setText(text);
}
void MainWindow::setStatusFPSText(const QString& text)
{
m_status_fps_widget->setText(text);
}
void MainWindow::setStatusVPSText(const QString& text)
{
m_status_vps_widget->setText(text);
}
void MainWindow::setStatusSpeedText(const QString& text)
{
m_status_speed_widget->setText(text);
}
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_surface && !isRenderingToMain())
{
#ifdef DISPLAY_SURFACE_WINDOW
if (m_display_surface->title() != display_title)
m_display_surface->setTitle(display_title);
#else
if (m_display_container->windowTitle() != display_title)
m_display_container->setWindowTitle(display_title);
#endif
}
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_surface;
// 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_surface && !isRenderingToMain())
{
#ifdef DISPLAY_SURFACE_WINDOW
QtUtils::SetWindowResizeable(m_display_surface, resizeable);
#else
QtUtils::SetWindowResizeable(m_display_container, resizeable);
#endif
}
}
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
{
return (m_ui.mainContainer->currentIndex() == 0);
}
bool MainWindow::isRenderingFullscreen() const
{
if (!MTGS::IsOpen() || !m_display_surface)
return false;
return m_display_surface->isFullScreen();
}
bool MainWindow::isRenderingToMain() const
{
return (m_display_container && m_ui.mainContainer->indexOf(m_display_container) == 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() || g_emu_thread->isExclusiveFullscreen() || m_is_temporarily_windowed)) ||
Host::InNoGUIMode();
}
bool MainWindow::shouldMouseLock() const
{
if (!s_vm_valid || s_vm_paused)
return false;
if (!Host::GetBoolSettingValue("EmuCore", "EnableMouseLock", false))
return false;
if (m_display_created == false || m_display_surface == nullptr)
return false;
const bool windowsHidden =
#ifdef ENABLE_QT_DEBUGGER
(!g_debugger_window || g_debugger_window->isHidden()) &&
#endif
(!m_controller_settings_window || m_controller_settings_window->isHidden()) &&
(!m_settings_window || m_settings_window->isHidden());
if (!windowsHidden)
return false;
#ifdef DISPLAY_SURFACE_WINDOW
if (isRenderingToMain())
{
const auto* displayWindow = window();
return displayWindow ? (displayWindow->isActiveWindow() || displayWindow->isFullScreen()) : false;
}
else
return m_display_surface->isActive() || m_display_surface->isFullScreen();
#else
const auto* displayWindow = isRenderingToMain() ? window() : m_display_container->window();
return displayWindow ? (displayWindow->isActiveWindow() || displayWindow->isFullScreen()) : false;
#endif
}
bool MainWindow::shouldAbortForMemcardBusy(const VMLock& lock)
{
if (MemcardBusy::IsBusy() && !GSDumpReplayer::IsReplayingDump())
{
QMessageBox msgbox(lock.getDialogParent());
msgbox.setIcon(QMessageBox::Warning);
msgbox.setWindowTitle(tr("WARNING: Memory Card Busy"));
msgbox.setWindowIcon(QtHost::GetAppIcon());
msgbox.setWindowModality(Qt::WindowModal);
msgbox.setTextFormat(Qt::RichText);
msgbox.setText(tr("Your memory card is still saving data."));
msgbox.setInformativeText(tr("WARNING: Shutting down now can <b>IRREVERSIBLY CORRUPT YOUR MEMORY CARD.</b><br><br>"
"You are strongly advised to select 'No' and let the save finish.<br><br>"
"Do you want to shutdown anyway and <b>IRREVERSIBLY CORRUPT YOUR MEMORY CARD</b>?"));
msgbox.setStandardButtons(QMessageBox::Yes | QMessageBox::No);
msgbox.setDefaultButton(QMessageBox::No);
if (msgbox.exec() != 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_surface)
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_surface)
m_display_surface->setFocus();
}
void MainWindow::refreshGameList(bool invalidate_cache, bool popup_on_error)
{
m_game_list_widget->refresh(invalidate_cache, popup_on_error);
}
void MainWindow::cancelGameListRefresh()
{
m_game_list_widget->cancelRefresh();
}
void MainWindow::updateGameListBackground()
{
if (m_game_list_widget)
m_game_list_widget->setCustomBackground();
}
void MainWindow::reportInfo(const QString& title, const QString& message)
{
QMessageBox::information(this, title, message);
}
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::reportStateLoadError(const QString& message, std::optional<s32> slot, bool backup)
{
const bool prompt_on_error = Host::GetBaseBoolSettingValue("UI", "PromptOnStateLoadSaveFailure", true);
if (!prompt_on_error)
{
SaveState_ReportLoadErrorOSD(message.toStdString(), slot, backup);
return;
}
QString title;
if (slot.has_value())
{
if (backup)
title = tr("Failed to Load State From Backup Slot %1").arg(*slot);
else
title = tr("Failed to Load State From Slot %1").arg(*slot);
}
else
{
title = tr("Failed to Load State");
}
VMLock lock(pauseAndLockVM());
QCheckBox* do_not_show_again = new QCheckBox(tr("Do not show again"));
QPointer<QMessageBox> message_box = new QMessageBox(this);
message_box->setWindowTitle(title);
message_box->setText(message);
message_box->setIcon(QMessageBox::Critical);
message_box->addButton(QMessageBox::Ok);
message_box->setDefaultButton(QMessageBox::Ok);
message_box->setCheckBox(do_not_show_again);
message_box->exec();
if (message_box.isNull())
return;
if (do_not_show_again->isChecked())
{
Host::SetBaseBoolSettingValue("UI", "PromptOnStateLoadSaveFailure", false);
Host::CommitBaseSettingChanges();
if (m_settings_window)
{
InterfaceSettingsWidget* interface_settings = m_settings_window->getInterfaceSettingsWidget();
interface_settings->updatePromptOnStateLoadSaveFailureCheckbox(Qt::Unchecked);
}
}
delete message_box;
}
void MainWindow::reportStateSaveError(const QString& message, std::optional<s32> slot)
{
const bool prompt_on_error = Host::GetBaseBoolSettingValue("UI", "PromptOnStateLoadSaveFailure", true);
if (!prompt_on_error)
{
SaveState_ReportSaveErrorOSD(message.toStdString(), slot);
return;
}
QString title;
if (slot.has_value())
title = tr("Failed to Save State To Slot %1").arg(*slot);
else
title = tr("Failed to Save State");
VMLock lock(pauseAndLockVM());
QCheckBox* do_not_show_again = new QCheckBox(tr("Do not show again"));
QPointer<QMessageBox> message_box = new QMessageBox(this);
message_box->setWindowTitle(title);
message_box->setText(message);
message_box->setIcon(QMessageBox::Critical);
message_box->addButton(QMessageBox::Ok);
message_box->setDefaultButton(QMessageBox::Ok);
message_box->setCheckBox(do_not_show_again);
message_box->exec();
if (message_box.isNull())
return;
if (do_not_show_again->isChecked())
{
Host::SetBaseBoolSettingValue("UI", "PromptOnStateLoadSaveFailure", false);
Host::CommitBaseSettingChanges();
if (m_settings_window)
{
InterfaceSettingsWidget* interface_settings = m_settings_window->getInterfaceSettingsWidget();
interface_settings->updatePromptOnStateLoadSaveFailureCheckbox(Qt::Unchecked);
}
}
delete message_box;
}
void MainWindow::runOnUIThread(const std::function<void()>& 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() && !Host::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_surface)
updateDisplayWidgetCursor();
updateWindowState();
LogWindow::updateSettings();
}
std::optional<WindowInfo> MainWindow::getWindowInfo()
{
if (!m_display_surface || isRenderingToMain())
return QtUtils::GetWindowInfoForWindow(this);
else if (m_display_surface)
return QtUtils::GetWindowInfoForWindow(m_display_surface);
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()
{
const std::optional<GameList::Entry> entry = m_game_list_widget->getSelectedEntry();
if (!entry.has_value())
return;
m_ui.statusBar->showMessage(QString::fromStdString(entry->path));
}
void MainWindow::onGameListEntryActivated()
{
const std::optional<GameList::Entry> entry = m_game_list_widget->getSelectedEntry();
if (!entry.has_value())
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<bool> 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<s32>(-1) : std::optional<s32>(), std::nullopt);
}
void MainWindow::onGameListEntryContextMenuRequested(const QPoint& point)
{
const std::optional<GameList::Entry> entry = m_game_list_widget->getSelectedEntry();
QMenu menu;
if (entry.has_value())
{
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->serial, entry->crc, entry->type == GameList::EntryType::ELF, nullptr);
});
}
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); });
#if !defined(__APPLE__)
connect(menu.addAction(tr("Create Game Shortcut...")), &QAction::triggered, [this]() { MainWindow::onCreateGameShortcutTriggered(); });
#endif
connect(menu.addAction(tr("Exclude From List")), &QAction::triggered,
[this, entry]() { getSettingsWindow()->getGameListSettingsWidget()->addExcludedPath(entry->path); });
const time_t entry_played_time = GameList::GetCachedPlayedTimeForSerial(entry->serial);
// Best two options given zero play time are to grey this out or to not show it at all.
if (entry_played_time)
connect(menu.addAction(tr("Reset Play Time")), &QAction::triggered, [this, entry, entry_played_time]() { clearGameListEntryPlayTime(*entry, entry_played_time); });
// Check Wiki Page functionality is based on a serial redirect.
if (!entry->serial.empty())
connect(menu.addAction(tr("Check Wiki Page")), &QAction::triggered, [this, entry]() { goToWikiPage(*entry); });
menu.addSeparator();
action = menu.addAction(tr("Open Memory Card Folder"));
connect(action, &QAction::triggered, [this, entry]() { openMemoryCardFolder(); });
action = menu.addAction(tr("Open Snapshots Folder"));
connect(action, &QAction::triggered, [this, entry]() { openSnapshotsFolderForGame(*entry); });
action = menu.addAction(tr("Open Texture Dump/Replacement Folder"));
connect(action, &QAction::triggered, [this, entry]() { openTextureFolderForGame(*entry); });
action = menu.addAction(tr("Open Video Capture Folder"));
connect(action, &QAction::triggered, [this, entry]() { openVideoCaptureFolder(*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); });
#ifdef ENABLE_QT_DEBUGGER
if (m_ui.menuDebug->menuAction()->isVisible())
{
action = menu.addAction(tr("Boot and Debug"));
connect(action, &QAction::triggered, [this, entry]() {
DebugInterface::setPauseOnEntry(true);
startGameListEntry(*entry);
DebuggerWindow::getInstance()->show();
});
}
#endif
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<VMBootParameters> params = std::make_shared<VMBootParameters>();
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_surface)
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()
{
doGameSettings(nullptr);
}
void MainWindow::onGitHubRepositoryActionTriggered()
{
QtUtils::OpenURL(this, AboutDialog::getGitHubRepositoryUrl());
}
void MainWindow::onWebsiteActionTriggered()
{
QtUtils::OpenURL(this, AboutDialog::getWebsiteUrl());
}
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("<p>Sorry, you are trying to update an ARMSX2 version which is not an official GitHub release. To "
"prevent incompatibilities, the auto-updater is only enabled on official builds.</p>"
"<p>To obtain an official build, please download from the link below:</p>"
"<p><a href=\"https://armsx2.net/\">https://armsx2.net/</a></p>");
#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(this);
connect(&dlg, &CoverDownloadDialog::coverRefreshRequested, m_game_list_widget, &GameListWidget::refreshGridCovers);
dlg.exec();
}
#if !defined(__APPLE__)
void MainWindow::onCreateGameShortcutTriggered()
{
const std::optional<GameList::Entry> entry = m_game_list_widget->getSelectedEntry();
if (!entry.has_value())
return;
const QString title = QString::fromStdString(entry->GetTitle());
const QString path = QString::fromStdString(entry->path);
ShortcutCreationDialog dlg(this, title, path);
dlg.exec();
}
#endif
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()
{
QtHost::UpdateApplicationTheme();
reloadThemeSpecificImages();
}
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([]() {
g_main_window->m_ui.actionInputRecNew->setEnabled(false);
g_main_window->m_ui.actionInputRecStop->setEnabled(true);
g_main_window->m_ui.actionReset->setEnabled(!g_InputRecording.isTypeSavestate());
g_main_window->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(tr("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([]() {
g_main_window->m_ui.actionInputRecNew->setEnabled(false);
g_main_window->m_ui.actionInputRecStop->setEnabled(true);
g_main_window->m_ui.actionReset->setEnabled(!g_InputRecording.isTypeSavestate());
g_main_window->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([]() {
g_main_window->m_ui.actionInputRecNew->setEnabled(true);
g_main_window->m_ui.actionInputRecStop->setEnabled(false);
g_main_window->m_ui.actionReset->setEnabled(true);
g_main_window->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);
if (s_record_on_start)
{
m_ui.actionVideoCapture->setChecked(true);
s_record_on_start = false;
}
}
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_surface)
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_surface)
{
updateDisplayWidgetCursor();
m_display_surface->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_volume_widget->setText(empty_string);
m_status_gpu_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 || Host::InBatchMode())
{
quit();
return;
}
if (m_display_surface)
updateDisplayWidgetCursor();
else
switchToGameListView();
// reload played time
if (m_game_list_widget->isShowingGameList())
m_game_list_widget->refresh(false, 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();
}
if (event->type() == QEvent::ActivationChange)
m_game_list_widget->updateCustomBackgroundState();
}
static QString getFilenameFromMimeData(const QMimeData* md)
{
QString filename;
if (md->hasUrls())
{
// only one url accepted
const QList<QUrl> 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)
{
if (startFile(getFilenameFromMimeData(event->mimeData())))
event->acceptProposedAction();
}
bool MainWindow::startFile(const QString& filename)
{
const auto mcLock = pauseAndLockVM();
// Check if memcard is busy, deny request if so
if (shouldAbortForMemcardBusy(mcLock))
{
return false;
}
std::string filename_str = filename.toStdString();
if (VMManager::IsSaveStateFileName(filename_str))
{
// 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 true;
}
if (!VMManager::IsLoadableFileName(filename_str))
return false;
// if we're already running, do a disc change, otherwise start
if (!s_vm_valid)
{
doStartFile(std::nullopt, filename);
return true;
}
if (VMManager::IsDiscFileName(filename_str) || VMManager::IsBlockDumpFileName(filename_str))
{
doDiscChange(CDVD_SourceType::Iso, filename);
}
else if (VMManager::IsElfFileName(filename_str))
{
const auto lock = pauseAndLockVM();
if (QMessageBox::Yes != 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?")))
{
return true;
}
g_emu_thread->setELFOverride(filename);
switchToEmulationView();
}
else if (VMManager::IsGSDumpFileName(filename_str))
{
if (!GSDumpReplayer::IsReplayingDump())
{
QMessageBox::critical(this, tr("Error"), tr("Cannot change from game to GS dump without shutting down first."));
}
g_emu_thread->changeGSDump(filename);
switchToEmulationView();
}
return true;
}
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);
// Set up the raw input device for mouse grabbing
RAWINPUTDEVICE rid;
rid.usUsagePage = 0x01; // Generic desktop controls
rid.usUsage = 0x02; // Mouse
rid.dwFlags = RIDEV_INPUTSINK;
rid.hwndTarget = (HWND)winId();
RegisterRawInputDevices(&rid, 1, sizeof(RAWINPUTDEVICE));
#endif
}
void MainWindow::unregisterForDeviceNotifications()
{
#ifdef _WIN32
if (!m_device_notification_handle)
return;
UnregisterDeviceNotification(static_cast<HDEVNOTIFY>(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<const MSG*>(message);
if (msg->message == WM_DEVICECHANGE && msg->wParam == DBT_DEVNODES_CHANGED)
{
g_emu_thread->reloadInputDevices();
*result = 1;
return true;
}
if (msg->message == WM_INPUT)
{
UINT dwSize = 0;
GetRawInputData((HRAWINPUT)msg->lParam, RID_INPUT, nullptr, &dwSize, sizeof(RAWINPUTHEADER));
if (dwSize > 0)
{
std::vector<BYTE> lpb(dwSize);
if (GetRawInputData((HRAWINPUT)msg->lParam, RID_INPUT, lpb.data(), &dwSize, sizeof(RAWINPUTHEADER)) == dwSize)
{
const RAWINPUT* raw = reinterpret_cast<const RAWINPUT*>(lpb.data());
if (raw->header.dwType == RIM_TYPEMOUSE)
{
const RAWMOUSE& mouse = raw->data.mouse;
if (mouse.usFlags == MOUSE_MOVE_ABSOLUTE || mouse.usFlags == MOUSE_MOVE_RELATIVE)
{
POINT cursorPos;
if (GetCursorPos(&cursorPos))
checkMousePosition(cursorPos.x, cursorPos.y);
}
}
}
}
}
}
return QMainWindow::nativeEvent(eventType, message, result);
}
#endif
std::optional<WindowInfo> 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");
const bool is_fullscreen = isRenderingFullscreen();
const bool is_rendering_to_main = isRenderingToMain();
const bool changing_surfaceless = (!m_display_surface != surfaceless);
if (m_display_created && !recreate_window && fullscreen == is_fullscreen && is_rendering_to_main == render_to_main &&
!changing_surfaceless)
{
return m_display_surface ? m_display_surface->getWindowInfo() : WindowInfo();
}
// Skip recreating the surface if we're just transitioning between fullscreen and windowed with render-to-main off.
if (m_display_created && !recreate_window && !is_rendering_to_main && !render_to_main && !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();
#ifdef DISPLAY_SURFACE_WINDOW
auto* displayWindow = m_display_surface;
#else
auto* displayWindow = m_display_container;
#endif
if (fullscreen)
displayWindow->showFullScreen();
else
{
// Needs to exit fullscreen before resizing
displayWindow->showNormal();
if (m_is_temporarily_windowed && g_emu_thread->shouldRenderToMain())
displayWindow->setGeometry(geometry());
else
restoreDisplayWindowGeometryFromConfig();
}
updateDisplayWidgetCursor();
m_display_surface->setFocus();
updateWindowState();
QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
return m_display_surface->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<WindowInfo> wi = m_display_surface->getWindowInfo();
if (!wi.has_value())
{
QMessageBox::critical(this, tr("Error"), tr("Failed to get window info from widget"));
destroyDisplayWidget(true);
return std::nullopt;
}
m_display_is_exclusive_fullscreen = g_emu_thread->isExclusiveFullscreen();
g_emu_thread->connectDisplaySignals(m_display_surface);
updateWindowTitle();
updateWindowState();
updateDisplayWidgetCursor();
m_display_surface->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();
}
m_display_surface = new DisplaySurface();
if (fullscreen || !render_to_main)
{
#ifdef DISPLAY_SURFACE_WINDOW
m_display_surface->setTitle(windowTitle());
m_display_surface->setIcon(windowIcon());
#else
m_display_container = m_display_surface->createWindowContainer();
m_display_container->setWindowTitle(windowTitle());
m_display_container->setWindowIcon(windowIcon());
#endif
}
else
{
m_display_container = m_display_surface->createWindowContainer(getContentParent());
}
if (fullscreen || g_emu_thread->isExclusiveFullscreen())
{
// On Wayland, while move/restoreGeometry can't position the window, it can influence which screen they show up on.
// Other platforms can position windows fine, but the only thing that matters here is the screen.
#ifdef DISPLAY_SURFACE_WINDOW
if (isVisible() && g_emu_thread->shouldRenderToMain())
m_display_surface->setGeometry(screen()->geometry());
else
restoreDisplayWindowGeometryFromConfig();
if (fullscreen)
m_display_surface->showFullScreen();
else
m_display_surface->showNormal();
#else
if (isVisible() && g_emu_thread->shouldRenderToMain())
m_display_container->move(screen()->availableGeometry().topLeft());
else
restoreDisplayWindowGeometryFromConfig();
if (fullscreen)
m_display_container->showFullScreen();
else
m_display_container->showNormal();
#endif
}
else if (!render_to_main)
{
#ifdef DISPLAY_SURFACE_WINDOW
if (m_is_temporarily_windowed && g_emu_thread->shouldRenderToMain())
m_display_surface->setGeometry(geometry());
else
restoreDisplayWindowGeometryFromConfig();
m_display_surface->showNormal();
#else
if (m_is_temporarily_windowed && g_emu_thread->shouldRenderToMain())
m_display_container->setGeometry(geometry());
else
restoreDisplayWindowGeometryFromConfig();
m_display_container->showNormal();
#endif
}
else
{
pxAssertRel(m_ui.mainContainer->count() == 1, "Has no display widget");
m_ui.mainContainer->addWidget(m_display_container);
m_ui.mainContainer->setCurrentIndex(1);
}
// Attatch drag and drop signals
connect(m_display_surface, &DisplaySurface::dragEnterEvent, this, &MainWindow::dragEnterEvent);
connect(m_display_surface, &DisplaySurface::dropEvent, this, &MainWindow::dropEvent);
updateDisplayRelatedActions(true, render_to_main, fullscreen);
// We need the surface visible.
QGuiApplication::sync();
}
void MainWindow::displayResizeRequested(qint32 width, qint32 height)
{
if (!m_display_surface)
return;
// unapply the pixel scaling factor for hidpi
const float dpr = devicePixelRatioF();
width = static_cast<qint32>(std::max(static_cast<int>(std::lroundf(static_cast<float>(width) / dpr)), 1));
height = static_cast<qint32>(std::max(static_cast<int>(std::lroundf(static_cast<float>(height) / dpr)), 1));
#ifdef DISPLAY_SURFACE_WINDOW
if (!m_display_container)
{
// no parent - rendering to separate window. easy.
QtUtils::ResizePotentiallyFixedSizeWindow(m_display_surface, width, height);
return;
}
#else
if (!m_display_container->parent())
{
// no parent - rendering to separate window. easy.
QtUtils::ResizePotentiallyFixedSizeWindow(m_display_container, width, height);
return;
}
#endif
// 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_container->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_surface && !s_vm_paused)
updateDisplayWidgetCursor();
}
void MainWindow::mouseLockRequested(bool state)
{
#ifdef __linux__ // Mouse locking is only supported on X11
const bool mouse_lock_supported = QGuiApplication::platformName().toLower() == "xcb";
if (!mouse_lock_supported)
return;
#endif
Host::SetBaseBoolSettingValue("EmuCore", "EnableMouseLock", state);
Host::CommitBaseSettingChanges();
if (state)
{
setupMouseMoveHandler();
}
else
{
Common::DetachMousePositionCb();
}
if (m_settings_window)
{
InterfaceSettingsWidget* interface_settings = m_settings_window->getInterfaceSettingsWidget();
interface_settings->updateMouseLockCheckbox(state ? Qt::Checked : Qt::Unchecked);
}
}
void MainWindow::releaseRenderWindow()
{
// Now we can safely destroy the display window.
destroyDisplayWidget(true);
m_display_created = false;
m_display_is_exclusive_fullscreen = false;
m_ui.actionViewSystemDisplay->setEnabled(false);
m_ui.actionFullscreen->setEnabled(false);
}
void MainWindow::destroyDisplayWidget(bool show_game_list)
{
if (!m_display_surface)
return;
if (!(m_display_surface->isFullScreen() || m_display_is_exclusive_fullscreen) && !isRenderingToMain())
saveDisplayWindowGeometryToConfig();
if (isRenderingToMain())
{
pxAssertRel(m_ui.mainContainer->indexOf(m_display_container) == 1, "Display widget in stack");
m_ui.mainContainer->removeWidget(m_display_container);
if (show_game_list)
{
m_ui.mainContainer->setCurrentIndex(0);
m_game_list_widget->resizeTableViewColumnsToFit();
}
}
if (m_display_container)
{
m_display_container->deleteLater();
m_display_container = nullptr;
// m_display_surface will be destroyed by the container's dtor
m_display_surface = nullptr;
}
else
{
m_display_surface->deleteLater();
m_display_surface = nullptr;
}
updateDisplayRelatedActions(false, false, false);
}
void MainWindow::updateDisplayWidgetCursor()
{
pxAssertRel(m_display_surface, "Should have a display widget");
m_display_surface->updateRelativeMode(s_vm_valid && !s_vm_paused && m_relative_mouse_mode);
m_display_surface->updateCursor(s_vm_valid && !s_vm_paused && shouldHideMouseCursor());
}
void MainWindow::setupMouseMoveHandler()
{
auto mouse_cb_fn = [](int x, int y) {
if (g_main_window)
g_main_window->checkMousePosition(x, y);
};
if (!Common::AttachMousePositionCb(mouse_cb_fn))
{
Console.Warning("Unable to setup mouse position cb!");
}
return;
}
void MainWindow::checkMousePosition(int x, int y)
{
// This function is called from a different thread on Linux/macOS
// kaboom can happen when the widget is destroyed after shouldMouseLock is called, so queue everything to the UI thread
QtHost::RunOnUIThread([this, x, y]() {
if (!shouldMouseLock())
return;
// physical mouse position
const QPoint physicalPos(x, y);
const auto* displayWindow = m_display_surface;
// logical (DIP) frame rect
const QSize logicalSize = displayWindow->size();
#ifdef DISPLAY_SURFACE_WINDOW
const QPoint logicalPosition = isRenderingToMain() ? (displayWindow->position() + displayWindow->parent()->position()) : displayWindow->position();
#else
const QPoint logicalPosition = displayWindow->position() + displayWindow->parent()->position();
#endif
// The offset to the origin of the current screen is in device-independent pixels while the origin itself is native!
// The logicalPosition is the sum of these two values, so we need to separate them and only scale the offset
const QScreen* screen = displayWindow->screen();
// If we fail to get the screen associated with the window, avoid mouse locking as it's probably in an unexpected position.
if (!screen)
return;
const QPoint screenPosition = screen->geometry().topLeft();
// physical frame rect
const qreal scale = displayWindow->devicePixelRatio();
const QRectF physicalBounds(
screenPosition.x() + (logicalPosition.x() - screenPosition.x()) * scale,
screenPosition.y() + (logicalPosition.y() - screenPosition.y()) * scale,
logicalSize.width() * scale,
logicalSize.height() * scale);
if (physicalBounds.contains(physicalPos))
return;
Common::SetMousePosition(
std::clamp(physicalPos.x(), (int)physicalBounds.left(), (int)physicalBounds.right()),
std::clamp(physicalPos.y(), (int)physicalBounds.top(), (int)physicalBounds.bottom()));
});
}
void MainWindow::saveDisplayWindowGeometryToConfig()
{
if (m_display_surface->isFullScreen())
{
// if we somehow ended up here, don't save the fullscreen state to the config
return;
}
const QByteArray geometry = m_display_surface->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));
if (!geometry.isEmpty())
{
m_display_surface->restoreGeometry(geometry);
// make sure we're not loading a dodgy config which had fullscreen set...
#ifdef DISPLAY_SURFACE_WINDOW
m_display_surface->setWindowStates(m_display_surface->windowStates() & ~(Qt::WindowFullScreen | Qt::WindowActive));
#else
m_display_container->setWindowState(m_display_container->windowState() & ~(Qt::WindowFullScreen | Qt::WindowActive));
#endif
}
else
{
// default size
#ifdef DISPLAY_SURFACE_WINDOW
m_display_surface->resize(640, 480);
#else
m_display_container->resize(640, 480);
#endif
}
}
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->getInterfaceSettingsWidget(), &InterfaceSettingsWidget::backgroundChanged, m_game_list_widget, [this] { m_game_list_widget->setCustomBackground(); });
connect(m_settings_window->getInterfaceSettingsWidget(), &InterfaceSettingsWidget::preferEnglishGameListChanged, this, [] {
g_main_window->m_game_list_widget->refreshGridCovers();
Host::RunOnGSThread([] { FullscreenUI::PreferEnglishGameListChanged(); });
});
}
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);
}
void MainWindow::doGameSettings(const char* category)
{
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())
{
const QString& path = (s_current_elf_override.isEmpty() ? s_current_disc_path : s_current_elf_override);
std::optional<GameList::Entry> entry;
{
auto lock = GameList::GetLock();
const GameList::Entry* entry_ptr = GameList::GetEntryForPath(path.toUtf8().constData());
if (entry_ptr)
entry = *entry_ptr;
}
if (entry.has_value())
{
SettingsWindow::openGamePropertiesDialog(
&*entry, entry->title, entry->serial, entry->crc, !s_current_elf_override.isEmpty(), category);
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, false, category);
}
else
{
SettingsWindow::openGamePropertiesDialog(
nullptr, s_current_title.toStdString(), std::string(), s_current_disc_crc, true, category);
}
}
#ifdef ENABLE_QT_DEBUGGER
void MainWindow::openDebugger()
{
DebuggerWindow* dwnd = DebuggerWindow::getInstance();
dwnd->isVisible() ? dwnd->activateWindow() : dwnd->show();
}
#endif
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<std::string> 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<s32> save_slot, std::optional<bool> fast_boot, bool load_backup)
{
std::shared_ptr<VMBootParameters> params = std::make_shared<VMBootParameters>();
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(), load_backup);
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, const time_t entry_played_time)
{
if (QMessageBox::question(this, tr("Confirm Reset"),
tr("Are you sure you want to reset the play time for '%1' (%2)?\n\nYour current play time is %3.\n\nThis action cannot be undone.")
.arg(entry.title.empty() ? tr("empty title") : QString::fromStdString(entry.title),
entry.serial.empty() ? tr("no serial") : QString::fromStdString(entry.serial),
QString::fromStdString(GameList::FormatTimespan(entry_played_time, true))),
(QMessageBox::Yes | QMessageBox::No), QMessageBox::No) == QMessageBox::Yes)
{
GameList::ClearPlayedTimeForSerial(entry.serial);
m_game_list_widget->refresh(false, false);
}
}
void MainWindow::goToWikiPage(const GameList::Entry& entry)
{
QtUtils::OpenURL(this, fmt::format("https://wiki.pcsx2.net/{}", entry.serial).c_str());
}
void MainWindow::openSnapshotsFolderForGame(const GameList::Entry& entry)
{
// Go to top-level snapshots directory if not organizing by game.
if (EmuConfig.GS.OrganizeSnapshotsByGame && !entry.title.empty())
{
const bool prefer_english = Host::GetBaseBoolSettingValue("UI", "PreferEnglishGameList", false);
std::string game_name = (prefer_english && !entry.title_en.empty()) ? entry.title_en : entry.title;
Path::SanitizeFileName(&game_name);
const std::string game_dir = Path::Combine(EmuFolders::Snapshots, game_name);
// Make sure the per-game directory exists or that we can successfully create it.
if (FileSystem::DirectoryExists(game_dir.c_str()) || FileSystem::CreateDirectoryPath(game_dir.c_str(), false))
{
const QFileInfo fi(QString::fromStdString(game_dir));
QtUtils::OpenURL(this, QUrl::fromLocalFile(fi.absoluteFilePath()));
return;
}
QMessageBox::critical(this, tr("Error"), tr("Failed to create snapshots directory '%1'\n\nOpening default directory.").arg(QString::fromStdString(game_dir)));
}
QtUtils::OpenURL(this, QUrl::fromLocalFile(QString::fromStdString(EmuFolders::Snapshots)));
}
void MainWindow::openTextureFolderForGame(const GameList::Entry& entry)
{
const std::string serial = entry.serial;
const std::string game_texture_dir = Path::Combine(EmuFolders::Textures, serial);
// Make sure the per-game directory exists or that we can successfully create it.
if (FileSystem::DirectoryExists(game_texture_dir.c_str()) || FileSystem::CreateDirectoryPath(game_texture_dir.c_str(), false))
{
const QFileInfo fi(QString::fromStdString(game_texture_dir));
QtUtils::OpenURL(this, QUrl::fromLocalFile(fi.absoluteFilePath()));
return;
}
QMessageBox::critical(this, tr("Error"), tr("Failed to create game texture directory '%1'\n\nOpening default directory.").arg(QString::fromStdString(game_texture_dir)));
QtUtils::OpenURL(this, QUrl::fromLocalFile(QString::fromStdString(EmuFolders::Textures)));
}
void MainWindow::openMemoryCardFolder()
{
const std::string memcard_dir = EmuFolders::MemoryCards;
// Make sure directory exists or that we can successfully create it.
if (FileSystem::DirectoryExists(memcard_dir.c_str()) || FileSystem::CreateDirectoryPath(memcard_dir.c_str(), false))
{
const QFileInfo fi(QString::fromStdString(memcard_dir));
QtUtils::OpenURL(this, QUrl::fromLocalFile(fi.absoluteFilePath()));
return;
}
QMessageBox::critical(this, tr("Error"), tr("Failed to open memory card directory."));
}
void MainWindow::openVideoCaptureFolder(const GameList::Entry& entry)
{
// Go to top-level video directory if not organizing by game.
if (EmuConfig.GS.OrganizeVideoCaptureByGame && !entry.title.empty())
{
const bool prefer_english = Host::GetBaseBoolSettingValue("UI", "PreferEnglishGameList", false);
std::string game_name = (prefer_english && !entry.title_en.empty()) ? entry.title_en : entry.title;
Path::SanitizeFileName(&game_name);
const std::string game_dir = Path::Combine(EmuFolders::Videos, game_name);
// Make sure the per-game directory exists or that we can successfully create it.
if (FileSystem::DirectoryExists(game_dir.c_str()) || FileSystem::CreateDirectoryPath(game_dir.c_str(), false))
{
const QFileInfo fi(QString::fromStdString(game_dir));
QtUtils::OpenURL(this, QUrl::fromLocalFile(fi.absoluteFilePath()));
return;
}
QMessageBox::critical(this, tr("Error"), tr("Failed to create game video capture directory '%1'\n\nOpening default directory.").arg(QString::fromStdString(game_dir)));
}
QtUtils::OpenURL(this, QUrl::fromLocalFile(QString::fromStdString(EmuFolders::Videos)));
}
std::optional<bool> 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, bool load_backup)
{
if (s_vm_valid)
{
// easy when we're running
g_emu_thread->loadStateFromSlot(slot, load_backup);
return;
}
else
{
// we're not currently running, therefore we must've right clicked in the game list
const std::optional<GameList::Entry> entry = m_game_list_widget->getSelectedEntry();
if (!entry.has_value())
return;
startGameListEntry(*entry, slot, std::nullopt, load_backup);
}
}
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<VMBootParameters> params = std::make_shared<VMBootParameters>();
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<qint64>(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..."));
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;
}
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, true);
if (!FileSystem::StatFile(state_filename.c_str(), &sd))
continue;
action = menu->addAction(tr("Load Backup Slot %1 (%2)").arg(i).arg(formatTimestampForSaveStateMenu(sd.ModificationTime)));
connect(action, &QAction::triggered, [this, i]() { loadSaveStateSlot(i, true); });
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("%n save states deleted.", "", 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::updateGameGridActions(const bool show_game_grid, const bool show_grid_cover_titles)
{
m_ui.actionGridViewShowTitles->setEnabled(show_game_grid);
m_ui.actionGridViewShowFullTitles->setEnabled(show_game_grid && show_grid_cover_titles);
m_ui.actionGridViewZoomIn->setEnabled(show_game_grid);
m_ui.actionGridViewZoomOut->setEnabled(show_game_grid);
m_ui.actionGridViewRefreshCovers->setEnabled(show_game_grid);
}
void MainWindow::doStartFile(std::optional<CDVD_SourceType> source, const QString& path)
{
if (s_vm_valid)
return;
std::shared_ptr<VMBootParameters> params = std::make_shared<VMBootParameters>();
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<bool> 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);
}
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();
}
// We have to do this after resetting the VM, otherwise the reset would be
// deferred since the VM would be running, and then VMLock::~VMLock would
// trample the reset state with its unpause event.
switchToEmulationView();
}
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();
#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);
// Process events untill both EmuThread and Qt have finished exiting fullscreen
while (QtHost::IsVMValid() && (g_emu_thread->isFullscreen() || m_display_surface->isFullScreen()))
{
QApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
}
}
// Ensure main window is visible.
if (!g_main_window->isVisible())
g_main_window->show();
g_main_window->raise();
g_main_window->activateWindow();
#ifdef DISPLAY_SURFACE_WINDOW
if (!m_display_container)
{
// Create a temporary parent for the dialog.
QWidget* dialog_parent = new QWidget();
dialog_parent->setAttribute(Qt::WA_NativeWindow);
QWindow* window_handle = dialog_parent->windowHandle();
// Set the transient parent to the display surface.
// This will position the dialog_parent over the display surface (and thus so will any dialogs)
// and also enforces the focus lock of modal dialogs against the display surface.
// This works even without showing the dialog_parent window.
window_handle->setTransientParent(m_display_surface);
return VMLock(dialog_parent, was_paused, was_fullscreen, true);
}
else
return VMLock(m_display_container, was_paused, was_fullscreen, false);
#else
return VMLock(m_display_container, was_paused, was_fullscreen, false);
#endif
}
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, bool owns_parent)
: m_dialog_parent(dialog_parent)
, m_has_lock(true)
, m_was_paused(was_paused)
, m_was_fullscreen(was_fullscreen)
, m_owns_dialog_parent(owns_parent)
{
QtHost::LockVMWithDialog();
}
MainWindow::VMLock::VMLock(VMLock&& lock)
: m_dialog_parent(lock.m_dialog_parent)
, m_has_lock(lock.m_has_lock)
, m_was_paused(lock.m_was_paused)
, m_was_fullscreen(lock.m_was_fullscreen)
, m_owns_dialog_parent(lock.m_owns_dialog_parent)
{
lock.m_dialog_parent = nullptr;
lock.m_has_lock = false;
lock.m_was_paused = true;
lock.m_was_fullscreen = false;
lock.m_owns_dialog_parent = false;
}
MainWindow::VMLock::~VMLock()
{
if (m_has_lock)
QtHost::UnlockVMWithDialog();
if (m_owns_dialog_parent && m_dialog_parent)
{
m_dialog_parent->deleteLater();
m_dialog_parent = nullptr;
}
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)
{
if (m_has_lock)
QtHost::UnlockVMWithDialog();
m_has_lock = lock.m_has_lock;
m_dialog_parent = lock.m_dialog_parent;
m_was_paused = lock.m_was_paused;
m_was_fullscreen = lock.m_was_fullscreen;
lock.m_has_lock = false;
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;
}
#include "moc_MainWindow.cpp"