duckstation

duckstation, but archived from the revision just before upstream changed it to a proprietary software project, this version is the libre one
git clone https://git.neptards.moe/u3shit/duckstation.git
Log | Files | Refs | README | LICENSE

mainwindow.cpp (104069B)


      1 // SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin <stenzek@gmail.com>
      2 // SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0)
      3 
      4 #include "mainwindow.h"
      5 #include "aboutdialog.h"
      6 #include "achievementlogindialog.h"
      7 #include "autoupdaterdialog.h"
      8 #include "cheatmanagerwindow.h"
      9 #include "coverdownloaddialog.h"
     10 #include "debuggerwindow.h"
     11 #include "displaywidget.h"
     12 #include "gamelistmodel.h"
     13 #include "gamelistsettingswidget.h"
     14 #include "gamelistwidget.h"
     15 #include "interfacesettingswidget.h"
     16 #include "logwindow.h"
     17 #include "memorycardeditorwindow.h"
     18 #include "memoryscannerwindow.h"
     19 #include "qthost.h"
     20 #include "qtutils.h"
     21 #include "selectdiscdialog.h"
     22 #include "settingswindow.h"
     23 #include "settingwidgetbinder.h"
     24 
     25 #include "core/achievements.h"
     26 #include "core/game_list.h"
     27 #include "core/host.h"
     28 #include "core/memory_card.h"
     29 #include "core/settings.h"
     30 #include "core/system.h"
     31 
     32 #include "util/cd_image.h"
     33 #include "util/gpu_device.h"
     34 
     35 #include "common/assert.h"
     36 #include "common/error.h"
     37 #include "common/file_system.h"
     38 #include "common/log.h"
     39 
     40 #include <QtCore/QDebug>
     41 #include <QtCore/QFile>
     42 #include <QtCore/QFileInfo>
     43 #include <QtCore/QMimeData>
     44 #include <QtCore/QUrl>
     45 #include <QtGui/QActionGroup>
     46 #include <QtGui/QCursor>
     47 #include <QtGui/QWindowStateChangeEvent>
     48 #include <QtWidgets/QFileDialog>
     49 #include <QtWidgets/QInputDialog>
     50 #include <QtWidgets/QMessageBox>
     51 #include <QtWidgets/QProgressBar>
     52 #include <QtWidgets/QStyleFactory>
     53 #include <cmath>
     54 
     55 #ifdef _WIN32
     56 #include "common/windows_headers.h"
     57 #include <Dbt.h>
     58 #include <VersionHelpers.h>
     59 #endif
     60 
     61 #ifdef __APPLE__
     62 #include "common/cocoa_tools.h"
     63 #endif
     64 
     65 Log_SetChannel(MainWindow);
     66 
     67 static constexpr char DISC_IMAGE_FILTER[] = QT_TRANSLATE_NOOP(
     68   "MainWindow",
     69   "All File Types (*.bin *.img *.iso *.cue *.chd *.ecm *.mds *.pbp *.exe *.psexe *.ps-exe *.psf *.minipsf "
     70   "*.m3u);;Single-Track "
     71   "Raw Images (*.bin *.img *.iso);;Cue Sheets (*.cue);;MAME CHD Images (*.chd);;Error Code Modeler Images "
     72   "(*.ecm);;Media Descriptor Sidecar Images (*.mds);;PlayStation EBOOTs (*.pbp *.PBP);;PlayStation Executables (*.exe "
     73   "*.psexe *.ps-exe);;Portable Sound Format Files (*.psf *.minipsf);;Playlists (*.m3u)");
     74 
     75 MainWindow* g_main_window = nullptr;
     76 
     77 #if defined(_WIN32) || defined(__APPLE__)
     78 static const bool s_use_central_widget = false;
     79 #else
     80 // Qt Wayland is broken. Any sort of stacked widget usage fails to update,
     81 // leading to broken window resizes, no display rendering, etc. So, we mess
     82 // with the central widget instead. Which we can't do on xorg, because it
     83 // breaks window resizing there...
     84 static bool s_use_central_widget = false;
     85 #endif
     86 
     87 // UI thread VM validity.
     88 static bool s_system_valid = false;
     89 static bool s_system_paused = false;
     90 static QString s_current_game_title;
     91 static QString s_current_game_serial;
     92 static QString s_current_game_path;
     93 static QIcon s_current_game_icon;
     94 
     95 bool QtHost::IsSystemPaused()
     96 {
     97   return s_system_paused;
     98 }
     99 
    100 bool QtHost::IsSystemValid()
    101 {
    102   return s_system_valid;
    103 }
    104 
    105 const QString& QtHost::GetCurrentGameTitle()
    106 {
    107   return s_current_game_title;
    108 }
    109 
    110 const QString& QtHost::GetCurrentGameSerial()
    111 {
    112   return s_current_game_serial;
    113 }
    114 
    115 const QString& QtHost::GetCurrentGamePath()
    116 {
    117   return s_current_game_path;
    118 }
    119 
    120 MainWindow::MainWindow() : QMainWindow(nullptr)
    121 {
    122   Assert(!g_main_window);
    123   g_main_window = this;
    124 
    125 #if !defined(_WIN32) && !defined(__APPLE__)
    126   s_use_central_widget = DisplayContainer::isRunningOnWayland();
    127 #endif
    128 
    129   initialize();
    130 }
    131 
    132 MainWindow::~MainWindow()
    133 {
    134   Assert(!m_display_widget);
    135   Assert(!m_debugger_window);
    136   cancelGameListRefresh();
    137 
    138   // we compare here, since recreate destroys the window later
    139   if (g_main_window == this)
    140     g_main_window = nullptr;
    141 
    142 #ifdef _WIN32
    143   unregisterForDeviceNotifications();
    144 #endif
    145 #ifdef __APPLE__
    146   CocoaTools::RemoveThemeChangeHandler(this);
    147 #endif
    148 }
    149 
    150 void MainWindow::initialize()
    151 {
    152   m_ui.setupUi(this);
    153   setupAdditionalUi();
    154   connectSignals();
    155 
    156   restoreStateFromConfig();
    157   switchToGameListView();
    158   updateWindowTitle();
    159 
    160 #ifdef ENABLE_RAINTEGRATION
    161   if (Achievements::IsUsingRAIntegration())
    162     Achievements::RAIntegration::MainWindowChanged((void*)winId());
    163 #endif
    164 
    165 #ifdef _WIN32
    166   registerForDeviceNotifications();
    167 #endif
    168 
    169 #ifdef __APPLE__
    170   CocoaTools::AddThemeChangeHandler(this,
    171                                     [](void* ctx) { QtHost::RunOnUIThread([] { g_main_window->updateTheme(); }); });
    172 #endif
    173 }
    174 
    175 void MainWindow::reportError(const QString& title, const QString& message)
    176 {
    177   QMessageBox::critical(this, title, message, QMessageBox::Ok);
    178 }
    179 
    180 bool MainWindow::confirmMessage(const QString& title, const QString& message)
    181 {
    182   SystemLock lock(pauseAndLockSystem());
    183 
    184   return (QMessageBox::question(this, title, message) == QMessageBox::Yes);
    185 }
    186 
    187 void MainWindow::onStatusMessage(const QString& message)
    188 {
    189   m_ui.statusBar->showMessage(message);
    190 }
    191 
    192 void MainWindow::registerForDeviceNotifications()
    193 {
    194 #ifdef _WIN32
    195   // We use these notifications to detect when a controller is connected or disconnected.
    196   DEV_BROADCAST_DEVICEINTERFACE_W filter = {
    197     sizeof(DEV_BROADCAST_DEVICEINTERFACE_W), DBT_DEVTYP_DEVICEINTERFACE, 0u, {}, {}};
    198   m_device_notification_handle = RegisterDeviceNotificationW(
    199     (HANDLE)winId(), &filter, DEVICE_NOTIFY_WINDOW_HANDLE | DEVICE_NOTIFY_ALL_INTERFACE_CLASSES);
    200 #endif
    201 }
    202 
    203 void MainWindow::unregisterForDeviceNotifications()
    204 {
    205 #ifdef _WIN32
    206   if (!m_device_notification_handle)
    207     return;
    208 
    209   UnregisterDeviceNotification(static_cast<HDEVNOTIFY>(m_device_notification_handle));
    210   m_device_notification_handle = nullptr;
    211 #endif
    212 }
    213 
    214 #ifdef _WIN32
    215 
    216 bool MainWindow::nativeEvent(const QByteArray& eventType, void* message, qintptr* result)
    217 {
    218   static constexpr const char win_type[] = "windows_generic_MSG";
    219   if (eventType == QByteArray(win_type, sizeof(win_type) - 1))
    220   {
    221     const MSG* msg = static_cast<const MSG*>(message);
    222     if (msg->message == WM_DEVICECHANGE && msg->wParam == DBT_DEVNODES_CHANGED)
    223     {
    224       g_emu_thread->reloadInputDevices();
    225       *result = 1;
    226       return true;
    227     }
    228   }
    229 
    230   return QMainWindow::nativeEvent(eventType, message, result);
    231 }
    232 
    233 #endif
    234 
    235 std::optional<WindowInfo> MainWindow::acquireRenderWindow(bool recreate_window, bool fullscreen, bool render_to_main,
    236                                                           bool surfaceless, bool use_main_window_pos)
    237 {
    238   DEV_LOG("acquireRenderWindow() recreate={} fullscreen={} render_to_main={} surfaceless={} use_main_window_pos={}",
    239           recreate_window ? "true" : "false", fullscreen ? "true" : "false", render_to_main ? "true" : "false",
    240           surfaceless ? "true" : "false", use_main_window_pos ? "true" : "false");
    241 
    242   QWidget* container =
    243     m_display_container ? static_cast<QWidget*>(m_display_container) : static_cast<QWidget*>(m_display_widget);
    244   const bool is_fullscreen = isRenderingFullscreen();
    245   const bool is_rendering_to_main = isRenderingToMain();
    246   const bool changing_surfaceless = (!m_display_widget != surfaceless);
    247   if (m_display_created && !recreate_window && fullscreen == is_fullscreen && is_rendering_to_main == render_to_main &&
    248       !changing_surfaceless)
    249   {
    250     return m_display_widget ? m_display_widget->getWindowInfo() : WindowInfo();
    251   }
    252 
    253   // Skip recreating the surface if we're just transitioning between fullscreen and windowed with render-to-main off.
    254   // .. except on Wayland, where everything tends to break if you don't recreate.
    255   const bool has_container = (m_display_container != nullptr);
    256   const bool needs_container = DisplayContainer::isNeeded(fullscreen, render_to_main);
    257   if (m_display_created && !recreate_window && !is_rendering_to_main && !render_to_main &&
    258       has_container == needs_container && !needs_container && !changing_surfaceless)
    259   {
    260     DEV_LOG("Toggling to {} without recreating surface", (fullscreen ? "fullscreen" : "windowed"));
    261 
    262     // since we don't destroy the display widget, we need to save it here
    263     if (!is_fullscreen && !is_rendering_to_main)
    264       saveDisplayWindowGeometryToConfig();
    265 
    266     if (fullscreen)
    267     {
    268       container->showFullScreen();
    269     }
    270     else
    271     {
    272       if (use_main_window_pos)
    273         container->setGeometry(geometry());
    274       else
    275         restoreDisplayWindowGeometryFromConfig();
    276 
    277       container->showNormal();
    278     }
    279 
    280     updateDisplayWidgetCursor();
    281     m_display_widget->setFocus();
    282     updateWindowState();
    283 
    284     QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
    285     return m_display_widget->getWindowInfo();
    286   }
    287 
    288   destroyDisplayWidget(surfaceless);
    289   m_display_created = true;
    290 
    291   // if we're going to surfaceless, we're done here
    292   if (surfaceless)
    293     return WindowInfo();
    294 
    295   createDisplayWidget(fullscreen, render_to_main, use_main_window_pos);
    296 
    297   std::optional<WindowInfo> wi = m_display_widget->getWindowInfo();
    298   if (!wi.has_value())
    299   {
    300     QMessageBox::critical(this, tr("Error"), tr("Failed to get window info from widget"));
    301     destroyDisplayWidget(true);
    302     return std::nullopt;
    303   }
    304 
    305   g_emu_thread->connectDisplaySignals(m_display_widget);
    306 
    307   updateWindowTitle();
    308   updateWindowState();
    309 
    310   updateDisplayWidgetCursor();
    311   updateDisplayRelatedActions(true, render_to_main, fullscreen);
    312   QtUtils::ShowOrRaiseWindow(QtUtils::GetRootWidget(m_display_widget));
    313   m_display_widget->setFocus();
    314 
    315   return wi;
    316 }
    317 
    318 void MainWindow::createDisplayWidget(bool fullscreen, bool render_to_main, bool use_main_window_pos)
    319 {
    320   // If we're rendering to main and were hidden (e.g. coming back from fullscreen),
    321   // make sure we're visible before trying to add ourselves. Otherwise Wayland breaks.
    322   if (!fullscreen && render_to_main && !isVisible())
    323   {
    324     setVisible(true);
    325     QGuiApplication::sync();
    326   }
    327 
    328   QWidget* container;
    329   if (DisplayContainer::isNeeded(fullscreen, render_to_main))
    330   {
    331     m_display_container = new DisplayContainer();
    332     m_display_widget = new DisplayWidget(m_display_container);
    333     m_display_container->setDisplayWidget(m_display_widget);
    334     container = m_display_container;
    335   }
    336   else
    337   {
    338     m_display_widget = new DisplayWidget((!fullscreen && render_to_main) ? getContentParent() : nullptr);
    339     container = m_display_widget;
    340   }
    341 
    342   if (fullscreen || !render_to_main)
    343   {
    344     container->setWindowTitle(windowTitle());
    345     container->setWindowIcon(windowIcon());
    346   }
    347 
    348   if (fullscreen)
    349   {
    350     // Don't risk doing this on Wayland, it really doesn't like window state changes,
    351     // and positioning has no effect anyway.
    352     if (!s_use_central_widget)
    353     {
    354       if (isVisible() && g_emu_thread->shouldRenderToMain())
    355         container->move(pos());
    356       else
    357         restoreDisplayWindowGeometryFromConfig();
    358     }
    359 
    360     container->showFullScreen();
    361   }
    362   else if (!render_to_main)
    363   {
    364     // See lameland comment above.
    365     if (use_main_window_pos && !s_use_central_widget)
    366       container->setGeometry(geometry());
    367     else
    368       restoreDisplayWindowGeometryFromConfig();
    369     container->showNormal();
    370   }
    371   else if (s_use_central_widget)
    372   {
    373     m_game_list_widget->setVisible(false);
    374     takeCentralWidget();
    375     m_game_list_widget->setParent(this); // takeCentralWidget() removes parent
    376     setCentralWidget(m_display_widget);
    377     m_display_widget->setFocus();
    378     update();
    379   }
    380   else
    381   {
    382     AssertMsg(m_ui.mainContainer->count() == 1, "Has no display widget");
    383     m_ui.mainContainer->addWidget(container);
    384     m_ui.mainContainer->setCurrentIndex(1);
    385   }
    386 
    387   updateDisplayRelatedActions(true, render_to_main, fullscreen);
    388 
    389   // We need the surface visible.
    390   QGuiApplication::sync();
    391 }
    392 
    393 void MainWindow::displayResizeRequested(qint32 width, qint32 height)
    394 {
    395   if (!m_display_widget)
    396     return;
    397 
    398   // unapply the pixel scaling factor for hidpi
    399   const float dpr = devicePixelRatioF();
    400   width = static_cast<qint32>(std::max(static_cast<int>(std::lroundf(static_cast<float>(width) / dpr)), 1));
    401   height = static_cast<qint32>(std::max(static_cast<int>(std::lroundf(static_cast<float>(height) / dpr)), 1));
    402 
    403   if (m_display_container || !m_display_widget->parent())
    404   {
    405     // no parent - rendering to separate window. easy.
    406     QtUtils::ResizePotentiallyFixedSizeWindow(getDisplayContainer(), width, height);
    407     return;
    408   }
    409 
    410   // we are rendering to the main window. we have to add in the extra height from the toolbar/status bar.
    411   const s32 extra_height = this->height() - m_display_widget->height();
    412   QtUtils::ResizePotentiallyFixedSizeWindow(this, width, height + extra_height);
    413 }
    414 
    415 void MainWindow::releaseRenderWindow()
    416 {
    417   // Now we can safely destroy the display window.
    418   destroyDisplayWidget(true);
    419   m_display_created = false;
    420 
    421   updateDisplayRelatedActions(false, false, false);
    422 
    423   m_ui.actionViewSystemDisplay->setEnabled(false);
    424   m_ui.actionFullscreen->setEnabled(false);
    425 }
    426 
    427 void MainWindow::destroyDisplayWidget(bool show_game_list)
    428 {
    429   if (!m_display_widget)
    430     return;
    431 
    432   if (!isRenderingFullscreen() && !isRenderingToMain())
    433     saveDisplayWindowGeometryToConfig();
    434 
    435   if (m_display_container)
    436     m_display_container->removeDisplayWidget();
    437 
    438   if (isRenderingToMain())
    439   {
    440     if (s_use_central_widget)
    441     {
    442       AssertMsg(centralWidget() == m_display_widget, "Display widget is currently central");
    443       takeCentralWidget();
    444       if (show_game_list)
    445       {
    446         m_game_list_widget->setVisible(true);
    447         setCentralWidget(m_game_list_widget);
    448         m_game_list_widget->resizeTableViewColumnsToFit();
    449       }
    450     }
    451     else
    452     {
    453       AssertMsg(m_ui.mainContainer->indexOf(m_display_widget) == 1, "Display widget in stack");
    454       m_ui.mainContainer->removeWidget(m_display_widget);
    455       if (show_game_list)
    456       {
    457         m_ui.mainContainer->setCurrentIndex(0);
    458         m_game_list_widget->resizeTableViewColumnsToFit();
    459       }
    460     }
    461   }
    462 
    463   if (m_display_widget)
    464   {
    465     m_display_widget->destroy();
    466     m_display_widget = nullptr;
    467   }
    468 
    469   if (m_display_container)
    470   {
    471     m_display_container->deleteLater();
    472     m_display_container = nullptr;
    473   }
    474 }
    475 
    476 void MainWindow::updateDisplayWidgetCursor()
    477 {
    478   m_display_widget->updateRelativeMode(s_system_valid && !s_system_paused && m_relative_mouse_mode);
    479   m_display_widget->updateCursor(s_system_valid && !s_system_paused && shouldHideMouseCursor());
    480 }
    481 
    482 void MainWindow::updateDisplayRelatedActions(bool has_surface, bool render_to_main, bool fullscreen)
    483 {
    484   // rendering to main, or switched to gamelist/grid
    485   m_ui.actionViewSystemDisplay->setEnabled((has_surface && render_to_main) || (!has_surface && g_gpu_device));
    486   m_ui.menuWindowSize->setEnabled(has_surface && !fullscreen);
    487   m_ui.actionFullscreen->setEnabled(has_surface);
    488 
    489   {
    490     QSignalBlocker blocker(m_ui.actionFullscreen);
    491     m_ui.actionFullscreen->setChecked(fullscreen);
    492   }
    493 }
    494 
    495 void MainWindow::focusDisplayWidget()
    496 {
    497   if (!m_display_widget || centralWidget() != m_display_widget)
    498     return;
    499 
    500   m_display_widget->setFocus();
    501 }
    502 
    503 QWidget* MainWindow::getContentParent()
    504 {
    505   return s_use_central_widget ? static_cast<QWidget*>(this) : static_cast<QWidget*>(m_ui.mainContainer);
    506 }
    507 
    508 QWidget* MainWindow::getDisplayContainer() const
    509 {
    510   return (m_display_container ? static_cast<QWidget*>(m_display_container) : static_cast<QWidget*>(m_display_widget));
    511 }
    512 
    513 void MainWindow::onMouseModeRequested(bool relative_mode, bool hide_cursor)
    514 {
    515   m_relative_mouse_mode = relative_mode;
    516   m_hide_mouse_cursor = hide_cursor;
    517   if (m_display_widget)
    518     updateDisplayWidgetCursor();
    519 }
    520 
    521 void MainWindow::onSystemStarting()
    522 {
    523   s_system_valid = false;
    524   s_system_paused = false;
    525 
    526   updateEmulationActions(true, false, Achievements::IsHardcoreModeActive());
    527 }
    528 
    529 void MainWindow::onSystemStarted()
    530 {
    531   m_was_disc_change_request = false;
    532   s_system_valid = true;
    533   updateEmulationActions(false, true, Achievements::IsHardcoreModeActive());
    534   updateWindowTitle();
    535   updateStatusBarWidgetVisibility();
    536   updateDisplayWidgetCursor();
    537 }
    538 
    539 void MainWindow::onSystemPaused()
    540 {
    541   // update UI
    542   {
    543     QSignalBlocker sb(m_ui.actionPause);
    544     m_ui.actionPause->setChecked(true);
    545   }
    546 
    547   s_system_paused = true;
    548   updateStatusBarWidgetVisibility();
    549   m_ui.statusBar->showMessage(tr("Paused"));
    550   if (m_display_widget)
    551     updateDisplayWidgetCursor();
    552 }
    553 
    554 void MainWindow::onSystemResumed()
    555 {
    556   // update UI
    557   {
    558     QSignalBlocker sb(m_ui.actionPause);
    559     m_ui.actionPause->setChecked(false);
    560   }
    561 
    562   s_system_paused = false;
    563   m_was_disc_change_request = false;
    564   m_ui.statusBar->clearMessage();
    565   updateStatusBarWidgetVisibility();
    566   if (m_display_widget)
    567   {
    568     updateDisplayWidgetCursor();
    569     m_display_widget->setFocus();
    570   }
    571 }
    572 
    573 void MainWindow::onSystemDestroyed()
    574 {
    575   // update UI
    576   {
    577     QSignalBlocker sb(m_ui.actionPause);
    578     m_ui.actionPause->setChecked(false);
    579   }
    580 
    581   s_system_valid = false;
    582   s_system_paused = false;
    583 
    584   // If we're closing or in batch mode, quit the whole application now.
    585   if (m_is_closing || QtHost::InBatchMode())
    586   {
    587     destroySubWindows();
    588     quit();
    589     return;
    590   }
    591 
    592   updateEmulationActions(false, false, Achievements::IsHardcoreModeActive());
    593   if (m_display_widget)
    594     updateDisplayWidgetCursor();
    595   else
    596     switchToGameListView();
    597 
    598   // reload played time
    599   if (m_game_list_widget->isShowingGameList())
    600     m_game_list_widget->refresh(false);
    601 }
    602 
    603 void MainWindow::onRunningGameChanged(const QString& filename, const QString& game_serial, const QString& game_title)
    604 {
    605   s_current_game_path = filename;
    606   s_current_game_title = game_title;
    607   s_current_game_serial = game_serial;
    608   s_current_game_icon = m_game_list_widget->getModel()->getIconForGame(filename);
    609 
    610   updateWindowTitle();
    611 }
    612 
    613 void MainWindow::onMediaCaptureStarted()
    614 {
    615   QSignalBlocker sb(m_ui.actionMediaCapture);
    616   m_ui.actionMediaCapture->setChecked(true);
    617 }
    618 
    619 void MainWindow::onMediaCaptureStopped()
    620 {
    621   QSignalBlocker sb(m_ui.actionMediaCapture);
    622   m_ui.actionMediaCapture->setChecked(false);
    623 }
    624 
    625 void MainWindow::onApplicationStateChanged(Qt::ApplicationState state)
    626 {
    627   if (!s_system_valid)
    628     return;
    629 
    630   const bool focus_loss = (state != Qt::ApplicationActive);
    631   if (focus_loss)
    632   {
    633     if (g_settings.pause_on_focus_loss && !m_was_paused_by_focus_loss && !s_system_paused)
    634     {
    635       g_emu_thread->setSystemPaused(true);
    636       m_was_paused_by_focus_loss = true;
    637     }
    638 
    639     // Clear the state of all keyboard binds.
    640     // That way, if we had a key held down, and lost focus, the bind won't be stuck enabled because we never
    641     // got the key release message, because it happened in another window which "stole" the event.
    642     g_emu_thread->clearInputBindStateFromSource(InputManager::MakeHostKeyboardKey(0));
    643   }
    644   else
    645   {
    646     if (m_was_paused_by_focus_loss)
    647     {
    648       if (s_system_paused)
    649         g_emu_thread->setSystemPaused(false);
    650       m_was_paused_by_focus_loss = false;
    651     }
    652   }
    653 }
    654 
    655 void MainWindow::onStartFileActionTriggered()
    656 {
    657   QString filename = QDir::toNativeSeparators(
    658     QFileDialog::getOpenFileName(this, tr("Select Disc Image"), QString(), tr(DISC_IMAGE_FILTER), nullptr));
    659   if (filename.isEmpty())
    660     return;
    661 
    662   startFileOrChangeDisc(filename);
    663 }
    664 
    665 std::string MainWindow::getDeviceDiscPath(const QString& title)
    666 {
    667   std::string ret;
    668 
    669   auto devices = CDImage::GetDeviceList();
    670   if (devices.empty())
    671   {
    672     QMessageBox::critical(this, title,
    673                           tr("Could not find any CD-ROM devices. Please ensure you have a CD-ROM drive connected and "
    674                              "sufficient permissions to access it."));
    675     return ret;
    676   }
    677 
    678   // if there's only one, select it automatically
    679   if (devices.size() == 1)
    680   {
    681     ret = std::move(devices.front().first);
    682     return ret;
    683   }
    684 
    685   QStringList input_options;
    686   for (const auto& [path, name] : devices)
    687     input_options.append(tr("%1 (%2)").arg(QString::fromStdString(name)).arg(QString::fromStdString(path)));
    688 
    689   QInputDialog input_dialog(this);
    690   input_dialog.setWindowTitle(title);
    691   input_dialog.setLabelText(tr("Select disc drive:"));
    692   input_dialog.setInputMode(QInputDialog::TextInput);
    693   input_dialog.setOptions(QInputDialog::UseListViewForComboBoxItems);
    694   input_dialog.setComboBoxEditable(false);
    695   input_dialog.setComboBoxItems(std::move(input_options));
    696   if (input_dialog.exec() == 0)
    697     return ret;
    698 
    699   const int selected_index = input_dialog.comboBoxItems().indexOf(input_dialog.textValue());
    700   if (selected_index < 0 || static_cast<u32>(selected_index) >= devices.size())
    701     return ret;
    702 
    703   ret = std::move(devices[selected_index].first);
    704   return ret;
    705 }
    706 
    707 void MainWindow::quit()
    708 {
    709   // Make sure VM is gone. It really should be if we're here.
    710   if (s_system_valid)
    711   {
    712     g_emu_thread->shutdownSystem(false, true);
    713     while (s_system_valid)
    714       QApplication::processEvents(QEventLoop::ExcludeUserInputEvents, 1);
    715   }
    716 
    717   // Big picture might still be active.
    718   if (m_display_created)
    719     g_emu_thread->stopFullscreenUI();
    720 
    721   // Ensure subwindows are removed before quitting. That way the log window cancelling
    722   // the close event won't cancel the quit process.
    723   destroySubWindows();
    724   QGuiApplication::quit();
    725 }
    726 
    727 void MainWindow::recreate()
    728 {
    729   std::optional<QPoint> settings_window_pos;
    730   int settings_window_row = 0;
    731   std::optional<QPoint> controller_settings_window_pos;
    732   ControllerSettingsWindow::Category controller_settings_window_row =
    733     ControllerSettingsWindow::Category::GlobalSettings;
    734   if (m_settings_window && m_settings_window->isVisible())
    735   {
    736     settings_window_pos = m_settings_window->pos();
    737     settings_window_row = m_settings_window->getCategoryRow();
    738   }
    739   if (m_controller_settings_window && m_controller_settings_window->isVisible())
    740   {
    741     controller_settings_window_pos = m_controller_settings_window->pos();
    742     controller_settings_window_row = m_controller_settings_window->getCurrentCategory();
    743   }
    744 
    745   // Remove subwindows before switching to surfaceless, because otherwise e.g. the debugger can cause funkyness.
    746   destroySubWindows();
    747 
    748   const bool was_display_created = m_display_created;
    749   if (was_display_created)
    750   {
    751     g_emu_thread->setSurfaceless(true);
    752     while (m_display_widget || !g_emu_thread->isSurfaceless())
    753       QApplication::processEvents(QEventLoop::ExcludeUserInputEvents, 1);
    754 
    755     m_display_created = false;
    756   }
    757 
    758   // We need to close input sources, because e.g. DInput uses our window handle.
    759   g_emu_thread->closeInputSources();
    760 
    761   close();
    762   g_main_window = nullptr;
    763 
    764   MainWindow* new_main_window = new MainWindow();
    765   DebugAssert(g_main_window == new_main_window);
    766   new_main_window->show();
    767   deleteLater();
    768 
    769   // Recreate log window as well. Then make sure we're still on top.
    770   LogWindow::updateSettings();
    771   new_main_window->raise();
    772   new_main_window->activateWindow();
    773 
    774   // Reload the sources we just closed.
    775   g_emu_thread->reloadInputSources();
    776 
    777   if (was_display_created)
    778   {
    779     g_emu_thread->setSurfaceless(false);
    780     g_main_window->updateEmulationActions(false, System::IsValid(), Achievements::IsHardcoreModeActive());
    781     g_main_window->onFullscreenUIStateChange(g_emu_thread->isRunningFullscreenUI());
    782   }
    783 
    784   if (settings_window_pos.has_value())
    785   {
    786     SettingsWindow* dlg = g_main_window->getSettingsWindow();
    787     dlg->move(settings_window_pos.value());
    788     dlg->setCategoryRow(settings_window_row);
    789     QtUtils::ShowOrRaiseWindow(dlg);
    790   }
    791   if (controller_settings_window_pos.has_value())
    792   {
    793     ControllerSettingsWindow* dlg = g_main_window->getControllerSettingsWindow();
    794     dlg->move(controller_settings_window_pos.value());
    795     dlg->setCategory(controller_settings_window_row);
    796     QtUtils::ShowOrRaiseWindow(dlg);
    797   }
    798 }
    799 
    800 void MainWindow::destroySubWindows()
    801 {
    802   QtUtils::CloseAndDeleteWindow(m_memory_scanner_window);
    803   QtUtils::CloseAndDeleteWindow(m_debugger_window);
    804   QtUtils::CloseAndDeleteWindow(m_cheat_manager_window);
    805   QtUtils::CloseAndDeleteWindow(m_memory_card_editor_window);
    806   QtUtils::CloseAndDeleteWindow(m_controller_settings_window);
    807   QtUtils::CloseAndDeleteWindow(m_settings_window);
    808 
    809   SettingsWindow::closeGamePropertiesDialogs();
    810 
    811   LogWindow::destroy();
    812 }
    813 
    814 void MainWindow::populateGameListContextMenu(const GameList::Entry* entry, QWidget* parent_window, QMenu* menu)
    815 {
    816   QAction* resume_action = nullptr;
    817   QMenu* load_state_menu = nullptr;
    818 
    819   if (!entry->IsDiscSet())
    820   {
    821     resume_action = menu->addAction(tr("Resume"));
    822     resume_action->setEnabled(false);
    823 
    824     load_state_menu = menu->addMenu(tr("Load State"));
    825     load_state_menu->setEnabled(false);
    826 
    827     if (!entry->serial.empty())
    828     {
    829       std::vector<SaveStateInfo> available_states(System::GetAvailableSaveStates(entry->serial.c_str()));
    830       const QString timestamp_format = QLocale::system().dateTimeFormat(QLocale::ShortFormat);
    831       const bool challenge_mode = Achievements::IsHardcoreModeActive();
    832       for (SaveStateInfo& ssi : available_states)
    833       {
    834         if (ssi.global)
    835           continue;
    836 
    837         const s32 slot = ssi.slot;
    838         const QDateTime timestamp(QDateTime::fromSecsSinceEpoch(static_cast<qint64>(ssi.timestamp)));
    839         const QString timestamp_str(timestamp.toString(timestamp_format));
    840 
    841         QAction* action;
    842         if (slot < 0)
    843         {
    844           resume_action->setText(tr("Resume (%1)").arg(timestamp_str));
    845           resume_action->setEnabled(!challenge_mode);
    846           action = resume_action;
    847         }
    848         else
    849         {
    850           load_state_menu->setEnabled(true);
    851           action = load_state_menu->addAction(tr("Game Save %1 (%2)").arg(slot).arg(timestamp_str));
    852         }
    853 
    854         action->setDisabled(challenge_mode);
    855         connect(action, &QAction::triggered,
    856                 [this, entry, path = std::move(ssi.path)]() { startFile(entry->path, std::move(path), std::nullopt); });
    857       }
    858     }
    859   }
    860 
    861   QAction* open_memory_cards_action = menu->addAction(tr("Edit Memory Cards..."));
    862   connect(open_memory_cards_action, &QAction::triggered, [entry]() {
    863     QString paths[2];
    864     for (u32 i = 0; i < 2; i++)
    865       paths[i] = QString::fromStdString(System::GetGameMemoryCardPath(entry->serial, entry->path, i));
    866 
    867     g_main_window->openMemoryCardEditor(paths[0], paths[1]);
    868   });
    869 
    870   if (!entry->IsDiscSet())
    871   {
    872     const bool has_any_states = resume_action->isEnabled() || load_state_menu->isEnabled();
    873     QAction* delete_save_states_action = menu->addAction(tr("Delete Save States..."));
    874     delete_save_states_action->setEnabled(has_any_states);
    875     if (has_any_states)
    876     {
    877       connect(delete_save_states_action, &QAction::triggered, [parent_window, entry] {
    878         if (QMessageBox::warning(
    879               parent_window, tr("Confirm Save State Deletion"),
    880               tr("Are you sure you want to delete all save states for %1?\n\nThe saves will not be recoverable.")
    881                 .arg(QString::fromStdString(entry->serial)),
    882               QMessageBox::Yes, QMessageBox::No) != QMessageBox::Yes)
    883         {
    884           return;
    885         }
    886 
    887         System::DeleteSaveStates(entry->serial.c_str(), true);
    888       });
    889     }
    890   }
    891 }
    892 
    893 static QString FormatTimestampForSaveStateMenu(u64 timestamp)
    894 {
    895   const QDateTime qtime(QDateTime::fromSecsSinceEpoch(static_cast<qint64>(timestamp)));
    896   return qtime.toString(QLocale::system().dateTimeFormat(QLocale::ShortFormat));
    897 }
    898 
    899 void MainWindow::populateLoadStateMenu(const char* game_serial, QMenu* menu)
    900 {
    901   auto add_slot = [this, game_serial, menu](const QString& title, const QString& empty_title, bool global, s32 slot) {
    902     std::optional<SaveStateInfo> ssi = System::GetSaveStateInfo(global ? nullptr : game_serial, slot);
    903 
    904     const QString menu_title =
    905       ssi.has_value() ? title.arg(slot).arg(FormatTimestampForSaveStateMenu(ssi->timestamp)) : empty_title.arg(slot);
    906 
    907     QAction* load_action = menu->addAction(menu_title);
    908     load_action->setEnabled(ssi.has_value());
    909     if (ssi.has_value())
    910     {
    911       const QString path(QString::fromStdString(ssi->path));
    912       connect(load_action, &QAction::triggered, this, [path]() { g_emu_thread->loadState(path); });
    913     }
    914   };
    915 
    916   menu->clear();
    917 
    918   connect(menu->addAction(tr("Load From File...")), &QAction::triggered, []() {
    919     const QString path = QDir::toNativeSeparators(
    920       QFileDialog::getOpenFileName(g_main_window, tr("Select Save State File"), QString(), tr("Save States (*.sav)")));
    921     if (path.isEmpty())
    922       return;
    923 
    924     g_emu_thread->loadState(path);
    925   });
    926   QAction* load_from_state = menu->addAction(tr("Undo Load State"));
    927   load_from_state->setEnabled(System::CanUndoLoadState());
    928   connect(load_from_state, &QAction::triggered, g_emu_thread, &EmuThread::undoLoadState);
    929   menu->addSeparator();
    930 
    931   if (game_serial && std::strlen(game_serial) > 0)
    932   {
    933     for (u32 slot = 1; slot <= System::PER_GAME_SAVE_STATE_SLOTS; slot++)
    934       add_slot(tr("Game Save %1 (%2)"), tr("Game Save %1 (Empty)"), false, static_cast<s32>(slot));
    935 
    936     menu->addSeparator();
    937   }
    938 
    939   for (u32 slot = 1; slot <= System::GLOBAL_SAVE_STATE_SLOTS; slot++)
    940     add_slot(tr("Global Save %1 (%2)"), tr("Global Save %1 (Empty)"), true, static_cast<s32>(slot));
    941 }
    942 
    943 void MainWindow::populateSaveStateMenu(const char* game_serial, QMenu* menu)
    944 {
    945   auto add_slot = [game_serial, menu](const QString& title, const QString& empty_title, bool global, s32 slot) {
    946     std::optional<SaveStateInfo> ssi = System::GetSaveStateInfo(global ? nullptr : game_serial, slot);
    947 
    948     const QString menu_title =
    949       ssi.has_value() ? title.arg(slot).arg(FormatTimestampForSaveStateMenu(ssi->timestamp)) : empty_title.arg(slot);
    950 
    951     QAction* save_action = menu->addAction(menu_title);
    952     connect(save_action, &QAction::triggered, [global, slot]() { g_emu_thread->saveState(global, slot); });
    953   };
    954 
    955   menu->clear();
    956 
    957   connect(menu->addAction(tr("Save To File...")), &QAction::triggered, []() {
    958     if (!System::IsValid())
    959       return;
    960 
    961     const QString path = QDir::toNativeSeparators(
    962       QFileDialog::getSaveFileName(g_main_window, tr("Select Save State File"), QString(), tr("Save States (*.sav)")));
    963     if (path.isEmpty())
    964       return;
    965 
    966     g_emu_thread->saveState(QDir::toNativeSeparators(path));
    967   });
    968   menu->addSeparator();
    969 
    970   if (game_serial && std::strlen(game_serial) > 0)
    971   {
    972     for (u32 slot = 1; slot <= System::PER_GAME_SAVE_STATE_SLOTS; slot++)
    973       add_slot(tr("Game Save %1 (%2)"), tr("Game Save %1 (Empty)"), false, static_cast<s32>(slot));
    974 
    975     menu->addSeparator();
    976   }
    977 
    978   for (u32 slot = 1; slot <= System::GLOBAL_SAVE_STATE_SLOTS; slot++)
    979     add_slot(tr("Global Save %1 (%2)"), tr("Global Save %1 (Empty)"), true, static_cast<s32>(slot));
    980 }
    981 
    982 void MainWindow::populateChangeDiscSubImageMenu(QMenu* menu, QActionGroup* action_group)
    983 {
    984   if (!s_system_valid)
    985     return;
    986 
    987   if (System::HasMediaSubImages())
    988   {
    989     const u32 count = System::GetMediaSubImageCount();
    990     const u32 current = System::GetMediaSubImageIndex();
    991     for (u32 i = 0; i < count; i++)
    992     {
    993       QAction* action = action_group->addAction(QString::fromStdString(System::GetMediaSubImageTitle(i)));
    994       action->setCheckable(true);
    995       action->setChecked(i == current);
    996       connect(action, &QAction::triggered, [i]() { g_emu_thread->changeDiscFromPlaylist(i); });
    997       menu->addAction(action);
    998     }
    999   }
   1000   else if (const GameDatabase::Entry* entry = System::GetGameDatabaseEntry(); entry && !entry->disc_set_serials.empty())
   1001   {
   1002     auto lock = GameList::GetLock();
   1003     for (const auto& [title, glentry] : GameList::GetMatchingEntriesForSerial(entry->disc_set_serials))
   1004     {
   1005       QAction* action = action_group->addAction(QString::fromStdString(title));
   1006       QString path = QString::fromStdString(glentry->path);
   1007       action->setCheckable(true);
   1008       action->setChecked(path == s_current_game_path);
   1009       connect(action, &QAction::triggered, [path = std::move(path)]() { g_emu_thread->changeDisc(path, false, true); });
   1010       menu->addAction(action);
   1011     }
   1012   }
   1013 }
   1014 
   1015 void MainWindow::updateCheatActionsVisibility()
   1016 {
   1017   // If the cheat system is disabled, put an action to enable it in place of the menu under System.
   1018   const bool cheats_enabled = Host::GetBoolSettingValue("Console", "EnableCheats", false);
   1019   m_ui.actionCheats->setVisible(!cheats_enabled);
   1020   m_ui.menuCheats->menuAction()->setVisible(cheats_enabled);
   1021 }
   1022 
   1023 void MainWindow::onCheatsActionTriggered()
   1024 {
   1025   const bool cheats_enabled = Host::GetBoolSettingValue("Console", "EnableCheats", false);
   1026   if (cheats_enabled)
   1027   {
   1028     m_ui.menuCheats->exec(QCursor::pos());
   1029     return;
   1030   }
   1031 
   1032   SystemLock lock(pauseAndLockSystem());
   1033   QMessageBox mb(this);
   1034   mb.setWindowTitle(tr("Enable Cheats"));
   1035   mb.setText(
   1036     tr("Using cheats can have unpredictable effects on games, causing crashes, graphical glitches, and corrupted "
   1037        "saves. By using the cheat manager, you agree that it is an unsupported configuration, and we will not "
   1038        "provide you with any assistance when games break.\n\nCheats persist through save states even after being "
   1039        "disabled, please remember to reset/reboot the game after turning off any codes.\n\nAre you sure you want "
   1040        "to continue?"));
   1041   mb.setIcon(QMessageBox::Warning);
   1042   QPushButton* global = mb.addButton(tr("Enable For All Games"), QMessageBox::DestructiveRole);
   1043   QPushButton* game = mb.addButton(tr("Enable For This Game"), QMessageBox::AcceptRole);
   1044   game->setEnabled(s_system_valid && !s_current_game_serial.isEmpty());
   1045   QPushButton* cancel = mb.addButton(tr("Cancel"), QMessageBox::RejectRole);
   1046   mb.setDefaultButton(cancel);
   1047   mb.setEscapeButton(cancel);
   1048   mb.exec();
   1049 
   1050   if (mb.clickedButton() == global)
   1051   {
   1052     // enable globally
   1053     Host::SetBaseBoolSettingValue("Console", "EnableCheats", true);
   1054     Host::CommitBaseSettingChanges();
   1055     g_emu_thread->applySettings(false);
   1056   }
   1057   else if (mb.clickedButton() == game)
   1058   {
   1059     if (!SettingsWindow::setGameSettingsBoolForSerial(s_current_game_serial.toStdString(), "Console", "EnableCheats",
   1060                                                       true))
   1061     {
   1062       QMessageBox::critical(this, tr("Error"), tr("Failed to enable cheats for %1.").arg(s_current_game_serial));
   1063       return;
   1064     }
   1065 
   1066     g_emu_thread->reloadGameSettings(false);
   1067   }
   1068   else
   1069   {
   1070     // do nothing
   1071     return;
   1072   }
   1073 }
   1074 
   1075 void MainWindow::onCheatsMenuAboutToShow()
   1076 {
   1077   m_ui.menuCheats->clear();
   1078   connect(m_ui.menuCheats->addAction(tr("Cheat Manager")), &QAction::triggered, this, &MainWindow::openCheatManager);
   1079   m_ui.menuCheats->addSeparator();
   1080   populateCheatsMenu(m_ui.menuCheats);
   1081 }
   1082 
   1083 void MainWindow::populateCheatsMenu(QMenu* menu)
   1084 {
   1085   const bool has_cheat_list = (s_system_valid && System::HasCheatList());
   1086 
   1087   QMenu* enabled_menu = menu->addMenu(tr("&Enabled Cheats"));
   1088   enabled_menu->setEnabled(s_system_valid);
   1089   QMenu* apply_menu = menu->addMenu(tr("&Apply Cheats"));
   1090   apply_menu->setEnabled(s_system_valid);
   1091 
   1092   if (has_cheat_list)
   1093   {
   1094     CheatList* cl = System::GetCheatList();
   1095     for (const std::string& group : cl->GetCodeGroups())
   1096     {
   1097       QMenu* enabled_submenu = nullptr;
   1098       QMenu* apply_submenu = nullptr;
   1099 
   1100       for (u32 i = 0; i < cl->GetCodeCount(); i++)
   1101       {
   1102         CheatCode& cc = cl->GetCode(i);
   1103         if (cc.group != group)
   1104           continue;
   1105 
   1106         QString desc(QString::fromStdString(cc.description));
   1107         if (cc.IsManuallyActivated())
   1108         {
   1109           if (!apply_submenu)
   1110           {
   1111             apply_menu->setEnabled(true);
   1112             apply_submenu = apply_menu->addMenu(QString::fromStdString(group));
   1113           }
   1114 
   1115           QAction* action = apply_submenu->addAction(desc);
   1116           connect(action, &QAction::triggered, [i]() { g_emu_thread->applyCheat(i); });
   1117         }
   1118         else
   1119         {
   1120           if (!enabled_submenu)
   1121           {
   1122             enabled_menu->setEnabled(true);
   1123             enabled_submenu = enabled_menu->addMenu(QString::fromStdString(group));
   1124           }
   1125 
   1126           QAction* action = enabled_submenu->addAction(desc);
   1127           action->setCheckable(true);
   1128           action->setChecked(cc.enabled);
   1129           connect(action, &QAction::toggled, [i](bool enabled) { g_emu_thread->setCheatEnabled(i, enabled); });
   1130         }
   1131       }
   1132     }
   1133   }
   1134 }
   1135 
   1136 const GameList::Entry* MainWindow::resolveDiscSetEntry(const GameList::Entry* entry,
   1137                                                        std::unique_lock<std::recursive_mutex>& lock)
   1138 {
   1139   if (!entry || entry->type != GameList::EntryType::DiscSet)
   1140     return entry;
   1141 
   1142   // disc set... need to figure out the disc we want
   1143   SelectDiscDialog dlg(entry->path, this);
   1144 
   1145   lock.unlock();
   1146   const int res = dlg.exec();
   1147   lock.lock();
   1148 
   1149   return res ? GameList::GetEntryForPath(dlg.getSelectedDiscPath()) : nullptr;
   1150 }
   1151 
   1152 std::shared_ptr<SystemBootParameters> MainWindow::getSystemBootParameters(std::string file)
   1153 {
   1154   std::shared_ptr<SystemBootParameters> ret = std::make_shared<SystemBootParameters>(std::move(file));
   1155   ret->start_media_capture = m_ui.actionMediaCapture->isChecked();
   1156   return ret;
   1157 }
   1158 
   1159 std::optional<bool> MainWindow::promptForResumeState(const std::string& save_state_path)
   1160 {
   1161   FILESYSTEM_STAT_DATA sd;
   1162   if (save_state_path.empty() || !FileSystem::StatFile(save_state_path.c_str(), &sd))
   1163     return false;
   1164 
   1165   QMessageBox msgbox(this);
   1166   msgbox.setIcon(QMessageBox::Question);
   1167   msgbox.setWindowTitle(tr("Load Resume State"));
   1168   msgbox.setWindowModality(Qt::WindowModal);
   1169   msgbox.setText(tr("A resume save state was found for this game, saved at:\n\n%1.\n\nDo you want to load this state, "
   1170                     "or start from a fresh boot?")
   1171                    .arg(QDateTime::fromSecsSinceEpoch(sd.ModificationTime, Qt::UTC).toLocalTime().toString()));
   1172 
   1173   QPushButton* load = msgbox.addButton(tr("Load State"), QMessageBox::AcceptRole);
   1174   QPushButton* boot = msgbox.addButton(tr("Fresh Boot"), QMessageBox::RejectRole);
   1175   QPushButton* delboot = msgbox.addButton(tr("Delete And Boot"), QMessageBox::RejectRole);
   1176   msgbox.addButton(QMessageBox::Cancel);
   1177   msgbox.setDefaultButton(load);
   1178   msgbox.exec();
   1179 
   1180   QAbstractButton* clicked = msgbox.clickedButton();
   1181   if (load == clicked)
   1182   {
   1183     return true;
   1184   }
   1185   else if (boot == clicked)
   1186   {
   1187     return false;
   1188   }
   1189   else if (delboot == clicked)
   1190   {
   1191     if (!FileSystem::DeleteFile(save_state_path.c_str()))
   1192     {
   1193       QMessageBox::critical(this, tr("Error"),
   1194                             tr("Failed to delete save state file '%1'.").arg(QString::fromStdString(save_state_path)));
   1195     }
   1196 
   1197     return false;
   1198   }
   1199 
   1200   return std::nullopt;
   1201 }
   1202 
   1203 void MainWindow::startFile(std::string path, std::optional<std::string> save_path, std::optional<bool> fast_boot)
   1204 {
   1205   std::shared_ptr<SystemBootParameters> params = getSystemBootParameters(std::move(path));
   1206   params->override_fast_boot = fast_boot;
   1207   if (save_path.has_value())
   1208     params->save_state = std::move(save_path.value());
   1209 
   1210   g_emu_thread->bootSystem(std::move(params));
   1211 }
   1212 
   1213 void MainWindow::startFileOrChangeDisc(const QString& path)
   1214 {
   1215   if (s_system_valid)
   1216   {
   1217     // this is a disc change
   1218     promptForDiscChange(path);
   1219     return;
   1220   }
   1221 
   1222   // try to find the serial for the game
   1223   std::string path_str(path.toStdString());
   1224   std::string serial(GameDatabase::GetSerialForPath(path_str.c_str()));
   1225   std::optional<std::string> save_path;
   1226   if (!serial.empty())
   1227   {
   1228     std::string resume_path(System::GetGameSaveStateFileName(serial.c_str(), -1));
   1229     std::optional<bool> resume = promptForResumeState(resume_path);
   1230     if (!resume.has_value())
   1231     {
   1232       // cancelled
   1233       return;
   1234     }
   1235     else if (resume.value())
   1236       save_path = std::move(resume_path);
   1237   }
   1238 
   1239   // only resume if the option is enabled, and we have one for this game
   1240   startFile(std::move(path_str), std::move(save_path), std::nullopt);
   1241 }
   1242 
   1243 void MainWindow::promptForDiscChange(const QString& path)
   1244 {
   1245   SystemLock lock(pauseAndLockSystem());
   1246 
   1247   bool reset_system = false;
   1248   if (!m_was_disc_change_request)
   1249   {
   1250     QMessageBox mb(QMessageBox::Question, tr("Confirm Disc Change"),
   1251                    tr("Do you want to swap discs or boot the new image (via system reset)?"), QMessageBox::NoButton,
   1252                    this);
   1253     /*const QAbstractButton* const swap_button = */ mb.addButton(tr("Swap Disc"), QMessageBox::YesRole);
   1254     const QAbstractButton* const reset_button = mb.addButton(tr("Reset"), QMessageBox::NoRole);
   1255     const QAbstractButton* const cancel_button = mb.addButton(tr("Cancel"), QMessageBox::RejectRole);
   1256     mb.exec();
   1257 
   1258     const QAbstractButton* const clicked_button = mb.clickedButton();
   1259     if (!clicked_button || clicked_button == cancel_button)
   1260       return;
   1261 
   1262     reset_system = (clicked_button == reset_button);
   1263   }
   1264 
   1265   switchToEmulationView();
   1266 
   1267   g_emu_thread->changeDisc(path, reset_system, true);
   1268 }
   1269 
   1270 void MainWindow::onStartDiscActionTriggered()
   1271 {
   1272   std::string path(getDeviceDiscPath(tr("Start Disc")));
   1273   if (path.empty())
   1274     return;
   1275 
   1276   g_emu_thread->bootSystem(getSystemBootParameters(std::move(path)));
   1277 }
   1278 
   1279 void MainWindow::onStartBIOSActionTriggered()
   1280 {
   1281   g_emu_thread->bootSystem(getSystemBootParameters(std::string()));
   1282 }
   1283 
   1284 void MainWindow::onChangeDiscFromFileActionTriggered()
   1285 {
   1286   QString filename = QDir::toNativeSeparators(
   1287     QFileDialog::getOpenFileName(this, tr("Select Disc Image"), QString(), tr(DISC_IMAGE_FILTER), nullptr));
   1288   if (filename.isEmpty())
   1289     return;
   1290 
   1291   g_emu_thread->changeDisc(filename, false, true);
   1292 }
   1293 
   1294 void MainWindow::onChangeDiscFromGameListActionTriggered()
   1295 {
   1296   m_was_disc_change_request = true;
   1297   switchToGameListView();
   1298 }
   1299 
   1300 void MainWindow::onChangeDiscFromDeviceActionTriggered()
   1301 {
   1302   std::string path(getDeviceDiscPath(tr("Change Disc")));
   1303   if (path.empty())
   1304     return;
   1305 
   1306   g_emu_thread->changeDisc(QString::fromStdString(path), false, true);
   1307 }
   1308 
   1309 void MainWindow::onChangeDiscMenuAboutToShow()
   1310 {
   1311   populateChangeDiscSubImageMenu(m_ui.menuChangeDisc, m_ui.actionGroupChangeDiscSubImages);
   1312 }
   1313 
   1314 void MainWindow::onChangeDiscMenuAboutToHide()
   1315 {
   1316   for (QAction* action : m_ui.actionGroupChangeDiscSubImages->actions())
   1317   {
   1318     m_ui.actionGroupChangeDiscSubImages->removeAction(action);
   1319     m_ui.menuChangeDisc->removeAction(action);
   1320     action->deleteLater();
   1321   }
   1322 }
   1323 
   1324 void MainWindow::onLoadStateMenuAboutToShow()
   1325 {
   1326   populateLoadStateMenu(s_current_game_serial.toUtf8().constData(), m_ui.menuLoadState);
   1327 }
   1328 
   1329 void MainWindow::onSaveStateMenuAboutToShow()
   1330 {
   1331   populateSaveStateMenu(s_current_game_serial.toUtf8().constData(), m_ui.menuSaveState);
   1332 }
   1333 
   1334 void MainWindow::onStartFullscreenUITriggered()
   1335 {
   1336   if (m_display_widget)
   1337     g_emu_thread->stopFullscreenUI();
   1338   else
   1339     g_emu_thread->startFullscreenUI();
   1340 }
   1341 
   1342 void MainWindow::onFullscreenUIStateChange(bool running)
   1343 {
   1344   m_ui.actionStartFullscreenUI->setText(running ? tr("Stop Big Picture Mode") : tr("Start Big Picture Mode"));
   1345   m_ui.actionStartFullscreenUI2->setText(running ? tr("Exit Big Picture") : tr("Big Picture"));
   1346 }
   1347 
   1348 void MainWindow::onRemoveDiscActionTriggered()
   1349 {
   1350   g_emu_thread->changeDisc(QString(), false, true);
   1351 }
   1352 
   1353 void MainWindow::onViewToolbarActionToggled(bool checked)
   1354 {
   1355   Host::SetBaseBoolSettingValue("UI", "ShowToolbar", checked);
   1356   Host::CommitBaseSettingChanges();
   1357   m_ui.toolBar->setVisible(checked);
   1358 }
   1359 
   1360 void MainWindow::onViewLockToolbarActionToggled(bool checked)
   1361 {
   1362   Host::SetBaseBoolSettingValue("UI", "LockToolbar", checked);
   1363   Host::CommitBaseSettingChanges();
   1364   m_ui.toolBar->setMovable(!checked);
   1365 }
   1366 
   1367 void MainWindow::onViewStatusBarActionToggled(bool checked)
   1368 {
   1369   Host::SetBaseBoolSettingValue("UI", "ShowStatusBar", checked);
   1370   Host::CommitBaseSettingChanges();
   1371   m_ui.statusBar->setVisible(checked);
   1372 }
   1373 
   1374 void MainWindow::onViewGameListActionTriggered()
   1375 {
   1376   switchToGameListView();
   1377   m_game_list_widget->showGameList();
   1378 }
   1379 
   1380 void MainWindow::onViewGameGridActionTriggered()
   1381 {
   1382   switchToGameListView();
   1383   m_game_list_widget->showGameGrid();
   1384 }
   1385 
   1386 void MainWindow::onViewSystemDisplayTriggered()
   1387 {
   1388   if (m_display_created)
   1389     switchToEmulationView();
   1390 }
   1391 
   1392 void MainWindow::onViewGamePropertiesActionTriggered()
   1393 {
   1394   if (!s_system_valid)
   1395     return;
   1396 
   1397   Host::RunOnCPUThread([]() {
   1398     const std::string& path = System::GetDiscPath();
   1399     const std::string& serial = System::GetGameSerial();
   1400     if (path.empty() || serial.empty())
   1401       return;
   1402 
   1403     QtHost::RunOnUIThread([path = path, serial = serial]() {
   1404       SettingsWindow::openGamePropertiesDialog(path, System::GetGameTitle(), serial, System::GetDiscRegion());
   1405     });
   1406   });
   1407 }
   1408 
   1409 void MainWindow::onGitHubRepositoryActionTriggered()
   1410 {
   1411   QtUtils::OpenURL(this, "https://github.com/stenzek/duckstation/");
   1412 }
   1413 
   1414 void MainWindow::onIssueTrackerActionTriggered()
   1415 {
   1416   QtUtils::OpenURL(this, "https://www.duckstation.org/issues.html");
   1417 }
   1418 
   1419 void MainWindow::onDiscordServerActionTriggered()
   1420 {
   1421   QtUtils::OpenURL(this, "https://www.duckstation.org/discord.html");
   1422 }
   1423 
   1424 void MainWindow::onAboutActionTriggered()
   1425 {
   1426   AboutDialog about(this);
   1427   about.exec();
   1428 }
   1429 
   1430 void MainWindow::onGameListRefreshProgress(const QString& status, int current, int total)
   1431 {
   1432   m_ui.statusBar->showMessage(status);
   1433   setProgressBar(current, total);
   1434 }
   1435 
   1436 void MainWindow::onGameListRefreshComplete()
   1437 {
   1438   m_ui.statusBar->clearMessage();
   1439   clearProgressBar();
   1440 }
   1441 
   1442 void MainWindow::onGameListSelectionChanged()
   1443 {
   1444   auto lock = GameList::GetLock();
   1445   const GameList::Entry* entry = m_game_list_widget->getSelectedEntry();
   1446   if (!entry)
   1447     return;
   1448 
   1449   m_ui.statusBar->showMessage(QString::fromStdString(entry->path));
   1450 }
   1451 
   1452 void MainWindow::onGameListEntryActivated()
   1453 {
   1454   auto lock = GameList::GetLock();
   1455   const GameList::Entry* entry = resolveDiscSetEntry(m_game_list_widget->getSelectedEntry(), lock);
   1456   if (!entry)
   1457     return;
   1458 
   1459   if (s_system_valid)
   1460   {
   1461     // change disc on double click
   1462     if (!entry->IsDisc())
   1463     {
   1464       QMessageBox::critical(this, tr("Error"), tr("You must select a disc to change discs."));
   1465       return;
   1466     }
   1467 
   1468     promptForDiscChange(QString::fromStdString(entry->path));
   1469     return;
   1470   }
   1471 
   1472   std::optional<std::string> save_path;
   1473   if (!entry->serial.empty())
   1474   {
   1475     std::string resume_path(System::GetGameSaveStateFileName(entry->serial.c_str(), -1));
   1476     std::optional<bool> resume = promptForResumeState(resume_path);
   1477     if (!resume.has_value())
   1478     {
   1479       // cancelled
   1480       return;
   1481     }
   1482     else if (resume.value())
   1483       save_path = std::move(resume_path);
   1484   }
   1485 
   1486   // only resume if the option is enabled, and we have one for this game
   1487   startFile(entry->path, std::move(save_path), std::nullopt);
   1488 }
   1489 
   1490 void MainWindow::onGameListEntryContextMenuRequested(const QPoint& point)
   1491 {
   1492   auto lock = GameList::GetLock();
   1493   const GameList::Entry* entry = m_game_list_widget->getSelectedEntry();
   1494 
   1495   QMenu menu;
   1496 
   1497   // Hopefully this pointer doesn't disappear... it shouldn't.
   1498   if (entry)
   1499   {
   1500     if (!entry->IsDiscSet())
   1501     {
   1502       connect(menu.addAction(tr("Properties...")), &QAction::triggered, [entry]() {
   1503         SettingsWindow::openGamePropertiesDialog(entry->path, entry->title, entry->serial, entry->region);
   1504       });
   1505 
   1506       connect(menu.addAction(tr("Open Containing Directory...")), &QAction::triggered, [this, entry]() {
   1507         const QFileInfo fi(QString::fromStdString(entry->path));
   1508         QtUtils::OpenURL(this, QUrl::fromLocalFile(fi.absolutePath()));
   1509       });
   1510 
   1511       connect(menu.addAction(tr("Set Cover Image...")), &QAction::triggered,
   1512               [this, entry]() { setGameListEntryCoverImage(entry); });
   1513 
   1514       menu.addSeparator();
   1515 
   1516       if (!s_system_valid)
   1517       {
   1518         populateGameListContextMenu(entry, this, &menu);
   1519         menu.addSeparator();
   1520 
   1521         connect(menu.addAction(tr("Default Boot")), &QAction::triggered,
   1522                 [this, entry]() { g_emu_thread->bootSystem(getSystemBootParameters(entry->path)); });
   1523 
   1524         connect(menu.addAction(tr("Fast Boot")), &QAction::triggered, [this, entry]() {
   1525           std::shared_ptr<SystemBootParameters> boot_params = getSystemBootParameters(entry->path);
   1526           boot_params->override_fast_boot = true;
   1527           g_emu_thread->bootSystem(std::move(boot_params));
   1528         });
   1529 
   1530         connect(menu.addAction(tr("Full Boot")), &QAction::triggered, [this, entry]() {
   1531           std::shared_ptr<SystemBootParameters> boot_params = getSystemBootParameters(entry->path);
   1532           boot_params->override_fast_boot = false;
   1533           g_emu_thread->bootSystem(std::move(boot_params));
   1534         });
   1535 
   1536         if (m_ui.menuDebug->menuAction()->isVisible() && !Achievements::IsHardcoreModeActive())
   1537         {
   1538           connect(menu.addAction(tr("Boot and Debug")), &QAction::triggered, [this, entry]() {
   1539             m_open_debugger_on_start = true;
   1540 
   1541             std::shared_ptr<SystemBootParameters> boot_params = getSystemBootParameters(entry->path);
   1542             boot_params->override_start_paused = true;
   1543             g_emu_thread->bootSystem(std::move(boot_params));
   1544           });
   1545         }
   1546       }
   1547       else
   1548       {
   1549         connect(menu.addAction(tr("Change Disc")), &QAction::triggered, [this, entry]() {
   1550           g_emu_thread->changeDisc(QString::fromStdString(entry->path), false, true);
   1551           g_emu_thread->setSystemPaused(false);
   1552           switchToEmulationView();
   1553         });
   1554       }
   1555 
   1556       menu.addSeparator();
   1557 
   1558       connect(menu.addAction(tr("Exclude From List")), &QAction::triggered,
   1559               [this, entry]() { getSettingsWindow()->getGameListSettingsWidget()->addExcludedPath(entry->path); });
   1560 
   1561       connect(menu.addAction(tr("Reset Play Time")), &QAction::triggered,
   1562               [this, entry]() { clearGameListEntryPlayTime(entry); });
   1563     }
   1564     else
   1565     {
   1566       connect(menu.addAction(tr("Properties...")), &QAction::triggered, [disc_set_name = entry->path]() {
   1567         // resolve path first
   1568         auto lock = GameList::GetLock();
   1569         const GameList::Entry* first_disc = GameList::GetFirstDiscSetMember(disc_set_name);
   1570         if (first_disc)
   1571         {
   1572           SettingsWindow::openGamePropertiesDialog(first_disc->path, first_disc->title, first_disc->serial,
   1573                                                    first_disc->region);
   1574         }
   1575       });
   1576 
   1577       connect(menu.addAction(tr("Set Cover Image...")), &QAction::triggered,
   1578               [this, entry]() { setGameListEntryCoverImage(entry); });
   1579 
   1580       menu.addSeparator();
   1581 
   1582       populateGameListContextMenu(entry, this, &menu);
   1583 
   1584       menu.addSeparator();
   1585 
   1586       connect(menu.addAction(tr("Select Disc")), &QAction::triggered, this, &MainWindow::onGameListEntryActivated);
   1587 
   1588       menu.addSeparator();
   1589 
   1590       connect(menu.addAction(tr("Exclude From List")), &QAction::triggered,
   1591               [this, entry]() { getSettingsWindow()->getGameListSettingsWidget()->addExcludedPath(entry->path); });
   1592     }
   1593   }
   1594 
   1595   menu.addSeparator();
   1596 
   1597   connect(menu.addAction(tr("Add Search Directory...")), &QAction::triggered,
   1598           [this]() { getSettingsWindow()->getGameListSettingsWidget()->addSearchDirectory(this); });
   1599 
   1600   menu.exec(point);
   1601 }
   1602 
   1603 void MainWindow::setGameListEntryCoverImage(const GameList::Entry* entry)
   1604 {
   1605   const QString filename = QDir::toNativeSeparators(QFileDialog::getOpenFileName(
   1606     this, tr("Select Cover Image"), QString(), tr("All Cover Image Types (*.jpg *.jpeg *.png *.webp)")));
   1607   if (filename.isEmpty())
   1608     return;
   1609 
   1610   const QString old_filename = QString::fromStdString(GameList::GetCoverImagePathForEntry(entry));
   1611   const QString new_filename =
   1612     QString::fromStdString(GameList::GetNewCoverImagePathForEntry(entry, filename.toUtf8().constData(), false));
   1613   if (new_filename.isEmpty())
   1614     return;
   1615 
   1616   if (!old_filename.isEmpty())
   1617   {
   1618     if (QFileInfo(old_filename) == QFileInfo(filename))
   1619     {
   1620       QMessageBox::critical(this, tr("Copy Error"), tr("You must select a different file to the current cover image."));
   1621       return;
   1622     }
   1623 
   1624     if (QMessageBox::question(this, tr("Cover Already Exists"),
   1625                               tr("A cover image for this game already exists, do you wish to replace it?"),
   1626                               QMessageBox::Yes, QMessageBox::No) != QMessageBox::Yes)
   1627     {
   1628       return;
   1629     }
   1630   }
   1631 
   1632   if (QFile::exists(new_filename) && !QFile::remove(new_filename))
   1633   {
   1634     QMessageBox::critical(this, tr("Copy Error"), tr("Failed to remove existing cover '%1'").arg(new_filename));
   1635     return;
   1636   }
   1637   if (!QFile::copy(filename, new_filename))
   1638   {
   1639     QMessageBox::critical(this, tr("Copy Error"), tr("Failed to copy '%1' to '%2'").arg(filename).arg(new_filename));
   1640     return;
   1641   }
   1642   if (!old_filename.isEmpty() && old_filename != new_filename && !QFile::remove(old_filename))
   1643   {
   1644     QMessageBox::critical(this, tr("Copy Error"), tr("Failed to remove '%1'").arg(old_filename));
   1645     return;
   1646   }
   1647   m_game_list_widget->refreshGridCovers();
   1648 }
   1649 
   1650 void MainWindow::clearGameListEntryPlayTime(const GameList::Entry* entry)
   1651 {
   1652   if (QMessageBox::question(
   1653         this, tr("Confirm Reset"),
   1654         tr("Are you sure you want to reset the play time for '%1'?\n\nThis action cannot be undone.")
   1655           .arg(QString::fromStdString(entry->title))) != QMessageBox::Yes)
   1656   {
   1657     return;
   1658   }
   1659 
   1660   GameList::ClearPlayedTimeForSerial(entry->serial);
   1661   m_game_list_widget->refresh(false);
   1662 }
   1663 
   1664 void MainWindow::setupAdditionalUi()
   1665 {
   1666   const bool status_bar_visible = Host::GetBaseBoolSettingValue("UI", "ShowStatusBar", true);
   1667   m_ui.actionViewStatusBar->setChecked(status_bar_visible);
   1668   m_ui.statusBar->setVisible(status_bar_visible);
   1669 
   1670   const bool toolbar_visible = Host::GetBaseBoolSettingValue("UI", "ShowToolbar", false);
   1671   m_ui.actionViewToolbar->setChecked(toolbar_visible);
   1672   m_ui.toolBar->setVisible(toolbar_visible);
   1673 
   1674   const bool toolbars_locked = Host::GetBaseBoolSettingValue("UI", "LockToolbar", false);
   1675   m_ui.actionViewLockToolbar->setChecked(toolbars_locked);
   1676   m_ui.toolBar->setMovable(!toolbars_locked);
   1677   m_ui.toolBar->setContextMenuPolicy(Qt::PreventContextMenu);
   1678 
   1679   m_game_list_widget = new GameListWidget(getContentParent());
   1680   m_game_list_widget->initialize();
   1681   m_ui.actionGridViewShowTitles->setChecked(m_game_list_widget->isShowingGridCoverTitles());
   1682   m_ui.actionMergeDiscSets->setChecked(m_game_list_widget->isMergingDiscSets());
   1683   m_ui.actionShowGameIcons->setChecked(m_game_list_widget->isShowingGameIcons());
   1684   if (s_use_central_widget)
   1685   {
   1686     m_ui.mainContainer = nullptr; // setCentralWidget() will delete this
   1687     setCentralWidget(m_game_list_widget);
   1688   }
   1689   else
   1690   {
   1691     m_ui.mainContainer->addWidget(m_game_list_widget);
   1692   }
   1693 
   1694   m_status_progress_widget = new QProgressBar(m_ui.statusBar);
   1695   m_status_progress_widget->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed);
   1696   m_status_progress_widget->setFixedSize(140, 16);
   1697   m_status_progress_widget->setMinimum(0);
   1698   m_status_progress_widget->setMaximum(100);
   1699   m_status_progress_widget->hide();
   1700 
   1701   m_status_renderer_widget = new QLabel(m_ui.statusBar);
   1702   m_status_renderer_widget->setFixedHeight(16);
   1703   m_status_renderer_widget->setFixedSize(65, 16);
   1704   m_status_renderer_widget->hide();
   1705 
   1706   m_status_resolution_widget = new QLabel(m_ui.statusBar);
   1707   m_status_resolution_widget->setFixedHeight(16);
   1708   m_status_resolution_widget->setFixedSize(70, 16);
   1709   m_status_resolution_widget->hide();
   1710 
   1711   m_status_fps_widget = new QLabel(m_ui.statusBar);
   1712   m_status_fps_widget->setFixedSize(85, 16);
   1713   m_status_fps_widget->hide();
   1714 
   1715   m_status_vps_widget = new QLabel(m_ui.statusBar);
   1716   m_status_vps_widget->setFixedSize(125, 16);
   1717   m_status_vps_widget->hide();
   1718 
   1719   m_settings_toolbar_menu = new QMenu(m_ui.toolBar);
   1720   m_settings_toolbar_menu->addAction(m_ui.actionSettings);
   1721   m_settings_toolbar_menu->addAction(m_ui.actionViewGameProperties);
   1722 
   1723   m_ui.actionGridViewShowTitles->setChecked(m_game_list_widget->isShowingGridCoverTitles());
   1724 
   1725   updateDebugMenuVisibility();
   1726   updateCheatActionsVisibility();
   1727 
   1728   for (u32 i = 0; i < static_cast<u32>(CPUExecutionMode::Count); i++)
   1729   {
   1730     const CPUExecutionMode mode = static_cast<CPUExecutionMode>(i);
   1731     QAction* action =
   1732       m_ui.menuCPUExecutionMode->addAction(QString::fromUtf8(Settings::GetCPUExecutionModeDisplayName(mode)));
   1733     action->setCheckable(true);
   1734     connect(action, &QAction::triggered, [this, mode]() {
   1735       Host::SetBaseStringSettingValue("CPU", "ExecutionMode", Settings::GetCPUExecutionModeName(mode));
   1736       Host::CommitBaseSettingChanges();
   1737       g_emu_thread->applySettings();
   1738       updateDebugMenuCPUExecutionMode();
   1739     });
   1740   }
   1741   updateDebugMenuCPUExecutionMode();
   1742 
   1743   for (u32 i = 0; i < static_cast<u32>(GPURenderer::Count); i++)
   1744   {
   1745     const GPURenderer renderer = static_cast<GPURenderer>(i);
   1746     QAction* action = m_ui.menuRenderer->addAction(QString::fromUtf8(Settings::GetRendererDisplayName(renderer)));
   1747     action->setCheckable(true);
   1748     connect(action, &QAction::triggered, [this, renderer]() {
   1749       Host::SetBaseStringSettingValue("GPU", "Renderer", Settings::GetRendererName(renderer));
   1750       Host::CommitBaseSettingChanges();
   1751       g_emu_thread->applySettings();
   1752       updateDebugMenuGPURenderer();
   1753     });
   1754   }
   1755   updateDebugMenuGPURenderer();
   1756 
   1757   for (u32 i = 0; i < static_cast<u32>(DisplayCropMode::Count); i++)
   1758   {
   1759     const DisplayCropMode crop_mode = static_cast<DisplayCropMode>(i);
   1760     QAction* action =
   1761       m_ui.menuCropMode->addAction(QString::fromUtf8(Settings::GetDisplayCropModeDisplayName(crop_mode)));
   1762     action->setCheckable(true);
   1763     connect(action, &QAction::triggered, [this, crop_mode]() {
   1764       Host::SetBaseStringSettingValue("Display", "CropMode", Settings::GetDisplayCropModeName(crop_mode));
   1765       Host::CommitBaseSettingChanges();
   1766       g_emu_thread->applySettings();
   1767       updateDebugMenuCropMode();
   1768     });
   1769   }
   1770   updateDebugMenuCropMode();
   1771 
   1772   for (u32 scale = 1; scale <= 10; scale++)
   1773   {
   1774     QAction* action = m_ui.menuWindowSize->addAction(tr("%1x Scale").arg(scale));
   1775     connect(action, &QAction::triggered, [scale]() { g_emu_thread->requestDisplaySize(scale); });
   1776   }
   1777 
   1778 #ifdef ENABLE_RAINTEGRATION
   1779   if (Achievements::IsUsingRAIntegration())
   1780   {
   1781     QMenu* raMenu = new QMenu(QStringLiteral("RAIntegration"), m_ui.menu_Tools);
   1782     connect(raMenu, &QMenu::aboutToShow, this, [this, raMenu]() {
   1783       raMenu->clear();
   1784 
   1785       const auto items = Achievements::RAIntegration::GetMenuItems();
   1786       for (const auto& [id, title, checked] : items)
   1787       {
   1788         if (id == 0)
   1789         {
   1790           raMenu->addSeparator();
   1791           continue;
   1792         }
   1793 
   1794         QAction* raAction = raMenu->addAction(QString::fromUtf8(title));
   1795         if (checked)
   1796         {
   1797           raAction->setCheckable(true);
   1798           raAction->setChecked(checked);
   1799         }
   1800 
   1801         connect(raAction, &QAction::triggered, this,
   1802                 [id = id]() { Host::RunOnCPUThread([id]() { Achievements::RAIntegration::ActivateMenuItem(id); }); });
   1803       }
   1804     });
   1805     m_ui.menu_Tools->insertMenu(m_ui.actionOpenDataDirectory, raMenu);
   1806   }
   1807 #endif
   1808 }
   1809 
   1810 void MainWindow::updateEmulationActions(bool starting, bool running, bool cheevos_challenge_mode)
   1811 {
   1812   m_ui.actionStartFile->setDisabled(starting || running);
   1813   m_ui.actionStartDisc->setDisabled(starting || running);
   1814   m_ui.actionStartBios->setDisabled(starting || running);
   1815   m_ui.actionResumeLastState->setDisabled(starting || running || cheevos_challenge_mode);
   1816   m_ui.actionStartFullscreenUI->setDisabled(starting || running);
   1817   m_ui.actionStartFullscreenUI2->setDisabled(starting || running);
   1818 
   1819   m_ui.actionPowerOff->setDisabled(starting || !running);
   1820   m_ui.actionPowerOffWithoutSaving->setDisabled(starting || !running);
   1821   m_ui.actionReset->setDisabled(starting || !running);
   1822   m_ui.actionPause->setDisabled(starting || !running);
   1823   m_ui.actionChangeDisc->setDisabled(starting || !running);
   1824   m_ui.actionCheats->setDisabled(cheevos_challenge_mode);
   1825   m_ui.actionCheatsToolbar->setDisabled(cheevos_challenge_mode);
   1826   m_ui.actionScreenshot->setDisabled(starting || !running);
   1827   m_ui.menuChangeDisc->setDisabled(starting || !running);
   1828   m_ui.menuCheats->setDisabled(cheevos_challenge_mode);
   1829   m_ui.actionCPUDebugger->setDisabled(cheevos_challenge_mode);
   1830   m_ui.actionMemoryScanner->setDisabled(cheevos_challenge_mode);
   1831   m_ui.actionDumpRAM->setDisabled(starting || !running || cheevos_challenge_mode);
   1832   m_ui.actionDumpVRAM->setDisabled(starting || !running || cheevos_challenge_mode);
   1833   m_ui.actionDumpSPURAM->setDisabled(starting || !running || cheevos_challenge_mode);
   1834 
   1835   m_ui.actionSaveState->setDisabled(starting || !running);
   1836   m_ui.menuSaveState->setDisabled(starting || !running);
   1837   m_ui.menuWindowSize->setDisabled(starting || !running);
   1838 
   1839   m_ui.actionViewGameProperties->setDisabled(starting || !running);
   1840 
   1841   if (starting || running)
   1842   {
   1843     if (!m_ui.toolBar->actions().contains(m_ui.actionPowerOff))
   1844     {
   1845       m_ui.toolBar->insertAction(m_ui.actionResumeLastState, m_ui.actionPowerOff);
   1846       m_ui.toolBar->removeAction(m_ui.actionResumeLastState);
   1847     }
   1848   }
   1849   else
   1850   {
   1851     if (!m_ui.toolBar->actions().contains(m_ui.actionResumeLastState))
   1852     {
   1853       m_ui.toolBar->insertAction(m_ui.actionPowerOff, m_ui.actionResumeLastState);
   1854       m_ui.toolBar->removeAction(m_ui.actionPowerOff);
   1855     }
   1856 
   1857     m_ui.actionViewGameProperties->setEnabled(false);
   1858   }
   1859 
   1860   if (m_open_debugger_on_start && running)
   1861     openCPUDebugger();
   1862   if ((!starting && !running) || running)
   1863     m_open_debugger_on_start = false;
   1864 
   1865   m_ui.statusBar->clearMessage();
   1866 }
   1867 
   1868 void MainWindow::updateStatusBarWidgetVisibility()
   1869 {
   1870   auto Update = [this](QWidget* widget, bool visible, int stretch) {
   1871     if (widget->isVisible())
   1872     {
   1873       m_ui.statusBar->removeWidget(widget);
   1874       widget->hide();
   1875     }
   1876 
   1877     if (visible)
   1878     {
   1879       m_ui.statusBar->addPermanentWidget(widget, stretch);
   1880       widget->show();
   1881     }
   1882   };
   1883 
   1884   Update(m_status_renderer_widget, s_system_valid && !s_system_paused, 0);
   1885   Update(m_status_resolution_widget, s_system_valid && !s_system_paused, 0);
   1886   Update(m_status_fps_widget, s_system_valid && !s_system_paused, 0);
   1887   Update(m_status_vps_widget, s_system_valid && !s_system_paused, 0);
   1888 }
   1889 
   1890 void MainWindow::updateWindowTitle()
   1891 {
   1892   QString suffix(QtHost::GetAppConfigSuffix());
   1893   QString main_title(QtHost::GetAppNameAndVersion() + suffix);
   1894   QString display_title(s_current_game_title + suffix);
   1895 
   1896   if (!s_system_valid || s_current_game_title.isEmpty())
   1897     display_title = main_title;
   1898   else if (isRenderingToMain())
   1899     main_title = display_title;
   1900 
   1901   if (windowTitle() != main_title)
   1902     setWindowTitle(main_title);
   1903   setWindowIcon(s_current_game_icon.isNull() ? QtHost::GetAppIcon() : s_current_game_icon);
   1904 
   1905   if (m_display_widget && !isRenderingToMain())
   1906   {
   1907     QWidget* container =
   1908       m_display_container ? static_cast<QWidget*>(m_display_container) : static_cast<QWidget*>(m_display_widget);
   1909     if (container->windowTitle() != display_title)
   1910       container->setWindowTitle(display_title);
   1911     container->setWindowIcon(s_current_game_icon.isNull() ? QtHost::GetAppIcon() : s_current_game_icon);
   1912   }
   1913 
   1914   if (g_log_window)
   1915     g_log_window->updateWindowTitle();
   1916 }
   1917 
   1918 void MainWindow::updateWindowState(bool force_visible)
   1919 {
   1920   // Skip all of this when we're closing, since we don't want to make ourselves visible and cancel it.
   1921   if (m_is_closing)
   1922     return;
   1923 
   1924   const bool hide_window = !isRenderingToMain() && shouldHideMainWindow();
   1925   const bool disable_resize = Host::GetBoolSettingValue("Main", "DisableWindowResize", false);
   1926   const bool has_window = s_system_valid || m_display_widget;
   1927 
   1928   // Need to test both valid and display widget because of startup (vm invalid while window is created).
   1929   const bool visible = force_visible || !hide_window || !has_window;
   1930   if (isVisible() != visible)
   1931     setVisible(visible);
   1932 
   1933   // No point changing realizability if we're not visible.
   1934   const bool resizeable = force_visible || !disable_resize || !has_window;
   1935   if (visible)
   1936     QtUtils::SetWindowResizeable(this, resizeable);
   1937 
   1938   // Update the display widget too if rendering separately.
   1939   if (m_display_widget && !isRenderingToMain())
   1940     QtUtils::SetWindowResizeable(getDisplayContainer(), resizeable);
   1941 }
   1942 
   1943 void MainWindow::setProgressBar(int current, int total)
   1944 {
   1945   const int value = (total != 0) ? ((current * 100) / total) : 0;
   1946   if (m_status_progress_widget->value() != value)
   1947     m_status_progress_widget->setValue(value);
   1948 
   1949   if (m_status_progress_widget->isVisible())
   1950     return;
   1951 
   1952   m_status_progress_widget->show();
   1953   m_ui.statusBar->addPermanentWidget(m_status_progress_widget);
   1954 }
   1955 
   1956 void MainWindow::clearProgressBar()
   1957 {
   1958   if (!m_status_progress_widget->isVisible())
   1959     return;
   1960 
   1961   m_status_progress_widget->hide();
   1962   m_ui.statusBar->removeWidget(m_status_progress_widget);
   1963 }
   1964 
   1965 bool MainWindow::isShowingGameList() const
   1966 {
   1967   if (s_use_central_widget)
   1968     return (centralWidget() == m_game_list_widget);
   1969   else
   1970     return (m_ui.mainContainer->currentIndex() == 0);
   1971 }
   1972 
   1973 bool MainWindow::isRenderingFullscreen() const
   1974 {
   1975   if (!g_gpu_device || !m_display_widget)
   1976     return false;
   1977 
   1978   return getDisplayContainer()->isFullScreen();
   1979 }
   1980 
   1981 bool MainWindow::isRenderingToMain() const
   1982 {
   1983   if (s_use_central_widget)
   1984     return (m_display_widget && centralWidget() == m_display_widget);
   1985   else
   1986     return (m_display_widget && m_ui.mainContainer->indexOf(m_display_widget) == 1);
   1987 }
   1988 
   1989 bool MainWindow::shouldHideMouseCursor() const
   1990 {
   1991   return m_hide_mouse_cursor ||
   1992          (isRenderingFullscreen() && Host::GetBoolSettingValue("Main", "HideCursorInFullscreen", true));
   1993 }
   1994 
   1995 bool MainWindow::shouldHideMainWindow() const
   1996 {
   1997   return Host::GetBoolSettingValue("Main", "HideMainWindowWhenRunning", false) ||
   1998          (g_emu_thread->shouldRenderToMain() && !isRenderingToMain()) || QtHost::InNoGUIMode();
   1999 }
   2000 
   2001 void MainWindow::switchToGameListView()
   2002 {
   2003   if (!isShowingGameList())
   2004   {
   2005     if (m_display_created)
   2006     {
   2007       m_was_paused_on_surface_loss = s_system_paused;
   2008       if (!s_system_paused)
   2009         g_emu_thread->setSystemPaused(true);
   2010 
   2011       // switch to surfaceless. we have to wait until the display widget is gone before we swap over.
   2012       g_emu_thread->setSurfaceless(true);
   2013       while (m_display_widget)
   2014         QApplication::processEvents(QEventLoop::ExcludeUserInputEvents, 1);
   2015     }
   2016   }
   2017 
   2018   m_game_list_widget->setFocus();
   2019 }
   2020 
   2021 void MainWindow::switchToEmulationView()
   2022 {
   2023   if (!m_display_created || !isShowingGameList())
   2024     return;
   2025 
   2026   // we're no longer surfaceless! this will call back to UpdateDisplay(), which will swap the widget out.
   2027   g_emu_thread->setSurfaceless(false);
   2028 
   2029   // resume if we weren't paused at switch time
   2030   if (s_system_paused && !m_was_paused_on_surface_loss)
   2031     g_emu_thread->setSystemPaused(false);
   2032 
   2033   if (m_display_widget)
   2034     m_display_widget->setFocus();
   2035 }
   2036 
   2037 void MainWindow::connectSignals()
   2038 {
   2039   updateEmulationActions(false, false, Achievements::IsHardcoreModeActive());
   2040 
   2041   connect(qApp, &QGuiApplication::applicationStateChanged, this, &MainWindow::onApplicationStateChanged);
   2042 
   2043   connect(m_ui.actionStartFile, &QAction::triggered, this, &MainWindow::onStartFileActionTriggered);
   2044   connect(m_ui.actionStartDisc, &QAction::triggered, this, &MainWindow::onStartDiscActionTriggered);
   2045   connect(m_ui.actionStartBios, &QAction::triggered, this, &MainWindow::onStartBIOSActionTriggered);
   2046   connect(m_ui.actionResumeLastState, &QAction::triggered, g_emu_thread, &EmuThread::resumeSystemFromMostRecentState);
   2047   connect(m_ui.actionChangeDisc, &QAction::triggered, [this] { m_ui.menuChangeDisc->exec(QCursor::pos()); });
   2048   connect(m_ui.actionChangeDiscFromFile, &QAction::triggered, this, &MainWindow::onChangeDiscFromFileActionTriggered);
   2049   connect(m_ui.actionChangeDiscFromDevice, &QAction::triggered, this,
   2050           &MainWindow::onChangeDiscFromDeviceActionTriggered);
   2051   connect(m_ui.actionChangeDiscFromGameList, &QAction::triggered, this,
   2052           &MainWindow::onChangeDiscFromGameListActionTriggered);
   2053   connect(m_ui.menuChangeDisc, &QMenu::aboutToShow, this, &MainWindow::onChangeDiscMenuAboutToShow);
   2054   connect(m_ui.menuChangeDisc, &QMenu::aboutToHide, this, &MainWindow::onChangeDiscMenuAboutToHide);
   2055   connect(m_ui.menuLoadState, &QMenu::aboutToShow, this, &MainWindow::onLoadStateMenuAboutToShow);
   2056   connect(m_ui.menuSaveState, &QMenu::aboutToShow, this, &MainWindow::onSaveStateMenuAboutToShow);
   2057   connect(m_ui.menuCheats, &QMenu::aboutToShow, this, &MainWindow::onCheatsMenuAboutToShow);
   2058   connect(m_ui.actionCheats, &QAction::triggered, this, &MainWindow::onCheatsActionTriggered);
   2059   connect(m_ui.actionCheatsToolbar, &QAction::triggered, this, &MainWindow::onCheatsActionTriggered);
   2060   connect(m_ui.actionStartFullscreenUI, &QAction::triggered, this, &MainWindow::onStartFullscreenUITriggered);
   2061   connect(m_ui.actionStartFullscreenUI2, &QAction::triggered, this, &MainWindow::onStartFullscreenUITriggered);
   2062   connect(m_ui.actionRemoveDisc, &QAction::triggered, this, &MainWindow::onRemoveDiscActionTriggered);
   2063   connect(m_ui.actionAddGameDirectory, &QAction::triggered,
   2064           [this]() { getSettingsWindow()->getGameListSettingsWidget()->addSearchDirectory(this); });
   2065   connect(m_ui.actionPowerOff, &QAction::triggered, this,
   2066           [this]() { requestShutdown(true, true, g_settings.save_state_on_exit); });
   2067   connect(m_ui.actionPowerOffWithoutSaving, &QAction::triggered, this,
   2068           [this]() { requestShutdown(false, false, false); });
   2069   connect(m_ui.actionReset, &QAction::triggered, this, []() { g_emu_thread->resetSystem(true); });
   2070   connect(m_ui.actionPause, &QAction::toggled, this, [](bool active) { g_emu_thread->setSystemPaused(active); });
   2071   connect(m_ui.actionScreenshot, &QAction::triggered, g_emu_thread, &EmuThread::saveScreenshot);
   2072   connect(m_ui.actionScanForNewGames, &QAction::triggered, this, [this]() { refreshGameList(false); });
   2073   connect(m_ui.actionRescanAllGames, &QAction::triggered, this, [this]() { refreshGameList(true); });
   2074   connect(m_ui.actionLoadState, &QAction::triggered, this, [this]() { m_ui.menuLoadState->exec(QCursor::pos()); });
   2075   connect(m_ui.actionSaveState, &QAction::triggered, this, [this]() { m_ui.menuSaveState->exec(QCursor::pos()); });
   2076   connect(m_ui.actionExit, &QAction::triggered, this, &MainWindow::close);
   2077   connect(m_ui.actionFullscreen, &QAction::triggered, g_emu_thread, &EmuThread::toggleFullscreen);
   2078   connect(m_ui.actionSettings, &QAction::triggered, [this]() { doSettings(); });
   2079   connect(m_ui.actionSettings2, &QAction::triggered, this, &MainWindow::onSettingsTriggeredFromToolbar);
   2080   connect(m_ui.actionInterfaceSettings, &QAction::triggered, [this]() { doSettings("Interface"); });
   2081   connect(m_ui.actionBIOSSettings, &QAction::triggered, [this]() { doSettings("BIOS"); });
   2082   connect(m_ui.actionConsoleSettings, &QAction::triggered, [this]() { doSettings("Console"); });
   2083   connect(m_ui.actionEmulationSettings, &QAction::triggered, [this]() { doSettings("Emulation"); });
   2084   connect(m_ui.actionGameListSettings, &QAction::triggered, [this]() { doSettings("Game List"); });
   2085   connect(m_ui.actionHotkeySettings, &QAction::triggered,
   2086           [this]() { doControllerSettings(ControllerSettingsWindow::Category::HotkeySettings); });
   2087   connect(m_ui.actionControllerSettings, &QAction::triggered,
   2088           [this]() { doControllerSettings(ControllerSettingsWindow::Category::GlobalSettings); });
   2089   connect(m_ui.actionMemoryCardSettings, &QAction::triggered, [this]() { doSettings("Memory Cards"); });
   2090   connect(m_ui.actionGraphicsSettings, &QAction::triggered, [this]() { doSettings("Graphics"); });
   2091   connect(m_ui.actionPostProcessingSettings, &QAction::triggered, [this]() { doSettings("Post-Processing"); });
   2092   connect(m_ui.actionAudioSettings, &QAction::triggered, [this]() { doSettings("Audio"); });
   2093   connect(m_ui.actionAchievementSettings, &QAction::triggered, [this]() { doSettings("Achievements"); });
   2094   connect(m_ui.actionFolderSettings, &QAction::triggered, [this]() { doSettings("Folders"); });
   2095   connect(m_ui.actionAdvancedSettings, &QAction::triggered, [this]() { doSettings("Advanced"); });
   2096   connect(m_ui.actionViewToolbar, &QAction::toggled, this, &MainWindow::onViewToolbarActionToggled);
   2097   connect(m_ui.actionViewLockToolbar, &QAction::toggled, this, &MainWindow::onViewLockToolbarActionToggled);
   2098   connect(m_ui.actionViewStatusBar, &QAction::toggled, this, &MainWindow::onViewStatusBarActionToggled);
   2099   connect(m_ui.actionViewGameList, &QAction::triggered, this, &MainWindow::onViewGameListActionTriggered);
   2100   connect(m_ui.actionViewGameGrid, &QAction::triggered, this, &MainWindow::onViewGameGridActionTriggered);
   2101   connect(m_ui.actionViewSystemDisplay, &QAction::triggered, this, &MainWindow::onViewSystemDisplayTriggered);
   2102   connect(m_ui.actionViewGameProperties, &QAction::triggered, this, &MainWindow::onViewGamePropertiesActionTriggered);
   2103   connect(m_ui.actionGitHubRepository, &QAction::triggered, this, &MainWindow::onGitHubRepositoryActionTriggered);
   2104   connect(m_ui.actionIssueTracker, &QAction::triggered, this, &MainWindow::onIssueTrackerActionTriggered);
   2105   connect(m_ui.actionDiscordServer, &QAction::triggered, this, &MainWindow::onDiscordServerActionTriggered);
   2106   connect(m_ui.actionViewThirdPartyNotices, &QAction::triggered, this,
   2107           [this]() { AboutDialog::showThirdPartyNotices(this); });
   2108   connect(m_ui.actionAboutQt, &QAction::triggered, qApp, &QApplication::aboutQt);
   2109   connect(m_ui.actionAbout, &QAction::triggered, this, &MainWindow::onAboutActionTriggered);
   2110   connect(m_ui.actionCheckForUpdates, &QAction::triggered, this, &MainWindow::onCheckForUpdatesActionTriggered);
   2111   connect(m_ui.actionMemoryCardEditor, &QAction::triggered, this, &MainWindow::onToolsMemoryCardEditorTriggered);
   2112   connect(m_ui.actionMemoryScanner, &QAction::triggered, this, &MainWindow::onToolsMemoryScannerTriggered);
   2113   connect(m_ui.actionCoverDownloader, &QAction::triggered, this, &MainWindow::onToolsCoverDownloaderTriggered);
   2114   connect(m_ui.actionMediaCapture, &QAction::toggled, this, &MainWindow::onToolsMediaCaptureToggled);
   2115   connect(m_ui.actionCPUDebugger, &QAction::triggered, this, &MainWindow::openCPUDebugger);
   2116   SettingWidgetBinder::BindWidgetToBoolSetting(nullptr, m_ui.actionEnableGDBServer, "Debug", "EnableGDBServer", false);
   2117   connect(m_ui.actionOpenDataDirectory, &QAction::triggered, this, &MainWindow::onToolsOpenDataDirectoryTriggered);
   2118   connect(m_ui.actionMergeDiscSets, &QAction::triggered, m_game_list_widget, &GameListWidget::setMergeDiscSets);
   2119   connect(m_ui.actionShowGameIcons, &QAction::triggered, m_game_list_widget, &GameListWidget::setShowGameIcons);
   2120   connect(m_ui.actionGridViewShowTitles, &QAction::triggered, m_game_list_widget, &GameListWidget::setShowCoverTitles);
   2121   connect(m_ui.actionGridViewZoomIn, &QAction::triggered, m_game_list_widget, [this]() {
   2122     if (isShowingGameList())
   2123       m_game_list_widget->gridZoomIn();
   2124   });
   2125   connect(m_ui.actionGridViewZoomOut, &QAction::triggered, m_game_list_widget, [this]() {
   2126     if (isShowingGameList())
   2127       m_game_list_widget->gridZoomOut();
   2128   });
   2129   connect(m_ui.actionGridViewRefreshCovers, &QAction::triggered, m_game_list_widget,
   2130           &GameListWidget::refreshGridCovers);
   2131 
   2132   connect(g_emu_thread, &EmuThread::settingsResetToDefault, this, &MainWindow::onSettingsResetToDefault,
   2133           Qt::QueuedConnection);
   2134   connect(g_emu_thread, &EmuThread::errorReported, this, &MainWindow::reportError, Qt::BlockingQueuedConnection);
   2135   connect(g_emu_thread, &EmuThread::messageConfirmed, this, &MainWindow::confirmMessage, Qt::BlockingQueuedConnection);
   2136   connect(g_emu_thread, &EmuThread::statusMessage, this, &MainWindow::onStatusMessage);
   2137   connect(g_emu_thread, &EmuThread::onAcquireRenderWindowRequested, this, &MainWindow::acquireRenderWindow,
   2138           Qt::BlockingQueuedConnection);
   2139   connect(g_emu_thread, &EmuThread::onReleaseRenderWindowRequested, this, &MainWindow::releaseRenderWindow);
   2140   connect(g_emu_thread, &EmuThread::onResizeRenderWindowRequested, this, &MainWindow::displayResizeRequested,
   2141           Qt::BlockingQueuedConnection);
   2142   connect(g_emu_thread, &EmuThread::focusDisplayWidgetRequested, this, &MainWindow::focusDisplayWidget);
   2143   connect(g_emu_thread, &EmuThread::systemStarting, this, &MainWindow::onSystemStarting);
   2144   connect(g_emu_thread, &EmuThread::systemStarted, this, &MainWindow::onSystemStarted);
   2145   connect(g_emu_thread, &EmuThread::systemDestroyed, this, &MainWindow::onSystemDestroyed);
   2146   connect(g_emu_thread, &EmuThread::systemPaused, this, &MainWindow::onSystemPaused);
   2147   connect(g_emu_thread, &EmuThread::systemResumed, this, &MainWindow::onSystemResumed);
   2148   connect(g_emu_thread, &EmuThread::runningGameChanged, this, &MainWindow::onRunningGameChanged);
   2149   connect(g_emu_thread, &EmuThread::mediaCaptureStarted, this, &MainWindow::onMediaCaptureStarted);
   2150   connect(g_emu_thread, &EmuThread::mediaCaptureStopped, this, &MainWindow::onMediaCaptureStopped);
   2151   connect(g_emu_thread, &EmuThread::mouseModeRequested, this, &MainWindow::onMouseModeRequested);
   2152   connect(g_emu_thread, &EmuThread::fullscreenUIStateChange, this, &MainWindow::onFullscreenUIStateChange);
   2153   connect(g_emu_thread, &EmuThread::achievementsLoginRequested, this, &MainWindow::onAchievementsLoginRequested);
   2154   connect(g_emu_thread, &EmuThread::achievementsChallengeModeChanged, this,
   2155           &MainWindow::onAchievementsChallengeModeChanged);
   2156   connect(g_emu_thread, &EmuThread::onCoverDownloaderOpenRequested, this, &MainWindow::onToolsCoverDownloaderTriggered);
   2157 
   2158   // These need to be queued connections to stop crashing due to menus opening/closing and switching focus.
   2159   connect(m_game_list_widget, &GameListWidget::refreshProgress, this, &MainWindow::onGameListRefreshProgress);
   2160   connect(m_game_list_widget, &GameListWidget::refreshComplete, this, &MainWindow::onGameListRefreshComplete);
   2161   connect(m_game_list_widget, &GameListWidget::selectionChanged, this, &MainWindow::onGameListSelectionChanged,
   2162           Qt::QueuedConnection);
   2163   connect(m_game_list_widget, &GameListWidget::entryActivated, this, &MainWindow::onGameListEntryActivated,
   2164           Qt::QueuedConnection);
   2165   connect(m_game_list_widget, &GameListWidget::entryContextMenuRequested, this,
   2166           &MainWindow::onGameListEntryContextMenuRequested, Qt::QueuedConnection);
   2167   connect(m_game_list_widget, &GameListWidget::addGameDirectoryRequested, this,
   2168           [this]() { getSettingsWindow()->getGameListSettingsWidget()->addSearchDirectory(this); });
   2169 
   2170   SettingWidgetBinder::BindWidgetToBoolSetting(nullptr, m_ui.actionDisableAllEnhancements, "Main",
   2171                                                "DisableAllEnhancements", false);
   2172   SettingWidgetBinder::BindWidgetToBoolSetting(nullptr, m_ui.actionDebugDumpCPUtoVRAMCopies, "Debug",
   2173                                                "DumpCPUToVRAMCopies", false);
   2174   SettingWidgetBinder::BindWidgetToBoolSetting(nullptr, m_ui.actionDebugDumpVRAMtoCPUCopies, "Debug",
   2175                                                "DumpVRAMToCPUCopies", false);
   2176   connect(m_ui.actionDumpRAM, &QAction::triggered, [this]() {
   2177     const QString filename = QDir::toNativeSeparators(
   2178       QFileDialog::getSaveFileName(this, tr("Destination File"), QString(), tr("Binary Files (*.bin)")));
   2179     if (filename.isEmpty())
   2180       return;
   2181 
   2182     g_emu_thread->dumpRAM(filename);
   2183   });
   2184   connect(m_ui.actionDumpVRAM, &QAction::triggered, [this]() {
   2185     const QString filename = QDir::toNativeSeparators(QFileDialog::getSaveFileName(
   2186       this, tr("Destination File"), QString(), tr("Binary Files (*.bin);;PNG Images (*.png)")));
   2187     if (filename.isEmpty())
   2188       return;
   2189 
   2190     g_emu_thread->dumpVRAM(filename);
   2191   });
   2192   connect(m_ui.actionDumpSPURAM, &QAction::triggered, [this]() {
   2193     const QString filename = QDir::toNativeSeparators(
   2194       QFileDialog::getSaveFileName(this, tr("Destination File"), QString(), tr("Binary Files (*.bin)")));
   2195     if (filename.isEmpty())
   2196       return;
   2197 
   2198     g_emu_thread->dumpSPURAM(filename);
   2199   });
   2200   SettingWidgetBinder::BindWidgetToBoolSetting(nullptr, m_ui.actionDebugShowVRAM, "Debug", "ShowVRAM", false);
   2201   SettingWidgetBinder::BindWidgetToBoolSetting(nullptr, m_ui.actionDebugShowGPUState, "Debug", "ShowGPUState", false);
   2202   SettingWidgetBinder::BindWidgetToBoolSetting(nullptr, m_ui.actionDebugShowCDROMState, "Debug", "ShowCDROMState",
   2203                                                false);
   2204   SettingWidgetBinder::BindWidgetToBoolSetting(nullptr, m_ui.actionDebugShowSPUState, "Debug", "ShowSPUState", false);
   2205   SettingWidgetBinder::BindWidgetToBoolSetting(nullptr, m_ui.actionDebugShowTimersState, "Debug", "ShowTimersState",
   2206                                                false);
   2207   SettingWidgetBinder::BindWidgetToBoolSetting(nullptr, m_ui.actionDebugShowMDECState, "Debug", "ShowMDECState", false);
   2208   SettingWidgetBinder::BindWidgetToBoolSetting(nullptr, m_ui.actionDebugShowDMAState, "Debug", "ShowDMAState", false);
   2209 }
   2210 
   2211 void MainWindow::updateTheme()
   2212 {
   2213   QtHost::UpdateApplicationTheme();
   2214   reloadThemeSpecificImages();
   2215 }
   2216 
   2217 void MainWindow::reloadThemeSpecificImages()
   2218 {
   2219   m_game_list_widget->reloadThemeSpecificImages();
   2220 }
   2221 
   2222 void MainWindow::onSettingsThemeChanged()
   2223 {
   2224 #ifdef _WIN32
   2225   const QString old_style_name = qApp->style()->name();
   2226 #endif
   2227 
   2228   updateTheme();
   2229 
   2230 #ifdef _WIN32
   2231   // Work around a bug where the background colour of menus is broken when changing to/from the windowsvista theme.
   2232   const QString new_style_name = qApp->style()->name();
   2233   if ((old_style_name == QStringLiteral("windowsvista")) != (new_style_name == QStringLiteral("windowsvista")))
   2234     recreate();
   2235 #endif
   2236 }
   2237 
   2238 void MainWindow::onSettingsResetToDefault(bool system, bool controller)
   2239 {
   2240   if (system && m_settings_window)
   2241   {
   2242     const bool had_settings_window = m_settings_window->isVisible();
   2243     m_settings_window->close();
   2244     m_settings_window->deleteLater();
   2245     m_settings_window = nullptr;
   2246 
   2247     if (had_settings_window)
   2248       doSettings();
   2249   }
   2250 
   2251   if (controller && m_controller_settings_window)
   2252   {
   2253     const bool had_controller_settings_window = m_controller_settings_window->isVisible();
   2254     m_controller_settings_window->close();
   2255     m_controller_settings_window->deleteLater();
   2256     m_controller_settings_window = nullptr;
   2257 
   2258     if (had_controller_settings_window)
   2259       doControllerSettings(ControllerSettingsWindow::Category::GlobalSettings);
   2260   }
   2261 
   2262   updateDebugMenuCPUExecutionMode();
   2263   updateDebugMenuGPURenderer();
   2264   updateDebugMenuCropMode();
   2265   updateDebugMenuVisibility();
   2266 }
   2267 
   2268 void MainWindow::saveStateToConfig()
   2269 {
   2270   if (!isVisible() || ((windowState() & Qt::WindowFullScreen) != Qt::WindowNoState))
   2271     return;
   2272 
   2273   bool changed = false;
   2274 
   2275   const QByteArray geometry(saveGeometry());
   2276   const QByteArray geometry_b64(geometry.toBase64());
   2277   const std::string old_geometry_b64(Host::GetBaseStringSettingValue("UI", "MainWindowGeometry"));
   2278   if (old_geometry_b64 != geometry_b64.constData())
   2279   {
   2280     Host::SetBaseStringSettingValue("UI", "MainWindowGeometry", geometry_b64.constData());
   2281     changed = true;
   2282   }
   2283 
   2284   const QByteArray state(saveState());
   2285   const QByteArray state_b64(state.toBase64());
   2286   const std::string old_state_b64(Host::GetBaseStringSettingValue("UI", "MainWindowState"));
   2287   if (old_state_b64 != state_b64.constData())
   2288   {
   2289     Host::SetBaseStringSettingValue("UI", "MainWindowState", state_b64.constData());
   2290     changed = true;
   2291   }
   2292 
   2293   if (changed)
   2294     Host::CommitBaseSettingChanges();
   2295 }
   2296 
   2297 void MainWindow::restoreStateFromConfig()
   2298 {
   2299   {
   2300     const std::string geometry_b64 = Host::GetBaseStringSettingValue("UI", "MainWindowGeometry");
   2301     const QByteArray geometry = QByteArray::fromBase64(QByteArray::fromStdString(geometry_b64));
   2302     if (!geometry.isEmpty())
   2303       restoreGeometry(geometry);
   2304   }
   2305 
   2306   {
   2307     const std::string state_b64 = Host::GetBaseStringSettingValue("UI", "MainWindowState");
   2308     const QByteArray state = QByteArray::fromBase64(QByteArray::fromStdString(state_b64));
   2309     if (!state.isEmpty())
   2310     {
   2311       restoreState(state);
   2312 
   2313       // make sure we're not loading a dodgy config which had fullscreen set...
   2314       setWindowState(windowState() & ~(Qt::WindowFullScreen | Qt::WindowActive));
   2315     }
   2316 
   2317     {
   2318       QSignalBlocker sb(m_ui.actionViewToolbar);
   2319       m_ui.actionViewToolbar->setChecked(!m_ui.toolBar->isHidden());
   2320     }
   2321     {
   2322       QSignalBlocker sb(m_ui.actionViewStatusBar);
   2323       m_ui.actionViewStatusBar->setChecked(!m_ui.statusBar->isHidden());
   2324     }
   2325   }
   2326 }
   2327 
   2328 void MainWindow::saveDisplayWindowGeometryToConfig()
   2329 {
   2330   QWidget* container = getDisplayContainer();
   2331   if (container->windowState() & Qt::WindowFullScreen)
   2332   {
   2333     // if we somehow ended up here, don't save the fullscreen state to the config
   2334     return;
   2335   }
   2336 
   2337   const QByteArray geometry = container->saveGeometry();
   2338   const QByteArray geometry_b64 = geometry.toBase64();
   2339   const std::string old_geometry_b64 = Host::GetBaseStringSettingValue("UI", "DisplayWindowGeometry");
   2340   if (old_geometry_b64 != geometry_b64.constData())
   2341   {
   2342     Host::SetBaseStringSettingValue("UI", "DisplayWindowGeometry", geometry_b64.constData());
   2343     Host::CommitBaseSettingChanges();
   2344   }
   2345 }
   2346 
   2347 void MainWindow::restoreDisplayWindowGeometryFromConfig()
   2348 {
   2349   const std::string geometry_b64 = Host::GetBaseStringSettingValue("UI", "DisplayWindowGeometry");
   2350   const QByteArray geometry = QByteArray::fromBase64(QByteArray::fromStdString(geometry_b64));
   2351   QWidget* container = getDisplayContainer();
   2352   if (!geometry.isEmpty())
   2353   {
   2354     container->restoreGeometry(geometry);
   2355 
   2356     // make sure we're not loading a dodgy config which had fullscreen set...
   2357     container->setWindowState(container->windowState() & ~(Qt::WindowFullScreen | Qt::WindowActive));
   2358   }
   2359   else
   2360   {
   2361     // default size
   2362     container->resize(640, 480);
   2363   }
   2364 }
   2365 
   2366 SettingsWindow* MainWindow::getSettingsWindow()
   2367 {
   2368   if (!m_settings_window)
   2369   {
   2370     m_settings_window = new SettingsWindow();
   2371     connect(m_settings_window->getInterfaceSettingsWidget(), &InterfaceSettingsWidget::themeChanged, this,
   2372             &MainWindow::onSettingsThemeChanged);
   2373   }
   2374 
   2375   return m_settings_window;
   2376 }
   2377 
   2378 void MainWindow::doSettings(const char* category /* = nullptr */)
   2379 {
   2380   SettingsWindow* dlg = getSettingsWindow();
   2381   QtUtils::ShowOrRaiseWindow(dlg);
   2382   if (category)
   2383     dlg->setCategory(category);
   2384 }
   2385 
   2386 ControllerSettingsWindow* MainWindow::getControllerSettingsWindow()
   2387 {
   2388   if (!m_controller_settings_window)
   2389     m_controller_settings_window = new ControllerSettingsWindow();
   2390 
   2391   return m_controller_settings_window;
   2392 }
   2393 
   2394 void MainWindow::doControllerSettings(
   2395   ControllerSettingsWindow::Category category /*= ControllerSettingsDialog::Category::Count*/)
   2396 {
   2397   ControllerSettingsWindow* dlg = getControllerSettingsWindow();
   2398   QtUtils::ShowOrRaiseWindow(dlg);
   2399   if (category != ControllerSettingsWindow::Category::Count)
   2400     dlg->setCategory(category);
   2401 }
   2402 
   2403 void MainWindow::openInputProfileEditor(const std::string_view name)
   2404 {
   2405   ControllerSettingsWindow* dlg = getControllerSettingsWindow();
   2406   QtUtils::ShowOrRaiseWindow(dlg);
   2407   dlg->switchProfile(name);
   2408 }
   2409 
   2410 void MainWindow::updateDebugMenuCPUExecutionMode()
   2411 {
   2412   std::optional<CPUExecutionMode> current_mode =
   2413     Settings::ParseCPUExecutionMode(Host::GetBaseStringSettingValue("CPU", "ExecutionMode").c_str());
   2414   if (!current_mode.has_value())
   2415     return;
   2416 
   2417   const QString current_mode_display_name =
   2418     QString::fromUtf8(Settings::GetCPUExecutionModeDisplayName(current_mode.value()));
   2419   for (QObject* obj : m_ui.menuCPUExecutionMode->children())
   2420   {
   2421     QAction* action = qobject_cast<QAction*>(obj);
   2422     if (action)
   2423       action->setChecked(action->text() == current_mode_display_name);
   2424   }
   2425 }
   2426 
   2427 void MainWindow::updateDebugMenuGPURenderer()
   2428 {
   2429   // update the menu with the new selected renderer
   2430   std::optional<GPURenderer> current_renderer =
   2431     Settings::ParseRendererName(Host::GetBaseStringSettingValue("GPU", "Renderer").c_str());
   2432   if (!current_renderer.has_value())
   2433     return;
   2434 
   2435   const QString current_renderer_display_name =
   2436     QString::fromUtf8(Settings::GetRendererDisplayName(current_renderer.value()));
   2437   for (QObject* obj : m_ui.menuRenderer->children())
   2438   {
   2439     QAction* action = qobject_cast<QAction*>(obj);
   2440     if (action)
   2441       action->setChecked(action->text() == current_renderer_display_name);
   2442   }
   2443 }
   2444 
   2445 void MainWindow::updateDebugMenuCropMode()
   2446 {
   2447   std::optional<DisplayCropMode> current_crop_mode =
   2448     Settings::ParseDisplayCropMode(Host::GetBaseStringSettingValue("Display", "CropMode").c_str());
   2449   if (!current_crop_mode.has_value())
   2450     return;
   2451 
   2452   const QString current_crop_mode_display_name =
   2453     QString::fromUtf8(Settings::GetDisplayCropModeDisplayName(current_crop_mode.value()));
   2454   for (QObject* obj : m_ui.menuCropMode->children())
   2455   {
   2456     QAction* action = qobject_cast<QAction*>(obj);
   2457     if (action)
   2458       action->setChecked(action->text() == current_crop_mode_display_name);
   2459   }
   2460 }
   2461 
   2462 void MainWindow::showEvent(QShowEvent* event)
   2463 {
   2464   QMainWindow::showEvent(event);
   2465 
   2466   // This is a bit silly, but for some reason resizing *before* the window is shown
   2467   // gives the incorrect sizes for columns, if you set the style before setting up
   2468   // the rest of the window... so, instead, let's just force it to be resized on show.
   2469   if (isShowingGameList())
   2470     m_game_list_widget->resizeTableViewColumnsToFit();
   2471 }
   2472 
   2473 void MainWindow::closeEvent(QCloseEvent* event)
   2474 {
   2475   // If there's no VM, we can just exit as normal.
   2476   if (!s_system_valid || !m_display_created)
   2477   {
   2478     saveStateToConfig();
   2479     if (m_display_created)
   2480       g_emu_thread->stopFullscreenUI();
   2481     destroySubWindows();
   2482     QMainWindow::closeEvent(event);
   2483     return;
   2484   }
   2485 
   2486   // But if there is, we have to cancel the action, regardless of whether we ended exiting
   2487   // or not. The window still needs to be visible while GS is shutting down.
   2488   event->ignore();
   2489 
   2490   // Exit cancelled?
   2491   if (!requestShutdown(true, true, g_settings.save_state_on_exit))
   2492     return;
   2493 
   2494   // Application will be exited in VM stopped handler.
   2495   saveStateToConfig();
   2496   m_is_closing = true;
   2497 }
   2498 
   2499 void MainWindow::changeEvent(QEvent* event)
   2500 {
   2501   if (static_cast<QWindowStateChangeEvent*>(event)->oldState() & Qt::WindowMinimized)
   2502   {
   2503     // TODO: This should check the render-to-main option.
   2504     if (m_display_widget)
   2505       g_emu_thread->redrawDisplayWindow();
   2506   }
   2507 
   2508   if (event->type() == QEvent::StyleChange)
   2509   {
   2510     QtHost::SetIconThemeFromStyle();
   2511     reloadThemeSpecificImages();
   2512   }
   2513 
   2514   QMainWindow::changeEvent(event);
   2515 }
   2516 
   2517 static QString getFilenameFromMimeData(const QMimeData* md)
   2518 {
   2519   QString filename;
   2520   if (md->hasUrls())
   2521   {
   2522     // only one url accepted
   2523     const QList<QUrl> urls(md->urls());
   2524     if (urls.size() == 1)
   2525       filename = QDir::toNativeSeparators(urls.front().toLocalFile());
   2526   }
   2527 
   2528   return filename;
   2529 }
   2530 
   2531 void MainWindow::dragEnterEvent(QDragEnterEvent* event)
   2532 {
   2533   const std::string filename(getFilenameFromMimeData(event->mimeData()).toStdString());
   2534   if (!System::IsLoadableFilename(filename) && !System::IsSaveStateFilename(filename))
   2535     return;
   2536 
   2537   event->acceptProposedAction();
   2538 }
   2539 
   2540 void MainWindow::dropEvent(QDropEvent* event)
   2541 {
   2542   const QString qfilename(getFilenameFromMimeData(event->mimeData()));
   2543   const std::string filename(qfilename.toStdString());
   2544   if (!System::IsLoadableFilename(filename) && !System::IsSaveStateFilename(filename))
   2545     return;
   2546 
   2547   event->acceptProposedAction();
   2548 
   2549   if (System::IsSaveStateFilename(filename))
   2550   {
   2551     g_emu_thread->loadState(qfilename);
   2552     return;
   2553   }
   2554 
   2555   if (s_system_valid)
   2556     promptForDiscChange(qfilename);
   2557   else
   2558     startFileOrChangeDisc(qfilename);
   2559 }
   2560 
   2561 void MainWindow::moveEvent(QMoveEvent* event)
   2562 {
   2563   QMainWindow::moveEvent(event);
   2564 
   2565   if (g_log_window && g_log_window->isAttachedToMainWindow())
   2566     g_log_window->reattachToMainWindow();
   2567 }
   2568 
   2569 void MainWindow::resizeEvent(QResizeEvent* event)
   2570 {
   2571   QMainWindow::resizeEvent(event);
   2572 
   2573   if (g_log_window && g_log_window->isAttachedToMainWindow())
   2574     g_log_window->reattachToMainWindow();
   2575 }
   2576 
   2577 void MainWindow::startupUpdateCheck()
   2578 {
   2579   if (!Host::GetBaseBoolSettingValue("AutoUpdater", "CheckAtStartup", true))
   2580     return;
   2581 
   2582   checkForUpdates(false);
   2583 }
   2584 
   2585 void MainWindow::updateDebugMenuVisibility()
   2586 {
   2587   const bool visible = QtHost::ShouldShowDebugOptions();
   2588   m_ui.menuDebug->menuAction()->setVisible(visible);
   2589 }
   2590 
   2591 void MainWindow::refreshGameList(bool invalidate_cache)
   2592 {
   2593   m_game_list_widget->refresh(invalidate_cache);
   2594 }
   2595 
   2596 void MainWindow::refreshGameListModel()
   2597 {
   2598   m_game_list_widget->refreshModel();
   2599 }
   2600 
   2601 void MainWindow::cancelGameListRefresh()
   2602 {
   2603   m_game_list_widget->cancelRefresh();
   2604 }
   2605 
   2606 void MainWindow::runOnUIThread(const std::function<void()>& func)
   2607 {
   2608   func();
   2609 }
   2610 
   2611 bool MainWindow::requestShutdown(bool allow_confirm /* = true */, bool allow_save_to_state /* = true */,
   2612                                  bool save_state /* = true */)
   2613 {
   2614   if (!s_system_valid)
   2615     return true;
   2616 
   2617   // If we don't have a serial, we can't save state.
   2618   allow_save_to_state &= !s_current_game_serial.isEmpty();
   2619   save_state &= allow_save_to_state;
   2620 
   2621   // Only confirm on UI thread because we need to display a msgbox.
   2622   if (!m_is_closing && allow_confirm && Host::GetBoolSettingValue("Main", "ConfirmPowerOff", true))
   2623   {
   2624     SystemLock lock(pauseAndLockSystem());
   2625 
   2626     QMessageBox msgbox(lock.getDialogParent());
   2627     msgbox.setIcon(QMessageBox::Question);
   2628     msgbox.setWindowTitle(tr("Confirm Shutdown"));
   2629     msgbox.setWindowModality(Qt::WindowModal);
   2630     msgbox.setText(tr("Are you sure you want to shut down the virtual machine?"));
   2631 
   2632     QCheckBox* save_cb = new QCheckBox(tr("Save State For Resume"), &msgbox);
   2633     save_cb->setChecked(allow_save_to_state && save_state);
   2634     save_cb->setEnabled(allow_save_to_state);
   2635     msgbox.setCheckBox(save_cb);
   2636     msgbox.addButton(QMessageBox::Yes);
   2637     msgbox.addButton(QMessageBox::No);
   2638     msgbox.setDefaultButton(QMessageBox::Yes);
   2639     if (msgbox.exec() != QMessageBox::Yes)
   2640       return false;
   2641 
   2642     save_state = save_cb->isChecked();
   2643 
   2644     // Don't switch back to fullscreen when we're shutting down anyway.
   2645     lock.cancelResume();
   2646   }
   2647 
   2648   // This is a little bit annoying. Qt will close everything down if we don't have at least one window visible,
   2649   // but we might not be visible because the user is using render-to-separate and hide. We don't want to always
   2650   // reshow the main window during display updates, because otherwise fullscreen transitions and renderer switches
   2651   // would briefly show and then hide the main window. So instead, we do it on shutdown, here. Except if we're in
   2652   // batch mode, when we're going to exit anyway.
   2653   if (!isRenderingToMain() && isHidden() && !QtHost::InBatchMode() && !g_emu_thread->isRunningFullscreenUI())
   2654     updateWindowState(true);
   2655 
   2656   // Now we can actually shut down the VM.
   2657   g_emu_thread->shutdownSystem(save_state, true);
   2658   return true;
   2659 }
   2660 
   2661 void MainWindow::requestExit(bool allow_confirm /* = true */)
   2662 {
   2663   // this is block, because otherwise closeEvent() will also prompt
   2664   if (!requestShutdown(allow_confirm, true, g_settings.save_state_on_exit))
   2665     return;
   2666 
   2667   // VM stopped signal won't have fired yet, so queue an exit if we still have one.
   2668   // Otherwise, immediately exit, because there's no VM to exit us later.
   2669   if (s_system_valid)
   2670     m_is_closing = true;
   2671   else
   2672     quit();
   2673 }
   2674 
   2675 void MainWindow::checkForSettingChanges()
   2676 {
   2677   LogWindow::updateSettings();
   2678   updateWindowState();
   2679   updateCheatActionsVisibility();
   2680 }
   2681 
   2682 std::optional<WindowInfo> MainWindow::getWindowInfo()
   2683 {
   2684   if (!m_display_widget || isRenderingToMain())
   2685     return QtUtils::GetWindowInfoForWidget(this);
   2686   else if (QWidget* widget = getDisplayContainer())
   2687     return QtUtils::GetWindowInfoForWidget(widget);
   2688   else
   2689     return std::nullopt;
   2690 }
   2691 
   2692 void MainWindow::onCheckForUpdatesActionTriggered()
   2693 {
   2694   // Wipe out the last version, that way it displays the update if we've previously skipped it.
   2695   Host::DeleteBaseSettingValue("AutoUpdater", "LastVersion");
   2696   Host::CommitBaseSettingChanges();
   2697   checkForUpdates(true);
   2698 }
   2699 
   2700 void MainWindow::openMemoryCardEditor(const QString& card_a_path, const QString& card_b_path)
   2701 {
   2702   for (const QString& card_path : {card_a_path, card_b_path})
   2703   {
   2704     if (!card_path.isEmpty() && !QFile::exists(card_path))
   2705     {
   2706       if (QMessageBox::question(
   2707             this, tr("Memory Card Not Found"),
   2708             tr("Memory card '%1' does not exist. Do you want to create an empty memory card?").arg(card_path),
   2709             QMessageBox::Yes, QMessageBox::No) == QMessageBox::Yes)
   2710       {
   2711         Error error;
   2712         if (!MemoryCardEditorWindow::createMemoryCard(card_path, &error))
   2713         {
   2714           QMessageBox::critical(this, tr("Memory Card Not Found"),
   2715                                 tr("Failed to create memory card '%1': %2")
   2716                                   .arg(card_path)
   2717                                   .arg(QString::fromStdString(error.GetDescription())));
   2718         }
   2719       }
   2720     }
   2721   }
   2722 
   2723   if (!m_memory_card_editor_window)
   2724     m_memory_card_editor_window = new MemoryCardEditorWindow();
   2725 
   2726   QtUtils::ShowOrRaiseWindow(m_memory_card_editor_window);
   2727 
   2728   if (!card_a_path.isEmpty())
   2729   {
   2730     if (!m_memory_card_editor_window->setCardA(card_a_path))
   2731     {
   2732       QMessageBox::critical(
   2733         this, tr("Memory Card Not Found"),
   2734         tr("Memory card '%1' could not be found. Try starting the game and saving to create it.").arg(card_a_path));
   2735     }
   2736   }
   2737   if (!card_b_path.isEmpty())
   2738   {
   2739     if (!m_memory_card_editor_window->setCardB(card_b_path))
   2740     {
   2741       QMessageBox::critical(
   2742         this, tr("Memory Card Not Found"),
   2743         tr("Memory card '%1' could not be found. Try starting the game and saving to create it.").arg(card_b_path));
   2744     }
   2745   }
   2746 }
   2747 
   2748 void MainWindow::onAchievementsLoginRequested(Achievements::LoginRequestReason reason)
   2749 {
   2750   const auto lock = pauseAndLockSystem();
   2751 
   2752   AchievementLoginDialog dlg(lock.getDialogParent(), reason);
   2753   dlg.exec();
   2754 }
   2755 
   2756 void MainWindow::onAchievementsChallengeModeChanged(bool enabled)
   2757 {
   2758   if (enabled)
   2759   {
   2760     QtUtils::CloseAndDeleteWindow(m_cheat_manager_window);
   2761     QtUtils::CloseAndDeleteWindow(m_debugger_window);
   2762     QtUtils::CloseAndDeleteWindow(m_memory_scanner_window);
   2763   }
   2764 
   2765   updateEmulationActions(false, System::IsValid(), enabled);
   2766 }
   2767 
   2768 void MainWindow::onToolsMemoryCardEditorTriggered()
   2769 {
   2770   openMemoryCardEditor(QString(), QString());
   2771 }
   2772 
   2773 void MainWindow::onToolsCoverDownloaderTriggered()
   2774 {
   2775   // This can be invoked via big picture, so exit fullscreen.
   2776   SystemLock lock(pauseAndLockSystem());
   2777   CoverDownloadDialog dlg(lock.getDialogParent());
   2778   connect(&dlg, &CoverDownloadDialog::coverRefreshRequested, m_game_list_widget, &GameListWidget::refreshGridCovers);
   2779   dlg.exec();
   2780 }
   2781 
   2782 void MainWindow::onToolsMediaCaptureToggled(bool checked)
   2783 {
   2784   if (!QtHost::IsSystemValid())
   2785   {
   2786     // leave it for later, we'll fill in the boot params
   2787     return;
   2788   }
   2789 
   2790   if (!checked)
   2791   {
   2792     Host::RunOnCPUThread(&System::StopMediaCapture);
   2793     return;
   2794   }
   2795 
   2796   const std::string container =
   2797     Host::GetStringSettingValue("MediaCapture", "Container", Settings::DEFAULT_MEDIA_CAPTURE_CONTAINER);
   2798   const QString qcontainer = QString::fromStdString(container);
   2799   const QString filter(tr("%1 Files (*.%2)").arg(qcontainer.toUpper()).arg(qcontainer));
   2800 
   2801   QString path =
   2802     QString::fromStdString(System::GetNewMediaCapturePath(QtHost::GetCurrentGameTitle().toStdString(), container));
   2803   path = QDir::toNativeSeparators(QFileDialog::getSaveFileName(this, tr("Media Capture"), path, filter));
   2804   if (path.isEmpty())
   2805   {
   2806     // uncheck it again
   2807     const QSignalBlocker sb(m_ui.actionMediaCapture);
   2808     m_ui.actionMediaCapture->setChecked(false);
   2809     return;
   2810   }
   2811 
   2812   Host::RunOnCPUThread([path = path.toStdString()]() { System::StartMediaCapture(path); });
   2813 }
   2814 
   2815 void MainWindow::onToolsMemoryScannerTriggered()
   2816 {
   2817   if (Achievements::IsHardcoreModeActive())
   2818     return;
   2819 
   2820   if (!m_memory_scanner_window)
   2821   {
   2822     m_memory_scanner_window = new MemoryScannerWindow();
   2823     connect(m_memory_scanner_window, &MemoryScannerWindow::closed, this, [this]() {
   2824       m_memory_scanner_window->deleteLater();
   2825       m_memory_scanner_window = nullptr;
   2826     });
   2827   }
   2828 
   2829   QtUtils::ShowOrRaiseWindow(m_memory_scanner_window);
   2830 }
   2831 
   2832 void MainWindow::openCheatManager()
   2833 {
   2834   if (Achievements::IsHardcoreModeActive())
   2835     return;
   2836 
   2837   if (!m_cheat_manager_window)
   2838   {
   2839     m_cheat_manager_window = new CheatManagerWindow();
   2840     connect(m_cheat_manager_window, &CheatManagerWindow::closed, this, [this]() {
   2841       m_cheat_manager_window->deleteLater();
   2842       m_cheat_manager_window = nullptr;
   2843     });
   2844   }
   2845 
   2846   QtUtils::ShowOrRaiseWindow(m_cheat_manager_window);
   2847 }
   2848 
   2849 void MainWindow::openCPUDebugger()
   2850 {
   2851   if (!m_debugger_window)
   2852   {
   2853     m_debugger_window = new DebuggerWindow();
   2854     connect(m_debugger_window, &DebuggerWindow::closed, this, [this]() {
   2855       m_debugger_window->deleteLater();
   2856       m_debugger_window = nullptr;
   2857     });
   2858   }
   2859 
   2860   QtUtils::ShowOrRaiseWindow(m_debugger_window);
   2861 }
   2862 
   2863 void MainWindow::onToolsOpenDataDirectoryTriggered()
   2864 {
   2865   QtUtils::OpenURL(this, QUrl::fromLocalFile(QString::fromStdString(EmuFolders::DataRoot)));
   2866 }
   2867 
   2868 void MainWindow::onSettingsTriggeredFromToolbar()
   2869 {
   2870   if (s_system_valid)
   2871     m_settings_toolbar_menu->exec(QCursor::pos());
   2872   else
   2873     doSettings();
   2874 }
   2875 
   2876 void MainWindow::checkForUpdates(bool display_message)
   2877 {
   2878   if (!AutoUpdaterDialog::isSupported())
   2879   {
   2880     if (display_message)
   2881     {
   2882       QMessageBox mbox(this);
   2883       mbox.setWindowTitle(tr("Updater Error"));
   2884       mbox.setWindowModality(Qt::WindowModal);
   2885       mbox.setTextFormat(Qt::RichText);
   2886 
   2887       QString message;
   2888       if (!AutoUpdaterDialog::isOfficialBuild())
   2889       {
   2890         message =
   2891           tr("<p>Sorry, you are trying to update a DuckStation version which is not an official GitHub release. To "
   2892              "prevent incompatibilities, the auto-updater is only enabled on official builds.</p>"
   2893              "<p>Please download an official release from from <a "
   2894              "href=\"https://www.duckstation.org/\">duckstation.org</a>.</p>");
   2895       }
   2896       else
   2897       {
   2898         message = tr("Automatic updating is not supported on the current platform.");
   2899       }
   2900 
   2901       mbox.setText(message);
   2902       mbox.setIcon(QMessageBox::Critical);
   2903       mbox.exec();
   2904     }
   2905 
   2906     return;
   2907   }
   2908 
   2909   if (m_auto_updater_dialog)
   2910     return;
   2911 
   2912   m_auto_updater_dialog = new AutoUpdaterDialog(this);
   2913   connect(m_auto_updater_dialog, &AutoUpdaterDialog::updateCheckCompleted, this, &MainWindow::onUpdateCheckComplete);
   2914   m_auto_updater_dialog->queueUpdateCheck(display_message);
   2915 }
   2916 
   2917 void* MainWindow::getNativeWindowId()
   2918 {
   2919   return (void*)winId();
   2920 }
   2921 
   2922 void MainWindow::onUpdateCheckComplete()
   2923 {
   2924   if (!m_auto_updater_dialog)
   2925     return;
   2926 
   2927   m_auto_updater_dialog->deleteLater();
   2928   m_auto_updater_dialog = nullptr;
   2929 }
   2930 
   2931 MainWindow::SystemLock MainWindow::pauseAndLockSystem()
   2932 {
   2933   // To switch out of fullscreen when displaying a popup, or not to?
   2934   // For Windows, with driver's direct scanout, what renders behind tends to be hit and miss.
   2935   // We can't draw anything over exclusive fullscreen, so get out of it in that case.
   2936   // Wayland's a pain as usual, we need to recreate the window, which means there'll be a brief
   2937   // period when there's no window, and Qt might shut us down. So avoid it there.
   2938   // On MacOS, it forces a workspace switch, which is kinda jarring.
   2939 
   2940 #ifndef __APPLE__
   2941   const bool was_fullscreen = g_emu_thread->isFullscreen() && !s_use_central_widget;
   2942 #else
   2943   const bool was_fullscreen = false;
   2944 #endif
   2945   const bool was_paused = !s_system_valid || s_system_paused;
   2946 
   2947   // We need to switch out of exclusive fullscreen before we can display our popup.
   2948   // However, we do not want to switch back to render-to-main, the window might have generated this event.
   2949   if (was_fullscreen)
   2950   {
   2951     g_emu_thread->setFullscreen(false, false);
   2952 
   2953     // Container could change... thanks Wayland.
   2954     QWidget* container;
   2955     while (s_system_valid &&
   2956            (g_emu_thread->isFullscreen() || !(container = getDisplayContainer()) || container->isFullScreen()))
   2957     {
   2958       QApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
   2959     }
   2960   }
   2961 
   2962   if (!was_paused)
   2963   {
   2964     g_emu_thread->setSystemPaused(true);
   2965 
   2966     // Need to wait for the pause to go through, and make the main window visible if needed.
   2967     while (!s_system_paused)
   2968       QApplication::processEvents(QEventLoop::ExcludeUserInputEvents, 1);
   2969 
   2970     // Ensure it's visible before we try to create any dialogs parented to us.
   2971     QApplication::sync();
   2972   }
   2973 
   2974   // Now we'll either have a borderless window, or a regular window (if we were exclusive fullscreen).
   2975   QWidget* dialog_parent = getDisplayContainer();
   2976 
   2977   return SystemLock(dialog_parent, was_paused, was_fullscreen);
   2978 }
   2979 
   2980 MainWindow::SystemLock::SystemLock(QWidget* dialog_parent, bool was_paused, bool was_fullscreen)
   2981   : m_dialog_parent(dialog_parent), m_was_paused(was_paused), m_was_fullscreen(was_fullscreen)
   2982 {
   2983 }
   2984 
   2985 MainWindow::SystemLock::SystemLock(SystemLock&& lock)
   2986   : m_dialog_parent(lock.m_dialog_parent), m_was_paused(lock.m_was_paused), m_was_fullscreen(lock.m_was_fullscreen)
   2987 {
   2988   lock.m_dialog_parent = nullptr;
   2989   lock.m_was_paused = true;
   2990   lock.m_was_fullscreen = false;
   2991 }
   2992 
   2993 MainWindow::SystemLock::~SystemLock()
   2994 {
   2995   if (m_was_fullscreen)
   2996     g_emu_thread->setFullscreen(true, true);
   2997   if (!m_was_paused)
   2998     g_emu_thread->setSystemPaused(false);
   2999 }
   3000 
   3001 void MainWindow::SystemLock::cancelResume()
   3002 {
   3003   m_was_paused = true;
   3004   m_was_fullscreen = false;
   3005 }