Qt: Implement context menu in game list

This commit is contained in:
Connor McLaughlin
2020-03-02 11:08:16 +10:00
parent 0c40903f74
commit 69f03959aa
12 changed files with 305 additions and 91 deletions

View File

@ -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<int>(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<int>(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<int>(m_game_list->GetEntryCount()))
return nullptr;
return &m_game_list->GetEntries().at(source_index.row());
}

View File

@ -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;

View File

@ -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 <QtCore/QFileInfo>
#include <QtCore/QUrl>
#include <QtGui/QDesktopServices>
#include <QtWidgets/QFileDialog>
@ -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<int>(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<int>(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);

View File

@ -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;

View File

@ -30,6 +30,8 @@ QtHostInterface::QtHostInterface(QObject* parent)
: QObject(parent), CommonHostInterface(),
m_qsettings(QString::fromStdString(GetSettingsFileName()), QSettings::IniFormat)
{
qRegisterMetaType<SystemBootParameters>();
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<SaveStateInfo> 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<qint64>(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())

View File

@ -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 <QtCore/QByteArray>
@ -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();