diff --git a/launcher/Application.cpp b/launcher/Application.cpp index 0c9f0487..aa937964 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -685,6 +685,8 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv) m_settings->registerSetting("UpdateDialogGeometry", ""); + m_settings->registerSetting("ModDownloadGeometry", ""); + // HACK: This code feels so stupid is there a less stupid way of doing this? { m_settings->registerSetting("PastebinURL", ""); diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index 490202cf..dfd56d26 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -896,6 +896,8 @@ SET(LAUNCHER_SOURCES ui/widgets/PageContainer.cpp ui/widgets/PageContainer.h ui/widgets/PageContainer_p.h + ui/widgets/ProjectItem.h + ui/widgets/ProjectItem.cpp ui/widgets/VersionListView.cpp ui/widgets/VersionListView.h ui/widgets/VersionSelectWidget.cpp diff --git a/launcher/modplatform/ModAPI.h b/launcher/modplatform/ModAPI.h index 4114d83c..c7408835 100644 --- a/launcher/modplatform/ModAPI.h +++ b/launcher/modplatform/ModAPI.h @@ -73,7 +73,7 @@ class ModAPI { }; virtual void searchMods(CallerType* caller, SearchArgs&& args) const = 0; - virtual void getModInfo(CallerType* caller, ModPlatform::IndexedPack& pack) = 0; + virtual void getModInfo(ModPlatform::IndexedPack& pack, std::function callback) = 0; virtual auto getProject(QString addonId, QByteArray* response) const -> NetJob* = 0; virtual auto getProjects(QStringList addonIds, QByteArray* response) const -> NetJob* = 0; @@ -85,7 +85,7 @@ class ModAPI { ModLoaderTypes loaders; }; - virtual void getVersions(CallerType* caller, VersionSearchArgs&& args) const = 0; + virtual void getVersions(VersionSearchArgs&& args, std::function callback) const = 0; static auto getModLoaderString(ModLoaderType type) -> const QString { switch (type) { diff --git a/launcher/modplatform/ModIndex.h b/launcher/modplatform/ModIndex.h index 89fe1c5c..518fed7c 100644 --- a/launcher/modplatform/ModIndex.h +++ b/launcher/modplatform/ModIndex.h @@ -75,6 +75,8 @@ struct ExtraPackData { QString sourceUrl; QString wikiUrl; QString discordUrl; + + QString body; }; struct IndexedPack { diff --git a/launcher/modplatform/flame/FlameAPI.cpp b/launcher/modplatform/flame/FlameAPI.cpp index 0ff04f72..9c74918b 100644 --- a/launcher/modplatform/flame/FlameAPI.cpp +++ b/launcher/modplatform/flame/FlameAPI.cpp @@ -67,6 +67,43 @@ auto FlameAPI::getModFileChangelog(int modId, int fileId) -> QString return changelog; } +auto FlameAPI::getModDescription(int modId) -> QString +{ + QEventLoop lock; + QString description; + + auto* netJob = new NetJob(QString("Flame::ModDescription"), APPLICATION->network()); + auto* response = new QByteArray(); + netJob->addNetAction(Net::Download::makeByteArray( + QString("https://api.curseforge.com/v1/mods/%1/description") + .arg(QString::number(modId)), response)); + + QObject::connect(netJob, &NetJob::succeeded, [netJob, response, &description] { + QJsonParseError parse_error{}; + QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); + if (parse_error.error != QJsonParseError::NoError) { + qWarning() << "Error while parsing JSON response from Flame::ModDescription at " << parse_error.offset + << " reason: " << parse_error.errorString(); + qWarning() << *response; + + netJob->failed(parse_error.errorString()); + return; + } + + description = Json::ensureString(doc.object(), "data"); + }); + + QObject::connect(netJob, &NetJob::finished, [response, &lock] { + delete response; + lock.quit(); + }); + + netJob->start(); + lock.exec(); + + return description; +} + auto FlameAPI::getLatestVersion(VersionSearchArgs&& args) -> ModPlatform::IndexedVersion { QEventLoop loop; diff --git a/launcher/modplatform/flame/FlameAPI.h b/launcher/modplatform/flame/FlameAPI.h index 336df387..4eac0664 100644 --- a/launcher/modplatform/flame/FlameAPI.h +++ b/launcher/modplatform/flame/FlameAPI.h @@ -7,6 +7,7 @@ class FlameAPI : public NetworkModAPI { public: auto matchFingerprints(const QList& fingerprints, QByteArray* response) -> NetJob::Ptr; auto getModFileChangelog(int modId, int fileId) -> QString; + auto getModDescription(int modId) -> QString; auto getLatestVersion(VersionSearchArgs&& args) -> ModPlatform::IndexedVersion; diff --git a/launcher/modplatform/flame/FlameModIndex.cpp b/launcher/modplatform/flame/FlameModIndex.cpp index 746018e2..32aa4bdb 100644 --- a/launcher/modplatform/flame/FlameModIndex.cpp +++ b/launcher/modplatform/flame/FlameModIndex.cpp @@ -4,10 +4,9 @@ #include "minecraft/MinecraftInstance.h" #include "minecraft/PackProfile.h" #include "modplatform/flame/FlameAPI.h" -#include "net/NetJob.h" -static ModPlatform::ProviderCapabilities ProviderCaps; static FlameAPI api; +static ModPlatform::ProviderCapabilities ProviderCaps; void FlameMod::loadIndexedPack(ModPlatform::IndexedPack& pack, QJsonObject& obj) { @@ -31,10 +30,11 @@ void FlameMod::loadIndexedPack(ModPlatform::IndexedPack& pack, QJsonObject& obj) pack.authors.append(packAuthor); } - loadExtraPackData(pack, obj); + pack.extraDataLoaded = false; + loadURLs(pack, obj); } -void FlameMod::loadExtraPackData(ModPlatform::IndexedPack& pack, QJsonObject& obj) +void FlameMod::loadURLs(ModPlatform::IndexedPack& pack, QJsonObject& obj) { auto links_obj = Json::ensureObject(obj, "links"); @@ -50,7 +50,16 @@ void FlameMod::loadExtraPackData(ModPlatform::IndexedPack& pack, QJsonObject& ob if(pack.extraData.wikiUrl.endsWith('/')) pack.extraData.wikiUrl.chop(1); - pack.extraDataLoaded = true; + if (!pack.extraData.body.isEmpty()) + pack.extraDataLoaded = true; +} + +void FlameMod::loadBody(ModPlatform::IndexedPack& pack, QJsonObject& obj) +{ + pack.extraData.body = api.getModDescription(pack.addonId.toInt()); + + if (!pack.extraData.issuesUrl.isEmpty() || !pack.extraData.sourceUrl.isEmpty() || !pack.extraData.wikiUrl.isEmpty()) + pack.extraDataLoaded = true; } static QString enumToString(int hash_algorithm) diff --git a/launcher/modplatform/flame/FlameModIndex.h b/launcher/modplatform/flame/FlameModIndex.h index a839dd83..db63cdbb 100644 --- a/launcher/modplatform/flame/FlameModIndex.h +++ b/launcher/modplatform/flame/FlameModIndex.h @@ -12,7 +12,8 @@ namespace FlameMod { void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj); -void loadExtraPackData(ModPlatform::IndexedPack& m, QJsonObject& obj); +void loadURLs(ModPlatform::IndexedPack& m, QJsonObject& obj); +void loadBody(ModPlatform::IndexedPack& m, QJsonObject& obj); void loadIndexedPackVersions(ModPlatform::IndexedPack& pack, QJsonArray& arr, const shared_qobject_ptr& network, diff --git a/launcher/modplatform/helpers/NetworkModAPI.cpp b/launcher/modplatform/helpers/NetworkModAPI.cpp index 90edfe31..866e7540 100644 --- a/launcher/modplatform/helpers/NetworkModAPI.cpp +++ b/launcher/modplatform/helpers/NetworkModAPI.cpp @@ -31,48 +31,48 @@ void NetworkModAPI::searchMods(CallerType* caller, SearchArgs&& args) const netJob->start(); } -void NetworkModAPI::getModInfo(CallerType* caller, ModPlatform::IndexedPack& pack) +void NetworkModAPI::getModInfo(ModPlatform::IndexedPack& pack, std::function callback) { auto response = new QByteArray(); auto job = getProject(pack.addonId.toString(), response); - QObject::connect(job, &NetJob::succeeded, caller, [caller, &pack, response] { + QObject::connect(job, &NetJob::succeeded, [callback, &pack, response] { QJsonParseError parse_error{}; QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); if (parse_error.error != QJsonParseError::NoError) { - qWarning() << "Error while parsing JSON response from " << caller->debugName() << " at " << parse_error.offset + qWarning() << "Error while parsing JSON response for mod info at " << parse_error.offset << " reason: " << parse_error.errorString(); qWarning() << *response; return; } - caller->infoRequestFinished(doc, pack); + callback(doc, pack); }); job->start(); } -void NetworkModAPI::getVersions(CallerType* caller, VersionSearchArgs&& args) const +void NetworkModAPI::getVersions(VersionSearchArgs&& args, std::function callback) const { - auto netJob = new NetJob(QString("%1::ModVersions(%2)").arg(caller->debugName()).arg(args.addonId), APPLICATION->network()); + auto netJob = new NetJob(QString("ModVersions(%2)").arg(args.addonId), APPLICATION->network()); auto response = new QByteArray(); netJob->addNetAction(Net::Download::makeByteArray(getVersionsURL(args), response)); - QObject::connect(netJob, &NetJob::succeeded, caller, [response, caller, args] { + QObject::connect(netJob, &NetJob::succeeded, [response, callback, args] { QJsonParseError parse_error{}; QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); if (parse_error.error != QJsonParseError::NoError) { - qWarning() << "Error while parsing JSON response from " << caller->debugName() << " at " << parse_error.offset + qWarning() << "Error while parsing JSON response for getting versions at " << parse_error.offset << " reason: " << parse_error.errorString(); qWarning() << *response; return; } - caller->versionRequestSucceeded(doc, args.addonId); + callback(doc, args.addonId); }); - QObject::connect(netJob, &NetJob::finished, caller, [response, netJob] { + QObject::connect(netJob, &NetJob::finished, [response, netJob] { netJob->deleteLater(); delete response; }); diff --git a/launcher/modplatform/helpers/NetworkModAPI.h b/launcher/modplatform/helpers/NetworkModAPI.h index 989bcec4..b8af22c7 100644 --- a/launcher/modplatform/helpers/NetworkModAPI.h +++ b/launcher/modplatform/helpers/NetworkModAPI.h @@ -5,8 +5,8 @@ class NetworkModAPI : public ModAPI { public: void searchMods(CallerType* caller, SearchArgs&& args) const override; - void getModInfo(CallerType* caller, ModPlatform::IndexedPack& pack) override; - void getVersions(CallerType* caller, VersionSearchArgs&& args) const override; + void getModInfo(ModPlatform::IndexedPack& pack, std::function callback) override; + void getVersions(VersionSearchArgs&& args, std::function callback) const override; auto getProject(QString addonId, QByteArray* response) const -> NetJob* override; diff --git a/launcher/modplatform/modrinth/ModrinthPackIndex.cpp b/launcher/modplatform/modrinth/ModrinthPackIndex.cpp index e50dd96d..3e53becb 100644 --- a/launcher/modplatform/modrinth/ModrinthPackIndex.cpp +++ b/launcher/modplatform/modrinth/ModrinthPackIndex.cpp @@ -87,6 +87,8 @@ void Modrinth::loadExtraPackData(ModPlatform::IndexedPack& pack, QJsonObject& ob pack.extraData.donate.append(donate); } + pack.extraData.body = Json::ensureString(obj, "body"); + pack.extraDataLoaded = true; } diff --git a/launcher/ui/dialogs/ModDownloadDialog.cpp b/launcher/ui/dialogs/ModDownloadDialog.cpp index bc5e5206..7382d1cf 100644 --- a/launcher/ui/dialogs/ModDownloadDialog.cpp +++ b/launcher/ui/dialogs/ModDownloadDialog.cpp @@ -19,36 +19,33 @@ #include "ModDownloadDialog.h" #include -#include #include +#include #include "Application.h" -#include "ProgressDialog.h" #include "ReviewMessageBox.h" +#include #include #include #include -#include -#include "ui/widgets/PageContainer.h" -#include "ui/pages/modplatform/modrinth/ModrinthModPage.h" #include "ModDownloadTask.h" +#include "ui/pages/modplatform/flame/FlameModPage.h" +#include "ui/pages/modplatform/modrinth/ModrinthModPage.h" +#include "ui/widgets/PageContainer.h" - -ModDownloadDialog::ModDownloadDialog(const std::shared_ptr &mods, QWidget *parent, - BaseInstance *instance) - : QDialog(parent), mods(mods), m_instance(instance) +ModDownloadDialog::ModDownloadDialog(const std::shared_ptr& mods, QWidget* parent, BaseInstance* instance) + : QDialog(parent), mods(mods), m_verticalLayout(new QVBoxLayout(this)), m_instance(instance) { setObjectName(QStringLiteral("ModDownloadDialog")); - - resize(std::max(0.5*parent->width(), 400.0), std::max(0.75*parent->height(), 400.0)); - - m_verticalLayout = new QVBoxLayout(this); m_verticalLayout->setObjectName(QStringLiteral("verticalLayout")); + resize(std::max(0.5 * parent->width(), 400.0), std::max(0.75 * parent->height(), 400.0)); + setWindowIcon(APPLICATION->getThemedIcon("new")); - // NOTE: m_buttons must be initialized before PageContainer, because it indirectly accesses m_buttons through setSuggestedPack! Do not move this below. + // NOTE: m_buttons must be initialized before PageContainer, because it indirectly accesses m_buttons through setSuggestedPack! Do not + // move this below. m_buttons = new QDialogButtonBox(QDialogButtonBox::Help | QDialogButtonBox::Ok | QDialogButtonBox::Cancel); m_container = new PageContainer(this); @@ -58,12 +55,17 @@ ModDownloadDialog::ModDownloadDialog(const std::shared_ptr &mods m_container->addButtons(m_buttons); + connect(m_container, &PageContainer::selectedPageChanged, this, &ModDownloadDialog::selectedPageChanged); + // Bonk Qt over its stupid head and make sure it understands which button is the default one... // See: https://stackoverflow.com/questions/24556831/qbuttonbox-set-default-button auto OkButton = m_buttons->button(QDialogButtonBox::Ok); OkButton->setEnabled(false); OkButton->setDefault(true); OkButton->setAutoDefault(true); + OkButton->setText(tr("Review and confirm")); + OkButton->setShortcut(tr("Ctrl+Return")); + OkButton->setToolTip(tr("Opens a new popup to review your selected mods and confirm your selection. Shortcut: Ctrl+Return")); connect(OkButton, &QPushButton::clicked, this, &ModDownloadDialog::confirm); auto CancelButton = m_buttons->button(QDialogButtonBox::Cancel); @@ -78,7 +80,9 @@ ModDownloadDialog::ModDownloadDialog(const std::shared_ptr &mods QMetaObject::connectSlotsByName(this); setWindowModality(Qt::WindowModal); - setWindowTitle("Download mods"); + setWindowTitle(dialogTitle()); + + restoreGeometry(QByteArray::fromBase64(APPLICATION->settings()->get("ModDownloadGeometry").toByteArray())); } QString ModDownloadDialog::dialogTitle() @@ -88,6 +92,7 @@ QString ModDownloadDialog::dialogTitle() void ModDownloadDialog::reject() { + APPLICATION->settings()->set("ModDownloadGeometry", saveGeometry().toBase64()); QDialog::reject(); } @@ -114,12 +119,13 @@ void ModDownloadDialog::confirm() void ModDownloadDialog::accept() { + APPLICATION->settings()->set("ModDownloadGeometry", saveGeometry().toBase64()); QDialog::accept(); } -QList ModDownloadDialog::getPages() +QList ModDownloadDialog::getPages() { - QList pages; + QList pages; pages.append(new ModrinthModPage(this, m_instance)); if (APPLICATION->capabilities() & Application::SupportsFlame) @@ -128,7 +134,7 @@ QList ModDownloadDialog::getPages() return pages; } -void ModDownloadDialog::addSelectedMod(const QString& name, ModDownloadTask* task) +void ModDownloadDialog::addSelectedMod(QString name, ModDownloadTask* task) { removeSelectedMod(name); modTask.insert(name, task); @@ -136,16 +142,16 @@ void ModDownloadDialog::addSelectedMod(const QString& name, ModDownloadTask* tas m_buttons->button(QDialogButtonBox::Ok)->setEnabled(!modTask.isEmpty()); } -void ModDownloadDialog::removeSelectedMod(const QString &name) +void ModDownloadDialog::removeSelectedMod(QString name) { - if(modTask.contains(name)) + if (modTask.contains(name)) delete modTask.find(name).value(); modTask.remove(name); m_buttons->button(QDialogButtonBox::Ok)->setEnabled(!modTask.isEmpty()); } -bool ModDownloadDialog::isModSelected(const QString &name, const QString& filename) const +bool ModDownloadDialog::isModSelected(QString name, QString filename) const { // FIXME: Is there a way to check for versions without checking the filename // as a heuristic, other than adding such info to ModDownloadTask itself? @@ -153,16 +159,31 @@ bool ModDownloadDialog::isModSelected(const QString &name, const QString& filena return iter != modTask.end() && (iter.value()->getFilename() == filename); } -bool ModDownloadDialog::isModSelected(const QString &name) const +bool ModDownloadDialog::isModSelected(QString name) const { auto iter = modTask.find(name); return iter != modTask.end(); } -ModDownloadDialog::~ModDownloadDialog() +const QList ModDownloadDialog::getTasks() { -} - -const QList ModDownloadDialog::getTasks() { return modTask.values(); } + +void ModDownloadDialog::selectedPageChanged(BasePage* previous, BasePage* selected) +{ + auto* prev_page = dynamic_cast(previous); + if (!prev_page) { + qCritical() << "Page '" << previous->displayName() << "' in ModDownloadDialog is not a ModPage!"; + return; + } + + auto* selected_page = dynamic_cast(selected); + if (!selected_page) { + qCritical() << "Page '" << selected->displayName() << "' in ModDownloadDialog is not a ModPage!"; + return; + } + + // Same effect as having a global search bar + selected_page->setSearchTerm(prev_page->getSearchTerm()); +} diff --git a/launcher/ui/dialogs/ModDownloadDialog.h b/launcher/ui/dialogs/ModDownloadDialog.h index 1fa1f058..18a5f0f3 100644 --- a/launcher/ui/dialogs/ModDownloadDialog.h +++ b/launcher/ui/dialogs/ModDownloadDialog.h @@ -21,11 +21,9 @@ #include #include -#include "BaseVersion.h" -#include "ui/pages/BasePageProvider.h" -#include "minecraft/mod/ModFolderModel.h" #include "ModDownloadTask.h" -#include "ui/pages/modplatform/flame/FlameModPage.h" +#include "minecraft/mod/ModFolderModel.h" +#include "ui/pages/BasePageProvider.h" namespace Ui { @@ -36,21 +34,21 @@ class PageContainer; class QDialogButtonBox; class ModrinthModPage; -class ModDownloadDialog : public QDialog, public BasePageProvider +class ModDownloadDialog final : public QDialog, public BasePageProvider { Q_OBJECT public: - explicit ModDownloadDialog(const std::shared_ptr &mods, QWidget *parent, BaseInstance *instance); - ~ModDownloadDialog(); + explicit ModDownloadDialog(const std::shared_ptr& mods, QWidget* parent, BaseInstance* instance); + ~ModDownloadDialog() override = default; QString dialogTitle() override; - QList getPages() override; + QList getPages() override; - void addSelectedMod(const QString & name = QString(), ModDownloadTask * task = nullptr); - void removeSelectedMod(const QString & name = QString()); - bool isModSelected(const QString & name, const QString & filename) const; - bool isModSelected(const QString & name) const; + void addSelectedMod(QString name = QString(), ModDownloadTask* task = nullptr); + void removeSelectedMod(QString name = QString()); + bool isModSelected(QString name, QString filename) const; + bool isModSelected(QString name) const; const QList getTasks(); const std::shared_ptr &mods; @@ -60,6 +58,9 @@ public slots: void accept() override; void reject() override; +private slots: + void selectedPageChanged(BasePage* previous, BasePage* selected); + private: Ui::ModDownloadDialog *ui = nullptr; PageContainer * m_container = nullptr; diff --git a/launcher/ui/dialogs/ReviewMessageBox.cpp b/launcher/ui/dialogs/ReviewMessageBox.cpp index e664e566..7c25c91c 100644 --- a/launcher/ui/dialogs/ReviewMessageBox.cpp +++ b/launcher/ui/dialogs/ReviewMessageBox.cpp @@ -1,11 +1,16 @@ #include "ReviewMessageBox.h" #include "ui_ReviewMessageBox.h" +#include + ReviewMessageBox::ReviewMessageBox(QWidget* parent, QString const& title, QString const& icon) : QDialog(parent), ui(new Ui::ReviewMessageBox) { ui->setupUi(this); + auto back_button = ui->buttonBox->button(QDialogButtonBox::Cancel); + back_button->setText(tr("Back")); + connect(ui->buttonBox, &QDialogButtonBox::accepted, this, &ReviewMessageBox::accept); connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &ReviewMessageBox::reject); } diff --git a/launcher/ui/pages/modplatform/ModModel.cpp b/launcher/ui/pages/modplatform/ModModel.cpp index 06a6f6b8..029e2be0 100644 --- a/launcher/ui/pages/modplatform/ModModel.cpp +++ b/launcher/ui/pages/modplatform/ModModel.cpp @@ -2,15 +2,27 @@ #include "BuildConfig.h" #include "Json.h" +#include "ModPage.h" #include "minecraft/MinecraftInstance.h" #include "minecraft/PackProfile.h" #include "ui/dialogs/ModDownloadDialog.h" +#include "ui/widgets/ProjectItem.h" + #include namespace ModPlatform { -ListModel::ListModel(ModPage* parent) : QAbstractListModel(parent), m_parent(parent) {} +// HACK: We need this to prevent callbacks from calling the ListModel after it has already been deleted. +// This leaks a tiny bit of memory per time the user has opened the mod dialog. How to make this better? +static QHash s_running; + +ListModel::ListModel(ModPage* parent) : QAbstractListModel(parent), m_parent(parent) { s_running.insert(this, true); } + +ListModel::~ListModel() +{ + s_running.find(this).value() = false; +} auto ListModel::debugName() const -> QString { @@ -39,9 +51,6 @@ auto ListModel::data(const QModelIndex& index, int role) const -> QVariant ModPlatform::IndexedPack pack = modpacks.at(pos); switch (role) { - case Qt::DisplayRole: { - return pack.name; - } case Qt::ToolTipRole: { if (pack.description.length() > 100) { // some magic to prevent to long tooltips and replace html linebreaks @@ -64,20 +73,20 @@ auto ListModel::data(const QModelIndex& index, int role) const -> QVariant ((ListModel*)this)->requestLogo(pack.logoName, pack.logoUrl); return icon; } + case Qt::SizeHintRole: + return QSize(0, 58); case Qt::UserRole: { QVariant v; v.setValue(pack); return v; } - case Qt::FontRole: { - QFont font; - if (m_parent->getDialog()->isModSelected(pack.name)) { - font.setBold(true); - font.setUnderline(true); - } - - return font; - } + // Custom data + case UserDataTypes::TITLE: + return pack.name; + case UserDataTypes::DESCRIPTION: + return pack.description; + case UserDataTypes::SELECTED: + return m_parent->getDialog()->isModSelected(pack.name); default: break; } @@ -85,11 +94,27 @@ auto ListModel::data(const QModelIndex& index, int role) const -> QVariant return {}; } -void ListModel::requestModVersions(ModPlatform::IndexedPack const& current) +bool ListModel::setData(const QModelIndex &index, const QVariant &value, int role) +{ + int pos = index.row(); + if (pos >= modpacks.size() || pos < 0 || !index.isValid()) + return false; + + modpacks[pos] = value.value(); + + return true; +} + +void ListModel::requestModVersions(ModPlatform::IndexedPack const& current, QModelIndex index) { auto profile = (dynamic_cast((dynamic_cast(parent()))->m_instance))->getPackProfile(); - m_parent->apiProvider()->getVersions(this, { current.addonId.toString(), getMineVersions(), profile->getModLoaders() }); + m_parent->apiProvider()->getVersions({ current.addonId.toString(), getMineVersions(), profile->getModLoaders() }, + [this, current, index](QJsonDocument& doc, QString addonId) { + if (!s_running.constFind(this).value()) + return; + versionRequestSucceeded(doc, addonId, index); + }); } void ListModel::performPaginatedSearch() @@ -100,9 +125,13 @@ void ListModel::performPaginatedSearch() this, { nextSearchOffset, currentSearchTerm, getSorts()[currentSort], profile->getModLoaders(), getMineVersions() }); } -void ListModel::requestModInfo(ModPlatform::IndexedPack& current) +void ListModel::requestModInfo(ModPlatform::IndexedPack& current, QModelIndex index) { - m_parent->apiProvider()->getModInfo(this, current); + m_parent->apiProvider()->getModInfo(current, [this, index](QJsonDocument& doc, ModPlatform::IndexedPack& pack) { + if (!s_running.constFind(this).value()) + return; + infoRequestFinished(doc, pack, index); + }); } void ListModel::refresh() @@ -256,7 +285,7 @@ void ListModel::searchRequestFailed(QString reason) } } -void ListModel::infoRequestFinished(QJsonDocument& doc, ModPlatform::IndexedPack& pack) +void ListModel::infoRequestFinished(QJsonDocument& doc, ModPlatform::IndexedPack& pack, const QModelIndex& index) { qDebug() << "Loading mod info"; @@ -268,10 +297,20 @@ void ListModel::infoRequestFinished(QJsonDocument& doc, ModPlatform::IndexedPack qWarning() << "Error while reading " << debugName() << " mod info: " << e.cause(); } + // Check if the index is still valid for this mod or not + if (pack.addonId == data(index, Qt::UserRole).value().addonId) { + // Cache info :^) + QVariant new_pack; + new_pack.setValue(pack); + if (!setData(index, new_pack, Qt::UserRole)) { + qWarning() << "Failed to cache mod info!"; + } + } + m_parent->updateUi(); } -void ListModel::versionRequestSucceeded(QJsonDocument doc, QString addonId) +void ListModel::versionRequestSucceeded(QJsonDocument doc, QString addonId, const QModelIndex& index) { auto& current = m_parent->getCurrent(); if (addonId != current.addonId) { @@ -287,6 +326,14 @@ void ListModel::versionRequestSucceeded(QJsonDocument doc, QString addonId) qWarning() << "Error while reading " << debugName() << " mod version: " << e.cause(); } + // Cache info :^) + QVariant new_pack; + new_pack.setValue(current); + if (!setData(index, new_pack, Qt::UserRole)) { + qWarning() << "Failed to cache mod versions!"; + } + + m_parent->updateModVersions(); } diff --git a/launcher/ui/pages/modplatform/ModModel.h b/launcher/ui/pages/modplatform/ModModel.h index dd22407c..a58c7c55 100644 --- a/launcher/ui/pages/modplatform/ModModel.h +++ b/launcher/ui/pages/modplatform/ModModel.h @@ -2,7 +2,6 @@ #include -#include "modplatform/ModAPI.h" #include "modplatform/ModIndex.h" #include "net/NetJob.h" @@ -19,7 +18,7 @@ class ListModel : public QAbstractListModel { public: ListModel(ModPage* parent); - ~ListModel() override = default; + ~ListModel() override; inline auto rowCount(const QModelIndex& parent) const -> int override { return modpacks.size(); }; inline auto columnCount(const QModelIndex& parent) const -> int override { return 1; }; @@ -29,15 +28,17 @@ class ListModel : public QAbstractListModel { /* Retrieve information from the model at a given index with the given role */ auto data(const QModelIndex& index, int role) const -> QVariant override; + bool setData(const QModelIndex &index, const QVariant &value, int role) override; inline void setActiveJob(NetJob::Ptr ptr) { jobPtr = ptr; } + inline NetJob* activeJob() { return jobPtr.get(); } /* Ask the API for more information */ void fetchMore(const QModelIndex& parent) override; void refresh(); void searchWithTerm(const QString& term, const int sort, const bool filter_changed); - void requestModInfo(ModPlatform::IndexedPack& current); - void requestModVersions(const ModPlatform::IndexedPack& current); + void requestModInfo(ModPlatform::IndexedPack& current, QModelIndex index); + void requestModVersions(const ModPlatform::IndexedPack& current, QModelIndex index); virtual void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) = 0; virtual void loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) {}; @@ -51,9 +52,9 @@ class ListModel : public QAbstractListModel { void searchRequestFinished(QJsonDocument& doc); void searchRequestFailed(QString reason); - void infoRequestFinished(QJsonDocument& doc, ModPlatform::IndexedPack& pack); + void infoRequestFinished(QJsonDocument& doc, ModPlatform::IndexedPack& pack, const QModelIndex& index); - void versionRequestSucceeded(QJsonDocument doc, QString addonId); + void versionRequestSucceeded(QJsonDocument doc, QString addonId, const QModelIndex& index); protected slots: diff --git a/launcher/ui/pages/modplatform/ModPage.cpp b/launcher/ui/pages/modplatform/ModPage.cpp index 200fe59e..a34a74db 100644 --- a/launcher/ui/pages/modplatform/ModPage.cpp +++ b/launcher/ui/pages/modplatform/ModPage.cpp @@ -40,9 +40,12 @@ #include #include +#include + #include "minecraft/MinecraftInstance.h" #include "minecraft/PackProfile.h" #include "ui/dialogs/ModDownloadDialog.h" +#include "ui/widgets/ProjectItem.h" ModPage::ModPage(ModDownloadDialog* dialog, BaseInstance* instance, ModAPI* api) : QWidget(dialog) @@ -50,17 +53,30 @@ ModPage::ModPage(ModDownloadDialog* dialog, BaseInstance* instance, ModAPI* api) , ui(new Ui::ModPage) , dialog(dialog) , filter_widget(static_cast(instance)->getPackProfile()->getComponentVersion("net.minecraft"), this) + , m_fetch_progress(this, false) , api(api) { ui->setupUi(this); + connect(ui->searchButton, &QPushButton::clicked, this, &ModPage::triggerSearch); connect(ui->modFilterButton, &QPushButton::clicked, this, &ModPage::filterMods); + + m_search_timer.setTimerType(Qt::TimerType::CoarseTimer); + m_search_timer.setSingleShot(true); + + connect(&m_search_timer, &QTimer::timeout, this, &ModPage::triggerSearch); + ui->searchEdit->installEventFilter(this); ui->versionSelectionBox->view()->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); ui->versionSelectionBox->view()->parentWidget()->setMaximumHeight(300); - ui->gridLayout_3->addWidget(&filter_widget, 0, 0, 1, ui->gridLayout_3->columnCount()); + m_fetch_progress.hideIfInactive(true); + m_fetch_progress.setFixedHeight(24); + m_fetch_progress.progressFormat(""); + + ui->gridLayout_3->addWidget(&m_fetch_progress, 0, 0, 1, ui->gridLayout_3->columnCount()); + ui->gridLayout_3->addWidget(&filter_widget, 1, 0, 1, ui->gridLayout_3->columnCount()); filter_widget.setInstance(static_cast(m_instance)); m_filter = filter_widget.getFilter(); @@ -71,6 +87,9 @@ ModPage::ModPage(ModDownloadDialog* dialog, BaseInstance* instance, ModAPI* api) connect(&filter_widget, &ModFilterWidget::filterUnchanged, this, [&]{ ui->searchButton->setStyleSheet("text-decoration: none"); }); + + ui->packView->setItemDelegate(new ProjectItemDelegate(this)); + ui->packView->installEventFilter(this); } ModPage::~ModPage() @@ -93,6 +112,23 @@ auto ModPage::eventFilter(QObject* watched, QEvent* event) -> bool auto* keyEvent = dynamic_cast(event); if (keyEvent->key() == Qt::Key_Return) { triggerSearch(); + keyEvent->accept(); + return true; + } else { + if (m_search_timer.isActive()) + m_search_timer.stop(); + + m_search_timer.start(350); + } + } else if (watched == ui->packView && event->type() == QEvent::KeyPress) { + auto* keyEvent = dynamic_cast(event); + if (keyEvent->key() == Qt::Key_Return) { + onModSelected(); + + // To have the 'select mod' button outlined instead of the 'review and confirm' one + ui->modSelectionButton->setFocus(Qt::FocusReason::ShortcutFocusReason); + ui->packView->setFocus(Qt::FocusReason::NoFocusReason); + keyEvent->accept(); return true; } @@ -120,16 +156,26 @@ void ModPage::triggerSearch() updateSelectionButton(); } - listModel->searchWithTerm(ui->searchEdit->text(), ui->sortByBox->currentIndex(), changed); + listModel->searchWithTerm(getSearchTerm(), ui->sortByBox->currentIndex(), changed); + m_fetch_progress.watch(listModel->activeJob()); } -void ModPage::onSelectionChanged(QModelIndex first, QModelIndex second) +QString ModPage::getSearchTerm() const +{ + return ui->searchEdit->text(); +} +void ModPage::setSearchTerm(QString term) +{ + ui->searchEdit->setText(term); +} + +void ModPage::onSelectionChanged(QModelIndex curr, QModelIndex prev) { ui->versionSelectionBox->clear(); - if (!first.isValid()) { return; } + if (!curr.isValid()) { return; } - current = listModel->data(first, Qt::UserRole).value(); + current = listModel->data(curr, Qt::UserRole).value(); if (!current.versionsLoaded) { qDebug() << QString("Loading %1 mod versions").arg(debugName()); @@ -137,7 +183,7 @@ void ModPage::onSelectionChanged(QModelIndex first, QModelIndex second) ui->modSelectionButton->setText(tr("Loading versions...")); ui->modSelectionButton->setEnabled(false); - listModel->requestModVersions(current); + listModel->requestModVersions(current, curr); } else { for (int i = 0; i < current.versions.size(); i++) { ui->versionSelectionBox->addItem(current.versions[i].version, QVariant(i)); @@ -149,7 +195,8 @@ void ModPage::onSelectionChanged(QModelIndex first, QModelIndex second) if(!current.extraDataLoaded){ qDebug() << QString("Loading %1 mod info").arg(debugName()); - listModel->requestModInfo(current); + + listModel->requestModInfo(current, curr); } updateUi(); @@ -167,6 +214,9 @@ void ModPage::onVersionSelectionChanged(QString data) void ModPage::onModSelected() { + if (selectedVersion < 0) + return; + auto& version = current.versions[selectedVersion]; if (dialog->isModSelected(current.name, version.fileName)) { dialog->removeSelectedMod(current.name); @@ -176,6 +226,9 @@ void ModPage::onModSelected() } updateSelectionButton(); + + /* Force redraw on the mods list when the selection changes */ + ui->packView->adjustSize(); } @@ -285,5 +338,6 @@ void ModPage::updateUi() text += "
"; - ui->packDescription->setHtml(text + current.description); + HoeDown h; + ui->packDescription->setHtml(text + (current.extraData.body.isEmpty() ? current.description : h.process(current.extraData.body.toUtf8()))); } diff --git a/launcher/ui/pages/modplatform/ModPage.h b/launcher/ui/pages/modplatform/ModPage.h index cf00e16e..09c38d8b 100644 --- a/launcher/ui/pages/modplatform/ModPage.h +++ b/launcher/ui/pages/modplatform/ModPage.h @@ -8,6 +8,7 @@ #include "ui/pages/BasePage.h" #include "ui/pages/modplatform/ModModel.h" #include "ui/widgets/ModFilterWidget.h" +#include "ui/widgets/ProgressWidget.h" class ModDownloadDialog; @@ -45,6 +46,11 @@ class ModPage : public QWidget, public BasePage { auto getFilter() const -> const std::shared_ptr { return m_filter; } auto getDialog() const -> const ModDownloadDialog* { return dialog; } + /** Get the current term in the search bar. */ + auto getSearchTerm() const -> QString; + /** Programatically set the term in the search bar. */ + void setSearchTerm(QString); + auto getCurrent() -> ModPlatform::IndexedPack& { return current; } void updateModVersions(int prev_count = -1); @@ -70,10 +76,15 @@ class ModPage : public QWidget, public BasePage { ModFilterWidget filter_widget; std::shared_ptr m_filter; + ProgressWidget m_fetch_progress; + ModPlatform::ListModel* listModel = nullptr; ModPlatform::IndexedPack current; std::unique_ptr api; int selectedVersion = -1; + + // Used to do instant searching with a delay to cache quick changes + QTimer m_search_timer; }; diff --git a/launcher/ui/pages/modplatform/flame/FlameModModel.cpp b/launcher/ui/pages/modplatform/flame/FlameModModel.cpp index 8de2e545..bc2c686c 100644 --- a/launcher/ui/pages/modplatform/flame/FlameModModel.cpp +++ b/launcher/ui/pages/modplatform/flame/FlameModModel.cpp @@ -12,6 +12,12 @@ void ListModel::loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) FlameMod::loadIndexedPack(m, obj); } +// We already deal with the URLs when initializing the pack, due to the API response's structure +void ListModel::loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) +{ + FlameMod::loadBody(m, obj); +} + void ListModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) { FlameMod::loadIndexedPackVersions(m, arr, APPLICATION->network(), m_parent->m_instance); diff --git a/launcher/ui/pages/modplatform/flame/FlameModModel.h b/launcher/ui/pages/modplatform/flame/FlameModModel.h index 707c1bb1..6a6aef2e 100644 --- a/launcher/ui/pages/modplatform/flame/FlameModModel.h +++ b/launcher/ui/pages/modplatform/flame/FlameModModel.h @@ -13,6 +13,7 @@ class ListModel : public ModPlatform::ListModel { private: void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) override; + void loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) override; void loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) override; auto documentToArray(QJsonDocument& obj) const -> QJsonArray override; diff --git a/launcher/ui/widgets/Common.cpp b/launcher/ui/widgets/Common.cpp index f72f3596..097bb6d4 100644 --- a/launcher/ui/widgets/Common.cpp +++ b/launcher/ui/widgets/Common.cpp @@ -1,27 +1,33 @@ #include "Common.h" // Origin: Qt -QStringList viewItemTextLayout(QTextLayout &textLayout, int lineWidth, qreal &height, - qreal &widthUsed) +// More specifically, this is a trimmed down version on the algorithm in: +// https://code.woboq.org/qt5/qtbase/src/widgets/styles/qcommonstyle.cpp.html#846 +QList> viewItemTextLayout(QTextLayout& textLayout, int lineWidth, qreal& height) { - QStringList lines; + QList> lines; height = 0; - widthUsed = 0; + textLayout.beginLayout(); + QString str = textLayout.text(); - while (true) - { + while (true) { QTextLine line = textLayout.createLine(); + if (!line.isValid()) break; if (line.textLength() == 0) break; + line.setLineWidth(lineWidth); line.setPosition(QPointF(0, height)); + height += line.height(); - lines.append(str.mid(line.textStart(), line.textLength())); - widthUsed = qMax(widthUsed, line.naturalTextWidth()); + + lines.append(std::make_pair(line.naturalTextWidth(), str.mid(line.textStart(), line.textLength()))); } + textLayout.endLayout(); + return lines; } diff --git a/launcher/ui/widgets/Common.h b/launcher/ui/widgets/Common.h index b3fbe1a0..b3dd5ca8 100644 --- a/launcher/ui/widgets/Common.h +++ b/launcher/ui/widgets/Common.h @@ -1,6 +1,9 @@ #pragma once -#include + #include -QStringList viewItemTextLayout(QTextLayout &textLayout, int lineWidth, qreal &height, - qreal &widthUsed); \ No newline at end of file +/** Cuts out the text in textLayout into smaller pieces, according to the lineWidth. + * Returns a list of pairs, each containing the width of that line and that line's string, respectively. + * The total height of those lines is set in the last argument, 'height'. + */ +QList> viewItemTextLayout(QTextLayout& textLayout, int lineWidth, qreal& height); diff --git a/launcher/ui/widgets/PageContainer.cpp b/launcher/ui/widgets/PageContainer.cpp index 419ccb66..8d606820 100644 --- a/launcher/ui/widgets/PageContainer.cpp +++ b/launcher/ui/widgets/PageContainer.cpp @@ -244,7 +244,14 @@ void PageContainer::help() void PageContainer::currentChanged(const QModelIndex ¤t) { - showPage(current.isValid() ? m_proxyModel->mapToSource(current).row() : -1); + int selected_index = current.isValid() ? m_proxyModel->mapToSource(current).row() : -1; + + auto* selected = m_model->pages().at(selected_index); + auto* previous = m_currentPage; + + emit selectedPageChanged(previous, selected); + + showPage(selected_index); } bool PageContainer::prepareToClose() diff --git a/launcher/ui/widgets/PageContainer.h b/launcher/ui/widgets/PageContainer.h index 86f549eb..80d87a9b 100644 --- a/launcher/ui/widgets/PageContainer.h +++ b/launcher/ui/widgets/PageContainer.h @@ -95,6 +95,10 @@ private: public slots: void help(); +signals: + /** Emitted when the currently selected page is changed */ + void selectedPageChanged(BasePage* previous, BasePage* selected); + private slots: void currentChanged(const QModelIndex ¤t); void showPage(int row); diff --git a/launcher/ui/widgets/ProgressWidget.cpp b/launcher/ui/widgets/ProgressWidget.cpp index 911e555d..b60d9a7a 100644 --- a/launcher/ui/widgets/ProgressWidget.cpp +++ b/launcher/ui/widgets/ProgressWidget.cpp @@ -1,66 +1,104 @@ // Licensed under the Apache-2.0 license. See README.md for details. #include "ProgressWidget.h" -#include -#include -#include #include +#include +#include +#include #include "tasks/Task.h" -ProgressWidget::ProgressWidget(QWidget *parent) - : QWidget(parent) +ProgressWidget::ProgressWidget(QWidget* parent, bool show_label) : QWidget(parent) { - m_label = new QLabel(this); - m_label->setWordWrap(true); + auto* layout = new QVBoxLayout(this); + + if (show_label) { + m_label = new QLabel(this); + m_label->setWordWrap(true); + layout->addWidget(m_label); + } + m_bar = new QProgressBar(this); m_bar->setMinimum(0); m_bar->setMaximum(100); - QVBoxLayout *layout = new QVBoxLayout(this); - layout->addWidget(m_label); layout->addWidget(m_bar); - layout->addStretch(); + setLayout(layout); } -void ProgressWidget::start(std::shared_ptr task) +void ProgressWidget::reset() { - if (m_task) - { - disconnect(m_task.get(), 0, this, 0); - } - m_task = task; - connect(m_task.get(), &Task::finished, this, &ProgressWidget::handleTaskFinish); - connect(m_task.get(), &Task::status, this, &ProgressWidget::handleTaskStatus); - connect(m_task.get(), &Task::progress, this, &ProgressWidget::handleTaskProgress); - connect(m_task.get(), &Task::destroyed, this, &ProgressWidget::taskDestroyed); - if (!m_task->isRunning()) - { - QMetaObject::invokeMethod(m_task.get(), "start", Qt::QueuedConnection); - } + m_bar->reset(); } + +void ProgressWidget::progressFormat(QString format) +{ + if (format.isEmpty()) + m_bar->setTextVisible(false); + else + m_bar->setFormat(format); +} + +void ProgressWidget::watch(Task* task) +{ + if (!task) + return; + + if (m_task) + disconnect(m_task, nullptr, this, nullptr); + + m_task = task; + + connect(m_task, &Task::finished, this, &ProgressWidget::handleTaskFinish); + connect(m_task, &Task::status, this, &ProgressWidget::handleTaskStatus); + connect(m_task, &Task::progress, this, &ProgressWidget::handleTaskProgress); + connect(m_task, &Task::destroyed, this, &ProgressWidget::taskDestroyed); + + show(); +} + +void ProgressWidget::start(Task* task) +{ + watch(task); + if (!m_task->isRunning()) + QMetaObject::invokeMethod(m_task, "start", Qt::QueuedConnection); +} + bool ProgressWidget::exec(std::shared_ptr task) { QEventLoop loop; + connect(task.get(), &Task::finished, &loop, &QEventLoop::quit); - start(task); + + start(task.get()); + if (task->isRunning()) - { loop.exec(); - } + return task->wasSuccessful(); } +void ProgressWidget::show() +{ + setHidden(false); +} +void ProgressWidget::hide() +{ + setHidden(true); +} + void ProgressWidget::handleTaskFinish() { - if (!m_task->wasSuccessful()) - { + if (!m_task->wasSuccessful() && m_label) m_label->setText(m_task->failReason()); - } + + if (m_hide_if_inactive) + hide(); } -void ProgressWidget::handleTaskStatus(const QString &status) +void ProgressWidget::handleTaskStatus(const QString& status) { - m_label->setText(status); + if (m_label) + m_label->setText(status); } void ProgressWidget::handleTaskProgress(qint64 current, qint64 total) { diff --git a/launcher/ui/widgets/ProgressWidget.h b/launcher/ui/widgets/ProgressWidget.h index fa67748a..4d9097b8 100644 --- a/launcher/ui/widgets/ProgressWidget.h +++ b/launcher/ui/widgets/ProgressWidget.h @@ -9,24 +9,48 @@ class Task; class QProgressBar; class QLabel; -class ProgressWidget : public QWidget -{ +class ProgressWidget : public QWidget { Q_OBJECT -public: - explicit ProgressWidget(QWidget *parent = nullptr); + public: + explicit ProgressWidget(QWidget* parent = nullptr, bool show_label = true); -public slots: - void start(std::shared_ptr task); + /** Whether to hide the widget automatically if it's watching no running task. */ + void hideIfInactive(bool hide) { m_hide_if_inactive = hide; } + + /** Reset the displayed progress to 0 */ + void reset(); + + /** The text that shows up in the middle of the progress bar. + * By default it's '%p%', with '%p' being the total progress in percentage. + */ + void progressFormat(QString); + + public slots: + /** Watch the progress of a task. */ + void watch(Task* task); + + /** Watch the progress of a task, and start it if needed */ + void start(Task* task); + + /** Blocking way of waiting for a task to finish. */ bool exec(std::shared_ptr task); -private slots: + /** Un-hide the widget if needed. */ + void show(); + + /** Make the widget invisible. */ + void hide(); + + private slots: void handleTaskFinish(); - void handleTaskStatus(const QString &status); + void handleTaskStatus(const QString& status); void handleTaskProgress(qint64 current, qint64 total); void taskDestroyed(); -private: - QLabel *m_label; - QProgressBar *m_bar; - std::shared_ptr m_task; + private: + QLabel* m_label = nullptr; + QProgressBar* m_bar = nullptr; + Task* m_task = nullptr; + + bool m_hide_if_inactive = false; }; diff --git a/launcher/ui/widgets/ProjectItem.cpp b/launcher/ui/widgets/ProjectItem.cpp new file mode 100644 index 00000000..56ae35fb --- /dev/null +++ b/launcher/ui/widgets/ProjectItem.cpp @@ -0,0 +1,78 @@ +#include "ProjectItem.h" + +#include "Common.h" + +#include +#include + +ProjectItemDelegate::ProjectItemDelegate(QWidget* parent) : QStyledItemDelegate(parent) {} + +void ProjectItemDelegate::paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const +{ + painter->save(); + + QStyleOptionViewItem opt(option); + initStyleOption(&opt, index); + + auto& rect = opt.rect; + auto icon_width = rect.height(), icon_height = rect.height(); + auto remaining_width = rect.width() - icon_width; + + if (opt.state & QStyle::State_Selected) { + painter->fillRect(rect, opt.palette.highlight()); + painter->setPen(opt.palette.highlightedText().color()); + } else if (opt.state & QStyle::State_MouseOver) { + painter->fillRect(rect, opt.palette.window()); + } + + { // Icon painting + // Square-sized, occupying the left portion + opt.icon.paint(painter, rect.x(), rect.y(), icon_width, icon_height); + } + + { // Title painting + auto title = index.data(UserDataTypes::TITLE).toString(); + + painter->save(); + + auto font = opt.font; + if (index.data(UserDataTypes::SELECTED).toBool()) { + // Set nice font + font.setBold(true); + font.setUnderline(true); + } + + font.setPointSize(font.pointSize() + 2); + painter->setFont(font); + + // On the top, aligned to the left after the icon + painter->drawText(rect.x() + icon_width, rect.y() + QFontMetrics(font).height(), title); + + painter->restore(); + } + + { // Description painting + auto description = index.data(UserDataTypes::DESCRIPTION).toString(); + + QTextLayout text_layout(description, opt.font); + + qreal height = 0; + auto cut_text = viewItemTextLayout(text_layout, remaining_width, height); + + // Get first line unconditionally + description = cut_text.first().second; + // Get second line, elided if needed + if (cut_text.size() > 1) { + if (cut_text.size() > 2) + description += opt.fontMetrics.elidedText(cut_text.at(1).second, opt.textElideMode, cut_text.at(1).first); + else + description += cut_text.at(1).second; + } + + // On the bottom, aligned to the left after the icon, and featuring at most two lines of text (with some margin space to spare) + painter->drawText(rect.x() + icon_width, rect.y() + rect.height() - 2.2 * opt.fontMetrics.height(), remaining_width, + 2 * opt.fontMetrics.height(), Qt::TextWordWrap, description); + } + + painter->restore(); +} diff --git a/launcher/ui/widgets/ProjectItem.h b/launcher/ui/widgets/ProjectItem.h new file mode 100644 index 00000000..f668edf6 --- /dev/null +++ b/launcher/ui/widgets/ProjectItem.h @@ -0,0 +1,25 @@ +#pragma once + +#include + +/* Custom data types for our custom list models :) */ +enum UserDataTypes { + TITLE = 257, // QString + DESCRIPTION = 258, // QString + SELECTED = 259 // bool +}; + +/** This is an item delegate composed of: + * - An Icon on the left + * - A title + * - A description + * */ +class ProjectItemDelegate final : public QStyledItemDelegate { + Q_OBJECT + + public: + ProjectItemDelegate(QWidget* parent); + + void paint(QPainter*, const QStyleOptionViewItem&, const QModelIndex&) const override; + +};