aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.github/workflows/build.yml16
-rw-r--r--launcher/Application.cpp35
-rw-r--r--launcher/Application.h9
-rw-r--r--launcher/CMakeLists.txt2
-rw-r--r--launcher/minecraft/MinecraftInstance.cpp9
-rw-r--r--launcher/minecraft/launch/DirectJavaLaunch.cpp4
-rw-r--r--launcher/minecraft/launch/LauncherPartLaunch.cpp27
-rw-r--r--launcher/modplatform/ModAPI.h4
-rw-r--r--launcher/modplatform/ModIndex.h2
-rw-r--r--launcher/modplatform/atlauncher/ATLPackInstallTask.cpp2
-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/net/Download.cpp2
-rw-r--r--launcher/net/NetAction.h2
-rw-r--r--launcher/net/NetJob.cpp202
-rw-r--r--launcher/net/NetJob.h48
-rw-r--r--launcher/net/Upload.cpp2
-rw-r--r--launcher/tasks/ConcurrentTask.cpp30
-rw-r--r--launcher/tasks/ConcurrentTask.h4
-rw-r--r--launcher/tasks/MultipleOptionsTask.cpp41
-rw-r--r--launcher/tasks/MultipleOptionsTask.h14
-rw-r--r--launcher/tasks/SequentialTask.cpp102
-rw-r--r--launcher/tasks/SequentialTask.h60
-rw-r--r--launcher/tasks/Task_test.cpp125
-rw-r--r--launcher/ui/dialogs/AboutDialog.ui4
-rw-r--r--launcher/ui/dialogs/ModDownloadDialog.cpp73
-rw-r--r--launcher/ui/dialogs/ModDownloadDialog.h25
-rw-r--r--launcher/ui/dialogs/NewInstanceDialog.cpp2
-rw-r--r--launcher/ui/dialogs/ReviewMessageBox.cpp5
-rw-r--r--launcher/ui/pages/global/AccountListPage.cpp2
-rw-r--r--launcher/ui/pages/global/MinecraftPage.cpp10
-rw-r--r--launcher/ui/pages/instance/InstanceSettingsPage.cpp14
-rw-r--r--launcher/ui/pages/modplatform/ModModel.cpp90
-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/pages/modplatform/modrinth/ModrinthModel.cpp5
-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
-rw-r--r--libraries/launcher/org/polymc/impl/OneSixLauncher.java86
53 files changed, 891 insertions, 643 deletions
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 99d9cd07..26820d47 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -27,7 +27,6 @@ jobs:
qt_host: linux
qt_version: '6.2.4'
qt_modules: 'qt5compat qtimageformats'
- qt_path: /home/runner/work/PolyMC/Qt
- os: windows-2022
name: "Windows-Legacy"
@@ -45,7 +44,6 @@ jobs:
qt_host: mac
qt_version: '6.3.0'
qt_modules: 'qt5compat qtimageformats'
- qt_path: /Users/runner/work/PolyMC/Qt
runs-on: ${{ matrix.os }}
@@ -141,24 +139,16 @@ jobs:
run: |
sudo apt-get -y install qtbase5-dev qtchooser qt5-qmake qtbase5-dev-tools libqt5core5a libqt5network5 libqt5gui5
- - name: Cache Qt (macOS and AppImage)
- id: cache-qt
- if: matrix.qt_ver == 6 && runner.os != 'Windows'
- uses: actions/cache@v3
- with:
- path: '${{ matrix.qt_path }}/${{ matrix.qt_version }}'
- key: ${{ matrix.qt_host }}-${{ matrix.qt_version }}-"${{ matrix.qt_modules }}"-qt_cache
-
- name: Install Qt (macOS and AppImage)
if: matrix.qt_ver == 6 && runner.os != 'Windows'
- uses: jurplel/install-qt-action@v2
+ uses: jurplel/install-qt-action@v3
with:
version: ${{ matrix.qt_version }}
host: ${{ matrix.qt_host }}
target: 'desktop'
modules: ${{ matrix.qt_modules }}
- cached: ${{ steps.cache-qt.outputs.cache-hit }}
- aqtversion: ==2.1.*
+ cache: true
+ cache-key-prefix: ${{ matrix.qt_host }}-${{ matrix.qt_version }}-"${{ matrix.qt_modules }}"-qt_cache
- name: Prepare AppImage (Linux)
if: runner.os == 'Linux' && matrix.qt_ver != 5
diff --git a/launcher/Application.cpp b/launcher/Application.cpp
index 553b3229..aa937964 100644
--- a/launcher/Application.cpp
+++ b/launcher/Application.cpp
@@ -113,6 +113,11 @@
#include <sys.h>
+#ifdef Q_OS_LINUX
+#include <dlfcn.h>
+#include "gamemode_client.h"
+#endif
+
#if defined Q_OS_WIN32
#ifndef WIN32_LEAN_AND_MEAN
@@ -680,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", "");
@@ -920,6 +927,8 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv)
{
return;
}
+
+ updateCapabilities();
performMainStartupAction();
}
@@ -1568,14 +1577,30 @@ shared_qobject_ptr<Meta::Index> Application::metadataIndex()
return m_metadataIndex;
}
-Application::Capabilities Application::currentCapabilities()
+void Application::updateCapabilities()
{
- Capabilities c;
+ m_capabilities = None;
if (!getMSAClientID().isEmpty())
- c |= SupportsMSA;
+ m_capabilities |= SupportsMSA;
if (!getFlameAPIKey().isEmpty())
- c |= SupportsFlame;
- return c;
+ m_capabilities |= SupportsFlame;
+
+#ifdef Q_OS_LINUX
+ if (gamemode_query_status() >= 0)
+ m_capabilities |= SupportsGameMode;
+
+ {
+ void *dummy = dlopen("libMangoHud_dlsym.so", RTLD_LAZY);
+ // try normal variant as well
+ if (dummy == NULL)
+ dummy = dlopen("libMangoHud.so", RTLD_LAZY);
+
+ if (dummy != NULL) {
+ dlclose(dummy);
+ m_capabilities |= SupportsMangoHud;
+ }
+ }
+#endif
}
QString Application::getJarPath(QString jarFile)
diff --git a/launcher/Application.h b/launcher/Application.h
index 019c3c3d..41fd4c47 100644
--- a/launcher/Application.h
+++ b/launcher/Application.h
@@ -95,6 +95,8 @@ public:
SupportsMSA = 1 << 0,
SupportsFlame = 1 << 1,
+ SupportsGameMode = 1 << 2,
+ SupportsMangoHud = 1 << 3,
};
Q_DECLARE_FLAGS(Capabilities, Capability)
@@ -162,7 +164,7 @@ public:
shared_qobject_ptr<Meta::Index> metadataIndex();
- Capabilities currentCapabilities();
+ void updateCapabilities();
/*!
* Finds and returns the full path to a jar file.
@@ -180,6 +182,10 @@ public:
return m_rootPath;
}
+ const Capabilities capabilities() {
+ return m_capabilities;
+ }
+
/*!
* Opens a json file using either a system default editor, or, if not empty, the editor
* specified in the settings
@@ -258,6 +264,7 @@ private:
QString m_rootPath;
Status m_status = Application::StartingUp;
+ Capabilities m_capabilities;
#ifdef Q_OS_MACOS
Qt::ApplicationState m_prevAppState = Qt::ApplicationInactive;
diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt
index 234ff454..a5303e94 100644
--- a/launcher/CMakeLists.txt
+++ b/launcher/CMakeLists.txt
@@ -904,6 +904,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/minecraft/MinecraftInstance.cpp b/launcher/minecraft/MinecraftInstance.cpp
index cf127525..9478b1b8 100644
--- a/launcher/minecraft/MinecraftInstance.cpp
+++ b/launcher/minecraft/MinecraftInstance.cpp
@@ -455,13 +455,11 @@ QProcessEnvironment MinecraftInstance::createLaunchEnvironment()
QProcessEnvironment env = createEnvironment();
#ifdef Q_OS_LINUX
- if (settings()->get("EnableMangoHud").toBool())
+ if (settings()->get("EnableMangoHud").toBool() && APPLICATION->capabilities() & Application::SupportsMangoHud)
{
auto preload = env.value("LD_PRELOAD", "") + ":libMangoHud_dlsym.so:libMangoHud.so";
- auto lib_path = env.value("LD_LIBRARY_PATH", "") + ":/usr/local/$LIB/mangohud/:/usr/$LIB/mangohud/";
env.insert("LD_PRELOAD", preload);
- env.insert("LD_LIBRARY_PATH", lib_path);
env.insert("MANGOHUD", "1");
}
@@ -570,11 +568,6 @@ QString MinecraftInstance::createLaunchScript(AuthSessionPtr session, MinecraftS
if(!profile)
return QString();
- for (auto cp : getClassPath())
- {
- launchScript += "classPath " + cp + "\n";
- }
-
auto mainClass = getMainClass();
if (!mainClass.isEmpty())
{
diff --git a/launcher/minecraft/launch/DirectJavaLaunch.cpp b/launcher/minecraft/launch/DirectJavaLaunch.cpp
index 152485b3..ca55cd2e 100644
--- a/launcher/minecraft/launch/DirectJavaLaunch.cpp
+++ b/launcher/minecraft/launch/DirectJavaLaunch.cpp
@@ -21,6 +21,8 @@
#include <FileSystem.h>
#include <Commandline.h>
+#include "Application.h"
+
#ifdef Q_OS_LINUX
#include "gamemode_client.h"
#endif
@@ -86,7 +88,7 @@ void DirectJavaLaunch::executeTask()
}
#ifdef Q_OS_LINUX
- if (instance->settings()->get("EnableFeralGamemode").toBool())
+ if (instance->settings()->get("EnableFeralGamemode").toBool() && APPLICATION->capabilities() & Application::SupportsGameMode)
{
auto pid = m_process.processId();
if (pid)
diff --git a/launcher/minecraft/launch/LauncherPartLaunch.cpp b/launcher/minecraft/launch/LauncherPartLaunch.cpp
index 3b905bf5..ce477ad7 100644
--- a/launcher/minecraft/launch/LauncherPartLaunch.cpp
+++ b/launcher/minecraft/launch/LauncherPartLaunch.cpp
@@ -95,8 +95,8 @@ bool fitsInLocal8bit(const QString & string)
void LauncherPartLaunch::executeTask()
{
- QString newLaunchJar = APPLICATION->getJarPath("NewLaunch.jar");
- if (newLaunchJar.isEmpty())
+ QString jarPath = APPLICATION->getJarPath("NewLaunch.jar");
+ if (jarPath.isEmpty())
{
const char *reason = QT_TR_NOOP("Launcher library could not be found. Please check your installation.");
emit logLine(tr(reason), MessageLevel::Fatal);
@@ -119,6 +119,9 @@ void LauncherPartLaunch::executeTask()
// make detachable - this will keep the process running even if the object is destroyed
m_process.setDetachable(true);
+ auto classPath = minecraftInstance->getClassPath();
+ classPath.prepend(jarPath);
+
auto natPath = minecraftInstance->getNativePath();
#ifdef Q_OS_WIN
if (!fitsInLocal8bit(natPath))
@@ -134,7 +137,23 @@ void LauncherPartLaunch::executeTask()
#endif
args << "-cp";
- args << newLaunchJar;
+#ifdef Q_OS_WIN
+ QStringList processed;
+ for(auto & item: classPath)
+ {
+ if (!fitsInLocal8bit(item))
+ {
+ processed << shortPathName(item);
+ }
+ else
+ {
+ processed << item;
+ }
+ }
+ args << processed.join(';');
+#else
+ args << classPath.join(':');
+#endif
args << "org.polymc.EntryPoint";
qDebug() << args.join(' ');
@@ -162,7 +181,7 @@ void LauncherPartLaunch::executeTask()
}
#ifdef Q_OS_LINUX
- if (instance->settings()->get("EnableFeralGamemode").toBool())
+ if (instance->settings()->get("EnableFeralGamemode").toBool() && APPLICATION->capabilities() & Application::SupportsGameMode)
{
auto pid = m_process.processId();
if (pid)
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/atlauncher/ATLPackInstallTask.cpp b/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp
index 5ed13470..70a35395 100644
--- a/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp
+++ b/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp
@@ -136,7 +136,7 @@ void PackInstallTask::onDownloadSucceeded()
default:
emitFailed(tr("Unsupported installation mode"));
- break;
+ return;
}
// Display message if one exists
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/net/Download.cpp b/launcher/net/Download.cpp
index 3778b939..fd3dbedc 100644
--- a/launcher/net/Download.cpp
+++ b/launcher/net/Download.cpp
@@ -118,7 +118,7 @@ void Download::executeTask()
}
request.setHeader(QNetworkRequest::UserAgentHeader, APPLICATION->getUserAgent().toUtf8());
- if (APPLICATION->currentCapabilities() & Application::SupportsFlame
+ if (APPLICATION->capabilities() & Application::SupportsFlame
&& request.url().host().contains("api.curseforge.com")) {
request.setRawHeader("x-api-key", APPLICATION->getFlameAPIKey().toUtf8());
};
diff --git a/launcher/net/NetAction.h b/launcher/net/NetAction.h
index 729d4132..d9c4fadc 100644
--- a/launcher/net/NetAction.h
+++ b/launcher/net/NetAction.h
@@ -54,6 +54,8 @@ class NetAction : public Task {
QUrl url() { return m_url; }
auto index() -> int { return m_index_within_job; }
+ void setNetwork(shared_qobject_ptr<QNetworkAccessManager> network) { m_network = network; }
+
protected slots:
virtual void downloadProgress(qint64 bytesReceived, qint64 bytesTotal) = 0;
virtual void downloadError(QNetworkReply::NetworkError error) = 0;
diff --git a/launcher/net/NetJob.cpp b/launcher/net/NetJob.cpp
index bab35fa5..20d75976 100644
--- a/launcher/net/NetJob.cpp
+++ b/launcher/net/NetJob.cpp
@@ -35,204 +35,90 @@
*/
#include "NetJob.h"
-#include "Download.h"
auto NetJob::addNetAction(NetAction::Ptr action) -> bool
{
- action->m_index_within_job = m_downloads.size();
- m_downloads.append(action);
- part_info pi;
- m_parts_progress.append(pi);
-
- partProgress(m_parts_progress.count() - 1, action->getProgress(), action->getTotalProgress());
-
- if (action->isRunning()) {
- connect(action.get(), &NetAction::succeeded, [this, action]{ partSucceeded(action->index()); });
- connect(action.get(), &NetAction::failed, [this, action](QString){ partFailed(action->index()); });
- connect(action.get(), &NetAction::aborted, [this, action](){ partAborted(action->index()); });
- connect(action.get(), &NetAction::progress, [this, action](qint64 done, qint64 total) { partProgress(action->index(), done, total); });
- connect(action.get(), &NetAction::status, this, &NetJob::status);
- } else {
- m_todo.append(m_parts_progress.size() - 1);
- }
+ action->m_index_within_job = m_queue.size();
+ m_queue.append(action);
+
+ action->setNetwork(m_network);
return true;
}
+void NetJob::startNext()
+{
+ if (m_queue.isEmpty() && m_doing.isEmpty()) {
+ // We're finished, check for failures and retry if we can (up to 3 times)
+ if (!m_failed.isEmpty() && m_try < 3) {
+ m_try += 1;
+ while (!m_failed.isEmpty())
+ m_queue.enqueue(m_failed.take(*m_failed.keyBegin()));
+ }
+ }
+
+ ConcurrentTask::startNext();
+}
+
+auto NetJob::size() const -> int
+{
+ return m_queue.size() + m_doing.size() + m_done.size();
+}
+
auto NetJob::canAbort() const -> bool
{
bool canFullyAbort = true;
// can abort the downloads on the queue?
- for (auto index : m_todo) {
- auto part = m_downloads[index];
+ for (auto part : m_queue)
canFullyAbort &= part->canAbort();
- }
+
// can abort the active downloads?
- for (auto index : m_doing) {
- auto part = m_downloads[index];
+ for (auto part : m_doing)
canFullyAbort &= part->canAbort();
- }
return canFullyAbort;
}
-void NetJob::executeTask()
-{
- // hack that delays early failures so they can be caught easier
- QMetaObject::invokeMethod(this, "startMoreParts", Qt::QueuedConnection);
-}
-
-auto NetJob::getFailedFiles() -> QStringList
-{
- QStringList failed;
- for (auto index : m_failed) {
- failed.push_back(m_downloads[index]->url().toString());
- }
- failed.sort();
- return failed;
-}
-
auto NetJob::abort() -> bool
{
bool fullyAborted = true;
// fail all downloads on the queue
-#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
- QSet<int> todoSet(m_todo.begin(), m_todo.end());
- m_failed.unite(todoSet);
-#else
- m_failed.unite(m_todo.toSet());
-#endif
- m_todo.clear();
+ for (auto task : m_queue)
+ m_failed.insert(task.get(), task);
+ m_queue.clear();
// abort active downloads
auto toKill = m_doing.values();
- for (auto index : toKill) {
- auto part = m_downloads[index];
+ for (auto part : toKill) {
fullyAborted &= part->abort();
}
return fullyAborted;
}
-void NetJob::partSucceeded(int index)
-{
- // do progress. all slots are 1 in size at least
- auto& slot = m_parts_progress[index];
- partProgress(index, slot.total_progress, slot.total_progress);
-
- m_doing.remove(index);
- m_done.insert(index);
- m_downloads[index].get()->disconnect(this);
-
- startMoreParts();
-}
-
-void NetJob::partFailed(int index)
+auto NetJob::getFailedActions() -> QList<NetAction*>
{
- m_doing.remove(index);
-
- auto& slot = m_parts_progress[index];
- // Can try 3 times before failing by definitive
- if (slot.failures == 3) {
- m_failed.insert(index);
- } else {
- slot.failures++;
- m_todo.enqueue(index);
+ QList<NetAction*> failed;
+ for (auto index : m_failed) {
+ failed.push_back(dynamic_cast<NetAction*>(index.get()));
}
-
- m_downloads[index].get()->disconnect(this);
-
- startMoreParts();
-}
-
-void NetJob::partAborted(int index)
-{
- m_aborted = true;
-
- m_doing.remove(index);
- m_failed.insert(index);
- m_downloads[index].get()->disconnect(this);
-
- startMoreParts();
+ return failed;
}
-void NetJob::partProgress(int index, qint64 bytesReceived, qint64 bytesTotal)
+auto NetJob::getFailedFiles() -> QList<QString>
{
- auto& slot = m_parts_progress[index];
- slot.current_progress = bytesReceived;
- slot.total_progress = bytesTotal;
-
- int done = m_done.size();
- int doing = m_doing.size();
- int all = m_parts_progress.size();
-
- qint64 bytesAll = 0;
- qint64 bytesTotalAll = 0;
- for (auto& partIdx : m_doing) {
- auto part = m_parts_progress[partIdx];
- // do not count parts with unknown/nonsensical total size
- if (part.total_progress <= 0) {
- continue;
- }
- bytesAll += part.current_progress;
- bytesTotalAll += part.total_progress;
- }
-
- qint64 inprogress = (bytesTotalAll == 0) ? 0 : (bytesAll * 1000) / bytesTotalAll;
- auto current = done * 1000 + doing * inprogress;
- auto current_total = all * 1000;
- // HACK: make sure it never jumps backwards.
- // FAIL: This breaks if the size is not known (or is it something else?) and jumps to 1000, so if it is 1000 reset it to inprogress
- if (m_current_progress == 1000) {
- m_current_progress = inprogress;
- }
- if (m_current_progress > current) {
- current = m_current_progress;
+ QList<QString> failed;
+ for (auto index : m_failed) {
+ failed.append(static_cast<NetAction*>(index.get())->url().toString());
}
- m_current_progress = current;
- setProgress(current, current_total);
+ return failed;
}
-void NetJob::startMoreParts()
+void NetJob::updateState()
{
- if (!isRunning()) {
- // this actually makes sense. You can put running m_downloads into a NetJob and then not start it until much later.
- return;
- }
-
- // OK. We are actively processing tasks, proceed.
- // Check for final conditions if there's nothing in the queue.
- if (!m_todo.size()) {
- if (!m_doing.size()) {
- if (!m_failed.size()) {
- emitSucceeded();
- } else if (m_aborted) {
- emitAborted();
- } else {
- emitFailed(tr("Job '%1' failed to process:\n%2").arg(objectName()).arg(getFailedFiles().join("\n")));
- }
- }
- return;
- }
-
- // There's work to do, try to start more parts, to a maximum of 6 concurrent ones.
- while (m_doing.size() < 6) {
- if (m_todo.size() == 0)
- return;
- int doThis = m_todo.dequeue();
- m_doing.insert(doThis);
-
- auto part = m_downloads[doThis];
-
- // connect signals :D
- connect(part.get(), &NetAction::succeeded, this, [this, part]{ partSucceeded(part->index()); });
- connect(part.get(), &NetAction::failed, this, [this, part](QString){ partFailed(part->index()); });
- connect(part.get(), &NetAction::aborted, this, [this, part]{ partAborted(part->index()); });
- connect(part.get(), &NetAction::progress, this, [this, part](qint64 done, qint64 total) { partProgress(part->index(), done, total); });
- connect(part.get(), &NetAction::status, this, &NetJob::status);
-
- part->startAction(m_network);
- }
+ emit progress(m_done.count(), m_total_size);
+ setStatus(tr("Executing %1 task(s) (%2 out of %3 are done)")
+ .arg(QString::number(m_doing.count()), QString::number(m_done.count()), QString::number(m_total_size)));
}
diff --git a/launcher/net/NetJob.h b/launcher/net/NetJob.h
index 63c1cf51..cd5d5e48 100644
--- a/launcher/net/NetJob.h
+++ b/launcher/net/NetJob.h
@@ -39,64 +39,40 @@
#include <QObject>
#include "NetAction.h"
-#include "tasks/Task.h"
+#include "tasks/ConcurrentTask.h"
// Those are included so that they are also included by anyone using NetJob
#include "net/Download.h"
#include "net/HttpMetaCache.h"
-class NetJob : public Task {
+class NetJob : public ConcurrentTask {
Q_OBJECT
public:
using Ptr = shared_qobject_ptr<NetJob>;
- explicit NetJob(QString job_name, shared_qobject_ptr<QNetworkAccessManager> network) : Task(), m_network(network)
- {
- setObjectName(job_name);
- }
- virtual ~NetJob() = default;
+ explicit NetJob(QString job_name, shared_qobject_ptr<QNetworkAccessManager> network) : ConcurrentTask(nullptr, job_name), m_network(network) {}
+ ~NetJob() override = default;
- void executeTask() override;
+ void startNext() override;
- auto canAbort() const -> bool override;
+ auto size() const -> int;
+ auto canAbort() const -> bool override;
auto addNetAction(NetAction::Ptr action) -> bool;
- auto operator[](int index) -> NetAction::Ptr { return m_downloads[index]; }
- auto at(int index) -> const NetAction::Ptr { return m_downloads.at(index); }
- auto size() const -> int { return m_downloads.size(); }
- auto first() -> NetAction::Ptr { return m_downloads.size() != 0 ? m_downloads[0] : NetAction::Ptr{}; }
-
- auto getFailedFiles() -> QStringList;
+ auto getFailedActions() -> QList<NetAction*>;
+ auto getFailedFiles() -> QList<QString>;
public slots:
// Qt can't handle auto at the start for some reason?
bool abort() override;
- private slots:
- void startMoreParts();
-
- void partProgress(int index, qint64 bytesReceived, qint64 bytesTotal);
- void partSucceeded(int index);
- void partFailed(int index);
- void partAborted(int index);
+ protected:
+ void updateState() override;
private:
shared_qobject_ptr<QNetworkAccessManager> m_network;
- struct part_info {
- qint64 current_progress = 0;
- qint64 total_progress = 1;
- int failures = 0;
- };
-
- QList<NetAction::Ptr> m_downloads;
- QList<part_info> m_parts_progress;
- QQueue<int> m_todo;
- QSet<int> m_doing;
- QSet<int> m_done;
- QSet<int> m_failed;
- qint64 m_current_progress = 0;
- bool m_aborted = false;
+ int m_try = 1;
};
diff --git a/launcher/net/Upload.cpp b/launcher/net/Upload.cpp
index cfda4b4e..f3b19022 100644
--- a/launcher/net/Upload.cpp
+++ b/launcher/net/Upload.cpp
@@ -216,7 +216,7 @@ namespace Net {
}
request.setHeader(QNetworkRequest::UserAgentHeader, APPLICATION->getUserAgent().toUtf8());
- if (APPLICATION->currentCapabilities() & Application::SupportsFlame
+ if (APPLICATION->capabilities() & Application::SupportsFlame
&& request.url().host().contains("api.curseforge.com")) {
request.setRawHeader("x-api-key", APPLICATION->getFlameAPIKey().toUtf8());
}
diff --git a/launcher/tasks/ConcurrentTask.cpp b/launcher/tasks/ConcurrentTask.cpp
index ab7cbd03..484ac58e 100644
--- a/launcher/tasks/ConcurrentTask.cpp
+++ b/launcher/tasks/ConcurrentTask.cpp
@@ -37,32 +37,39 @@ void ConcurrentTask::executeTask()
{
m_total_size = m_queue.size();
- for (int i = 0; i < m_total_max_size; i++) {
+ int num_starts = std::min(m_total_max_size, m_total_size);
+ for (int i = 0; i < num_starts; i++) {
QMetaObject::invokeMethod(this, &ConcurrentTask::startNext, Qt::QueuedConnection);
}
}
bool ConcurrentTask::abort()
{
+ m_queue.clear();
+ m_aborted = true;
+
if (m_doing.isEmpty()) {
// Don't call emitAborted() here, we want to bypass the 'is the task running' check
emit aborted();
emit finished();
- m_aborted = true;
return true;
}
- m_queue.clear();
+ bool suceedeed = true;
- m_aborted = true;
- for (auto task : m_doing)
- m_aborted &= task->abort();
+ QMutableHashIterator<Task*, Task::Ptr> doing_iter(m_doing);
+ while (doing_iter.hasNext()) {
+ auto task = doing_iter.next();
+ suceedeed &= (task.value())->abort();
+ }
- if (m_aborted)
+ if (suceedeed)
emitAborted();
+ else
+ emitFailed(tr("Failed to abort all running tasks."));
- return m_aborted;
+ return suceedeed;
}
void ConcurrentTask::startNext()
@@ -70,7 +77,7 @@ void ConcurrentTask::startNext()
if (m_aborted || m_doing.count() > m_total_max_size)
return;
- if (m_queue.isEmpty() && m_doing.isEmpty()) {
+ if (m_queue.isEmpty() && m_doing.isEmpty() && !wasSuccessful()) {
emitSucceeded();
return;
}
@@ -131,11 +138,6 @@ void ConcurrentTask::subTaskStatus(const QString& msg)
void ConcurrentTask::subTaskProgress(qint64 current, qint64 total)
{
- if (total == 0) {
- setProgress(0, 100);
- return;
- }
-
m_stepProgress = current;
m_stepTotalProgress = total;
}
diff --git a/launcher/tasks/ConcurrentTask.h b/launcher/tasks/ConcurrentTask.h
index 5898899d..f1279d32 100644
--- a/launcher/tasks/ConcurrentTask.h
+++ b/launcher/tasks/ConcurrentTask.h
@@ -9,7 +9,9 @@ class ConcurrentTask : public Task {
Q_OBJECT
public:
explicit ConcurrentTask(QObject* parent = nullptr, QString task_name = "", int max_concurrent = 6);
- virtual ~ConcurrentTask();
+ ~ConcurrentTask() override;
+
+ bool canAbort() const override { return true; }
inline auto isMultiStep() const -> bool override { return m_queue.size() > 1; };
auto getStepProgress() const -> qint64 override;
diff --git a/launcher/tasks/MultipleOptionsTask.cpp b/launcher/tasks/MultipleOptionsTask.cpp
index 6e853568..5ad6181f 100644
--- a/launcher/tasks/MultipleOptionsTask.cpp
+++ b/launcher/tasks/MultipleOptionsTask.cpp
@@ -6,43 +6,22 @@ MultipleOptionsTask::MultipleOptionsTask(QObject* parent, const QString& task_na
void MultipleOptionsTask::startNext()
{
- Task* previous = nullptr;
- if (m_currentIndex != -1) {
- previous = m_queue[m_currentIndex].get();
- disconnect(previous, 0, this, 0);
- }
-
- m_currentIndex++;
- if ((previous && previous->wasSuccessful())) {
+ if (m_done.size() != m_failed.size()) {
emitSucceeded();
return;
}
- Task::Ptr next = m_queue[m_currentIndex];
-
- connect(next.get(), &Task::failed, this, &MultipleOptionsTask::subTaskFailed);
- connect(next.get(), &Task::succeeded, this, &MultipleOptionsTask::startNext);
-
- connect(next.get(), &Task::status, this, &MultipleOptionsTask::subTaskStatus);
- connect(next.get(), &Task::stepStatus, this, &MultipleOptionsTask::subTaskStatus);
-
- connect(next.get(), &Task::progress, this, &MultipleOptionsTask::subTaskProgress);
-
- qDebug() << QString("Making attemp %1 out of %2").arg(m_currentIndex + 1).arg(m_queue.size());
- setStatus(tr("Making attempt #%1 out of %2").arg(m_currentIndex + 1).arg(m_queue.size()));
- setStepStatus(next->isMultiStep() ? next->getStepStatus() : next->getStatus());
+ if (m_queue.isEmpty()) {
+ emitFailed(tr("All attempts have failed!"));
+ qWarning() << "All attempts have failed!";
+ return;
+ }
- next->start();
+ ConcurrentTask::startNext();
}
-void MultipleOptionsTask::subTaskFailed(QString const& reason)
+void MultipleOptionsTask::updateState()
{
- qDebug() << QString("Failed attempt #%1 of %2. Reason: %3").arg(m_currentIndex + 1).arg(m_queue.size()).arg(reason);
- if(m_currentIndex < m_queue.size() - 1) {
- startNext();
- return;
- }
-
- qWarning() << QString("All attempts have failed!");
- emitFailed();
+ setProgress(m_done.count(), m_total_size);
+ setStatus(tr("Attempting task %1 out of %2").arg(QString::number(m_doing.count() + m_done.count()), QString::number(m_total_size)));
}
diff --git a/launcher/tasks/MultipleOptionsTask.h b/launcher/tasks/MultipleOptionsTask.h
index 7c508b00..db7d4d9a 100644
--- a/launcher/tasks/MultipleOptionsTask.h
+++ b/launcher/tasks/MultipleOptionsTask.h
@@ -5,15 +5,13 @@
/* This task type will attempt to do run each of it's subtasks in sequence,
* until one of them succeeds. When that happens, the remaining tasks will not run.
* */
-class MultipleOptionsTask : public SequentialTask
-{
+class MultipleOptionsTask : public SequentialTask {
Q_OBJECT
-public:
- explicit MultipleOptionsTask(QObject *parent = nullptr, const QString& task_name = "");
- virtual ~MultipleOptionsTask() = default;
+ public:
+ explicit MultipleOptionsTask(QObject* parent = nullptr, const QString& task_name = "");
+ ~MultipleOptionsTask() override = default;
-private
-slots:
+ private slots:
void startNext() override;
- void subTaskFailed(const QString &msg) override;
+ void updateState() override;
};
diff --git a/launcher/tasks/SequentialTask.cpp b/launcher/tasks/SequentialTask.cpp
index f1e1a889..a34137cb 100644
--- a/launcher/tasks/SequentialTask.cpp
+++ b/launcher/tasks/SequentialTask.cpp
@@ -2,107 +2,21 @@
#include <QDebug>
-SequentialTask::SequentialTask(QObject* parent, const QString& task_name) : Task(parent), m_name(task_name), m_currentIndex(-1) {}
-
-SequentialTask::~SequentialTask()
-{
- for(auto task : m_queue){
- if(task)
- task->deleteLater();
- }
-}
-
-auto SequentialTask::getStepProgress() const -> qint64
-{
- return m_stepProgress;
-}
-
-auto SequentialTask::getStepTotalProgress() const -> qint64
-{
- return m_stepTotalProgress;
-}
-
-void SequentialTask::addTask(Task::Ptr task)
-{
- m_queue.append(task);
-}
-
-void SequentialTask::executeTask()
-{
- m_currentIndex = -1;
- startNext();
-}
-
-bool SequentialTask::abort()
-{
- if(m_currentIndex == -1 || m_currentIndex >= m_queue.size()) {
- if(m_currentIndex == -1) {
- // Don't call emitAborted() here, we want to bypass the 'is the task running' check
- emit aborted();
- emit finished();
- }
-
- m_aborted = true;
- return true;
- }
-
- bool succeeded = m_queue[m_currentIndex]->abort();
- m_aborted = succeeded;
-
- if (succeeded)
- emitAborted();
-
- return succeeded;
-}
+SequentialTask::SequentialTask(QObject* parent, QString task_name) : ConcurrentTask(parent, task_name, 1) {}
void SequentialTask::startNext()
{
- if (m_aborted)
- return;
-
- if (m_currentIndex != -1 && m_currentIndex < m_queue.size()) {
- Task::Ptr previous = m_queue.at(m_currentIndex);
- disconnect(previous.get(), 0, this, 0);
- }
-
- m_currentIndex++;
- if (m_queue.isEmpty() || m_currentIndex >= m_queue.size()) {
- emitSucceeded();
+ if (m_failed.size() > 0) {
+ emitFailed(tr("One of the tasks failed!"));
+ qWarning() << m_failed.constBegin()->get()->failReason();
return;
}
- Task::Ptr next = m_queue[m_currentIndex];
-
- connect(next.get(), SIGNAL(failed(QString)), this, SLOT(subTaskFailed(QString)));
- connect(next.get(), SIGNAL(succeeded()), this, SLOT(startNext()));
-
- connect(next.get(), SIGNAL(status(QString)), this, SLOT(subTaskStatus(QString)));
- connect(next.get(), SIGNAL(stepStatus(QString)), this, SLOT(subTaskStatus(QString)));
-
- connect(next.get(), SIGNAL(progress(qint64, qint64)), this, SLOT(subTaskProgress(qint64, qint64)));
- setStatus(tr("Executing task %1 out of %2").arg(m_currentIndex + 1).arg(m_queue.size()));
- setStepStatus(next->isMultiStep() ? next->getStepStatus() : next->getStatus());
-
- setProgress(m_currentIndex + 1, m_queue.count());
-
- next->start();
+ ConcurrentTask::startNext();
}
-void SequentialTask::subTaskFailed(const QString& msg)
+void SequentialTask::updateState()
{
- emitFailed(msg);
-}
-void SequentialTask::subTaskStatus(const QString& msg)
-{
- setStepStatus(msg);
-}
-void SequentialTask::subTaskProgress(qint64 current, qint64 total)
-{
- if (total == 0) {
- setProgress(0, 100);
- return;
- }
-
- m_stepProgress = current;
- m_stepTotalProgress = total;
+ setProgress(m_done.count(), m_total_size);
+ setStatus(tr("Executing task %1 out of %2").arg(QString::number(m_doing.count() + m_done.count()), QString::number(m_total_size)));
}
diff --git a/launcher/tasks/SequentialTask.h b/launcher/tasks/SequentialTask.h
index f5a58b1b..5eace96e 100644
--- a/launcher/tasks/SequentialTask.h
+++ b/launcher/tasks/SequentialTask.h
@@ -1,49 +1,21 @@
#pragma once
-#include "Task.h"
-#include "QObjectPtr.h"
-
-#include <QQueue>
-
-class SequentialTask : public Task
-{
+#include "ConcurrentTask.h"
+
+/** A concurrent task that only allows one concurrent task :)
+ *
+ * This should be used when there's a need to maintain a strict ordering of task executions, and
+ * the starting of a task is contingent on the success of the previous one.
+ *
+ * See MultipleOptionsTask if that's not the case.
+ */
+class SequentialTask : public ConcurrentTask {
Q_OBJECT
-public:
- explicit SequentialTask(QObject *parent = nullptr, const QString& task_name = "");
- virtual ~SequentialTask();
-
- inline auto isMultiStep() const -> bool override { return m_queue.size() > 1; };
- auto getStepProgress() const -> qint64 override;
- auto getStepTotalProgress() const -> qint64 override;
-
- inline auto getStepStatus() const -> QString override { return m_step_status; }
-
- void addTask(Task::Ptr task);
-
-public slots:
- bool abort() override;
-
-protected
-slots:
- void executeTask() override;
-
- virtual void startNext();
- virtual void subTaskFailed(const QString &msg);
- virtual void subTaskStatus(const QString &msg);
- virtual void subTaskProgress(qint64 current, qint64 total);
-
-protected:
- void setStepStatus(QString status) { m_step_status = status; emit stepStatus(status); };
-
-protected:
- QString m_name;
- QString m_step_status;
-
- QQueue<Task::Ptr > m_queue;
- int m_currentIndex;
-
- qint64 m_stepProgress = 0;
- qint64 m_stepTotalProgress = 100;
+ public:
+ explicit SequentialTask(QObject* parent = nullptr, QString task_name = "");
+ ~SequentialTask() override = default;
- bool m_aborted = false;
+ protected:
+ void startNext() override;
+ void updateState() override;
};
diff --git a/launcher/tasks/Task_test.cpp b/launcher/tasks/Task_test.cpp
index ef153a6a..b56ee8a6 100644
--- a/launcher/tasks/Task_test.cpp
+++ b/launcher/tasks/Task_test.cpp
@@ -1,5 +1,8 @@
#include <QTest>
+#include "ConcurrentTask.h"
+#include "MultipleOptionsTask.h"
+#include "SequentialTask.h"
#include "Task.h"
/* Does nothing. Only used for testing. */
@@ -9,7 +12,10 @@ class BasicTask : public Task {
friend class TaskTest;
private:
- void executeTask() override {};
+ void executeTask() override
+ {
+ emitSucceeded();
+ };
};
/* Does nothing. Only used for testing. */
@@ -60,6 +66,123 @@ class TaskTest : public QObject {
QCOMPARE(t.getProgress(), current);
QCOMPARE(t.getTotalProgress(), total);
}
+
+ void test_basicRun(){
+ BasicTask t;
+ QObject::connect(&t, &Task::finished, [&]{ QVERIFY2(t.wasSuccessful(), "Task finished but was not successful when it should have been."); });
+ t.start();
+
+ QVERIFY2(QTest::qWaitFor([&]() {
+ return t.isFinished();
+ }, 1000), "Task didn't finish as it should.");
+ }
+
+ void test_basicConcurrentRun(){
+ BasicTask t1;
+ BasicTask t2;
+ BasicTask t3;
+
+ ConcurrentTask t;
+
+ t.addTask(&t1);
+ t.addTask(&t2);
+ t.addTask(&t3);
+
+ QObject::connect(&t, &Task::finished, [&]{
+ QVERIFY2(t.wasSuccessful(), "Task finished but was not successful when it should have been.");
+ QVERIFY(t1.wasSuccessful());
+ QVERIFY(t2.wasSuccessful());
+ QVERIFY(t3.wasSuccessful());
+ });
+
+ t.start();
+ QVERIFY2(QTest::qWaitFor([&]() {
+ return t.isFinished();
+ }, 1000), "Task didn't finish as it should.");
+ }
+
+ // Tests if starting new tasks after the 6 initial ones is working
+ void test_moreConcurrentRun(){
+ BasicTask t1, t2, t3, t4, t5, t6, t7, t8, t9;
+
+ ConcurrentTask t;
+
+ t.addTask(&t1);
+ t.addTask(&t2);
+ t.addTask(&t3);
+ t.addTask(&t4);
+ t.addTask(&t5);
+ t.addTask(&t6);
+ t.addTask(&t7);
+ t.addTask(&t8);
+ t.addTask(&t9);
+
+ QObject::connect(&t, &Task::finished, [&]{
+ QVERIFY2(t.wasSuccessful(), "Task finished but was not successful when it should have been.");
+ QVERIFY(t1.wasSuccessful());
+ QVERIFY(t2.wasSuccessful());
+ QVERIFY(t3.wasSuccessful());
+ QVERIFY(t4.wasSuccessful());
+ QVERIFY(t5.wasSuccessful());
+ QVERIFY(t6.wasSuccessful());
+ QVERIFY(t7.wasSuccessful());
+ QVERIFY(t8.wasSuccessful());
+ QVERIFY(t9.wasSuccessful());
+ });
+
+ t.start();
+ QVERIFY2(QTest::qWaitFor([&]() {
+ return t.isFinished();
+ }, 1000), "Task didn't finish as it should.");
+ }
+
+ void test_basicSequentialRun(){
+ BasicTask t1;
+ BasicTask t2;
+ BasicTask t3;
+
+ SequentialTask t;
+
+ t.addTask(&t1);
+ t.addTask(&t2);
+ t.addTask(&t3);
+
+ QObject::connect(&t, &Task::finished, [&]{
+ QVERIFY2(t.wasSuccessful(), "Task finished but was not successful when it should have been.");
+ QVERIFY(t1.wasSuccessful());
+ QVERIFY(t2.wasSuccessful());
+ QVERIFY(t3.wasSuccessful());
+ });
+
+ t.start();
+ QVERIFY2(QTest::qWaitFor([&]() {
+ return t.isFinished();
+ }, 1000), "Task didn't finish as it should.");
+ }
+
+ void test_basicMultipleOptionsRun(){
+ BasicTask t1;
+ BasicTask t2;
+ BasicTask t3;
+
+ MultipleOptionsTask t;
+
+ t.addTask(&t1);
+ t.addTask(&t2);
+ t.addTask(&t3);
+
+ QObject::connect(&t, &Task::finished, [&]{
+ QVERIFY2(t.wasSuccessful(), "Task finished but was not successful when it should have been.");
+ QVERIFY(t1.wasSuccessful());
+ QVERIFY(!t2.wasSuccessful());
+ QVERIFY(!t3.wasSuccessful());
+ });
+
+ t.start();
+ QVERIFY2(QTest::qWaitFor([&]() {
+ return t.isFinished();
+ }, 1000), "Task didn't finish as it should.");
+ }
};
QTEST_GUILESS_MAIN(TaskTest)
diff --git a/launcher/ui/dialogs/AboutDialog.ui b/launcher/ui/dialogs/AboutDialog.ui
index 6eaa0c4e..e0429321 100644
--- a/launcher/ui/dialogs/AboutDialog.ui
+++ b/launcher/ui/dialogs/AboutDialog.ui
@@ -6,8 +6,8 @@
<rect>
<x>0</x>
<y>0</y>
- <width>783</width>
- <height>843</height>
+ <width>573</width>
+ <height>600</height>
</rect>
</property>
<property name="minimumSize">
diff --git a/launcher/ui/dialogs/ModDownloadDialog.cpp b/launcher/ui/dialogs/ModDownloadDialog.cpp
index e4fc3ecc..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,21 +119,22 @@ 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->currentCapabilities() & Application::SupportsFlame)
+ if (APPLICATION->capabilities() & Application::SupportsFlame)
pages.append(new FlameModPage(this, m_instance));
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/NewInstanceDialog.cpp b/launcher/ui/dialogs/NewInstanceDialog.cpp
index 35bba9be..675f8b15 100644
--- a/launcher/ui/dialogs/NewInstanceDialog.cpp
+++ b/launcher/ui/dialogs/NewInstanceDialog.cpp
@@ -157,7 +157,7 @@ QList<BasePage *> NewInstanceDialog::getPages()
pages.append(new VanillaPage(this));
pages.append(importPage);
pages.append(new AtlPage(this));
- if (APPLICATION->currentCapabilities() & Application::SupportsFlame)
+ if (APPLICATION->capabilities() & Application::SupportsFlame)
pages.append(new FlamePage(this));
pages.append(new FtbPage(this));
pages.append(new LegacyFTB::Page(this));
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/global/AccountListPage.cpp b/launcher/ui/pages/global/AccountListPage.cpp
index fcc43add..a4f4dfb9 100644
--- a/launcher/ui/pages/global/AccountListPage.cpp
+++ b/launcher/ui/pages/global/AccountListPage.cpp
@@ -96,7 +96,7 @@ AccountListPage::AccountListPage(QWidget *parent)
updateButtonStates();
// Xbox authentication won't work without a client identifier, so disable the button if it is missing
- if (~APPLICATION->currentCapabilities() & Application::SupportsMSA) {
+ if (~APPLICATION->capabilities() & Application::SupportsMSA) {
ui->actionAddMicrosoft->setVisible(false);
ui->actionAddMicrosoft->setToolTip(tr("No Microsoft Authentication client ID was set."));
}
diff --git a/launcher/ui/pages/global/MinecraftPage.cpp b/launcher/ui/pages/global/MinecraftPage.cpp
index e3ac7e7c..cc597fe0 100644
--- a/launcher/ui/pages/global/MinecraftPage.cpp
+++ b/launcher/ui/pages/global/MinecraftPage.cpp
@@ -122,6 +122,16 @@ void MinecraftPage::loadSettings()
ui->perfomanceGroupBox->setVisible(false);
#endif
+ if (!(APPLICATION->capabilities() & Application::SupportsGameMode)) {
+ ui->enableFeralGamemodeCheck->setDisabled(true);
+ ui->enableFeralGamemodeCheck->setToolTip(tr("Feral Interactive's GameMode could not be found on your system."));
+ }
+
+ if (!(APPLICATION->capabilities() & Application::SupportsMangoHud)) {
+ ui->enableMangoHud->setDisabled(true);
+ ui->enableMangoHud->setToolTip(tr("MangoHud could not be found on your system."));
+ }
+
ui->showGameTime->setChecked(s->get("ShowGameTime").toBool());
ui->showGlobalGameTime->setChecked(s->get("ShowGlobalGameTime").toBool());
ui->recordGameTime->setChecked(s->get("RecordGameTime").toBool());
diff --git a/launcher/ui/pages/instance/InstanceSettingsPage.cpp b/launcher/ui/pages/instance/InstanceSettingsPage.cpp
index f11cf992..03910745 100644
--- a/launcher/ui/pages/instance/InstanceSettingsPage.cpp
+++ b/launcher/ui/pages/instance/InstanceSettingsPage.cpp
@@ -348,9 +348,19 @@ void InstanceSettingsPage::loadSettings()
ui->enableMangoHud->setChecked(m_settings->get("EnableMangoHud").toBool());
ui->useDiscreteGpuCheck->setChecked(m_settings->get("UseDiscreteGpu").toBool());
- #if !defined(Q_OS_LINUX)
+#if !defined(Q_OS_LINUX)
ui->settingsTabs->setTabVisible(ui->settingsTabs->indexOf(ui->performancePage), false);
- #endif
+#endif
+
+ if (!(APPLICATION->capabilities() & Application::SupportsGameMode)) {
+ ui->enableFeralGamemodeCheck->setDisabled(true);
+ ui->enableFeralGamemodeCheck->setToolTip(tr("Feral Interactive's GameMode could not be found on your system."));
+ }
+
+ if (!(APPLICATION->capabilities() & Application::SupportsMangoHud)) {
+ ui->enableMangoHud->setDisabled(true);
+ ui->enableMangoHud->setToolTip(tr("MangoHud could not be found on your system."));
+ }
// Miscellanous
ui->gameTimeGroupBox->setChecked(m_settings->get("OverrideGameTime").toBool());
diff --git a/launcher/ui/pages/modplatform/ModModel.cpp b/launcher/ui/pages/modplatform/ModModel.cpp
index 94b1f099..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()
@@ -230,10 +259,11 @@ void ListModel::searchRequestFinished(QJsonDocument& doc)
void ListModel::searchRequestFailed(QString reason)
{
- if (!jobPtr->first()->m_reply) {
+ auto failed_action = jobPtr->getFailedActions().at(0);
+ if (!failed_action->m_reply) {
// Network error
QMessageBox::critical(nullptr, tr("Error"), tr("A network error occurred. Could not load mods."));
- } else if (jobPtr->first()->m_reply && jobPtr->first()->m_reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 409) {
+ } else if (failed_action->m_reply && failed_action->m_reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 409) {
// 409 Gone, notify user to update
QMessageBox::critical(nullptr, tr("Error"),
//: %1 refers to the launcher itself
@@ -255,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";
@@ -267,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) {
@@ -286,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/pages/modplatform/modrinth/ModrinthModel.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp
index 3633d575..614be434 100644
--- a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp
+++ b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp
@@ -301,10 +301,11 @@ void ModpackListModel::searchRequestFinished(QJsonDocument& doc_all)
void ModpackListModel::searchRequestFailed(QString reason)
{
- if (!jobPtr->first()->m_reply) {
+ auto failed_action = jobPtr->getFailedActions().at(0);
+ if (!failed_action->m_reply) {
// Network error
QMessageBox::critical(nullptr, tr("Error"), tr("A network error occurred. Could not load modpacks."));
- } else if (jobPtr->first()->m_reply && jobPtr->first()->m_reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 409) {
+ } else if (failed_action->m_reply && failed_action->m_reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 409) {
// 409 Gone, notify user to update
QMessageBox::critical(nullptr, tr("Error"),
//: %1 refers to the launcher itself
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;
+
+};
diff --git a/libraries/launcher/org/polymc/impl/OneSixLauncher.java b/libraries/launcher/org/polymc/impl/OneSixLauncher.java
index 250fe0f2..362ff8d6 100644
--- a/libraries/launcher/org/polymc/impl/OneSixLauncher.java
+++ b/libraries/launcher/org/polymc/impl/OneSixLauncher.java
@@ -1,53 +1,16 @@
-// SPDX-License-Identifier: GPL-3.0-only
-/*
- * PolyMC - Minecraft Launcher
- * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
+/* Copyright 2012-2021 MultiMC Contributors
*
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, version 3.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
*
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
+ * http://www.apache.org/licenses/LICENSE-2.0
*
- * Linking this library statically or dynamically with other modules is
- * making a combined work based on this library. Thus, the terms and
- * conditions of the GNU General Public License cover the whole
- * combination.
- *
- * As a special exception, the copyright holders of this library give
- * you permission to link this library with independent modules to
- * produce an executable, regardless of the license terms of these
- * independent modules, and to copy and distribute the resulting
- * executable under terms of your choice, provided that you also meet,
- * for each linked independent module, the terms and conditions of the
- * license of that module. An independent module is a module which is
- * not derived from or based on this library. If you modify this
- * library, you may extend this exception to your version of the
- * library, but you are not obliged to do so. If you do not wish to do
- * so, delete this exception statement from your version.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <https://www.gnu.org/licenses/>.
- *
- * This file incorporates work covered by the following copyright and
- * permission notice:
- *
- * Copyright 2013-2021 MultiMC Contributors
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
*/
package org.polymc.impl;
@@ -61,9 +24,6 @@ import java.applet.Applet;
import java.io.File;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
-import java.net.URL;
-import java.net.URLClassLoader;
-import java.nio.file.Paths;
import java.util.Collections;
import java.util.List;
import java.util.logging.Level;
@@ -77,7 +37,6 @@ public final class OneSixLauncher implements Launcher {
private static final Logger LOGGER = Logger.getLogger("OneSixLauncher");
// parameters, separated from ParamBucket
- private final List<String> classPath;
private final List<String> mcParams;
private final List<String> traits;
private final String appletClass;
@@ -94,8 +53,11 @@ public final class OneSixLauncher implements Launcher {
private final String serverAddress;
private final String serverPort;
+ private final ClassLoader classLoader;
+
public OneSixLauncher(Parameters params) {
- classPath = params.allSafe("classPath", Collections.<String>emptyList());
+ classLoader = ClassLoader.getSystemClassLoader();
+
mcParams = params.allSafe("param", Collections.<String>emptyList());
mainClass = params.firstSafe("mainClass", "net.minecraft.client.Minecraft");
appletClass = params.firstSafe("appletClass", "net.minecraft.client.MinecraftApplet");
@@ -142,7 +104,7 @@ public final class OneSixLauncher implements Launcher {
method.invoke(null, (Object) mcParams.toArray(new String[0]));
}
- private void legacyLaunch(ClassLoader classLoader) throws Exception {
+ private void legacyLaunch() throws Exception {
// Get the Minecraft Class and set the base folder
Class<?> minecraftClass = classLoader.loadClass(mainClass);
@@ -189,7 +151,7 @@ public final class OneSixLauncher implements Launcher {
invokeMain(minecraftClass);
}
- private void launchWithMainClass(ClassLoader classLoader) throws Exception {
+ private void launchWithMainClass() throws Exception {
// window size, title and state, onesix
// FIXME: there is no good way to maximize the minecraft window in onesix.
@@ -215,24 +177,12 @@ public final class OneSixLauncher implements Launcher {
@Override
public void launch() throws Exception {
- URL[] classPathURLs = new URL[classPath.size()];
- for (int i = 0; i < classPath.size(); i++) {
- File f = new File(classPath.get(i));
- classPathURLs[i] = f.toURI().toURL();
- }
- // Some mod loaders (Fabric) read this property to determine the classpath.
- String systemClassPath = System.getProperty("java.class.path");
- systemClassPath += File.pathSeparator + String.join(File.pathSeparator, classPath);
- System.setProperty("java.class.path", systemClassPath);
-
- ClassLoader classLoader = new URLClassLoader(classPathURLs, getClass().getClassLoader());
-
if (traits.contains("legacyLaunch") || traits.contains("alphaLaunch")) {
// legacy launch uses the applet wrapper
- legacyLaunch(classLoader);
+ legacyLaunch();
} else {
// normal launch just calls main()
- launchWithMainClass(classLoader);
+ launchWithMainClass();
}
}