aboutsummaryrefslogtreecommitdiff
path: root/launcher
diff options
context:
space:
mode:
Diffstat (limited to 'launcher')
-rw-r--r--launcher/Application.cpp2
-rw-r--r--launcher/CMakeLists.txt2
-rw-r--r--launcher/modplatform/ModAPI.h4
-rw-r--r--launcher/modplatform/ModIndex.h2
-rw-r--r--launcher/modplatform/flame/FlameAPI.cpp37
-rw-r--r--launcher/modplatform/flame/FlameAPI.h1
-rw-r--r--launcher/modplatform/flame/FlameModIndex.cpp19
-rw-r--r--launcher/modplatform/flame/FlameModIndex.h3
-rw-r--r--launcher/modplatform/helpers/NetworkModAPI.cpp20
-rw-r--r--launcher/modplatform/helpers/NetworkModAPI.h4
-rw-r--r--launcher/modplatform/modrinth/ModrinthPackIndex.cpp2
-rw-r--r--launcher/ui/dialogs/ModDownloadDialog.cpp71
-rw-r--r--launcher/ui/dialogs/ModDownloadDialog.h25
-rw-r--r--launcher/ui/dialogs/ReviewMessageBox.cpp5
-rw-r--r--launcher/ui/pages/modplatform/ModModel.cpp85
-rw-r--r--launcher/ui/pages/modplatform/ModModel.h13
-rw-r--r--launcher/ui/pages/modplatform/ModPage.cpp70
-rw-r--r--launcher/ui/pages/modplatform/ModPage.h11
-rw-r--r--launcher/ui/pages/modplatform/flame/FlameModModel.cpp6
-rw-r--r--launcher/ui/pages/modplatform/flame/FlameModModel.h1
-rw-r--r--launcher/ui/widgets/Common.cpp22
-rw-r--r--launcher/ui/widgets/Common.h9
-rw-r--r--launcher/ui/widgets/PageContainer.cpp9
-rw-r--r--launcher/ui/widgets/PageContainer.h4
-rw-r--r--launcher/ui/widgets/ProgressWidget.cpp94
-rw-r--r--launcher/ui/widgets/ProgressWidget.h48
-rw-r--r--launcher/ui/widgets/ProjectItem.cpp78
-rw-r--r--launcher/ui/widgets/ProjectItem.h25
28 files changed, 530 insertions, 142 deletions
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<void(QJsonDocument&, ModPlatform::IndexedPack&)> 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<void(QJsonDocument&, QString)> 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<uint>& 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<QNetworkAccessManager>& 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<void(QJsonDocument&, ModPlatform::IndexedPack&)> 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<void(QJsonDocument&, QString)> 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<void(QJsonDocument&, ModPlatform::IndexedPack&)> callback) override;
+ void getVersions(VersionSearchArgs&& args, std::function<void(QJsonDocument&, QString)> 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 <BaseVersion.h>
-#include <icons/IconList.h>
#include <InstanceList.h>
+#include <icons/IconList.h>
#include "Application.h"
-#include "ProgressDialog.h"
#include "ReviewMessageBox.h"
+#include <QDialogButtonBox>
#include <QLayout>
#include <QPushButton>
#include <QValidator>
-#include <QDialogButtonBox>
-#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<ModFolderModel> &mods, QWidget *parent,
- BaseInstance *instance)
- : QDialog(parent), mods(mods), m_instance(instance)
+ModDownloadDialog::ModDownloadDialog(const std::shared_ptr<ModFolderModel>& 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<ModFolderModel> &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<ModFolderModel> &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<BasePage *> ModDownloadDialog::getPages()
+QList<BasePage*> ModDownloadDialog::getPages()
{
- QList<BasePage *> pages;
+ QList<BasePage*> pages;
pages.append(new ModrinthModPage(this, m_instance));
if (APPLICATION->capabilities() & Application::SupportsFlame)
@@ -128,7 +134,7 @@ QList<BasePage *> 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<ModDownloadTask*> ModDownloadDialog::getTasks()
{
+ return modTask.values();
}
-const QList<ModDownloadTask*> ModDownloadDialog::getTasks() {
- return modTask.values();
+void ModDownloadDialog::selectedPageChanged(BasePage* previous, BasePage* selected)
+{
+ auto* prev_page = dynamic_cast<ModPage*>(previous);
+ if (!prev_page) {
+ qCritical() << "Page '" << previous->displayName() << "' in ModDownloadDialog is not a ModPage!";
+ return;
+ }
+
+ auto* selected_page = dynamic_cast<ModPage*>(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 <QDialog>
#include <QVBoxLayout>
-#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<ModFolderModel> &mods, QWidget *parent, BaseInstance *instance);
- ~ModDownloadDialog();
+ explicit ModDownloadDialog(const std::shared_ptr<ModFolderModel>& mods, QWidget* parent, BaseInstance* instance);
+ ~ModDownloadDialog() override = default;
QString dialogTitle() override;
- QList<BasePage *> getPages() override;
+ QList<BasePage*> 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<ModDownloadTask*> getTasks();
const std::shared_ptr<ModFolderModel> &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 <QPushButton>
+
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 <QMessageBox>
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<ListModel*, bool> 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<ModPlatform::IndexedPack>();
+
+ return true;
+}
+
+void ListModel::requestModVersions(ModPlatform::IndexedPack const& current, QModelIndex index)
{
auto profile = (dynamic_cast<MinecraftInstance*>((dynamic_cast<ModPage*>(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<ModPlatform::IndexedPack>().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 <QAbstractListModel>
-#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 <QKeyEvent>
#include <memory>
+#include <HoeDown.h>
+
#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<MinecraftInstance*>(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<MinecraftInstance*>(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()
@@ -95,6 +114,23 @@ auto ModPage::eventFilter(QObject* watched, QEvent* event) -> bool
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<QKeyEvent*>(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;
}
}
return QWidget::eventFilter(watched, event);
@@ -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());
+}
+
+QString ModPage::getSearchTerm() const
+{
+ return ui->searchEdit->text();
+}
+void ModPage::setSearchTerm(QString term)
+{
+ ui->searchEdit->setText(term);
}
-void ModPage::onSelectionChanged(QModelIndex first, QModelIndex second)
+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<ModPlatform::IndexedPack>();
+ current = listModel->data(curr, Qt::UserRole).value<ModPlatform::IndexedPack>();
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 += "<hr>";
- 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<ModFilterWidget::Filter> { 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<ModFilterWidget::Filter> m_filter;
+ ProgressWidget m_fetch_progress;
+
ModPlatform::ListModel* listModel = nullptr;
ModPlatform::IndexedPack current;
std::unique_ptr<ModAPI> 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<std::pair<qreal, QString>> viewItemTextLayout(QTextLayout& textLayout, int lineWidth, qreal& height)
{
- QStringList lines;
+ QList<std::pair<qreal, QString>> 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 <QStringList>
+
#include <QTextLayout>
-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<std::pair<qreal, QString>> 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 &current)
{
- 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 &current);
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 <QProgressBar>
+#include <QEventLoop>
#include <QLabel>
+#include <QProgressBar>
#include <QVBoxLayout>
-#include <QEventLoop>
#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> task)
+void ProgressWidget::reset()
+{
+ 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.get(), 0, this, 0);
- }
+ disconnect(m_task, nullptr, this, nullptr);
+
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);
+
+ 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.get(), "start", Qt::QueuedConnection);
- }
+ QMetaObject::invokeMethod(m_task, "start", Qt::QueuedConnection);
}
+
bool ProgressWidget::exec(std::shared_ptr<Task> 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> 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> 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<Task> 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 <QIcon>
+#include <QPainter>
+
+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 <QStyledItemDelegate>
+
+/* 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;
+
+};