diff options
Diffstat (limited to 'launcher')
53 files changed, 1430 insertions, 219 deletions
diff --git a/launcher/Application.cpp b/launcher/Application.cpp index 8447083c..3f313ee4 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -97,6 +97,7 @@ #include <QIcon> #include "InstanceList.h" +#include "MTPixmapCache.h" #include <minecraft/auth/AccountList.h> #include "icons/IconList.h" @@ -142,6 +143,8 @@ static const QLatin1String liveCheckFile("live.check"); +PixmapCache* PixmapCache::s_instance = nullptr; + namespace { void appDebugOutput(QtMsgType type, const QMessageLogContext &context, const QString &msg) { @@ -694,6 +697,9 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv) m_globalSettingsProvider->addPage<AccountListPage>(); m_globalSettingsProvider->addPage<APIPage>(); } + + PixmapCache::setInstance(new PixmapCache(this)); + qDebug() << "<> Settings loaded."; } diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index 879c3157..439feb44 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -89,6 +89,8 @@ set(CORE_SOURCES # Time MMCTime.h MMCTime.cpp + + MTPixmapCache.h ) if (UNIX AND NOT CYGWIN AND NOT APPLE) set(CORE_SOURCES @@ -689,6 +691,8 @@ SET(LAUNCHER_SOURCES ui/pages/instance/GameOptionsPage.h ui/pages/instance/VersionPage.cpp ui/pages/instance/VersionPage.h + ui/pages/instance/ManagedPackPage.cpp + ui/pages/instance/ManagedPackPage.h ui/pages/instance/TexturePackPage.h ui/pages/instance/ResourcePackPage.h ui/pages/instance/ShaderPackPage.h @@ -928,6 +932,7 @@ qt_wrap_ui(LAUNCHER_UI ui/pages/instance/OtherLogsPage.ui ui/pages/instance/InstanceSettingsPage.ui ui/pages/instance/VersionPage.ui + ui/pages/instance/ManagedPackPage.ui ui/pages/instance/WorldListPage.ui ui/pages/instance/ScreenshotsPage.ui ui/pages/modplatform/atlauncher/AtlOptionalModDialog.ui diff --git a/launcher/FileSystem.cpp b/launcher/FileSystem.cpp index 1da50e21..3e8e10a5 100644 --- a/launcher/FileSystem.cpp +++ b/launcher/FileSystem.cpp @@ -372,8 +372,6 @@ bool createShortcut(QString destination, QString target, QStringList args, QStri return true; #elif defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD) - destination += ".desktop"; - QFile f(destination); f.open(QIODevice::WriteOnly | QIODevice::Text); QTextStream stream(&f); diff --git a/launcher/InstanceImportTask.cpp b/launcher/InstanceImportTask.cpp index 5f459649..b97870da 100644 --- a/launcher/InstanceImportTask.cpp +++ b/launcher/InstanceImportTask.cpp @@ -55,11 +55,9 @@ #include <quazip/quazipdir.h> -InstanceImportTask::InstanceImportTask(const QUrl sourceUrl, QWidget* parent) -{ - m_sourceUrl = sourceUrl; - m_parent = parent; -} +InstanceImportTask::InstanceImportTask(const QUrl sourceUrl, QWidget* parent, QMap<QString, QString>&& extra_info) + : m_sourceUrl(sourceUrl), m_extra_info(extra_info), m_parent(parent) +{} bool InstanceImportTask::abort() { @@ -259,14 +257,28 @@ void InstanceImportTask::extractAborted() void InstanceImportTask::processFlame() { - auto* inst_creation_task = new FlameCreationTask(m_stagingPath, m_globalSettings, m_parent); + auto pack_id_it = m_extra_info.constFind("pack_id"); + Q_ASSERT(pack_id_it != m_extra_info.constEnd()); + auto pack_id = pack_id_it.value(); + + auto pack_version_id_it = m_extra_info.constFind("pack_version_id"); + Q_ASSERT(pack_version_id_it != m_extra_info.constEnd()); + auto pack_version_id = pack_version_id_it.value(); + + QString original_instance_id; + auto original_instance_id_it = m_extra_info.constFind("original_instance_id"); + if (original_instance_id_it != m_extra_info.constEnd()) + original_instance_id = original_instance_id_it.value(); + + auto* inst_creation_task = new FlameCreationTask(m_stagingPath, m_globalSettings, m_parent, pack_id, pack_version_id, original_instance_id); inst_creation_task->setName(*this); inst_creation_task->setIcon(m_instIcon); inst_creation_task->setGroup(m_instGroup); + inst_creation_task->setConfirmUpdate(shouldConfirmUpdate()); connect(inst_creation_task, &Task::succeeded, this, [this, inst_creation_task] { - setOverride(inst_creation_task->shouldOverride()); + setOverride(inst_creation_task->shouldOverride(), inst_creation_task->originalInstanceID()); emitSucceeded(); }); connect(inst_creation_task, &Task::failed, this, &InstanceImportTask::emitFailed); @@ -323,14 +335,29 @@ void InstanceImportTask::processMultiMC() void InstanceImportTask::processModrinth() { - auto* inst_creation_task = new ModrinthCreationTask(m_stagingPath, m_globalSettings, m_parent, m_sourceUrl.toString()); + auto pack_id_it = m_extra_info.constFind("pack_id"); + Q_ASSERT(pack_id_it != m_extra_info.constEnd()); + auto pack_id = pack_id_it.value(); + + QString pack_version_id; + auto pack_version_id_it = m_extra_info.constFind("pack_version_id"); + if (pack_version_id_it != m_extra_info.constEnd()) + pack_version_id = pack_version_id_it.value(); + + QString original_instance_id; + auto original_instance_id_it = m_extra_info.constFind("original_instance_id"); + if (original_instance_id_it != m_extra_info.constEnd()) + original_instance_id = original_instance_id_it.value(); + + auto* inst_creation_task = new ModrinthCreationTask(m_stagingPath, m_globalSettings, m_parent, pack_id, pack_version_id, original_instance_id); inst_creation_task->setName(*this); inst_creation_task->setIcon(m_instIcon); inst_creation_task->setGroup(m_instGroup); + inst_creation_task->setConfirmUpdate(shouldConfirmUpdate()); connect(inst_creation_task, &Task::succeeded, this, [this, inst_creation_task] { - setOverride(inst_creation_task->shouldOverride()); + setOverride(inst_creation_task->shouldOverride(), inst_creation_task->originalInstanceID()); emitSucceeded(); }); connect(inst_creation_task, &Task::failed, this, &InstanceImportTask::emitFailed); diff --git a/launcher/InstanceImportTask.h b/launcher/InstanceImportTask.h index ef70c819..6b8ac966 100644 --- a/launcher/InstanceImportTask.h +++ b/launcher/InstanceImportTask.h @@ -56,7 +56,7 @@ class InstanceImportTask : public InstanceTask { Q_OBJECT public: - explicit InstanceImportTask(const QUrl sourceUrl, QWidget* parent = nullptr); + explicit InstanceImportTask(const QUrl sourceUrl, QWidget* parent = nullptr, QMap<QString, QString>&& extra_info = {}); bool abort() override; const QVector<Flame::File> &getBlockedFiles() const @@ -101,6 +101,10 @@ private: /* data */ Modrinth, } m_modpackType = ModpackType::Unknown; + // Extra info we might need, that's available before, but can't be derived from + // the source URL / the resource it points to alone. + QMap<QString, QString> m_extra_info; + //FIXME: nuke QWidget* m_parent; }; diff --git a/launcher/InstanceList.cpp b/launcher/InstanceList.cpp index cebd70d7..68e3e92c 100644 --- a/launcher/InstanceList.cpp +++ b/launcher/InstanceList.cpp @@ -816,7 +816,7 @@ class InstanceStaging : public Task { void childSucceded() { unsigned sleepTime = backoff(); - if (m_parent->commitStagedInstance(m_stagingPath, m_instance_name, m_groupName, m_child->shouldOverride())) + if (m_parent->commitStagedInstance(m_stagingPath, m_instance_name, m_groupName, *m_child.get())) { emitSucceeded(); return; @@ -880,25 +880,22 @@ QString InstanceList::getStagedInstancePath() return path; } -bool InstanceList::commitStagedInstance(const QString& path, InstanceName const& instanceName, const QString& groupName, bool should_override) +bool InstanceList::commitStagedInstance(const QString& path, InstanceName const& instanceName, const QString& groupName, InstanceTask const& commiting) { QDir dir; QString instID; InstancePtr inst; + auto should_override = commiting.shouldOverride(); + if (should_override) { - // This is to avoid problems when the instance folder gets manually renamed - if ((inst = getInstanceByManagedName(instanceName.originalName()))) { - instID = QFileInfo(inst->instanceRoot()).fileName(); - } else if ((inst = getInstanceByManagedName(instanceName.modifiedName()))) { - instID = QFileInfo(inst->instanceRoot()).fileName(); - } else { - instID = FS::RemoveInvalidFilenameChars(instanceName.modifiedName(), '-'); - } + instID = commiting.originalInstanceID(); } else { instID = FS::DirNameFromString(instanceName.modifiedName(), m_instDir); } + Q_ASSERT(!instID.isEmpty()); + { WatchLock lock(m_watcher, m_instDir); QString destination = FS::PathCombine(m_instDir, instID); diff --git a/launcher/InstanceList.h b/launcher/InstanceList.h index 3673298f..edacba3c 100644 --- a/launcher/InstanceList.h +++ b/launcher/InstanceList.h @@ -133,7 +133,7 @@ public: * should_override is used when another similar instance already exists, and we want to override it * - for instance, when updating it. */ - bool commitStagedInstance(const QString& keyPath, const InstanceName& instanceName, const QString& groupName, bool should_override); + bool commitStagedInstance(const QString& keyPath, const InstanceName& instanceName, const QString& groupName, const InstanceTask&); /** * Destroy a previously created staging area given by @keyPath - used when creation fails. diff --git a/launcher/InstancePageProvider.h b/launcher/InstancePageProvider.h index bf29377d..5d8beca9 100644 --- a/launcher/InstancePageProvider.h +++ b/launcher/InstancePageProvider.h @@ -5,6 +5,7 @@ #include "ui/pages/BasePageProvider.h" #include "ui/pages/instance/LogPage.h" #include "ui/pages/instance/VersionPage.h" +#include "ui/pages/instance/ManagedPackPage.h" #include "ui/pages/instance/ModFolderPage.h" #include "ui/pages/instance/ResourcePackPage.h" #include "ui/pages/instance/TexturePackPage.h" @@ -33,6 +34,7 @@ public: values.append(new LogPage(inst)); std::shared_ptr<MinecraftInstance> onesix = std::dynamic_pointer_cast<MinecraftInstance>(inst); values.append(new VersionPage(onesix.get())); + values.append(ManagedPackPage::createPage(onesix.get())); auto modsPage = new ModFolderPage(onesix.get(), onesix->loaderModList()); modsPage->setFilter("%1 (*.zip *.jar *.litemod)"); values.append(modsPage); diff --git a/launcher/InstanceTask.cpp b/launcher/InstanceTask.cpp index 55a44fd3..06682782 100644 --- a/launcher/InstanceTask.cpp +++ b/launcher/InstanceTask.cpp @@ -18,6 +18,29 @@ InstanceNameChange askForChangingInstanceName(QWidget* parent, const QString& ol return InstanceNameChange::ShouldKeep; } +ShouldUpdate askIfShouldUpdate(QWidget *parent, QString original_version_name) +{ + auto info = CustomMessageBox::selectable( + parent, QObject::tr("Similar modpack was found!"), + QObject::tr("One or more of your instances are from this same modpack%1. Do you want to create a " + "separate instance, or update the existing one?\n\nNOTE: Make sure you made a backup of your important instance data before " + "updating, as worlds can be corrupted and some configuration may be lost (due to pack overrides).") + .arg(original_version_name), + QMessageBox::Information, QMessageBox::Ok | QMessageBox::Reset | QMessageBox::Abort); + info->setButtonText(QMessageBox::Ok, QObject::tr("Update existing instance")); + info->setButtonText(QMessageBox::Abort, QObject::tr("Create new instance")); + info->setButtonText(QMessageBox::Reset, QObject::tr("Cancel")); + + info->exec(); + + if (info->clickedButton() == info->button(QMessageBox::Ok)) + return ShouldUpdate::Update; + if (info->clickedButton() == info->button(QMessageBox::Abort)) + return ShouldUpdate::SkipUpdating; + return ShouldUpdate::Cancel; + +} + QString InstanceName::name() const { if (!m_modified_name.isEmpty()) diff --git a/launcher/InstanceTask.h b/launcher/InstanceTask.h index e35533fc..7c02160a 100644 --- a/launcher/InstanceTask.h +++ b/launcher/InstanceTask.h @@ -6,6 +6,8 @@ /* Helpers */ enum class InstanceNameChange { ShouldChange, ShouldKeep }; [[nodiscard]] InstanceNameChange askForChangingInstanceName(QWidget* parent, const QString& old_name, const QString& new_name); +enum class ShouldUpdate { Update, SkipUpdating, Cancel }; +[[nodiscard]] ShouldUpdate askIfShouldUpdate(QWidget* parent, QString original_version_name); struct InstanceName { public: @@ -42,10 +44,20 @@ class InstanceTask : public Task, public InstanceName { void setGroup(const QString& group) { m_instGroup = group; } QString group() const { return m_instGroup; } + [[nodiscard]] bool shouldConfirmUpdate() const { return m_confirm_update; } + void setConfirmUpdate(bool confirm) { m_confirm_update = confirm; } + bool shouldOverride() const { return m_override_existing; } + [[nodiscard]] QString originalInstanceID() const { return m_original_instance_id; }; + protected: - void setOverride(bool override) { m_override_existing = override; } + void setOverride(bool override, QString instance_id_to_override = {}) + { + m_override_existing = override; + if (!instance_id_to_override.isEmpty()) + m_original_instance_id = instance_id_to_override; + } protected: /* data */ SettingsObjectPtr m_globalSettings; @@ -54,4 +66,7 @@ class InstanceTask : public Task, public InstanceName { QString m_stagingPath; bool m_override_existing = false; + bool m_confirm_update = true; + + QString m_original_instance_id; }; diff --git a/launcher/MTPixmapCache.h b/launcher/MTPixmapCache.h new file mode 100644 index 00000000..57847a0e --- /dev/null +++ b/launcher/MTPixmapCache.h @@ -0,0 +1,95 @@ +#pragma once + +#include <QCoreApplication> +#include <QPixmapCache> +#include <QThread> + +#define GET_TYPE() \ + Qt::ConnectionType type; \ + if (QThread::currentThread() != QCoreApplication::instance()->thread()) \ + type = Qt::BlockingQueuedConnection; \ + else \ + type = Qt::DirectConnection; + +#define DEFINE_FUNC_NO_PARAM(NAME, RET_TYPE) \ + static RET_TYPE NAME() \ + { \ + RET_TYPE ret; \ + GET_TYPE() \ + QMetaObject::invokeMethod(s_instance, "_" #NAME, type, Q_RETURN_ARG(RET_TYPE, ret)); \ + return ret; \ + } +#define DEFINE_FUNC_ONE_PARAM(NAME, RET_TYPE, PARAM_1_TYPE) \ + static RET_TYPE NAME(PARAM_1_TYPE p1) \ + { \ + RET_TYPE ret; \ + GET_TYPE() \ + QMetaObject::invokeMethod(s_instance, "_" #NAME, type, Q_RETURN_ARG(RET_TYPE, ret), Q_ARG(PARAM_1_TYPE, p1)); \ + return ret; \ + } +#define DEFINE_FUNC_TWO_PARAM(NAME, RET_TYPE, PARAM_1_TYPE, PARAM_2_TYPE) \ + static RET_TYPE NAME(PARAM_1_TYPE p1, PARAM_2_TYPE p2) \ + { \ + RET_TYPE ret; \ + GET_TYPE() \ + QMetaObject::invokeMethod(s_instance, "_" #NAME, type, Q_RETURN_ARG(RET_TYPE, ret), Q_ARG(PARAM_1_TYPE, p1), \ + Q_ARG(PARAM_2_TYPE, p2)); \ + return ret; \ + } + +/** A wrapper around QPixmapCache with thread affinity with the main thread. + */ +class PixmapCache final : public QObject { + Q_OBJECT + + public: + PixmapCache(QObject* parent) : QObject(parent) {} + ~PixmapCache() override = default; + + static PixmapCache& instance() { return *s_instance; } + static void setInstance(PixmapCache* i) { s_instance = i; } + + public: + DEFINE_FUNC_NO_PARAM(cacheLimit, int) + DEFINE_FUNC_NO_PARAM(clear, bool) + DEFINE_FUNC_TWO_PARAM(find, bool, const QString&, QPixmap*) + DEFINE_FUNC_TWO_PARAM(find, bool, const QPixmapCache::Key&, QPixmap*) + DEFINE_FUNC_TWO_PARAM(insert, bool, const QString&, const QPixmap&) + DEFINE_FUNC_ONE_PARAM(insert, QPixmapCache::Key, const QPixmap&) + DEFINE_FUNC_ONE_PARAM(remove, bool, const QString&) + DEFINE_FUNC_ONE_PARAM(remove, bool, const QPixmapCache::Key&) + DEFINE_FUNC_TWO_PARAM(replace, bool, const QPixmapCache::Key&, const QPixmap&) + DEFINE_FUNC_ONE_PARAM(setCacheLimit, bool, int) + + // NOTE: Every function returns something non-void to simplify the macros. + private slots: + int _cacheLimit() { return QPixmapCache::cacheLimit(); } + bool _clear() + { + QPixmapCache::clear(); + return true; + } + bool _find(const QString& key, QPixmap* pixmap) { return QPixmapCache::find(key, pixmap); } + bool _find(const QPixmapCache::Key& key, QPixmap* pixmap) { return QPixmapCache::find(key, pixmap); } + bool _insert(const QString& key, const QPixmap& pixmap) { return QPixmapCache::insert(key, pixmap); } + QPixmapCache::Key _insert(const QPixmap& pixmap) { return QPixmapCache::insert(pixmap); } + bool _remove(const QString& key) + { + QPixmapCache::remove(key); + return true; + } + bool _remove(const QPixmapCache::Key& key) + { + QPixmapCache::remove(key); + return true; + } + bool _replace(const QPixmapCache::Key& key, const QPixmap& pixmap) { return QPixmapCache::replace(key, pixmap); } + bool _setCacheLimit(int n) + { + QPixmapCache::setCacheLimit(n); + return true; + } + + private: + static PixmapCache* s_instance; +}; diff --git a/launcher/minecraft/mod/ResourcePack.cpp b/launcher/minecraft/mod/ResourcePack.cpp index 4a9ad21b..3a2fd771 100644 --- a/launcher/minecraft/mod/ResourcePack.cpp +++ b/launcher/minecraft/mod/ResourcePack.cpp @@ -1,9 +1,11 @@ #include "ResourcePack.h" +#include <QCoreApplication> #include <QDebug> #include <QMap> #include <QRegularExpression> +#include "MTPixmapCache.h" #include "Version.h" #include "minecraft/mod/tasks/LocalResourcePackParseTask.h" @@ -43,16 +45,22 @@ void ResourcePack::setImage(QImage new_image) Q_ASSERT(!new_image.isNull()); if (m_pack_image_cache_key.key.isValid()) - QPixmapCache::remove(m_pack_image_cache_key.key); + PixmapCache::instance().remove(m_pack_image_cache_key.key); - m_pack_image_cache_key.key = QPixmapCache::insert(QPixmap::fromImage(new_image)); + m_pack_image_cache_key.key = PixmapCache::instance().insert(QPixmap::fromImage(new_image)); m_pack_image_cache_key.was_ever_used = true; + + // This can happen if the pixmap is too big to fit in the cache :c + if (!m_pack_image_cache_key.key.isValid()) { + qWarning() << "Could not insert a image cache entry! Ignoring it."; + m_pack_image_cache_key.was_ever_used = false; + } } QPixmap ResourcePack::image(QSize size) { QPixmap cached_image; - if (QPixmapCache::find(m_pack_image_cache_key.key, &cached_image)) { + if (PixmapCache::instance().find(m_pack_image_cache_key.key, &cached_image)) { if (size.isNull()) return cached_image; return cached_image.scaled(size); diff --git a/launcher/modplatform/flame/FlameInstanceCreationTask.cpp b/launcher/modplatform/flame/FlameInstanceCreationTask.cpp index f9258f24..729268d7 100644 --- a/launcher/modplatform/flame/FlameInstanceCreationTask.cpp +++ b/launcher/modplatform/flame/FlameInstanceCreationTask.cpp @@ -81,13 +81,19 @@ bool FlameCreationTask::updateInstance() auto instance_list = APPLICATION->instances(); // FIXME: How to handle situations when there's more than one install already for a given modpack? - auto inst = instance_list->getInstanceByManagedName(originalName()); + InstancePtr inst; + if (auto original_id = originalInstanceID(); !original_id.isEmpty()) { + inst = instance_list->getInstanceById(original_id); + Q_ASSERT(inst); + } else { + inst = instance_list->getInstanceByManagedName(originalName()); - if (!inst) { - inst = instance_list->getInstanceById(originalName()); + if (!inst) { + inst = instance_list->getInstanceById(originalName()); - if (!inst) - return false; + if (!inst) + return false; + } } QString index_path(FS::PathCombine(m_stagingPath, "manifest.json")); @@ -102,25 +108,14 @@ bool FlameCreationTask::updateInstance() auto version_id = inst->getManagedPackVersionName(); auto version_str = !version_id.isEmpty() ? tr(" (version %1)").arg(version_id) : ""; - auto info = CustomMessageBox::selectable( - m_parent, tr("Similar modpack was found!"), - tr("One or more of your instances are from this same modpack%1. Do you want to create a " - "separate instance, or update the existing one?\n\nNOTE: Make sure you made a backup of your important instance data before " - "updating, as worlds can be corrupted and some configuration may be lost (due to pack overrides).") - .arg(version_str), - QMessageBox::Information, QMessageBox::Ok | QMessageBox::Reset | QMessageBox::Abort); - info->setButtonText(QMessageBox::Ok, tr("Update existing instance")); - info->setButtonText(QMessageBox::Abort, tr("Create new instance")); - info->setButtonText(QMessageBox::Reset, tr("Cancel")); - - info->exec(); - - if (info->clickedButton() == info->button(QMessageBox::Abort)) - return false; - - if (info->clickedButton() == info->button(QMessageBox::Reset)) { - m_abort = true; - return false; + if (shouldConfirmUpdate()) { + auto should_update = askIfShouldUpdate(m_parent, version_str); + if (should_update == ShouldUpdate::SkipUpdating) + return false; + if (should_update == ShouldUpdate::Cancel) { + m_abort = true; + return false; + } } QDir old_inst_dir(inst->instanceRoot()); @@ -244,7 +239,7 @@ bool FlameCreationTask::updateInstance() } } - setOverride(true); + setOverride(true, inst->id()); qDebug() << "Will override instance!"; m_instance = inst; @@ -366,7 +361,7 @@ bool FlameCreationTask::createInstance() FS::deletePath(jarmodsPath); } - instance.setManagedPack("flame", {}, m_pack.name, {}, m_pack.version); + instance.setManagedPack("flame", m_managed_id, m_pack.name, m_managed_version_id, m_pack.version); instance.setName(name()); m_mod_id_resolver = new Flame::FileResolvingTask(APPLICATION->network(), m_pack); @@ -390,14 +385,6 @@ bool FlameCreationTask::createInstance() setAbortable(false); auto inst = m_instance.value(); - // Only change the name if it didn't use a custom name, so that the previous custom name - // is preserved, but if we're using the original one, we update the version string. - // NOTE: This needs to come before the copyManagedPack call! - if (inst->name().contains(inst->getManagedPackVersionName())) { - if (askForChangingInstanceName(m_parent, inst->name(), instance.name()) == InstanceNameChange::ShouldChange) - inst->setName(instance.name()); - } - inst->copyManagedPack(instance); } @@ -419,6 +406,7 @@ void FlameCreationTask::idResolverSucceeded(QEventLoop& loop) blocked_mod.hash = result.hash; blocked_mod.matched = false; blocked_mod.localPath = ""; + blocked_mod.targetFolder = result.targetFolder; blocked_mods.append(blocked_mod); @@ -464,7 +452,7 @@ void FlameCreationTask::copyBlockedMods(QList<BlockedMod> const& blocked_mods) continue; } - auto dest_path = FS::PathCombine(m_stagingPath, "minecraft", "mods", mod.name); + auto dest_path = FS::PathCombine(m_stagingPath, "minecraft", mod.targetFolder, mod.name); setStatus(tr("Copying Blocked Mods (%1 out of %2 are done)").arg(QString::number(i), QString::number(total))); diff --git a/launcher/modplatform/flame/FlameInstanceCreationTask.h b/launcher/modplatform/flame/FlameInstanceCreationTask.h index 5d227ee5..3a1c729f 100644 --- a/launcher/modplatform/flame/FlameInstanceCreationTask.h +++ b/launcher/modplatform/flame/FlameInstanceCreationTask.h @@ -51,11 +51,21 @@ class FlameCreationTask final : public InstanceCreationTask { Q_OBJECT public: - FlameCreationTask(const QString& staging_path, SettingsObjectPtr global_settings, QWidget* parent) - : InstanceCreationTask(), m_parent(parent) + FlameCreationTask(const QString& staging_path, + SettingsObjectPtr global_settings, + QWidget* parent, + QString id, + QString version_id, + QString original_instance_id = {}) + : InstanceCreationTask() + , m_parent(parent) + , m_managed_id(std::move(id)) + , m_managed_version_id(std::move(version_id)) { setStagingPath(staging_path); setParentSettings(global_settings); + + m_original_instance_id = std::move(original_instance_id); } bool abort() override; @@ -78,5 +88,7 @@ class FlameCreationTask final : public InstanceCreationTask { NetJob* m_process_update_file_info_job = nullptr; NetJob::Ptr m_files_job = nullptr; + QString m_managed_id, m_managed_version_id; + std::optional<InstancePtr> m_instance; }; diff --git a/launcher/modplatform/modpacksch/FTBPackInstallTask.cpp b/launcher/modplatform/modpacksch/FTBPackInstallTask.cpp index 48caa938..2979663d 100644 --- a/launcher/modplatform/modpacksch/FTBPackInstallTask.cpp +++ b/launcher/modplatform/modpacksch/FTBPackInstallTask.cpp @@ -195,6 +195,7 @@ void PackInstallTask::onResolveModsSucceeded() blocked_mod.hash = results_file.hash; blocked_mod.matched = false; blocked_mod.localPath = ""; + blocked_mod.targetFolder = results_file.targetFolder; m_blocked_mods.append(blocked_mod); @@ -366,7 +367,7 @@ void PackInstallTask::copyBlockedMods() continue; } - auto dest_path = FS::PathCombine(m_stagingPath, ".minecraft", "mods", mod.name); + auto dest_path = FS::PathCombine(m_stagingPath, ".minecraft", mod.targetFolder, mod.name); setStatus(tr("Copying Blocked Mods (%1 out of %2 are done)").arg(QString::number(i), QString::number(total))); diff --git a/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.cpp b/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.cpp index ddeea224..1c0e8979 100644 --- a/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.cpp +++ b/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.cpp @@ -33,13 +33,19 @@ bool ModrinthCreationTask::updateInstance() auto instance_list = APPLICATION->instances(); // FIXME: How to handle situations when there's more than one install already for a given modpack? - auto inst = instance_list->getInstanceByManagedName(originalName()); + InstancePtr inst; + if (auto original_id = originalInstanceID(); !original_id.isEmpty()) { + inst = instance_list->getInstanceById(original_id); + Q_ASSERT(inst); + } else { + inst = instance_list->getInstanceByManagedName(originalName()); - if (!inst) { - inst = instance_list->getInstanceById(originalName()); + if (!inst) { + inst = instance_list->getInstanceById(originalName()); - if (!inst) - return false; + if (!inst) + return false; + } } QString index_path = FS::PathCombine(m_stagingPath, "modrinth.index.json"); @@ -49,25 +55,14 @@ bool ModrinthCreationTask::updateInstance() auto version_name = inst->getManagedPackVersionName(); auto version_str = !version_name.isEmpty() ? tr(" (version %1)").arg(version_name) : ""; - auto info = CustomMessageBox::selectable( - m_parent, tr("Similar modpack was found!"), - tr("One or more of your instances are from this same modpack%1. Do you want to create a " - "separate instance, or update the existing one?\n\nNOTE: Make sure you made a backup of your important instance data before " - "updating, as worlds can be corrupted and some configuration may be lost (due to pack overrides).") - .arg(version_str), - QMessageBox::Information, QMessageBox::Ok | QMessageBox::Reset | QMessageBox::Abort); - info->setButtonText(QMessageBox::Ok, tr("Create new instance")); - info->setButtonText(QMessageBox::Abort, tr("Update existing instance")); - info->setButtonText(QMessageBox::Reset, tr("Cancel")); - - info->exec(); - - if (info->clickedButton() == info->button(QMessageBox::Ok)) - return false; - - if (info->clickedButton() == info->button(QMessageBox::Reset)) { - m_abort = true; - return false; + if (shouldConfirmUpdate()) { + auto should_update = askIfShouldUpdate(m_parent, version_str); + if (should_update == ShouldUpdate::SkipUpdating) + return false; + if (should_update == ShouldUpdate::Cancel) { + m_abort = true; + return false; + } } // Remove repeated files, we don't need to download them! @@ -149,7 +144,7 @@ bool ModrinthCreationTask::updateInstance() } - setOverride(true); + setOverride(true, inst->id()); qDebug() << "Will override instance!"; m_instance = inst; @@ -222,7 +217,7 @@ bool ModrinthCreationTask::createInstance() instance.setIconKey("modrinth"); } - instance.setManagedPack("modrinth", getManagedPackID(), m_managed_name, m_managed_version_id, version()); + instance.setManagedPack("modrinth", m_managed_id, m_managed_name, m_managed_version_id, version()); instance.setName(name()); instance.saveNow(); @@ -295,7 +290,8 @@ bool ModrinthCreationTask::parseManifest(const QString& index_path, std::vector< } if (set_managed_info) { - m_managed_version_id = Json::ensureString(obj, "versionId", {}, "Managed ID"); + if (m_managed_version_id.isEmpty()) + m_managed_version_id = Json::ensureString(obj, "versionId", {}, "Managed ID"); m_managed_name = Json::ensureString(obj, "name", {}, "Managed Name"); } @@ -395,13 +391,3 @@ bool ModrinthCreationTask::parseManifest(const QString& index_path, std::vector< return true; } - -QString ModrinthCreationTask::getManagedPackID() const -{ - if (!m_source_url.isEmpty()) { - QRegularExpression regex(R"(data\/(.*)\/versions)"); - return regex.match(m_source_url).captured(1); - } - - return {}; -} diff --git a/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.h b/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.h index e459aadf..122fc5ce 100644 --- a/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.h +++ b/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.h @@ -14,11 +14,21 @@ class ModrinthCreationTask final : public InstanceCreationTask { Q_OBJECT public: - ModrinthCreationTask(QString staging_path, SettingsObjectPtr global_settings, QWidget* parent, QString source_url = {}) - : InstanceCreationTask(), m_parent(parent), m_source_url(std::move(source_url)) + ModrinthCreationTask(QString staging_path, + SettingsObjectPtr global_settings, + QWidget* parent, + QString id, + QString version_id = {}, + QString original_instance_id = {}) + : InstanceCreationTask() + , m_parent(parent) + , m_managed_id(std::move(id)) + , m_managed_version_id(std::move(version_id)) { setStagingPath(staging_path); setParentSettings(global_settings); + + m_original_instance_id = std::move(original_instance_id); } bool abort() override; @@ -28,14 +38,12 @@ class ModrinthCreationTask final : public InstanceCreationTask { private: bool parseManifest(const QString&, std::vector<Modrinth::File>&, bool set_managed_info = true, bool show_optional_dialog = true); - QString getManagedPackID() const; private: QWidget* m_parent = nullptr; QString minecraftVersion, fabricVersion, quiltVersion, forgeVersion; QString m_managed_id, m_managed_version_id, m_managed_name; - QString m_source_url; std::vector<Modrinth::File> m_files; NetJob::Ptr m_files_job; diff --git a/launcher/modplatform/modrinth/ModrinthPackManifest.cpp b/launcher/modplatform/modrinth/ModrinthPackManifest.cpp index 96f54067..4dca786f 100644 --- a/launcher/modplatform/modrinth/ModrinthPackManifest.cpp +++ b/launcher/modplatform/modrinth/ModrinthPackManifest.cpp @@ -128,6 +128,7 @@ auto loadIndexedVersion(QJsonObject &obj) -> ModpackVersion file.name = Json::requireString(obj, "name"); file.version = Json::requireString(obj, "version_number"); + file.changelog = Json::ensureString(obj, "changelog"); file.id = Json::requireString(obj, "id"); file.project_id = Json::requireString(obj, "project_id"); diff --git a/launcher/modplatform/modrinth/ModrinthPackManifest.h b/launcher/modplatform/modrinth/ModrinthPackManifest.h index 035dc62e..2973dfba 100644 --- a/launcher/modplatform/modrinth/ModrinthPackManifest.h +++ b/launcher/modplatform/modrinth/ModrinthPackManifest.h @@ -80,6 +80,7 @@ struct ModpackExtra { struct ModpackVersion { QString name; QString version; + QString changelog; QString id; QString project_id; diff --git a/launcher/resources/breeze_dark/breeze_dark.qrc b/launcher/resources/breeze_dark/breeze_dark.qrc index 79707828..320ca817 100644 --- a/launcher/resources/breeze_dark/breeze_dark.qrc +++ b/launcher/resources/breeze_dark/breeze_dark.qrc @@ -19,6 +19,7 @@ <file>scalable/loadermods.svg</file> <file>scalable/log.svg</file> <file>scalable/minecraft.svg</file> + <file>scalable/matrix.svg</file> <file>scalable/new.svg</file> <file>scalable/news.svg</file> <file>scalable/notes.svg</file> diff --git a/launcher/resources/breeze_dark/scalable/discord.svg b/launcher/resources/breeze_dark/scalable/discord.svg index 22ee27ba..2e6d8899 100644 --- a/launcher/resources/breeze_dark/scalable/discord.svg +++ b/launcher/resources/breeze_dark/scalable/discord.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 127.14 96.36"><defs><style>.cls-1{fill:#fff;}</style></defs><g id="图层_2" data-name="图层 2"><g id="Discord_Logos" data-name="Discord Logos"><g id="Discord_Logo_-_Large_-_White" data-name="Discord Logo - Large - White"><path class="cls-1" d="M107.7,8.07A105.15,105.15,0,0,0,81.47,0a72.06,72.06,0,0,0-3.36,6.83A97.68,97.68,0,0,0,49,6.83,72.37,72.37,0,0,0,45.64,0,105.89,105.89,0,0,0,19.39,8.09C2.79,32.65-1.71,56.6.54,80.21h0A105.73,105.73,0,0,0,32.71,96.36,77.7,77.7,0,0,0,39.6,85.25a68.42,68.42,0,0,1-10.85-5.18c.91-.66,1.8-1.34,2.66-2a75.57,75.57,0,0,0,64.32,0c.87.71,1.76,1.39,2.66,2a68.68,68.68,0,0,1-10.87,5.19,77,77,0,0,0,6.89,11.1A105.25,105.25,0,0,0,126.6,80.22h0C129.24,52.84,122.09,29.11,107.7,8.07ZM42.45,65.69C36.18,65.69,31,60,31,53s5-12.74,11.43-12.74S54,46,53.89,53,48.84,65.69,42.45,65.69Zm42.24,0C78.41,65.69,73.25,60,73.25,53s5-12.74,11.44-12.74S96.23,46,96.12,53,91.08,65.69,84.69,65.69Z"/></g></g></g></svg>
\ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 127.14 96.36"><defs><style>.cls-1{fill:#eff0f1;}</style></defs><g id="图层_2" data-name="图层 2"><g id="Discord_Logos" data-name="Discord Logos"><g id="Discord_Logo_-_Large_-_White" data-name="Discord Logo - Large - White"><path class="cls-1" d="M107.7,8.07A105.15,105.15,0,0,0,81.47,0a72.06,72.06,0,0,0-3.36,6.83A97.68,97.68,0,0,0,49,6.83,72.37,72.37,0,0,0,45.64,0,105.89,105.89,0,0,0,19.39,8.09C2.79,32.65-1.71,56.6.54,80.21h0A105.73,105.73,0,0,0,32.71,96.36,77.7,77.7,0,0,0,39.6,85.25a68.42,68.42,0,0,1-10.85-5.18c.91-.66,1.8-1.34,2.66-2a75.57,75.57,0,0,0,64.32,0c.87.71,1.76,1.39,2.66,2a68.68,68.68,0,0,1-10.87,5.19,77,77,0,0,0,6.89,11.1A105.25,105.25,0,0,0,126.6,80.22h0C129.24,52.84,122.09,29.11,107.7,8.07ZM42.45,65.69C36.18,65.69,31,60,31,53s5-12.74,11.43-12.74S54,46,53.89,53,48.84,65.69,42.45,65.69Zm42.24,0C78.41,65.69,73.25,60,73.25,53s5-12.74,11.44-12.74S96.23,46,96.12,53,91.08,65.69,84.69,65.69Z"/></g></g></g></svg>
\ No newline at end of file diff --git a/launcher/resources/breeze_dark/scalable/new.svg b/launcher/resources/breeze_dark/scalable/new.svg index 9ee910e7..31601727 100644 --- a/launcher/resources/breeze_dark/scalable/new.svg +++ b/launcher/resources/breeze_dark/scalable/new.svg @@ -1,18 +1,13 @@ -<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"> - <defs - id="defs3051"> - <style - type="text/css" - id="current-color-scheme"> +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> + <defs id="defs3051"> + <style type="text/css" id="current-color-scheme"> .ColorScheme-Text { color:#eff0f1; } </style> </defs> - <path - style="fill:currentColor;fill-opacity:1;stroke:none" - d="M 4 4 L 4 28 L 17 28 L 17 27 L 5 27 L 5 14 L 10 14 L 13 11 L 27 11 L 27 17 L 28 17 L 28 7 L 18 7 L 15 4 L 4 4 z M 22 17 L 22 22 L 17 22 L 17 23 L 22 23 L 22 28 L 23 28 L 23 23 L 28 23 L 28 22 L 23 22 L 23 17 L 22 17 z " - id="path99" + <path style="fill:currentColor;fill-opacity:1;stroke:none" + d="M 3 2 L 3 14 L 8 14 L 8 13 L 4 13 L 4 3 L 9 3 L 9 6 L 12 6 L 12 9 L 13 9 L 13 6 L 13 5 L 10 2 L 9 2 L 3 2 z M 10 9 L 10 11 L 8 11 L 8 12 L 10 12 L 10 14 L 11 14 L 11 12 L 13 12 L 13 11 L 11 11 L 11 9 L 10 9 z " class="ColorScheme-Text" /> </svg> diff --git a/launcher/resources/breeze_light/breeze_light.qrc b/launcher/resources/breeze_light/breeze_light.qrc index ae8dbf3b..e88cd9a0 100644 --- a/launcher/resources/breeze_light/breeze_light.qrc +++ b/launcher/resources/breeze_light/breeze_light.qrc @@ -19,6 +19,7 @@ <file>scalable/loadermods.svg</file> <file>scalable/log.svg</file> <file>scalable/minecraft.svg</file> + <file>scalable/matrix.svg</file> <file>scalable/new.svg</file> <file>scalable/news.svg</file> <file>scalable/notes.svg</file> diff --git a/launcher/resources/breeze_light/scalable/centralmods.svg b/launcher/resources/breeze_light/scalable/centralmods.svg index 4035e51c..174206c4 100644 --- a/launcher/resources/breeze_light/scalable/centralmods.svg +++ b/launcher/resources/breeze_light/scalable/centralmods.svg @@ -1 +1 @@ -<svg width="8" height="8" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M3 3.5v.25h-.5V6h3V3.75H5V3.5h-.75v.25h-.5V3.5H3Zm-.25.5h2.5v1.75h-2.5V4Z" fill="#EFF0F1"/><path d="M1 1v6h6l-.25-.25h-5.5V3.5H2.5l.75-.75h3.5v4L7 7V1.75H4.5L3.75 1H1Z" fill="#EFF0F1"/></svg>
\ No newline at end of file +<svg width="8" height="8" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M3 3.5v.25h-.5V6h3V3.75H5V3.5h-.75v.25h-.5V3.5H3Zm-.25.5h2.5v1.75h-2.5V4Z" fill="#232629"/><path d="M1 1v6h6l-.25-.25h-5.5V3.5H2.5l.75-.75h3.5v4L7 7V1.75H4.5L3.75 1H1Z" fill="#232629"/></svg>
\ No newline at end of file diff --git a/launcher/resources/breeze_light/scalable/coremods.svg b/launcher/resources/breeze_light/scalable/coremods.svg index ec4ecea8..e4615cfa 100644 --- a/launcher/resources/breeze_light/scalable/coremods.svg +++ b/launcher/resources/breeze_light/scalable/coremods.svg @@ -1 +1 @@ -<svg width="8" height="8" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M3.167 2.46v.208H2.75v1.875h2.5V2.668h-.417V2.46h-.625v.208h-.416V2.46h-.625Zm-.209.417h2.084v1.458H2.958V2.877Z" fill="#EFF0F1"/><path d="M1.5 1v6h5V1h-5Zm1.5.5h2a1 1 0 0 1 1 1V3a.5.5 0 1 0 0 1v1l-.5.5h-3L2 5V4a.5.5 0 1 0 0-1v-.5a1 1 0 0 1 1-1ZM2 6h2v.5H2V6Zm3 0h1v.5H5V6Z" fill="#EFF0F1"/></svg>
\ No newline at end of file +<svg width="8" height="8" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M3.167 2.46v.208H2.75v1.875h2.5V2.668h-.417V2.46h-.625v.208h-.416V2.46h-.625Zm-.209.417h2.084v1.458H2.958V2.877Z" fill="#232629"/><path d="M1.5 1v6h5V1h-5Zm1.5.5h2a1 1 0 0 1 1 1V3a.5.5 0 1 0 0 1v1l-.5.5h-3L2 5V4a.5.5 0 1 0 0-1v-.5a1 1 0 0 1 1-1ZM2 6h2v.5H2V6Zm3 0h1v.5H5V6Z" fill="#232629"/></svg>
\ No newline at end of file diff --git a/launcher/resources/breeze_light/scalable/discord.svg b/launcher/resources/breeze_light/scalable/discord.svg index 22ee27ba..136239f7 100644 --- a/launcher/resources/breeze_light/scalable/discord.svg +++ b/launcher/resources/breeze_light/scalable/discord.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 127.14 96.36"><defs><style>.cls-1{fill:#fff;}</style></defs><g id="图层_2" data-name="图层 2"><g id="Discord_Logos" data-name="Discord Logos"><g id="Discord_Logo_-_Large_-_White" data-name="Discord Logo - Large - White"><path class="cls-1" d="M107.7,8.07A105.15,105.15,0,0,0,81.47,0a72.06,72.06,0,0,0-3.36,6.83A97.68,97.68,0,0,0,49,6.83,72.37,72.37,0,0,0,45.64,0,105.89,105.89,0,0,0,19.39,8.09C2.79,32.65-1.71,56.6.54,80.21h0A105.73,105.73,0,0,0,32.71,96.36,77.7,77.7,0,0,0,39.6,85.25a68.42,68.42,0,0,1-10.85-5.18c.91-.66,1.8-1.34,2.66-2a75.57,75.57,0,0,0,64.32,0c.87.71,1.76,1.39,2.66,2a68.68,68.68,0,0,1-10.87,5.19,77,77,0,0,0,6.89,11.1A105.25,105.25,0,0,0,126.6,80.22h0C129.24,52.84,122.09,29.11,107.7,8.07ZM42.45,65.69C36.18,65.69,31,60,31,53s5-12.74,11.43-12.74S54,46,53.89,53,48.84,65.69,42.45,65.69Zm42.24,0C78.41,65.69,73.25,60,73.25,53s5-12.74,11.44-12.74S96.23,46,96.12,53,91.08,65.69,84.69,65.69Z"/></g></g></g></svg>
\ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 127.14 96.36"><defs><style>.cls-1{fill:#232629;}</style></defs><g id="图层_2" data-name="图层 2"><g id="Discord_Logos" data-name="Discord Logos"><g id="Discord_Logo_-_Large_-_White" data-name="Discord Logo - Large - White"><path class="cls-1" d="M107.7,8.07A105.15,105.15,0,0,0,81.47,0a72.06,72.06,0,0,0-3.36,6.83A97.68,97.68,0,0,0,49,6.83,72.37,72.37,0,0,0,45.64,0,105.89,105.89,0,0,0,19.39,8.09C2.79,32.65-1.71,56.6.54,80.21h0A105.73,105.73,0,0,0,32.71,96.36,77.7,77.7,0,0,0,39.6,85.25a68.42,68.42,0,0,1-10.85-5.18c.91-.66,1.8-1.34,2.66-2a75.57,75.57,0,0,0,64.32,0c.87.71,1.76,1.39,2.66,2a68.68,68.68,0,0,1-10.87,5.19,77,77,0,0,0,6.89,11.1A105.25,105.25,0,0,0,126.6,80.22h0C129.24,52.84,122.09,29.11,107.7,8.07ZM42.45,65.69C36.18,65.69,31,60,31,53s5-12.74,11.43-12.74S54,46,53.89,53,48.84,65.69,42.45,65.69Zm42.24,0C78.41,65.69,73.25,60,73.25,53s5-12.74,11.44-12.74S96.23,46,96.12,53,91.08,65.69,84.69,65.69Z"/></g></g></g></svg>
\ No newline at end of file diff --git a/launcher/resources/breeze_light/scalable/jarmods.svg b/launcher/resources/breeze_light/scalable/jarmods.svg index 49a45d36..72a8e504 100644 --- a/launcher/resources/breeze_light/scalable/jarmods.svg +++ b/launcher/resources/breeze_light/scalable/jarmods.svg @@ -1 +1 @@ -<svg width="8" height="8" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M2 1.5V2H1v4.5h6V2H6v-.5H4.5V2h-1v-.5H2Zm-.5 1h5V6h-5V2.5Z" fill="#EFF0F1"/><path d="M3.93 4.37s-.19-.364-.183-.63c.005-.19.434-.378.603-.651.168-.273-.022-.54-.022-.54s.043.197-.07.4c-.112.203-.525.322-.686.672-.16.35.357.75.357.75Z" fill="#EFF0F1"/><path d="M4.637 3.264s-.645.246-.645.525c0 .28.175.372.203.463.028.091-.049.245-.049.245s.252-.175.21-.378c-.042-.203-.238-.267-.126-.47.075-.136.407-.385.407-.385Z" fill="#EFF0F1"/><path d="M3.859 4.741c.595-.021.812-.209.812-.209-.385.105-1.407.098-1.415.021-.006-.077.316-.14.316-.14s-.505 0-.546.126c-.043.126.238.223.833.202ZM4.72 5.036s.583-.124.526-.44c-.07-.386-.477-.169-.477-.169s.288 0 .316.175c.028.175-.364.434-.364.434ZM4.434 4.868s-.147.038-.364.063c-.292.033-.645.007-.673-.042-.028-.05.05-.077.05-.077-.35.084-.16.23.251.26.352.023.876-.106.876-.106l-.14-.098ZM3.53 5.174s-.159.004-.168.088c-.01.084.098.159.49.182.392.024.668-.107.668-.107l-.178-.107s-.112.023-.285.046c-.173.024-.527-.018-.541-.051-.014-.032.014-.051.014-.051Z" fill="#EFF0F1"/><path d="M5.057 5.552c.06-.065-.019-.117-.019-.117s.028.033-.009.07c-.037.037-.378.13-.924.158-.546.028-1.14-.05-1.159-.121-.018-.07.304-.126.304-.126-.037.005-.485.014-.5.136-.013.12.197.22 1.037.22.84 0 1.21-.155 1.27-.22Z" fill="#EFF0F1"/><path d="M4.73 5.828c-.368.074-1.489.027-1.489.027s.728.173 1.56.029c.397-.07.42-.262.42-.262s-.122.13-.49.206Z" fill="#EFF0F1"/></svg>
\ No newline at end of file +<svg width="8" height="8" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M2 1.5V2H1v4.5h6V2H6v-.5H4.5V2h-1v-.5H2Zm-.5 1h5V6h-5V2.5Z" fill="#232629"/><path d="M3.93 4.37s-.19-.364-.183-.63c.005-.19.434-.378.603-.651.168-.273-.022-.54-.022-.54s.043.197-.07.4c-.112.203-.525.322-.686.672-.16.35.357.75.357.75Z" fill="#232629"/><path d="M4.637 3.264s-.645.246-.645.525c0 .28.175.372.203.463.028.091-.049.245-.049.245s.252-.175.21-.378c-.042-.203-.238-.267-.126-.47.075-.136.407-.385.407-.385Z" fill="#232629"/><path d="M3.859 4.741c.595-.021.812-.209.812-.209-.385.105-1.407.098-1.415.021-.006-.077.316-.14.316-.14s-.505 0-.546.126c-.043.126.238.223.833.202ZM4.72 5.036s.583-.124.526-.44c-.07-.386-.477-.169-.477-.169s.288 0 .316.175c.028.175-.364.434-.364.434ZM4.434 4.868s-.147.038-.364.063c-.292.033-.645.007-.673-.042-.028-.05.05-.077.05-.077-.35.084-.16.23.251.26.352.023.876-.106.876-.106l-.14-.098ZM3.53 5.174s-.159.004-.168.088c-.01.084.098.159.49.182.392.024.668-.107.668-.107l-.178-.107s-.112.023-.285.046c-.173.024-.527-.018-.541-.051-.014-.032.014-.051.014-.051Z" fill="#232629"/><path d="M5.057 5.552c.06-.065-.019-.117-.019-.117s.028.033-.009.07c-.037.037-.378.13-.924.158-.546.028-1.14-.05-1.159-.121-.018-.07.304-.126.304-.126-.037.005-.485.014-.5.136-.013.12.197.22 1.037.22.84 0 1.21-.155 1.27-.22Z" fill="#232629"/><path d="M4.73 5.828c-.368.074-1.489.027-1.489.027s.728.173 1.56.029c.397-.07.42-.262.42-.262s-.122.13-.49.206Z" fill="#232629"/></svg>
\ No newline at end of file diff --git a/launcher/resources/breeze_light/scalable/new.svg b/launcher/resources/breeze_light/scalable/new.svg index 51babd76..6434a18e 100644 --- a/launcher/resources/breeze_light/scalable/new.svg +++ b/launcher/resources/breeze_light/scalable/new.svg @@ -1,18 +1,13 @@ -<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"> - <defs - id="defs3051"> - <style - type="text/css" - id="current-color-scheme"> +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> + <defs id="defs3051"> + <style type="text/css" id="current-color-scheme"> .ColorScheme-Text { color:#232629; } </style> </defs> - <path - style="fill:currentColor;fill-opacity:1;stroke:none" - d="M 4 4 L 4 28 L 17 28 L 17 27 L 5 27 L 5 14 L 10 14 L 13 11 L 27 11 L 27 17 L 28 17 L 28 7 L 18 7 L 15 4 L 4 4 z M 22 17 L 22 22 L 17 22 L 17 23 L 22 23 L 22 28 L 23 28 L 23 23 L 28 23 L 28 22 L 23 22 L 23 17 L 22 17 z " - id="path99" + <path style="fill:currentColor;fill-opacity:1;stroke:none" + d="M 3 2 L 3 14 L 8 14 L 8 13 L 4 13 L 4 3 L 9 3 L 9 6 L 12 6 L 12 9 L 13 9 L 13 6 L 13 5 L 10 2 L 9 2 L 3 2 z M 10 9 L 10 11 L 8 11 L 8 12 L 10 12 L 10 14 L 11 14 L 11 12 L 13 12 L 13 11 L 11 11 L 11 9 L 10 9 z " class="ColorScheme-Text" /> </svg> diff --git a/launcher/ui/InstanceWindow.cpp b/launcher/ui/InstanceWindow.cpp index 09ce0d67..c62b370f 100644 --- a/launcher/ui/InstanceWindow.cpp +++ b/launcher/ui/InstanceWindow.cpp @@ -132,6 +132,12 @@ InstanceWindow::InstanceWindow(InstancePtr instance, QWidget *parent) { connect(m_instance.get(), &BaseInstance::statusChanged, this, &InstanceWindow::on_instanceStatusChanged); } + + // add ourself as the modpack page's instance window + { + static_cast<ManagedPackPage*>(m_container->getPage("managed_pack"))->setInstanceWindow(this); + } + show(); } diff --git a/launcher/ui/MainWindow.cpp b/launcher/ui/MainWindow.cpp index 6f855f91..cc81d8e3 100644 --- a/launcher/ui/MainWindow.cpp +++ b/launcher/ui/MainWindow.cpp @@ -2270,10 +2270,25 @@ void MainWindow::on_actionCreateInstanceShortcut_triggered() QMessageBox::critical(this, tr("Create instance shortcut"), tr("Failed to create icon for shortcut.")); return; } - - if (FS::createShortcut(FS::PathCombine(desktopPath, m_selectedInstance->name()), - appPath, { "--launch", m_selectedInstance->id() }, - m_selectedInstance->name(), iconPath)) { + + QString desktopFilePath = FS::PathCombine(desktopPath, m_selectedInstance->name() + ".desktop"); + QStringList args; + if (DesktopServices::isFlatpak()) { + QFileDialog fileDialog; + // workaround to make sure the portal file dialog opens in the desktop directory + fileDialog.setDirectoryUrl(desktopPath); + desktopFilePath = fileDialog.getSaveFileName( + this, tr("Create Shortcut"), desktopFilePath, + tr("Desktop Entries (*.desktop)")); + if (desktopFilePath.isEmpty()) + return; // file dialog canceled by user + appPath = "flatpak"; + QString flatpakAppId = BuildConfig.LAUNCHER_DESKTOPFILENAME; + flatpakAppId.remove(".desktop"); + args.append({ "run", flatpakAppId }); + } + args.append({ "--launch", m_selectedInstance->id() }); + if (FS::createShortcut(desktopFilePath, appPath, args, m_selectedInstance->name(), iconPath)) { QMessageBox::information(this, tr("Create instance shortcut"), tr("Created a shortcut to this instance on your desktop!")); } else diff --git a/launcher/ui/dialogs/BlockedModsDialog.cpp b/launcher/ui/dialogs/BlockedModsDialog.cpp index 346ceff2..8b49bd1a 100644 --- a/launcher/ui/dialogs/BlockedModsDialog.cpp +++ b/launcher/ui/dialogs/BlockedModsDialog.cpp @@ -84,7 +84,15 @@ void BlockedModsDialog::dragEnterEvent(QDragEnterEvent* e) void BlockedModsDialog::dropEvent(QDropEvent* e) { - for (const QUrl& url : e->mimeData()->urls()) { + for (QUrl& url : e->mimeData()->urls()) { + if (url.scheme().isEmpty()) { // ensure isLocalFile() works correctly + url.setScheme("file"); + } + + if (!url.isLocalFile()) { // can't drop external files here. + continue; + } + QString filePath = url.toLocalFile(); qDebug() << "[Blocked Mods Dialog] Dropped file:" << filePath; addHashTask(filePath); diff --git a/launcher/ui/dialogs/BlockedModsDialog.h b/launcher/ui/dialogs/BlockedModsDialog.h index f63d04b4..014f488a 100644 --- a/launcher/ui/dialogs/BlockedModsDialog.h +++ b/launcher/ui/dialogs/BlockedModsDialog.h @@ -41,6 +41,7 @@ struct BlockedMod { QString hash; bool matched; QString localPath; + QString targetFolder; }; QT_BEGIN_NAMESPACE diff --git a/launcher/ui/pages/BasePageContainer.h b/launcher/ui/pages/BasePageContainer.h index f8c7adeb..b41fe12a 100644 --- a/launcher/ui/pages/BasePageContainer.h +++ b/launcher/ui/pages/BasePageContainer.h @@ -1,10 +1,13 @@ #pragma once +class BasePage; + class BasePageContainer { public: virtual ~BasePageContainer(){}; virtual bool selectPage(QString pageId) = 0; + virtual BasePage* getPage(QString pageId) { return nullptr; }; virtual void refreshContainer() = 0; virtual bool requestClose() = 0; }; diff --git a/launcher/ui/pages/instance/ExternalResourcesPage.cpp b/launcher/ui/pages/instance/ExternalResourcesPage.cpp index 5c919573..c66d1368 100644 --- a/launcher/ui/pages/instance/ExternalResourcesPage.cpp +++ b/launcher/ui/pages/instance/ExternalResourcesPage.cpp @@ -67,11 +67,21 @@ void ExternalResourcesPage::ShowContextMenu(const QPoint& pos) void ExternalResourcesPage::openedImpl() { m_model->startWatching(); + + auto const setting_name = QString("WideBarVisibility_%1").arg(id()); + if (!APPLICATION->settings()->contains(setting_name)) + m_wide_bar_setting = APPLICATION->settings()->registerSetting(setting_name); + else + m_wide_bar_setting = APPLICATION->settings()->getSetting(setting_name); + + ui->actionsToolbar->setVisibilityState(m_wide_bar_setting->get().toByteArray()); } void ExternalResourcesPage::closedImpl() { m_model->stopWatching(); + + m_wide_bar_setting->set(ui->actionsToolbar->getVisibilityState()); } void ExternalResourcesPage::retranslate() diff --git a/launcher/ui/pages/instance/ExternalResourcesPage.h b/launcher/ui/pages/instance/ExternalResourcesPage.h index 11058bf6..2d1a5b51 100644 --- a/launcher/ui/pages/instance/ExternalResourcesPage.h +++ b/launcher/ui/pages/instance/ExternalResourcesPage.h @@ -4,6 +4,7 @@ #include <QSortFilterProxyModel> #include "Application.h" +#include "settings/Setting.h" #include "minecraft/MinecraftInstance.h" #include "ui/pages/BasePage.h" @@ -70,4 +71,6 @@ class ExternalResourcesPage : public QMainWindow, public BasePage { QString m_viewFilter; bool m_controlsEnabled = true; + + std::shared_ptr<Setting> m_wide_bar_setting = nullptr; }; diff --git a/launcher/ui/pages/instance/ManagedPackPage.cpp b/launcher/ui/pages/instance/ManagedPackPage.cpp new file mode 100644 index 00000000..7a0d234c --- /dev/null +++ b/launcher/ui/pages/instance/ManagedPackPage.cpp @@ -0,0 +1,432 @@ +// SPDX-FileCopyrightText: 2022 flow <flowlnlnln@gmail.com> +// +// SPDX-License-Identifier: GPL-3.0-only + +#include "ManagedPackPage.h" +#include "ui_ManagedPackPage.h" + +#include <QListView> +#include <QProxyStyle> + +#include <HoeDown.h> + +#include "Application.h" +#include "BuildConfig.h" +#include "InstanceImportTask.h" +#include "InstanceList.h" +#include "InstanceTask.h" +#include "Json.h" + +#include "modplatform/modrinth/ModrinthPackManifest.h" + +#include "ui/InstanceWindow.h" +#include "ui/dialogs/CustomMessageBox.h" +#include "ui/dialogs/ProgressDialog.h" + +/** This is just to override the combo box popup behavior so that the combo box doesn't take the whole screen. + * ... thanks Qt. + */ +class NoBigComboBoxStyle : public QProxyStyle { + Q_OBJECT + + public: + NoBigComboBoxStyle(QStyle* style) : QProxyStyle(style) {} + + // clang-format off + int styleHint(QStyle::StyleHint hint, const QStyleOption* option = nullptr, const QWidget* widget = nullptr, QStyleHintReturn* returnData = nullptr) const override + { + if (hint == QStyle::SH_ComboBox_Popup) + return false; + + return QProxyStyle::styleHint(hint, option, widget, returnData); + } + // clang-format on +}; + +ManagedPackPage* ManagedPackPage::createPage(BaseInstance* inst, QString type, QWidget* parent) +{ + if (type == "modrinth") + return new ModrinthManagedPackPage(inst, nullptr, parent); + if (type == "flame" && (APPLICATION->capabilities() & Application::SupportsFlame)) + return new FlameManagedPackPage(inst, nullptr, parent); + + return new GenericManagedPackPage(inst, nullptr, parent); +} + +ManagedPackPage::ManagedPackPage(BaseInstance* inst, InstanceWindow* instance_window, QWidget* parent) + : QWidget(parent), m_instance_window(instance_window), ui(new Ui::ManagedPackPage), m_inst(inst) +{ + Q_ASSERT(inst); + + ui->setupUi(this); + + ui->versionsComboBox->setStyle(new NoBigComboBoxStyle(ui->versionsComboBox->style())); + + ui->reloadButton->setVisible(false); + connect(ui->reloadButton, &QPushButton::clicked, this, [this](bool){ + ui->reloadButton->setVisible(false); + + m_loaded = false; + // Pretend we're opening the page again + openedImpl(); + }); +} + +ManagedPackPage::~ManagedPackPage() +{ + delete ui; +} + +void ManagedPackPage::openedImpl() +{ + ui->packName->setText(m_inst->getManagedPackName()); + ui->packVersion->setText(m_inst->getManagedPackVersionName()); + ui->packOrigin->setText(tr("Website: <a href=%1>%2</a> | Pack ID: %3 | Version ID: %4") + .arg(url(), displayName(), m_inst->getManagedPackID(), m_inst->getManagedPackVersionID())); + + parseManagedPack(); +} + +QString ManagedPackPage::displayName() const +{ + auto type = m_inst->getManagedPackType(); + if (type.isEmpty()) + return {}; + if (type == "flame") + type = "CurseForge"; + return type.replace(0, 1, type[0].toUpper()); +} + +QIcon ManagedPackPage::icon() const +{ + return APPLICATION->getThemedIcon(m_inst->getManagedPackType()); +} + +QString ManagedPackPage::helpPage() const +{ + return {}; +} + +void ManagedPackPage::retranslate() +{ + ui->retranslateUi(this); +} + +bool ManagedPackPage::shouldDisplay() const +{ + return m_inst->isManagedPack(); +} + +bool ManagedPackPage::runUpdateTask(InstanceTask* task) +{ + Q_ASSERT(task); + + unique_qobject_ptr<Task> wrapped_task(APPLICATION->instances()->wrapInstanceTask(task)); + + connect(task, &Task::failed, + [this](QString reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show(); }); + connect(task, &Task::succeeded, [this, task]() { + QStringList warnings = task->warnings(); + if (warnings.count()) + CustomMessageBox::selectable(this, tr("Warnings"), warnings.join('\n'), QMessageBox::Warning)->show(); + }); + connect(task, &Task::aborted, [this] { + CustomMessageBox::selectable(this, tr("Task aborted"), tr("The task has been aborted by the user."), QMessageBox::Information) + ->show(); + }); + + ProgressDialog loadDialog(this); + loadDialog.setSkipButton(true, tr("Abort")); + loadDialog.execWithTask(task); + + return task->wasSuccessful(); +} + +void ManagedPackPage::suggestVersion() +{ + ui->updateButton->setText(tr("Update pack")); + ui->updateButton->setDisabled(false); +} + +void ManagedPackPage::setFailState() +{ + qDebug() << "Setting fail state!"; + + // We block signals here so that suggestVersion() doesn't get called, causing an assertion fail. + ui->versionsComboBox->blockSignals(true); + ui->versionsComboBox->clear(); + ui->versionsComboBox->addItem(tr("Failed to search for available versions."), {}); + ui->versionsComboBox->blockSignals(false); + + ui->changelogTextBrowser->setText(tr("Failed to request changelog data for this modpack.")); + + ui->updateButton->setText(tr("Cannot update!")); + ui->updateButton->setDisabled(true); + + ui->reloadButton->setVisible(true); +} + +ModrinthManagedPackPage::ModrinthManagedPackPage(BaseInstance* inst, InstanceWindow* instance_window, QWidget* parent) + : ManagedPackPage(inst, instance_window, parent) +{ + Q_ASSERT(inst->isManagedPack()); + connect(ui->versionsComboBox, SIGNAL(currentIndexChanged(int)), this, SLOT(suggestVersion())); + connect(ui->updateButton, &QPushButton::pressed, this, &ModrinthManagedPackPage::update); +} + +// MODRINTH + +void ModrinthManagedPackPage::parseManagedPack() +{ + qDebug() << "Parsing Modrinth pack"; + + // No need for the extra work because we already have everything we need. + if (m_loaded) + return; + + if (m_fetch_job && m_fetch_job->isRunning()) + m_fetch_job->abort(); + + m_fetch_job.reset(new NetJob(QString("Modrinth::PackVersions(%1)").arg(m_inst->getManagedPackName()), APPLICATION->network())); + auto response = std::make_shared<QByteArray>(); + + QString id = m_inst->getManagedPackID(); + + m_fetch_job->addNetAction(Net::Download::makeByteArray(QString("%1/project/%2/version").arg(BuildConfig.MODRINTH_PROD_URL, id), response.get())); + + QObject::connect(m_fetch_job.get(), &NetJob::succeeded, this, [this, response, id] { + QJsonParseError parse_error{}; + QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); + if (parse_error.error != QJsonParseError::NoError) { + qWarning() << "Error while parsing JSON response from Modrinth at " << parse_error.offset + << " reason: " << parse_error.errorString(); + qWarning() << *response; + + setFailState(); + + return; + } + + try { + Modrinth::loadIndexedVersions(m_pack, doc); + } catch (const JSONValidationError& e) { + qDebug() << *response; + qWarning() << "Error while reading modrinth modpack version: " << e.cause(); + + setFailState(); + return; + } + + // We block signals here so that suggestVersion() doesn't get called, causing an assertion fail. + ui->versionsComboBox->blockSignals(true); + ui->versionsComboBox->clear(); + ui->versionsComboBox->blockSignals(false); + + for (auto version : m_pack.versions) { + QString name; + + if (!version.name.contains(version.version)) + name = QString("%1 — %2").arg(version.name, version.version); + else + name = version.name; + + // NOTE: the id from version isn't the same id in the modpack format spec... + // e.g. HexMC's 4.4.0 has versionId 4.0.0 in the modpack index.............. + if (version.version == m_inst->getManagedPackVersionName()) + name.append(tr(" (Current)")); + + ui->versionsComboBox->addItem(name, QVariant(version.id)); + } + + suggestVersion(); + + m_loaded = true; + }); + QObject::connect(m_fetch_job.get(), &NetJob::failed, this, &ModrinthManagedPackPage::setFailState); + QObject::connect(m_fetch_job.get(), &NetJob::aborted, this, &ModrinthManagedPackPage::setFailState); + + ui->changelogTextBrowser->setText(tr("Fetching changelogs...")); + + m_fetch_job->start(); +} + +QString ModrinthManagedPackPage::url() const +{ + return "https://modrinth.com/mod/" + m_inst->getManagedPackID(); +} + +void ModrinthManagedPackPage::suggestVersion() +{ + auto index = ui->versionsComboBox->currentIndex(); + auto version = m_pack.versions.at(index); + + HoeDown md_parser; + ui->changelogTextBrowser->setHtml(md_parser.process(version.changelog.toUtf8())); + + ManagedPackPage::suggestVersion(); +} + +void ModrinthManagedPackPage::update() +{ + auto index = ui->versionsComboBox->currentIndex(); + auto version = m_pack.versions.at(index); + + QMap<QString, QString> extra_info; + // NOTE: Don't use 'm_pack.id' here, since we didn't completely parse all the metadata for the pack, including this field. + extra_info.insert("pack_id", m_inst->getManagedPackID()); + extra_info.insert("pack_version_id", version.id); + extra_info.insert("original_instance_id", m_inst->id()); + + auto extracted = new InstanceImportTask(version.download_url, this, std::move(extra_info)); + + InstanceName inst_name(m_inst->getManagedPackName(), version.version); + inst_name.setName(m_inst->name().replace(m_inst->getManagedPackVersionName(), version.version)); + extracted->setName(inst_name); + + extracted->setGroup(APPLICATION->instances()->getInstanceGroup(m_inst->id())); + extracted->setIcon(m_inst->iconKey()); + extracted->setConfirmUpdate(false); + + auto did_succeed = runUpdateTask(extracted); + + if (m_instance_window && did_succeed) + m_instance_window->close(); +} + +// FLAME + +FlameManagedPackPage::FlameManagedPackPage(BaseInstance* inst, InstanceWindow* instance_window, QWidget* parent) + : ManagedPackPage(inst, instance_window, parent) +{ + Q_ASSERT(inst->isManagedPack()); + connect(ui->versionsComboBox, SIGNAL(currentIndexChanged(int)), this, SLOT(suggestVersion())); + connect(ui->updateButton, &QPushButton::pressed, this, &FlameManagedPackPage::update); +} + +void FlameManagedPackPage::parseManagedPack() +{ + qDebug() << "Parsing Flame pack"; + + // We need to tell the user to redownload the pack, since we didn't save the required info previously + if (m_inst->getManagedPackID().isEmpty()) { + setFailState(); + QString message = + tr("<h1>Hey there!</h1>" + "<h4>" + "It seems like your Pack ID is null. This is because of a bug in older versions of the launcher.<br/>" + "Unfortunately, we can't do the proper API requests without this information.<br/>" + "<br/>" + "So, in order for this feature to work, you will need to re-download the modpack from the built-in downloader.<br/>" + "<br/>" + "Don't worry though, it will ask you to update this instance instead, so you'll not lose this instance!" + "</h4>"); + + ui->changelogTextBrowser->setHtml(message); + return; + } + + // No need for the extra work because we already have everything we need. + if (m_loaded) + return; + + if (m_fetch_job && m_fetch_job->isRunning()) + m_fetch_job->abort(); + + m_fetch_job.reset(new NetJob(QString("Flame::PackVersions(%1)").arg(m_inst->getManagedPackName()), APPLICATION->network())); + auto response = std::make_shared<QByteArray>(); + + QString id = m_inst->getManagedPackID(); + + m_fetch_job->addNetAction(Net::Download::makeByteArray(QString("%1/mods/%2/files").arg(BuildConfig.FLAME_BASE_URL, id), response.get())); + + QObject::connect(m_fetch_job.get(), &NetJob::succeeded, this, [this, response, id] { + QJsonParseError parse_error{}; + QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); + if (parse_error.error != QJsonParseError::NoError) { + qWarning() << "Error while parsing JSON response from Flame at " << parse_error.offset + << " reason: " << parse_error.errorString(); + qWarning() << *response; + + setFailState(); + + return; + } + + try { + auto obj = doc.object(); + auto data = Json::ensureArray(obj, "data"); + Flame::loadIndexedPackVersions(m_pack, data); + } catch (const JSONValidationError& e) { + qDebug() << *response; + qWarning() << "Error while reading flame modpack version: " << e.cause(); + + setFailState(); + return; + } + + // We block signals here so that suggestVersion() doesn't get called, causing an assertion fail. + ui->versionsComboBox->blockSignals(true); + ui->versionsComboBox->clear(); + ui->versionsComboBox->blockSignals(false); + + for (auto version : m_pack.versions) { + QString name; + + name = version.version; + + if (version.fileId == m_inst->getManagedPackVersionID().toInt()) + name.append(tr(" (Current)")); + + ui->versionsComboBox->addItem(name, QVariant(version.fileId)); + } + + suggestVersion(); + + m_loaded = true; + }); + QObject::connect(m_fetch_job.get(), &NetJob::failed, this, &FlameManagedPackPage::setFailState); + QObject::connect(m_fetch_job.get(), &NetJob::aborted, this, &FlameManagedPackPage::setFailState); + + m_fetch_job->start(); +} + +QString FlameManagedPackPage::url() const +{ + // FIXME: We should display the websiteUrl field, but this requires doing the API request first :( + return {}; +} + +void FlameManagedPackPage::suggestVersion() +{ + auto index = ui->versionsComboBox->currentIndex(); + auto version = m_pack.versions.at(index); + + ui->changelogTextBrowser->setHtml(m_api.getModFileChangelog(m_inst->getManagedPackID().toInt(), version.fileId)); + + ManagedPackPage::suggestVersion(); +} + +void FlameManagedPackPage::update() +{ + auto index = ui->versionsComboBox->currentIndex(); + auto version = m_pack.versions.at(index); + + QMap<QString, QString> extra_info; + extra_info.insert("pack_id", m_inst->getManagedPackID()); + extra_info.insert("pack_version_id", QString::number(version.fileId)); + extra_info.insert("original_instance_id", m_inst->id()); + + auto extracted = new InstanceImportTask(version.downloadUrl, this, std::move(extra_info)); + + extracted->setName(m_inst->name()); + extracted->setGroup(APPLICATION->instances()->getInstanceGroup(m_inst->id())); + extracted->setIcon(m_inst->iconKey()); + extracted->setConfirmUpdate(false); + + auto did_succeed = runUpdateTask(extracted); + + if (m_instance_window && did_succeed) + m_instance_window->close(); +} + +#include "ManagedPackPage.moc" diff --git a/launcher/ui/pages/instance/ManagedPackPage.h b/launcher/ui/pages/instance/ManagedPackPage.h new file mode 100644 index 00000000..d29a5e88 --- /dev/null +++ b/launcher/ui/pages/instance/ManagedPackPage.h @@ -0,0 +1,152 @@ +// SPDX-FileCopyrightText: 2022 flow <flowlnlnln@gmail.com> +// +// SPDX-License-Identifier: GPL-3.0-only + +#pragma once + +#include "BaseInstance.h" + +#include "modplatform/modrinth/ModrinthAPI.h" +#include "modplatform/modrinth/ModrinthPackManifest.h" + +#include "modplatform/flame/FlameAPI.h" +#include "modplatform/flame/FlamePackIndex.h" + +#include "ui/pages/BasePage.h" + +#include <QWidget> + +namespace Ui { +class ManagedPackPage; +} + +class InstanceTask; +class InstanceWindow; + +class ManagedPackPage : public QWidget, public BasePage { + Q_OBJECT + + public: + inline static ManagedPackPage* createPage(BaseInstance* inst, QWidget* parent = nullptr) + { + return ManagedPackPage::createPage(inst, inst->getManagedPackType(), parent); + } + + static ManagedPackPage* createPage(BaseInstance* inst, QString type, QWidget* parent = nullptr); + ~ManagedPackPage() override; + + [[nodiscard]] QString displayName() const override; + [[nodiscard]] QIcon icon() const override; + [[nodiscard]] QString helpPage() const override; + [[nodiscard]] QString id() const override { return "managed_pack"; } + [[nodiscard]] bool shouldDisplay() const override; + + void openedImpl() override; + + bool apply() override { return true; } + void retranslate() override; + + /** Gets the necessary information about the managed pack, such as + * available versions*/ + virtual void parseManagedPack(){}; + + /** URL of the managed pack. + * Not the version-specific one. + */ + [[nodiscard]] virtual QString url() const { return {}; }; + + void setInstanceWindow(InstanceWindow* window) { m_instance_window = window; } + + public slots: + /** Gets the current version selection and update the UI, including the update button and the changelog. + */ + virtual void suggestVersion(); + + virtual void update(){}; + + protected slots: + /** Does the necessary UI changes for when something failed. + * + * This includes: + * - Setting an appropriate text on the version selector to indicate a fail; + * - Setting an appropriate text on the changelog text browser to indicate a fail; + * - Disable the update button. + */ + void setFailState(); + + protected: + ManagedPackPage(BaseInstance* inst, InstanceWindow* instance_window, QWidget* parent = nullptr); + + /** Run the InstanceTask, with a progress dialog and all. + * Similar to MainWindow::instanceFromInstanceTask + * + * Returns whether the task was successful. + */ + bool runUpdateTask(InstanceTask*); + + protected: + InstanceWindow* m_instance_window = nullptr; + + Ui::ManagedPackPage* ui; + BaseInstance* m_inst; + + bool m_loaded = false; +}; + +/** Simple page for when we aren't a managed pack. */ +class GenericManagedPackPage final : public ManagedPackPage { + Q_OBJECT + + public: + GenericManagedPackPage(BaseInstance* inst, InstanceWindow* instance_window, QWidget* parent = nullptr) + : ManagedPackPage(inst, instance_window, parent) + {} + ~GenericManagedPackPage() override = default; + + // TODO: We may want to show this page with some useful info at some point. + [[nodiscard]] bool shouldDisplay() const override { return false; }; +}; + +class ModrinthManagedPackPage final : public ManagedPackPage { + Q_OBJECT + + public: + ModrinthManagedPackPage(BaseInstance* inst, InstanceWindow* instance_window, QWidget* parent = nullptr); + ~ModrinthManagedPackPage() override = default; + + void parseManagedPack() override; + [[nodiscard]] QString url() const override; + + public slots: + void suggestVersion() override; + + void update() override; + + private: + NetJob::Ptr m_fetch_job = nullptr; + + Modrinth::Modpack m_pack; + ModrinthAPI m_api; +}; + +class FlameManagedPackPage final : public ManagedPackPage { + Q_OBJECT + + public: + FlameManagedPackPage(BaseInstance* inst, InstanceWindow* instance_window, QWidget* parent = nullptr); + ~FlameManagedPackPage() override = default; + + void parseManagedPack() override; + [[nodiscard]] QString url() const override; + + public slots: + void suggestVersion() override; + + void update() override; + + private: + NetJob::Ptr m_fetch_job = nullptr; + + Flame::IndexedPack m_pack; + FlameAPI m_api; +}; diff --git a/launcher/ui/pages/instance/ManagedPackPage.ui b/launcher/ui/pages/instance/ManagedPackPage.ui new file mode 100644 index 00000000..bbe44a94 --- /dev/null +++ b/launcher/ui/pages/instance/ManagedPackPage.ui @@ -0,0 +1,193 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>ManagedPackPage</class> + <widget class="QWidget" name="ManagedPackPage"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>731</width> + <height>538</height> + </rect> + </property> + <layout class="QGridLayout" name="gridLayout"> + <property name="leftMargin"> + <number>9</number> + </property> + <property name="topMargin"> + <number>9</number> + </property> + <property name="rightMargin"> + <number>9</number> + </property> + <property name="bottomMargin"> + <number>9</number> + </property> + <item row="0" column="0"> + <layout class="QVBoxLayout" name="verticalLayout_2"> + <item> + <widget class="QGroupBox" name="packInformationBox"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Preferred" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="title"> + <string>Pack information</string> + </property> + <layout class="QFormLayout" name="formLayout_2"> + <item row="0" column="0"> + <layout class="QHBoxLayout" name="packNameLayout"> + <item> + <widget class="QLabel" name="packNameLabel"> + <property name="text"> + <string>Pack name:</string> + </property> + </widget> + </item> + <item> + <widget class="QLabel" name="packName"> + <property name="text"> + <string notr="true">placeholder</string> + </property> + </widget> + </item> + </layout> + </item> + <item row="1" column="0"> + <layout class="QHBoxLayout" name="packVersionLayout"> + <item> + <widget class="QLabel" name="packVersionLabel"> + <property name="text"> + <string>Current version:</string> + </property> + </widget> + </item> + <item> + <widget class="QLabel" name="packVersion"> + <property name="cursor"> + <cursorShape>IBeamCursor</cursorShape> + </property> + <property name="text"> + <string notr="true">placeholder</string> + </property> + <property name="textInteractionFlags"> + <set>Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse</set> + </property> + </widget> + </item> + </layout> + </item> + <item row="2" column="0"> + <layout class="QHBoxLayout" name="packOriginLayout"> + <item> + <widget class="QLabel" name="packOriginLabel"> + <property name="text"> + <string>Provider information:</string> + </property> + </widget> + </item> + <item> + <widget class="QLabel" name="packOrigin"> + <property name="cursor"> + <cursorShape>IBeamCursor</cursorShape> + </property> + <property name="text"> + <string notr="true">placeholder</string> + </property> + <property name="textFormat"> + <enum>Qt::RichText</enum> + </property> + <property name="openExternalLinks"> + <bool>true</bool> + </property> + <property name="textInteractionFlags"> + <set>Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse</set> + </property> + </widget> + </item> + </layout> + </item> + </layout> + </widget> + </item> + <item> + <widget class="Line" name="line"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + </widget> + </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout"> + <item> + <widget class="QLabel" name="updateToVersionLabel"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Fixed" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>Update to version:</string> + </property> + </widget> + </item> + <item> + <widget class="QComboBox" name="versionsComboBox"/> + </item> + <item> + <widget class="QPushButton" name="updateButton"> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="sizePolicy"> + <sizepolicy hsizetype="Fixed" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>Fetching versions...</string> + </property> + </widget> + </item> + </layout> + </item> + <item> + <widget class="QGroupBox" name="changelogBox"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Preferred" vsizetype="Expanding"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="title"> + <string>Changelog</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout_3"> + <item> + <widget class="QTextBrowser" name="changelogTextBrowser"> + <property name="placeholderText"> + <string>No changelog available for this version!</string> + </property> + </widget> + </item> + </layout> + </widget> + </item> + </layout> + </item> + <item row="1" column="0"> + <widget class="QPushButton" name="reloadButton"> + <property name="text"> + <string>Reload page</string> + </property> + </widget> + </item> + </layout> + </widget> + <resources/> + <connections/> +</ui> diff --git a/launcher/ui/pages/instance/ScreenshotsPage.cpp b/launcher/ui/pages/instance/ScreenshotsPage.cpp index c97253e4..0092aef3 100644 --- a/launcher/ui/pages/instance/ScreenshotsPage.cpp +++ b/launcher/ui/pages/instance/ScreenshotsPage.cpp @@ -537,6 +537,19 @@ void ScreenshotsPage::openedImpl() ui->listView->setModel(nullptr); } } + + auto const setting_name = QString("WideBarVisibility_%1").arg(id()); + if (!APPLICATION->settings()->contains(setting_name)) + m_wide_bar_setting = APPLICATION->settings()->registerSetting(setting_name); + else + m_wide_bar_setting = APPLICATION->settings()->getSetting(setting_name); + + ui->toolBar->setVisibilityState(m_wide_bar_setting->get().toByteArray()); +} + +void ScreenshotsPage::closedImpl() +{ + m_wide_bar_setting->set(ui->toolBar->getVisibilityState()); } #include "ScreenshotsPage.moc" diff --git a/launcher/ui/pages/instance/ScreenshotsPage.h b/launcher/ui/pages/instance/ScreenshotsPage.h index c22706af..2eb0de04 100644 --- a/launcher/ui/pages/instance/ScreenshotsPage.h +++ b/launcher/ui/pages/instance/ScreenshotsPage.h @@ -40,6 +40,8 @@ #include "ui/pages/BasePage.h" #include <Application.h> +#include "settings/Setting.h" + class QFileSystemModel; class QIdentityProxyModel; namespace Ui @@ -59,7 +61,8 @@ public: explicit ScreenshotsPage(QString path, QWidget *parent = 0); virtual ~ScreenshotsPage(); - virtual void openedImpl() override; + void openedImpl() override; + void closedImpl() override; enum { @@ -110,4 +113,6 @@ private: QString m_folder; bool m_valid = false; bool m_uploadActive = false; + + std::shared_ptr<Setting> m_wide_bar_setting = nullptr; }; diff --git a/launcher/ui/pages/instance/ServersPage.cpp b/launcher/ui/pages/instance/ServersPage.cpp index d64bcb76..a625e20b 100644 --- a/launcher/ui/pages/instance/ServersPage.cpp +++ b/launcher/ui/pages/instance/ServersPage.cpp @@ -765,11 +765,21 @@ void ServersPage::updateState() void ServersPage::openedImpl() { m_model->observe(); + + auto const setting_name = QString("WideBarVisibility_%1").arg(id()); + if (!APPLICATION->settings()->contains(setting_name)) + m_wide_bar_setting = APPLICATION->settings()->registerSetting(setting_name); + else + m_wide_bar_setting = APPLICATION->settings()->getSetting(setting_name); + + ui->toolBar->setVisibilityState(m_wide_bar_setting->get().toByteArray()); } void ServersPage::closedImpl() { m_model->unobserve(); + + m_wide_bar_setting->set(ui->toolBar->getVisibilityState()); } void ServersPage::on_actionAdd_triggered() diff --git a/launcher/ui/pages/instance/ServersPage.h b/launcher/ui/pages/instance/ServersPage.h index ee63353a..476e7d70 100644 --- a/launcher/ui/pages/instance/ServersPage.h +++ b/launcher/ui/pages/instance/ServersPage.h @@ -42,6 +42,8 @@ #include "ui/pages/BasePage.h" #include <Application.h> +#include "settings/Setting.h" + namespace Ui { class ServersPage; @@ -112,5 +114,7 @@ private: // data Ui::ServersPage *ui = nullptr; ServersModel * m_model = nullptr; InstancePtr m_inst = nullptr; + + std::shared_ptr<Setting> m_wide_bar_setting = nullptr; }; diff --git a/launcher/ui/pages/instance/VersionPage.cpp b/launcher/ui/pages/instance/VersionPage.cpp index 7f98cba2..c8a65f10 100644 --- a/launcher/ui/pages/instance/VersionPage.cpp +++ b/launcher/ui/pages/instance/VersionPage.cpp @@ -126,6 +126,21 @@ void VersionPage::retranslate() ui->retranslateUi(this); } +void VersionPage::openedImpl() +{ + auto const setting_name = QString("WideBarVisibility_%1").arg(id()); + if (!APPLICATION->settings()->contains(setting_name)) + m_wide_bar_setting = APPLICATION->settings()->registerSetting(setting_name); + else + m_wide_bar_setting = APPLICATION->settings()->getSetting(setting_name); + + ui->toolBar->setVisibilityState(m_wide_bar_setting->get().toByteArray()); +} +void VersionPage::closedImpl() +{ + m_wide_bar_setting->set(ui->toolBar->getVisibilityState()); +} + QMenu * VersionPage::createPopupMenu() { QMenu* filteredMenu = QMainWindow::createPopupMenu(); diff --git a/launcher/ui/pages/instance/VersionPage.h b/launcher/ui/pages/instance/VersionPage.h index 23d2a1b3..166f36bb 100644 --- a/launcher/ui/pages/instance/VersionPage.h +++ b/launcher/ui/pages/instance/VersionPage.h @@ -70,6 +70,9 @@ public: virtual bool shouldDisplay() const override; void retranslate() override; + void openedImpl() override; + void closedImpl() override; + private slots: void on_actionChange_version_triggered(); void on_actionInstall_Forge_triggered(); @@ -116,6 +119,8 @@ private: int currentIdx = 0; bool controlsEnabled = false; + std::shared_ptr<Setting> m_wide_bar_setting = nullptr; + public slots: void versionCurrent(const QModelIndex ¤t, const QModelIndex &previous); diff --git a/launcher/ui/pages/instance/WorldListPage.cpp b/launcher/ui/pages/instance/WorldListPage.cpp index 85cc01ff..93458ce4 100644 --- a/launcher/ui/pages/instance/WorldListPage.cpp +++ b/launcher/ui/pages/instance/WorldListPage.cpp @@ -113,11 +113,21 @@ WorldListPage::WorldListPage(BaseInstance *inst, std::shared_ptr<WorldList> worl void WorldListPage::openedImpl() { m_worlds->startWatching(); + + auto const setting_name = QString("WideBarVisibility_%1").arg(id()); + if (!APPLICATION->settings()->contains(setting_name)) + m_wide_bar_setting = APPLICATION->settings()->registerSetting(setting_name); + else + m_wide_bar_setting = APPLICATION->settings()->getSetting(setting_name); + + ui->toolBar->setVisibilityState(m_wide_bar_setting->get().toByteArray()); } void WorldListPage::closedImpl() { m_worlds->stopWatching(); + + m_wide_bar_setting->set(ui->toolBar->getVisibilityState()); } WorldListPage::~WorldListPage() diff --git a/launcher/ui/pages/instance/WorldListPage.h b/launcher/ui/pages/instance/WorldListPage.h index 1dc9e53e..925521be 100644 --- a/launcher/ui/pages/instance/WorldListPage.h +++ b/launcher/ui/pages/instance/WorldListPage.h @@ -42,6 +42,8 @@ #include <Application.h> #include <LoggedProcess.h> +#include "settings/Setting.h" + class WorldList; namespace Ui { @@ -102,6 +104,8 @@ private: unique_qobject_ptr<LoggedProcess> m_mceditProcess; bool m_mceditStarting = false; + std::shared_ptr<Setting> m_wide_bar_setting = nullptr; + private slots: void on_actionCopy_Seed_triggered(); void on_actionMCEdit_triggered(); diff --git a/launcher/ui/pages/modplatform/flame/FlamePage.cpp b/launcher/ui/pages/modplatform/flame/FlamePage.cpp index a65b6585..f9ac4a78 100644 --- a/launcher/ui/pages/modplatform/flame/FlamePage.cpp +++ b/launcher/ui/pages/modplatform/flame/FlamePage.cpp @@ -197,12 +197,18 @@ void FlamePage::suggestCurrent() return; } - if (selectedVersion.isEmpty() || selectedVersion == "-1") { + if (m_selected_version_index == -1) { dialog->setSuggestedPack(); return; } - dialog->setSuggestedPack(current.name, new InstanceImportTask(selectedVersion,this)); + auto version = current.versions.at(m_selected_version_index); + + QMap<QString, QString> extra_info; + extra_info.insert("pack_id", QString::number(current.addonId)); + extra_info.insert("pack_version_id", QString::number(version.fileId)); + + dialog->setSuggestedPack(current.name, new InstanceImportTask(version.downloadUrl, this, std::move(extra_info))); QString editedLogoName; editedLogoName = "curseforge_" + current.logoName.section(".", 0, 0); listModel->getLogo(current.logoName, current.logoUrl, @@ -211,11 +217,18 @@ void FlamePage::suggestCurrent() void FlamePage::onVersionSelectionChanged(QString data) { - if (data.isNull() || data.isEmpty()) { - selectedVersion = ""; + bool is_blocked = false; + ui->versionSelectionBox->currentData().toInt(&is_blocked); + + if (data.isNull() || data.isEmpty() || is_blocked) { + m_selected_version_index = -1; return; } - selectedVersion = ui->versionSelectionBox->currentData().toString(); + + m_selected_version_index = ui->versionSelectionBox->currentIndex(); + + Q_ASSERT(current.versions.at(m_selected_version_index).downloadUrl == ui->versionSelectionBox->currentData().toString()); + suggestCurrent(); } diff --git a/launcher/ui/pages/modplatform/flame/FlamePage.h b/launcher/ui/pages/modplatform/flame/FlamePage.h index 8130e416..8bdca38e 100644 --- a/launcher/ui/pages/modplatform/flame/FlamePage.h +++ b/launcher/ui/pages/modplatform/flame/FlamePage.h @@ -99,5 +99,5 @@ private: Flame::ListModel* listModel = nullptr; Flame::IndexedPack current; - QString selectedVersion; + int m_selected_version_index = -1; }; diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp index 4482774c..8ab2ad1d 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp @@ -300,7 +300,11 @@ void ModrinthPage::suggestCurrent() for (auto& ver : current.versions) { if (ver.id == selectedVersion) { - dialog->setSuggestedPack(current.name, ver.version, new InstanceImportTask(ver.download_url, this)); + QMap<QString, QString> extra_info; + extra_info.insert("pack_id", current.id); + extra_info.insert("pack_version_id", ver.id); + + dialog->setSuggestedPack(current.name, ver.version, new InstanceImportTask(ver.download_url, this, std::move(extra_info))); auto iconName = current.iconName; m_model->getLogo(iconName, current.iconUrl.toString(), [this, iconName](QString logo) { dialog->setSuggestedIconFromFile(logo, iconName); }); diff --git a/launcher/ui/widgets/PageContainer.cpp b/launcher/ui/widgets/PageContainer.cpp index 8d606820..0a06a351 100644 --- a/launcher/ui/widgets/PageContainer.cpp +++ b/launcher/ui/widgets/PageContainer.cpp @@ -130,6 +130,11 @@ bool PageContainer::selectPage(QString pageId) return false; } +BasePage* PageContainer::getPage(QString pageId) +{ + return m_model->findPageEntryById(pageId); +} + void PageContainer::refreshContainer() { m_proxyModel->invalidate(); diff --git a/launcher/ui/widgets/PageContainer.h b/launcher/ui/widgets/PageContainer.h index 80d87a9b..97e294dc 100644 --- a/launcher/ui/widgets/PageContainer.h +++ b/launcher/ui/widgets/PageContainer.h @@ -79,6 +79,7 @@ public: } virtual bool selectPage(QString pageId) override; + BasePage* getPage(QString pageId) override; void refreshContainer() override; virtual void setParentContainer(BasePageContainer * container) diff --git a/launcher/ui/widgets/WideBar.cpp b/launcher/ui/widgets/WideBar.cpp index 79f1e0c9..428be563 100644 --- a/launcher/ui/widgets/WideBar.cpp +++ b/launcher/ui/widgets/WideBar.cpp @@ -1,19 +1,24 @@ #include "WideBar.h" + +#include <QContextMenuEvent> +#include <QCryptographicHash> #include <QToolButton> -#include <QMenu> -class ActionButton : public QToolButton -{ +class ActionButton : public QToolButton { Q_OBJECT -public: - ActionButton(QAction * action, QWidget * parent = 0) : QToolButton(parent), m_action(action) { + public: + ActionButton(QAction* action, QWidget* parent = nullptr) : QToolButton(parent), m_action(action) + { setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); + connect(action, &QAction::changed, this, &ActionButton::actionChanged); connect(this, &ActionButton::clicked, action, &QAction::trigger); + actionChanged(); }; -private slots: - void actionChanged() { + public slots: + void actionChanged() + { setEnabled(m_action->isEnabled()); setChecked(m_action->isChecked()); setCheckable(m_action->isCheckable()); @@ -23,137 +28,242 @@ private slots: setHidden(!m_action->isVisible()); setFocusPolicy(Qt::NoFocus); } -private: - QAction * m_action; -}; + private: + QAction* m_action; +}; WideBar::WideBar(const QString& title, QWidget* parent) : QToolBar(title, parent) { setFloatable(false); setMovable(false); + + setContextMenuPolicy(Qt::ContextMenuPolicy::CustomContextMenu); + connect(this, &QToolBar::customContextMenuRequested, this, &WideBar::showVisibilityMenu); } WideBar::WideBar(QWidget* parent) : QToolBar(parent) { setFloatable(false); setMovable(false); -} - -struct WideBar::BarEntry { - enum Type { - None, - Action, - Separator, - Spacer - } type = None; - QAction *qAction = nullptr; - QAction *wideAction = nullptr; -}; - -WideBar::~WideBar() -{ - for(auto *iter: m_entries) { - delete iter; - } + setContextMenuPolicy(Qt::ContextMenuPolicy::CustomContextMenu); + connect(this, &QToolBar::customContextMenuRequested, this, &WideBar::showVisibilityMenu); } void WideBar::addAction(QAction* action) { - auto entry = new BarEntry(); - entry->qAction = addWidget(new ActionButton(action, this)); - entry->wideAction = action; - entry->type = BarEntry::Action; + BarEntry entry; + entry.bar_action = addWidget(new ActionButton(action, this)); + entry.menu_action = action; + entry.type = BarEntry::Type::Action; + m_entries.push_back(entry); + + m_menu_state = MenuState::Dirty; } void WideBar::addSeparator() { - auto entry = new BarEntry(); - entry->qAction = QToolBar::addSeparator(); - entry->type = BarEntry::Separator; + BarEntry entry; + entry.bar_action = QToolBar::addSeparator(); + entry.type = BarEntry::Type::Separator; + m_entries.push_back(entry); } -auto WideBar::getMatching(QAction* act) -> QList<BarEntry*>::iterator +auto WideBar::getMatching(QAction* act) -> QList<BarEntry>::iterator { - auto iter = std::find_if(m_entries.begin(), m_entries.end(), [act](BarEntry * entry) { - return entry->wideAction == act; - }); - + auto iter = std::find_if(m_entries.begin(), m_entries.end(), [act](BarEntry const& entry) { return entry.menu_action == act; }); + return iter; } -void WideBar::insertActionBefore(QAction* before, QAction* action){ +void WideBar::insertActionBefore(QAction* before, QAction* action) +{ auto iter = getMatching(before); - if(iter == m_entries.end()) + if (iter == m_entries.end()) return; - auto entry = new BarEntry(); - entry->qAction = insertWidget((*iter)->qAction, new ActionButton(action, this)); - entry->wideAction = action; - entry->type = BarEntry::Action; + BarEntry entry; + entry.bar_action = insertWidget(iter->bar_action, new ActionButton(action, this)); + entry.menu_action = action; + entry.type = BarEntry::Type::Action; + m_entries.insert(iter, entry); + + m_menu_state = MenuState::Dirty; } -void WideBar::insertActionAfter(QAction* after, QAction* action){ +void WideBar::insertActionAfter(QAction* after, QAction* action) +{ auto iter = getMatching(after); - if(iter == m_entries.end()) + if (iter == m_entries.end()) return; - auto entry = new BarEntry(); - entry->qAction = insertWidget((*(iter+1))->qAction, new ActionButton(action, this)); - entry->wideAction = action; - entry->type = BarEntry::Action; + BarEntry entry; + entry.bar_action = insertWidget((iter + 1)->bar_action, new ActionButton(action, this)); + entry.menu_action = action; + entry.type = BarEntry::Type::Action; + m_entries.insert(iter + 1, entry); + + m_menu_state = MenuState::Dirty; } void WideBar::insertSpacer(QAction* action) { auto iter = getMatching(action); - if(iter == m_entries.end()) + if (iter == m_entries.end()) return; - QWidget* spacer = new QWidget(); + auto* spacer = new QWidget(); spacer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); - auto entry = new BarEntry(); - entry->qAction = insertWidget((*iter)->qAction, spacer); - entry->type = BarEntry::Spacer; + BarEntry entry; + entry.bar_action = insertWidget(iter->bar_action, spacer); + entry.type = BarEntry::Type::Spacer; m_entries.insert(iter, entry); } void WideBar::insertSeparator(QAction* before) { auto iter = getMatching(before); - if(iter == m_entries.end()) + if (iter == m_entries.end()) return; - auto entry = new BarEntry(); - entry->qAction = QToolBar::insertSeparator(before); - entry->type = BarEntry::Separator; + BarEntry entry; + entry.bar_action = QToolBar::insertSeparator(before); + entry.type = BarEntry::Type::Separator; + m_entries.insert(iter, entry); } -QMenu * WideBar::createContextMenu(QWidget *parent, const QString & title) +QMenu* WideBar::createContextMenu(QWidget* parent, const QString& title) { - QMenu *contextMenu = new QMenu(title, parent); - for(auto & item: m_entries) { - switch(item->type) { + auto* contextMenu = new QMenu(title, parent); + for (auto& item : m_entries) { + switch (item.type) { default: - case BarEntry::None: + case BarEntry::Type::None: break; - case BarEntry::Separator: - case BarEntry::Spacer: + case BarEntry::Type::Separator: + case BarEntry::Type::Spacer: contextMenu->addSeparator(); break; - case BarEntry::Action: - contextMenu->addAction(item->wideAction); + case BarEntry::Type::Action: + contextMenu->addAction(item.menu_action); break; } } return contextMenu; } +static void copyAction(QAction* from, QAction* to) +{ + Q_ASSERT(from); + Q_ASSERT(to); + + to->setText(from->text()); + to->setIcon(from->icon()); + to->setToolTip(from->toolTip()); +} + +void WideBar::showVisibilityMenu(QPoint const& position) +{ + if (!m_bar_menu) + m_bar_menu = std::make_unique<QMenu>(this); + + if (m_menu_state == MenuState::Dirty) { + for (auto* old_action : m_bar_menu->actions()) + old_action->deleteLater(); + + m_bar_menu->clear(); + + for (auto& entry : m_entries) { + if (entry.type != BarEntry::Type::Action) + continue; + + auto act = new QAction(); + copyAction(entry.menu_action, act); + + act->setCheckable(true); + act->setChecked(entry.bar_action->isVisible()); + + connect(act, &QAction::toggled, entry.bar_action, [this, &entry](bool toggled){ + entry.bar_action->setVisible(toggled); + + // NOTE: This is needed so that disabled actions get reflected on the button when it is made visible. + static_cast<ActionButton*>(widgetForAction(entry.bar_action))->actionChanged(); + }); + + m_bar_menu->addAction(act); + } + + m_menu_state = MenuState::Fresh; + } + + m_bar_menu->popup(mapToGlobal(position)); +} + +[[nodiscard]] QByteArray WideBar::getVisibilityState() const +{ + QByteArray state; + + for (auto const& entry : m_entries) { + if (entry.type != BarEntry::Type::Action) + continue; + + state.append(entry.bar_action->isVisible() ? '1' : '0'); + } + + state.append(','); + state.append(getHash()); + + return state; +} + +void WideBar::setVisibilityState(QByteArray&& state) +{ + auto split = state.split(','); + + auto bits = split.first(); + auto hash = split.last(); + + // If the actions changed, we better not try to load the old one to avoid unwanted hiding + if (!checkHash(hash)) + return; + + qsizetype i = 0; + for (auto& entry : m_entries) { + if (entry.type != BarEntry::Type::Action) + continue; + if (i == bits.size()) + break; + + entry.bar_action->setVisible(bits.at(i++) == '1'); + + // NOTE: This is needed so that disabled actions get reflected on the button when it is made visible. + static_cast<ActionButton*>(widgetForAction(entry.bar_action))->actionChanged(); + } +} + +QByteArray WideBar::getHash() const +{ + QCryptographicHash hash(QCryptographicHash::Sha1); + for (auto const& entry : m_entries) { + if (entry.type != BarEntry::Type::Action) + continue; + hash.addData(entry.menu_action->text().toLatin1()); + } + + return hash.result().toBase64(); +} + +bool WideBar::checkHash(QByteArray const& old_hash) const +{ + return old_hash == getHash(); +} + + #include "WideBar.moc" diff --git a/launcher/ui/widgets/WideBar.h b/launcher/ui/widgets/WideBar.h index 8ff62ef2..a0a7896c 100644 --- a/launcher/ui/widgets/WideBar.h +++ b/launcher/ui/widgets/WideBar.h @@ -2,9 +2,10 @@ #include <QAction> #include <QMap> +#include <QMenu> #include <QToolBar> -class QMenu; +#include <memory> class WideBar : public QToolBar { Q_OBJECT @@ -12,7 +13,7 @@ class WideBar : public QToolBar { public: explicit WideBar(const QString& title, QWidget* parent = nullptr); explicit WideBar(QWidget* parent = nullptr); - virtual ~WideBar(); + ~WideBar() override = default; void addAction(QAction* action); void addSeparator(); @@ -23,12 +24,31 @@ class WideBar : public QToolBar { void insertActionAfter(QAction* after, QAction* action); QMenu* createContextMenu(QWidget* parent = nullptr, const QString& title = QString()); + void showVisibilityMenu(const QPoint&); + + // Ideally we would use a QBitArray for this, but it doesn't support string conversion, + // so using it in settings is very messy. + + [[nodiscard]] QByteArray getVisibilityState() const; + void setVisibilityState(QByteArray&&); private: - struct BarEntry; + struct BarEntry { + enum class Type { None, Action, Separator, Spacer } type = Type::None; + QAction* bar_action = nullptr; + QAction* menu_action = nullptr; + }; - auto getMatching(QAction* act) -> QList<BarEntry*>::iterator; + auto getMatching(QAction* act) -> QList<BarEntry>::iterator; + + /** Used to distinguish between versions of the WideBar with different actions */ + [[nodiscard]] QByteArray getHash() const; + [[nodiscard]] bool checkHash(QByteArray const&) const; private: - QList<BarEntry*> m_entries; + QList<BarEntry> m_entries; + + // Menu to toggle visibility from buttons in the bar + std::unique_ptr<QMenu> m_bar_menu = nullptr; + enum class MenuState { Fresh, Dirty } m_menu_state = MenuState::Dirty; }; |