mirror of
https://github.com/ARMSX2/ARMSX2.git
synced 2026-08-24 16:50:16 -07:00
71 commits from474ad59818to2cf8dabe6b, 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 by431ca0c063, 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.
3913 lines
128 KiB
C++
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"
|