diff --git a/src/duckstation-qt/CMakeLists.txt b/src/duckstation-qt/CMakeLists.txt index 63b4ab99a..2e1ea28d7 100644 --- a/src/duckstation-qt/CMakeLists.txt +++ b/src/duckstation-qt/CMakeLists.txt @@ -12,6 +12,9 @@ set(SRCS audiosettingswidget.cpp audiosettingswidget.h audiosettingswidget.ui + autoupdaterdialog.cpp + autoupdaterdialog.h + autoupdaterdialog.ui consolesettingswidget.cpp consolesettingswidget.h consolesettingswidget.ui diff --git a/src/duckstation-qt/autoupdaterdialog.cpp b/src/duckstation-qt/autoupdaterdialog.cpp new file mode 100644 index 000000000..cbaf71f26 --- /dev/null +++ b/src/duckstation-qt/autoupdaterdialog.cpp @@ -0,0 +1,444 @@ +#include "autoupdaterdialog.h" +#include "common/file_system.h" +#include "common/log.h" +#include "common/minizip_helpers.h" +#include "common/string_util.h" +#include "qthostinterface.h" +#include "qtutils.h" +#include "scmversion/scmversion.h" +#include "unzip.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +Log_SetChannel(AutoUpdaterDialog); + +// Logic to detect whether we can use the auto updater. +// Currently Windows-only, and requires that the channel be defined by the buildbot. +#ifdef WIN32 +#if defined(__has_include) && __has_include("scmversion/tag.h") +#include "scmversion/tag.h" +#ifdef SCM_RELEASE_TAG +#define AUTO_UPDATER_SUPPORTED +#endif +#endif +#endif + +#ifdef AUTO_UPDATER_SUPPORTED + +static constexpr char LATEST_TAG_URL[] = "https://api.github.com/repos/stenzek/duckstation/tags"; +static constexpr char LATEST_RELEASE_URL[] = + "https://api.github.com/repos/stenzek/duckstation/releases/tags/" SCM_RELEASE_TAG; +static constexpr char UPDATE_ASSET_FILENAME[] = SCM_RELEASE_ASSET; + +#else + +static constexpr char LATEST_TAG_URL[] = ""; +static constexpr char LATEST_RELEASE_URL[] = ""; +static constexpr char UPDATE_ASSET_FILENAME[] = ""; + +#endif + +AutoUpdaterDialog::AutoUpdaterDialog(QtHostInterface* host_interface, QWidget* parent /* = nullptr */) + : QDialog(parent), m_host_interface(host_interface) +{ + m_network_access_mgr = new QNetworkAccessManager(this); + + m_ui.setupUi(this); + + setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); + + // m_ui.description->setTextInteractionFlags(Qt::TextBrowserInteraction); + // m_ui.description->setOpenExternalLinks(true); + + connect(m_ui.downloadAndInstall, &QPushButton::clicked, this, &AutoUpdaterDialog::downloadUpdateClicked); + connect(m_ui.skipThisUpdate, &QPushButton::clicked, this, &AutoUpdaterDialog::skipThisUpdateClicked); + connect(m_ui.remindMeLater, &QPushButton::clicked, this, &AutoUpdaterDialog::remindMeLaterClicked); +} + +AutoUpdaterDialog::~AutoUpdaterDialog() = default; + +bool AutoUpdaterDialog::isSupported() +{ +#ifdef AUTO_UPDATER_SUPPORTED + return true; +#else + return false; +#endif +} + +void AutoUpdaterDialog::reportError(const char* msg, ...) +{ + std::va_list ap; + va_start(ap, msg); + std::string full_msg = StringUtil::StdStringFromFormatV(msg, ap); + va_end(ap); + + QMessageBox::critical(this, tr("Updater Error"), QString::fromStdString(full_msg)); +} + +void AutoUpdaterDialog::queueUpdateCheck(bool display_message) +{ + connect(m_network_access_mgr, &QNetworkAccessManager::finished, this, &AutoUpdaterDialog::getLatestTagComplete); + + QUrl url(QUrl::fromEncoded(QByteArray(LATEST_TAG_URL, sizeof(LATEST_TAG_URL) - 1))); + QNetworkRequest request(url); + request.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true); + m_network_access_mgr->get(request); + + m_display_messages = display_message; +} + +void AutoUpdaterDialog::queueGetLatestRelease() +{ + connect(m_network_access_mgr, &QNetworkAccessManager::finished, this, &AutoUpdaterDialog::getLatestReleaseComplete); + + QUrl url(QUrl::fromEncoded(QByteArray(LATEST_RELEASE_URL, sizeof(LATEST_RELEASE_URL) - 1))); + QNetworkRequest request(url); + request.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true); + m_network_access_mgr->get(request); +} + +void AutoUpdaterDialog::getLatestTagComplete(QNetworkReply* reply) +{ + // this might fail due to a lack of internet connection - in which case, don't spam the user with messages every time. + m_network_access_mgr->disconnect(this); + reply->deleteLater(); + + if (reply->error() == QNetworkReply::NoError) + { + const QByteArray reply_json(reply->readAll()); + QJsonParseError parse_error; + QJsonDocument doc(QJsonDocument::fromJson(reply_json, &parse_error)); + if (doc.isArray()) + { + const QJsonArray doc_array(doc.array()); + for (const QJsonValue& val : doc_array) + { + if (!val.isObject()) + continue; + + if (val["name"].toString() != QStringLiteral(SCM_RELEASE_TAG)) + continue; + + m_latest_sha = val["commit"].toObject()["sha"].toString(); + if (m_latest_sha.isEmpty()) + continue; + + if (updateNeeded()) + { + queueGetLatestRelease(); + return; + } + else + { + if (m_display_messages) + QMessageBox::information(this, tr("Automatic Updater"), + tr("No updates are currently available. Please try again later.")); + emit updateCheckCompleted(); + return; + } + } + + if (m_display_messages) + reportError("latest release not found in JSON"); + } + else + { + if (m_display_messages) + reportError("JSON is not an array"); + } + } + else + { + if (m_display_messages) + reportError("Failed to download latest tag info: %d", static_cast(reply->error())); + } + + emit updateCheckCompleted(); +} + +void AutoUpdaterDialog::getLatestReleaseComplete(QNetworkReply* reply) +{ + m_network_access_mgr->disconnect(this); + reply->deleteLater(); + + if (reply->error() == QNetworkReply::NoError) + { + const QByteArray reply_json(reply->readAll()); + QJsonParseError parse_error; + QJsonDocument doc(QJsonDocument::fromJson(reply_json, &parse_error)); + if (doc.isObject()) + { + const QJsonObject doc_object(doc.object()); + + // search for the correct file + const QJsonArray assets(doc_object["assets"].toArray()); + const QString asset_filename(UPDATE_ASSET_FILENAME); + for (const QJsonValue& asset : assets) + { + const QJsonObject asset_obj(asset.toObject()); + if (asset_obj["name"] == asset_filename) + { + m_download_url = asset_obj["browser_download_url"].toString(); + if (!m_download_url.isEmpty()) + { + m_ui.currentVersion->setText(tr("Current Version: %1 (%2)").arg(g_scm_hash_str).arg(__TIMESTAMP__)); + m_ui.newVersion->setText( + tr("New Version: %1 (%2)").arg(m_latest_sha).arg(doc_object["published_at"].toString())); + m_ui.updateNotes->setText(doc_object["body"].toString()); + exec(); + emit updateCheckCompleted(); + return; + } + + break; + } + } + + reportError("Asset/asset download not found"); + } + else + { + reportError("JSON is not an object"); + } + } + else + { + reportError("Failed to download latest release info: %d", static_cast(reply->error())); + } + + emit updateCheckCompleted(); +} + +void AutoUpdaterDialog::downloadUpdateClicked() +{ + QUrl url(m_download_url); + QNetworkRequest request(url); + request.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true); + QNetworkReply* reply = m_network_access_mgr->get(request); + + QProgressDialog progress(tr("Downloading %1...").arg(m_download_url), tr("Cancel"), 0, 1); + progress.setWindowTitle(tr("Automatic Updater")); + progress.setWindowIcon(windowIcon()); + progress.setAutoClose(false); + + connect(reply, &QNetworkReply::downloadProgress, [&progress](quint64 received, quint64 total) { + progress.setRange(0, static_cast(total)); + progress.setValue(static_cast(received)); + }); + + connect(m_network_access_mgr, &QNetworkAccessManager::finished, [this, &progress](QNetworkReply* reply) { + m_network_access_mgr->disconnect(); + + if (reply->error() != QNetworkReply::NoError) + { + reportError("Download failed: %s", reply->errorString().toUtf8().constData()); + progress.done(-1); + return; + } + + const QByteArray data = reply->readAll(); + if (data.isEmpty()) + { + reportError("Download failed: Update is empty"); + progress.done(-1); + return; + } + + if (processUpdate(data)) + progress.done(1); + else + progress.done(-1); + }); + + const int result = progress.exec(); + if (result == 0) + { + // cancelled + reply->abort(); + } + else if (result == 1) + { + // updater started + m_host_interface->requestExit(); + done(0); + } + + reply->deleteLater(); +} + +bool AutoUpdaterDialog::updateNeeded() const +{ + QString last_checked_sha = + QString::fromStdString(m_host_interface->GetStringSettingValue("AutoUpdater", "LastVersion")); + + Log_InfoPrintf("Current SHA: %s", g_scm_hash_str); + Log_InfoPrintf("Latest SHA: %s", m_latest_sha.toUtf8().constData()); + Log_InfoPrintf("Last Checked SHA: %s", last_checked_sha.toUtf8().constData()); + if (m_latest_sha == g_scm_hash_str || m_latest_sha == last_checked_sha) + { + Log_InfoPrintf("No update needed."); + return false; + } + + Log_InfoPrintf("Update needed."); + return true; +} + +void AutoUpdaterDialog::skipThisUpdateClicked() +{ + m_host_interface->SetStringSettingValue("AutoUpdater", "LastVersion", m_latest_sha.toUtf8().constData()); + done(0); +} + +void AutoUpdaterDialog::remindMeLaterClicked() +{ + done(0); +} + +#ifdef WIN32 + +bool AutoUpdaterDialog::processUpdate(const QByteArray& update_data) +{ + const QString update_directory = QCoreApplication::applicationDirPath(); + const QString update_zip_path = update_directory + QStringLiteral("\\update.zip"); + const QString updater_path = update_directory + QStringLiteral("\\updater.exe"); + + Q_ASSERT(!update_zip_path.isEmpty() && !updater_path.isEmpty() && !update_directory.isEmpty()); + if ((QFile::exists(update_zip_path) && !QFile::remove(update_zip_path)) || + (QFile::exists(updater_path) && !QFile::remove(updater_path))) + { + reportError("Removing existing update zip/updater failed"); + return false; + } + + { + QFile update_zip_file(update_zip_path); + if (!update_zip_file.open(QIODevice::WriteOnly) || update_zip_file.write(update_data) != update_data.size()) + { + reportError("Writing update zip to '%s' failed", update_zip_path.toUtf8().constData()); + return false; + } + update_zip_file.close(); + } + + if (!extractUpdater(update_zip_path, updater_path)) + { + reportError("Extracting updater failed"); + return false; + } + + if (!doUpdate(update_zip_path, updater_path, update_directory)) + { + reportError("Launching updater failed"); + return false; + } + + return true; +} + +bool AutoUpdaterDialog::extractUpdater(const QString& zip_path, const QString& destination_path) +{ + unzFile zf = MinizipHelpers::OpenUnzFile(zip_path.toUtf8().constData()); + if (!zf) + { + reportError("Failed to open update zip"); + return false; + } + + if (unzLocateFile(zf, "updater.exe", 0) != UNZ_OK || unzOpenCurrentFile(zf) != UNZ_OK) + { + reportError("Failed to locate updater.exe"); + unzClose(zf); + return false; + } + + QFile updater_exe(destination_path); + if (!updater_exe.open(QIODevice::WriteOnly)) + { + reportError("Failed to open updater.exe for writing"); + unzClose(zf); + return false; + } + + static constexpr size_t CHUNK_SIZE = 4096; + char chunk[CHUNK_SIZE]; + for (;;) + { + int size = unzReadCurrentFile(zf, chunk, CHUNK_SIZE); + if (size < 0) + { + reportError("Failed to decompress updater exe"); + unzClose(zf); + updater_exe.close(); + updater_exe.remove(); + return false; + } + else if (size == 0) + { + break; + } + + if (updater_exe.write(chunk, size) != size) + { + reportError("Failed to write updater exe"); + unzClose(zf); + updater_exe.close(); + updater_exe.remove(); + return false; + } + } + + unzClose(zf); + updater_exe.close(); + return true; +} + +bool AutoUpdaterDialog::doUpdate(const QString& zip_path, const QString& updater_path, const QString& destination_path) +{ + const QString program_path = QCoreApplication::applicationFilePath(); + if (program_path.isEmpty()) + { + reportError("Failed to get current application path"); + return false; + } + + QStringList arguments; + arguments << QStringLiteral("%1").arg(QCoreApplication::applicationPid()); + arguments << destination_path; + arguments << zip_path; + arguments << program_path; + + // this will leak, but not sure how else to handle it... + QProcess* updater_process = new QProcess(); + updater_process->setProgram(updater_path); + updater_process->setArguments(arguments); + updater_process->start(QIODevice::NotOpen); + if (!updater_process->waitForStarted()) + { + reportError("Failed to start updater"); + return false; + } + + return true; +} + +#else + +bool AutoUpdaterDialog::processUpdate(const QByteArray& update_data) +{ + return false; +} + +#endif diff --git a/src/duckstation-qt/autoupdaterdialog.h b/src/duckstation-qt/autoupdaterdialog.h new file mode 100644 index 000000000..dcd3b4195 --- /dev/null +++ b/src/duckstation-qt/autoupdaterdialog.h @@ -0,0 +1,55 @@ +#pragma once +#include "ui_autoupdaterdialog.h" +#include + +class QNetworkAccessManager; +class QNetworkReply; + +class QtHostInterface; + +class AutoUpdaterDialog final : public QDialog +{ + Q_OBJECT + +public: + explicit AutoUpdaterDialog(QtHostInterface* host_interface, QWidget* parent = nullptr); + ~AutoUpdaterDialog(); + + static bool isSupported(); + +Q_SIGNALS: + void updateCheckCompleted(); + +public Q_SLOTS: + void queueUpdateCheck(bool display_message); + void queueGetLatestRelease(); + +private Q_SLOTS: + void getLatestTagComplete(QNetworkReply* reply); + void getLatestReleaseComplete(QNetworkReply* reply); + + void downloadUpdateClicked(); + void skipThisUpdateClicked(); + void remindMeLaterClicked(); + +private: + void reportError(const char* msg, ...); + bool updateNeeded() const; + +#ifdef WIN32 + bool processUpdate(const QByteArray& update_data); + bool extractUpdater(const QString& zip_path, const QString& destination_path); + bool doUpdate(const QString& zip_path, const QString& updater_path, const QString& destination_path); +#else + bool processUpdate(const QByteArray& update_data); +#endif + + Ui::AutoUpdaterDialog m_ui; + + QtHostInterface* m_host_interface; + QNetworkAccessManager* m_network_access_mgr = nullptr; + QString m_latest_sha; + QString m_download_url; + + bool m_display_messages = false; +}; diff --git a/src/duckstation-qt/autoupdaterdialog.ui b/src/duckstation-qt/autoupdaterdialog.ui new file mode 100644 index 000000000..a5b456956 --- /dev/null +++ b/src/duckstation-qt/autoupdaterdialog.ui @@ -0,0 +1,110 @@ + + + AutoUpdaterDialog + + + Qt::ApplicationModal + + + + 0 + 0 + 651 + 474 + + + + Automatic Updater + + + true + + + + + + + 16 + 75 + true + + + + Update Available + + + + + + + Current Version: + + + + + + + New Version: + + + + + + + Update Notes: + + + + + + + true + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Download and Install... + + + + + + + Skip This Update + + + + + + + Remind Me Later + + + true + + + + + + + + + + diff --git a/src/duckstation-qt/duckstation-qt.vcxproj b/src/duckstation-qt/duckstation-qt.vcxproj index ccc5f6842..13df310ed 100644 --- a/src/duckstation-qt/duckstation-qt.vcxproj +++ b/src/duckstation-qt/duckstation-qt.vcxproj @@ -38,6 +38,7 @@ + @@ -74,6 +75,7 @@ + @@ -145,6 +147,7 @@ + @@ -195,6 +198,11 @@ + + + Document + + diff --git a/src/duckstation-qt/duckstation-qt.vcxproj.filters b/src/duckstation-qt/duckstation-qt.vcxproj.filters index a97a5dd23..ce67f9945 100644 --- a/src/duckstation-qt/duckstation-qt.vcxproj.filters +++ b/src/duckstation-qt/duckstation-qt.vcxproj.filters @@ -46,6 +46,8 @@ + + @@ -85,6 +87,7 @@ + @@ -98,6 +101,7 @@ + diff --git a/src/duckstation-qt/generalsettingswidget.cpp b/src/duckstation-qt/generalsettingswidget.cpp index 634ab1f8e..2f3d4e67d 100644 --- a/src/duckstation-qt/generalsettingswidget.cpp +++ b/src/duckstation-qt/generalsettingswidget.cpp @@ -1,4 +1,5 @@ #include "generalsettingswidget.h" +#include "autoupdaterdialog.h" #include "settingsdialog.h" #include "settingwidgetbinder.h" @@ -80,16 +81,27 @@ GeneralSettingsWidget::GeneralSettingsWidget(QtHostInterface* host_interface, QW tr("Shows the current emulation speed of the system in the top-right corner of the display as a percentage.")); // Since this one is compile-time selected, we don't put it in the .ui file. + const int last_row_count = m_ui.formLayout_4->rowCount(); #ifdef WITH_DISCORD_PRESENCE { QCheckBox* enableDiscordPresence = new QCheckBox(tr("Enable Discord Presence"), m_ui.groupBox_4); SettingWidgetBinder::BindWidgetToBoolSetting(m_host_interface, enableDiscordPresence, "Main", "EnableDiscordPresence"); - m_ui.formLayout_4->addWidget(enableDiscordPresence, m_ui.formLayout_4->rowCount(), 0); + m_ui.formLayout_4->addWidget(enableDiscordPresence, last_row_count, 0); dialog->registerWidgetHelp(enableDiscordPresence, tr("Enable Discord Presence"), tr("Unchecked"), tr("Shows the game you are currently playing as part of your profile in Discord.")); } #endif + if (AutoUpdaterDialog::isSupported()) + { + QCheckBox* enableDiscordPresence = new QCheckBox(tr("Enable Automatic Update Check"), m_ui.groupBox_4); + SettingWidgetBinder::BindWidgetToBoolSetting(m_host_interface, enableDiscordPresence, "AutoUpdater", + "CheckAtStartup"); + m_ui.formLayout_4->addWidget(enableDiscordPresence, last_row_count, 1); + dialog->registerWidgetHelp(enableDiscordPresence, tr("Enable Automatic Update Check"), tr("Checked"), + tr("Automatically checks for updates to the program on startup. Updates can be deferred " + "until later or skipped entirely.")); + } } GeneralSettingsWidget::~GeneralSettingsWidget() = default; diff --git a/src/duckstation-qt/main.cpp b/src/duckstation-qt/main.cpp index 7f5ce7804..8eb740aaf 100644 --- a/src/duckstation-qt/main.cpp +++ b/src/duckstation-qt/main.cpp @@ -46,6 +46,10 @@ int main(int argc, char* argv[]) host_interface->bootSystem(*boot_params); boot_params.reset(); } + else + { + window->startupUpdateCheck(); + } int result = app.exec(); diff --git a/src/duckstation-qt/mainwindow.cpp b/src/duckstation-qt/mainwindow.cpp index 22b92b136..374662089 100644 --- a/src/duckstation-qt/mainwindow.cpp +++ b/src/duckstation-qt/mainwindow.cpp @@ -1,5 +1,6 @@ #include "mainwindow.h" #include "aboutdialog.h" +#include "autoupdaterdialog.h" #include "common/assert.h" #include "core/game_list.h" #include "core/host_display.h" @@ -609,6 +610,7 @@ void MainWindow::connectSignals() connect(m_ui.actionIssueTracker, &QAction::triggered, this, &MainWindow::onIssueTrackerActionTriggered); connect(m_ui.actionDiscordServer, &QAction::triggered, this, &MainWindow::onDiscordServerActionTriggered); connect(m_ui.actionAbout, &QAction::triggered, this, &MainWindow::onAboutActionTriggered); + connect(m_ui.actionCheckForUpdates, &QAction::triggered, [this]() { checkForUpdates(true); }); connect(m_host_interface, &QtHostInterface::errorReported, this, &MainWindow::reportError, Qt::BlockingQueuedConnection); @@ -820,3 +822,38 @@ void MainWindow::changeEvent(QEvent* event) QMainWindow::changeEvent(event); } + +void MainWindow::startupUpdateCheck() +{ + if (!m_host_interface->GetBoolSettingValue("AutoUpdater", "CheckAtStartup", true)) + return; + + checkForUpdates(false); +} + +void MainWindow::checkForUpdates(bool display_message) +{ + if (!AutoUpdaterDialog::isSupported()) + { + if (display_message) + QMessageBox::critical(this, tr("Updater Error"), tr("Updates are not supported on this build.")); + + return; + } + + if (m_auto_updater_dialog) + return; + + m_auto_updater_dialog = new AutoUpdaterDialog(m_host_interface, 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; +} diff --git a/src/duckstation-qt/mainwindow.h b/src/duckstation-qt/mainwindow.h index 82978fbf1..1facc3fa8 100644 --- a/src/duckstation-qt/mainwindow.h +++ b/src/duckstation-qt/mainwindow.h @@ -13,6 +13,7 @@ class QThread; class GameListWidget; class QtHostInterface; class QtDisplayWidget; +class AutoUpdaterDialog; class HostDisplay; struct GameListEntry; @@ -25,6 +26,9 @@ public: explicit MainWindow(QtHostInterface* host_interface); ~MainWindow(); + /// Performs update check if enabled in settings. + void startupUpdateCheck(); + private Q_SLOTS: void reportError(const QString& message); void reportMessage(const QString& message); @@ -61,6 +65,9 @@ private Q_SLOTS: void onGameListEntryDoubleClicked(const GameListEntry* entry); void onGameListContextMenuRequested(const QPoint& point, const GameListEntry* entry); + void checkForUpdates(bool display_message); + void onUpdateCheckComplete(); + protected: void closeEvent(QCloseEvent* event) override; void changeEvent(QEvent* event) override; @@ -94,6 +101,7 @@ private: QLabel* m_status_frame_time_widget = nullptr; SettingsDialog* m_settings_dialog = nullptr; + AutoUpdaterDialog* m_auto_updater_dialog = nullptr; bool m_emulation_running = false; }; diff --git a/src/duckstation-qt/mainwindow.ui b/src/duckstation-qt/mainwindow.ui index 13dfb1458..a8d927a7d 100644 --- a/src/duckstation-qt/mainwindow.ui +++ b/src/duckstation-qt/mainwindow.ui @@ -122,6 +122,8 @@ + + @@ -346,6 +348,11 @@ &Discord Server... + + + Check for &Updates... + + &About... diff --git a/src/duckstation-qt/qthostinterface.cpp b/src/duckstation-qt/qthostinterface.cpp index 710aa01ee..dfecfcda4 100644 --- a/src/duckstation-qt/qthostinterface.cpp +++ b/src/duckstation-qt/qthostinterface.cpp @@ -2,6 +2,7 @@ #include "common/assert.h" #include "common/audio_stream.h" #include "common/byte_stream.h" +#include "common/file_system.h" #include "common/log.h" #include "common/string_util.h" #include "core/controller.h" @@ -720,7 +721,7 @@ void QtHostInterface::saveInputProfile(const QString& profile_name) QString QtHostInterface::getUserDirectoryRelativePath(const QString& arg) const { QString result = QString::fromStdString(m_user_directory); - result += '/'; + result += FS_OSPATH_SEPERATOR_CHARACTER; result += arg; return result; } @@ -728,11 +729,16 @@ QString QtHostInterface::getUserDirectoryRelativePath(const QString& arg) const QString QtHostInterface::getProgramDirectoryRelativePath(const QString& arg) const { QString result = QString::fromStdString(m_program_directory); - result += '/'; + result += FS_OSPATH_SEPERATOR_CHARACTER; result += arg; return result; } +QString QtHostInterface::getProgramDirectory() const +{ + return QString::fromStdString(m_program_directory); +} + void QtHostInterface::powerOffSystem() { if (!isOnWorkerThread()) diff --git a/src/duckstation-qt/qthostinterface.h b/src/duckstation-qt/qthostinterface.h index 96de18982..86f613605 100644 --- a/src/duckstation-qt/qthostinterface.h +++ b/src/duckstation-qt/qthostinterface.h @@ -71,6 +71,7 @@ public: ALWAYS_INLINE const HotkeyInfoList& getHotkeyInfoList() const { return GetHotkeyInfoList(); } ALWAYS_INLINE ControllerInterface* getControllerInterface() const { return GetControllerInterface(); } ALWAYS_INLINE bool inBatchMode() const { return InBatchMode(); } + ALWAYS_INLINE void requestExit() { RequestExit(); } ALWAYS_INLINE bool isOnWorkerThread() const { return QThread::currentThread() == m_worker_thread; } @@ -99,6 +100,9 @@ public: /// Returns a list of supported languages and codes (suffixes for translation files). static std::vector> getAvailableLanguageList(); + /// Returns program directory as a QString. + QString getProgramDirectory() const; + Q_SIGNALS: void errorReported(const QString& message); void messageReported(const QString& message);