Merge pull request #939 from flowln/mod_downloader_improve

Some more UI / UX improvements to the mod downloader!
This commit is contained in:
flow 2022-09-07 08:27:11 -03:00 committed by GitHub
commit 1b0ca47682
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 535 additions and 147 deletions

View File

@ -685,6 +685,8 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv)
m_settings->registerSetting("UpdateDialogGeometry", ""); m_settings->registerSetting("UpdateDialogGeometry", "");
m_settings->registerSetting("ModDownloadGeometry", "");
// HACK: This code feels so stupid is there a less stupid way of doing this? // HACK: This code feels so stupid is there a less stupid way of doing this?
{ {
m_settings->registerSetting("PastebinURL", ""); m_settings->registerSetting("PastebinURL", "");

View File

@ -896,6 +896,8 @@ SET(LAUNCHER_SOURCES
ui/widgets/PageContainer.cpp ui/widgets/PageContainer.cpp
ui/widgets/PageContainer.h ui/widgets/PageContainer.h
ui/widgets/PageContainer_p.h ui/widgets/PageContainer_p.h
ui/widgets/ProjectItem.h
ui/widgets/ProjectItem.cpp
ui/widgets/VersionListView.cpp ui/widgets/VersionListView.cpp
ui/widgets/VersionListView.h ui/widgets/VersionListView.h
ui/widgets/VersionSelectWidget.cpp ui/widgets/VersionSelectWidget.cpp

View File

@ -73,7 +73,7 @@ class ModAPI {
}; };
virtual void searchMods(CallerType* caller, SearchArgs&& args) const = 0; 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 getProject(QString addonId, QByteArray* response) const -> NetJob* = 0;
virtual auto getProjects(QStringList addonIds, QByteArray* response) const -> NetJob* = 0; virtual auto getProjects(QStringList addonIds, QByteArray* response) const -> NetJob* = 0;
@ -85,7 +85,7 @@ class ModAPI {
ModLoaderTypes loaders; 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 { static auto getModLoaderString(ModLoaderType type) -> const QString {
switch (type) { switch (type) {

View File

@ -75,6 +75,8 @@ struct ExtraPackData {
QString sourceUrl; QString sourceUrl;
QString wikiUrl; QString wikiUrl;
QString discordUrl; QString discordUrl;
QString body;
}; };
struct IndexedPack { struct IndexedPack {

View File

@ -67,6 +67,43 @@ auto FlameAPI::getModFileChangelog(int modId, int fileId) -> QString
return changelog; 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 auto FlameAPI::getLatestVersion(VersionSearchArgs&& args) -> ModPlatform::IndexedVersion
{ {
QEventLoop loop; QEventLoop loop;

View File

@ -7,6 +7,7 @@ class FlameAPI : public NetworkModAPI {
public: public:
auto matchFingerprints(const QList<uint>& fingerprints, QByteArray* response) -> NetJob::Ptr; auto matchFingerprints(const QList<uint>& fingerprints, QByteArray* response) -> NetJob::Ptr;
auto getModFileChangelog(int modId, int fileId) -> QString; auto getModFileChangelog(int modId, int fileId) -> QString;
auto getModDescription(int modId) -> QString;
auto getLatestVersion(VersionSearchArgs&& args) -> ModPlatform::IndexedVersion; auto getLatestVersion(VersionSearchArgs&& args) -> ModPlatform::IndexedVersion;

View File

@ -4,10 +4,9 @@
#include "minecraft/MinecraftInstance.h" #include "minecraft/MinecraftInstance.h"
#include "minecraft/PackProfile.h" #include "minecraft/PackProfile.h"
#include "modplatform/flame/FlameAPI.h" #include "modplatform/flame/FlameAPI.h"
#include "net/NetJob.h"
static ModPlatform::ProviderCapabilities ProviderCaps;
static FlameAPI api; static FlameAPI api;
static ModPlatform::ProviderCapabilities ProviderCaps;
void FlameMod::loadIndexedPack(ModPlatform::IndexedPack& pack, QJsonObject& obj) void FlameMod::loadIndexedPack(ModPlatform::IndexedPack& pack, QJsonObject& obj)
{ {
@ -31,10 +30,11 @@ void FlameMod::loadIndexedPack(ModPlatform::IndexedPack& pack, QJsonObject& obj)
pack.authors.append(packAuthor); 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"); auto links_obj = Json::ensureObject(obj, "links");
@ -50,6 +50,15 @@ void FlameMod::loadExtraPackData(ModPlatform::IndexedPack& pack, QJsonObject& ob
if(pack.extraData.wikiUrl.endsWith('/')) if(pack.extraData.wikiUrl.endsWith('/'))
pack.extraData.wikiUrl.chop(1); pack.extraData.wikiUrl.chop(1);
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; pack.extraDataLoaded = true;
} }

View File

@ -12,7 +12,8 @@
namespace FlameMod { namespace FlameMod {
void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj); 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, void loadIndexedPackVersions(ModPlatform::IndexedPack& pack,
QJsonArray& arr, QJsonArray& arr,
const shared_qobject_ptr<QNetworkAccessManager>& network, const shared_qobject_ptr<QNetworkAccessManager>& network,

View File

@ -31,48 +31,48 @@ void NetworkModAPI::searchMods(CallerType* caller, SearchArgs&& args) const
netJob->start(); 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 response = new QByteArray();
auto job = getProject(pack.addonId.toString(), response); 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{}; QJsonParseError parse_error{};
QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error);
if (parse_error.error != QJsonParseError::NoError) { 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(); << " reason: " << parse_error.errorString();
qWarning() << *response; qWarning() << *response;
return; return;
} }
caller->infoRequestFinished(doc, pack); callback(doc, pack);
}); });
job->start(); 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(); auto response = new QByteArray();
netJob->addNetAction(Net::Download::makeByteArray(getVersionsURL(args), response)); 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{}; QJsonParseError parse_error{};
QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error);
if (parse_error.error != QJsonParseError::NoError) { 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(); << " reason: " << parse_error.errorString();
qWarning() << *response; qWarning() << *response;
return; 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(); netJob->deleteLater();
delete response; delete response;
}); });

View File

@ -5,8 +5,8 @@
class NetworkModAPI : public ModAPI { class NetworkModAPI : public ModAPI {
public: public:
void searchMods(CallerType* caller, SearchArgs&& args) const override; void searchMods(CallerType* caller, SearchArgs&& args) const override;
void getModInfo(CallerType* caller, ModPlatform::IndexedPack& pack) override; void getModInfo(ModPlatform::IndexedPack& pack, std::function<void(QJsonDocument&, ModPlatform::IndexedPack&)> callback) override;
void getVersions(CallerType* caller, VersionSearchArgs&& args) const override; void getVersions(VersionSearchArgs&& args, std::function<void(QJsonDocument&, QString)> callback) const override;
auto getProject(QString addonId, QByteArray* response) const -> NetJob* override; auto getProject(QString addonId, QByteArray* response) const -> NetJob* override;

View File

@ -87,6 +87,8 @@ void Modrinth::loadExtraPackData(ModPlatform::IndexedPack& pack, QJsonObject& ob
pack.extraData.donate.append(donate); pack.extraData.donate.append(donate);
} }
pack.extraData.body = Json::ensureString(obj, "body");
pack.extraDataLoaded = true; pack.extraDataLoaded = true;
} }

View File

@ -19,36 +19,33 @@
#include "ModDownloadDialog.h" #include "ModDownloadDialog.h"
#include <BaseVersion.h> #include <BaseVersion.h>
#include <icons/IconList.h>
#include <InstanceList.h> #include <InstanceList.h>
#include <icons/IconList.h>
#include "Application.h" #include "Application.h"
#include "ProgressDialog.h"
#include "ReviewMessageBox.h" #include "ReviewMessageBox.h"
#include <QDialogButtonBox>
#include <QLayout> #include <QLayout>
#include <QPushButton> #include <QPushButton>
#include <QValidator> #include <QValidator>
#include <QDialogButtonBox>
#include "ui/widgets/PageContainer.h"
#include "ui/pages/modplatform/modrinth/ModrinthModPage.h"
#include "ModDownloadTask.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)
ModDownloadDialog::ModDownloadDialog(const std::shared_ptr<ModFolderModel> &mods, QWidget *parent, : QDialog(parent), mods(mods), m_verticalLayout(new QVBoxLayout(this)), m_instance(instance)
BaseInstance *instance)
: QDialog(parent), mods(mods), m_instance(instance)
{ {
setObjectName(QStringLiteral("ModDownloadDialog")); setObjectName(QStringLiteral("ModDownloadDialog"));
m_verticalLayout->setObjectName(QStringLiteral("verticalLayout"));
resize(std::max(0.5 * parent->width(), 400.0), std::max(0.75 * parent->height(), 400.0)); 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"));
setWindowIcon(APPLICATION->getThemedIcon("new")); 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_buttons = new QDialogButtonBox(QDialogButtonBox::Help | QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
m_container = new PageContainer(this); m_container = new PageContainer(this);
@ -58,12 +55,17 @@ ModDownloadDialog::ModDownloadDialog(const std::shared_ptr<ModFolderModel> &mods
m_container->addButtons(m_buttons); 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... // 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 // See: https://stackoverflow.com/questions/24556831/qbuttonbox-set-default-button
auto OkButton = m_buttons->button(QDialogButtonBox::Ok); auto OkButton = m_buttons->button(QDialogButtonBox::Ok);
OkButton->setEnabled(false); OkButton->setEnabled(false);
OkButton->setDefault(true); OkButton->setDefault(true);
OkButton->setAutoDefault(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); connect(OkButton, &QPushButton::clicked, this, &ModDownloadDialog::confirm);
auto CancelButton = m_buttons->button(QDialogButtonBox::Cancel); auto CancelButton = m_buttons->button(QDialogButtonBox::Cancel);
@ -78,7 +80,9 @@ ModDownloadDialog::ModDownloadDialog(const std::shared_ptr<ModFolderModel> &mods
QMetaObject::connectSlotsByName(this); QMetaObject::connectSlotsByName(this);
setWindowModality(Qt::WindowModal); setWindowModality(Qt::WindowModal);
setWindowTitle("Download mods"); setWindowTitle(dialogTitle());
restoreGeometry(QByteArray::fromBase64(APPLICATION->settings()->get("ModDownloadGeometry").toByteArray()));
} }
QString ModDownloadDialog::dialogTitle() QString ModDownloadDialog::dialogTitle()
@ -88,6 +92,7 @@ QString ModDownloadDialog::dialogTitle()
void ModDownloadDialog::reject() void ModDownloadDialog::reject()
{ {
APPLICATION->settings()->set("ModDownloadGeometry", saveGeometry().toBase64());
QDialog::reject(); QDialog::reject();
} }
@ -114,6 +119,7 @@ void ModDownloadDialog::confirm()
void ModDownloadDialog::accept() void ModDownloadDialog::accept()
{ {
APPLICATION->settings()->set("ModDownloadGeometry", saveGeometry().toBase64());
QDialog::accept(); QDialog::accept();
} }
@ -128,7 +134,7 @@ QList<BasePage *> ModDownloadDialog::getPages()
return pages; return pages;
} }
void ModDownloadDialog::addSelectedMod(const QString& name, ModDownloadTask* task) void ModDownloadDialog::addSelectedMod(QString name, ModDownloadTask* task)
{ {
removeSelectedMod(name); removeSelectedMod(name);
modTask.insert(name, task); modTask.insert(name, task);
@ -136,7 +142,7 @@ void ModDownloadDialog::addSelectedMod(const QString& name, ModDownloadTask* tas
m_buttons->button(QDialogButtonBox::Ok)->setEnabled(!modTask.isEmpty()); 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(); delete modTask.find(name).value();
@ -145,7 +151,7 @@ void ModDownloadDialog::removeSelectedMod(const QString &name)
m_buttons->button(QDialogButtonBox::Ok)->setEnabled(!modTask.isEmpty()); 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 // FIXME: Is there a way to check for versions without checking the filename
// as a heuristic, other than adding such info to ModDownloadTask itself? // 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); 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); auto iter = modTask.find(name);
return iter != modTask.end(); return iter != modTask.end();
} }
ModDownloadDialog::~ModDownloadDialog() const QList<ModDownloadTask*> ModDownloadDialog::getTasks()
{ {
}
const QList<ModDownloadTask*> ModDownloadDialog::getTasks() {
return modTask.values(); 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());
}

View File

@ -21,11 +21,9 @@
#include <QDialog> #include <QDialog>
#include <QVBoxLayout> #include <QVBoxLayout>
#include "BaseVersion.h"
#include "ui/pages/BasePageProvider.h"
#include "minecraft/mod/ModFolderModel.h"
#include "ModDownloadTask.h" #include "ModDownloadTask.h"
#include "ui/pages/modplatform/flame/FlameModPage.h" #include "minecraft/mod/ModFolderModel.h"
#include "ui/pages/BasePageProvider.h"
namespace Ui namespace Ui
{ {
@ -36,21 +34,21 @@ class PageContainer;
class QDialogButtonBox; class QDialogButtonBox;
class ModrinthModPage; class ModrinthModPage;
class ModDownloadDialog : public QDialog, public BasePageProvider class ModDownloadDialog final : public QDialog, public BasePageProvider
{ {
Q_OBJECT Q_OBJECT
public: public:
explicit ModDownloadDialog(const std::shared_ptr<ModFolderModel>& mods, QWidget* parent, BaseInstance* instance); explicit ModDownloadDialog(const std::shared_ptr<ModFolderModel>& mods, QWidget* parent, BaseInstance* instance);
~ModDownloadDialog(); ~ModDownloadDialog() override = default;
QString dialogTitle() override; QString dialogTitle() override;
QList<BasePage*> getPages() override; QList<BasePage*> getPages() override;
void addSelectedMod(const QString & name = QString(), ModDownloadTask * task = nullptr); void addSelectedMod(QString name = QString(), ModDownloadTask* task = nullptr);
void removeSelectedMod(const QString & name = QString()); void removeSelectedMod(QString name = QString());
bool isModSelected(const QString & name, const QString & filename) const; bool isModSelected(QString name, QString filename) const;
bool isModSelected(const QString & name) const; bool isModSelected(QString name) const;
const QList<ModDownloadTask*> getTasks(); const QList<ModDownloadTask*> getTasks();
const std::shared_ptr<ModFolderModel> &mods; const std::shared_ptr<ModFolderModel> &mods;
@ -60,6 +58,9 @@ public slots:
void accept() override; void accept() override;
void reject() override; void reject() override;
private slots:
void selectedPageChanged(BasePage* previous, BasePage* selected);
private: private:
Ui::ModDownloadDialog *ui = nullptr; Ui::ModDownloadDialog *ui = nullptr;
PageContainer * m_container = nullptr; PageContainer * m_container = nullptr;

View File

@ -1,11 +1,16 @@
#include "ReviewMessageBox.h" #include "ReviewMessageBox.h"
#include "ui_ReviewMessageBox.h" #include "ui_ReviewMessageBox.h"
#include <QPushButton>
ReviewMessageBox::ReviewMessageBox(QWidget* parent, QString const& title, QString const& icon) ReviewMessageBox::ReviewMessageBox(QWidget* parent, QString const& title, QString const& icon)
: QDialog(parent), ui(new Ui::ReviewMessageBox) : QDialog(parent), ui(new Ui::ReviewMessageBox)
{ {
ui->setupUi(this); 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::accepted, this, &ReviewMessageBox::accept);
connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &ReviewMessageBox::reject); connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &ReviewMessageBox::reject);
} }

View File

@ -2,15 +2,27 @@
#include "BuildConfig.h" #include "BuildConfig.h"
#include "Json.h" #include "Json.h"
#include "ModPage.h"
#include "minecraft/MinecraftInstance.h" #include "minecraft/MinecraftInstance.h"
#include "minecraft/PackProfile.h" #include "minecraft/PackProfile.h"
#include "ui/dialogs/ModDownloadDialog.h" #include "ui/dialogs/ModDownloadDialog.h"
#include "ui/widgets/ProjectItem.h"
#include <QMessageBox> #include <QMessageBox>
namespace ModPlatform { 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 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); ModPlatform::IndexedPack pack = modpacks.at(pos);
switch (role) { switch (role) {
case Qt::DisplayRole: {
return pack.name;
}
case Qt::ToolTipRole: { case Qt::ToolTipRole: {
if (pack.description.length() > 100) { if (pack.description.length() > 100) {
// some magic to prevent to long tooltips and replace html linebreaks // 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); ((ListModel*)this)->requestLogo(pack.logoName, pack.logoUrl);
return icon; return icon;
} }
case Qt::SizeHintRole:
return QSize(0, 58);
case Qt::UserRole: { case Qt::UserRole: {
QVariant v; QVariant v;
v.setValue(pack); v.setValue(pack);
return v; return v;
} }
case Qt::FontRole: { // Custom data
QFont font; case UserDataTypes::TITLE:
if (m_parent->getDialog()->isModSelected(pack.name)) { return pack.name;
font.setBold(true); case UserDataTypes::DESCRIPTION:
font.setUnderline(true); return pack.description;
} case UserDataTypes::SELECTED:
return m_parent->getDialog()->isModSelected(pack.name);
return font;
}
default: default:
break; break;
} }
@ -85,11 +94,27 @@ auto ListModel::data(const QModelIndex& index, int role) const -> QVariant
return {}; 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(); 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() void ListModel::performPaginatedSearch()
@ -100,9 +125,13 @@ void ListModel::performPaginatedSearch()
this, { nextSearchOffset, currentSearchTerm, getSorts()[currentSort], profile->getModLoaders(), getMineVersions() }); 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() 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"; qDebug() << "Loading mod info";
@ -268,10 +297,20 @@ void ListModel::infoRequestFinished(QJsonDocument& doc, ModPlatform::IndexedPack
qWarning() << "Error while reading " << debugName() << " mod info: " << e.cause(); 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(); 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(); auto& current = m_parent->getCurrent();
if (addonId != current.addonId) { if (addonId != current.addonId) {
@ -287,6 +326,14 @@ void ListModel::versionRequestSucceeded(QJsonDocument doc, QString addonId)
qWarning() << "Error while reading " << debugName() << " mod version: " << e.cause(); 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(); m_parent->updateModVersions();
} }

View File

@ -2,7 +2,6 @@
#include <QAbstractListModel> #include <QAbstractListModel>
#include "modplatform/ModAPI.h"
#include "modplatform/ModIndex.h" #include "modplatform/ModIndex.h"
#include "net/NetJob.h" #include "net/NetJob.h"
@ -19,7 +18,7 @@ class ListModel : public QAbstractListModel {
public: public:
ListModel(ModPage* parent); ListModel(ModPage* parent);
~ListModel() override = default; ~ListModel() override;
inline auto rowCount(const QModelIndex& parent) const -> int override { return modpacks.size(); }; inline auto rowCount(const QModelIndex& parent) const -> int override { return modpacks.size(); };
inline auto columnCount(const QModelIndex& parent) const -> int override { return 1; }; 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 */ /* Retrieve information from the model at a given index with the given role */
auto data(const QModelIndex& index, int role) const -> QVariant override; 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 void setActiveJob(NetJob::Ptr ptr) { jobPtr = ptr; }
inline NetJob* activeJob() { return jobPtr.get(); }
/* Ask the API for more information */ /* Ask the API for more information */
void fetchMore(const QModelIndex& parent) override; void fetchMore(const QModelIndex& parent) override;
void refresh(); void refresh();
void searchWithTerm(const QString& term, const int sort, const bool filter_changed); void searchWithTerm(const QString& term, const int sort, const bool filter_changed);
void requestModInfo(ModPlatform::IndexedPack& current); void requestModInfo(ModPlatform::IndexedPack& current, QModelIndex index);
void requestModVersions(const ModPlatform::IndexedPack& current); void requestModVersions(const ModPlatform::IndexedPack& current, QModelIndex index);
virtual void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) = 0; virtual void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) = 0;
virtual void loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) {}; virtual void loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) {};
@ -51,9 +52,9 @@ class ListModel : public QAbstractListModel {
void searchRequestFinished(QJsonDocument& doc); void searchRequestFinished(QJsonDocument& doc);
void searchRequestFailed(QString reason); 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: protected slots:

View File

@ -40,9 +40,12 @@
#include <QKeyEvent> #include <QKeyEvent>
#include <memory> #include <memory>
#include <HoeDown.h>
#include "minecraft/MinecraftInstance.h" #include "minecraft/MinecraftInstance.h"
#include "minecraft/PackProfile.h" #include "minecraft/PackProfile.h"
#include "ui/dialogs/ModDownloadDialog.h" #include "ui/dialogs/ModDownloadDialog.h"
#include "ui/widgets/ProjectItem.h"
ModPage::ModPage(ModDownloadDialog* dialog, BaseInstance* instance, ModAPI* api) ModPage::ModPage(ModDownloadDialog* dialog, BaseInstance* instance, ModAPI* api)
: QWidget(dialog) : QWidget(dialog)
@ -50,17 +53,30 @@ ModPage::ModPage(ModDownloadDialog* dialog, BaseInstance* instance, ModAPI* api)
, ui(new Ui::ModPage) , ui(new Ui::ModPage)
, dialog(dialog) , dialog(dialog)
, filter_widget(static_cast<MinecraftInstance*>(instance)->getPackProfile()->getComponentVersion("net.minecraft"), this) , filter_widget(static_cast<MinecraftInstance*>(instance)->getPackProfile()->getComponentVersion("net.minecraft"), this)
, m_fetch_progress(this, false)
, api(api) , api(api)
{ {
ui->setupUi(this); ui->setupUi(this);
connect(ui->searchButton, &QPushButton::clicked, this, &ModPage::triggerSearch); connect(ui->searchButton, &QPushButton::clicked, this, &ModPage::triggerSearch);
connect(ui->modFilterButton, &QPushButton::clicked, this, &ModPage::filterMods); 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->searchEdit->installEventFilter(this);
ui->versionSelectionBox->view()->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); ui->versionSelectionBox->view()->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
ui->versionSelectionBox->view()->parentWidget()->setMaximumHeight(300); 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)); filter_widget.setInstance(static_cast<MinecraftInstance*>(m_instance));
m_filter = filter_widget.getFilter(); m_filter = filter_widget.getFilter();
@ -71,6 +87,9 @@ ModPage::ModPage(ModDownloadDialog* dialog, BaseInstance* instance, ModAPI* api)
connect(&filter_widget, &ModFilterWidget::filterUnchanged, this, [&]{ connect(&filter_widget, &ModFilterWidget::filterUnchanged, this, [&]{
ui->searchButton->setStyleSheet("text-decoration: none"); ui->searchButton->setStyleSheet("text-decoration: none");
}); });
ui->packView->setItemDelegate(new ProjectItemDelegate(this));
ui->packView->installEventFilter(this);
} }
ModPage::~ModPage() ModPage::~ModPage()
@ -93,6 +112,23 @@ auto ModPage::eventFilter(QObject* watched, QEvent* event) -> bool
auto* keyEvent = dynamic_cast<QKeyEvent*>(event); auto* keyEvent = dynamic_cast<QKeyEvent*>(event);
if (keyEvent->key() == Qt::Key_Return) { if (keyEvent->key() == Qt::Key_Return) {
triggerSearch(); 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(); keyEvent->accept();
return true; return true;
} }
@ -120,16 +156,26 @@ void ModPage::triggerSearch()
updateSelectionButton(); 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(); 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) { if (!current.versionsLoaded) {
qDebug() << QString("Loading %1 mod versions").arg(debugName()); 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->setText(tr("Loading versions..."));
ui->modSelectionButton->setEnabled(false); ui->modSelectionButton->setEnabled(false);
listModel->requestModVersions(current); listModel->requestModVersions(current, curr);
} else { } else {
for (int i = 0; i < current.versions.size(); i++) { for (int i = 0; i < current.versions.size(); i++) {
ui->versionSelectionBox->addItem(current.versions[i].version, QVariant(i)); ui->versionSelectionBox->addItem(current.versions[i].version, QVariant(i));
@ -149,7 +195,8 @@ void ModPage::onSelectionChanged(QModelIndex first, QModelIndex second)
if(!current.extraDataLoaded){ if(!current.extraDataLoaded){
qDebug() << QString("Loading %1 mod info").arg(debugName()); qDebug() << QString("Loading %1 mod info").arg(debugName());
listModel->requestModInfo(current);
listModel->requestModInfo(current, curr);
} }
updateUi(); updateUi();
@ -167,6 +214,9 @@ void ModPage::onVersionSelectionChanged(QString data)
void ModPage::onModSelected() void ModPage::onModSelected()
{ {
if (selectedVersion < 0)
return;
auto& version = current.versions[selectedVersion]; auto& version = current.versions[selectedVersion];
if (dialog->isModSelected(current.name, version.fileName)) { if (dialog->isModSelected(current.name, version.fileName)) {
dialog->removeSelectedMod(current.name); dialog->removeSelectedMod(current.name);
@ -176,6 +226,9 @@ void ModPage::onModSelected()
} }
updateSelectionButton(); updateSelectionButton();
/* Force redraw on the mods list when the selection changes */
ui->packView->adjustSize();
} }
@ -285,5 +338,6 @@ void ModPage::updateUi()
text += "<hr>"; 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())));
} }

View File

@ -8,6 +8,7 @@
#include "ui/pages/BasePage.h" #include "ui/pages/BasePage.h"
#include "ui/pages/modplatform/ModModel.h" #include "ui/pages/modplatform/ModModel.h"
#include "ui/widgets/ModFilterWidget.h" #include "ui/widgets/ModFilterWidget.h"
#include "ui/widgets/ProgressWidget.h"
class ModDownloadDialog; 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 getFilter() const -> const std::shared_ptr<ModFilterWidget::Filter> { return m_filter; }
auto getDialog() const -> const ModDownloadDialog* { return dialog; } 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; } auto getCurrent() -> ModPlatform::IndexedPack& { return current; }
void updateModVersions(int prev_count = -1); void updateModVersions(int prev_count = -1);
@ -70,10 +76,15 @@ class ModPage : public QWidget, public BasePage {
ModFilterWidget filter_widget; ModFilterWidget filter_widget;
std::shared_ptr<ModFilterWidget::Filter> m_filter; std::shared_ptr<ModFilterWidget::Filter> m_filter;
ProgressWidget m_fetch_progress;
ModPlatform::ListModel* listModel = nullptr; ModPlatform::ListModel* listModel = nullptr;
ModPlatform::IndexedPack current; ModPlatform::IndexedPack current;
std::unique_ptr<ModAPI> api; std::unique_ptr<ModAPI> api;
int selectedVersion = -1; int selectedVersion = -1;
// Used to do instant searching with a delay to cache quick changes
QTimer m_search_timer;
}; };

View File

@ -12,6 +12,12 @@ void ListModel::loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj)
FlameMod::loadIndexedPack(m, 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) void ListModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr)
{ {
FlameMod::loadIndexedPackVersions(m, arr, APPLICATION->network(), m_parent->m_instance); FlameMod::loadIndexedPackVersions(m, arr, APPLICATION->network(), m_parent->m_instance);

View File

@ -13,6 +13,7 @@ class ListModel : public ModPlatform::ListModel {
private: private:
void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) override; void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) override;
void loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) override;
void loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) override; void loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) override;
auto documentToArray(QJsonDocument& obj) const -> QJsonArray override; auto documentToArray(QJsonDocument& obj) const -> QJsonArray override;

View File

@ -1,27 +1,33 @@
#include "Common.h" #include "Common.h"
// Origin: Qt // Origin: Qt
QStringList viewItemTextLayout(QTextLayout &textLayout, int lineWidth, qreal &height, // More specifically, this is a trimmed down version on the algorithm in:
qreal &widthUsed) // 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; height = 0;
widthUsed = 0;
textLayout.beginLayout(); textLayout.beginLayout();
QString str = textLayout.text(); QString str = textLayout.text();
while (true) while (true) {
{
QTextLine line = textLayout.createLine(); QTextLine line = textLayout.createLine();
if (!line.isValid()) if (!line.isValid())
break; break;
if (line.textLength() == 0) if (line.textLength() == 0)
break; break;
line.setLineWidth(lineWidth); line.setLineWidth(lineWidth);
line.setPosition(QPointF(0, height)); line.setPosition(QPointF(0, height));
height += line.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(); textLayout.endLayout();
return lines; return lines;
} }

View File

@ -1,6 +1,9 @@
#pragma once #pragma once
#include <QStringList>
#include <QTextLayout> #include <QTextLayout>
QStringList viewItemTextLayout(QTextLayout &textLayout, int lineWidth, qreal &height, /** Cuts out the text in textLayout into smaller pieces, according to the lineWidth.
qreal &widthUsed); * 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);

View File

@ -244,7 +244,14 @@ void PageContainer::help()
void PageContainer::currentChanged(const QModelIndex &current) 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() bool PageContainer::prepareToClose()

View File

@ -95,6 +95,10 @@ private:
public slots: public slots:
void help(); void help();
signals:
/** Emitted when the currently selected page is changed */
void selectedPageChanged(BasePage* previous, BasePage* selected);
private slots: private slots:
void currentChanged(const QModelIndex &current); void currentChanged(const QModelIndex &current);
void showPage(int row); void showPage(int row);

View File

@ -1,65 +1,103 @@
// Licensed under the Apache-2.0 license. See README.md for details. // Licensed under the Apache-2.0 license. See README.md for details.
#include "ProgressWidget.h" #include "ProgressWidget.h"
#include <QProgressBar>
#include <QLabel>
#include <QVBoxLayout>
#include <QEventLoop> #include <QEventLoop>
#include <QLabel>
#include <QProgressBar>
#include <QVBoxLayout>
#include "tasks/Task.h" #include "tasks/Task.h"
ProgressWidget::ProgressWidget(QWidget *parent) ProgressWidget::ProgressWidget(QWidget* parent, bool show_label) : QWidget(parent)
: QWidget(parent)
{ {
auto* layout = new QVBoxLayout(this);
if (show_label) {
m_label = new QLabel(this); m_label = new QLabel(this);
m_label->setWordWrap(true); m_label->setWordWrap(true);
layout->addWidget(m_label);
}
m_bar = new QProgressBar(this); m_bar = new QProgressBar(this);
m_bar->setMinimum(0); m_bar->setMinimum(0);
m_bar->setMaximum(100); m_bar->setMaximum(100);
QVBoxLayout *layout = new QVBoxLayout(this);
layout->addWidget(m_label);
layout->addWidget(m_bar); layout->addWidget(m_bar);
layout->addStretch();
setLayout(layout); 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) if (m_task)
{ disconnect(m_task, nullptr, this, nullptr);
disconnect(m_task.get(), 0, this, 0);
}
m_task = task; m_task = task;
connect(m_task.get(), &Task::finished, this, &ProgressWidget::handleTaskFinish);
connect(m_task.get(), &Task::status, this, &ProgressWidget::handleTaskStatus); connect(m_task, &Task::finished, this, &ProgressWidget::handleTaskFinish);
connect(m_task.get(), &Task::progress, this, &ProgressWidget::handleTaskProgress); connect(m_task, &Task::status, this, &ProgressWidget::handleTaskStatus);
connect(m_task.get(), &Task::destroyed, this, &ProgressWidget::taskDestroyed); connect(m_task, &Task::progress, this, &ProgressWidget::handleTaskProgress);
if (!m_task->isRunning()) connect(m_task, &Task::destroyed, this, &ProgressWidget::taskDestroyed);
show();
}
void ProgressWidget::start(Task* task)
{ {
QMetaObject::invokeMethod(m_task.get(), "start", Qt::QueuedConnection); watch(task);
} if (!m_task->isRunning())
QMetaObject::invokeMethod(m_task, "start", Qt::QueuedConnection);
} }
bool ProgressWidget::exec(std::shared_ptr<Task> task) bool ProgressWidget::exec(std::shared_ptr<Task> task)
{ {
QEventLoop loop; QEventLoop loop;
connect(task.get(), &Task::finished, &loop, &QEventLoop::quit); connect(task.get(), &Task::finished, &loop, &QEventLoop::quit);
start(task);
start(task.get());
if (task->isRunning()) if (task->isRunning())
{
loop.exec(); loop.exec();
}
return task->wasSuccessful(); return task->wasSuccessful();
} }
void ProgressWidget::show()
{
setHidden(false);
}
void ProgressWidget::hide()
{
setHidden(true);
}
void ProgressWidget::handleTaskFinish() void ProgressWidget::handleTaskFinish()
{ {
if (!m_task->wasSuccessful()) if (!m_task->wasSuccessful() && m_label)
{
m_label->setText(m_task->failReason()); m_label->setText(m_task->failReason());
}
if (m_hide_if_inactive)
hide();
} }
void ProgressWidget::handleTaskStatus(const QString& status) void ProgressWidget::handleTaskStatus(const QString& status)
{ {
if (m_label)
m_label->setText(status); m_label->setText(status);
} }
void ProgressWidget::handleTaskProgress(qint64 current, qint64 total) void ProgressWidget::handleTaskProgress(qint64 current, qint64 total)

View File

@ -9,16 +9,38 @@ class Task;
class QProgressBar; class QProgressBar;
class QLabel; class QLabel;
class ProgressWidget : public QWidget class ProgressWidget : public QWidget {
{
Q_OBJECT Q_OBJECT
public: public:
explicit ProgressWidget(QWidget *parent = nullptr); explicit ProgressWidget(QWidget* parent = nullptr, bool show_label = true);
/** 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: public slots:
void start(std::shared_ptr<Task> task); /** 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); bool exec(std::shared_ptr<Task> task);
/** Un-hide the widget if needed. */
void show();
/** Make the widget invisible. */
void hide();
private slots: private slots:
void handleTaskFinish(); void handleTaskFinish();
void handleTaskStatus(const QString& status); void handleTaskStatus(const QString& status);
@ -26,7 +48,9 @@ private slots:
void taskDestroyed(); void taskDestroyed();
private: private:
QLabel *m_label; QLabel* m_label = nullptr;
QProgressBar *m_bar; QProgressBar* m_bar = nullptr;
std::shared_ptr<Task> m_task; Task* m_task = nullptr;
bool m_hide_if_inactive = false;
}; };

View File

@ -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();
}

View File

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