From 69f03959aa2b755acb71ac920dc5b25eccab8b31 Mon Sep 17 00:00:00 2001 From: Connor McLaughlin Date: Mon, 2 Mar 2020 11:08:16 +1000 Subject: [PATCH] Qt: Implement context menu in game list --- src/core/host_interface.cpp | 34 +++-- src/core/host_interface.h | 7 +- src/core/system.cpp | 32 +++-- src/core/system.h | 14 ++- src/duckstation-qt/gamelistwidget.cpp | 51 ++++++-- src/duckstation-qt/gamelistwidget.h | 8 +- src/duckstation-qt/mainwindow.cpp | 140 ++++++++++++++++----- src/duckstation-qt/mainwindow.h | 7 ++ src/duckstation-qt/qthostinterface.cpp | 75 ++++++++--- src/duckstation-qt/qthostinterface.h | 9 +- src/duckstation-sdl/main.cpp | 4 +- src/duckstation-sdl/sdl_host_interface.cpp | 15 ++- 12 files changed, 305 insertions(+), 91 deletions(-) diff --git a/src/core/host_interface.cpp b/src/core/host_interface.cpp index b0f0aaa02..509dc7bfd 100644 --- a/src/core/host_interface.cpp +++ b/src/core/host_interface.cpp @@ -77,7 +77,7 @@ void HostInterface::CreateAudioStream() m_audio_stream->Reconfigure(AUDIO_SAMPLE_RATE, AUDIO_CHANNELS, AUDIO_BUFFER_SIZE, 4); } -bool HostInterface::BootSystemFromFile(const char* filename) +bool HostInterface::BootSystem(const SystemBootParameters& parameters) { if (!AcquireHostDisplay()) { @@ -92,7 +92,7 @@ bool HostInterface::BootSystemFromFile(const char* filename) CreateAudioStream(); m_system = System::Create(this); - if (!m_system->Boot(filename)) + if (!m_system->Boot(parameters)) { ReportFormattedError("System failed to boot. The log may contain more information."); DestroySystem(); @@ -111,11 +111,6 @@ bool HostInterface::BootSystemFromFile(const char* filename) return true; } -bool HostInterface::BootSystemFromBIOS() -{ - return BootSystemFromFile(nullptr); -} - void HostInterface::PauseSystem(bool paused) { if (paused == m_paused) @@ -439,7 +434,8 @@ bool HostInterface::LoadState(const char* filename) } else { - if (!BootSystemFromFile(nullptr)) + SystemBootParameters boot_params; + if (!BootSystem(boot_params)) { ReportFormattedError("Failed to boot system to load state from '%s'.", filename); return false; @@ -512,7 +508,9 @@ bool HostInterface::SaveState(bool global, s32 slot) bool HostInterface::ResumeSystemFromState(const char* filename, bool boot_on_failure) { - if (!BootSystemFromFile(filename)) + SystemBootParameters boot_params; + boot_params.filename = filename; + if (!BootSystem(boot_params)) return false; const bool global = m_system->GetRunningCode().empty(); @@ -762,6 +760,20 @@ std::vector HostInterface::GetAvailableSaveStates( return si; } +void HostInterface::DeleteSaveStates(const char* game_code, bool resume) +{ + const std::vector states(GetAvailableSaveStates(game_code)); + for (const SaveStateInfo& si : states) + { + if (si.global || (!resume && si.slot < 0)) + continue; + + Log_InfoPrintf("Removing save state at '%s'", si.path.c_str()); + if (!FileSystem::DeleteFile(si.path.c_str())) + Log_ErrorPrintf("Failed to delete save state file '%s'", si.path.c_str()); + } +} + std::string HostInterface::GetMostRecentResumeSaveStatePath() const { std::vector files; @@ -970,7 +982,9 @@ void HostInterface::RecreateSystem() } DestroySystem(); - if (!BootSystemFromFile(nullptr)) + + SystemBootParameters boot_params; + if (!BootSystem(boot_params)) { ReportError("Failed to boot system after recreation."); return; diff --git a/src/core/host_interface.h b/src/core/host_interface.h index 1def726c7..4814f26b4 100644 --- a/src/core/host_interface.h +++ b/src/core/host_interface.h @@ -18,6 +18,7 @@ class HostDisplay; class GameList; class System; +struct SystemBootParameters; class HostInterface { @@ -42,8 +43,7 @@ public: /// Access to emulated system. ALWAYS_INLINE System* GetSystem() const { return m_system.get(); } - bool BootSystemFromFile(const char* filename); - bool BootSystemFromBIOS(); + bool BootSystem(const SystemBootParameters& parameters); void PauseSystem(bool paused); void ResetSystem(); void PowerOffSystem(); @@ -90,6 +90,9 @@ public: /// such as compiling shaders when starting up. void DisplayLoadingScreen(const char* message, int progress_min = -1, int progress_max = -1, int progress_value = -1); + /// Deletes save states for the specified game code. If resume is set, the resume state is deleted too. + void DeleteSaveStates(const char* game_code, bool resume); + protected: enum : u32 { diff --git a/src/core/system.cpp b/src/core/system.cpp index 4afeec216..6c8702706 100644 --- a/src/core/system.cpp +++ b/src/core/system.cpp @@ -30,6 +30,12 @@ Log_SetChannel(System); #include #endif +SystemBootParameters::SystemBootParameters() = default; + +SystemBootParameters::SystemBootParameters(std::string filename_) : filename(filename_) {} + +SystemBootParameters::~SystemBootParameters() = default; + System::System(HostInterface* host_interface) : m_host_interface(host_interface) { m_cpu = std::make_unique(); @@ -102,14 +108,14 @@ void System::SetCPUExecutionMode(CPUExecutionMode mode) m_cpu_code_cache->SetUseRecompiler(mode == CPUExecutionMode::Recompiler); } -bool System::Boot(const char* filename) +bool System::Boot(const SystemBootParameters& params) { // Load CD image up and detect region. std::unique_ptr media; bool exe_boot = false; - if (filename) + if (!params.filename.empty()) { - exe_boot = GameList::IsExeFileName(filename); + exe_boot = GameList::IsExeFileName(params.filename.c_str()); if (exe_boot) { if (m_region == ConsoleRegion::Auto) @@ -120,11 +126,11 @@ bool System::Boot(const char* filename) } else { - Log_InfoPrintf("Loading CD image '%s'...", filename); - media = CDImage::Open(filename); + Log_InfoPrintf("Loading CD image '%s'...", params.filename.c_str()); + media = CDImage::Open(params.filename.c_str()); if (!media) { - m_host_interface->ReportFormattedError("Failed to load CD image '%s'", filename); + m_host_interface->ReportFormattedError("Failed to load CD image '%s'", params.filename.c_str()); return false; } @@ -134,7 +140,8 @@ bool System::Boot(const char* filename) if (detected_region) { m_region = detected_region.value(); - Log_InfoPrintf("Auto-detected %s region for '%s'", Settings::GetConsoleRegionName(m_region), filename); + Log_InfoPrintf("Auto-detected %s region for '%s'", Settings::GetConsoleRegionName(m_region), + params.filename.c_str()); } else { @@ -172,19 +179,22 @@ bool System::Boot(const char* filename) BIOS::PatchBIOSEnableTTY(*bios_image, bios_hash); // Load EXE late after BIOS. - if (exe_boot && !LoadEXE(filename, *bios_image)) + if (exe_boot && !LoadEXE(params.filename.c_str(), *bios_image)) { - m_host_interface->ReportFormattedError("Failed to load EXE file '%s'", filename); + m_host_interface->ReportFormattedError("Failed to load EXE file '%s'", params.filename.c_str()); return false; } // Notify change of disc. - UpdateRunningGame(filename, media.get()); + UpdateRunningGame(params.filename.c_str(), media.get()); // Insert CD, and apply fastboot patch if enabled. m_cdrom->InsertMedia(std::move(media)); - if (m_cdrom->HasMedia() && GetSettings().bios_patch_fast_boot) + if (m_cdrom->HasMedia() && + (params.override_fast_boot.has_value() ? params.override_fast_boot.value() : GetSettings().bios_patch_fast_boot)) + { BIOS::PatchBIOSFastBoot(*bios_image, bios_hash); + } // Load the patched BIOS up. m_bus->SetBIOS(*bios_image); diff --git a/src/core/system.h b/src/core/system.h index 7d5ad2dd7..476f8ca41 100644 --- a/src/core/system.h +++ b/src/core/system.h @@ -28,11 +28,21 @@ class SPU; class MDEC; class SIO; +struct SystemBootParameters +{ + SystemBootParameters(); + SystemBootParameters(std::string filename_); + ~SystemBootParameters(); + + std::string filename; + std::optional override_fast_boot; +}; + class System { +public: friend TimingEvent; -public: ~System(); /// Creates a new System. @@ -75,7 +85,7 @@ public: float GetAverageFrameTime() const { return m_average_frame_time; } float GetWorstFrameTime() const { return m_worst_frame_time; } - bool Boot(const char* filename); + bool Boot(const SystemBootParameters& params); void Reset(); bool LoadState(ByteStream* state); diff --git a/src/duckstation-qt/gamelistwidget.cpp b/src/duckstation-qt/gamelistwidget.cpp index ef80d0da0..95126cf0b 100644 --- a/src/duckstation-qt/gamelistwidget.cpp +++ b/src/duckstation-qt/gamelistwidget.cpp @@ -261,6 +261,7 @@ void GameListWidget::initialize(QtHostInterface* host_interface) m_table_view->setSortingEnabled(true); m_table_view->setSelectionMode(QAbstractItemView::SingleSelection); m_table_view->setSelectionBehavior(QAbstractItemView::SelectRows); + m_table_view->setContextMenuPolicy(Qt::CustomContextMenu); m_table_view->setAlternatingRowColors(true); m_table_view->setShowGrid(false); m_table_view->setCurrentIndex({}); @@ -269,9 +270,11 @@ void GameListWidget::initialize(QtHostInterface* host_interface) m_table_view->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn); m_table_view->resizeColumnsToContents(); - connect(m_table_view, &QTableView::doubleClicked, this, &GameListWidget::onTableViewItemDoubleClicked); connect(m_table_view->selectionModel(), &QItemSelectionModel::currentChanged, this, &GameListWidget::onSelectionModelCurrentChanged); + connect(m_table_view, &QTableView::doubleClicked, this, &GameListWidget::onTableViewItemDoubleClicked); + connect(m_table_view, &QTableView::customContextMenuRequested, this, + &GameListWidget::onTableViewContextMenuRequested); insertWidget(0, m_table_view); setCurrentIndex(0); @@ -282,16 +285,6 @@ void GameListWidget::onGameListRefreshed() m_table_model->refresh(); } -void GameListWidget::onTableViewItemDoubleClicked(const QModelIndex& index) -{ - const QModelIndex source_index = m_table_sort_model->mapToSource(index); - if (!source_index.isValid() || source_index.row() >= static_cast(m_game_list->GetEntryCount())) - return; - - const GameListEntry& entry = m_game_list->GetEntries().at(source_index.row()); - emit bootEntryRequested(&entry); -} - void GameListWidget::onSelectionModelCurrentChanged(const QModelIndex& current, const QModelIndex& previous) { const QModelIndex source_index = m_table_sort_model->mapToSource(current); @@ -305,9 +298,45 @@ void GameListWidget::onSelectionModelCurrentChanged(const QModelIndex& current, emit entrySelected(&entry); } +void GameListWidget::onTableViewItemDoubleClicked(const QModelIndex& index) +{ + const QModelIndex source_index = m_table_sort_model->mapToSource(index); + if (!source_index.isValid() || source_index.row() >= static_cast(m_game_list->GetEntryCount())) + return; + + const GameListEntry& entry = m_game_list->GetEntries().at(source_index.row()); + emit entryDoubleClicked(&entry); +} + +void GameListWidget::onTableViewContextMenuRequested(const QPoint& point) +{ + const GameListEntry* entry = getSelectedEntry(); + if (!entry) + return; + + emit entryContextMenuRequested(m_table_view->mapToGlobal(point), entry); +} + void GameListWidget::resizeEvent(QResizeEvent* event) { QStackedWidget::resizeEvent(event); QtUtils::ResizeColumnsForTableView(m_table_view, {32, 80, -1, 60, 100}); } + +const GameListEntry* GameListWidget::getSelectedEntry() const +{ + const QItemSelectionModel* selection_model = m_table_view->selectionModel(); + if (!selection_model->hasSelection()) + return nullptr; + + const QModelIndexList selected_rows = selection_model->selectedRows(); + if (selected_rows.empty()) + return nullptr; + + const QModelIndex source_index = m_table_sort_model->mapToSource(selected_rows[0]); + if (!source_index.isValid() || source_index.row() >= static_cast(m_game_list->GetEntryCount())) + return nullptr; + + return &m_game_list->GetEntries().at(source_index.row()); +} diff --git a/src/duckstation-qt/gamelistwidget.h b/src/duckstation-qt/gamelistwidget.h index 03d25a178..626b0681d 100644 --- a/src/duckstation-qt/gamelistwidget.h +++ b/src/duckstation-qt/gamelistwidget.h @@ -22,17 +22,21 @@ public: Q_SIGNALS: void entrySelected(const GameListEntry* entry); - void bootEntryRequested(const GameListEntry* entry); + void entryDoubleClicked(const GameListEntry* entry); + void entryContextMenuRequested(const QPoint& point, const GameListEntry* entry); private Q_SLOTS: void onGameListRefreshed(); - void onTableViewItemDoubleClicked(const QModelIndex& index); void onSelectionModelCurrentChanged(const QModelIndex& current, const QModelIndex& previous); + void onTableViewItemDoubleClicked(const QModelIndex& index); + void onTableViewContextMenuRequested(const QPoint& point); protected: void resizeEvent(QResizeEvent* event); private: + const GameListEntry* getSelectedEntry() const; + QtHostInterface* m_host_interface = nullptr; GameList* m_game_list = nullptr; diff --git a/src/duckstation-qt/mainwindow.cpp b/src/duckstation-qt/mainwindow.cpp index 0d4110ff3..f906c8eb4 100644 --- a/src/duckstation-qt/mainwindow.cpp +++ b/src/duckstation-qt/mainwindow.cpp @@ -2,6 +2,7 @@ #include "common/assert.h" #include "core/game_list.h" #include "core/settings.h" +#include "core/system.h" #include "gamelistsettingswidget.h" #include "gamelistwidget.h" #include "qtdisplaywindow.h" @@ -9,6 +10,7 @@ #include "qtsettingsinterface.h" #include "settingsdialog.h" #include "settingwidgetbinder.h" +#include #include #include #include @@ -47,7 +49,7 @@ bool MainWindow::confirmMessage(const QString& message) { const int result = QMessageBox::question(this, tr("DuckStation"), message); focusDisplayWidget(); - + return (result == QMessageBox::Yes); } @@ -174,7 +176,15 @@ void MainWindow::onStartDiscActionTriggered() if (filename.isEmpty()) return; - m_host_interface->bootSystemFromFile(std::move(filename)); + SystemBootParameters boot_params; + boot_params.filename = filename.toStdString(); + m_host_interface->bootSystem(boot_params); +} + +void MainWindow::onStartBIOSActionTriggered() +{ + SystemBootParameters boot_params; + m_host_interface->bootSystem(boot_params); } void MainWindow::onChangeDiscFromFileActionTriggered() @@ -193,9 +203,8 @@ void MainWindow::onChangeDiscFromGameListActionTriggered() switchToGameListView(); } -static void OpenURL(QWidget* parent, const char* url) +static void OpenURL(QWidget* parent, const QUrl& qurl) { - const QUrl qurl(QUrl::fromEncoded(QByteArray(url, static_cast(std::strlen(url))))); if (!QDesktopServices::openUrl(qurl)) { QMessageBox::critical(parent, QObject::tr("Failed to open URL"), @@ -203,6 +212,11 @@ static void OpenURL(QWidget* parent, const char* url) } } +static void OpenURL(QWidget* parent, const char* url) +{ + return OpenURL(parent, QUrl::fromEncoded(QByteArray(url, static_cast(std::strlen(url))))); +} + void MainWindow::onGitHubRepositoryActionTriggered() { OpenURL(this, "https://github.com/stenzek/duckstation/"); @@ -215,6 +229,90 @@ void MainWindow::onIssueTrackerActionTriggered() void MainWindow::onAboutActionTriggered() {} +void MainWindow::onGameListEntrySelected(const GameListEntry* entry) +{ + if (!entry) + { + m_ui.statusBar->clearMessage(); + m_host_interface->populateSaveStateMenus("", m_ui.menuLoadState, m_ui.menuSaveState); + return; + } + + m_ui.statusBar->showMessage(QString::fromStdString(entry->path)); + m_host_interface->populateSaveStateMenus(entry->code.c_str(), m_ui.menuLoadState, m_ui.menuSaveState); +} + +void MainWindow::onGameListEntryDoubleClicked(const GameListEntry* entry) +{ + // if we're not running, boot the system, otherwise swap discs + QString path = QString::fromStdString(entry->path); + if (!m_emulation_running) + { + if (!entry->code.empty() && m_host_interface->getSettingValue("General/SaveStateOnExit", true).toBool()) + { + m_host_interface->resumeSystemFromState(path, true); + } + else + { + SystemBootParameters boot_params; + boot_params.filename = path.toStdString(); + m_host_interface->bootSystem(boot_params); + } + } + else + { + m_host_interface->changeDisc(path); + m_host_interface->pauseSystem(false); + switchToEmulationView(); + } +} + +void MainWindow::onGameListContextMenuRequested(const QPoint& point, const GameListEntry* entry) +{ + QMenu menu; + + // Hopefully this pointer doesn't disappear... it shouldn't. + if (entry) + { + connect(menu.addAction(tr("Properties...")), &QAction::triggered, [this]() { reportError(tr("TODO")); }); + + connect(menu.addAction(tr("Open Containing Directory...")), &QAction::triggered, [this, entry]() { + const QFileInfo fi(QString::fromStdString(entry->path)); + OpenURL(this, QUrl::fromLocalFile(fi.absolutePath())); + }); + + menu.addSeparator(); + + if (!entry->code.empty()) + { + m_host_interface->populateGameListContextMenu(entry->code.c_str(), this, &menu); + menu.addSeparator(); + } + + connect(menu.addAction(tr("Default Boot")), &QAction::triggered, + [this, entry]() { m_host_interface->bootSystem(SystemBootParameters(entry->path)); }); + + connect(menu.addAction(tr("Fast Boot")), &QAction::triggered, [this, entry]() { + SystemBootParameters boot_params(entry->path); + boot_params.override_fast_boot = true; + m_host_interface->bootSystem(boot_params); + }); + + connect(menu.addAction(tr("Full Boot")), &QAction::triggered, [this, entry]() { + SystemBootParameters boot_params(entry->path); + boot_params.override_fast_boot = false; + m_host_interface->bootSystem(boot_params); + }); + + menu.addSeparator(); + } + + connect(menu.addAction(tr("Add Search Directory...")), &QAction::triggered, + [this]() { getSettingsDialog()->getGameListSettingsWidget()->addSearchDirectory(this); }); + + menu.exec(point); +} + void MainWindow::setupAdditionalUi() { m_game_list_widget = new GameListWidget(m_ui.mainContainer); @@ -320,7 +418,7 @@ void MainWindow::connectSignals() onEmulationPaused(false); connect(m_ui.actionStartDisc, &QAction::triggered, this, &MainWindow::onStartDiscActionTriggered); - connect(m_ui.actionStartBios, &QAction::triggered, m_host_interface, &QtHostInterface::bootSystemFromBIOS); + connect(m_ui.actionStartBios, &QAction::triggered, this, &MainWindow::onStartBIOSActionTriggered); connect(m_ui.actionChangeDisc, &QAction::triggered, [this] { m_ui.menuChangeDisc->exec(QCursor::pos()); }); connect(m_ui.actionChangeDiscFromFile, &QAction::triggered, this, &MainWindow::onChangeDiscFromFileActionTriggered); connect(m_ui.actionChangeDiscFromGameList, &QAction::triggered, this, @@ -369,34 +467,10 @@ void MainWindow::connectSignals() &MainWindow::onSystemPerformanceCountersUpdated); connect(m_host_interface, &QtHostInterface::runningGameChanged, this, &MainWindow::onRunningGameChanged); - connect(m_game_list_widget, &GameListWidget::bootEntryRequested, [this](const GameListEntry* entry) { - // if we're not running, boot the system, otherwise swap discs - QString path = QString::fromStdString(entry->path); - if (!m_emulation_running) - { - if (!entry->code.empty() && m_host_interface->getSettingValue("General/SaveStateOnExit", true).toBool()) - m_host_interface->resumeSystemFromState(path, true); - else - m_host_interface->bootSystemFromFile(path); - } - else - { - m_host_interface->changeDisc(path); - m_host_interface->pauseSystem(false); - switchToEmulationView(); - } - }); - connect(m_game_list_widget, &GameListWidget::entrySelected, [this](const GameListEntry* entry) { - if (!entry) - { - m_ui.statusBar->clearMessage(); - m_host_interface->populateSaveStateMenus("", m_ui.menuLoadState, m_ui.menuSaveState); - return; - } - - m_ui.statusBar->showMessage(QString::fromStdString(entry->path)); - m_host_interface->populateSaveStateMenus(entry->code.c_str(), m_ui.menuLoadState, m_ui.menuSaveState); - }); + connect(m_game_list_widget, &GameListWidget::entrySelected, this, &MainWindow::onGameListEntrySelected); + connect(m_game_list_widget, &GameListWidget::entryDoubleClicked, this, &MainWindow::onGameListEntryDoubleClicked); + connect(m_game_list_widget, &GameListWidget::entryContextMenuRequested, this, + &MainWindow::onGameListContextMenuRequested); m_host_interface->populateSaveStateMenus(nullptr, m_ui.menuLoadState, m_ui.menuSaveState); diff --git a/src/duckstation-qt/mainwindow.h b/src/duckstation-qt/mainwindow.h index 12b883417..233a9cec9 100644 --- a/src/duckstation-qt/mainwindow.h +++ b/src/duckstation-qt/mainwindow.h @@ -13,6 +13,8 @@ class QThread; class GameListWidget; class QtHostInterface; +struct GameListEntry; + class MainWindow final : public QMainWindow { Q_OBJECT @@ -39,12 +41,17 @@ private Q_SLOTS: void onRunningGameChanged(const QString& filename, const QString& game_code, const QString& game_title); void onStartDiscActionTriggered(); + void onStartBIOSActionTriggered(); void onChangeDiscFromFileActionTriggered(); void onChangeDiscFromGameListActionTriggered(); void onGitHubRepositoryActionTriggered(); void onIssueTrackerActionTriggered(); void onAboutActionTriggered(); + void onGameListEntrySelected(const GameListEntry* entry); + void onGameListEntryDoubleClicked(const GameListEntry* entry); + void onGameListContextMenuRequested(const QPoint& point, const GameListEntry* entry); + protected: void closeEvent(QCloseEvent* event) override; diff --git a/src/duckstation-qt/qthostinterface.cpp b/src/duckstation-qt/qthostinterface.cpp index 296cfb307..f8712bb24 100644 --- a/src/duckstation-qt/qthostinterface.cpp +++ b/src/duckstation-qt/qthostinterface.cpp @@ -30,6 +30,8 @@ QtHostInterface::QtHostInterface(QObject* parent) : QObject(parent), CommonHostInterface(), m_qsettings(QString::fromStdString(GetSettingsFileName()), QSettings::IniFormat) { + qRegisterMetaType(); + loadSettings(); refreshGameList(); createThread(); @@ -149,15 +151,15 @@ QtDisplayWindow* QtHostInterface::createDisplayWindow() return m_display_window; } -void QtHostInterface::bootSystemFromFile(const QString& filename) +void QtHostInterface::bootSystem(const SystemBootParameters& params) { if (!isOnWorkerThread()) { - QMetaObject::invokeMethod(this, "bootSystemFromFile", Qt::QueuedConnection, Q_ARG(const QString&, filename)); + QMetaObject::invokeMethod(this, "bootSystem", Qt::QueuedConnection, Q_ARG(const SystemBootParameters&, params)); return; } - HostInterface::BootSystemFromFile(filename.toStdString().c_str()); + HostInterface::BootSystem(params); } void QtHostInterface::resumeSystemFromState(const QString& filename, bool boot_on_failure) @@ -175,17 +177,6 @@ void QtHostInterface::resumeSystemFromState(const QString& filename, bool boot_o HostInterface::ResumeSystemFromState(filename.toStdString().c_str(), boot_on_failure); } -void QtHostInterface::bootSystemFromBIOS() -{ - if (!isOnWorkerThread()) - { - QMetaObject::invokeMethod(this, "bootSystemFromBIOS", Qt::QueuedConnection); - return; - } - - HostInterface::BootSystemFromBIOS(); -} - void QtHostInterface::handleKeyEvent(int key, bool pressed) { if (!isOnWorkerThread()) @@ -469,6 +460,62 @@ void QtHostInterface::populateSaveStateMenus(const char* game_code, QMenu* load_ } } +void QtHostInterface::populateGameListContextMenu(const char* game_code, QWidget* parent_window, QMenu* menu) +{ + QAction* resume_action = menu->addAction(tr("Resume")); + resume_action->setEnabled(false); + + QMenu* load_state_menu = menu->addMenu(tr("Load State")); + load_state_menu->setEnabled(false); + + const std::vector available_states(GetAvailableSaveStates(game_code)); + for (const SaveStateInfo& ssi : available_states) + { + if (ssi.global) + continue; + + const s32 slot = ssi.slot; + const QDateTime timestamp(QDateTime::fromSecsSinceEpoch(static_cast(ssi.timestamp))); + const QString timestamp_str(timestamp.toString(Qt::SystemLocaleShortDate)); + const QString path(QString::fromStdString(ssi.path)); + + QAction* action; + if (slot < 0) + { + resume_action->setText(tr("Resume (%1)").arg(timestamp_str)); + resume_action->setEnabled(true); + action = resume_action; + } + else + { + load_state_menu->setEnabled(true); + action = load_state_menu->addAction(tr("%1 Save %2 (%3)").arg(tr("Game")).arg(slot).arg(timestamp_str)); + } + + connect(action, &QAction::triggered, [this, path]() { loadState(path); }); + } + + const bool has_any_states = resume_action->isEnabled() || load_state_menu->isEnabled(); + QAction* delete_save_states_action = menu->addAction(tr("Delete Save States...")); + delete_save_states_action->setEnabled(has_any_states); + if (has_any_states) + { + const std::string game_code_copy(game_code); + connect(delete_save_states_action, &QAction::triggered, [this, parent_window, game_code_copy] { + if (QMessageBox::warning( + parent_window, tr("Confirm Save State Deletion"), + tr("Are you sure you want to delete all save states for %1?\n\nThe saves will not be recoverable.") + .arg(game_code_copy.c_str()), + QMessageBox::Yes, QMessageBox::No) != QMessageBox::Yes) + { + return; + } + + DeleteSaveStates(game_code_copy.c_str(), true); + }); + } +} + void QtHostInterface::loadState(const QString& filename) { if (!isOnWorkerThread()) diff --git a/src/duckstation-qt/qthostinterface.h b/src/duckstation-qt/qthostinterface.h index 2bbcf0607..e4803887e 100644 --- a/src/duckstation-qt/qthostinterface.h +++ b/src/duckstation-qt/qthostinterface.h @@ -1,5 +1,6 @@ #pragma once #include "core/host_interface.h" +#include "core/system.h" #include "frontend-common/common_host_interface.h" #include "opengldisplaywindow.h" #include @@ -23,6 +24,8 @@ class QTimer; class GameList; +Q_DECLARE_METATYPE(SystemBootParameters); + class QtHostInterface : public QObject, private CommonHostInterface { Q_OBJECT @@ -52,6 +55,9 @@ public: void populateSaveStateMenus(const char* game_code, QMenu* load_menu, QMenu* save_menu); + /// Fills menu with save state info and handlers. + void populateGameListContextMenu(const char* game_code, QWidget* parent_window, QMenu* menu); + Q_SIGNALS: void errorReported(const QString& message); void messageReported(const QString& message); @@ -75,9 +81,8 @@ public Q_SLOTS: void applySettings(); void updateInputMap(); void handleKeyEvent(int key, bool pressed); - void bootSystemFromFile(const QString& filename); + void bootSystem(const SystemBootParameters& params); void resumeSystemFromState(const QString& filename, bool boot_on_failure); - void bootSystemFromBIOS(); void powerOffSystem(); void synchronousPowerOffSystem(); void resetSystem(); diff --git a/src/duckstation-sdl/main.cpp b/src/duckstation-sdl/main.cpp index e03e2f084..984145a24 100644 --- a/src/duckstation-sdl/main.cpp +++ b/src/duckstation-sdl/main.cpp @@ -47,7 +47,9 @@ static int Run(int argc, char* argv[]) // boot/load state if (boot_filename) { - if (host_interface->BootSystemFromFile(boot_filename) && state_index.has_value()) + SystemBootParameters boot_params; + boot_params.filename = boot_filename; + if (host_interface->BootSystem(boot_params) && state_index.has_value()) host_interface->LoadState(state_is_global, state_index.value()); } else if (state_index.has_value()) diff --git a/src/duckstation-sdl/sdl_host_interface.cpp b/src/duckstation-sdl/sdl_host_interface.cpp index 1fde71b3a..07a21da43 100644 --- a/src/duckstation-sdl/sdl_host_interface.cpp +++ b/src/duckstation-sdl/sdl_host_interface.cpp @@ -673,7 +673,10 @@ void SDLHostInterface::DrawMainMenuBar() } if (ImGui::MenuItem("Start BIOS", nullptr, false, !system_enabled)) { - RunLater([this]() { BootSystemFromBIOS(); }); + RunLater([this]() { + SystemBootParameters boot_params; + BootSystem(boot_params); + }); ClearImGuiFocus(); } @@ -995,7 +998,10 @@ void SDLHostInterface::DrawPoweredOffWindow() ImGui::SetCursorPosX(button_left); if (ImGui::Button("Start BIOS", button_size)) { - RunLater([this]() { BootSystemFromFile(nullptr); }); + RunLater([this]() { + SystemBootParameters boot_params; + BootSystem(boot_params); + }); ClearImGuiFocus(); } ImGui::NewLine(); @@ -1372,7 +1378,10 @@ void SDLHostInterface::DoStartDisc() return; AddFormattedOSDMessage(2.0f, "Starting disc from '%s'...", path); - BootSystemFromFile(path); + + SystemBootParameters boot_params; + boot_params.filename = path; + BootSystem(boot_params); } void SDLHostInterface::DoChangeDisc()