();
g_emu_thread->startVM(std::move(params));
}
void MainWindow::onChangeDiscFromFileActionTriggered()
{
VMLock lock(pauseAndLockVM());
QString filename =
QFileDialog::getOpenFileName(lock.getDialogParent(), tr("Select Disc Image"), QString(), tr(DISC_IMAGE_FILTER), nullptr);
if (filename.isEmpty())
return;
g_emu_thread->changeDisc(CDVD_SourceType::Iso, filename);
}
void MainWindow::onChangeDiscFromGameListActionTriggered()
{
m_was_disc_change_request = true;
switchToGameListView();
}
void MainWindow::onChangeDiscFromDeviceActionTriggered()
{
QString path(getDiscDevicePath(tr("Change Disc")));
if (path.isEmpty())
return;
g_emu_thread->changeDisc(CDVD_SourceType::Disc, path);
}
void MainWindow::onRemoveDiscActionTriggered()
{
g_emu_thread->changeDisc(CDVD_SourceType::NoDisc, QString());
}
void MainWindow::onChangeDiscMenuAboutToShow()
{
// TODO: This is where we would populate the playlist if there is one.
}
void MainWindow::onChangeDiscMenuAboutToHide()
{
}
void MainWindow::onLoadStateMenuAboutToShow()
{
m_ui.menuLoadState->clear();
updateSaveStateMenusEnableState(!m_current_disc_serial.isEmpty());
populateLoadStateMenu(m_ui.menuLoadState, m_current_disc_path, m_current_disc_serial, m_current_disc_crc);
}
void MainWindow::onSaveStateMenuAboutToShow()
{
m_ui.menuSaveState->clear();
updateSaveStateMenusEnableState(!m_current_disc_serial.isEmpty());
populateSaveStateMenu(m_ui.menuSaveState, m_current_disc_serial, m_current_disc_crc);
}
void MainWindow::onViewToolbarActionToggled(bool checked)
{
Host::SetBaseBoolSettingValue("UI", "ShowToolbar", checked);
Host::CommitBaseSettingChanges();
m_ui.toolBar->setVisible(checked);
}
void MainWindow::onViewLockToolbarActionToggled(bool checked)
{
Host::SetBaseBoolSettingValue("UI", "LockToolbar", checked);
Host::CommitBaseSettingChanges();
m_ui.toolBar->setMovable(!checked);
}
void MainWindow::onViewStatusBarActionToggled(bool checked)
{
Host::SetBaseBoolSettingValue("UI", "ShowStatusBar", checked);
Host::CommitBaseSettingChanges();
m_ui.statusBar->setVisible(checked);
}
void MainWindow::onViewGameListActionTriggered()
{
switchToGameListView();
m_game_list_widget->showGameList();
}
void MainWindow::onViewGameGridActionTriggered()
{
switchToGameListView();
m_game_list_widget->showGameGrid();
}
void MainWindow::onViewSystemDisplayTriggered()
{
if (m_display_created)
switchToEmulationView();
}
void MainWindow::onViewGamePropertiesActionTriggered()
{
if (!s_vm_valid)
return;
// prefer to use a game list entry, if we have one, that way the summary is populated
if (!m_current_disc_path.isEmpty() || !m_current_elf_override.isEmpty())
{
auto lock = GameList::GetLock();
const QString& path = (m_current_elf_override.isEmpty() ? m_current_disc_path : m_current_elf_override);
const GameList::Entry* entry = GameList::GetEntryForPath(path.toUtf8().constData());
if (entry)
{
SettingsDialog::openGamePropertiesDialog(
entry, entry->title, m_current_elf_override.isEmpty() ? entry->serial : std::string(), entry->crc);
return;
}
}
// open properties for the current running file (isn't in the game list)
if (m_current_disc_crc == 0)
{
QMessageBox::critical(this, tr("Game Properties"), tr("Game properties is unavailable for the current game."));
return;
}
// can't use serial for ELFs, because they might have a disc set
if (m_current_elf_override.isEmpty())
{
SettingsDialog::openGamePropertiesDialog(
nullptr, m_current_title.toStdString(), m_current_disc_serial.toStdString(), m_current_disc_crc);
}
else
{
SettingsDialog::openGamePropertiesDialog(
nullptr, m_current_title.toStdString(), std::string(), m_current_disc_crc);
}
}
void MainWindow::onGitHubRepositoryActionTriggered()
{
QtUtils::OpenURL(this, AboutDialog::getGitHubRepositoryUrl());
}
void MainWindow::onSupportForumsActionTriggered()
{
QtUtils::OpenURL(this, AboutDialog::getSupportForumsUrl());
}
void MainWindow::onDiscordServerActionTriggered()
{
QtUtils::OpenURL(this, AboutDialog::getDiscordServerUrl());
}
void MainWindow::onAboutActionTriggered()
{
AboutDialog about(this);
about.exec();
}
void MainWindow::checkForUpdates(bool display_message, bool force_check)
{
if (!AutoUpdaterDialog::isSupported())
{
if (display_message)
{
QMessageBox mbox(this);
mbox.setWindowTitle(tr("Updater Error"));
mbox.setTextFormat(Qt::RichText);
QString message;
#ifdef _WIN32
message = tr("Sorry, you are trying to update a PCSX2 version which is not an official GitHub release. To "
"prevent incompatibilities, the auto-updater is only enabled on official builds.
"
"To obtain an official build, please download from the link below:
"
"https://pcsx2.net/downloads/
");
#else
message = tr("Automatic updating is not supported on the current platform.");
#endif
mbox.setText(message);
mbox.setIcon(QMessageBox::Critical);
mbox.exec();
}
return;
}
if (m_auto_updater_dialog)
return;
if (force_check)
{
// Wipe out the last version, that way it displays the update if we've previously skipped it.
Host::RemoveBaseSettingValue("AutoUpdater", "LastVersion");
Host::CommitBaseSettingChanges();
}
m_auto_updater_dialog = new AutoUpdaterDialog(this);
connect(m_auto_updater_dialog, &AutoUpdaterDialog::updateCheckCompleted, this, &MainWindow::onUpdateCheckComplete);
m_auto_updater_dialog->queueUpdateCheck(display_message);
}
void MainWindow::onUpdateCheckComplete()
{
if (!m_auto_updater_dialog)
return;
m_auto_updater_dialog->deleteLater();
m_auto_updater_dialog = nullptr;
}
void MainWindow::startupUpdateCheck()
{
if (!Host::GetBaseBoolSettingValue("AutoUpdater", "CheckAtStartup", true))
return;
checkForUpdates(false, false);
}
void MainWindow::onToolsOpenDataDirectoryTriggered()
{
const QString path(QString::fromStdString(EmuFolders::DataRoot));
QtUtils::OpenURL(this, QUrl::fromLocalFile(path));
}
void MainWindow::onToolsCoverDownloaderTriggered()
{
CoverDownloadDialog dlg(this);
connect(&dlg, &CoverDownloadDialog::coverRefreshRequested, m_game_list_widget, &GameListWidget::refreshGridCovers);
dlg.exec();
}
void MainWindow::updateTheme()
{
QtHost::UpdateApplicationTheme();
m_game_list_widget->refreshImages();
}
void MainWindow::updateLanguage()
{
QtHost::InstallTranslator();
recreate();
}
void MainWindow::onInputRecNewActionTriggered()
{
const bool wasPaused = s_vm_paused;
const bool wasRunning = s_vm_valid;
if (wasRunning && !wasPaused)
{
g_emu_thread->setVMPaused(true);
}
NewInputRecordingDlg dlg(this);
const auto result = dlg.exec();
if (result == QDialog::Accepted)
{
Host::RunOnCPUThread(
[&, filePath = dlg.getFilePath(), fromSavestate = dlg.getInputRecType() == InputRecording::Type::FROM_SAVESTATE,
authorName = dlg.getAuthorName()]() {
if (g_InputRecording.create(filePath, fromSavestate, authorName))
{
QtHost::RunOnUIThread([&]() {
m_ui.actionInputRecNew->setEnabled(false);
m_ui.actionInputRecStop->setEnabled(true);
m_ui.actionReset->setEnabled(!g_InputRecording.isTypeSavestate());
m_ui.actionToolbarReset->setEnabled(!g_InputRecording.isTypeSavestate());
});
}
});
}
if (wasRunning && !wasPaused)
{
g_emu_thread->setVMPaused(false);
}
}
void MainWindow::onInputRecPlayActionTriggered()
{
const bool wasPaused = s_vm_paused;
if (!wasPaused)
{
g_emu_thread->setVMPaused(true);
}
QFileDialog dialog(this);
dialog.setFileMode(QFileDialog::ExistingFile);
dialog.setWindowTitle("Select a File");
dialog.setNameFilter(tr("Input Recording Files (*.p2m2)"));
QStringList fileNames;
if (dialog.exec())
{
fileNames = dialog.selectedFiles();
}
else
{
if (!wasPaused)
{
g_emu_thread->setVMPaused(false);
return;
}
}
if (fileNames.length() > 0)
{
if (g_InputRecording.isActive())
{
Host::RunOnCPUThread([]() { g_InputRecording.stop(); });
m_ui.actionInputRecStop->setEnabled(false);
}
Host::RunOnCPUThread([&, filename = fileNames.first().toStdString()]() {
if (g_InputRecording.play(filename))
{
QtHost::RunOnUIThread([&]() {
m_ui.actionInputRecNew->setEnabled(false);
m_ui.actionInputRecStop->setEnabled(true);
m_ui.actionReset->setEnabled(!g_InputRecording.isTypeSavestate());
m_ui.actionToolbarReset->setEnabled(!g_InputRecording.isTypeSavestate());
});
}
});
}
}
void MainWindow::onInputRecStopActionTriggered()
{
if (g_InputRecording.isActive())
{
Host::RunOnCPUThread([&]() {
g_InputRecording.stop();
QtHost::RunOnUIThread([&]() {
m_ui.actionInputRecNew->setEnabled(true);
m_ui.actionInputRecStop->setEnabled(false);
m_ui.actionReset->setEnabled(true);
m_ui.actionToolbarReset->setEnabled(true);
});
});
}
}
void MainWindow::onInputRecOpenSettingsTriggered()
{
// TODO - Vaser - Implement
}
InputRecordingViewer* MainWindow::getInputRecordingViewer()
{
if (!m_input_recording_viewer)
{
m_input_recording_viewer = new InputRecordingViewer(this);
}
return m_input_recording_viewer;
}
void MainWindow::updateInputRecordingActions(bool started)
{
m_ui.actionInputRecNew->setEnabled(started);
m_ui.actionInputRecPlay->setEnabled(started);
}
void MainWindow::onInputRecOpenViewer()
{
InputRecordingViewer* viewer = getInputRecordingViewer();
if (!viewer->isVisible())
{
viewer->show();
}
}
void MainWindow::onVMStarting()
{
s_vm_valid = true;
updateEmulationActions(true, false, false);
updateWindowTitle();
// prevent loading state until we're fully initialized
updateSaveStateMenusEnableState(false);
}
void MainWindow::onVMStarted()
{
s_vm_valid = true;
m_was_disc_change_request = false;
updateEmulationActions(true, true, false);
updateWindowTitle();
updateStatusBarWidgetVisibility();
updateInputRecordingActions(true);
}
void MainWindow::onVMPaused()
{
// update UI
{
QSignalBlocker sb(m_ui.actionPause);
m_ui.actionPause->setChecked(true);
}
{
QSignalBlocker sb(m_ui.actionToolbarPause);
m_ui.actionToolbarPause->setChecked(true);
}
s_vm_paused = true;
updateWindowTitle();
updateStatusBarWidgetVisibility();
m_last_fps_status = m_status_verbose_widget->text();
m_status_verbose_widget->setText(tr("Paused"));
if (m_display_widget)
updateDisplayWidgetCursor();
}
void MainWindow::onVMResumed()
{
// update UI
{
QSignalBlocker sb(m_ui.actionPause);
m_ui.actionPause->setChecked(false);
}
{
QSignalBlocker sb(m_ui.actionToolbarPause);
m_ui.actionToolbarPause->setChecked(false);
}
s_vm_paused = false;
m_was_disc_change_request = false;
updateWindowTitle();
updateStatusBarWidgetVisibility();
m_status_verbose_widget->setText(m_last_fps_status);
m_last_fps_status = QString();
if (m_display_widget)
{
updateDisplayWidgetCursor();
m_display_widget->setFocus();
}
}
void MainWindow::onVMStopped()
{
s_vm_valid = false;
s_vm_paused = false;
m_last_fps_status = QString();
updateEmulationActions(false, false, false);
updateWindowTitle();
updateWindowState();
updateStatusBarWidgetVisibility();
updateInputRecordingActions(false);
// If we're closing or in batch mode, quit the whole application now.
if (m_is_closing || QtHost::InBatchMode())
{
QApplication::processEvents(QEventLoop::ExcludeUserInputEvents, 1);
QCoreApplication::quit();
return;
}
if (m_display_widget)
updateDisplayWidgetCursor();
else
switchToGameListView();
// reload played time
if (m_game_list_widget->isShowingGameList())
m_game_list_widget->refresh(false);
}
void MainWindow::onGameChanged(const QString& title, const QString& elf_override, const QString& disc_path,
const QString& serial, quint32 disc_crc, quint32 crc)
{
m_current_title = title;
m_current_elf_override = elf_override;
m_current_disc_path = disc_path;
m_current_disc_serial = serial;
m_current_disc_crc = disc_crc;
m_current_running_crc = crc;
updateWindowTitle();
updateSaveStateMenusEnableState(!serial.isEmpty());
}
void MainWindow::showEvent(QShowEvent* event)
{
QMainWindow::showEvent(event);
// This is a bit silly, but for some reason resizing *before* the window is shown
// gives the incorrect sizes for columns, if you set the style before setting up
// the rest of the window... so, instead, let's just force it to be resized on show.
if (isShowingGameList())
m_game_list_widget->resizeTableViewColumnsToFit();
#ifdef ENABLE_RAINTEGRATION
if (Achievements::IsUsingRAIntegration())
Achievements::RAIntegration::MainWindowChanged((void*)winId());
#endif
}
void MainWindow::closeEvent(QCloseEvent* event)
{
// If there's no VM, we can just exit as normal.
if (!s_vm_valid)
{
saveStateToConfig();
QMainWindow::closeEvent(event);
return;
}
// But if there is, we have to cancel the action, regardless of whether we ended exiting
// or not. The window still needs to be visible while GS is shutting down.
event->ignore();
// Exit cancelled?
if (!requestShutdown(true, true, EmuConfig.SaveStateOnShutdown))
return;
// Application will be exited in VM stopped handler.
m_is_closing = true;
}
static QString getFilenameFromMimeData(const QMimeData* md)
{
QString filename;
if (md->hasUrls())
{
// only one url accepted
const QList urls(md->urls());
if (urls.size() == 1)
filename = QDir::toNativeSeparators(urls.front().toLocalFile());
}
return filename;
}
void MainWindow::dragEnterEvent(QDragEnterEvent* event)
{
const std::string filename(getFilenameFromMimeData(event->mimeData()).toStdString());
// allow save states being dragged in
if (!VMManager::IsLoadableFileName(filename) && !VMManager::IsSaveStateFileName(filename))
return;
event->acceptProposedAction();
}
void MainWindow::dropEvent(QDropEvent* event)
{
const QString filename(getFilenameFromMimeData(event->mimeData()));
const std::string filename_str(filename.toStdString());
if (VMManager::IsSaveStateFileName(filename_str))
{
// can't load a save state without a current VM
if (s_vm_valid)
{
event->acceptProposedAction();
g_emu_thread->loadState(filename);
}
else
{
QMessageBox::critical(this, tr("Load State Failed"), tr("Cannot load a save state without a running VM."));
}
}
else if (VMManager::IsLoadableFileName(filename_str))
{
// if we're already running, do a disc change, otherwise start
event->acceptProposedAction();
if (s_vm_valid)
doDiscChange(CDVD_SourceType::Iso, filename);
else
doStartFile(std::nullopt, filename);
}
}
void MainWindow::registerForDeviceNotifications()
{
#ifdef _WIN32
// We use these notifications to detect when a controller is connected or disconnected.
DEV_BROADCAST_DEVICEINTERFACE_W filter = {sizeof(DEV_BROADCAST_DEVICEINTERFACE_W), DBT_DEVTYP_DEVICEINTERFACE};
m_device_notification_handle =
RegisterDeviceNotificationW((HANDLE)winId(), &filter, DEVICE_NOTIFY_WINDOW_HANDLE | DEVICE_NOTIFY_ALL_INTERFACE_CLASSES);
#endif
}
void MainWindow::unregisterForDeviceNotifications()
{
#ifdef _WIN32
if (!m_device_notification_handle)
return;
UnregisterDeviceNotification(static_cast(m_device_notification_handle));
m_device_notification_handle = nullptr;
#endif
}
#ifdef _WIN32
bool MainWindow::nativeEvent(const QByteArray& eventType, void* message, qintptr* result)
{
static constexpr const char win_type[] = "windows_generic_MSG";
if (eventType == QByteArray(win_type, sizeof(win_type) - 1))
{
const MSG* msg = static_cast(message);
if (msg->message == WM_DEVICECHANGE && msg->wParam == DBT_DEVNODES_CHANGED)
{
g_emu_thread->reloadInputDevices();
*result = 1;
return true;
}
}
return QMainWindow::nativeEvent(eventType, message, result);
}
#endif
std::optional MainWindow::acquireRenderWindow(bool recreate_window, bool fullscreen, bool render_to_main, bool surfaceless)
{
DevCon.WriteLn("acquireRenderWindow() recreate=%s fullscreen=%s render_to_main=%s surfaceless=%s", recreate_window ? "true" : "false",
fullscreen ? "true" : "false", render_to_main ? "true" : "false", surfaceless ? "true" : "false");
QWidget* container = m_display_container ? static_cast(m_display_container) : static_cast(m_display_widget);
const bool is_fullscreen = isRenderingFullscreen();
const bool is_rendering_to_main = isRenderingToMain();
const bool changing_surfaceless = (!m_display_widget != surfaceless);
if (m_display_created && !recreate_window && fullscreen == is_fullscreen && is_rendering_to_main == render_to_main &&
!changing_surfaceless)
{
return m_display_widget ? m_display_widget->getWindowInfo() : WindowInfo();
}
// Skip recreating the surface if we're just transitioning between fullscreen and windowed with render-to-main off.
// .. except on Wayland, where everything tends to break if you don't recreate.
const bool has_container = (m_display_container != nullptr);
const bool needs_container = DisplayContainer::isNeeded(fullscreen, render_to_main);
if (m_display_created && !recreate_window && !is_rendering_to_main && !render_to_main && has_container == needs_container &&
!needs_container && !changing_surfaceless)
{
DevCon.WriteLn("Toggling to %s without recreating surface", (fullscreen ? "fullscreen" : "windowed"));
// since we don't destroy the display widget, we need to save it here
if (!is_fullscreen && !is_rendering_to_main)
saveDisplayWindowGeometryToConfig();
if (fullscreen)
{
container->showFullScreen();
}
else
{
restoreDisplayWindowGeometryFromConfig();
container->showNormal();
}
updateDisplayWidgetCursor();
m_display_widget->setFocus();
updateWindowState();
QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
return m_display_widget->getWindowInfo();
}
destroyDisplayWidget(surfaceless);
m_display_created = true;
// if we're going to surfaceless, we're done here
if (surfaceless)
return WindowInfo();
createDisplayWidget(fullscreen, render_to_main);
// we need the surface visible.. this might be able to be replaced with something else
QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
std::optional wi = m_display_widget->getWindowInfo();
if (!wi.has_value())
{
QMessageBox::critical(this, tr("Error"), tr("Failed to get window info from widget"));
destroyDisplayWidget(true);
return std::nullopt;
}
g_emu_thread->connectDisplaySignals(m_display_widget);
updateWindowTitle();
updateWindowState();
m_ui.actionStartFullscreenUI->setEnabled(false);
m_ui.actionToolbarStartFullscreenUI->setEnabled(false);
updateDisplayWidgetCursor();
m_display_widget->setFocus();
return wi;
}
void MainWindow::createDisplayWidget(bool fullscreen, bool render_to_main)
{
// If we're rendering to main and were hidden (e.g. coming back from fullscreen),
// make sure we're visible before trying to add ourselves. Otherwise Wayland breaks.
if (!fullscreen && render_to_main && !isVisible())
{
setVisible(true);
QGuiApplication::sync();
}
QWidget* container;
if (DisplayContainer::isNeeded(fullscreen, render_to_main))
{
m_display_container = new DisplayContainer();
m_display_widget = new DisplayWidget(m_display_container);
m_display_container->setDisplayWidget(m_display_widget);
container = m_display_container;
}
else
{
m_display_widget = new DisplayWidget((!fullscreen && render_to_main) ? getContentParent() : nullptr);
container = m_display_widget;
}
if (fullscreen || !render_to_main)
{
container->setWindowTitle(windowTitle());
container->setWindowIcon(windowIcon());
}
if (fullscreen)
{
// Don't risk doing this on Wayland, it really doesn't like window state changes,
// and positioning has no effect anyway.
if (!s_use_central_widget)
{
if (isVisible() && g_emu_thread->shouldRenderToMain())
container->move(pos());
else
restoreDisplayWindowGeometryFromConfig();
}
container->showFullScreen();
}
else if (!render_to_main)
{
restoreDisplayWindowGeometryFromConfig();
container->showNormal();
}
else if (s_use_central_widget)
{
m_game_list_widget->setVisible(false);
takeCentralWidget();
m_game_list_widget->setParent(this); // takeCentralWidget() removes parent
setCentralWidget(m_display_widget);
m_display_widget->setFocus();
update();
}
else
{
pxAssertRel(m_ui.mainContainer->count() == 1, "Has no display widget");
m_ui.mainContainer->addWidget(container);
m_ui.mainContainer->setCurrentIndex(1);
}
updateDisplayRelatedActions(true, render_to_main, fullscreen);
// We need the surface visible.
QGuiApplication::sync();
}
void MainWindow::displayResizeRequested(qint32 width, qint32 height)
{
if (!m_display_widget)
return;
// unapply the pixel scaling factor for hidpi
const float dpr = devicePixelRatioF();
width = static_cast(std::max(static_cast(std::lroundf(static_cast(width) / dpr)), 1));
height = static_cast(std::max(static_cast(std::lroundf(static_cast(height) / dpr)), 1));
if (m_display_container || !m_display_widget->parent())
{
// no parent - rendering to separate window. easy.
QtUtils::ResizePotentiallyFixedSizeWindow(getDisplayContainer(), width, height);
return;
}
// we are rendering to the main window. we have to add in the extra height from the toolbar/status bar.
const s32 extra_height = this->height() - m_display_widget->height();
QtUtils::ResizePotentiallyFixedSizeWindow(this, width, height + extra_height);
}
void MainWindow::relativeMouseModeRequested(bool enabled)
{
if (m_relative_mouse_mode == enabled)
return;
m_relative_mouse_mode = enabled;
if (m_display_widget && !s_vm_paused)
updateDisplayWidgetCursor();
}
void MainWindow::releaseRenderWindow()
{
// Now we can safely destroy the display window.
destroyDisplayWidget(true);
m_display_created = false;
m_ui.actionViewSystemDisplay->setEnabled(false);
m_ui.actionFullscreen->setEnabled(false);
m_ui.actionStartFullscreenUI->setEnabled(true);
m_ui.actionToolbarStartFullscreenUI->setEnabled(true);
}
void MainWindow::destroyDisplayWidget(bool show_game_list)
{
if (!m_display_widget)
return;
if (!isRenderingFullscreen() && !isRenderingToMain())
saveDisplayWindowGeometryToConfig();
if (m_display_container)
m_display_container->removeDisplayWidget();
if (isRenderingToMain())
{
if (s_use_central_widget)
{
pxAssertRel(centralWidget() == m_display_widget, "Display widget is currently central");
takeCentralWidget();
if (show_game_list)
{
m_game_list_widget->setVisible(true);
setCentralWidget(m_game_list_widget);
m_game_list_widget->resizeTableViewColumnsToFit();
}
}
else
{
pxAssertRel(m_ui.mainContainer->indexOf(m_display_widget) == 1, "Display widget in stack");
m_ui.mainContainer->removeWidget(m_display_widget);
if (show_game_list)
{
m_ui.mainContainer->setCurrentIndex(0);
m_game_list_widget->resizeTableViewColumnsToFit();
}
}
}
if (m_display_widget)
{
m_display_widget->deleteLater();
m_display_widget = nullptr;
}
if (m_display_container)
{
m_display_container->deleteLater();
m_display_container = nullptr;
}
updateDisplayRelatedActions(false, false, false);
}
void MainWindow::updateDisplayWidgetCursor()
{
pxAssertRel(m_display_widget, "Should have a display widget");
m_display_widget->updateRelativeMode(s_vm_valid && !s_vm_paused && m_relative_mouse_mode);
m_display_widget->updateCursor(s_vm_valid && !s_vm_paused && shouldHideMouseCursor());
}
void MainWindow::focusDisplayWidget()
{
if (!m_display_widget || centralWidget() != m_display_widget)
return;
m_display_widget->setFocus();
}
QWidget* MainWindow::getDisplayContainer() const
{
return (m_display_container ? static_cast(m_display_container) : static_cast(m_display_widget));
}
void MainWindow::saveDisplayWindowGeometryToConfig()
{
QWidget* container = getDisplayContainer();
if (container->windowState() & Qt::WindowFullScreen)
{
// if we somehow ended up here, don't save the fullscreen state to the config
return;
}
const QByteArray geometry = getDisplayContainer()->saveGeometry();
const QByteArray geometry_b64 = geometry.toBase64();
const std::string old_geometry_b64 = Host::GetBaseStringSettingValue("UI", "DisplayWindowGeometry");
if (old_geometry_b64 != geometry_b64.constData())
{
Host::SetBaseStringSettingValue("UI", "DisplayWindowGeometry", geometry_b64.constData());
Host::CommitBaseSettingChanges();
}
}
void MainWindow::restoreDisplayWindowGeometryFromConfig()
{
const std::string geometry_b64 = Host::GetBaseStringSettingValue("UI", "DisplayWindowGeometry");
const QByteArray geometry = QByteArray::fromBase64(QByteArray::fromStdString(geometry_b64));
QWidget* container = getDisplayContainer();
if (!geometry.isEmpty())
{
container->restoreGeometry(geometry);
// make sure we're not loading a dodgy config which had fullscreen set...
container->setWindowState(container->windowState() & ~(Qt::WindowFullScreen | Qt::WindowActive));
}
else
{
// default size
container->resize(640, 480);
}
}
SettingsDialog* MainWindow::getSettingsDialog()
{
if (!m_settings_dialog)
{
m_settings_dialog = new SettingsDialog(this);
connect(m_settings_dialog->getInterfaceSettingsWidget(), &InterfaceSettingsWidget::themeChanged, this, &MainWindow::updateTheme);
connect(m_settings_dialog->getInterfaceSettingsWidget(), &InterfaceSettingsWidget::languageChanged, this, [this]() {
// reopen settings dialog after it applies
updateLanguage();
// If you doSettings now, on macOS, the window will somehow end up underneath the main window that was created above
// Delay it slightly...
QtHost::RunOnUIThread([]{
g_main_window->doSettings("Interface");
});
});
}
return m_settings_dialog;
}
void MainWindow::doSettings(const char* category /* = nullptr */)
{
SettingsDialog* dlg = getSettingsDialog();
if (dlg->isVisible())
{
dlg->raise();
}
else
{
dlg->setModal(false);
dlg->show();
}
if (category)
dlg->setCategory(category);
}
DebuggerWindow* MainWindow::getDebuggerWindow()
{
if (!m_debugger_window)
m_debugger_window = new DebuggerWindow(this);
return m_debugger_window;
}
void MainWindow::openDebugger()
{
DebuggerWindow* dwnd = getDebuggerWindow();
dwnd->isVisible() ? dwnd->hide() : dwnd->show();
}
ControllerSettingsDialog* MainWindow::getControllerSettingsDialog()
{
if (!m_controller_settings_dialog)
m_controller_settings_dialog = new ControllerSettingsDialog(this);
return m_controller_settings_dialog;
}
void MainWindow::doControllerSettings(ControllerSettingsDialog::Category category)
{
ControllerSettingsDialog* dlg = getControllerSettingsDialog();
if (!dlg->isVisible())
{
dlg->setModal(false);
dlg->show();
}
if (category != ControllerSettingsDialog::Category::Count)
dlg->setCategory(category);
}
QString MainWindow::getDiscDevicePath(const QString& title)
{
QString ret;
const std::vector devices(GetOpticalDriveList());
if (devices.empty())
{
QMessageBox::critical(this, title,
tr("Could not find any CD/DVD-ROM devices. Please ensure you have a drive connected and "
"sufficient permissions to access it."));
return ret;
}
// if there's only one, select it automatically
if (devices.size() == 1)
{
ret = QString::fromStdString(devices.front());
return ret;
}
QStringList input_options;
for (const std::string& name : devices)
input_options.append(QString::fromStdString(name));
QInputDialog input_dialog(this);
input_dialog.setWindowTitle(title);
input_dialog.setLabelText(tr("Select disc drive:"));
input_dialog.setInputMode(QInputDialog::TextInput);
input_dialog.setOptions(QInputDialog::UseListViewForComboBoxItems);
input_dialog.setComboBoxEditable(false);
input_dialog.setComboBoxItems(std::move(input_options));
if (input_dialog.exec() == 0)
return ret;
ret = input_dialog.textValue();
return ret;
}
void MainWindow::startGameListEntry(const GameList::Entry* entry, std::optional save_slot, std::optional fast_boot)
{
std::shared_ptr params = std::make_shared();
params->fast_boot = fast_boot;
GameList::FillBootParametersForEntry(params.get(), entry);
if (save_slot.has_value() && !entry->serial.empty())
{
std::string state_filename = VMManager::GetSaveStateFileName(entry->serial.c_str(), entry->crc, save_slot.value());
if (!FileSystem::FileExists(state_filename.c_str()))
{
QMessageBox::critical(this, tr("Error"), tr("This save state does not exist."));
return;
}
params->save_state = std::move(state_filename);
}
g_emu_thread->startVM(std::move(params));
}
void MainWindow::setGameListEntryCoverImage(const GameList::Entry* entry)
{
const QString filename(
QFileDialog::getOpenFileName(this, tr("Select Cover Image"), QString(), tr("All Cover Image Types (*.jpg *.jpeg *.png)")));
if (filename.isEmpty())
return;
if (!GameList::GetCoverImagePathForEntry(entry).empty())
{
if (QMessageBox::question(this, tr("Cover Already Exists"),
tr("A cover image for this game already exists, do you wish to replace it?"), QMessageBox::Yes,
QMessageBox::No) != QMessageBox::Yes)
{
return;
}
}
const QString new_filename(QString::fromStdString(GameList::GetNewCoverImagePathForEntry(entry, filename.toUtf8().constData())));
if (new_filename.isEmpty())
return;
if (QFile::exists(new_filename) && !QFile::remove(new_filename))
{
QMessageBox::critical(this, tr("Copy Error"), tr("Failed to remove existing cover '%1'").arg(new_filename));
return;
}
if (!QFile::copy(filename, new_filename))
{
QMessageBox::critical(this, tr("Copy Error"), tr("Failed to copy '%1' to '%2'").arg(filename).arg(new_filename));
return;
}
m_game_list_widget->refreshGridCovers();
}
void MainWindow::clearGameListEntryPlayTime(const GameList::Entry* entry)
{
if (QMessageBox::question(this, tr("Confirm Reset"),
tr("Are you sure you want to reset the play time for '%1'?\n\nThis action cannot be undone.")
.arg(QString::fromStdString(entry->title))) != QMessageBox::Yes)
{
return;
}
GameList::ClearPlayedTimeForSerial(entry->serial);
m_game_list_widget->refresh(false);
}
std::optional MainWindow::promptForResumeState(const QString& save_state_path)
{
if (save_state_path.isEmpty())
return false;
QFileInfo fi(save_state_path);
if (!fi.exists())
return false;
QMessageBox msgbox(this);
msgbox.setIcon(QMessageBox::Question);
msgbox.setWindowTitle(tr("Load Resume State"));
msgbox.setText(
tr("A resume save state was found for this game, saved at:\n\n%1.\n\nDo you want to load this state, or start from a fresh boot?")
.arg(fi.lastModified().toLocalTime().toString()));
QPushButton* load = msgbox.addButton(tr("Load State"), QMessageBox::AcceptRole);
QPushButton* boot = msgbox.addButton(tr("Fresh Boot"), QMessageBox::RejectRole);
QPushButton* delboot = msgbox.addButton(tr("Delete And Boot"), QMessageBox::RejectRole);
msgbox.addButton(QMessageBox::Cancel);
msgbox.setDefaultButton(load);
msgbox.exec();
QAbstractButton* clicked = msgbox.clickedButton();
if (load == clicked)
{
return true;
}
else if (boot == clicked)
{
return false;
}
else if (delboot == clicked)
{
if (!QFile::remove(save_state_path))
QMessageBox::critical(this, tr("Error"), tr("Failed to delete save state file '%1'.").arg(save_state_path));
return false;
}
return std::nullopt;
}
void MainWindow::loadSaveStateSlot(s32 slot)
{
if (s_vm_valid)
{
// easy when we're running
g_emu_thread->loadStateFromSlot(slot);
return;
}
else
{
// we're not currently running, therefore we must've right clicked in the game list
const GameList::Entry* entry = m_game_list_widget->getSelectedEntry();
if (!entry)
return;
startGameListEntry(entry, slot, std::nullopt);
}
}
void MainWindow::loadSaveStateFile(const QString& filename, const QString& state_filename)
{
if (s_vm_valid)
{
if (!filename.isEmpty() && m_current_disc_path != filename)
g_emu_thread->changeDisc(CDVD_SourceType::Iso, m_current_disc_path);
g_emu_thread->loadState(state_filename);
}
else
{
std::shared_ptr params = std::make_shared();
params->filename = filename.toStdString();
params->save_state = state_filename.toStdString();
g_emu_thread->startVM(std::move(params));
}
}
static QString formatTimestampForSaveStateMenu(time_t timestamp)
{
const QDateTime qtime(QDateTime::fromSecsSinceEpoch(static_cast(timestamp)));
return qtime.toString(QLocale::system().dateTimeFormat(QLocale::ShortFormat));
}
void MainWindow::populateLoadStateMenu(QMenu* menu, const QString& filename, const QString& serial, quint32 crc)
{
if (serial.isEmpty())
return;
const bool is_right_click_menu = (menu != m_ui.menuLoadState);
bool has_any_states = false;
QAction* action = menu->addAction(is_right_click_menu ? tr("Load State File...") : tr("Load From File..."));
connect(action, &QAction::triggered, [this, filename]() {
const QString path(QFileDialog::getOpenFileName(this, tr("Select Save State File"), QString(), tr("Save States (*.p2s)")));
if (path.isEmpty())
return;
loadSaveStateFile(filename, path);
});
QAction* delete_save_states_action = menu->addAction(tr("Delete Save States..."));
// don't include undo in the right click menu
if (!is_right_click_menu)
{
QAction* load_undo_state = menu->addAction(tr("Undo Load State"));
load_undo_state->setEnabled(false); // CanUndoLoadState()
// connect(load_undo_state, &QAction::triggered, this, &QtHostInterface::undoLoadState);
menu->addSeparator();
}
const QByteArray game_serial_utf8(serial.toUtf8());
std::string state_filename;
FILESYSTEM_STAT_DATA sd;
if (is_right_click_menu)
{
state_filename = VMManager::GetSaveStateFileName(game_serial_utf8.constData(), crc, -1);
if (FileSystem::StatFile(state_filename.c_str(), &sd))
{
action = menu->addAction(tr("Resume (%2)").arg(formatTimestampForSaveStateMenu(sd.ModificationTime)));
connect(action, &QAction::triggered, [this]() { loadSaveStateSlot(-1); });
// Make bold to indicate it's the default choice when double-clicking
QtUtils::MarkActionAsDefault(action);
has_any_states = true;
}
}
for (s32 i = 1; i <= VMManager::NUM_SAVE_STATE_SLOTS; i++)
{
FILESYSTEM_STAT_DATA sd;
state_filename = VMManager::GetSaveStateFileName(game_serial_utf8.constData(), crc, i);
if (!FileSystem::StatFile(state_filename.c_str(), &sd))
continue;
action = menu->addAction(tr("Load Slot %1 (%2)").arg(i).arg(formatTimestampForSaveStateMenu(sd.ModificationTime)));
connect(action, &QAction::triggered, [this, i]() { loadSaveStateSlot(i); });
has_any_states = true;
}
delete_save_states_action->setEnabled(has_any_states);
if (has_any_states)
{
connect(delete_save_states_action, &QAction::triggered, this, [this, serial, crc] {
if (QMessageBox::warning(this, tr("Delete Save States"),
tr("Are you sure you want to delete all save states for %1?\n\nThe saves will not be recoverable.").arg(serial),
QMessageBox::Yes, QMessageBox::No) != QMessageBox::Yes)
{
return;
}
const u32 deleted = VMManager::DeleteSaveStates(serial.toUtf8().constData(), crc, true);
QMessageBox::information(this, tr("Delete Save States"), tr("%1 save states deleted.").arg(deleted));
});
}
}
void MainWindow::populateSaveStateMenu(QMenu* menu, const QString& serial, quint32 crc)
{
if (serial.isEmpty())
return;
connect(menu->addAction(tr("Save To File...")), &QAction::triggered, [this]() {
const QString path(QFileDialog::getSaveFileName(this, tr("Select Save State File"), QString(), tr("Save States (*.p2s)")));
if (path.isEmpty())
return;
g_emu_thread->saveState(path);
});
menu->addSeparator();
const QByteArray game_serial_utf8(serial.toUtf8());
for (s32 i = 1; i <= VMManager::NUM_SAVE_STATE_SLOTS; i++)
{
std::string filename(VMManager::GetSaveStateFileName(game_serial_utf8.constData(), crc, i));
FILESYSTEM_STAT_DATA sd;
QString timestamp;
if (FileSystem::StatFile(filename.c_str(), &sd))
timestamp = formatTimestampForSaveStateMenu(sd.ModificationTime);
else
timestamp = tr("Empty");
QString title(tr("Save Slot %1 (%2)").arg(i).arg(timestamp));
connect(menu->addAction(title), &QAction::triggered, [i]() { g_emu_thread->saveStateToSlot(i); });
}
}
void MainWindow::updateSaveStateMenusEnableState(bool enable)
{
const bool load_enabled = enable;
const bool save_enabled = enable && s_vm_valid;
m_ui.menuLoadState->setEnabled(load_enabled);
m_ui.actionToolbarLoadState->setEnabled(load_enabled);
m_ui.menuSaveState->setEnabled(save_enabled);
m_ui.actionToolbarSaveState->setEnabled(save_enabled);
}
void MainWindow::doStartFile(std::optional source, const QString& path)
{
if (s_vm_valid)
return;
std::shared_ptr params = std::make_shared();
params->source_type = source;
params->filename = path.toStdString();
// we might still be saving a resume state...
VMManager::WaitForSaveStateFlush();
// GetSaveStateFileName() might temporarily mount the ISO to get the serial.
cancelGameListRefresh();
const std::optional resume(
promptForResumeState(QString::fromStdString(VMManager::GetSaveStateFileName(params->filename.c_str(), -1))));
if (!resume.has_value())
return;
else if (resume.value())
params->state_index = -1;
g_emu_thread->startVM(std::move(params));
}
void MainWindow::doDiscChange(CDVD_SourceType source, const QString& path)
{
bool reset_system = false;
if (!m_was_disc_change_request && !GSDumpReplayer::IsReplayingDump())
{
QMessageBox message(QMessageBox::Question, tr("Confirm Disc Change"),
tr("Do you want to swap discs or boot the new image (via system reset)?"), QMessageBox::NoButton, this);
message.addButton(tr("Swap Disc"), QMessageBox::ActionRole);
QPushButton* reset_button = message.addButton(tr("Reset"), QMessageBox::ActionRole);
QPushButton* cancel_button = message.addButton(QMessageBox::Cancel);
message.setDefaultButton(cancel_button);
message.exec();
if (message.clickedButton() == cancel_button)
return;
reset_system = (message.clickedButton() == reset_button);
}
switchToEmulationView();
g_emu_thread->changeDisc(source, path);
if (reset_system)
g_emu_thread->resetVM();
}
MainWindow::VMLock MainWindow::pauseAndLockVM()
{
// To switch out of fullscreen when displaying a popup, or not to?
// For Windows, with driver's direct scanout, what renders behind tends to be hit and miss.
// We can't draw anything over exclusive fullscreen, so get out of it in that case.
// Wayland's a pain as usual, we need to recreate the window, which means there'll be a brief
// period when there's no window, and Qt might shut us down. So avoid it there.
// On MacOS, it forces a workspace switch, which is kinda jarring.
#ifndef __APPLE__
const bool was_fullscreen = g_emu_thread->isFullscreen() && !s_use_central_widget;
#else
const bool was_fullscreen = false;
#endif
const bool was_paused = s_vm_paused;
if (!was_paused)
g_emu_thread->setVMPaused(true);
// We need to switch out of exclusive fullscreen before we can display our popup.
// However, we do not want to switch back to render-to-main, the window might have generated this event.
if (was_fullscreen)
{
g_emu_thread->setFullscreen(false, false);
while (QtHost::IsVMValid() && g_emu_thread->isFullscreen())
QApplication::processEvents(QEventLoop::ExcludeUserInputEvents, 1);
}
// Now we'll either have a borderless window, or a regular window (if we were exclusive fullscreen).
QWidget* dialog_parent = getDisplayContainer();
return VMLock(dialog_parent, was_paused, was_fullscreen);
}
void MainWindow::rescanFile(const std::string& path)
{
m_game_list_widget->rescanFile(path);
}
MainWindow::VMLock::VMLock(QWidget* dialog_parent, bool was_paused, bool was_fullscreen)
: m_dialog_parent(dialog_parent)
, m_was_paused(was_paused)
, m_was_fullscreen(was_fullscreen)
{
}
MainWindow::VMLock::VMLock(VMLock&& lock)
: m_dialog_parent(lock.m_dialog_parent)
, m_was_paused(lock.m_was_paused)
, m_was_fullscreen(lock.m_was_fullscreen)
{
lock.m_dialog_parent = nullptr;
lock.m_was_paused = true;
lock.m_was_fullscreen = false;
}
MainWindow::VMLock::~VMLock()
{
if (m_was_fullscreen)
g_emu_thread->setFullscreen(true, true);
if (!m_was_paused)
g_emu_thread->setVMPaused(false);
}
void MainWindow::VMLock::cancelResume()
{
m_was_paused = true;
m_was_fullscreen = false;
}
bool QtHost::IsVMValid()
{
return s_vm_valid;
}
bool QtHost::IsVMPaused()
{
return s_vm_paused;
}