2026-01-12 05:18:12 -05:00
// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team
2024-07-30 13:42:36 +02:00
// SPDX-License-Identifier: GPL-3.0+
2021-12-13 22:12:54 +10:00
#include "AboutDialog.h"
2022-04-04 21:27:23 +10:00
#include "AutoUpdaterDialog.h"
2022-09-03 21:29:02 +10:00
#include "CoverDownloadDialog.h"
2021-12-13 22:12:54 +10:00
#include "DisplayWidget.h"
#include "GameList/GameListRefreshThread.h"
#include "GameList/GameListWidget.h"
2024-01-11 19:38:28 +10:00
#include "LogWindow.h"
2021-12-13 22:12:54 +10:00
#include "MainWindow.h"
#include "QtHost.h"
#include "QtUtils.h"
2023-05-13 14:30:41 +10:00
#include "SettingWidgetBinder.h"
2026-06-20 20:27:55 -07:00
#ifdef ENABLE_QT_DEBUGGER
2025-01-27 12:49:39 +00:00
#include "Debugger/Docking/DockManager.h"
2026-06-20 20:27:55 -07:00
#endif
2023-07-27 19:24:55 +10:00
#include "Settings/AchievementLoginDialog.h"
2023-10-14 18:45:09 +10:00
#include "Settings/ControllerSettingsWindow.h"
2021-12-13 22:12:54 +10:00
#include "Settings/GameListSettingsWidget.h"
#include "Settings/InterfaceSettingsWidget.h"
2024-01-18 21:10:32 +10:00
#include "Settings/MemoryCardCreateDialog.h"
2022-06-15 20:44:14 -04:00
#include "Tools/InputRecording/InputRecordingViewer.h"
2022-04-04 17:35:08 -04:00
#include "Tools/InputRecording/NewInputRecordingDlg.h"
2023-05-13 14:30:41 +10:00
2025-05-29 19:25:09 +07:00
#if !defined(__APPLE__)
#include "ShortcutCreationDialog.h"
#endif
2023-05-13 14:30:41 +10:00
#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"
2025-11-23 00:09:31 -06:00
#include "pcsx2/ImGui/FullscreenUI.h"
2023-06-24 17:46:36 +10:00
#include "pcsx2/MTGS.h"
2023-05-13 14:30:41 +10:00
#include "pcsx2/PerformanceMetrics.h"
2026-04-13 19:45:19 +07:00
#include "pcsx2/VMManager.h"
#include "pcsx2/ImGui/ImGuiOverlays.h"
#include "pcsx2/SPU2/spu2.h"
2023-05-13 14:30:41 +10:00
#include "pcsx2/Recording/InputRecording.h"
#include "pcsx2/Recording/InputRecordingControls.h"
2025-11-30 20:15:57 +00:00
#include "pcsx2/SaveState.h"
2023-12-08 01:44:50 -05:00
#include "pcsx2/SIO/Sio.h"
2025-07-01 08:07:48 +02:00
#include "pcsx2/GS/GSExtra.h"
2023-05-13 14:30:41 +10:00
#include "common/Assertions.h"
#include "common/CocoaTools.h"
#include "common/FileSystem.h"
2025-07-01 08:07:48 +02:00
#include "common/Path.h"
2023-05-13 14:30:41 +10:00
#include <QtCore/QDateTime>
2024-04-04 16:45:06 +10:00
#include <QtCore/QDir>
2023-05-13 14:30:41 +10:00
#include <QtGui/QCloseEvent>
#include <QtWidgets/QFileDialog>
#include <QtWidgets/QInputDialog>
#include <QtWidgets/QMessageBox>
#include <QtWidgets/QProgressBar>
2026-04-13 19:45:19 +07:00
#include <QtWidgets/QSlider>
2023-05-13 14:30:41 +10:00
#include <QtWidgets/QStyle>
#include <QtWidgets/QStyleFactory>
2026-04-13 19:45:19 +07:00
#include <QtWidgets/QWidgetAction>
#include <QtWidgets/QHBoxLayout>
2022-04-03 20:06:59 -04:00
2022-10-15 21:20:05 +10:00
#ifdef _WIN32
#include "common/RedtapeWindows.h"
#include <Dbt.h>
#endif
2022-12-04 18:03:30 +10:00
const char * MainWindow :: OPEN_FILE_FILTER =
2023-12-15 04:05:04 +01:00
QT_TRANSLATE_NOOP ( "MainWindow" , "All File Types (*.bin *.iso *.cue *.mdf *.chd *.cso *.zso *.gz *.elf *.irx *.gs *.gs.xz *.gs.zst *.dump);;"
2022-03-12 23:20:23 +10:00
"Single-Track Raw Images (*.bin *.iso);;"
"Cue Sheets (*.cue);;"
2023-05-13 04:31:24 +01:00
"Media Descriptor File (*.mdf);;"
2022-03-12 23:20:23 +10:00
"MAME CHD Images (*.chd);;"
"CSO Images (*.cso);;"
2023-12-15 04:05:04 +01:00
"ZSO Images (*.zso);;"
2022-04-08 04:00:08 +02:00
"GZ Images (*.gz);;"
2022-03-12 23:20:23 +10:00
"ELF Executables (*.elf);;"
"IRX Executables (*.irx);;"
2022-05-23 18:19:14 +02:00
"GS Dumps (*.gs *.gs.xz *.gs.zst);;"
"Block Dumps (*.dump)" );
2021-12-13 22:12:54 +10:00
2023-12-15 04:05:04 +01:00
const char * MainWindow :: DISC_IMAGE_FILTER = QT_TRANSLATE_NOOP ( "MainWindow" , "All File Types (*.bin *.iso *.cue *.mdf *.chd *.cso *.zso *.gz *.dump);;"
2022-12-04 18:03:30 +10:00
"Single-Track Raw Images (*.bin *.iso);;"
"Cue Sheets (*.cue);;"
2023-05-13 04:31:24 +01:00
"Media Descriptor File (*.mdf);;"
2022-12-04 18:03:30 +10:00
"MAME CHD Images (*.chd);;"
"CSO Images (*.cso);;"
2023-12-15 04:05:04 +01:00
"ZSO Images (*.zso);;"
2022-12-04 18:03:30 +10:00
"GZ Images (*.gz);;"
"Block Dumps (*.dump)" );
2022-06-28 23:34:45 +10:00
2021-12-13 22:12:54 +10:00
MainWindow * g_main_window = nullptr ;
2022-06-28 22:53:26 +10:00
// UI thread VM validity.
static bool s_vm_valid = false ;
static bool s_vm_paused = false ;
2024-01-11 18:19:27 +10:00
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 ;
2022-06-28 22:53:26 +10:00
2025-02-17 21:01:15 -05:00
static bool s_record_on_start = false ;
static QString s_path_to_recording_for_record_on_start ;
2025-11-21 22:35:19 +00:00
// 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
2022-11-20 14:26:23 +10:00
MainWindow :: MainWindow ()
2021-12-13 22:12:54 +10:00
{
pxAssert ( ! g_main_window );
g_main_window = this ;
}
MainWindow ::~ MainWindow ()
{
2022-12-20 23:47:09 +10:00
// make sure the game list isn't refreshing, because it's on a separate thread
cancelGameListRefresh ();
2023-10-14 18:45:09 +10:00
destroySubWindows ();
2023-10-11 21:04:36 -04:00
2025-02-17 10:41:31 -05:00
Common :: DetachMousePositionCb ();
2021-12-13 22:12:54 +10:00
// we compare here, since recreate destroys the window later
if ( g_main_window == this )
g_main_window = nullptr ;
2022-10-15 21:20:05 +10:00
#ifdef _WIN32
unregisterForDeviceNotifications ();
#endif
2021-12-13 22:12:54 +10:00
}
void MainWindow :: initialize ()
{
2022-05-25 17:32:10 -05:00
#ifdef __APPLE__
2025-03-28 21:12:47 -05:00
// The cocoa backing isn't initialized yet, delay this until stuff is set up with a `RunOnUIThread` call
2025-09-22 22:33:14 +01:00
QtHost :: RunOnUIThread ([ this ] {
2025-03-28 21:12:47 -05:00
CocoaTools :: MarkHelpMenu ( m_ui . menuHelp -> toNSMenu ());
});
2022-05-25 17:32:10 -05:00
#endif
2021-12-13 22:12:54 +10:00
m_ui . setupUi ( this );
setupAdditionalUi ();
connectSignals ();
2022-05-24 18:04:26 +10:00
connectVMThreadSignals ( g_emu_thread );
2021-12-13 22:12:54 +10:00
restoreStateFromConfig ();
switchToGameListView ();
updateWindowTitle ();
2023-09-09 14:47:26 +10:00
updateGameDependentActions ();
2026-05-17 23:16:05 +07:00
updateEmulationActions ( false , s_vm_valid , false );
updateStatusBarWidgetVisibility ();
if ( s_vm_paused )
{
m_ui . actionPause -> setChecked ( true );
m_ui . actionToolbarPause -> setChecked ( true );
}
2022-10-15 21:20:05 +10:00
#ifdef _WIN32
registerForDeviceNotifications ();
#endif
2025-02-17 10:41:31 -05:00
if ( Host :: GetBoolSettingValue ( "EmuCore" , "EnableMouseLock" , false ))
setupMouseMoveHandler ();
2021-12-13 22:12:54 +10:00
}
2022-05-25 15:52:07 -05:00
// 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 ());
}
}
2022-07-09 16:59:13 +10:00
QWidget * MainWindow :: getContentParent ()
{
2025-10-30 20:54:07 +00:00
return static_cast < QWidget *> ( m_ui . mainContainer );
2022-07-09 16:59:13 +10:00
}
2021-12-13 22:12:54 +10:00
void MainWindow :: setupAdditionalUi ()
{
2022-05-25 15:52:07 -05:00
makeIconsMasks ( menuBar ());
2024-01-13 01:19:51 +10:00
updateAdvancedSettingsVisibility ();
2026-04-13 19:45:19 +07:00
setupStatusBarWidgets ();
2022-11-23 23:20:49 +10:00
2022-05-24 22:37:44 +10:00
const bool toolbar_visible = Host :: GetBaseBoolSettingValue ( "UI" , "ShowToolbar" , false );
2021-12-13 22:12:54 +10:00
m_ui . actionViewToolbar -> setChecked ( toolbar_visible );
m_ui . toolBar -> setVisible ( toolbar_visible );
2022-05-24 22:37:44 +10:00
const bool toolbars_locked = Host :: GetBaseBoolSettingValue ( "UI" , "LockToolbar" , false );
2021-12-13 22:12:54 +10:00
m_ui . actionViewLockToolbar -> setChecked ( toolbars_locked );
m_ui . toolBar -> setMovable ( ! toolbars_locked );
m_ui . toolBar -> setContextMenuPolicy ( Qt :: PreventContextMenu );
2023-02-28 19:29:41 +01:00
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 );
2021-12-13 22:12:54 +10:00
for ( u32 scale = 0 ; scale <= 10 ; scale ++ )
{
2022-02-16 00:29:18 +10:00
QAction * action = m_ui . menuWindowSize -> addAction (( scale == 0 ) ? tr ( "Internal Resolution" ) : tr ( "%1x Scale" ). arg ( scale ));
2021-12-13 22:12:54 +10:00
connect ( action , & QAction :: triggered , [ scale ]() { g_emu_thread -> requestDisplaySize ( static_cast < float > ( scale )); });
}
2026-04-13 19:45:19 +07:00
const bool show_game_grid = Host :: GetBaseBoolSettingValue ( "UI" , "GameListGridView" , false );
2026-06-17 12:17:40 -04:00
const bool show_grid_cover_titles = Host :: GetBaseBoolSettingValue ( "UI" , "ShowGridCoverTitles" , false );
updateGameGridActions ( show_game_grid , show_grid_cover_titles );
2026-04-13 19:45:19 +07:00
m_game_list_widget = new GameListWidget ( getContentParent ());
m_game_list_widget -> initialize ();
m_ui . actionGridViewShowTitles -> setChecked ( m_game_list_widget -> getShowGridCoverTitles ());
2026-06-17 12:17:40 -04:00
m_ui . actionGridViewShowFullTitles -> setChecked ( m_game_list_widget -> getShowGridFullCoverTitles ());
2026-04-13 19:45:19 +07:00
m_ui . mainContainer -> addWidget ( m_game_list_widget );
2023-03-03 21:30:40 +10:00
updateEmulationActions ( false , false , false );
2022-09-15 19:43:21 +10:00
updateDisplayRelatedActions ( false , false , false );
2022-04-18 23:35:14 +10:00
#ifdef ENABLE_RAINTEGRATION
if ( Achievements :: IsUsingRAIntegration ())
{
2024-01-11 19:38:28 +10:00
QMenu * raMenu = new QMenu ( QStringLiteral ( "RAIntegration" ), m_ui . menuTools );
2022-04-18 23:35:14 +10:00
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 );
}
2022-12-04 18:03:30 +10:00
connect ( raAction , & QAction :: triggered , this ,
[ id = id ]() { Host :: RunOnCPUThread ([ id ]() { Achievements :: RAIntegration :: ActivateMenuItem ( id ); }, false ); });
2022-04-18 23:35:14 +10:00
}
});
2024-01-11 19:38:28 +10:00
m_ui . menuTools -> insertMenu ( m_ui . menuInputRecording -> menuAction (), raMenu );
2022-04-18 23:35:14 +10:00
}
#endif
2021-12-13 22:12:54 +10:00
}
2026-04-13 19:45:19 +07:00
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 &&
2026-06-11 05:23:59 +07:00
( game_layer -> ContainsValue ( "SPU2/Output" , "StandardVolume" ) || game_layer -> ContainsValue ( "SPU2/Output" , "OutputMuted" )));
2026-04-13 19:45:19 +07:00
});
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 );
});
});
}
2021-12-13 22:12:54 +10:00
void MainWindow :: connectSignals ()
{
connect ( m_ui . actionStartFile , & QAction :: triggered , this , & MainWindow :: onStartFileActionTriggered );
2022-06-28 23:34:45 +10:00
connect ( m_ui . actionStartDisc , & QAction :: triggered , this , & MainWindow :: onStartDiscActionTriggered );
2021-12-13 22:12:54 +10:00
connect ( m_ui . actionStartBios , & QAction :: triggered , this , & MainWindow :: onStartBIOSActionTriggered );
connect ( m_ui . actionChangeDiscFromFile , & QAction :: triggered , this , & MainWindow :: onChangeDiscFromFileActionTriggered );
2022-02-16 00:29:18 +10:00
connect ( m_ui . actionChangeDiscFromDevice , & QAction :: triggered , this , & MainWindow :: onChangeDiscFromDeviceActionTriggered );
connect ( m_ui . actionChangeDiscFromGameList , & QAction :: triggered , this , & MainWindow :: onChangeDiscFromGameListActionTriggered );
2022-06-29 00:00:51 +10:00
connect ( m_ui . actionRemoveDisc , & QAction :: triggered , this , & MainWindow :: onRemoveDiscActionTriggered );
2021-12-13 22:12:54 +10:00
connect ( m_ui . menuChangeDisc , & QMenu :: aboutToShow , this , & MainWindow :: onChangeDiscMenuAboutToShow );
connect ( m_ui . menuChangeDisc , & QMenu :: aboutToHide , this , & MainWindow :: onChangeDiscMenuAboutToHide );
2022-05-15 18:20:21 +10:00
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 ); });
2023-09-04 21:34:57 +10:00
connect ( m_ui . actionStartFullscreenUI , & QAction :: triggered , this , & MainWindow :: onStartFullscreenUITriggered );
connect ( m_ui . actionToolbarStartFullscreenUI , & QAction :: triggered , this , & MainWindow :: onStartFullscreenUITriggered );
2023-06-30 22:19:09 +10:00
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 ,
2023-10-14 18:45:09 +10:00
[ this ]() { doControllerSettings ( ControllerSettingsWindow :: Category :: GlobalSettings ); });
2025-09-08 16:37:26 -05:00
connect ( m_ui . actionToolbarHotkeySettings , & QAction :: triggered ,
[ this ]() { doControllerSettings ( ControllerSettingsWindow :: Category :: HotkeySettings ); });
2023-06-30 22:19:09 +10:00
connect ( m_ui . actionToolbarScreenshot , & QAction :: triggered , this , & MainWindow :: onScreenshotActionTriggered );
2021-12-13 22:12:54 +10:00
connect ( m_ui . actionExit , & QAction :: triggered , this , & MainWindow :: close );
2022-05-12 14:53:40 -07:00
connect ( m_ui . actionScreenshot , & QAction :: triggered , this , & MainWindow :: onScreenshotActionTriggered );
2021-12-13 22:12:54 +10:00
connect ( m_ui . menuLoadState , & QMenu :: aboutToShow , this , & MainWindow :: onLoadStateMenuAboutToShow );
connect ( m_ui . menuSaveState , & QMenu :: aboutToShow , this , & MainWindow :: onSaveStateMenuAboutToShow );
2022-02-16 00:29:18 +10:00
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" ); });
2026-03-07 17:02:39 -05:00
connect ( m_ui . actionOSDSettings , & QAction :: triggered , [ this ]() { doSettings ( "On-Screen Display" ); });
2022-02-16 00:29:18 +10:00
connect ( m_ui . actionAudioSettings , & QAction :: triggered , [ this ]() { doSettings ( "Audio" ); });
2022-03-25 19:24:41 +10:00
connect ( m_ui . actionMemoryCardSettings , & QAction :: triggered , [ this ]() { doSettings ( "Memory Cards" ); });
2022-03-13 17:58:39 +00:00
connect ( m_ui . actionDEV9Settings , & QAction :: triggered , [ this ]() { doSettings ( "Network & HDD" ); });
2022-06-18 20:20:24 +10:00
connect ( m_ui . actionFolderSettings , & QAction :: triggered , [ this ]() { doSettings ( "Folders" ); });
2022-04-18 23:35:14 +10:00
connect ( m_ui . actionAchievementSettings , & QAction :: triggered , [ this ]() { doSettings ( "Achievements" ); });
2022-12-04 18:03:30 +10:00
connect ( m_ui . actionControllerSettings , & QAction :: triggered ,
2023-10-14 18:45:09 +10:00
[ this ]() { doControllerSettings ( ControllerSettingsWindow :: Category :: GlobalSettings ); });
2022-12-04 18:03:30 +10:00
connect ( m_ui . actionHotkeySettings , & QAction :: triggered ,
2023-10-14 18:45:09 +10:00
[ this ]() { doControllerSettings ( ControllerSettingsWindow :: Category :: HotkeySettings ); });
2022-12-04 18:03:30 +10:00
connect ( m_ui . actionAddGameDirectory , & QAction :: triggered ,
2023-10-14 18:45:09 +10:00
[ this ]() { getSettingsWindow () -> getGameListSettingsWidget () -> addSearchDirectory ( this ); });
2025-11-06 14:36:16 +00:00
connect ( m_ui . actionScanForNewGames , & QAction :: triggered , [ this ]() { refreshGameList ( false , true ); });
connect ( m_ui . actionRescanAllGames , & QAction :: triggered , [ this ]() { refreshGameList ( true , true ); });
2021-12-13 22:12:54 +10:00
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 );
2026-07-23 19:48:13 -07:00
connect ( m_ui . actionWebsite , & QAction :: triggered , this , & MainWindow :: onWebsiteActionTriggered );
2021-12-13 22:12:54 +10:00
connect ( m_ui . actionAbout , & QAction :: triggered , this , & MainWindow :: onAboutActionTriggered );
connect ( m_ui . actionOpenDataDirectory , & QAction :: triggered , this , & MainWindow :: onToolsOpenDataDirectoryTriggered );
2022-09-03 21:29:02 +10:00
connect ( m_ui . actionCoverDownloader , & QAction :: triggered , this , & MainWindow :: onToolsCoverDownloaderTriggered );
2021-12-13 22:12:54 +10:00
connect ( m_ui . actionGridViewShowTitles , & QAction :: triggered , m_game_list_widget , & GameListWidget :: setShowCoverTitles );
2026-06-17 12:17:40 -04:00
connect ( m_ui . actionGridViewShowFullTitles , & QAction :: triggered , m_game_list_widget , & GameListWidget :: setShowFullCoverTitles );
2021-12-13 22:12:54 +10:00
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 ();
});
2022-02-16 00:29:18 +10:00
connect ( m_ui . actionGridViewRefreshCovers , & QAction :: triggered , m_game_list_widget , & GameListWidget :: refreshGridCovers );
2022-07-09 18:52:33 +10:00
connect ( m_game_list_widget , & GameListWidget :: layoutChange , this , [ this ]() {
QSignalBlocker sb ( m_ui . actionGridViewShowTitles );
2026-06-17 12:17:40 -04:00
QSignalBlocker sb2 ( m_ui . actionGridViewShowFullTitles );
2022-07-09 18:52:33 +10:00
m_ui . actionGridViewShowTitles -> setChecked ( m_game_list_widget -> getShowGridCoverTitles ());
2026-06-17 12:17:40 -04:00
m_ui . actionGridViewShowFullTitles -> setChecked ( m_game_list_widget -> getShowGridFullCoverTitles ());
updateGameGridActions ( m_game_list_widget -> isShowingGameGrid (), m_game_list_widget -> getShowGridCoverTitles ());
2022-07-09 18:52:33 +10:00
});
2021-12-13 22:12:54 +10:00
2022-04-03 23:46:05 +10:00
SettingWidgetBinder :: BindWidgetToBoolSetting ( nullptr , m_ui . actionViewStatusBarVerbose , "UI" , "VerboseStatusBar" , false );
2022-03-04 19:46:47 +10:00
SettingWidgetBinder :: BindWidgetToBoolSetting ( nullptr , m_ui . actionEnableSystemConsole , "Logging" , "EnableSystemConsole" , false );
2024-01-13 01:19:51 +10:00
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 ;
}
2022-03-04 19:46:47 +10:00
#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
2026-04-03 14:30:28 -04:00
SettingWidgetBinder :: BindWidgetToBoolSetting ( nullptr , m_ui . actionEnableEEConsoleLogging , "Logging" , "EnableEEConsole" , false );
SettingWidgetBinder :: BindWidgetToBoolSetting ( nullptr , m_ui . actionEnableIOPConsoleLogging , "Logging" , "EnableIOPConsole" , false );
2024-01-11 19:38:28 +10:00
SettingWidgetBinder :: BindWidgetToBoolSetting ( nullptr , m_ui . actionEnableLogWindow , "Logging" , "EnableLogWindow" , false );
2025-03-16 18:18:04 +00:00
SettingWidgetBinder :: BindWidgetToBoolSetting ( nullptr , m_ui . actionEnableFileLogging , "Logging" , "EnableFileLogging" , true );
2022-05-25 00:15:41 +10:00
SettingWidgetBinder :: BindWidgetToBoolSetting ( nullptr , m_ui . actionEnableLogTimestamps , "Logging" , "EnableTimestamps" , true );
2022-05-24 22:02:05 +10:00
SettingWidgetBinder :: BindWidgetToBoolSetting ( nullptr , m_ui . actionEnableCDVDVerboseReads , "EmuCore" , "CdvdVerboseReads" , false );
2022-05-24 22:22:15 +10:00
SettingWidgetBinder :: BindWidgetToBoolSetting ( nullptr , m_ui . actionSaveBlockDump , "EmuCore" , "CdvdDumpBlocks" , false );
2022-11-23 23:20:49 +10:00
m_ui . actionShowAdvancedSettings -> setChecked ( QtHost :: ShouldShowAdvancedSettings ());
2022-05-24 22:22:15 +10:00
connect ( m_ui . actionSaveBlockDump , & QAction :: toggled , this , & MainWindow :: onBlockDumpActionToggled );
2022-11-23 23:20:49 +10:00
connect ( m_ui . actionShowAdvancedSettings , & QAction :: toggled , this , & MainWindow :: onShowAdvancedSettingsToggled );
2022-05-16 21:10:34 +10:00
connect ( m_ui . actionSaveGSDump , & QAction :: triggered , this , & MainWindow :: onSaveGSDumpActionTriggered );
2024-07-19 22:42:42 +07:00
connect ( m_ui . actionVideoCapture , & QAction :: toggled , this , & MainWindow :: onVideoCaptureToggled );
2023-09-09 14:47:26 +10:00
connect ( m_ui . actionEditPatches , & QAction :: triggered , this , [ this ]() { onToolsEditCheatsPatchesTriggered ( false ); });
connect ( m_ui . actionEditCheats , & QAction :: triggered , this , [ this ]() { onToolsEditCheatsPatchesTriggered ( true ); });
2022-05-16 21:10:34 +10:00
2022-04-03 20:06:59 -04:00
// 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 );
2022-06-15 20:44:14 -04:00
connect ( m_ui . actionInputRecOpenViewer , & QAction :: triggered , this , & MainWindow :: onInputRecOpenViewer );
2022-04-03 20:06:59 -04:00
2021-12-13 22:12:54 +10:00
// 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 );
2022-02-16 00:29:18 +10:00
connect ( m_game_list_widget , & GameListWidget :: selectionChanged , this , & MainWindow :: onGameListSelectionChanged , Qt :: QueuedConnection );
connect ( m_game_list_widget , & GameListWidget :: entryActivated , this , & MainWindow :: onGameListEntryActivated , Qt :: QueuedConnection );
2022-12-04 18:03:30 +10:00
connect ( m_game_list_widget , & GameListWidget :: entryContextMenuRequested , this , & MainWindow :: onGameListEntryContextMenuRequested ,
Qt :: QueuedConnection );
2022-05-07 16:32:44 +10:00
connect ( m_game_list_widget , & GameListWidget :: addGameDirectoryRequested , this ,
2023-10-14 18:45:09 +10:00
[ this ]() { getSettingsWindow () -> getGameListSettingsWidget () -> addSearchDirectory ( this ); });
2023-12-31 17:45:13 +10:00
createRendererSwitchMenu ();
2021-12-13 22:12:54 +10:00
}
void MainWindow :: connectVMThreadSignals ( EmuThread * thread )
{
2024-05-15 07:42:19 +10:00
connect ( thread , & EmuThread :: statusMessage , this , & MainWindow :: onStatusMessage );
2023-04-25 22:52:41 +10:00
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 );
2023-07-23 15:07:46 +10:00
connect ( thread , & EmuThread :: onMouseModeRequested , this , & MainWindow :: mouseModeRequested );
2026-02-11 13:47:22 -06:00
connect ( thread , & EmuThread :: onMouseLockRequested , this , & MainWindow :: mouseLockRequested );
2023-09-04 21:34:57 +10:00
connect ( thread , & EmuThread :: onFullscreenUIStateChange , this , & MainWindow :: onFullscreenUIStateChange );
2021-12-13 22:12:54 +10:00
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 );
2023-07-06 23:18:30 +10:00
connect ( thread , & EmuThread :: onCaptureStarted , this , & MainWindow :: onCaptureStarted );
connect ( thread , & EmuThread :: onCaptureStopped , this , & MainWindow :: onCaptureStopped );
2023-07-27 19:24:55 +10:00
connect ( thread , & EmuThread :: onAchievementsLoginRequested , this , & MainWindow :: onAchievementsLoginRequested );
2023-09-17 20:19:51 +10:00
connect ( thread , & EmuThread :: onAchievementsHardcoreModeChanged , this , & MainWindow :: onAchievementsHardcoreModeChanged );
2021-12-13 22:12:54 +10:00
2023-12-08 01:44:50 -05:00
connect ( m_ui . actionReset , & QAction :: triggered , this , & MainWindow :: requestReset );
2021-12-13 22:12:54 +10:00
connect ( m_ui . actionPause , & QAction :: toggled , thread , & EmuThread :: setVMPaused );
connect ( m_ui . actionFullscreen , & QAction :: triggered , thread , & EmuThread :: toggleFullscreen );
2023-12-08 01:44:50 -05:00
connect ( m_ui . actionToolbarReset , & QAction :: triggered , this , & MainWindow :: requestReset );
2023-06-30 22:19:09 +10:00
connect ( m_ui . actionToolbarPause , & QAction :: toggled , thread , & EmuThread :: setVMPaused );
connect ( m_ui . actionToolbarFullscreen , & QAction :: triggered , thread , & EmuThread :: toggleFullscreen );
2021-12-13 22:12:54 +10:00
connect ( m_ui . actionToggleSoftwareRendering , & QAction :: triggered , thread , & EmuThread :: toggleSoftwareRendering );
2026-06-20 20:27:55 -07:00
#ifdef ENABLE_QT_DEBUGGER
2022-12-24 01:51:44 -05:00
connect ( m_ui . actionDebugger , & QAction :: triggered , this , & MainWindow :: openDebugger );
2026-06-20 20:27:55 -07:00
#else
m_ui . actionDebugger -> setVisible ( false );
#endif
2021-12-13 22:12:54 +10:00
connect ( m_ui . actionReloadPatches , & QAction :: triggered , thread , & EmuThread :: reloadPatches );
2023-12-31 17:45:13 +10:00
}
2021-12-13 22:12:54 +10:00
2023-12-31 17:45:13 +10:00
void MainWindow :: createRendererSwitchMenu ()
{
static constexpr const GSRendererType renderers [] = {
GSRendererType :: Auto ,
#if defined(_WIN32)
GSRendererType :: DX11 ,
GSRendererType :: DX12 ,
#elif defined(__APPLE__)
GSRendererType :: Metal ,
2021-12-13 22:12:54 +10:00
#endif
2026-06-08 01:14:16 -04:00
#if defined(ENABLE_OPENGL) && !defined(__APPLE__)
2023-12-31 17:45:13 +10:00
GSRendererType :: OGL ,
#endif
2026-06-08 01:14:16 -04:00
#if defined(ENABLE_VULKAN) && !defined(__APPLE__)
2023-12-31 17:45:13 +10:00
GSRendererType :: VK ,
#endif
GSRendererType :: SW ,
GSRendererType :: Null ,
};
const GSRendererType current_renderer = static_cast < GSRendererType > (
Host :: GetBaseIntSettingValue ( "EmuCore/GS" , "Renderer" , static_cast < int > ( GSRendererType :: Auto )));
2025-07-29 18:21:02 +01:00
QActionGroup * switch_renderer_group = new QActionGroup ( m_ui . menuDebugSwitchRenderer );
2023-12-31 17:45:13 +10:00
for ( const GSRendererType renderer : renderers )
2021-12-13 22:12:54 +10:00
{
2025-07-29 18:21:02 +01:00
QAction * action = new QAction (
QString :: fromUtf8 ( Pcsx2Config :: GSOptions :: GetRendererName ( renderer )), switch_renderer_group );
2023-12-31 17:45:13 +10:00
action -> setCheckable ( true );
action -> setChecked ( current_renderer == renderer );
2026-04-13 19:45:19 +07:00
connect ( action , & QAction :: triggered , [ renderer ] {
Host :: SetBaseIntSettingValue ( "EmuCore/GS" , "Renderer" , static_cast < int > ( renderer ));
Host :: CommitBaseSettingChanges ();
g_emu_thread -> applySettings ();
});
2021-12-13 22:12:54 +10:00
}
2025-07-29 18:21:02 +01:00
m_ui . menuDebugSwitchRenderer -> addActions ( switch_renderer_group -> actions ());
2021-12-13 22:12:54 +10:00
}
void MainWindow :: recreate ()
{
2024-06-23 06:40:25 +10:00
destroySubWindows ();
2023-12-03 17:37:27 +10:00
const bool was_display_created = m_display_created ;
if ( was_display_created )
{
g_emu_thread -> setSurfaceless ( true );
2025-10-30 20:54:07 +00:00
while ( m_display_surface || ! g_emu_thread -> isSurfaceless ())
2023-12-03 17:37:27 +10:00
QApplication :: processEvents ( QEventLoop :: ExcludeUserInputEvents , 1 );
m_display_created = false ;
}
2021-12-13 22:12:54 +10:00
2022-10-15 21:20:05 +10:00
// We need to close input sources, because e.g. DInput uses our window handle.
g_emu_thread -> closeInputSources ();
2021-12-13 22:12:54 +10:00
close ();
g_main_window = nullptr ;
2022-11-20 14:26:23 +10:00
MainWindow * new_main_window = new MainWindow ();
2023-12-03 17:37:27 +10:00
pxAssert ( g_main_window == new_main_window );
2021-12-13 22:12:54 +10:00
new_main_window -> initialize ();
2025-11-06 14:36:16 +00:00
new_main_window -> refreshGameList ( false , false );
2021-12-13 22:12:54 +10:00
new_main_window -> show ();
deleteLater ();
2022-10-15 21:20:05 +10:00
2024-01-11 19:38:28 +10:00
// Recreate log window as well. Then make sure we're still on top.
LogWindow :: updateSettings ();
new_main_window -> raise ();
new_main_window -> activateWindow ();
2022-10-15 21:20:05 +10:00
// Reload the sources we just closed.
2024-05-15 23:29:01 +10:00
g_emu_thread -> applySettings ();
2023-12-03 17:37:27 +10:00
if ( was_display_created )
{
g_emu_thread -> setSurfaceless ( false );
2025-09-20 22:30:02 -04:00
g_main_window -> updateEmulationActions ( false , s_vm_valid , false );
2023-12-03 17:37:27 +10:00
g_main_window -> onFullscreenUIStateChange ( g_emu_thread -> isRunningFullscreenUI ());
}
2026-05-17 23:16:05 +07:00
if ( s_vm_valid )
g_emu_thread -> updatePerformanceMetrics ( true );
2021-12-13 22:12:54 +10:00
}
2022-09-07 17:44:10 +10:00
void MainWindow :: recreateSettings ()
{
QString current_category ;
2023-10-15 00:44:11 -05:00
if ( m_settings_window )
2022-09-07 17:44:10 +10:00
{
2023-10-15 00:44:11 -05:00
const bool was_visible = m_settings_window -> isVisible ();
2022-11-23 23:20:49 +10:00
2023-10-15 00:44:11 -05:00
current_category = m_settings_window -> getCategory ();
m_settings_window -> hide ();
m_settings_window -> deleteLater ();
m_settings_window = nullptr ;
2022-11-23 23:20:49 +10:00
if ( ! was_visible )
return ;
2022-09-07 17:44:10 +10:00
}
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 ();
}
2024-03-22 22:01:42 +10:00
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 );
}
2024-08-23 00:09:46 +07:00
// Big picture might still be active.
if ( m_display_created )
g_emu_thread -> stopFullscreenUI ();
2024-03-22 22:01:42 +10:00
// Ensure subwindows are removed before quitting. That way the log window cancelling
// the close event won't cancel the quit process.
destroySubWindows ();
QGuiApplication :: quit ();
}
2023-10-14 18:45:09 +10:00
void MainWindow :: destroySubWindows ()
{
2026-06-20 20:27:55 -07:00
#ifdef ENABLE_QT_DEBUGGER
2025-01-27 12:49:39 +00:00
DebuggerWindow :: destroyInstance ();
2026-06-20 20:27:55 -07:00
#endif
2022-07-24 23:52:22 +10:00
2023-10-14 18:45:09 +10:00
if ( m_controller_settings_window )
{
m_controller_settings_window -> close ();
m_controller_settings_window -> deleteLater ();
m_controller_settings_window = nullptr ;
}
2023-10-15 00:44:11 -05:00
if ( m_settings_window )
2023-10-14 18:45:09 +10:00
{
2023-10-15 00:44:11 -05:00
m_settings_window -> close ();
m_settings_window -> deleteLater ();
m_settings_window = nullptr ;
2023-10-14 18:45:09 +10:00
}
2023-12-18 13:47:09 +10:00
SettingsWindow :: closeGamePropertiesDialogs ();
2024-01-11 19:38:28 +10:00
LogWindow :: destroy ();
2023-10-14 18:45:09 +10:00
}
2021-12-13 22:12:54 +10:00
2022-05-12 14:53:40 -07:00
void MainWindow :: onScreenshotActionTriggered ()
{
2022-05-16 21:10:34 +10:00
g_emu_thread -> queueSnapshot ( 0 );
}
void MainWindow :: onSaveGSDumpActionTriggered ()
{
g_emu_thread -> queueSnapshot ( 1 );
2022-05-12 14:53:40 -07:00
}
2022-05-24 22:22:15 +10:00
void MainWindow :: onBlockDumpActionToggled ( bool checked )
{
if ( ! checked )
return ;
2024-01-14 14:36:02 +10:00
std :: string old_directory = Host :: GetBaseStringSettingValue ( "EmuCore" , "BlockDumpSaveDirectory" , "" );
2022-05-24 22:22:15 +10:00
if ( old_directory . empty ())
old_directory = FileSystem :: GetWorkingDirectory ();
// prompt for a location to save
2024-01-14 14:36:02 +10:00
const QString new_dir ( QDir :: toNativeSeparators (
QFileDialog :: getExistingDirectory ( this , tr ( "Select location to save block dump:" ), QString :: fromStdString ( old_directory ))));
2022-05-24 22:22:15 +10:00
if ( new_dir . isEmpty ())
{
// disable it again
m_ui . actionSaveBlockDump -> setChecked ( false );
return ;
}
2022-09-07 17:44:10 +10:00
Host :: SetBaseStringSettingValue ( "EmuCore" , "BlockDumpSaveDirectory" , new_dir . toUtf8 (). constData ());
Host :: CommitBaseSettingChanges ();
2022-10-19 19:36:23 +01:00
g_emu_thread -> applySettings ();
2022-05-24 22:22:15 +10:00
}
2022-11-23 23:20:49 +10:00
void MainWindow :: onShowAdvancedSettingsToggled ( bool checked )
{
if ( checked && ! Host :: GetBaseBoolSettingValue ( "UI" , "AdvancedSettingsWarningShown" , false ))
{
QCheckBox * cb = new QCheckBox ( tr ( "Do not show again" ));
QMessageBox mb ( this );
2024-04-02 14:27:49 +07:00
mb . setWindowIcon ( QtHost :: GetAppIcon ());
mb . setWindowModality ( Qt :: WindowModal );
2022-11-23 23:20:49 +10:00
mb . setWindowTitle ( tr ( "Show Advanced Settings" ));
2022-12-04 18:03:30 +10:00
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 "
2026-07-23 19:48:13 -07:00
"The ARMSX2 team will not provide any support for configurations that modify these settings, you are on your own. \n\n "
2022-12-04 18:03:30 +10:00
"Are you sure you want to continue?" ));
2022-11-23 23:20:49 +10:00
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 ();
2024-01-13 01:19:51 +10:00
updateAdvancedSettingsVisibility ();
2022-11-23 23:20:49 +10:00
// just recreate the entire settings window, it's easier.
2023-10-15 00:44:11 -05:00
if ( m_settings_window )
2022-11-23 23:20:49 +10:00
recreateSettings ();
}
2024-01-13 01:19:51 +10:00
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 );
}
2024-07-19 22:42:42 +07:00
void MainWindow :: onVideoCaptureToggled ( bool checked )
2022-12-18 23:05:00 +10:00
{
if ( ! s_vm_valid )
2025-02-17 21:01:15 -05:00
{
2025-10-26 12:12:57 -05:00
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 );
2025-02-17 21:01:15 -05:00
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)) ;
2025-10-26 12:12:57 -05:00
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 ();
2025-02-17 21:01:15 -05:00
}
}
else
{
msgbox . setText ( tr ( "Did you want to cancel recording on boot?" ));
if ( msgbox . exec () == QMessageBox :: Yes )
s_record_on_start = false ;
}
2025-10-26 12:12:57 -05:00
QSignalBlocker sb ( m_ui . actionVideoCapture );
m_ui . actionVideoCapture -> setChecked ( s_record_on_start );
2022-12-18 23:05:00 +10:00
return ;
2025-02-17 21:01:15 -05:00
}
2022-12-18 23:05:00 +10:00
2023-07-06 23:18:30 +10:00
// Reset the checked state, we'll get updated by the GS thread.
2024-07-19 22:42:42 +07:00
QSignalBlocker sb ( m_ui . actionVideoCapture );
m_ui . actionVideoCapture -> setChecked ( ! checked );
2023-07-06 23:18:30 +10:00
2022-12-18 23:05:00 +10:00
if ( ! checked )
{
g_emu_thread -> endCapture ();
return ;
}
2025-02-26 20:25:55 +00:00
if ( s_record_on_start && ! s_path_to_recording_for_record_on_start . isEmpty ())
2025-02-17 21:01:15 -05:00
{
// 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)) ;
2022-12-18 23:05:00 +10:00
2025-02-17 21:01:15 -05:00
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 );
}
2022-12-18 23:05:00 +10:00
}
2023-07-06 23:18:30 +10:00
void MainWindow :: onCaptureStarted ( const QString & filename )
{
if ( ! s_vm_valid )
return ;
2024-07-19 22:42:42 +07:00
QSignalBlocker sb ( m_ui . actionVideoCapture );
m_ui . actionVideoCapture -> setChecked ( true );
2023-07-06 23:18:30 +10:00
}
void MainWindow :: onCaptureStopped ()
{
if ( ! s_vm_valid )
return ;
2024-07-19 22:42:42 +07:00
QSignalBlocker sb ( m_ui . actionVideoCapture );
m_ui . actionVideoCapture -> setChecked ( false );
2023-07-06 23:18:30 +10:00
}
2023-07-27 19:24:55 +10:00
void MainWindow :: onAchievementsLoginRequested ( Achievements :: LoginRequestReason reason )
{
auto lock = pauseAndLockVM ();
2023-09-17 20:19:51 +10:00
AchievementLoginDialog dlg ( lock . getDialogParent (), reason );
2023-07-27 19:24:55 +10:00
dlg . exec ();
}
2023-09-17 20:19:51 +10:00
void MainWindow :: onAchievementsHardcoreModeChanged ( bool enabled )
{
// disable debugger while hardcore mode is active
m_ui . actionDebugger -> setDisabled ( enabled );
2025-09-20 22:30:02 -04:00
// refresh emulation actions to show/hide load state buttons based on hardcore mode
updateEmulationActions ( s_vm_valid , s_vm_valid , false );
2023-09-17 20:19:51 +10:00
if ( enabled )
{
2026-06-20 20:27:55 -07:00
#ifdef ENABLE_QT_DEBUGGER
2024-03-31 18:22:50 +02:00
// If PauseOnEntry is enabled, we prompt the user to disable Hardcore Mode
// or cancel the action later, so we should keep the debugger around
2025-01-27 12:49:39 +00:00
if ( g_debugger_window && ! DebugInterface :: getPauseOnEntry ())
DebuggerWindow :: destroyInstance ();
2026-06-20 20:27:55 -07:00
#endif
2023-09-17 20:19:51 +10:00
}
}
2023-02-28 19:29:41 +01:00
void MainWindow :: onSettingsTriggeredFromToolbar ()
{
if ( s_vm_valid )
{
m_settings_toolbar_menu -> exec ( QCursor :: pos ());
}
else
{
doSettings ();
}
}
2021-12-13 22:12:54 +10:00
void MainWindow :: saveStateToConfig ()
{
2022-06-29 12:44:10 +10:00
if ( ! isVisible ())
return ;
2023-02-18 13:19:25 +10:00
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 ())
2021-12-13 22:12:54 +10:00
{
2023-02-18 13:19:25 +10:00
Host :: SetBaseStringSettingValue ( "UI" , "MainWindowGeometry" , geometry_b64 . constData ());
changed = true ;
2021-12-13 22:12:54 +10:00
}
2023-02-18 13:19:25 +10:00
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 ())
2021-12-13 22:12:54 +10:00
{
2023-02-18 13:19:25 +10:00
Host :: SetBaseStringSettingValue ( "UI" , "MainWindowState" , state_b64 . constData ());
changed = true ;
2021-12-13 22:12:54 +10:00
}
2023-02-18 13:19:25 +10:00
2026-05-05 11:58:06 -04:00
if ( Host :: ContainsBaseSettingValue ( "UI" , "MainWindowMaximized" ))
{
const bool maximized = Host :: GetBaseBoolSettingValue ( "UI" , "MainWindowMaximized" );
if ( maximized != isMaximized ())
{
Host :: SetBaseBoolSettingValue ( "UI" , "MainWindowMaximized" , isMaximized ());
changed = true ;
}
}
2026-05-04 18:41:57 -04:00
if ( Host :: ContainsBaseSettingValue ( "UI" , "MainWindowFullscreen" ))
{
const bool fullscreen = Host :: GetBaseBoolSettingValue ( "UI" , "MainWindowFullscreen" );
if ( fullscreen != isFullScreen ())
{
Host :: SetBaseBoolSettingValue ( "UI" , "MainWindowFullscreen" , isFullScreen ());
changed = true ;
}
}
2023-02-18 13:19:25 +10:00
if ( changed )
Host :: CommitBaseSettingChanges ();
2021-12-13 22:12:54 +10:00
}
void MainWindow :: restoreStateFromConfig ()
{
{
2022-05-24 22:37:44 +10:00
const std :: string geometry_b64 = Host :: GetBaseStringSettingValue ( "UI" , "MainWindowGeometry" );
2021-12-13 22:12:54 +10:00
const QByteArray geometry = QByteArray :: fromBase64 ( QByteArray :: fromStdString ( geometry_b64 ));
if ( ! geometry . isEmpty ())
restoreGeometry ( geometry );
}
{
2022-05-24 22:37:44 +10:00
const std :: string state_b64 = Host :: GetBaseStringSettingValue ( "UI" , "MainWindowState" );
2021-12-13 22:12:54 +10:00
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 ());
}
}
2026-05-04 18:41:57 -04:00
2026-05-05 11:58:06 -04:00
{
if ( Host :: ContainsBaseSettingValue ( "UI" , "MainWindowMaximized" ))
{
const bool maximized = Host :: GetBaseBoolSettingValue ( "UI" , "MainWindowMaximized" );
if ( maximized )
showMaximized ();
}
}
2026-05-04 18:41:57 -04:00
{
if ( Host :: ContainsBaseSettingValue ( "UI" , "MainWindowFullscreen" ))
{
const bool fullscreen = Host :: GetBaseBoolSettingValue ( "UI" , "MainWindowFullscreen" );
if ( fullscreen )
showFullScreen ();
}
}
2021-12-13 22:12:54 +10:00
}
2023-03-03 21:30:40 +10:00
void MainWindow :: updateEmulationActions ( bool starting , bool running , bool stopping )
2021-12-13 22:12:54 +10:00
{
2024-07-19 06:25:42 -05:00
const bool starting_or_running_or_stopping = starting || running || stopping ;
2021-12-13 22:12:54 +10:00
2024-07-19 06:25:42 -05:00
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 );
2021-12-13 22:12:54 +10:00
m_ui . actionPowerOff -> setEnabled ( running );
2022-05-07 22:56:44 +10:00
m_ui . actionPowerOffWithoutSaving -> setEnabled ( running );
2021-12-13 22:12:54 +10:00
m_ui . actionReset -> setEnabled ( running );
m_ui . actionPause -> setEnabled ( running );
m_ui . actionScreenshot -> setEnabled ( running );
m_ui . menuChangeDisc -> setEnabled ( running );
2025-09-20 22:30:02 -04:00
m_ui . menuLoadState -> setEnabled ( running && ! Achievements :: IsHardcoreModeActive ());
2021-12-13 22:12:54 +10:00
m_ui . menuSaveState -> setEnabled ( running );
2024-07-19 06:25:42 -05:00
m_ui . actionSaveGSDump -> setEnabled ( running );
2021-12-13 22:12:54 +10:00
2023-06-30 22:19:09 +10:00
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 );
2025-09-20 22:30:02 -04:00
m_ui . actionToolbarLoadState -> setEnabled ( running && ! Achievements :: IsHardcoreModeActive ());
2023-06-30 22:19:09 +10:00
m_ui . actionToolbarSaveState -> setEnabled ( running );
2021-12-13 22:12:54 +10:00
m_ui . actionViewGameProperties -> setEnabled ( running );
2024-07-19 22:42:42 +07:00
if ( ! running && m_ui . actionVideoCapture -> isChecked ())
2023-04-01 13:13:21 +10:00
{
2024-07-19 22:42:42 +07:00
QSignalBlocker sb ( m_ui . actionVideoCapture );
m_ui . actionVideoCapture -> setChecked ( false );
2023-04-01 13:13:21 +10:00
}
2022-12-18 23:05:00 +10:00
2021-12-13 22:12:54 +10:00
m_game_list_widget -> setDisabled ( starting && ! running );
2022-05-06 21:03:16 +10:00
if ( ! starting && ! running )
2023-04-01 13:13:21 +10:00
{
2023-06-30 22:19:09 +10:00
{
QSignalBlocker sb ( m_ui . actionPause );
m_ui . actionPause -> setChecked ( false );
}
{
QSignalBlocker sb ( m_ui . actionToolbarPause );
m_ui . actionToolbarPause -> setChecked ( false );
}
2023-04-01 13:13:21 +10:00
}
2022-05-23 00:12:21 +10:00
// scanning needs to be disabled while running
2024-07-19 06:25:42 -05:00
m_ui . actionScanForNewGames -> setDisabled ( starting_or_running_or_stopping );
m_ui . actionRescanAllGames -> setDisabled ( starting_or_running_or_stopping );
2021-12-13 22:12:54 +10:00
}
2022-09-15 19:43:21 +10:00
void MainWindow :: updateDisplayRelatedActions ( bool has_surface , bool render_to_main , bool fullscreen )
{
// rendering to main, or switched to gamelist/grid
2023-06-24 17:46:36 +10:00
m_ui . actionViewSystemDisplay -> setEnabled (( has_surface && render_to_main ) || ( ! has_surface && MTGS :: IsOpen ()));
2022-09-15 19:43:21 +10:00
m_ui . menuWindowSize -> setEnabled ( has_surface && ! fullscreen );
m_ui . actionFullscreen -> setEnabled ( has_surface );
2023-06-30 22:19:09 +10:00
m_ui . actionToolbarFullscreen -> setEnabled ( has_surface );
2022-09-15 19:43:21 +10:00
{
QSignalBlocker blocker ( m_ui . actionFullscreen );
m_ui . actionFullscreen -> setChecked ( fullscreen );
}
2023-06-30 22:19:09 +10:00
{
QSignalBlocker blocker ( m_ui . actionToolbarFullscreen );
m_ui . actionToolbarFullscreen -> setChecked ( fullscreen );
}
2022-09-15 19:43:21 +10:00
}
2022-04-03 23:46:05 +10:00
void MainWindow :: updateStatusBarWidgetVisibility ()
{
2022-06-28 23:34:45 +10:00
auto Update = [ this ]( QWidget * widget , bool visible , int stretch ) {
2026-04-13 19:45:19 +07:00
if ( widget -> isVisible () == visible )
return ;
2022-04-03 23:46:05 +10:00
if ( widget -> isVisible ())
{
m_ui . statusBar -> removeWidget ( widget );
widget -> hide ();
}
2026-04-13 19:45:19 +07:00
else
2022-04-03 23:46:05 +10:00
{
m_ui . statusBar -> addPermanentWidget ( widget , stretch );
widget -> show ();
}
};
2022-07-03 23:30:03 +10:00
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 );
2026-04-13 19:45:19 +07:00
Update ( m_status_gpu_widget , s_vm_valid , 0 );
2022-06-28 22:53:26 +10:00
Update ( m_status_fps_widget , s_vm_valid , 0 );
2022-07-03 23:30:03 +10:00
Update ( m_status_vps_widget , s_vm_valid , 0 );
2024-09-15 16:03:29 +01:00
Update ( m_status_speed_widget , s_vm_valid , 0 );
2026-04-13 19:45:19 +07:00
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 );
2022-04-03 23:46:05 +10:00
}
2021-12-13 22:12:54 +10:00
void MainWindow :: updateWindowTitle ()
{
2022-05-11 18:24:56 +10:00
QString suffix ( QtHost :: GetAppConfigSuffix ());
QString main_title ( QtHost :: GetAppNameAndVersion () + suffix );
2024-01-11 18:19:27 +10:00
QString display_title ( s_current_title + suffix );
2021-12-13 22:12:54 +10:00
2024-01-11 18:19:27 +10:00
if ( ! s_vm_valid || s_current_title . isEmpty ())
2022-05-06 21:03:16 +10:00
display_title = main_title ;
else if ( isRenderingToMain ())
main_title = display_title ;
if ( windowTitle () != main_title )
setWindowTitle ( main_title );
2025-11-21 22:35:19 +00:00
if ( m_display_surface && ! isRenderingToMain ())
2022-05-06 21:03:16 +10:00
{
2025-11-21 22:35:19 +00:00
#ifdef DISPLAY_SURFACE_WINDOW
if ( m_display_surface -> title () != display_title )
m_display_surface -> setTitle ( display_title );
#else
2025-10-30 20:54:07 +00:00
if ( m_display_container -> windowTitle () != display_title )
m_display_container -> setWindowTitle ( display_title );
2025-11-21 22:35:19 +00:00
#endif
2022-05-06 21:03:16 +10:00
}
2024-01-11 19:38:28 +10:00
if ( g_log_window )
g_log_window -> updateWindowTitle ();
2021-12-13 22:12:54 +10:00
}
2022-06-28 22:16:45 +10:00
void MainWindow :: updateWindowState ( bool force_visible )
2022-06-28 21:37:30 +10:00
{
2022-06-29 12:44:10 +10:00
// 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 ;
2022-07-12 13:11:56 +10:00
const bool hide_window = ! isRenderingToMain () && shouldHideMainWindow ();
2022-10-22 13:43:12 +10:00
const bool disable_resize = Host :: GetBoolSettingValue ( "UI" , "DisableWindowResize" , false );
2025-10-30 20:54:07 +00:00
const bool has_window = s_vm_valid || m_display_surface ;
2022-06-28 21:37:30 +10:00
2022-06-28 22:16:45 +10:00
// 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.
2025-10-30 20:54:07 +00:00
if ( m_display_surface && ! isRenderingToMain ())
2025-11-21 22:35:19 +00:00
{
#ifdef DISPLAY_SURFACE_WINDOW
QtUtils :: SetWindowResizeable ( m_display_surface , resizeable );
#else
2025-10-30 20:54:07 +00:00
QtUtils :: SetWindowResizeable ( m_display_container , resizeable );
2025-11-21 22:35:19 +00:00
#endif
}
2022-06-28 21:37:30 +10:00
}
2021-12-13 22:12:54 +10:00
void MainWindow :: setProgressBar ( int current , int total )
{
2022-07-18 08:47:57 +01:00
const int value = ( total != 0 ) ? (( current * 100 ) / total ) : 0 ;
2022-07-09 20:41:52 +10:00
if ( m_status_progress_widget -> value () != value )
m_status_progress_widget -> setValue ( value );
2021-12-13 22:12:54 +10:00
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 );
}
2022-03-13 00:52:52 +10:00
bool MainWindow :: isShowingGameList () const
{
2025-10-30 20:54:07 +00:00
return ( m_ui . mainContainer -> currentIndex () == 0 );
2022-05-06 21:03:16 +10:00
}
bool MainWindow :: isRenderingFullscreen () const
{
2025-10-30 20:54:07 +00:00
if ( ! MTGS :: IsOpen () || ! m_display_surface )
2022-05-06 21:03:16 +10:00
return false ;
2025-11-21 22:35:19 +00:00
return m_display_surface -> isFullScreen ();
2022-05-06 21:03:16 +10:00
}
bool MainWindow :: isRenderingToMain () const
{
2025-11-21 22:35:19 +00:00
return ( m_display_container && m_ui . mainContainer -> indexOf ( m_display_container ) == 1 );
2022-03-13 00:52:52 +10:00
}
2021-12-13 22:12:54 +10:00
2022-05-15 18:15:27 +10:00
bool MainWindow :: shouldHideMouseCursor () const
{
2023-07-23 15:07:46 +10:00
return (( isRenderingFullscreen () && Host :: GetBoolSettingValue ( "UI" , "HideMouseCursor" , false )) ||
m_relative_mouse_mode || m_hide_mouse_cursor );
2022-05-15 18:15:27 +10:00
}
2022-07-09 19:14:48 +10:00
bool MainWindow :: shouldHideMainWindow () const
{
2022-10-02 01:14:39 +10:00
// NOTE: We can't use isRenderingToMain() here, because this happens post-fullscreen-switch.
2024-07-19 12:26:16 +02:00
return ( Host :: GetBoolSettingValue ( "UI" , "HideMainWindowWhenRunning" , false ) && ! g_emu_thread -> shouldRenderToMain ()) ||
2026-04-15 18:59:22 -07:00
( g_emu_thread -> shouldRenderToMain () && ( isRenderingFullscreen () || g_emu_thread -> isExclusiveFullscreen () || m_is_temporarily_windowed )) ||
2025-06-12 23:16:13 -05:00
Host :: InNoGUIMode ();
2022-07-09 19:14:48 +10:00
}
2025-02-17 10:41:31 -05:00
bool MainWindow :: shouldMouseLock () const
2025-01-26 17:20:59 +01:00
{
if ( ! s_vm_valid || s_vm_paused )
return false ;
2025-02-17 10:41:31 -05:00
if ( ! Host :: GetBoolSettingValue ( "EmuCore" , "EnableMouseLock" , false ))
2025-01-26 17:20:59 +01:00
return false ;
2025-11-23 16:54:50 +00:00
if ( m_display_created == false || m_display_surface == nullptr )
2025-10-18 12:56:35 -04:00
return false ;
2026-06-20 20:27:55 -07:00
const bool windowsHidden =
#ifdef ENABLE_QT_DEBUGGER
( ! g_debugger_window || g_debugger_window -> isHidden ()) &&
#endif
2025-11-21 22:35:19 +00:00
( ! m_controller_settings_window || m_controller_settings_window -> isHidden ()) &&
( ! m_settings_window || m_settings_window -> isHidden ());
2025-01-26 17:20:59 +01:00
2025-11-21 22:35:19 +00:00
if ( ! windowsHidden )
2025-10-18 12:56:35 -04:00
return false ;
2025-11-21 22:35:19 +00:00
#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
2025-01-26 17:20:59 +01:00
}
2023-12-08 01:44:50 -05:00
bool MainWindow :: shouldAbortForMemcardBusy ( const VMLock & lock )
{
if ( MemcardBusy :: IsBusy () && ! GSDumpReplayer :: IsReplayingDump ())
{
2025-10-15 23:57:59 -04:00
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 );
2023-12-17 13:53:47 +10:00
2025-10-15 23:57:59 -04:00
if ( msgbox . exec () != QMessageBox :: Yes )
2023-12-08 01:44:50 -05:00
{
return true ;
}
}
return false ;
}
2021-12-13 22:12:54 +10:00
void MainWindow :: switchToGameListView ()
{
2022-07-09 16:59:13 +10:00
if ( isShowingGameList ())
2022-05-06 21:03:16 +10:00
{
m_game_list_widget -> setFocus ();
2021-12-13 22:12:54 +10:00
return ;
2022-05-06 21:03:16 +10:00
}
2021-12-13 22:12:54 +10:00
2022-05-15 18:20:21 +10:00
if ( m_display_created )
2021-12-13 22:12:54 +10:00
{
2022-06-28 22:53:26 +10:00
m_was_paused_on_surface_loss = s_vm_paused ;
if ( ! s_vm_paused )
2021-12-13 22:12:54 +10:00
g_emu_thread -> setVMPaused ( true );
2022-05-06 21:03:16 +10:00
// switch to surfaceless. we have to wait until the display widget is gone before we swap over.
g_emu_thread -> setSurfaceless ( true );
2025-10-30 20:54:07 +00:00
while ( m_display_surface )
2022-05-06 21:03:16 +10:00
QApplication :: processEvents ( QEventLoop :: ExcludeUserInputEvents , 1 );
2021-12-13 22:12:54 +10:00
}
}
void MainWindow :: switchToEmulationView ()
{
2022-05-15 18:20:21 +10:00
if ( ! m_display_created || ! isShowingGameList ())
2021-12-13 22:12:54 +10:00
return ;
2022-05-06 21:03:16 +10:00
// we're no longer surfaceless! this will call back to UpdateDisplay(), which will swap the widget out.
g_emu_thread -> setSurfaceless ( false );
2021-12-13 22:12:54 +10:00
2022-05-06 21:03:16 +10:00
// resume if we weren't paused at switch time
2022-06-28 22:53:26 +10:00
if ( s_vm_paused && ! m_was_paused_on_surface_loss )
2022-05-06 21:03:16 +10:00
g_emu_thread -> setVMPaused ( false );
2025-10-30 20:54:07 +00:00
if ( m_display_surface )
2025-11-21 22:35:19 +00:00
m_display_surface -> setFocus ();
2021-12-13 22:12:54 +10:00
}
2025-11-06 14:36:16 +00:00
void MainWindow :: refreshGameList ( bool invalidate_cache , bool popup_on_error )
2022-03-13 00:52:52 +10:00
{
2025-11-06 14:36:16 +00:00
m_game_list_widget -> refresh ( invalidate_cache , popup_on_error );
2022-03-13 00:52:52 +10:00
}
2021-12-13 22:12:54 +10:00
2022-05-15 18:16:24 +10:00
void MainWindow :: cancelGameListRefresh ()
{
m_game_list_widget -> cancelRefresh ();
}
2025-12-14 09:08:34 -05:00
void MainWindow :: updateGameListBackground ()
{
if ( m_game_list_widget )
m_game_list_widget -> setCustomBackground ();
}
2025-03-02 23:04:19 +00:00
void MainWindow :: reportInfo ( const QString & title , const QString & message )
{
QMessageBox :: information ( this , title , message );
}
2022-03-13 00:52:52 +10:00
void MainWindow :: reportError ( const QString & title , const QString & message )
{
QMessageBox :: critical ( this , title , message );
}
2021-12-13 22:12:54 +10:00
2022-08-20 20:20:36 +10:00
bool MainWindow :: confirmMessage ( const QString & title , const QString & message )
{
VMLock lock ( pauseAndLockVM ());
return ( QMessageBox :: question ( this , title , message ) == QMessageBox :: Yes );
}
2024-05-15 07:42:19 +10:00
void MainWindow :: onStatusMessage ( const QString & message )
{
m_ui . statusBar -> showMessage ( message );
}
2025-11-30 20:15:57 +00:00
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" ));
2025-11-30 22:24:00 +00:00
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" ));
2025-11-30 20:15:57 +00:00
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 ;
}
2022-04-06 20:49:00 +10:00
void MainWindow :: runOnUIThread ( const std :: function < void () >& func )
{
func ();
}
2023-12-08 01:44:50 -05:00
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 ();
}
2023-01-28 13:33:51 +10:00
bool MainWindow :: requestShutdown ( bool allow_confirm , bool allow_save_to_state , bool default_save_to_state )
2022-03-04 20:40:03 +10:00
{
2022-06-28 22:53:26 +10:00
if ( ! s_vm_valid )
2022-03-04 20:40:03 +10:00
return true ;
2022-06-20 18:03:38 +10:00
// If we don't have a crc, we can't save state.
2024-01-11 18:19:27 +10:00
allow_save_to_state &= ( s_current_disc_crc != 0 );
2022-05-15 18:20:21 +10:00
bool save_state = allow_save_to_state && default_save_to_state ;
2023-12-08 01:44:50 -05:00
VMLock lock ( pauseAndLockVM ());
2022-05-07 22:56:44 +10:00
2023-12-08 01:44:50 -05:00
// Check if memcard is busy, deny request if so.
if ( shouldAbortForMemcardBusy ( lock ))
{
return false ;
}
2023-12-17 13:53:47 +10:00
2022-06-20 18:03:38 +10:00
// Only confirm on UI thread because we need to display a msgbox.
2022-10-22 13:43:12 +10:00
if ( ! m_is_closing && allow_confirm && ! GSDumpReplayer :: IsReplayingDump () && Host :: GetBoolSettingValue ( "UI" , "ConfirmShutdown" , true ))
2022-03-25 20:14:23 +10:00
{
2022-05-07 22:56:44 +10:00
QMessageBox msgbox ( lock . getDialogParent ());
msgbox . setIcon ( QMessageBox :: Question );
msgbox . setWindowTitle ( tr ( "Confirm Shutdown" ));
2024-04-02 14:27:49 +07:00
msgbox . setWindowModality ( Qt :: WindowModal );
msgbox . setWindowIcon ( QtHost :: GetAppIcon ());
2022-10-31 20:05:47 +08:00
msgbox . setText ( tr ( "Are you sure you want to shut down the virtual machine?" ));
2022-05-07 22:56:44 +10:00
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 )
2022-03-25 20:14:23 +10:00
return false ;
2022-05-07 22:56:44 +10:00
save_state = save_cb -> isChecked ();
2022-06-20 18:03:38 +10:00
// Don't switch back to fullscreen when we're shutting down anyway.
lock . cancelResume ();
2022-03-25 20:14:23 +10:00
}
2022-03-04 20:40:03 +10:00
2022-06-28 21:37:30 +10:00
// 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.
2025-06-09 19:00:05 +07:00
if ( ! isRenderingToMain () && isHidden () && ! Host :: InBatchMode () && ! g_emu_thread -> isRunningFullscreenUI ())
2022-06-28 22:16:45 +10:00
updateWindowState ( true );
2022-06-28 21:37:30 +10:00
2023-03-03 21:30:40 +10:00
// 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 );
}
2022-06-28 21:37:30 +10:00
// Now we can actually shut down the VM.
2022-05-07 22:56:44 +10:00
g_emu_thread -> shutdownVM ( save_state );
2022-03-25 20:14:23 +10:00
return true ;
2022-03-04 20:40:03 +10:00
}
2023-01-28 13:33:51 +10:00
void MainWindow :: requestExit ( bool allow_confirm )
2022-03-04 20:40:03 +10:00
{
2023-04-02 19:26:25 +10:00
// requestShutdown() clears this flag.
const bool vm_was_valid = QtHost :: IsVMValid ();
2022-03-25 20:14:23 +10:00
// this is block, because otherwise closeEvent() will also prompt
2023-01-28 13:33:51 +10:00
if ( ! requestShutdown ( allow_confirm , true , EmuConfig . SaveStateOnShutdown ))
2022-03-04 20:40:03 +10:00
return ;
2023-01-28 13:33:51 +10:00
// 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.
2023-04-02 19:26:25 +10:00
if ( vm_was_valid )
2023-01-28 13:33:51 +10:00
m_is_closing = true ;
else
2024-03-22 22:01:42 +10:00
quit ();
2022-03-04 20:40:03 +10:00
}
2022-05-15 18:15:27 +10:00
void MainWindow :: checkForSettingChanges ()
{
2025-10-30 20:54:07 +00:00
if ( m_display_surface )
2022-12-05 01:00:06 +10:00
updateDisplayWidgetCursor ();
2022-06-28 21:37:30 +10:00
2022-06-28 22:16:45 +10:00
updateWindowState ();
2024-01-11 19:38:28 +10:00
LogWindow :: updateSettings ();
2022-05-15 18:15:27 +10:00
}
2022-10-15 19:03:52 +10:00
std :: optional < WindowInfo > MainWindow :: getWindowInfo ()
{
2025-10-30 20:54:07 +00:00
if ( ! m_display_surface || isRenderingToMain ())
return QtUtils :: GetWindowInfoForWindow ( this );
else if ( m_display_surface )
return QtUtils :: GetWindowInfoForWindow ( m_display_surface );
2022-12-30 14:51:40 +10:00
else
return std :: nullopt ;
2022-10-15 19:03:52 +10:00
}
2021-12-13 22:12:54 +10:00
void MainWindow :: onGameListRefreshProgress ( const QString & status , int current , int total )
{
m_ui . statusBar -> showMessage ( status );
setProgressBar ( current , total );
}
2022-03-13 00:52:52 +10:00
void MainWindow :: onGameListRefreshComplete ()
{
2023-01-31 21:54:29 +01:00
m_ui . statusBar -> clearMessage ();
2022-03-13 00:52:52 +10:00
clearProgressBar ();
}
2021-12-13 22:12:54 +10:00
void MainWindow :: onGameListSelectionChanged ()
{
2025-11-05 03:39:53 +00:00
const std :: optional < GameList :: Entry > entry = m_game_list_widget -> getSelectedEntry ();
if ( ! entry . has_value ())
2021-12-13 22:12:54 +10:00
return ;
m_ui . statusBar -> showMessage ( QString :: fromStdString ( entry -> path ));
}
void MainWindow :: onGameListEntryActivated ()
{
2025-11-05 03:39:53 +00:00
const std :: optional < GameList :: Entry > entry = m_game_list_widget -> getSelectedEntry ();
if ( ! entry . has_value ())
2021-12-13 22:12:54 +10:00
return ;
2022-06-28 22:53:26 +10:00
if ( s_vm_valid )
2021-12-13 22:12:54 +10:00
{
// change disc on double click
2022-06-28 23:34:45 +10:00
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 ));
2021-12-13 22:12:54 +10:00
return ;
}
2022-05-08 16:45:38 +10:00
// we might still be saving a resume state...
VMManager :: WaitForSaveStateFlush ();
2022-12-04 18:03:30 +10:00
const std :: optional < bool > resume =
promptForResumeState ( QString :: fromStdString ( VMManager :: GetSaveStateFileName ( entry -> serial . c_str (), entry -> crc , - 1 )));
2022-05-07 22:56:44 +10:00
if ( ! resume . has_value ())
{
// cancelled
return ;
}
2021-12-13 22:12:54 +10:00
// only resume if the option is enabled, and we have one for this game
2025-11-05 03:39:53 +00:00
startGameListEntry ( * entry , resume . value () ? std :: optional < s32 > ( - 1 ) : std :: optional < s32 > (), std :: nullopt );
2021-12-13 22:12:54 +10:00
}
void MainWindow :: onGameListEntryContextMenuRequested ( const QPoint & point )
{
2025-11-05 03:39:53 +00:00
const std :: optional < GameList :: Entry > entry = m_game_list_widget -> getSelectedEntry ();
2021-12-13 22:12:54 +10:00
QMenu menu ;
2025-11-05 03:39:53 +00:00
if ( entry . has_value ())
2021-12-13 22:12:54 +10:00
{
QAction * action = menu . addAction ( tr ( "Properties..." ));
2022-12-04 18:03:30 +10:00
action -> setEnabled ( ! entry -> serial . empty () || entry -> type == GameList :: EntryType :: ELF );
2022-02-16 00:29:18 +10:00
if ( action -> isEnabled ())
2022-12-04 18:03:30 +10:00
{
connect ( action , & QAction :: triggered , [ entry ]() {
2025-11-05 03:39:53 +00:00
SettingsWindow :: openGamePropertiesDialog ( &* entry ,
2025-02-26 20:25:55 +00:00
entry -> title , entry -> serial , entry -> crc , entry -> type == GameList :: EntryType :: ELF , nullptr );
2022-12-04 18:03:30 +10:00
});
}
2021-12-13 22:12:54 +10:00
2023-09-09 02:15:41 -05:00
action = menu . addAction ( QtUtils :: GetShowInFileExplorerMessage ());
2021-12-13 22:12:54 +10:00
connect ( action , & QAction :: triggered , [ this , entry ]() {
const QFileInfo fi ( QString :: fromStdString ( entry -> path ));
2023-09-09 02:15:41 -05:00
QtUtils :: ShowInFileExplorer ( this , fi );
2021-12-13 22:12:54 +10:00
});
action = menu . addAction ( tr ( "Set Cover Image..." ));
2025-11-05 03:39:53 +00:00
connect ( action , & QAction :: triggered , [ this , entry ]() { setGameListEntryCoverImage ( * entry ); });
2021-12-13 22:12:54 +10:00
2025-05-29 19:25:09 +07:00
#if !defined(__APPLE__)
connect ( menu . addAction ( tr ( "Create Game Shortcut..." )), & QAction :: triggered , [ this ]() { MainWindow :: onCreateGameShortcutTriggered (); });
#endif
2021-12-13 22:12:54 +10:00
connect ( menu . addAction ( tr ( "Exclude From List" )), & QAction :: triggered ,
2023-10-14 18:45:09 +10:00
[ this , entry ]() { getSettingsWindow () -> getGameListSettingsWidget () -> addExcludedPath ( entry -> path ); });
2021-12-13 22:12:54 +10:00
2025-10-20 18:17:20 -05:00
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 )
2025-11-05 03:39:53 +00:00
connect ( menu . addAction ( tr ( "Reset Play Time" )), & QAction :: triggered , [ this , entry , entry_played_time ]() { clearGameListEntryPlayTime ( * entry , entry_played_time ); });
2022-11-23 01:03:36 +10:00
2025-10-20 18:17:20 -05:00
// Check Wiki Page functionality is based on a serial redirect.
2024-07-19 17:52:46 -05:00
if ( ! entry -> serial . empty ())
2025-11-05 03:39:53 +00:00
connect ( menu . addAction ( tr ( "Check Wiki Page" )), & QAction :: triggered , [ this , entry ]() { goToWikiPage ( * entry ); });
2024-08-23 00:09:46 +07:00
2026-03-23 00:45:25 +07:00
menu . addSeparator ();
action = menu . addAction ( tr ( "Open Memory Card Folder" ));
connect ( action , & QAction :: triggered , [ this , entry ]() { openMemoryCardFolder (); });
2025-10-25 20:43:27 -05:00
action = menu . addAction ( tr ( "Open Snapshots Folder" ));
2025-11-05 03:39:53 +00:00
connect ( action , & QAction :: triggered , [ this , entry ]() { openSnapshotsFolderForGame ( * entry ); });
2026-03-23 00:45:25 +07:00
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 ); });
2021-12-13 22:12:54 +10:00
menu . addSeparator ();
2022-06-28 22:53:26 +10:00
if ( ! s_vm_valid )
2021-12-13 22:12:54 +10:00
{
action = menu . addAction ( tr ( "Default Boot" ));
2025-11-05 03:39:53 +00:00
connect ( action , & QAction :: triggered , [ this , entry ]() { startGameListEntry ( * entry ); });
2021-12-13 22:12:54 +10:00
// Make bold to indicate it's the default choice when double-clicking
2022-05-07 22:56:44 +10:00
if ( ! VMManager :: HasSaveStateInSlot ( entry -> serial . c_str (), entry -> crc , - 1 ))
2021-12-13 22:12:54 +10:00
QtUtils :: MarkActionAsDefault ( action );
action = menu . addAction ( tr ( "Fast Boot" ));
2025-11-05 03:39:53 +00:00
connect ( action , & QAction :: triggered , [ this , entry ]() { startGameListEntry ( * entry , std :: nullopt , true ); });
2021-12-13 22:12:54 +10:00
action = menu . addAction ( tr ( "Full Boot" ));
2025-11-05 03:39:53 +00:00
connect ( action , & QAction :: triggered , [ this , entry ]() { startGameListEntry ( * entry , std :: nullopt , false ); });
2021-12-13 22:12:54 +10:00
2026-06-20 20:27:55 -07:00
#ifdef ENABLE_QT_DEBUGGER
2021-12-13 22:12:54 +10:00
if ( m_ui . menuDebug -> menuAction () -> isVisible ())
{
action = menu . addAction ( tr ( "Boot and Debug" ));
2023-04-25 20:24:22 +10:00
connect ( action , & QAction :: triggered , [ this , entry ]() {
DebugInterface :: setPauseOnEntry ( true );
2025-11-05 03:39:53 +00:00
startGameListEntry ( * entry );
2025-01-27 12:49:39 +00:00
DebuggerWindow :: getInstance () -> show ();
2023-04-25 20:24:22 +10:00
});
2021-12-13 22:12:54 +10:00
}
2026-06-20 20:27:55 -07:00
#endif
2021-12-13 22:12:54 +10:00
menu . addSeparator ();
2022-02-16 00:29:18 +10:00
populateLoadStateMenu ( & menu , QString :: fromStdString ( entry -> path ), QString :: fromStdString ( entry -> serial ), entry -> crc );
2021-12-13 22:12:54 +10:00
}
2022-06-28 23:34:45 +10:00
else if ( entry -> IsDisc ())
2021-12-13 22:12:54 +10:00
{
action = menu . addAction ( tr ( "Change Disc" ));
connect ( action , & QAction :: triggered , [ this , entry ]() {
2022-06-28 23:34:45 +10:00
g_emu_thread -> changeDisc ( CDVD_SourceType :: Iso , QString :: fromStdString ( entry -> path ));
2021-12-13 22:12:54 +10:00
switchToEmulationView ();
});
QtUtils :: MarkActionAsDefault ( action );
}
menu . addSeparator ();
}
connect ( menu . addAction ( tr ( "Add Search Directory..." )), & QAction :: triggered ,
2023-10-14 18:45:09 +10:00
[ this ]() { getSettingsWindow () -> getGameListSettingsWidget () -> addSearchDirectory ( this ); });
2021-12-13 22:12:54 +10:00
menu . exec ( point );
}
void MainWindow :: onStartFileActionTriggered ()
{
2022-12-04 18:03:30 +10:00
const QString path (
QDir :: toNativeSeparators ( QFileDialog :: getOpenFileName ( this , tr ( "Start File" ), QString (), tr ( OPEN_FILE_FILTER ), nullptr )));
2022-05-26 17:49:40 +10:00
if ( path . isEmpty ())
2021-12-13 22:12:54 +10:00
return ;
2022-06-28 23:34:45 +10:00
doStartFile ( std :: nullopt , path );
}
void MainWindow :: onStartDiscActionTriggered ()
{
QString path ( getDiscDevicePath ( tr ( "Start Disc" )));
if ( path . isEmpty ())
return ;
doStartFile ( CDVD_SourceType :: Disc , path );
2021-12-13 22:12:54 +10:00
}
void MainWindow :: onStartBIOSActionTriggered ()
{
std :: shared_ptr < VMBootParameters > params = std :: make_shared < VMBootParameters > ();
g_emu_thread -> startVM ( std :: move ( params ));
}
void MainWindow :: onChangeDiscFromFileActionTriggered ()
{
2022-05-07 13:52:13 +10:00
VMLock lock ( pauseAndLockVM ());
2024-04-04 16:45:06 +10:00
QString filename = QDir :: toNativeSeparators (
QFileDialog :: getOpenFileName ( lock . getDialogParent (), tr ( "Select Disc Image" ), QString (), tr ( DISC_IMAGE_FILTER ), nullptr ));
2021-12-13 22:12:54 +10:00
if ( filename . isEmpty ())
return ;
2022-06-28 23:34:45 +10:00
g_emu_thread -> changeDisc ( CDVD_SourceType :: Iso , filename );
2021-12-13 22:12:54 +10:00
}
2022-03-13 00:52:52 +10:00
void MainWindow :: onChangeDiscFromGameListActionTriggered ()
{
2022-05-06 23:34:07 +10:00
m_was_disc_change_request = true ;
2022-03-13 00:52:52 +10:00
switchToGameListView ();
}
2021-12-13 22:12:54 +10:00
void MainWindow :: onChangeDiscFromDeviceActionTriggered ()
{
2022-06-28 23:34:45 +10:00
QString path ( getDiscDevicePath ( tr ( "Change Disc" )));
if ( path . isEmpty ())
return ;
g_emu_thread -> changeDisc ( CDVD_SourceType :: Disc , path );
2021-12-13 22:12:54 +10:00
}
2022-06-29 00:00:51 +10:00
void MainWindow :: onRemoveDiscActionTriggered ()
{
g_emu_thread -> changeDisc ( CDVD_SourceType :: NoDisc , QString ());
}
2021-12-13 22:12:54 +10:00
void MainWindow :: onChangeDiscMenuAboutToShow ()
{
// TODO: This is where we would populate the playlist if there is one.
}
2022-12-04 18:03:30 +10:00
void MainWindow :: onChangeDiscMenuAboutToHide ()
{
}
2021-12-13 22:12:54 +10:00
void MainWindow :: onLoadStateMenuAboutToShow ()
{
2023-01-27 18:31:30 +01:00
m_ui . menuLoadState -> clear ();
2024-01-11 18:19:27 +10:00
populateLoadStateMenu ( m_ui . menuLoadState , s_current_disc_path , s_current_disc_serial , s_current_disc_crc );
2021-12-13 22:12:54 +10:00
}
void MainWindow :: onSaveStateMenuAboutToShow ()
{
2023-01-27 18:31:30 +01:00
m_ui . menuSaveState -> clear ();
2024-01-11 18:19:27 +10:00
populateSaveStateMenu ( m_ui . menuSaveState , s_current_disc_serial , s_current_disc_crc );
2021-12-13 22:12:54 +10:00
}
2023-09-04 21:34:57 +10:00
void MainWindow :: onStartFullscreenUITriggered ()
{
2025-10-30 20:54:07 +00:00
if ( m_display_surface )
2023-09-04 21:34:57 +10:00
g_emu_thread -> stopFullscreenUI ();
else
g_emu_thread -> startFullscreenUI ( Host :: GetBaseBoolSettingValue ( "UI" , "StartFullscreen" , false ));
}
void MainWindow :: onFullscreenUIStateChange ( bool running )
{
2026-04-03 15:32:28 +05:30
m_ui . actionStartFullscreenUI -> setText ( running ? tr ( "Stop Big Picture Mode" ) : tr ( "Start Big Picture &Mode" ));
2023-09-04 21:34:57 +10:00
m_ui . actionToolbarStartFullscreenUI -> setText ( running ? tr ( "Exit Big Picture" , "In Toolbar" ) : tr ( "Big Picture" , "In Toolbar" ));
}
2021-12-13 22:12:54 +10:00
void MainWindow :: onViewToolbarActionToggled ( bool checked )
{
2022-09-07 17:44:10 +10:00
Host :: SetBaseBoolSettingValue ( "UI" , "ShowToolbar" , checked );
Host :: CommitBaseSettingChanges ();
2021-12-13 22:12:54 +10:00
m_ui . toolBar -> setVisible ( checked );
}
void MainWindow :: onViewLockToolbarActionToggled ( bool checked )
{
2022-09-07 17:44:10 +10:00
Host :: SetBaseBoolSettingValue ( "UI" , "LockToolbar" , checked );
Host :: CommitBaseSettingChanges ();
2021-12-13 22:12:54 +10:00
m_ui . toolBar -> setMovable ( ! checked );
}
void MainWindow :: onViewStatusBarActionToggled ( bool checked )
{
2022-09-07 17:44:10 +10:00
Host :: SetBaseBoolSettingValue ( "UI" , "ShowStatusBar" , checked );
Host :: CommitBaseSettingChanges ();
2021-12-13 22:12:54 +10:00
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 ()
{
2022-05-15 18:20:21 +10:00
if ( m_display_created )
2021-12-13 22:12:54 +10:00
switchToEmulationView ();
}
void MainWindow :: onViewGamePropertiesActionTriggered ()
{
2025-02-26 20:25:55 +00:00
doGameSettings ( nullptr );
2021-12-13 22:12:54 +10:00
}
2022-03-13 00:52:52 +10:00
void MainWindow :: onGitHubRepositoryActionTriggered ()
{
QtUtils :: OpenURL ( this , AboutDialog :: getGitHubRepositoryUrl ());
}
2021-12-13 22:12:54 +10:00
2026-07-23 19:48:13 -07:00
void MainWindow :: onWebsiteActionTriggered ()
2025-01-23 22:45:19 -06:00
{
2026-07-23 19:48:13 -07:00
QtUtils :: OpenURL ( this , AboutDialog :: getWebsiteUrl ());
2025-01-23 22:45:19 -06:00
}
2021-12-13 22:12:54 +10:00
void MainWindow :: onAboutActionTriggered ()
{
AboutDialog about ( this );
about . exec ();
}
2023-01-27 01:25:37 +10:00
void MainWindow :: checkForUpdates ( bool display_message , bool force_check )
2022-04-04 21:27:23 +10:00
{
if ( ! AutoUpdaterDialog :: isSupported ())
{
if ( display_message )
{
QMessageBox mbox ( this );
mbox . setWindowTitle ( tr ( "Updater Error" ));
2024-04-02 14:27:49 +07:00
mbox . setWindowIcon ( QtHost :: GetAppIcon ());
2022-04-04 21:27:23 +10:00
mbox . setTextFormat ( Qt :: RichText );
QString message ;
#ifdef _WIN32
2026-07-23 19:48:13 -07:00
message = tr ( "<p>Sorry, you are trying to update an ARMSX2 version which is not an official GitHub release. To "
2022-12-04 18:03:30 +10:00
"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>"
2026-07-23 19:48:13 -07:00
"<p><a href= \" https://armsx2.net/ \" >https://armsx2.net/</a></p>" );
2022-04-04 21:27:23 +10:00
#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 ;
2023-01-27 01:25:37 +10:00
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 ();
}
2022-04-04 21:27:23 +10:00
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 ()
{
2022-05-24 22:37:44 +10:00
if ( ! Host :: GetBaseBoolSettingValue ( "AutoUpdater" , "CheckAtStartup" , true ))
2022-04-04 21:27:23 +10:00
return ;
2023-01-27 01:25:37 +10:00
checkForUpdates ( false , false );
2022-04-04 21:27:23 +10:00
}
2021-12-13 22:12:54 +10:00
void MainWindow :: onToolsOpenDataDirectoryTriggered ()
{
2022-05-20 00:46:33 +10:00
const QString path ( QString :: fromStdString ( EmuFolders :: DataRoot ));
2021-12-13 22:12:54 +10:00
QtUtils :: OpenURL ( this , QUrl :: fromLocalFile ( path ));
}
2022-09-03 21:29:02 +10:00
void MainWindow :: onToolsCoverDownloaderTriggered ()
{
2023-10-02 14:57:20 +10:00
// This can be invoked via big picture, so exit fullscreen.
VMLock lock ( pauseAndLockVM ());
2025-10-26 10:57:14 -05:00
CoverDownloadDialog dlg ( this );
2022-09-03 21:29:02 +10:00
connect ( & dlg , & CoverDownloadDialog :: coverRefreshRequested , m_game_list_widget , & GameListWidget :: refreshGridCovers );
dlg . exec ();
}
2025-05-29 19:25:09 +07:00
#if !defined(__APPLE__)
void MainWindow :: onCreateGameShortcutTriggered ()
{
2025-11-05 03:39:53 +00:00
const std :: optional < GameList :: Entry > entry = m_game_list_widget -> getSelectedEntry ();
if ( ! entry . has_value ())
return ;
2025-05-29 19:25:09 +07:00
const QString title = QString :: fromStdString ( entry -> GetTitle ());
const QString path = QString :: fromStdString ( entry -> path );
2025-12-07 11:21:11 +07:00
ShortcutCreationDialog dlg ( this , title , path );
2025-05-29 19:25:09 +07:00
dlg . exec ();
}
#endif
2023-09-09 14:47:26 +10:00
void MainWindow :: onToolsEditCheatsPatchesTriggered ( bool cheats )
{
2024-01-11 18:19:27 +10:00
if ( s_current_disc_serial . isEmpty () || s_current_running_crc == 0 )
2023-09-09 14:47:26 +10:00
return ;
2024-01-11 18:19:27 +10:00
const std :: string path = Patch :: GetPnachFilename ( s_current_disc_serial . toStdString (), s_current_running_crc , cheats );
2023-09-09 14:47:26 +10:00
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 )));
}
2024-01-18 21:10:32 +10:00
void MainWindow :: onCreateMemoryCardOpenRequested ()
{
// This can be invoked via big picture, so exit fullscreen.
VMLock lock ( pauseAndLockVM ());
MemoryCardCreateDialog dlg ( lock . getDialogParent ());
dlg . exec ();
}
2022-07-24 23:52:22 +10:00
void MainWindow :: updateTheme ()
2021-12-13 22:12:54 +10:00
{
2023-04-25 20:24:22 +10:00
QtHost :: UpdateApplicationTheme ();
2023-09-09 15:55:38 +10:00
reloadThemeSpecificImages ();
}
void MainWindow :: reloadThemeSpecificImages ()
{
2023-09-09 13:30:53 +10:00
m_game_list_widget -> reloadThemeSpecificImages ();
2021-12-13 22:12:54 +10:00
}
2023-06-19 22:03:10 +10:00
void MainWindow :: updateLanguage ()
{
2024-01-07 15:27:39 +10:00
// Remove the settings window, so it doesn't mess with any popups that happen (e.g. font download).
destroySubWindows ();
QtHost :: InstallTranslator ( this );
2023-06-19 22:03:10 +10:00
recreate ();
}
2024-06-23 06:40:25 +10:00
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" );
});
}
2022-04-03 20:06:59 -04:00
void MainWindow :: onInputRecNewActionTriggered ()
{
2022-06-28 22:53:26 +10:00
const bool wasPaused = s_vm_paused ;
const bool wasRunning = s_vm_valid ;
2022-04-03 20:06:59 -04:00
if ( wasRunning && ! wasPaused )
{
2022-06-23 17:29:45 -04:00
g_emu_thread -> setVMPaused ( true );
2022-04-03 20:06:59 -04:00
}
NewInputRecordingDlg dlg ( this );
const auto result = dlg . exec ();
if ( result == QDialog :: Accepted )
{
2022-12-04 18:03:30 +10:00
Host :: RunOnCPUThread (
2025-09-22 22:33:14 +01:00
[ filePath = dlg . getFilePath (), fromSavestate = dlg . getInputRecType () == InputRecording :: Type :: FROM_SAVESTATE ,
2022-12-04 18:03:30 +10:00
authorName = dlg . getAuthorName ()]() {
if ( g_InputRecording . create ( filePath , fromSavestate , authorName ))
{
2025-09-22 22:33:14 +01:00
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 ());
2022-12-04 18:03:30 +10:00
});
}
2024-06-07 13:00:35 +01:00
else
{
Host :: ReportErrorAsync (
TRANSLATE_SV ( "MainWindow" , "Input Recording Failed" ),
fmt :: format ( TRANSLATE_FS ( "MainWindow" , "Failed to create file: {}" ), filePath ));
}
2022-12-04 18:03:30 +10:00
});
2022-04-03 20:06:59 -04:00
}
if ( wasRunning && ! wasPaused )
{
2022-06-23 17:29:45 -04:00
g_emu_thread -> setVMPaused ( false );
2022-04-03 20:06:59 -04:00
}
}
void MainWindow :: onInputRecPlayActionTriggered ()
{
2022-06-28 22:53:26 +10:00
const bool wasPaused = s_vm_paused ;
2022-04-03 20:06:59 -04:00
if ( ! wasPaused )
2022-06-15 20:44:14 -04:00
{
2022-06-23 17:29:45 -04:00
g_emu_thread -> setVMPaused ( true );
2022-06-15 20:44:14 -04:00
}
2022-04-03 20:06:59 -04:00
QFileDialog dialog ( this );
dialog . setFileMode ( QFileDialog :: ExistingFile );
2025-12-09 09:05:03 +07:00
dialog . setWindowTitle ( tr ( "Select a File" ));
2022-04-03 20:06:59 -04:00
dialog . setNameFilter ( tr ( "Input Recording Files (*.p2m2)" ));
QStringList fileNames ;
if ( dialog . exec ())
{
fileNames = dialog . selectedFiles ();
}
2022-06-15 20:44:14 -04:00
else
2022-04-03 20:06:59 -04:00
{
2022-06-15 20:44:14 -04:00
if ( ! wasPaused )
2022-04-03 20:06:59 -04:00
{
2022-06-23 17:29:45 -04:00
g_emu_thread -> setVMPaused ( false );
2022-04-03 20:06:59 -04:00
return ;
}
}
2022-06-15 20:44:14 -04:00
if ( fileNames . length () > 0 )
2022-04-03 20:06:59 -04:00
{
2022-06-15 20:44:14 -04:00
if ( g_InputRecording . isActive ())
{
2022-12-04 18:03:30 +10:00
Host :: RunOnCPUThread ([]() { g_InputRecording . stop (); });
2022-06-15 20:44:14 -04:00
m_ui . actionInputRecStop -> setEnabled ( false );
}
2025-09-22 22:33:14 +01:00
Host :: RunOnCPUThread ([ filename = QDir :: toNativeSeparators ( fileNames . first ()). toStdString ()]() {
2022-06-23 17:29:45 -04:00
if ( g_InputRecording . play ( filename ))
{
2025-09-22 22:33:14 +01:00
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 ());
2022-06-23 17:29:45 -04:00
});
}
2024-06-07 13:00:35 +01:00
else
{
Host :: ReportErrorAsync (
TRANSLATE_SV ( "MainWindow" , "Input Playback Failed" ),
fmt :: format ( TRANSLATE_FS ( "MainWindow" , "Failed to open file: {}" ), filename ));
}
2022-06-23 17:29:45 -04:00
});
2022-04-03 20:06:59 -04:00
}
}
void MainWindow :: onInputRecStopActionTriggered ()
{
2022-06-15 20:44:14 -04:00
if ( g_InputRecording . isActive ())
2022-04-03 20:06:59 -04:00
{
2025-09-22 22:33:14 +01:00
Host :: RunOnCPUThread ([]() {
2022-06-23 17:29:45 -04:00
g_InputRecording . stop ();
2025-09-22 22:33:14 +01:00
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 );
2022-06-23 17:29:45 -04:00
});
});
2022-04-03 20:06:59 -04:00
}
}
2022-06-15 20:44:14 -04:00
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 ();
}
}
2021-12-13 22:12:54 +10:00
void MainWindow :: onVMStarting ()
{
2022-06-28 22:53:26 +10:00
s_vm_valid = true ;
2023-03-03 21:30:40 +10:00
updateEmulationActions ( true , false , false );
2021-12-13 22:12:54 +10:00
updateWindowTitle ();
}
void MainWindow :: onVMStarted ()
{
2022-06-28 22:53:26 +10:00
s_vm_valid = true ;
2022-05-06 23:34:07 +10:00
m_was_disc_change_request = false ;
2023-03-03 21:30:40 +10:00
updateEmulationActions ( true , true , false );
2023-09-09 14:47:26 +10:00
updateGameDependentActions ();
2021-12-13 22:12:54 +10:00
updateWindowTitle ();
2022-04-03 23:46:05 +10:00
updateStatusBarWidgetVisibility ();
2022-06-15 20:44:14 -04:00
updateInputRecordingActions ( true );
2025-02-17 21:01:15 -05:00
if ( s_record_on_start )
{
m_ui . actionVideoCapture -> setChecked ( true );
s_record_on_start = false ;
}
2021-12-13 22:12:54 +10:00
}
void MainWindow :: onVMPaused ()
{
// update UI
{
QSignalBlocker sb ( m_ui . actionPause );
m_ui . actionPause -> setChecked ( true );
}
2023-06-30 22:19:09 +10:00
{
QSignalBlocker sb ( m_ui . actionToolbarPause );
m_ui . actionToolbarPause -> setChecked ( true );
}
2021-12-13 22:12:54 +10:00
2022-06-28 22:53:26 +10:00
s_vm_paused = true ;
2021-12-13 22:12:54 +10:00
updateWindowTitle ();
2022-04-03 23:46:05 +10:00
updateStatusBarWidgetVisibility ();
2022-07-03 23:30:03 +10:00
m_last_fps_status = m_status_verbose_widget -> text ();
m_status_verbose_widget -> setText ( tr ( "Paused" ));
2025-10-30 20:54:07 +00:00
if ( m_display_surface )
2022-12-05 01:00:06 +10:00
updateDisplayWidgetCursor ();
2021-12-13 22:12:54 +10:00
}
void MainWindow :: onVMResumed ()
{
// update UI
{
QSignalBlocker sb ( m_ui . actionPause );
m_ui . actionPause -> setChecked ( false );
}
2023-06-30 22:19:09 +10:00
{
QSignalBlocker sb ( m_ui . actionToolbarPause );
m_ui . actionToolbarPause -> setChecked ( false );
}
2021-12-13 22:12:54 +10:00
2022-06-28 22:53:26 +10:00
s_vm_paused = false ;
2022-05-06 23:34:07 +10:00
m_was_disc_change_request = false ;
2021-12-13 22:12:54 +10:00
updateWindowTitle ();
2022-04-03 23:46:05 +10:00
updateStatusBarWidgetVisibility ();
2022-07-03 23:30:03 +10:00
m_status_verbose_widget -> setText ( m_last_fps_status );
m_last_fps_status = QString ();
2025-10-30 20:54:07 +00:00
if ( m_display_surface )
2022-06-20 18:01:40 +10:00
{
2022-12-05 01:00:06 +10:00
updateDisplayWidgetCursor ();
2025-11-21 22:35:19 +00:00
m_display_surface -> setFocus ();
2022-06-20 18:01:40 +10:00
}
2021-12-13 22:12:54 +10:00
}
void MainWindow :: onVMStopped ()
{
2022-06-28 22:53:26 +10:00
s_vm_valid = false ;
s_vm_paused = false ;
2024-04-01 00:44:57 +10:00
const QString empty_string ;
m_last_fps_status = empty_string ;
m_status_renderer_widget -> setText ( empty_string );
m_status_resolution_widget -> setText ( empty_string );
2026-04-13 19:45:19 +07:00
m_status_volume_widget -> setText ( empty_string );
m_status_gpu_widget -> setText ( empty_string );
2024-04-01 00:44:57 +10:00
m_status_fps_widget -> setText ( empty_string );
m_status_vps_widget -> setText ( empty_string );
2024-09-15 16:03:29 +01:00
m_status_speed_widget -> setText ( empty_string );
2024-10-07 22:16:38 +01:00
m_status_verbose_widget -> setText ( empty_string );
2024-04-01 00:44:57 +10:00
2023-03-03 21:30:40 +10:00
updateEmulationActions ( false , false , false );
2023-09-09 14:47:26 +10:00
updateGameDependentActions ();
2021-12-13 22:12:54 +10:00
updateWindowTitle ();
2022-06-28 22:16:45 +10:00
updateWindowState ();
2022-04-03 23:46:05 +10:00
updateStatusBarWidgetVisibility ();
2022-06-15 20:44:14 -04:00
updateInputRecordingActions ( false );
2022-05-15 18:15:27 +10:00
2023-01-28 13:33:51 +10:00
// If we're closing or in batch mode, quit the whole application now.
2025-06-09 19:00:05 +07:00
if ( m_is_closing || Host :: InBatchMode ())
2023-01-28 13:33:51 +10:00
{
2024-03-22 22:01:42 +10:00
quit ();
2023-01-28 13:33:51 +10:00
return ;
}
2025-10-30 20:54:07 +00:00
if ( m_display_surface )
2022-12-05 01:00:06 +10:00
updateDisplayWidgetCursor ();
2022-05-15 18:15:27 +10:00
else
switchToGameListView ();
2022-10-22 14:52:01 +10:00
// reload played time
if ( m_game_list_widget -> isShowingGameList ())
2025-11-06 14:36:16 +00:00
m_game_list_widget -> refresh ( false , false );
2021-12-13 22:12:54 +10:00
}
2023-06-13 22:43:11 +10:00
void MainWindow :: onGameChanged ( const QString & title , const QString & elf_override , const QString & disc_path ,
const QString & serial , quint32 disc_crc , quint32 crc )
2021-12-13 22:12:54 +10:00
{
2024-01-11 18:19:27 +10:00
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 ;
2021-12-13 22:12:54 +10:00
updateWindowTitle ();
2023-09-09 14:47:26 +10:00
updateGameDependentActions ();
2021-12-13 22:12:54 +10:00
}
2022-06-05 02:13:05 +10:00
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 ();
2022-04-18 23:35:14 +10:00
#ifdef ENABLE_RAINTEGRATION
if ( Achievements :: IsUsingRAIntegration ())
Achievements :: RAIntegration :: MainWindowChanged (( void * ) winId ());
#endif
2022-06-05 02:13:05 +10:00
}
2021-12-13 22:12:54 +10:00
void MainWindow :: closeEvent ( QCloseEvent * event )
{
2023-02-04 18:35:05 +10:00
// If there's no VM, we can just exit as normal.
2023-12-03 17:37:27 +10:00
if ( ! s_vm_valid || ! m_display_created )
2022-03-04 20:40:03 +10:00
{
2024-03-31 20:39:16 +10:00
m_is_closing = true ;
2023-02-18 13:19:25 +10:00
saveStateToConfig ();
2023-12-03 17:37:27 +10:00
if ( m_display_created )
2023-09-04 21:18:37 +10:00
g_emu_thread -> stopFullscreenUI ();
2024-03-23 02:25:39 +10:00
destroySubWindows ();
QMainWindow :: closeEvent ( event );
2022-03-04 20:40:03 +10:00
return ;
}
2023-02-04 18:35:05 +10:00
// 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.
2022-06-29 12:44:10 +10:00
m_is_closing = true ;
2021-12-13 22:12:54 +10:00
}
2023-09-09 15:55:38 +10:00
void MainWindow :: changeEvent ( QEvent * event )
{
QMainWindow :: changeEvent ( event );
if ( event -> type () == QEvent :: StyleChange )
{
QtHost :: SetIconThemeFromStyle ();
reloadThemeSpecificImages ();
}
2025-03-23 22:26:52 +07:00
if ( event -> type () == QEvent :: ActivationChange )
m_game_list_widget -> updateCustomBackgroundState ();
2023-09-09 15:55:38 +10:00
}
2022-05-26 17:49:40 +10:00
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 )
2023-03-03 21:31:10 +10:00
filename = QDir :: toNativeSeparators ( urls . front (). toLocalFile ());
2022-05-26 17:49:40 +10:00
}
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 )
2025-06-10 19:32:13 -05:00
{
if ( startFile ( getFilenameFromMimeData ( event -> mimeData ())))
event -> acceptProposedAction ();
}
bool MainWindow :: startFile ( const QString & filename )
2022-05-26 17:49:40 +10:00
{
2023-12-08 01:44:50 -05:00
const auto mcLock = pauseAndLockVM ();
// Check if memcard is busy, deny request if so
if ( shouldAbortForMemcardBusy ( mcLock ))
{
2025-06-10 19:32:13 -05:00
return false ;
2023-12-08 01:44:50 -05:00
}
2025-06-10 19:32:13 -05:00
std :: string filename_str = filename . toStdString ();
2022-05-26 17:49:40 +10:00
if ( VMManager :: IsSaveStateFileName ( filename_str ))
{
2022-06-28 23:34:45 +10:00
// can't load a save state without a current VM
2022-06-28 22:53:26 +10:00
if ( s_vm_valid )
2022-05-26 17:49:40 +10:00
g_emu_thread -> loadState ( filename );
else
QMessageBox :: critical ( this , tr ( "Load State Failed" ), tr ( "Cannot load a save state without a running VM." ));
2023-09-09 15:37:31 +10:00
2025-06-10 19:32:13 -05:00
return true ;
2022-05-26 17:49:40 +10:00
}
2023-09-09 15:37:31 +10:00
if ( ! VMManager :: IsLoadableFileName ( filename_str ))
2025-06-10 19:32:13 -05:00
return false ;
2023-09-09 15:37:31 +10:00
// if we're already running, do a disc change, otherwise start
if ( ! s_vm_valid )
2022-05-26 17:49:40 +10:00
{
2023-09-09 15:37:31 +10:00
doStartFile ( std :: nullopt , filename );
2025-06-10 19:32:13 -05:00
return true ;
2023-09-09 15:37:31 +10:00
}
if ( VMManager :: IsDiscFileName ( filename_str ) || VMManager :: IsBlockDumpFileName ( filename_str ))
{
doDiscChange ( CDVD_SourceType :: Iso , filename );
}
else if ( VMManager :: IsElfFileName ( filename_str ))
{
const auto lock = pauseAndLockVM ();
2025-06-10 19:32:13 -05:00
if ( QMessageBox :: Yes != QMessageBox :: question ( this , tr ( "Confirm Reset" ),
2025-09-22 22:33:14 +01:00
tr ( "The new ELF cannot be loaded without resetting the virtual machine. Do you want to reset the virtual machine now?" )))
2023-09-09 15:37:31 +10:00
{
2025-06-10 19:32:13 -05:00
return true ;
2023-09-09 15:37:31 +10:00
}
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 ();
2022-06-28 23:34:45 +10:00
}
2025-06-10 19:32:13 -05:00
return true ;
2022-05-26 17:49:40 +10:00
}
2024-01-11 19:38:28 +10:00
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 ();
}
2022-10-15 21:20:05 +10:00
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 };
2022-12-04 18:03:30 +10:00
m_device_notification_handle =
RegisterDeviceNotificationW (( HANDLE ) winId (), & filter , DEVICE_NOTIFY_WINDOW_HANDLE | DEVICE_NOTIFY_ALL_INTERFACE_CLASSES );
2025-02-17 10:41:31 -05:00
// 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 ));
2022-10-15 21:20:05 +10:00
#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 ;
}
2025-02-17 10:41:31 -05:00
if ( msg -> message == WM_INPUT )
{
2025-10-16 19:01:54 -04:00
UINT dwSize = 0 ;
GetRawInputData (( HRAWINPUT ) msg -> lParam , RID_INPUT , nullptr , & dwSize , sizeof ( RAWINPUTHEADER ));
if ( dwSize > 0 )
2025-02-17 10:41:31 -05:00
{
2025-10-16 19:01:54 -04:00
std :: vector < BYTE > lpb ( dwSize );
if ( GetRawInputData (( HRAWINPUT ) msg -> lParam , RID_INPUT , lpb . data (), & dwSize , sizeof ( RAWINPUTHEADER )) == dwSize )
2025-02-17 10:41:31 -05:00
{
2025-10-16 19:01:54 -04:00
const RAWINPUT * raw = reinterpret_cast < const RAWINPUT *> ( lpb . data ());
if ( raw -> header . dwType == RIM_TYPEMOUSE )
2025-02-17 10:41:31 -05:00
{
2025-10-16 19:01:54 -04:00
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 );
}
2025-02-17 10:41:31 -05:00
}
}
}
}
2022-10-15 21:20:05 +10:00
}
return QMainWindow :: nativeEvent ( eventType , message , result );
}
#endif
2023-04-25 22:52:41 +10:00
std :: optional < WindowInfo > MainWindow :: acquireRenderWindow ( bool recreate_window , bool fullscreen , bool render_to_main , bool surfaceless )
2021-12-13 22:12:54 +10:00
{
2023-04-25 22:52:41 +10:00
DevCon . WriteLn ( "acquireRenderWindow() recreate=%s fullscreen=%s render_to_main=%s surfaceless=%s" , recreate_window ? "true" : "false" ,
2023-04-08 13:20:41 +10:00
fullscreen ? "true" : "false" , render_to_main ? "true" : "false" , surfaceless ? "true" : "false" );
2022-05-06 21:03:16 +10:00
2022-05-07 13:52:13 +10:00
const bool is_fullscreen = isRenderingFullscreen ();
const bool is_rendering_to_main = isRenderingToMain ();
2025-10-30 20:54:07 +00:00
const bool changing_surfaceless = ( ! m_display_surface != surfaceless );
2023-04-25 22:52:41 +10:00
if ( m_display_created && ! recreate_window && fullscreen == is_fullscreen && is_rendering_to_main == render_to_main &&
! changing_surfaceless )
{
2025-10-30 20:54:07 +00:00
return m_display_surface ? m_display_surface -> getWindowInfo () : WindowInfo ();
2023-04-25 22:52:41 +10:00
}
2021-12-13 22:12:54 +10:00
// Skip recreating the surface if we're just transitioning between fullscreen and windowed with render-to-main off.
2025-10-30 20:54:07 +00:00
if ( m_display_created && ! recreate_window && ! is_rendering_to_main && ! render_to_main && ! changing_surfaceless )
2021-12-13 22:12:54 +10:00
{
2022-05-07 13:52:13 +10:00
DevCon . WriteLn ( "Toggling to %s without recreating surface" , ( fullscreen ? "fullscreen" : "windowed" ));
2021-12-13 22:12:54 +10:00
2022-05-07 13:52:13 +10:00
// since we don't destroy the display widget, we need to save it here
if ( ! is_fullscreen && ! is_rendering_to_main )
saveDisplayWindowGeometryToConfig ();
2025-11-21 22:35:19 +00:00
#ifdef DISPLAY_SURFACE_WINDOW
auto * displayWindow = m_display_surface ;
#else
auto * displayWindow = m_display_container ;
#endif
2021-12-13 22:12:54 +10:00
if ( fullscreen )
2025-11-21 22:35:19 +00:00
displayWindow -> showFullScreen ();
2021-12-13 22:12:54 +10:00
else
{
2025-11-21 22:35:19 +00:00
// Needs to exit fullscreen before resizing
displayWindow -> showNormal ();
2023-11-21 14:33:59 +10:00
if ( m_is_temporarily_windowed && g_emu_thread -> shouldRenderToMain ())
2025-11-21 22:35:19 +00:00
displayWindow -> setGeometry ( geometry ());
2023-11-21 14:33:59 +10:00
else
restoreDisplayWindowGeometryFromConfig ();
2021-12-13 22:12:54 +10:00
}
2022-12-05 01:00:06 +10:00
updateDisplayWidgetCursor ();
2025-11-21 22:35:19 +00:00
m_display_surface -> setFocus ();
2022-10-02 01:14:39 +10:00
updateWindowState ();
2022-06-20 21:50:01 +10:00
2021-12-13 22:12:54 +10:00
QCoreApplication :: processEvents ( QEventLoop :: ExcludeUserInputEvents );
2025-10-30 20:54:07 +00:00
return m_display_surface -> getWindowInfo ();
2021-12-13 22:12:54 +10:00
}
2022-07-09 16:59:13 +10:00
destroyDisplayWidget ( surfaceless );
2023-04-25 22:52:41 +10:00
m_display_created = true ;
2021-12-13 22:12:54 +10:00
2022-05-06 21:03:16 +10:00
// if we're going to surfaceless, we're done here
if ( surfaceless )
2023-04-01 15:54:04 +10:00
return WindowInfo ();
2022-05-06 21:03:16 +10:00
2024-03-31 20:39:16 +10:00
// 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 ;
}
2023-04-01 15:54:04 +10:00
createDisplayWidget ( fullscreen , render_to_main );
2021-12-13 22:12:54 +10:00
2025-10-30 20:54:07 +00:00
std :: optional < WindowInfo > wi = m_display_surface -> getWindowInfo ();
2021-12-13 22:12:54 +10:00
if ( ! wi . has_value ())
{
2023-04-25 22:52:41 +10:00
QMessageBox :: critical ( this , tr ( "Error" ), tr ( "Failed to get window info from widget" ));
2022-07-09 16:59:13 +10:00
destroyDisplayWidget ( true );
2023-04-01 15:54:04 +10:00
return std :: nullopt ;
2021-12-13 22:12:54 +10:00
}
2026-04-16 16:15:46 -07:00
m_display_is_exclusive_fullscreen = g_emu_thread -> isExclusiveFullscreen ();
2025-10-30 20:54:07 +00:00
g_emu_thread -> connectDisplaySignals ( m_display_surface );
2022-04-15 18:45:41 +10:00
2022-05-06 21:03:16 +10:00
updateWindowTitle ();
2022-06-28 22:16:45 +10:00
updateWindowState ();
2022-06-28 21:37:30 +10:00
2022-12-05 01:00:06 +10:00
updateDisplayWidgetCursor ();
2025-11-21 22:35:19 +00:00
m_display_surface -> setFocus ();
2023-04-01 15:54:04 +10:00
return wi ;
2021-12-13 22:12:54 +10:00
}
2023-04-01 15:54:04 +10:00
void MainWindow :: createDisplayWidget ( bool fullscreen , bool render_to_main )
2022-07-09 16:59:13 +10:00
{
// 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 ();
}
2025-10-30 20:54:07 +00:00
m_display_surface = new DisplaySurface ();
if ( fullscreen || ! render_to_main )
2022-07-09 16:59:13 +10:00
{
2025-11-21 22:35:19 +00:00
#ifdef DISPLAY_SURFACE_WINDOW
m_display_surface -> setTitle ( windowTitle ());
m_display_surface -> setIcon ( windowIcon ());
#else
2025-10-30 20:54:07 +00:00
m_display_container = m_display_surface -> createWindowContainer ();
m_display_container -> setWindowTitle ( windowTitle ());
m_display_container -> setWindowIcon ( windowIcon ());
2025-11-21 22:35:19 +00:00
#endif
2022-07-09 16:59:13 +10:00
}
else
{
2025-10-30 20:54:07 +00:00
m_display_container = m_display_surface -> createWindowContainer ( getContentParent ());
2022-07-09 16:59:13 +10:00
}
2026-01-09 21:46:02 +00:00
if ( fullscreen || g_emu_thread -> isExclusiveFullscreen ())
2022-07-09 16:59:13 +10:00
{
2025-11-21 22:35:19 +00:00
// 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 ())
2026-04-13 07:30:43 -07:00
m_display_surface -> setGeometry ( screen () -> geometry ());
2025-11-21 22:35:19 +00:00
else
restoreDisplayWindowGeometryFromConfig ();
2026-01-09 21:46:02 +00:00
if ( fullscreen )
m_display_surface -> showFullScreen ();
else
m_display_surface -> showNormal ();
2025-11-21 22:35:19 +00:00
#else
2025-10-30 20:54:07 +00:00
if ( isVisible () && g_emu_thread -> shouldRenderToMain ())
2026-03-01 16:28:59 +00:00
m_display_container -> move ( screen () -> availableGeometry (). topLeft ());
2025-10-30 20:54:07 +00:00
else
restoreDisplayWindowGeometryFromConfig ();
2026-01-09 21:46:02 +00:00
if ( fullscreen )
m_display_container -> showFullScreen ();
else
m_display_container -> showNormal ();
2025-11-21 22:35:19 +00:00
#endif
2022-07-09 16:59:13 +10:00
}
else if ( ! render_to_main )
{
2025-11-21 22:35:19 +00:00
#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
2023-11-21 14:33:59 +10:00
if ( m_is_temporarily_windowed && g_emu_thread -> shouldRenderToMain ())
2025-10-30 20:54:07 +00:00
m_display_container -> setGeometry ( geometry ());
2023-11-21 14:33:59 +10:00
else
restoreDisplayWindowGeometryFromConfig ();
2025-10-30 20:54:07 +00:00
m_display_container -> showNormal ();
2025-11-21 22:35:19 +00:00
#endif
2022-07-09 16:59:13 +10:00
}
else
{
pxAssertRel ( m_ui . mainContainer -> count () == 1 , "Has no display widget" );
2025-10-30 20:54:07 +00:00
m_ui . mainContainer -> addWidget ( m_display_container );
2022-07-09 16:59:13 +10:00
m_ui . mainContainer -> setCurrentIndex ( 1 );
}
2025-11-17 22:38:19 +00:00
// Attatch drag and drop signals
connect ( m_display_surface , & DisplaySurface :: dragEnterEvent , this , & MainWindow :: dragEnterEvent );
connect ( m_display_surface , & DisplaySurface :: dropEvent , this , & MainWindow :: dropEvent );
2022-09-15 19:43:21 +10:00
updateDisplayRelatedActions ( true , render_to_main , fullscreen );
2022-07-09 16:59:13 +10:00
// We need the surface visible.
QGuiApplication :: sync ();
}
2021-12-13 22:12:54 +10:00
void MainWindow :: displayResizeRequested ( qint32 width , qint32 height )
{
2025-10-30 20:54:07 +00:00
if ( ! m_display_surface )
2021-12-13 22:12:54 +10:00
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 ));
2025-11-21 22:35:19 +00:00
#ifdef DISPLAY_SURFACE_WINDOW
if ( ! m_display_container )
{
// no parent - rendering to separate window. easy.
QtUtils :: ResizePotentiallyFixedSizeWindow ( m_display_surface , width , height );
return ;
}
#else
2025-10-30 20:54:07 +00:00
if ( ! m_display_container -> parent ())
2021-12-13 22:12:54 +10:00
{
// no parent - rendering to separate window. easy.
2025-10-30 20:54:07 +00:00
QtUtils :: ResizePotentiallyFixedSizeWindow ( m_display_container , width , height );
2021-12-13 22:12:54 +10:00
return ;
}
2025-11-21 22:35:19 +00:00
#endif
2021-12-13 22:12:54 +10:00
// we are rendering to the main window. we have to add in the extra height from the toolbar/status bar.
2025-10-30 20:54:07 +00:00
const s32 extra_height = this -> height () - m_display_container -> height ();
2022-06-28 22:16:45 +10:00
QtUtils :: ResizePotentiallyFixedSizeWindow ( this , width , height + extra_height );
2021-12-13 22:12:54 +10:00
}
2023-07-23 15:07:46 +10:00
void MainWindow :: mouseModeRequested ( bool relative_mode , bool hide_cursor )
2022-12-05 01:00:06 +10:00
{
2023-07-23 15:07:46 +10:00
if ( m_relative_mouse_mode == relative_mode && m_hide_mouse_cursor == hide_cursor )
2022-12-05 01:00:06 +10:00
return ;
2023-07-23 15:07:46 +10:00
m_relative_mouse_mode = relative_mode ;
m_hide_mouse_cursor = hide_cursor ;
2025-10-30 20:54:07 +00:00
if ( m_display_surface && ! s_vm_paused )
2022-12-05 01:00:06 +10:00
updateDisplayWidgetCursor ();
}
2026-02-11 13:47:22 -06:00
void MainWindow :: mouseLockRequested ( bool state )
{
#ifdef __linux__ // Mouse locking is only supported on X11
const bool mouse_lock_supported = QGuiApplication :: platformName (). toLower () == "xcb" ;
2026-02-13 14:02:14 -06:00
if ( ! mouse_lock_supported )
return ;
2026-02-11 13:47:22 -06:00
#endif
2026-02-13 14:02:14 -06:00
Host :: SetBaseBoolSettingValue ( "EmuCore" , "EnableMouseLock" , state );
Host :: CommitBaseSettingChanges ();
2026-02-11 13:47:22 -06:00
2026-02-13 14:02:14 -06:00
if ( state )
{
setupMouseMoveHandler ();
}
else
{
Common :: DetachMousePositionCb ();
}
2026-02-11 13:47:22 -06:00
2026-02-13 14:02:14 -06:00
if ( m_settings_window )
{
InterfaceSettingsWidget * interface_settings = m_settings_window -> getInterfaceSettingsWidget ();
interface_settings -> updateMouseLockCheckbox ( state ? Qt :: Checked : Qt :: Unchecked );
}
2026-02-11 13:47:22 -06:00
}
2023-04-25 22:52:41 +10:00
void MainWindow :: releaseRenderWindow ()
2022-03-13 00:52:52 +10:00
{
2022-06-28 21:37:30 +10:00
// Now we can safely destroy the display window.
2022-07-09 16:59:13 +10:00
destroyDisplayWidget ( true );
2022-05-15 18:20:21 +10:00
m_display_created = false ;
2026-04-16 16:15:46 -07:00
m_display_is_exclusive_fullscreen = false ;
2022-05-06 21:03:16 +10:00
2022-07-09 16:59:13 +10:00
m_ui . actionViewSystemDisplay -> setEnabled ( false );
m_ui . actionFullscreen -> setEnabled ( false );
}
void MainWindow :: destroyDisplayWidget ( bool show_game_list )
{
2025-10-30 20:54:07 +00:00
if ( ! m_display_surface )
2022-07-09 16:59:13 +10:00
return ;
2026-04-16 16:15:46 -07:00
if ( ! ( m_display_surface -> isFullScreen () || m_display_is_exclusive_fullscreen ) && ! isRenderingToMain ())
2022-07-09 16:59:13 +10:00
saveDisplayWindowGeometryToConfig ();
if ( isRenderingToMain ())
2022-05-06 21:03:16 +10:00
{
2025-10-30 20:54:07 +00:00
pxAssertRel ( m_ui . mainContainer -> indexOf ( m_display_container ) == 1 , "Display widget in stack" );
m_ui . mainContainer -> removeWidget ( m_display_container );
if ( show_game_list )
2022-07-09 16:59:13 +10:00
{
2025-10-30 20:54:07 +00:00
m_ui . mainContainer -> setCurrentIndex ( 0 );
m_game_list_widget -> resizeTableViewColumnsToFit ();
2022-07-09 16:59:13 +10:00
}
}
2025-11-21 22:35:19 +00:00
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 ;
}
2022-09-15 19:43:21 +10:00
updateDisplayRelatedActions ( false , false , false );
2022-03-13 00:52:52 +10:00
}
2021-12-13 22:12:54 +10:00
2022-12-05 01:00:06 +10:00
void MainWindow :: updateDisplayWidgetCursor ()
{
2025-10-30 20:54:07 +00:00
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 ());
2022-12-05 01:00:06 +10:00
}
2025-02-17 10:41:31 -05:00
void MainWindow :: setupMouseMoveHandler ()
2025-01-26 17:20:59 +01:00
{
2025-02-26 20:25:55 +00:00
auto mouse_cb_fn = []( int x , int y ) {
if ( g_main_window )
2025-02-17 10:41:31 -05:00
g_main_window -> checkMousePosition ( x , y );
};
2025-02-26 20:25:55 +00:00
if ( ! Common :: AttachMousePositionCb ( mouse_cb_fn ))
2025-02-17 10:41:31 -05:00
{
Console . Warning ( "Unable to setup mouse position cb!" );
}
return ;
2025-01-26 17:20:59 +01:00
}
2025-02-17 10:41:31 -05:00
void MainWindow :: checkMousePosition ( int x , int y )
2025-01-26 17:20:59 +01:00
{
2025-10-18 12:56:35 -04:00
// 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 ;
2025-01-26 17:20:59 +01:00
2025-10-18 12:56:35 -04:00
// physical mouse position
const QPoint physicalPos ( x , y );
2025-10-16 19:01:54 -04:00
2025-10-30 20:54:07 +00:00
const auto * displayWindow = m_display_surface ;
2025-10-16 19:01:54 -04:00
2025-10-18 12:56:35 -04:00
// logical (DIP) frame rect
2025-10-30 20:54:07 +00:00
const QSize logicalSize = displayWindow -> size ();
2025-11-21 22:35:19 +00:00
#ifdef DISPLAY_SURFACE_WINDOW
const QPoint logicalPosition = isRenderingToMain () ? ( displayWindow -> position () + displayWindow -> parent () -> position ()) : displayWindow -> position ();
#else
2025-10-30 20:54:07 +00:00
const QPoint logicalPosition = displayWindow -> position () + displayWindow -> parent () -> position ();
2025-11-21 22:35:19 +00:00
#endif
2025-10-16 19:01:54 -04:00
2025-11-23 16:53:18 +00:00
// 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 ();
2025-10-18 12:56:35 -04:00
// physical frame rect
2025-10-30 20:54:07 +00:00
const qreal scale = displayWindow -> devicePixelRatio ();
const QRectF physicalBounds (
2025-11-23 16:53:18 +00:00
screenPosition . x () + ( logicalPosition . x () - screenPosition . x ()) * scale ,
screenPosition . y () + ( logicalPosition . y () - screenPosition . y ()) * scale ,
2025-10-30 20:54:07 +00:00
logicalSize . width () * scale ,
logicalSize . height () * scale );
2025-01-26 17:20:59 +01:00
2025-10-18 12:56:35 -04:00
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 ()));
});
2025-01-26 17:20:59 +01:00
}
2021-12-13 22:12:54 +10:00
void MainWindow :: saveDisplayWindowGeometryToConfig ()
{
2025-11-21 22:35:19 +00:00
if ( m_display_surface -> isFullScreen ())
2022-05-07 13:52:13 +10:00
{
// if we somehow ended up here, don't save the fullscreen state to the config
return ;
}
2025-11-21 22:35:19 +00:00
const QByteArray geometry = m_display_surface -> saveGeometry ();
2021-12-13 22:12:54 +10:00
const QByteArray geometry_b64 = geometry . toBase64 ();
2022-05-24 22:37:44 +10:00
const std :: string old_geometry_b64 = Host :: GetBaseStringSettingValue ( "UI" , "DisplayWindowGeometry" );
2021-12-13 22:12:54 +10:00
if ( old_geometry_b64 != geometry_b64 . constData ())
2022-09-07 17:44:10 +10:00
{
Host :: SetBaseStringSettingValue ( "UI" , "DisplayWindowGeometry" , geometry_b64 . constData ());
Host :: CommitBaseSettingChanges ();
}
2021-12-13 22:12:54 +10:00
}
void MainWindow :: restoreDisplayWindowGeometryFromConfig ()
{
2022-05-24 22:37:44 +10:00
const std :: string geometry_b64 = Host :: GetBaseStringSettingValue ( "UI" , "DisplayWindowGeometry" );
2021-12-13 22:12:54 +10:00
const QByteArray geometry = QByteArray :: fromBase64 ( QByteArray :: fromStdString ( geometry_b64 ));
if ( ! geometry . isEmpty ())
2022-05-07 13:52:13 +10:00
{
2025-11-21 22:35:19 +00:00
m_display_surface -> restoreGeometry ( geometry );
2022-05-07 13:52:13 +10:00
// make sure we're not loading a dodgy config which had fullscreen set...
2025-11-21 22:35:19 +00:00
#ifdef DISPLAY_SURFACE_WINDOW
m_display_surface -> setWindowStates ( m_display_surface -> windowStates () & ~ ( Qt :: WindowFullScreen | Qt :: WindowActive ));
#else
2025-10-30 20:54:07 +00:00
m_display_container -> setWindowState ( m_display_container -> windowState () & ~ ( Qt :: WindowFullScreen | Qt :: WindowActive ));
2025-11-21 22:35:19 +00:00
#endif
2022-05-07 13:52:13 +10:00
}
2021-12-13 22:12:54 +10:00
else
2022-05-07 13:52:13 +10:00
{
// default size
2025-11-21 22:35:19 +00:00
#ifdef DISPLAY_SURFACE_WINDOW
m_display_surface -> resize ( 640 , 480 );
#else
2025-10-30 20:54:07 +00:00
m_display_container -> resize ( 640 , 480 );
2025-11-21 22:35:19 +00:00
#endif
2022-05-07 13:52:13 +10:00
}
2021-12-13 22:12:54 +10:00
}
2023-10-14 18:45:09 +10:00
SettingsWindow * MainWindow :: getSettingsWindow ()
2021-12-13 22:12:54 +10:00
{
2023-10-15 00:44:11 -05:00
if ( ! m_settings_window )
2021-12-13 22:12:54 +10:00
{
2023-10-15 00:44:11 -05:00
m_settings_window = new SettingsWindow ();
2024-06-23 06:40:25 +10:00
connect ( m_settings_window -> getInterfaceSettingsWidget (), & InterfaceSettingsWidget :: themeChanged , this , & MainWindow :: onThemeChanged );
connect ( m_settings_window -> getInterfaceSettingsWidget (), & InterfaceSettingsWidget :: languageChanged , this , & MainWindow :: onLanguageChanged );
2025-10-29 17:03:08 +07:00
connect ( m_settings_window -> getInterfaceSettingsWidget (), & InterfaceSettingsWidget :: backgroundChanged , m_game_list_widget , [ this ] { m_game_list_widget -> setCustomBackground (); });
2026-03-23 19:04:55 +07:00
connect ( m_settings_window -> getInterfaceSettingsWidget (), & InterfaceSettingsWidget :: preferEnglishGameListChanged , this , [] {
2023-09-09 17:36:10 -05:00
g_main_window -> m_game_list_widget -> refreshGridCovers ();
2025-11-23 00:09:31 -06:00
Host :: RunOnGSThread ([] { FullscreenUI :: PreferEnglishGameListChanged (); });
2023-09-09 17:36:10 -05:00
});
2021-12-13 22:12:54 +10:00
}
2023-10-15 00:44:11 -05:00
return m_settings_window ;
2021-12-13 22:12:54 +10:00
}
2022-02-16 00:29:18 +10:00
void MainWindow :: doSettings ( const char * category /* = nullptr */ )
2021-12-13 22:12:54 +10:00
{
2023-10-14 18:45:09 +10:00
SettingsWindow * dlg = getSettingsWindow ();
2023-12-17 13:53:47 +10:00
if ( ! dlg -> isVisible ())
{
2021-12-13 22:12:54 +10:00
dlg -> show ();
2023-12-17 13:53:47 +10:00
}
else
{
dlg -> raise ();
dlg -> activateWindow ();
dlg -> setFocus ();
}
2021-12-13 22:12:54 +10:00
2022-02-16 00:29:18 +10:00
if ( category )
2021-12-13 22:12:54 +10:00
dlg -> setCategory ( category );
}
2025-02-26 20:25:55 +00:00
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 );
2025-11-05 03:39:53 +00:00
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 ())
2025-02-26 20:25:55 +00:00
{
SettingsWindow :: openGamePropertiesDialog (
2025-11-05 03:39:53 +00:00
&* entry , entry -> title , entry -> serial , entry -> crc , ! s_current_elf_override . isEmpty (), category );
2025-02-26 20:25:55 +00:00
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 );
}
}
2026-06-20 20:27:55 -07:00
#ifdef ENABLE_QT_DEBUGGER
2022-12-24 01:51:44 -05:00
void MainWindow :: openDebugger ()
{
2025-01-27 12:49:39 +00:00
DebuggerWindow * dwnd = DebuggerWindow :: getInstance ();
2024-07-12 17:05:14 -05:00
dwnd -> isVisible () ? dwnd -> activateWindow () : dwnd -> show ();
2022-12-24 01:51:44 -05:00
}
2026-06-20 20:27:55 -07:00
#endif
2022-12-24 01:51:44 -05:00
2023-10-14 18:45:09 +10:00
void MainWindow :: doControllerSettings ( ControllerSettingsWindow :: Category category )
2021-12-13 22:12:54 +10:00
{
2023-12-17 13:53:47 +10:00
if ( ! m_controller_settings_window )
m_controller_settings_window = new ControllerSettingsWindow ();
if ( ! m_controller_settings_window -> isVisible ())
2021-12-13 22:12:54 +10:00
{
2023-12-17 13:53:47 +10:00
m_controller_settings_window -> show ();
2023-10-14 18:45:09 +10:00
}
else
{
2023-12-17 13:53:47 +10:00
m_controller_settings_window -> raise ();
m_controller_settings_window -> activateWindow ();
m_controller_settings_window -> setFocus ();
2021-12-13 22:12:54 +10:00
}
2023-12-17 13:53:47 +10:00
2023-10-14 18:45:09 +10:00
if ( category != ControllerSettingsWindow :: Category :: Count )
m_controller_settings_window -> setCategory ( category );
2021-12-13 22:12:54 +10:00
}
2022-06-28 23:34:45 +10:00
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 ;
}
2025-11-05 03:39:53 +00:00
void MainWindow :: startGameListEntry ( const GameList :: Entry & entry , std :: optional < s32 > save_slot , std :: optional < bool > fast_boot , bool load_backup )
2021-12-13 22:12:54 +10:00
{
std :: shared_ptr < VMBootParameters > params = std :: make_shared < VMBootParameters > ();
params -> fast_boot = fast_boot ;
2025-11-05 03:39:53 +00:00
GameList :: FillBootParametersForEntry ( params . get (), & entry );
2021-12-13 22:12:54 +10:00
2025-11-05 03:39:53 +00:00
if ( save_slot . has_value () && ! entry . serial . empty ())
2021-12-13 22:12:54 +10:00
{
2025-11-05 03:39:53 +00:00
std :: string state_filename = VMManager :: GetSaveStateFileName ( entry . serial . c_str (), entry . crc , save_slot . value (), load_backup );
2021-12-13 22:12:54 +10:00
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 ));
}
2025-11-05 03:39:53 +00:00
void MainWindow :: setGameListEntryCoverImage ( const GameList :: Entry & entry )
2021-12-13 22:12:54 +10:00
{
2023-09-09 13:24:31 +10:00
const QString filename = QDir :: toNativeSeparators (
2023-10-02 17:41:01 +10:00
QFileDialog :: getOpenFileName ( this , tr ( "Select Cover Image" ), QString (), tr ( "All Cover Image Types (*.jpg *.jpeg *.png *.webp)" )));
2021-12-13 22:12:54 +10:00
if ( filename . isEmpty ())
return ;
2025-11-05 03:39:53 +00:00
const QString old_filename = QString :: fromStdString ( GameList :: GetCoverImagePathForEntry ( & entry ));
const QString new_filename = QString :: fromStdString ( GameList :: GetNewCoverImagePathForEntry ( & entry , filename . toUtf8 (). constData (), true ));
2023-09-09 13:24:31 +10:00
if ( new_filename . isEmpty ())
return ;
2023-07-30 13:35:48 +10:00
if ( ! old_filename . isEmpty ())
2021-12-13 22:12:54 +10:00
{
2023-09-09 13:24:31 +10:00
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 ;
}
2022-12-04 18:03:30 +10:00
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 )
2021-12-13 22:12:54 +10:00
{
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 ;
}
2023-07-30 13:35:48 +10:00
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 ;
}
2021-12-13 22:12:54 +10:00
m_game_list_widget -> refreshGridCovers ();
}
2025-11-05 03:39:53 +00:00
void MainWindow :: clearGameListEntryPlayTime ( const GameList :: Entry & entry , const time_t entry_played_time )
2022-11-23 01:03:36 +10:00
{
if ( QMessageBox :: question ( this , tr ( "Confirm Reset" ),
2025-10-20 18:17:20 -05:00
tr ( "Are you sure you want to reset the play time for '%1' (%2)? \n\n Your current play time is %3. \n\n This action cannot be undone." )
2025-11-05 03:39:53 +00:00
. arg ( entry . title . empty () ? tr ( "empty title" ) : QString :: fromStdString ( entry . title ),
entry . serial . empty () ? tr ( "no serial" ) : QString :: fromStdString ( entry . serial ),
2025-11-06 14:36:16 +00:00
QString :: fromStdString ( GameList :: FormatTimespan ( entry_played_time , true ))),
( QMessageBox :: Yes | QMessageBox :: No ), QMessageBox :: No ) == QMessageBox :: Yes )
2022-11-23 01:03:36 +10:00
{
2025-11-05 03:39:53 +00:00
GameList :: ClearPlayedTimeForSerial ( entry . serial );
2025-11-06 14:36:16 +00:00
m_game_list_widget -> refresh ( false , false );
2022-11-23 01:03:36 +10:00
}
}
2025-11-05 03:39:53 +00:00
void MainWindow :: goToWikiPage ( const GameList :: Entry & entry )
2024-07-19 17:52:46 -05:00
{
2025-11-05 03:39:53 +00:00
QtUtils :: OpenURL ( this , fmt :: format ( "https://wiki.pcsx2.net/{}" , entry . serial ). c_str ());
2024-07-19 17:52:46 -05:00
}
2025-11-05 03:39:53 +00:00
void MainWindow :: openSnapshotsFolderForGame ( const GameList :: Entry & entry )
2025-07-01 08:07:48 +02:00
{
2025-10-25 20:43:27 -05:00
// Go to top-level snapshots directory if not organizing by game.
2025-11-05 03:39:53 +00:00
if ( EmuConfig . GS . OrganizeSnapshotsByGame && ! entry . title . empty ())
2025-07-01 08:07:48 +02:00
{
2026-03-23 00:45:25 +07:00
const bool prefer_english = Host :: GetBaseBoolSettingValue ( "UI" , "PreferEnglishGameList" , false );
std :: string game_name = ( prefer_english && ! entry . title_en . empty ()) ? entry . title_en : entry . title ;
2025-10-25 20:43:27 -05:00
Path :: SanitizeFileName ( & game_name );
2025-07-01 08:07:48 +02:00
2025-10-25 20:43:27 -05:00
const std :: string game_dir = Path :: Combine ( EmuFolders :: Snapshots , game_name );
2025-07-01 08:07:48 +02:00
2025-10-25 20:43:27 -05:00
// 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 ))
2025-07-01 08:07:48 +02:00
{
2025-10-25 20:43:27 -05:00
const QFileInfo fi ( QString :: fromStdString ( game_dir ));
QtUtils :: OpenURL ( this , QUrl :: fromLocalFile ( fi . absoluteFilePath ()));
2025-07-01 08:07:48 +02:00
return ;
}
2025-10-25 20:43:27 -05:00
QMessageBox :: critical ( this , tr ( "Error" ), tr ( "Failed to create snapshots directory '%1' \n\n Opening default directory." ). arg ( QString :: fromStdString ( game_dir )));
2025-07-01 08:07:48 +02:00
}
2025-10-25 20:43:27 -05:00
QtUtils :: OpenURL ( this , QUrl :: fromLocalFile ( QString :: fromStdString ( EmuFolders :: Snapshots )));
2025-07-01 08:07:48 +02:00
}
2026-03-23 00:45:25 +07:00
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\n Opening 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\n Opening default directory." ). arg ( QString :: fromStdString ( game_dir )));
}
QtUtils :: OpenURL ( this , QUrl :: fromLocalFile ( QString :: fromStdString ( EmuFolders :: Videos )));
}
2022-05-07 22:56:44 +10:00
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 );
2024-04-02 14:27:49 +07:00
msgbox . setWindowIcon ( QtHost :: GetAppIcon ());
2022-05-07 22:56:44 +10:00
msgbox . setWindowTitle ( tr ( "Load Resume State" ));
2024-04-02 14:27:49 +07:00
msgbox . setWindowModality ( Qt :: WindowModal );
2022-05-07 22:56:44 +10:00
msgbox . setText (
tr ( "A resume save state was found for this game, saved at: \n\n %1. \n\n Do 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 );
2022-05-25 01:12:56 +10:00
msgbox . addButton ( QMessageBox :: Cancel );
2022-05-07 22:56:44 +10:00
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 ;
}
2025-04-26 19:19:34 +07:00
void MainWindow :: loadSaveStateSlot ( s32 slot , bool load_backup )
2021-12-13 22:12:54 +10:00
{
2022-06-28 22:53:26 +10:00
if ( s_vm_valid )
2021-12-13 22:12:54 +10:00
{
// easy when we're running
2025-04-26 19:19:34 +07:00
g_emu_thread -> loadStateFromSlot ( slot , load_backup );
2021-12-13 22:12:54 +10:00
return ;
}
else
{
// we're not currently running, therefore we must've right clicked in the game list
2025-11-05 03:39:53 +00:00
const std :: optional < GameList :: Entry > entry = m_game_list_widget -> getSelectedEntry ();
if ( ! entry . has_value ())
2021-12-13 22:12:54 +10:00
return ;
2025-11-05 03:39:53 +00:00
startGameListEntry ( * entry , slot , std :: nullopt , load_backup );
2021-12-13 22:12:54 +10:00
}
}
void MainWindow :: loadSaveStateFile ( const QString & filename , const QString & state_filename )
{
2022-06-28 22:53:26 +10:00
if ( s_vm_valid )
2021-12-13 22:12:54 +10:00
{
2024-01-11 18:19:27 +10:00
if ( ! filename . isEmpty () && s_current_disc_path != filename )
g_emu_thread -> changeDisc ( CDVD_SourceType :: Iso , s_current_disc_path );
2022-05-26 17:36:05 +10:00
g_emu_thread -> loadState ( state_filename );
2021-12-13 22:12:54 +10:00
}
else
{
std :: shared_ptr < VMBootParameters > params = std :: make_shared < VMBootParameters > ();
2022-03-13 00:52:52 +10:00
params -> filename = filename . toStdString ();
2021-12-13 22:12:54 +10:00
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 );
2022-10-01 23:29:29 +10:00
bool has_any_states = false ;
2021-12-13 22:12:54 +10:00
QAction * action = menu -> addAction ( is_right_click_menu ? tr ( "Load State File..." ) : tr ( "Load From File..." ));
connect ( action , & QAction :: triggered , [ this , filename ]() {
2024-04-04 16:45:06 +10:00
const QString path = QDir :: toNativeSeparators ( QFileDialog :: getOpenFileName ( this ,
tr ( "Select Save State File" ), QString (), tr ( "Save States (*.p2s *.p2s.backup)" )));
2021-12-13 22:12:54 +10:00
if ( path . isEmpty ())
return ;
loadSaveStateFile ( filename , path );
});
2022-10-01 23:29:29 +10:00
QAction * delete_save_states_action = menu -> addAction ( tr ( "Delete Save States..." ));
2021-12-13 22:12:54 +10:00
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
2022-05-07 22:56:44 +10:00
QtUtils :: MarkActionAsDefault ( action );
2022-10-01 23:29:29 +10:00
has_any_states = true ;
2021-12-13 22:12:54 +10:00
}
}
2022-10-01 23:29:29 +10:00
for ( s32 i = 1 ; i <= VMManager :: NUM_SAVE_STATE_SLOTS ; i ++ )
2021-12-13 22:12:54 +10:00
{
FILESYSTEM_STAT_DATA sd ;
state_filename = VMManager :: GetSaveStateFileName ( game_serial_utf8 . constData (), crc , i );
if ( ! FileSystem :: StatFile ( state_filename . c_str (), & sd ))
continue ;
2022-06-15 22:39:31 +04:30
action = menu -> addAction ( tr ( "Load Slot %1 (%2)" ). arg ( i ). arg ( formatTimestampForSaveStateMenu ( sd . ModificationTime )));
2021-12-13 22:12:54 +10:00
connect ( action , & QAction :: triggered , [ this , i ]() { loadSaveStateSlot ( i ); });
2022-10-01 23:29:29 +10:00
has_any_states = true ;
}
2025-04-26 19:19:34 +07:00
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 ;
}
2022-10-01 23:29:29 +10:00
delete_save_states_action -> setEnabled ( has_any_states );
if ( has_any_states )
{
connect ( delete_save_states_action , & QAction :: triggered , this , [ this , serial , crc ] {
2022-12-04 18:03:30 +10:00
if ( QMessageBox :: warning ( this , tr ( "Delete Save States" ),
tr ( "Are you sure you want to delete all save states for %1? \n\n The saves will not be recoverable." ). arg ( serial ),
2022-10-01 23:29:29 +10:00
QMessageBox :: Yes , QMessageBox :: No ) != QMessageBox :: Yes )
{
return ;
}
const u32 deleted = VMManager :: DeleteSaveStates ( serial . toUtf8 (). constData (), crc , true );
2025-12-09 09:05:03 +07:00
QMessageBox :: information ( this , tr ( "Delete Save States" ), tr ( "%n save states deleted." , "" , deleted ));
2022-10-01 23:29:29 +10:00
});
2021-12-13 22:12:54 +10:00
}
}
void MainWindow :: populateSaveStateMenu ( QMenu * menu , const QString & serial , quint32 crc )
{
if ( serial . isEmpty ())
return ;
connect ( menu -> addAction ( tr ( "Save To File..." )), & QAction :: triggered , [ this ]() {
2024-04-04 16:45:06 +10:00
const QString path = QDir :: toNativeSeparators ( QFileDialog :: getSaveFileName (
this , tr ( "Select Save State File" ), QString (), tr ( "Save States (*.p2s)" )));
2021-12-13 22:12:54 +10:00
if ( path . isEmpty ())
return ;
g_emu_thread -> saveState ( path );
});
menu -> addSeparator ();
const QByteArray game_serial_utf8 ( serial . toUtf8 ());
2022-10-01 23:29:29 +10:00
for ( s32 i = 1 ; i <= VMManager :: NUM_SAVE_STATE_SLOTS ; i ++ )
2021-12-13 22:12:54 +10:00
{
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)) ;
2022-03-04 18:04:05 +10:00
connect ( menu -> addAction ( title ), & QAction :: triggered , [ i ]() { g_emu_thread -> saveStateToSlot ( i ); });
2021-12-13 22:12:54 +10:00
}
}
2023-09-09 14:47:26 +10:00
void MainWindow :: updateGameDependentActions ()
2021-12-13 22:12:54 +10:00
{
2024-01-11 18:19:27 +10:00
const bool can_use_pnach = ( s_vm_valid && ! s_current_disc_serial . isEmpty () && s_current_running_crc != 0 );
2023-09-09 14:47:26 +10:00
m_ui . actionEditCheats -> setEnabled ( can_use_pnach );
m_ui . actionEditPatches -> setEnabled ( can_use_pnach );
m_ui . actionReloadPatches -> setEnabled ( s_vm_valid );
2021-12-13 22:12:54 +10:00
}
2022-05-06 23:34:07 +10:00
2026-06-17 12:17:40 -04:00
void MainWindow :: updateGameGridActions ( const bool show_game_grid , const bool show_grid_cover_titles )
2025-07-28 18:22:10 +01:00
{
m_ui . actionGridViewShowTitles -> setEnabled ( show_game_grid );
2026-06-17 12:17:40 -04:00
m_ui . actionGridViewShowFullTitles -> setEnabled ( show_game_grid && show_grid_cover_titles );
2025-07-28 18:22:10 +01:00
m_ui . actionGridViewZoomIn -> setEnabled ( show_game_grid );
m_ui . actionGridViewZoomOut -> setEnabled ( show_game_grid );
m_ui . actionGridViewRefreshCovers -> setEnabled ( show_game_grid );
}
2022-06-28 23:34:45 +10:00
void MainWindow :: doStartFile ( std :: optional < CDVD_SourceType > source , const QString & path )
2022-05-26 17:49:40 +10:00
{
2022-06-28 22:53:26 +10:00
if ( s_vm_valid )
2022-05-26 17:49:40 +10:00
return ;
std :: shared_ptr < VMBootParameters > params = std :: make_shared < VMBootParameters > ();
2022-06-28 23:34:45 +10:00
params -> source_type = source ;
2022-05-26 17:49:40 +10:00
params -> filename = path . toStdString ();
// we might still be saving a resume state...
VMManager :: WaitForSaveStateFlush ();
2023-02-27 00:55:44 +10:00
// GetSaveStateFileName() might temporarily mount the ISO to get the serial.
cancelGameListRefresh ();
2022-05-26 17:49:40 +10:00
const std :: optional < bool > resume (
2022-12-04 18:03:30 +10:00
promptForResumeState ( QString :: fromStdString ( VMManager :: GetSaveStateFileName ( params -> filename . c_str (), - 1 ))));
2022-05-26 17:49:40 +10:00
if ( ! resume . has_value ())
return ;
else if ( resume . value ())
params -> state_index = - 1 ;
g_emu_thread -> startVM ( std :: move ( params ));
}
2022-06-28 23:34:45 +10:00
void MainWindow :: doDiscChange ( CDVD_SourceType source , const QString & path )
2022-05-06 23:34:07 +10:00
{
2023-09-09 19:46:37 +10:00
const auto lock = pauseAndLockVM ();
2023-12-08 01:44:50 -05:00
// Check if memcard is busy, deny request if so
if ( shouldAbortForMemcardBusy ( lock ))
{
return ;
}
2022-05-06 23:34:07 +10:00
bool reset_system = false ;
2023-09-09 15:37:31 +10:00
if ( ! m_was_disc_change_request )
2022-05-06 23:34:07 +10:00
{
2023-01-04 18:22:47 -08:00
QMessageBox message ( QMessageBox :: Question , tr ( "Confirm Disc Change" ),
2023-03-03 21:31:10 +10:00
tr ( "Do you want to swap discs or boot the new image (via system reset)?" ), QMessageBox :: NoButton , this );
2023-01-04 18:22:47 -08:00
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 )
2022-05-06 23:34:07 +10:00
return ;
2023-01-04 18:22:47 -08:00
reset_system = ( message . clickedButton () == reset_button );
2022-05-06 23:34:07 +10:00
}
2022-06-28 23:34:45 +10:00
g_emu_thread -> changeDisc ( source , path );
2022-05-06 23:34:07 +10:00
if ( reset_system )
2023-09-09 15:37:31 +10:00
{
// Clearing ELF override will reset the system.
2024-01-11 18:19:27 +10:00
if ( ! s_current_elf_override . isEmpty ())
2023-09-09 15:37:31 +10:00
g_emu_thread -> setELFOverride ( QString ());
else
g_emu_thread -> resetVM ();
}
2026-07-18 15:58:26 +01:00
// 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 ();
2022-05-06 23:34:07 +10:00
}
2022-05-07 13:52:13 +10:00
MainWindow :: VMLock MainWindow :: pauseAndLockVM ()
{
2023-04-25 22:52:41 +10:00
// 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__
2025-10-30 20:54:07 +00:00
const bool was_fullscreen = g_emu_thread -> isFullscreen ();
2023-04-25 22:52:41 +10:00
#else
const bool was_fullscreen = false ;
#endif
2022-06-28 22:53:26 +10:00
const bool was_paused = s_vm_paused ;
2022-05-07 13:52:13 +10:00
if ( ! was_paused )
g_emu_thread -> setVMPaused ( true );
2023-04-25 22:52:41 +10:00
// 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 )
{
2023-11-20 23:28:00 +10:00
// 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 ;
2023-04-25 22:52:41 +10:00
g_emu_thread -> setFullscreen ( false , false );
2023-11-20 23:28:00 +10:00
2025-10-30 20:54:07 +00:00
// Process events untill both EmuThread and Qt have finished exiting fullscreen
2025-11-21 22:35:19 +00:00
while ( QtHost :: IsVMValid () && ( g_emu_thread -> isFullscreen () || m_display_surface -> isFullScreen ()))
2023-11-20 23:28:00 +10:00
{
QApplication :: processEvents ( QEventLoop :: ExcludeUserInputEvents );
}
2023-04-25 22:52:41 +10:00
}
2024-01-07 15:27:39 +10:00
// Ensure main window is visible.
if ( ! g_main_window -> isVisible ())
g_main_window -> show ();
g_main_window -> raise ();
g_main_window -> activateWindow ();
2025-11-21 22:35:19 +00:00
#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
2022-05-07 13:52:13 +10:00
}
2022-12-04 18:03:30 +10:00
void MainWindow :: rescanFile ( const std :: string & path )
{
m_game_list_widget -> rescanFile ( path );
}
2025-11-21 22:35:19 +00:00
MainWindow :: VMLock :: VMLock ( QWidget * dialog_parent , bool was_paused , bool was_fullscreen , bool owns_parent )
2022-05-07 13:52:13 +10:00
: m_dialog_parent ( dialog_parent )
2025-11-15 16:26:08 +00:00
, m_has_lock ( true )
2022-05-07 13:52:13 +10:00
, m_was_paused ( was_paused )
2022-06-28 23:34:45 +10:00
, m_was_fullscreen ( was_fullscreen )
2025-11-21 22:35:19 +00:00
, m_owns_dialog_parent ( owns_parent )
2022-05-07 13:52:13 +10:00
{
2025-11-15 16:26:08 +00:00
QtHost :: LockVMWithDialog ();
2022-05-07 13:52:13 +10:00
}
MainWindow :: VMLock :: VMLock ( VMLock && lock )
: m_dialog_parent ( lock . m_dialog_parent )
2025-11-15 16:26:08 +00:00
, m_has_lock ( lock . m_has_lock )
2022-05-07 13:52:13 +10:00
, m_was_paused ( lock . m_was_paused )
, m_was_fullscreen ( lock . m_was_fullscreen )
2025-11-21 22:35:19 +00:00
, m_owns_dialog_parent ( lock . m_owns_dialog_parent )
2022-05-07 13:52:13 +10:00
{
lock . m_dialog_parent = nullptr ;
2025-11-15 16:26:08 +00:00
lock . m_has_lock = false ;
2022-06-20 18:03:38 +10:00
lock . m_was_paused = true ;
2022-05-07 13:52:13 +10:00
lock . m_was_fullscreen = false ;
2025-11-21 22:35:19 +00:00
lock . m_owns_dialog_parent = false ;
2022-05-07 13:52:13 +10:00
}
MainWindow :: VMLock ::~ VMLock ()
{
2025-11-15 16:26:08 +00:00
if ( m_has_lock )
QtHost :: UnlockVMWithDialog ();
2025-11-21 22:35:19 +00:00
if ( m_owns_dialog_parent && m_dialog_parent )
{
m_dialog_parent -> deleteLater ();
m_dialog_parent = nullptr ;
}
2022-05-07 13:52:13 +10:00
if ( m_was_fullscreen )
2023-11-20 23:28:00 +10:00
{
g_main_window -> m_is_temporarily_windowed = false ;
2023-04-25 22:52:41 +10:00
g_emu_thread -> setFullscreen ( true , true );
2023-11-20 23:28:00 +10:00
}
2023-04-05 18:15:38 +10:00
2022-05-07 13:52:13 +10:00
if ( ! m_was_paused )
g_emu_thread -> setVMPaused ( false );
}
2024-01-07 15:27:39 +10:00
MainWindow :: VMLock & MainWindow :: VMLock :: operator = ( VMLock && lock )
{
2025-11-15 16:26:08 +00:00
if ( m_has_lock )
QtHost :: UnlockVMWithDialog ();
m_has_lock = lock . m_has_lock ;
2024-01-07 15:27:39 +10:00
m_dialog_parent = lock . m_dialog_parent ;
m_was_paused = lock . m_was_paused ;
m_was_fullscreen = lock . m_was_fullscreen ;
2025-11-15 16:26:08 +00:00
lock . m_has_lock = false ;
2024-01-07 15:27:39 +10:00
lock . m_dialog_parent = nullptr ;
lock . m_was_paused = true ;
lock . m_was_fullscreen = false ;
return * this ;
}
2022-06-20 18:03:38 +10:00
void MainWindow :: VMLock :: cancelResume ()
{
m_was_paused = true ;
m_was_fullscreen = false ;
2023-11-20 23:28:00 +10:00
g_main_window -> m_is_temporarily_windowed = false ;
2022-06-20 18:03:38 +10:00
}
2022-06-28 22:53:26 +10:00
bool QtHost :: IsVMValid ()
{
return s_vm_valid ;
}
bool QtHost :: IsVMPaused ()
{
return s_vm_paused ;
}
2024-01-11 18:19:27 +10:00
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 ;
}
2026-03-03 16:49:47 +00:00
#include "moc_MainWindow.cpp"