Merge pull request #1052 from flowln/resource_model

This commit is contained in:
Sefa Eyeoglu 2022-08-28 16:52:53 +02:00 committed by GitHub
commit f371ec210c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
46 changed files with 2080 additions and 1237 deletions

View File

@ -318,10 +318,16 @@ set(MINECRAFT_SOURCES
minecraft/mod/ModDetails.h
minecraft/mod/ModFolderModel.h
minecraft/mod/ModFolderModel.cpp
minecraft/mod/Resource.h
minecraft/mod/Resource.cpp
minecraft/mod/ResourceFolderModel.h
minecraft/mod/ResourceFolderModel.cpp
minecraft/mod/ResourcePackFolderModel.h
minecraft/mod/ResourcePackFolderModel.cpp
minecraft/mod/TexturePackFolderModel.h
minecraft/mod/TexturePackFolderModel.cpp
minecraft/mod/ShaderPackFolderModel.h
minecraft/mod/tasks/BasicFolderLoadTask.h
minecraft/mod/tasks/ModFolderLoadTask.h
minecraft/mod/tasks/ModFolderLoadTask.cpp
minecraft/mod/tasks/LocalModParseTask.h
@ -375,8 +381,8 @@ ecm_add_test(minecraft/Library_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VER
# FIXME: shares data with FileSystem test
# TODO: needs testdata
ecm_add_test(minecraft/mod/ModFolderModel_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test
TEST_NAME ModFolderModel)
ecm_add_test(minecraft/mod/ResourceFolderModel_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test
TEST_NAME ResourceFolderModel)
ecm_add_test(minecraft/ParseUtils_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test
TEST_NAME ParseUtils)
@ -881,8 +887,8 @@ SET(LAUNCHER_SOURCES
ui/widgets/LineSeparator.h
ui/widgets/LogView.cpp
ui/widgets/LogView.h
ui/widgets/MCModInfoFrame.cpp
ui/widgets/MCModInfoFrame.h
ui/widgets/InfoFrame.cpp
ui/widgets/InfoFrame.h
ui/widgets/ModFilterWidget.cpp
ui/widgets/ModFilterWidget.h
ui/widgets/ModListView.cpp
@ -944,7 +950,7 @@ qt_wrap_ui(LAUNCHER_UI
ui/pages/modplatform/technic/TechnicPage.ui
ui/widgets/InstanceCardWidget.ui
ui/widgets/CustomCommands.ui
ui/widgets/MCModInfoFrame.ui
ui/widgets/InfoFrame.ui
ui/widgets/ModFilterWidget.ui
ui/dialogs/CopyInstanceDialog.ui
ui/dialogs/ProfileSetupDialog.ui

View File

@ -37,9 +37,9 @@ public:
modsPage->setFilter("%1 (*.zip *.jar *.litemod)");
values.append(modsPage);
values.append(new CoreModFolderPage(onesix.get(), onesix->coreModList()));
values.append(new ResourcePackPage(onesix.get()));
values.append(new TexturePackPage(onesix.get()));
values.append(new ShaderPackPage(onesix.get()));
values.append(new ResourcePackPage(onesix.get(), onesix->resourcePackList()));
values.append(new TexturePackPage(onesix.get(), onesix->texturePackList()));
values.append(new ShaderPackPage(onesix.get(), onesix->shaderPackList()));
values.append(new NotesPage(onesix.get()));
values.append(new WorldListPage(onesix.get(), onesix->worldList()));
values.append(new ServersPage(onesix));

View File

@ -148,7 +148,7 @@ bool MMCZip::createModdedJar(QString sourceJarPath, QString targetJarPath, const
// do not merge disabled mods.
if (!mod->enabled())
continue;
if (mod->type() == Mod::MOD_ZIPFILE)
if (mod->type() == ResourceType::ZIPFILE)
{
if (!mergeZipFiles(&zipOut, mod->fileinfo(), addedFiles))
{
@ -158,7 +158,7 @@ bool MMCZip::createModdedJar(QString sourceJarPath, QString targetJarPath, const
return false;
}
}
else if (mod->type() == Mod::MOD_SINGLEFILE)
else if (mod->type() == ResourceType::SINGLEFILE)
{
// FIXME: buggy - does not work with addedFiles
auto filename = mod->fileinfo();
@ -171,7 +171,7 @@ bool MMCZip::createModdedJar(QString sourceJarPath, QString targetJarPath, const
}
addedFiles.insert(filename.fileName());
}
else if (mod->type() == Mod::MOD_FOLDER)
else if (mod->type() == ResourceType::FOLDER)
{
// untested, but seems to be unused / not possible to reach
// FIXME: buggy - does not work with addedFiles

View File

@ -76,6 +76,7 @@
#include "mod/ModFolderModel.h"
#include "mod/ResourcePackFolderModel.h"
#include "mod/ShaderPackFolderModel.h"
#include "mod/TexturePackFolderModel.h"
#include "WorldList.h"
@ -714,7 +715,7 @@ QStringList MinecraftInstance::verboseDescription(AuthSessionPtr session, Minecr
});
for(auto mod: modList)
{
if(mod->type() == Mod::MOD_FOLDER)
if(mod->type() == ResourceType::FOLDER)
{
out << u8" [🖿] " + mod->fileinfo().completeBaseName() + " (folder)";
continue;
@ -1092,18 +1093,18 @@ std::shared_ptr<ModFolderModel> MinecraftInstance::coreModList() const
return m_core_mod_list;
}
std::shared_ptr<ModFolderModel> MinecraftInstance::resourcePackList() const
std::shared_ptr<ResourcePackFolderModel> MinecraftInstance::resourcePackList() const
{
if (!m_resource_pack_list)
{
m_resource_pack_list.reset(new ResourcePackFolderModel(resourcePacksDir()));
m_resource_pack_list->disableInteraction(isRunning());
connect(this, &BaseInstance::runningStatusChanged, m_resource_pack_list.get(), &ModFolderModel::disableInteraction);
m_resource_pack_list->enableInteraction(!isRunning());
connect(this, &BaseInstance::runningStatusChanged, m_resource_pack_list.get(), &ResourcePackFolderModel::disableInteraction);
}
return m_resource_pack_list;
}
std::shared_ptr<ModFolderModel> MinecraftInstance::texturePackList() const
std::shared_ptr<TexturePackFolderModel> MinecraftInstance::texturePackList() const
{
if (!m_texture_pack_list)
{
@ -1114,11 +1115,11 @@ std::shared_ptr<ModFolderModel> MinecraftInstance::texturePackList() const
return m_texture_pack_list;
}
std::shared_ptr<ModFolderModel> MinecraftInstance::shaderPackList() const
std::shared_ptr<ShaderPackFolderModel> MinecraftInstance::shaderPackList() const
{
if (!m_shader_pack_list)
{
m_shader_pack_list.reset(new ResourcePackFolderModel(shaderPacksDir()));
m_shader_pack_list.reset(new ShaderPackFolderModel(shaderPacksDir()));
m_shader_pack_list->disableInteraction(isRunning());
connect(this, &BaseInstance::runningStatusChanged, m_shader_pack_list.get(), &ModFolderModel::disableInteraction);
}

View File

@ -7,6 +7,10 @@
#include "minecraft/launch/MinecraftServerTarget.h"
class ModFolderModel;
class ResourceFolderModel;
class ResourcePackFolderModel;
class ShaderPackFolderModel;
class TexturePackFolderModel;
class WorldList;
class GameOptions;
class LaunchStep;
@ -72,9 +76,9 @@ public:
////// Mod Lists //////
std::shared_ptr<ModFolderModel> loaderModList() const;
std::shared_ptr<ModFolderModel> coreModList() const;
std::shared_ptr<ModFolderModel> resourcePackList() const;
std::shared_ptr<ModFolderModel> texturePackList() const;
std::shared_ptr<ModFolderModel> shaderPackList() const;
std::shared_ptr<ResourcePackFolderModel> resourcePackList() const;
std::shared_ptr<TexturePackFolderModel> texturePackList() const;
std::shared_ptr<ShaderPackFolderModel> shaderPackList() const;
std::shared_ptr<WorldList> worldList() const;
std::shared_ptr<GameOptions> gameOptionsModel() const;
@ -125,9 +129,9 @@ protected: // data
std::shared_ptr<PackProfile> m_components;
mutable std::shared_ptr<ModFolderModel> m_loader_mod_list;
mutable std::shared_ptr<ModFolderModel> m_core_mod_list;
mutable std::shared_ptr<ModFolderModel> m_resource_pack_list;
mutable std::shared_ptr<ModFolderModel> m_shader_pack_list;
mutable std::shared_ptr<ModFolderModel> m_texture_pack_list;
mutable std::shared_ptr<ResourcePackFolderModel> m_resource_pack_list;
mutable std::shared_ptr<ShaderPackFolderModel> m_shader_pack_list;
mutable std::shared_ptr<TexturePackFolderModel> m_texture_pack_list;
mutable std::shared_ptr<WorldList> m_world_list;
mutable std::shared_ptr<GameOptions> m_game_options;
};

View File

@ -36,130 +36,77 @@
#include "Mod.h"
#include <QDebug>
#include <QDir>
#include <QString>
#include <QRegularExpression>
#include <FileSystem.h>
#include <QDebug>
#include "Application.h"
#include "MetadataHandler.h"
#include "Version.h"
namespace {
ModDetails invalidDetails;
}
Mod::Mod(const QFileInfo& file)
Mod::Mod(const QFileInfo& file) : Resource(file), m_local_details()
{
repath(file);
m_changedDateTime = file.lastModified();
m_enabled = (file.suffix() != "disabled");
}
Mod::Mod(const QDir& mods_dir, const Metadata::ModStruct& metadata)
: m_file(mods_dir.absoluteFilePath(metadata.filename))
, m_internal_id(metadata.filename)
, m_name(metadata.name)
: Mod(mods_dir.absoluteFilePath(metadata.filename))
{
if (m_file.isDir()) {
m_type = MOD_FOLDER;
} else {
if (metadata.filename.endsWith(".zip") || metadata.filename.endsWith(".jar"))
m_type = MOD_ZIPFILE;
else if (metadata.filename.endsWith(".litemod"))
m_type = MOD_LITEMOD;
else
m_type = MOD_SINGLEFILE;
}
m_enabled = true;
m_changedDateTime = m_file.lastModified();
m_temp_metadata = std::make_shared<Metadata::ModStruct>(std::move(metadata));
}
void Mod::repath(const QFileInfo& file)
{
m_file = file;
QString name_base = file.fileName();
m_type = Mod::MOD_UNKNOWN;
m_internal_id = name_base;
if (m_file.isDir()) {
m_type = MOD_FOLDER;
m_name = name_base;
} else if (m_file.isFile()) {
if (name_base.endsWith(".disabled")) {
m_enabled = false;
name_base.chop(9);
} else {
m_enabled = true;
}
if (name_base.endsWith(".zip") || name_base.endsWith(".jar")) {
m_type = MOD_ZIPFILE;
name_base.chop(4);
} else if (name_base.endsWith(".litemod")) {
m_type = MOD_LITEMOD;
name_base.chop(8);
} else {
m_type = MOD_SINGLEFILE;
}
m_name = name_base;
}
}
auto Mod::enable(bool value) -> bool
{
if (m_type == Mod::MOD_UNKNOWN || m_type == Mod::MOD_FOLDER)
return false;
if (m_enabled == value)
return false;
QString path = m_file.absoluteFilePath();
QFile file(path);
if (value) {
if (!path.endsWith(".disabled"))
return false;
path.chop(9);
if (!file.rename(path))
return false;
} else {
path += ".disabled";
if (!file.rename(path))
return false;
}
if (status() == ModStatus::NoMetadata)
repath(QFileInfo(path));
m_enabled = value;
return true;
m_name = metadata.name;
m_local_details.metadata = std::make_shared<Metadata::ModStruct>(std::move(metadata));
}
void Mod::setStatus(ModStatus status)
{
if (m_localDetails) {
m_localDetails->status = status;
} else {
m_temp_status = status;
}
m_local_details.status = status;
}
void Mod::setMetadata(const Metadata::ModStruct& metadata)
void Mod::setMetadata(std::shared_ptr<Metadata::ModStruct>&& metadata)
{
if (status() == ModStatus::NoMetadata)
setStatus(ModStatus::Installed);
if (m_localDetails) {
m_localDetails->metadata = std::make_shared<Metadata::ModStruct>(std::move(metadata));
} else {
m_temp_metadata = std::make_shared<Metadata::ModStruct>(std::move(metadata));
m_local_details.metadata = metadata;
}
std::pair<int, bool> Mod::compare(const Resource& other, SortType type) const
{
auto cast_other = dynamic_cast<Mod const*>(&other);
if (!cast_other)
return Resource::compare(other, type);
switch (type) {
default:
case SortType::ENABLED:
case SortType::NAME:
case SortType::DATE: {
auto res = Resource::compare(other, type);
if (res.first != 0)
return res;
}
case SortType::VERSION: {
auto this_ver = Version(version());
auto other_ver = Version(cast_other->version());
if (this_ver > other_ver)
return { 1, type == SortType::VERSION };
if (this_ver < other_ver)
return { -1, type == SortType::VERSION };
}
}
return { 0, false };
}
bool Mod::applyFilter(QRegularExpression filter) const
{
if (filter.match(description()).hasMatch())
return true;
for (auto& author : authors()) {
if (filter.match(author).hasMatch()) {
return true;
}
}
return Resource::applyFilter(filter);
}
auto Mod::destroy(QDir& index_dir, bool preserve_metadata) -> bool
@ -175,13 +122,12 @@ auto Mod::destroy(QDir& index_dir, bool preserve_metadata) -> bool
}
}
m_type = MOD_UNKNOWN;
return FS::deletePath(m_file.filePath());
return Resource::destroy();
}
auto Mod::details() const -> const ModDetails&
{
return m_localDetails ? *m_localDetails : invalidDetails;
return m_local_details;
}
auto Mod::name() const -> QString
@ -218,35 +164,29 @@ auto Mod::authors() const -> QStringList
auto Mod::status() const -> ModStatus
{
if (!m_localDetails)
return m_temp_status;
return details().status;
}
auto Mod::metadata() -> std::shared_ptr<Metadata::ModStruct>
{
if (m_localDetails)
return m_localDetails->metadata;
return m_temp_metadata;
return m_local_details.metadata;
}
auto Mod::metadata() const -> const std::shared_ptr<Metadata::ModStruct>
{
if (m_localDetails)
return m_localDetails->metadata;
return m_temp_metadata;
return m_local_details.metadata;
}
void Mod::finishResolvingWithDetails(std::shared_ptr<ModDetails> details)
void Mod::finishResolvingWithDetails(ModDetails&& details)
{
m_resolving = false;
m_resolved = true;
m_localDetails = details;
m_is_resolving = false;
m_is_resolved = true;
setStatus(m_temp_status);
std::shared_ptr<Metadata::ModStruct> metadata = details.metadata;
if (details.status == ModStatus::Unknown)
details.status = m_local_details.status;
if (m_localDetails && m_temp_metadata && m_temp_metadata->isValid()) {
setMetadata(*m_temp_metadata);
m_temp_metadata.reset();
}
m_local_details = std::move(details);
if (metadata)
setMetadata(std::move(metadata));
}

View File

@ -39,38 +39,23 @@
#include <QFileInfo>
#include <QList>
#include "QObjectPtr.h"
#include "Resource.h"
#include "ModDetails.h"
class Mod : public QObject
class Mod : public Resource
{
Q_OBJECT
public:
enum ModType
{
MOD_UNKNOWN, //!< Indicates an unspecified mod type.
MOD_ZIPFILE, //!< The mod is a zip file containing the mod's class files.
MOD_SINGLEFILE, //!< The mod is a single file (not a zip file).
MOD_FOLDER, //!< The mod is in a folder on the filesystem.
MOD_LITEMOD, //!< The mod is a litemod
};
using Ptr = shared_qobject_ptr<Mod>;
using WeakPtr = QPointer<Mod>;
Mod() = default;
Mod(const QFileInfo &file);
explicit Mod(const QDir& mods_dir, const Metadata::ModStruct& metadata);
auto fileinfo() const -> QFileInfo { return m_file; }
auto dateTimeChanged() const -> QDateTime { return m_changedDateTime; }
auto internal_id() const -> QString { return m_internal_id; }
auto type() const -> ModType { return m_type; }
auto enabled() const -> bool { return m_enabled; }
auto valid() const -> bool { return m_type != MOD_UNKNOWN; }
Mod(const QDir& mods_dir, const Metadata::ModStruct& metadata);
Mod(QString file_path) : Mod(QFileInfo(file_path)) {}
auto details() const -> const ModDetails&;
auto name() const -> QString;
auto name() const -> QString override;
auto version() const -> QString;
auto homeurl() const -> QString;
auto description() const -> QString;
@ -81,46 +66,17 @@ public:
auto metadata() const -> const std::shared_ptr<Metadata::ModStruct>;
void setStatus(ModStatus status);
void setMetadata(const Metadata::ModStruct& metadata);
void setMetadata(std::shared_ptr<Metadata::ModStruct>&& metadata);
void setMetadata(const Metadata::ModStruct& metadata) { setMetadata(std::make_shared<Metadata::ModStruct>(metadata)); }
auto enable(bool value) -> bool;
[[nodiscard]] auto compare(Resource const& other, SortType type) const -> std::pair<int, bool> override;
[[nodiscard]] bool applyFilter(QRegularExpression filter) const override;
// delete all the files of this mod
// Delete all the files of this mod
auto destroy(QDir& index_dir, bool preserve_metadata = false) -> bool;
// change the mod's filesystem path (used by mod lists for *MAGIC* purposes)
void repath(const QFileInfo &file);
auto shouldResolve() const -> bool { return !m_resolving && !m_resolved; }
auto isResolving() const -> bool { return m_resolving; }
auto resolutionTicket() const -> int { return m_resolutionTicket; }
void setResolving(bool resolving, int resolutionTicket) {
m_resolving = resolving;
m_resolutionTicket = resolutionTicket;
}
void finishResolvingWithDetails(std::shared_ptr<ModDetails> details);
void finishResolvingWithDetails(ModDetails&& details);
protected:
QFileInfo m_file;
QDateTime m_changedDateTime;
QString m_internal_id;
/* Name as reported via the file name */
QString m_name;
ModType m_type = MOD_UNKNOWN;
/* If the mod has metadata, this will be filled in the constructor, and passed to
* the ModDetails when calling finishResolvingWithDetails */
std::shared_ptr<Metadata::ModStruct> m_temp_metadata;
/* Set the mod status while it doesn't have local details just yet */
ModStatus m_temp_status = ModStatus::NoMetadata;
std::shared_ptr<ModDetails> m_localDetails;
bool m_enabled = true;
bool m_resolving = false;
bool m_resolved = false;
int m_resolutionTicket = 0;
ModDetails m_local_details;
};

View File

@ -46,34 +46,77 @@ enum class ModStatus {
Installed, // Both JAR and Metadata are present
NotInstalled, // Only the Metadata is present
NoMetadata, // Only the JAR is present
Unknown, // Default status
};
struct ModDetails
{
/* Mod ID as defined in the ModLoader-specific metadata */
QString mod_id;
QString mod_id = {};
/* Human-readable name */
QString name;
QString name = {};
/* Human-readable mod version */
QString version;
QString version = {};
/* Human-readable minecraft version */
QString mcversion;
QString mcversion = {};
/* URL for mod's home page */
QString homeurl;
QString homeurl = {};
/* Human-readable description */
QString description;
QString description = {};
/* List of the author's names */
QStringList authors;
QStringList authors = {};
/* Installation status of the mod */
ModStatus status;
ModStatus status = ModStatus::Unknown;
/* Metadata information, if any */
std::shared_ptr<Metadata::ModStruct> metadata;
std::shared_ptr<Metadata::ModStruct> metadata = nullptr;
ModDetails() = default;
/** Metadata should be handled manually to properly set the mod status. */
ModDetails(ModDetails& other)
: mod_id(other.mod_id)
, name(other.name)
, version(other.version)
, mcversion(other.mcversion)
, homeurl(other.homeurl)
, description(other.description)
, authors(other.authors)
, status(other.status)
{}
ModDetails& operator=(ModDetails& other)
{
this->mod_id = other.mod_id;
this->name = other.name;
this->version = other.version;
this->mcversion = other.mcversion;
this->homeurl = other.homeurl;
this->description = other.description;
this->authors = other.authors;
this->status = other.status;
return *this;
}
ModDetails& operator=(ModDetails&& other)
{
this->mod_id = other.mod_id;
this->name = other.name;
this->version = other.version;
this->mcversion = other.mcversion;
this->homeurl = other.homeurl;
this->description = other.description;
this->authors = other.authors;
this->status = other.status;
return *this;
}
};

View File

@ -49,432 +49,53 @@
#include "minecraft/mod/tasks/LocalModParseTask.h"
#include "minecraft/mod/tasks/ModFolderLoadTask.h"
ModFolderModel::ModFolderModel(const QString &dir, bool is_indexed) : QAbstractListModel(), m_dir(dir), m_is_indexed(is_indexed)
ModFolderModel::ModFolderModel(const QString &dir, bool is_indexed) : ResourceFolderModel(QDir(dir)), m_is_indexed(is_indexed)
{
FS::ensureFolderPathExists(m_dir.absolutePath());
m_dir.setFilter(QDir::Readable | QDir::NoDotAndDotDot | QDir::Files | QDir::Dirs);
m_dir.setSorting(QDir::Name | QDir::IgnoreCase | QDir::LocaleAware);
m_watcher = new QFileSystemWatcher(this);
connect(m_watcher, SIGNAL(directoryChanged(QString)), this, SLOT(directoryChanged(QString)));
}
void ModFolderModel::startWatching()
{
if(is_watching)
return;
// Remove orphaned metadata next time
m_first_folder_load = true;
update();
// Watch the mods folder
is_watching = m_watcher->addPath(m_dir.absolutePath());
if (is_watching) {
qDebug() << "Started watching " << m_dir.absolutePath();
} else {
qDebug() << "Failed to start watching " << m_dir.absolutePath();
}
// Watch the mods index folder
is_watching = m_watcher->addPath(indexDir().absolutePath());
if (is_watching) {
qDebug() << "Started watching " << indexDir().absolutePath();
} else {
qDebug() << "Failed to start watching " << indexDir().absolutePath();
}
}
void ModFolderModel::stopWatching()
{
if(!is_watching)
return;
is_watching = !m_watcher->removePath(m_dir.absolutePath());
if (!is_watching) {
qDebug() << "Stopped watching " << m_dir.absolutePath();
} else {
qDebug() << "Failed to stop watching " << m_dir.absolutePath();
}
is_watching = !m_watcher->removePath(indexDir().absolutePath());
if (!is_watching) {
qDebug() << "Stopped watching " << indexDir().absolutePath();
} else {
qDebug() << "Failed to stop watching " << indexDir().absolutePath();
}
}
bool ModFolderModel::update()
{
if (!isValid()) {
return false;
}
if(m_update) {
scheduled_update = true;
return true;
}
auto index_dir = indexDir();
auto task = new ModFolderLoadTask(dir(), index_dir, m_is_indexed, m_first_folder_load);
m_first_folder_load = false;
m_update = task->result();
QThreadPool *threadPool = QThreadPool::globalInstance();
connect(task, &ModFolderLoadTask::succeeded, this, &ModFolderModel::finishUpdate);
threadPool->start(task);
return true;
}
void ModFolderModel::finishUpdate()
{
#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
auto currentList = modsIndex.keys();
QSet<QString> currentSet(currentList.begin(), currentList.end());
auto & newMods = m_update->mods;
auto newList = newMods.keys();
QSet<QString> newSet(newList.begin(), newList.end());
#else
QSet<QString> currentSet = modsIndex.keys().toSet();
auto& newMods = m_update->mods;
QSet<QString> newSet = newMods.keys().toSet();
#endif
// see if the kept mods changed in some way
{
QSet<QString> kept = currentSet;
kept.intersect(newSet);
for(auto& keptMod : kept) {
auto newMod = newMods[keptMod];
auto row = modsIndex[keptMod];
auto currentMod = mods[row];
if(newMod->dateTimeChanged() == currentMod->dateTimeChanged()) {
// no significant change, ignore...
continue;
}
auto oldMod = mods[row];
if(oldMod->isResolving()) {
activeTickets.remove(oldMod->resolutionTicket());
}
mods[row] = newMod;
resolveMod(mods[row]);
emit dataChanged(index(row, 0), index(row, columnCount(QModelIndex()) - 1));
}
}
// remove mods no longer present
{
QSet<QString> removed = currentSet;
QList<int> removedRows;
removed.subtract(newSet);
for(auto & removedMod: removed) {
removedRows.append(modsIndex[removedMod]);
}
std::sort(removedRows.begin(), removedRows.end(), std::greater<int>());
for(auto iter = removedRows.begin(); iter != removedRows.end(); iter++) {
int removedIndex = *iter;
beginRemoveRows(QModelIndex(), removedIndex, removedIndex);
auto removedIter = mods.begin() + removedIndex;
if((*removedIter)->isResolving()) {
activeTickets.remove((*removedIter)->resolutionTicket());
}
mods.erase(removedIter);
endRemoveRows();
}
}
// add new mods to the end
{
QSet<QString> added = newSet;
added.subtract(currentSet);
// When you have a Qt build with assertions turned on, proceeding here will abort the application
if (added.size() > 0) {
beginInsertRows(QModelIndex(), mods.size(), mods.size() + added.size() - 1);
for (auto& addedMod : added) {
mods.append(newMods[addedMod]);
resolveMod(mods.last());
}
endInsertRows();
}
}
// update index
{
modsIndex.clear();
int idx = 0;
for(auto mod: mods) {
modsIndex[mod->internal_id()] = idx;
idx++;
}
}
m_update.reset();
emit updateFinished();
if(scheduled_update) {
scheduled_update = false;
update();
}
}
void ModFolderModel::resolveMod(Mod::Ptr m)
{
if(!m->shouldResolve()) {
return;
}
auto task = new LocalModParseTask(nextResolutionTicket, m->type(), m->fileinfo());
auto result = task->result();
result->id = m->internal_id();
activeTickets.insert(nextResolutionTicket, result);
m->setResolving(true, nextResolutionTicket);
nextResolutionTicket++;
QThreadPool *threadPool = QThreadPool::globalInstance();
connect(task, &LocalModParseTask::finished, this, &ModFolderModel::finishModParse);
threadPool->start(task);
}
void ModFolderModel::finishModParse(int token)
{
auto iter = activeTickets.find(token);
if(iter == activeTickets.end()) {
return;
}
auto result = *iter;
activeTickets.remove(token);
int row = modsIndex[result->id];
auto mod = mods[row];
mod->finishResolvingWithDetails(result->details);
emit dataChanged(index(row), index(row, columnCount(QModelIndex()) - 1));
}
void ModFolderModel::disableInteraction(bool disabled)
{
if (interaction_disabled == disabled) {
return;
}
interaction_disabled = disabled;
if(size()) {
emit dataChanged(index(0), index(size() - 1));
}
}
void ModFolderModel::directoryChanged(QString path)
{
update();
}
bool ModFolderModel::isValid()
{
return m_dir.exists() && m_dir.isReadable();
}
auto ModFolderModel::selectedMods(QModelIndexList& indexes) -> QList<Mod::Ptr>
{
QList<Mod::Ptr> selected_mods;
for (auto i : indexes) {
if(i.column() != 0)
continue;
selected_mods.push_back(mods[i.row()]);
}
return selected_mods;
}
// FIXME: this does not take disabled mod (with extra .disable extension) into account...
bool ModFolderModel::installMod(const QString &filename)
{
if(interaction_disabled) {
return false;
}
// NOTE: fix for GH-1178: remove trailing slash to avoid issues with using the empty result of QFileInfo::fileName
auto originalPath = FS::NormalizePath(filename);
QFileInfo fileinfo(originalPath);
if (!fileinfo.exists() || !fileinfo.isReadable())
{
qWarning() << "Caught attempt to install non-existing file or file-like object:" << originalPath;
return false;
}
qDebug() << "installing: " << fileinfo.absoluteFilePath();
Mod installedMod(fileinfo);
if (!installedMod.valid())
{
qDebug() << originalPath << "is not a valid mod. Ignoring it.";
return false;
}
auto type = installedMod.type();
if (type == Mod::MOD_UNKNOWN)
{
qDebug() << "Cannot recognize mod type of" << originalPath << ", ignoring it.";
return false;
}
auto newpath = FS::NormalizePath(FS::PathCombine(m_dir.path(), fileinfo.fileName()));
if(originalPath == newpath)
{
qDebug() << "Overwriting the mod (" << originalPath << ") with itself makes no sense...";
return false;
}
if (type == Mod::MOD_SINGLEFILE || type == Mod::MOD_ZIPFILE || type == Mod::MOD_LITEMOD)
{
if(QFile::exists(newpath) || QFile::exists(newpath + QString(".disabled")))
{
if(!QFile::remove(newpath))
{
// FIXME: report error in a user-visible way
qWarning() << "Copy from" << originalPath << "to" << newpath << "has failed.";
return false;
}
qDebug() << newpath << "has been deleted.";
}
if (!QFile::copy(fileinfo.filePath(), newpath))
{
qWarning() << "Copy from" << originalPath << "to" << newpath << "has failed.";
// FIXME: report error in a user-visible way
return false;
}
FS::updateTimestamp(newpath);
QFileInfo newpathInfo(newpath);
installedMod.repath(newpathInfo);
update();
return true;
}
else if (type == Mod::MOD_FOLDER)
{
QString from = fileinfo.filePath();
if(QFile::exists(newpath))
{
qDebug() << "Ignoring folder " << from << ", it would merge with " << newpath;
return false;
}
if (!FS::copy(from, newpath)())
{
qWarning() << "Copy of folder from" << originalPath << "to" << newpath << "has (potentially partially) failed.";
return false;
}
QFileInfo newpathInfo(newpath);
installedMod.repath(newpathInfo);
update();
return true;
}
return false;
}
bool ModFolderModel::uninstallMod(const QString& filename, bool preserve_metadata)
{
for(auto mod : allMods()){
if(mod->fileinfo().fileName() == filename){
auto index_dir = indexDir();
mod->destroy(index_dir, preserve_metadata);
return true;
}
}
return false;
}
bool ModFolderModel::setModStatus(const QModelIndexList& indexes, ModStatusAction enable)
{
if(interaction_disabled) {
return false;
}
if(indexes.isEmpty())
return true;
for (auto index: indexes)
{
if(index.column() != 0) {
continue;
}
setModStatus(index.row(), enable);
}
return true;
}
bool ModFolderModel::deleteMods(const QModelIndexList& indexes)
{
if(interaction_disabled) {
return false;
}
if(indexes.isEmpty())
return true;
for (auto i: indexes)
{
if(i.column() != 0) {
continue;
}
auto m = mods[i.row()];
auto index_dir = indexDir();
m->destroy(index_dir);
}
return true;
}
int ModFolderModel::columnCount(const QModelIndex &parent) const
{
return NUM_COLUMNS;
m_column_sort_keys = { SortType::ENABLED, SortType::NAME, SortType::VERSION, SortType::DATE };
}
QVariant ModFolderModel::data(const QModelIndex &index, int role) const
{
if (!index.isValid())
return QVariant();
if (!validateIndex(index))
return {};
int row = index.row();
int column = index.column();
if (row < 0 || row >= mods.size())
return QVariant();
switch (role)
{
case Qt::DisplayRole:
switch (column)
{
case NameColumn:
return mods[row]->name();
return m_resources[row]->name();
case VersionColumn: {
switch(mods[row]->type()) {
case Mod::MOD_FOLDER:
switch(m_resources[row]->type()) {
case ResourceType::FOLDER:
return tr("Folder");
case Mod::MOD_SINGLEFILE:
case ResourceType::SINGLEFILE:
return tr("File");
default:
break;
}
return mods[row]->version();
return at(row)->version();
}
case DateColumn:
return mods[row]->dateTimeChanged();
return m_resources[row]->dateTimeChanged();
default:
return QVariant();
}
case Qt::ToolTipRole:
return mods[row]->internal_id();
return m_resources[row]->internal_id();
case Qt::CheckStateRole:
switch (column)
{
case ActiveColumn:
return mods[row]->enabled() ? Qt::Checked : Qt::Unchecked;
return at(row)->enabled() ? Qt::Checked : Qt::Unchecked;
default:
return QVariant();
}
@ -483,61 +104,6 @@ QVariant ModFolderModel::data(const QModelIndex &index, int role) const
}
}
bool ModFolderModel::setData(const QModelIndex &index, const QVariant &value, int role)
{
if (index.row() < 0 || index.row() >= rowCount(index) || !index.isValid())
{
return false;
}
if (role == Qt::CheckStateRole)
{
return setModStatus(index.row(), Toggle);
}
return false;
}
bool ModFolderModel::setModStatus(int row, ModFolderModel::ModStatusAction action)
{
if(row < 0 || row >= mods.size()) {
return false;
}
auto &mod = mods[row];
bool desiredStatus;
switch(action) {
case Enable:
desiredStatus = true;
break;
case Disable:
desiredStatus = false;
break;
case Toggle:
default:
desiredStatus = !mod->enabled();
break;
}
if(desiredStatus == mod->enabled()) {
return true;
}
// preserve the row, but change its ID
auto oldId = mod->internal_id();
if(!mod->enable(!mod->enabled())) {
return false;
}
auto newId = mod->internal_id();
if(modsIndex.contains(newId)) {
// NOTE: this could handle a corner case, where we are overwriting a file, because the same 'mod' exists both enabled and disabled
// But is it necessary?
}
modsIndex.remove(oldId);
modsIndex[newId] = row;
emit dataChanged(index(row, 0), index(row, columnCount(QModelIndex()) - 1));
return true;
}
QVariant ModFolderModel::headerData(int section, Qt::Orientation orientation, int role) const
{
switch (role)
@ -577,65 +143,151 @@ QVariant ModFolderModel::headerData(int section, Qt::Orientation orientation, in
return QVariant();
}
Qt::ItemFlags ModFolderModel::flags(const QModelIndex &index) const
int ModFolderModel::columnCount(const QModelIndex &parent) const
{
Qt::ItemFlags defaultFlags = QAbstractListModel::flags(index);
auto flags = defaultFlags;
if(interaction_disabled) {
flags &= ~Qt::ItemIsDropEnabled;
}
else
{
flags |= Qt::ItemIsDropEnabled;
if(index.isValid()) {
flags |= Qt::ItemIsUserCheckable;
return NUM_COLUMNS;
}
Task* ModFolderModel::createUpdateTask()
{
auto index_dir = indexDir();
auto task = new ModFolderLoadTask(dir(), index_dir, m_is_indexed, m_first_folder_load, this);
m_first_folder_load = false;
return task;
}
Task* ModFolderModel::createParseTask(Resource const& resource)
{
return new LocalModParseTask(m_next_resolution_ticket, resource.type(), resource.fileinfo());
}
bool ModFolderModel::uninstallMod(const QString& filename, bool preserve_metadata)
{
for(auto mod : allMods()){
if(mod->fileinfo().fileName() == filename){
auto index_dir = indexDir();
mod->destroy(index_dir, preserve_metadata);
update();
return true;
}
}
return flags;
return false;
}
Qt::DropActions ModFolderModel::supportedDropActions() const
bool ModFolderModel::deleteMods(const QModelIndexList& indexes)
{
// copy from outside, move from within and other mod lists
return Qt::CopyAction | Qt::MoveAction;
}
QStringList ModFolderModel::mimeTypes() const
{
QStringList types;
types << "text/uri-list";
return types;
}
bool ModFolderModel::dropMimeData(const QMimeData* data, Qt::DropAction action, int, int, const QModelIndex&)
{
if (action == Qt::IgnoreAction)
{
return true;
}
// check if the action is supported
if (!data || !(action & supportedDropActions()))
{
if(!m_can_interact) {
return false;
}
// files dropped from outside?
if (data->hasUrls())
{
auto urls = data->urls();
for (auto url : urls)
{
// only local files may be dropped...
if (!url.isLocalFile())
{
continue;
}
// TODO: implement not only copy, but also move
// FIXME: handle errors here
installMod(url.toLocalFile());
}
if(indexes.isEmpty())
return true;
for (auto i: indexes)
{
if(i.column() != 0) {
continue;
}
auto m = at(i.row());
auto index_dir = indexDir();
m->destroy(index_dir);
}
return false;
update();
return true;
}
bool ModFolderModel::isValid()
{
return m_dir.exists() && m_dir.isReadable();
}
bool ModFolderModel::startWatching()
{
// Remove orphaned metadata next time
m_first_folder_load = true;
return ResourceFolderModel::startWatching({ m_dir.absolutePath(), indexDir().absolutePath() });
}
bool ModFolderModel::stopWatching()
{
return ResourceFolderModel::stopWatching({ m_dir.absolutePath(), indexDir().absolutePath() });
}
auto ModFolderModel::selectedMods(QModelIndexList& indexes) -> QList<Mod*>
{
QList<Mod*> selected_resources;
for (auto i : indexes) {
if(i.column() != 0)
continue;
selected_resources.push_back(at(i.row()));
}
return selected_resources;
}
auto ModFolderModel::allMods() -> QList<Mod*>
{
QList<Mod*> mods;
for (auto& res : m_resources) {
mods.append(static_cast<Mod*>(res.get()));
}
return mods;
}
void ModFolderModel::onUpdateSucceeded()
{
auto update_results = static_cast<ModFolderLoadTask*>(m_current_update_task.get())->result();
auto& new_mods = update_results->mods;
#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
auto current_list = m_resources_index.keys();
QSet<QString> current_set(current_list.begin(), current_list.end());
auto new_list = new_mods.keys();
QSet<QString> new_set(new_list.begin(), new_list.end());
#else
QSet<QString> current_set(m_resources_index.keys().toSet());
QSet<QString> new_set(new_mods.keys().toSet());
#endif
applyUpdates(current_set, new_set, new_mods);
m_current_update_task.reset();
if (m_scheduled_update) {
m_scheduled_update = false;
update();
} else {
emit updateFinished();
}
}
void ModFolderModel::onParseSucceeded(int ticket, QString mod_id)
{
auto iter = m_active_parse_tasks.constFind(ticket);
if (iter == m_active_parse_tasks.constEnd())
return;
int row = m_resources_index[mod_id];
auto parse_task = *iter;
auto cast_task = static_cast<LocalModParseTask*>(parse_task.get());
Q_ASSERT(cast_task->token() == ticket);
auto resource = find(mod_id);
auto result = cast_task->result();
if (result && resource)
resource->finishResolvingWithDetails(std::move(result->details));
emit dataChanged(index(row), index(row, columnCount(QModelIndex()) - 1));
}

View File

@ -44,6 +44,7 @@
#include <QAbstractListModel>
#include "Mod.h"
#include "ResourceFolderModel.h"
#include "minecraft/mod/tasks/ModFolderLoadTask.h"
#include "minecraft/mod/tasks/LocalModParseTask.h"
@ -56,7 +57,7 @@ class QFileSystemWatcher;
* A legacy mod list.
* Backed by a folder.
*/
class ModFolderModel : public QAbstractListModel
class ModFolderModel : public ResourceFolderModel
{
Q_OBJECT
public:
@ -75,106 +76,38 @@ public:
};
ModFolderModel(const QString &dir, bool is_indexed = false);
virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
virtual bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override;
Qt::DropActions supportedDropActions() const override;
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
/// flags, mostly to support drag&drop
virtual Qt::ItemFlags flags(const QModelIndex &index) const override;
QStringList mimeTypes() const override;
bool dropMimeData(const QMimeData * data, Qt::DropAction action, int row, int column, const QModelIndex & parent) override;
QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override;
int columnCount(const QModelIndex &parent) const override;
virtual int rowCount(const QModelIndex &) const override
{
return size();
}
virtual QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override;
virtual int columnCount(const QModelIndex &parent) const override;
size_t size() const
{
return mods.size();
}
;
bool empty() const
{
return size() == 0;
}
Mod& operator[](size_t index)
{
return *mods[index];
}
const Mod& at(size_t index) const
{
return *mods.at(index);
}
/// Reloads the mod list and returns true if the list changed.
bool update();
/**
* Adds the given mod to the list at the given index - if the list supports custom ordering
*/
bool installMod(const QString& filename);
[[nodiscard]] Task* createUpdateTask() override;
[[nodiscard]] Task* createParseTask(Resource const&) override;
bool installMod(QString file_path) { return ResourceFolderModel::installResource(file_path); }
bool uninstallMod(const QString& filename, bool preserve_metadata = false);
/// Deletes all the selected mods
bool deleteMods(const QModelIndexList &indexes);
/// Enable or disable listed mods
bool setModStatus(const QModelIndexList &indexes, ModStatusAction action);
void startWatching();
void stopWatching();
bool isValid();
QDir& dir()
{
return m_dir;
}
bool startWatching() override;
bool stopWatching() override;
QDir indexDir()
{
return { QString("%1/.index").arg(dir().absolutePath()) };
}
QDir indexDir() { return { QString("%1/.index").arg(dir().absolutePath()) }; }
const QList<Mod::Ptr>& allMods()
{
return mods;
}
auto selectedMods(QModelIndexList& indexes) -> QList<Mod*>;
auto allMods() -> QList<Mod*>;
auto selectedMods(QModelIndexList& indexes) -> QList<Mod::Ptr>;
public slots:
void disableInteraction(bool disabled);
RESOURCE_HELPERS(Mod)
private
slots:
void directoryChanged(QString path);
void finishUpdate();
void finishModParse(int token);
signals:
void updateFinished();
private:
void resolveMod(Mod::Ptr m);
bool setModStatus(int index, ModStatusAction action);
void onUpdateSucceeded() override;
void onParseSucceeded(int ticket, QString resource_id) override;
protected:
QFileSystemWatcher *m_watcher;
bool is_watching = false;
ModFolderLoadTask::ResultPtr m_update;
bool scheduled_update = false;
bool interaction_disabled = false;
QDir m_dir;
bool m_is_indexed;
bool m_first_folder_load = true;
QMap<QString, int> modsIndex;
QMap<int, LocalModParseTask::ResultPtr> activeTickets;
int nextResolutionTicket = 0;
QList<Mod::Ptr> mods;
};

View File

@ -1,92 +0,0 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* PolyMC - Minecraft Launcher
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* This file incorporates work covered by the following copyright and
* permission notice:
*
* Copyright 2013-2021 MultiMC Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include <QTest>
#include <QTemporaryDir>
#include "FileSystem.h"
#include "minecraft/mod/ModFolderModel.h"
class ModFolderModelTest : public QObject
{
Q_OBJECT
private
slots:
// test for GH-1178 - install a folder with files to a mod list
void test_1178()
{
// source
QString source = QFINDTESTDATA("testdata/test_folder");
// sanity check
QVERIFY(!source.endsWith('/'));
auto verify = [](QString path)
{
QDir target_dir(FS::PathCombine(path, "test_folder"));
QVERIFY(target_dir.entryList().contains("pack.mcmeta"));
QVERIFY(target_dir.entryList().contains("assets"));
};
// 1. test with no trailing /
{
QString folder = source;
QTemporaryDir tempDir;
QEventLoop loop;
ModFolderModel m(tempDir.path(), true);
connect(&m, &ModFolderModel::updateFinished, &loop, &QEventLoop::quit);
m.installMod(folder);
loop.exec();
verify(tempDir.path());
}
// 2. test with trailing /
{
QString folder = source + '/';
QTemporaryDir tempDir;
QEventLoop loop;
ModFolderModel m(tempDir.path(), true);
connect(&m, &ModFolderModel::updateFinished, &loop, &QEventLoop::quit);
m.installMod(folder);
loop.exec();
verify(tempDir.path());
}
}
};
QTEST_GUILESS_MAIN(ModFolderModelTest)
#include "ModFolderModel_test.moc"

View File

@ -0,0 +1,147 @@
#include "Resource.h"
#include <QRegularExpression>
#include "FileSystem.h"
Resource::Resource(QObject* parent) : QObject(parent) {}
Resource::Resource(QFileInfo file_info) : QObject()
{
setFile(file_info);
}
void Resource::setFile(QFileInfo file_info)
{
m_file_info = file_info;
parseFile();
}
void Resource::parseFile()
{
QString file_name{ m_file_info.fileName() };
m_type = ResourceType::UNKNOWN;
m_internal_id = file_name;
if (m_file_info.isDir()) {
m_type = ResourceType::FOLDER;
m_name = file_name;
} else if (m_file_info.isFile()) {
if (file_name.endsWith(".disabled")) {
file_name.chop(9);
m_enabled = false;
}
if (file_name.endsWith(".zip") || file_name.endsWith(".jar")) {
m_type = ResourceType::ZIPFILE;
file_name.chop(4);
} else if (file_name.endsWith(".litemod")) {
m_type = ResourceType::LITEMOD;
file_name.chop(8);
} else {
m_type = ResourceType::SINGLEFILE;
}
m_name = file_name;
}
m_changed_date_time = m_file_info.lastModified();
}
static void removeThePrefix(QString& string)
{
QRegularExpression regex(QStringLiteral("^(?:the|teh) +"), QRegularExpression::CaseInsensitiveOption);
string.remove(regex);
string = string.trimmed();
}
std::pair<int, bool> Resource::compare(const Resource& other, SortType type) const
{
switch (type) {
default:
case SortType::ENABLED:
if (enabled() && !other.enabled())
return { 1, type == SortType::ENABLED };
if (!enabled() && other.enabled())
return { -1, type == SortType::ENABLED };
case SortType::NAME: {
QString this_name{ name() };
QString other_name{ other.name() };
removeThePrefix(this_name);
removeThePrefix(other_name);
auto compare_result = QString::compare(this_name, other_name, Qt::CaseInsensitive);
if (compare_result != 0)
return { compare_result, type == SortType::NAME };
}
case SortType::DATE:
if (dateTimeChanged() > other.dateTimeChanged())
return { 1, type == SortType::DATE };
if (dateTimeChanged() < other.dateTimeChanged())
return { -1, type == SortType::DATE };
}
return { 0, false };
}
bool Resource::applyFilter(QRegularExpression filter) const
{
return filter.match(name()).hasMatch();
}
bool Resource::enable(EnableAction action)
{
if (m_type == ResourceType::UNKNOWN || m_type == ResourceType::FOLDER)
return false;
QString path = m_file_info.absoluteFilePath();
QFile file(path);
bool enable = true;
switch (action) {
case EnableAction::ENABLE:
enable = true;
break;
case EnableAction::DISABLE:
enable = false;
break;
case EnableAction::TOGGLE:
default:
enable = !enabled();
break;
}
if (m_enabled == enable)
return false;
if (enable) {
// m_enabled is false, but there's no '.disabled' suffix.
// TODO: Report error?
if (!path.endsWith(".disabled"))
return false;
path.chop(9);
if (!file.rename(path))
return false;
} else {
path += ".disabled";
if (!file.rename(path))
return false;
}
setFile(QFileInfo(path));
m_enabled = enable;
return true;
}
bool Resource::destroy()
{
m_type = ResourceType::UNKNOWN;
return FS::deletePath(m_file_info.filePath());
}

View File

@ -0,0 +1,115 @@
#pragma once
#include <QDateTime>
#include <QFileInfo>
#include <QObject>
#include <QPointer>
#include "QObjectPtr.h"
enum class ResourceType {
UNKNOWN, //!< Indicates an unspecified resource type.
ZIPFILE, //!< The resource is a zip file containing the resource's class files.
SINGLEFILE, //!< The resource is a single file (not a zip file).
FOLDER, //!< The resource is in a folder on the filesystem.
LITEMOD, //!< The resource is a litemod
};
enum class SortType {
NAME,
DATE,
VERSION,
ENABLED,
};
enum class EnableAction {
ENABLE,
DISABLE,
TOGGLE
};
/** General class for managed resources. It mirrors a file in disk, with some more info
* for display and house-keeping purposes.
*
* Subclass it to add additional data / behavior, such as Mods or Resource packs.
*/
class Resource : public QObject {
Q_OBJECT
Q_DISABLE_COPY(Resource)
public:
using Ptr = shared_qobject_ptr<Resource>;
using WeakPtr = QPointer<Resource>;
Resource(QObject* parent = nullptr);
Resource(QFileInfo file_info);
Resource(QString file_path) : Resource(QFileInfo(file_path)) {}
~Resource() override = default;
void setFile(QFileInfo file_info);
void parseFile();
[[nodiscard]] auto fileinfo() const -> QFileInfo { return m_file_info; }
[[nodiscard]] auto dateTimeChanged() const -> QDateTime { return m_changed_date_time; }
[[nodiscard]] auto internal_id() const -> QString { return m_internal_id; }
[[nodiscard]] auto type() const -> ResourceType { return m_type; }
[[nodiscard]] bool enabled() const { return m_enabled; }
[[nodiscard]] virtual auto name() const -> QString { return m_name; }
[[nodiscard]] virtual bool valid() const { return m_type != ResourceType::UNKNOWN; }
/** Compares two Resources, for sorting purposes, considering a ascending order, returning:
* > 0: 'this' comes after 'other'
* = 0: 'this' is equal to 'other'
* < 0: 'this' comes before 'other'
*
* The second argument in the pair is true if the sorting type that decided which one is greater was 'type'.
*/
[[nodiscard]] virtual auto compare(Resource const& other, SortType type = SortType::NAME) const -> std::pair<int, bool>;
/** Returns whether the given filter should filter out 'this' (false),
* or if such filter includes the Resource (true).
*/
[[nodiscard]] virtual bool applyFilter(QRegularExpression filter) const;
/** Changes the enabled property, according to 'action'.
*
* Returns whether a change was applied to the Resource's properties.
*/
bool enable(EnableAction action);
[[nodiscard]] auto shouldResolve() const -> bool { return !m_is_resolving && !m_is_resolved; }
[[nodiscard]] auto isResolving() const -> bool { return m_is_resolving; }
[[nodiscard]] auto resolutionTicket() const -> int { return m_resolution_ticket; }
void setResolving(bool resolving, int resolutionTicket)
{
m_is_resolving = resolving;
m_resolution_ticket = resolutionTicket;
}
// Delete all files of this resource.
bool destroy();
protected:
/* The file corresponding to this resource. */
QFileInfo m_file_info;
/* The cached date when this file was last changed. */
QDateTime m_changed_date_time;
/* Internal ID for internal purposes. Properties such as human-readability should not be assumed. */
QString m_internal_id;
/* Name as reported via the file name. In the absence of a better name, this is shown to the user. */
QString m_name;
/* The type of file we're dealing with. */
ResourceType m_type = ResourceType::UNKNOWN;
/* Whether the resource is enabled (e.g. shows up in the game) or not. */
bool m_enabled = true;
/* Used to keep trach of pending / concluded actions on the resource. */
bool m_is_resolving = false;
bool m_is_resolved = false;
int m_resolution_ticket = 0;
};

View File

@ -0,0 +1,522 @@
#include "ResourceFolderModel.h"
#include <QDebug>
#include <QMimeData>
#include <QThreadPool>
#include <QUrl>
#include "FileSystem.h"
#include "minecraft/mod/tasks/BasicFolderLoadTask.h"
#include "tasks/Task.h"
ResourceFolderModel::ResourceFolderModel(QDir dir, QObject* parent) : QAbstractListModel(parent), m_dir(dir), m_watcher(this)
{
m_dir.setFilter(QDir::Readable | QDir::NoDotAndDotDot | QDir::Files | QDir::Dirs);
m_dir.setSorting(QDir::Name | QDir::IgnoreCase | QDir::LocaleAware);
connect(&m_watcher, &QFileSystemWatcher::directoryChanged, this, &ResourceFolderModel::directoryChanged);
}
bool ResourceFolderModel::startWatching(const QStringList paths)
{
if (m_is_watching)
return false;
auto couldnt_be_watched = m_watcher.addPaths(paths);
for (auto path : paths) {
if (couldnt_be_watched.contains(path))
qDebug() << "Failed to start watching " << path;
else
qDebug() << "Started watching " << path;
}
update();
m_is_watching = !m_is_watching;
return m_is_watching;
}
bool ResourceFolderModel::stopWatching(const QStringList paths)
{
if (!m_is_watching)
return false;
auto couldnt_be_stopped = m_watcher.removePaths(paths);
for (auto path : paths) {
if (couldnt_be_stopped.contains(path))
qDebug() << "Failed to stop watching " << path;
else
qDebug() << "Stopped watching " << path;
}
m_is_watching = !m_is_watching;
return !m_is_watching;
}
bool ResourceFolderModel::installResource(QString original_path)
{
if (!m_can_interact) {
return false;
}
// NOTE: fix for GH-1178: remove trailing slash to avoid issues with using the empty result of QFileInfo::fileName
original_path = FS::NormalizePath(original_path);
QFileInfo file_info(original_path);
if (!file_info.exists() || !file_info.isReadable()) {
qWarning() << "Caught attempt to install non-existing file or file-like object:" << original_path;
return false;
}
qDebug() << "Installing: " << file_info.absoluteFilePath();
Resource resource(file_info);
if (!resource.valid()) {
qWarning() << original_path << "is not a valid resource. Ignoring it.";
return false;
}
auto new_path = FS::NormalizePath(m_dir.filePath(file_info.fileName()));
if (original_path == new_path) {
qWarning() << "Overwriting the mod (" << original_path << ") with itself makes no sense...";
return false;
}
switch (resource.type()) {
case ResourceType::SINGLEFILE:
case ResourceType::ZIPFILE:
case ResourceType::LITEMOD: {
if (QFile::exists(new_path) || QFile::exists(new_path + QString(".disabled"))) {
if (!QFile::remove(new_path)) {
qCritical() << "Cleaning up new location (" << new_path << ") was unsuccessful!";
return false;
}
qDebug() << new_path << "has been deleted.";
}
if (!QFile::copy(original_path, new_path)) {
qCritical() << "Copy from" << original_path << "to" << new_path << "has failed.";
return false;
}
FS::updateTimestamp(new_path);
QFileInfo new_path_file_info(new_path);
resource.setFile(new_path_file_info);
if (!m_is_watching)
return update();
return true;
}
case ResourceType::FOLDER: {
if (QFile::exists(new_path)) {
qDebug() << "Ignoring folder '" << original_path << "', it would merge with" << new_path;
return false;
}
if (!FS::copy(original_path, new_path)()) {
qWarning() << "Copy of folder from" << original_path << "to" << new_path << "has (potentially partially) failed.";
return false;
}
QFileInfo newpathInfo(new_path);
resource.setFile(newpathInfo);
if (!m_is_watching)
return update();
return true;
}
default:
break;
}
return false;
}
bool ResourceFolderModel::uninstallResource(QString file_name)
{
for (auto& resource : m_resources) {
if (resource->fileinfo().fileName() == file_name) {
auto res = resource->destroy();
update();
return res;
}
}
return false;
}
bool ResourceFolderModel::deleteResources(const QModelIndexList& indexes)
{
if (!m_can_interact)
return false;
if (indexes.isEmpty())
return true;
for (auto i : indexes) {
if (i.column() != 0) {
continue;
}
auto& resource = m_resources.at(i.row());
resource->destroy();
}
update();
return true;
}
bool ResourceFolderModel::setResourceEnabled(const QModelIndexList &indexes, EnableAction action)
{
if (!m_can_interact)
return false;
if (indexes.isEmpty())
return true;
bool succeeded = true;
for (auto const& idx : indexes) {
if (!validateIndex(idx) || idx.column() != 0)
continue;
int row = idx.row();
auto& resource = m_resources[row];
// Preserve the row, but change its ID
auto old_id = resource->internal_id();
if (!resource->enable(action)) {
succeeded = false;
continue;
}
auto new_id = resource->internal_id();
if (m_resources_index.contains(new_id)) {
// FIXME: https://github.com/PolyMC/PolyMC/issues/550
}
m_resources_index.remove(old_id);
m_resources_index[new_id] = row;
emit dataChanged(index(row, 0), index(row, columnCount(QModelIndex()) - 1));
}
return succeeded;
}
static QMutex s_update_task_mutex;
bool ResourceFolderModel::update()
{
// We hold a lock here to prevent race conditions on the m_current_update_task reset.
QMutexLocker lock(&s_update_task_mutex);
// Already updating, so we schedule a future update and return.
if (m_current_update_task) {
m_scheduled_update = true;
return false;
}
m_current_update_task.reset(createUpdateTask());
if (!m_current_update_task)
return false;
connect(m_current_update_task.get(), &Task::succeeded, this, &ResourceFolderModel::onUpdateSucceeded,
Qt::ConnectionType::QueuedConnection);
connect(m_current_update_task.get(), &Task::failed, this, &ResourceFolderModel::onUpdateFailed, Qt::ConnectionType::QueuedConnection);
auto* thread_pool = QThreadPool::globalInstance();
thread_pool->start(m_current_update_task.get());
return true;
}
void ResourceFolderModel::resolveResource(Resource::Ptr res)
{
if (!res->shouldResolve()) {
return;
}
auto task = createParseTask(*res);
if (!task)
return;
m_ticket_mutex.lock();
int ticket = m_next_resolution_ticket;
m_next_resolution_ticket += 1;
m_ticket_mutex.unlock();
res->setResolving(true, ticket);
m_active_parse_tasks.insert(ticket, task);
connect(
task, &Task::succeeded, this, [=] { onParseSucceeded(ticket, res->internal_id()); }, Qt::ConnectionType::QueuedConnection);
connect(
task, &Task::failed, this, [=] { onParseFailed(ticket, res->internal_id()); }, Qt::ConnectionType::QueuedConnection);
connect(
task, &Task::finished, this, [=] { m_active_parse_tasks.remove(ticket); }, Qt::ConnectionType::QueuedConnection);
auto* thread_pool = QThreadPool::globalInstance();
thread_pool->start(task);
}
void ResourceFolderModel::onUpdateSucceeded()
{
auto update_results = static_cast<BasicFolderLoadTask*>(m_current_update_task.get())->result();
auto& new_resources = update_results->resources;
#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
auto current_list = m_resources_index.keys();
QSet<QString> current_set(current_list.begin(), current_list.end());
auto new_list = new_resources.keys();
QSet<QString> new_set(new_list.begin(), new_list.end());
#else
QSet<QString> current_set(m_resources_index.keys().toSet());
QSet<QString> new_set(new_resources.keys().toSet());
#endif
applyUpdates(current_set, new_set, new_resources);
m_current_update_task.reset();
if (m_scheduled_update) {
m_scheduled_update = false;
update();
} else {
emit updateFinished();
}
}
void ResourceFolderModel::onParseSucceeded(int ticket, QString resource_id)
{
auto iter = m_active_parse_tasks.constFind(ticket);
if (iter == m_active_parse_tasks.constEnd())
return;
int row = m_resources_index[resource_id];
emit dataChanged(index(row), index(row, columnCount(QModelIndex()) - 1));
}
Task* ResourceFolderModel::createUpdateTask()
{
return new BasicFolderLoadTask(m_dir);
}
bool ResourceFolderModel::hasPendingParseTasks() const
{
return !m_active_parse_tasks.isEmpty();
}
void ResourceFolderModel::directoryChanged(QString path)
{
update();
}
Qt::DropActions ResourceFolderModel::supportedDropActions() const
{
// copy from outside, move from within and other resource lists
return Qt::CopyAction | Qt::MoveAction;
}
Qt::ItemFlags ResourceFolderModel::flags(const QModelIndex& index) const
{
Qt::ItemFlags defaultFlags = QAbstractListModel::flags(index);
auto flags = defaultFlags;
if (!m_can_interact) {
flags &= ~Qt::ItemIsDropEnabled;
} else {
flags |= Qt::ItemIsDropEnabled;
if (index.isValid()) {
flags |= Qt::ItemIsUserCheckable;
}
}
return flags;
}
QStringList ResourceFolderModel::mimeTypes() const
{
QStringList types;
types << "text/uri-list";
return types;
}
bool ResourceFolderModel::dropMimeData(const QMimeData* data, Qt::DropAction action, int, int, const QModelIndex&)
{
if (action == Qt::IgnoreAction) {
return true;
}
// check if the action is supported
if (!data || !(action & supportedDropActions())) {
return false;
}
// files dropped from outside?
if (data->hasUrls()) {
auto urls = data->urls();
for (auto url : urls) {
// only local files may be dropped...
if (!url.isLocalFile()) {
continue;
}
// TODO: implement not only copy, but also move
// FIXME: handle errors here
installResource(url.toLocalFile());
}
return true;
}
return false;
}
bool ResourceFolderModel::validateIndex(const QModelIndex& index) const
{
if (!index.isValid())
return false;
int row = index.row();
if (row < 0 || row >= m_resources.size())
return false;
return true;
}
QVariant ResourceFolderModel::data(const QModelIndex& index, int role) const
{
if (!validateIndex(index))
return {};
int row = index.row();
int column = index.column();
switch (role) {
case Qt::DisplayRole:
switch (column) {
case NAME_COLUMN:
return m_resources[row]->name();
case DATE_COLUMN:
return m_resources[row]->dateTimeChanged();
default:
return {};
}
case Qt::ToolTipRole:
return m_resources[row]->internal_id();
case Qt::CheckStateRole:
switch (column) {
case ACTIVE_COLUMN:
return m_resources[row]->enabled() ? Qt::Checked : Qt::Unchecked;
default:
return {};
}
default:
return {};
}
}
bool ResourceFolderModel::setData(const QModelIndex& index, const QVariant& value, int role)
{
int row = index.row();
if (row < 0 || row >= rowCount(index) || !index.isValid())
return false;
if (role == Qt::CheckStateRole)
return setResourceEnabled({ index }, EnableAction::TOGGLE);
return false;
}
QVariant ResourceFolderModel::headerData(int section, Qt::Orientation orientation, int role) const
{
switch (role) {
case Qt::DisplayRole:
switch (section) {
case NAME_COLUMN:
return tr("Name");
case DATE_COLUMN:
return tr("Last modified");
default:
return {};
}
case Qt::ToolTipRole: {
switch (section) {
case ACTIVE_COLUMN:
//: Here, resource is a generic term for external resources, like Mods, Resource Packs, Shader Packs, etc.
return tr("Is the resource enabled?");
case NAME_COLUMN:
//: Here, resource is a generic term for external resources, like Mods, Resource Packs, Shader Packs, etc.
return tr("The name of the resource.");
case DATE_COLUMN:
//: Here, resource is a generic term for external resources, like Mods, Resource Packs, Shader Packs, etc.
return tr("The date and time this resource was last changed (or added).");
default:
return {};
}
}
default:
break;
}
return {};
}
QSortFilterProxyModel* ResourceFolderModel::createFilterProxyModel(QObject* parent)
{
return new ProxyModel(parent);
}
SortType ResourceFolderModel::columnToSortKey(size_t column) const
{
Q_ASSERT(m_column_sort_keys.size() == columnCount());
return m_column_sort_keys.at(column);
}
void ResourceFolderModel::enableInteraction(bool enabled)
{
if (m_can_interact == enabled)
return;
m_can_interact = enabled;
if (size())
emit dataChanged(index(0), index(size() - 1));
}
/* Standard Proxy Model for createFilterProxyModel */
[[nodiscard]] bool ResourceFolderModel::ProxyModel::filterAcceptsRow(int source_row, const QModelIndex& source_parent) const
{
auto* model = qobject_cast<ResourceFolderModel*>(sourceModel());
if (!model)
return true;
const auto& resource = model->at(source_row);
return resource.applyFilter(filterRegularExpression());
}
[[nodiscard]] bool ResourceFolderModel::ProxyModel::lessThan(const QModelIndex& source_left, const QModelIndex& source_right) const
{
auto* model = qobject_cast<ResourceFolderModel*>(sourceModel());
if (!model || !source_left.isValid() || !source_right.isValid() || source_left.column() != source_right.column()) {
return QSortFilterProxyModel::lessThan(source_left, source_right);
}
// we are now guaranteed to have two valid indexes in the same column... we love the provided invariants unconditionally and
// proceed.
auto column_sort_key = model->columnToSortKey(source_left.column());
auto const& resource_left = model->at(source_left.row());
auto const& resource_right = model->at(source_right.row());
auto compare_result = resource_left.compare(resource_right, column_sort_key);
if (compare_result.first == 0)
return QSortFilterProxyModel::lessThan(source_left, source_right);
if (compare_result.second || sortOrder() != Qt::DescendingOrder)
return (compare_result.first < 0);
return (compare_result.first > 0);
}

View File

@ -0,0 +1,326 @@
#pragma once
#include <QAbstractListModel>
#include <QDir>
#include <QFileSystemWatcher>
#include <QMutex>
#include <QSet>
#include <QSortFilterProxyModel>
#include "Resource.h"
#include "tasks/Task.h"
class QSortFilterProxyModel;
/** A basic model for external resources.
*
* This model manages a list of resources. As such, external users of such resources do not own them,
* and the resource's lifetime is contingent on the model's lifetime.
*
* TODO: Make the resources unique pointers accessible through weak pointers.
*/
class ResourceFolderModel : public QAbstractListModel {
Q_OBJECT
public:
ResourceFolderModel(QDir, QObject* parent = nullptr);
/** Starts watching the paths for changes.
*
* Returns whether starting to watch all the paths was successful.
* If one or more fails, it returns false.
*/
bool startWatching(const QStringList paths);
/** Stops watching the paths for changes.
*
* Returns whether stopping to watch all the paths was successful.
* If one or more fails, it returns false.
*/
bool stopWatching(const QStringList paths);
/* Helper methods for subclasses, using a predetermined list of paths. */
virtual bool startWatching() { return startWatching({ m_dir.absolutePath() }); };
virtual bool stopWatching() { return stopWatching({ m_dir.absolutePath() }); };
/** Given a path in the system, install that resource, moving it to its place in the
* instance file hierarchy.
*
* Returns whether the installation was succcessful.
*/
virtual bool installResource(QString path);
/** Uninstall (i.e. remove all data about it) a resource, given its file name.
*
* Returns whether the removal was successful.
*/
virtual bool uninstallResource(QString file_name);
virtual bool deleteResources(const QModelIndexList&);
/** Applies the given 'action' to the resources in 'indexes'.
*
* Returns whether the action was successfully applied to all resources.
*/
virtual bool setResourceEnabled(const QModelIndexList& indexes, EnableAction action);
/** Creates a new update task and start it. Returns false if no update was done, like when an update is already underway. */
virtual bool update();
/** Creates a new parse task, if needed, for 'res' and start it.*/
virtual void resolveResource(Resource::Ptr res);
[[nodiscard]] size_t size() const { return m_resources.size(); };
[[nodiscard]] bool empty() const { return size() == 0; }
[[nodiscard]] Resource& at(int index) { return *m_resources.at(index); }
[[nodiscard]] Resource const& at(int index) const { return *m_resources.at(index); }
[[nodiscard]] QList<Resource::Ptr> const& all() const { return m_resources; }
[[nodiscard]] QDir const& dir() const { return m_dir; }
/** Checks whether there's any parse tasks being done.
*
* Since they can be quite expensive, and are usually done in a separate thread, if we were to destroy the model while having
* such tasks would introduce an undefined behavior, most likely resulting in a crash.
*/
[[nodiscard]] bool hasPendingParseTasks() const;
/* Qt behavior */
/* Basic columns */
enum Columns { ACTIVE_COLUMN = 0, NAME_COLUMN, DATE_COLUMN, NUM_COLUMNS };
[[nodiscard]] int rowCount(const QModelIndex& = {}) const override { return size(); }
[[nodiscard]] int columnCount(const QModelIndex& = {}) const override { return NUM_COLUMNS; };
[[nodiscard]] Qt::DropActions supportedDropActions() const override;
/// flags, mostly to support drag&drop
[[nodiscard]] Qt::ItemFlags flags(const QModelIndex& index) const override;
[[nodiscard]] QStringList mimeTypes() const override;
bool dropMimeData(const QMimeData* data, Qt::DropAction action, int row, int column, const QModelIndex& parent) override;
[[nodiscard]] bool validateIndex(const QModelIndex& index) const;
[[nodiscard]] QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override;
bool setData(const QModelIndex& index, const QVariant& value, int role = Qt::EditRole) override;
[[nodiscard]] QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override;
/** This creates a proxy model to filter / sort the model for a UI.
*
* The actual comparisons and filtering are done directly by the Resource, so to modify behavior go there instead!
*/
QSortFilterProxyModel* createFilterProxyModel(QObject* parent = nullptr);
[[nodiscard]] SortType columnToSortKey(size_t column) const;
class ProxyModel : public QSortFilterProxyModel {
public:
explicit ProxyModel(QObject* parent = nullptr) : QSortFilterProxyModel(parent) {}
protected:
[[nodiscard]] bool filterAcceptsRow(int source_row, const QModelIndex& source_parent) const override;
[[nodiscard]] bool lessThan(const QModelIndex& source_left, const QModelIndex& source_right) const override;
};
public slots:
void enableInteraction(bool enabled);
void disableInteraction(bool disabled) { enableInteraction(!disabled); }
signals:
void updateFinished();
protected:
/** This creates a new update task to be executed by update().
*
* The task should load and parse all resources necessary, and provide a way of accessing such results.
*
* This Task is normally executed when opening a page, so it shouldn't contain much heavy work.
* If such work is needed, try using it in the Task create by createParseTask() instead!
*/
[[nodiscard]] virtual Task* createUpdateTask();
/** This creates a new parse task to be executed by onUpdateSucceeded().
*
* This task should load and parse all heavy info needed by a resource, such as parsing a manifest. It gets executed
* in the background, so it slowly updates the UI as tasks get done.
*/
[[nodiscard]] virtual Task* createParseTask(Resource const&) { return nullptr; };
/** Standard implementation of the model update logic.
*
* It uses set operations to find differences between the current state and the updated state,
* to act only on those disparities.
*
* The implementation is at the end of this header.
*/
template <typename T>
void applyUpdates(QSet<QString>& current_set, QSet<QString>& new_set, QMap<QString, T>& new_resources);
protected slots:
void directoryChanged(QString);
/** Called when the update task is successful.
*
* This usually calls static_cast on the specific Task type returned by createUpdateTask,
* so care must be taken in such cases.
* TODO: Figure out a way to express this relationship better without templated classes (Q_OBJECT macro disallows that).
*/
virtual void onUpdateSucceeded();
virtual void onUpdateFailed() {}
/** Called when the parse task with the given ticket is successful.
*
* This is just a simple reference implementation. You probably want to override it with your own logic in a subclass
* if the resource is complex and has more stuff to parse.
*/
virtual void onParseSucceeded(int ticket, QString resource_id);
virtual void onParseFailed(int ticket, QString resource_id) {}
protected:
// Represents the relationship between a column's index (represented by the list index), and it's sorting key.
// As such, the order in with they appear is very important!
QList<SortType> m_column_sort_keys = { SortType::ENABLED, SortType::NAME, SortType::DATE };
bool m_can_interact = true;
QDir m_dir;
QFileSystemWatcher m_watcher;
bool m_is_watching = false;
Task::Ptr m_current_update_task = nullptr;
bool m_scheduled_update = false;
QList<Resource::Ptr> m_resources;
// Represents the relationship between a resource's internal ID and it's row position on the model.
QMap<QString, int> m_resources_index;
QMap<int, Task::Ptr> m_active_parse_tasks;
int m_next_resolution_ticket = 0;
QMutex m_ticket_mutex;
};
/* A macro to define useful functions to handle Resource* -> T* more easily on derived classes */
#define RESOURCE_HELPERS(T) \
[[nodiscard]] T* operator[](size_t index) \
{ \
return static_cast<T*>(m_resources[index].get()); \
} \
[[nodiscard]] T* at(size_t index) \
{ \
return static_cast<T*>(m_resources[index].get()); \
} \
[[nodiscard]] const T* at(size_t index) const \
{ \
return static_cast<const T*>(m_resources.at(index).get()); \
} \
[[nodiscard]] T* first() \
{ \
return static_cast<T*>(m_resources.first().get()); \
} \
[[nodiscard]] T* last() \
{ \
return static_cast<T*>(m_resources.last().get()); \
} \
[[nodiscard]] T* find(QString id) \
{ \
auto iter = std::find_if(m_resources.constBegin(), m_resources.constEnd(), \
[&](Resource::Ptr const& r) { return r->internal_id() == id; }); \
if (iter == m_resources.constEnd()) \
return nullptr; \
return static_cast<T*>((*iter).get()); \
}
/* Template definition to avoid some code duplication */
template <typename T>
void ResourceFolderModel::applyUpdates(QSet<QString>& current_set, QSet<QString>& new_set, QMap<QString, T>& new_resources)
{
// see if the kept resources changed in some way
{
QSet<QString> kept_set = current_set;
kept_set.intersect(new_set);
for (auto const& kept : kept_set) {
auto row_it = m_resources_index.constFind(kept);
Q_ASSERT(row_it != m_resources_index.constEnd());
auto row = row_it.value();
auto& new_resource = new_resources[kept];
auto const& current_resource = m_resources[row];
if (new_resource->dateTimeChanged() == current_resource->dateTimeChanged()) {
// no significant change, ignore...
continue;
}
// If the resource is resolving, but something about it changed, we don't want to
// continue the resolving.
if (current_resource->isResolving()) {
auto task = (*m_active_parse_tasks.find(current_resource->resolutionTicket())).get();
task->abort();
}
m_resources[row].reset(new_resource);
resolveResource(m_resources.at(row));
emit dataChanged(index(row, 0), index(row, columnCount(QModelIndex()) - 1));
}
}
// remove resources no longer present
{
QSet<QString> removed_set = current_set;
removed_set.subtract(new_set);
QList<int> removed_rows;
for (auto& removed : removed_set)
removed_rows.append(m_resources_index[removed]);
std::sort(removed_rows.begin(), removed_rows.end(), std::greater<int>());
for (auto& removed_index : removed_rows) {
auto removed_it = m_resources.begin() + removed_index;
Q_ASSERT(removed_it != m_resources.end());
Q_ASSERT(removed_set.contains(removed_it->get()->internal_id()));
if ((*removed_it)->isResolving()) {
auto task = (*m_active_parse_tasks.find((*removed_it)->resolutionTicket())).get();
task->abort();
}
beginRemoveRows(QModelIndex(), removed_index, removed_index);
m_resources.erase(removed_it);
endRemoveRows();
}
}
// add new resources to the end
{
QSet<QString> added_set = new_set;
added_set.subtract(current_set);
// When you have a Qt build with assertions turned on, proceeding here will abort the application
if (added_set.size() > 0) {
beginInsertRows(QModelIndex(), m_resources.size(), m_resources.size() + added_set.size() - 1);
for (auto& added : added_set) {
auto res = new_resources[added];
m_resources.append(res);
resolveResource(res);
}
endInsertRows();
}
}
// update index
{
m_resources_index.clear();
int idx = 0;
for (auto const& mod : m_resources) {
m_resources_index[mod->internal_id()] = idx;
idx++;
}
}
}

View File

@ -0,0 +1,275 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* PolyMC - Minecraft Launcher
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* This file incorporates work covered by the following copyright and
* permission notice:
*
* Copyright 2013-2021 MultiMC Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include <QTest>
#include <QTemporaryDir>
#include <QTimer>
#include "FileSystem.h"
#include "minecraft/mod/ModFolderModel.h"
#include "minecraft/mod/ResourceFolderModel.h"
#define EXEC_UPDATE_TASK(EXEC, VERIFY) \
QEventLoop loop; \
\
connect(&model, &ResourceFolderModel::updateFinished, &loop, &QEventLoop::quit); \
\
QTimer expire_timer; \
expire_timer.callOnTimeout(&loop, &QEventLoop::quit); \
expire_timer.setSingleShot(true); \
expire_timer.start(4000); \
\
VERIFY(EXEC); \
loop.exec(); \
\
QVERIFY2(expire_timer.isActive(), "Timer has expired. The update never finished."); \
expire_timer.stop(); \
\
disconnect(&model, nullptr, nullptr, nullptr);
class ResourceFolderModelTest : public QObject
{
Q_OBJECT
private
slots:
// test for GH-1178 - install a folder with files to a mod list
void test_1178()
{
// source
QString source = QFINDTESTDATA("testdata/test_folder");
// sanity check
QVERIFY(!source.endsWith('/'));
auto verify = [](QString path)
{
QDir target_dir(FS::PathCombine(path, "test_folder"));
QVERIFY(target_dir.entryList().contains("pack.mcmeta"));
QVERIFY(target_dir.entryList().contains("assets"));
};
// 1. test with no trailing /
{
QString folder = source;
QTemporaryDir tempDir;
QEventLoop loop;
ModFolderModel m(tempDir.path(), true);
connect(&m, &ModFolderModel::updateFinished, &loop, &QEventLoop::quit);
QTimer expire_timer;
expire_timer.callOnTimeout(&loop, &QEventLoop::quit);
expire_timer.setSingleShot(true);
expire_timer.start(4000);
m.installMod(folder);
loop.exec();
QVERIFY2(expire_timer.isActive(), "Timer has expired. The update never finished.");
expire_timer.stop();
verify(tempDir.path());
}
// 2. test with trailing /
{
QString folder = source + '/';
QTemporaryDir tempDir;
QEventLoop loop;
ModFolderModel m(tempDir.path(), true);
connect(&m, &ModFolderModel::updateFinished, &loop, &QEventLoop::quit);
QTimer expire_timer;
expire_timer.callOnTimeout(&loop, &QEventLoop::quit);
expire_timer.setSingleShot(true);
expire_timer.start(4000);
m.installMod(folder);
loop.exec();
QVERIFY2(expire_timer.isActive(), "Timer has expired. The update never finished.");
expire_timer.stop();
verify(tempDir.path());
}
}
void test_addFromWatch()
{
QString source = QFINDTESTDATA("testdata");
ModFolderModel model(source);
QCOMPARE(model.size(), 0);
EXEC_UPDATE_TASK(model.startWatching(), )
for (auto mod : model.allMods())
qDebug() << mod->name();
QCOMPARE(model.size(), 2);
model.stopWatching();
while (model.hasPendingParseTasks()) {
QTest::qSleep(20);
QCoreApplication::processEvents();
}
}
void test_removeResource()
{
QString folder_resource = QFINDTESTDATA("testdata/test_folder");
QString file_mod = QFINDTESTDATA("testdata/supercoolmod.jar");
QTemporaryDir tmp;
ResourceFolderModel model(QDir(tmp.path()));
QCOMPARE(model.size(), 0);
{
EXEC_UPDATE_TASK(model.installResource(file_mod), QVERIFY)
}
QCOMPARE(model.size(), 1);
qDebug() << "Added first mod.";
{
EXEC_UPDATE_TASK(model.startWatching(), )
}
QCOMPARE(model.size(), 1);
qDebug() << "Started watching the temp folder.";
{
EXEC_UPDATE_TASK(model.installResource(folder_resource), QVERIFY)
}
QCOMPARE(model.size(), 2);
qDebug() << "Added second mod.";
{
EXEC_UPDATE_TASK(model.uninstallResource("supercoolmod.jar"), QVERIFY);
}
QCOMPARE(model.size(), 1);
qDebug() << "Removed first mod.";
QString mod_file_name {model.at(0).fileinfo().fileName()};
QVERIFY(!mod_file_name.isEmpty());
{
EXEC_UPDATE_TASK(model.uninstallResource(mod_file_name), QVERIFY);
}
QCOMPARE(model.size(), 0);
qDebug() << "Removed second mod.";
model.stopWatching();
while (model.hasPendingParseTasks()) {
QTest::qSleep(20);
QCoreApplication::processEvents();
}
}
void test_enable_disable()
{
QString folder_resource = QFINDTESTDATA("testdata/test_folder");
QString file_mod = QFINDTESTDATA("testdata/supercoolmod.jar");
QTemporaryDir tmp;
ResourceFolderModel model(tmp.path());
QCOMPARE(model.size(), 0);
{
EXEC_UPDATE_TASK(model.installResource(folder_resource), QVERIFY)
}
{
EXEC_UPDATE_TASK(model.installResource(file_mod), QVERIFY)
}
for (auto res : model.all())
qDebug() << res->name();
QCOMPARE(model.size(), 2);
auto& res_1 = model.at(0).type() != ResourceType::FOLDER ? model.at(0) : model.at(1);
auto& res_2 = model.at(0).type() == ResourceType::FOLDER ? model.at(0) : model.at(1);
auto id_1 = res_1.internal_id();
auto id_2 = res_2.internal_id();
bool initial_enabled_res_2 = res_2.enabled();
bool initial_enabled_res_1 = res_1.enabled();
QVERIFY(res_1.type() != ResourceType::FOLDER && res_1.type() != ResourceType::UNKNOWN);
qDebug() << "res_1 is of the correct type.";
QVERIFY(res_1.enabled());
qDebug() << "res_1 is initially enabled.";
QVERIFY(res_1.enable(EnableAction::TOGGLE));
QVERIFY(res_1.enabled() == !initial_enabled_res_1);
qDebug() << "res_1 got successfully toggled.";
QVERIFY(res_1.enable(EnableAction::TOGGLE));
qDebug() << "res_1 got successfully toggled again.";
QVERIFY(res_1.enabled() == initial_enabled_res_1);
QVERIFY(res_1.internal_id() == id_1);
qDebug() << "res_1 got back to its initial state.";
QVERIFY(!res_2.enable(initial_enabled_res_2 ? EnableAction::ENABLE : EnableAction::DISABLE));
QVERIFY(res_2.enabled() == initial_enabled_res_2);
QVERIFY(res_2.internal_id() == id_2);
while (model.hasPendingParseTasks()) {
QTest::qSleep(20);
QCoreApplication::processEvents();
}
}
};
QTEST_GUILESS_MAIN(ResourceFolderModelTest)
#include "ResourceFolderModel_test.moc"

View File

@ -0,0 +1,13 @@
#pragma once
#include "Resource.h"
class ResourcePack : public Resource {
Q_OBJECT
public:
using Ptr = shared_qobject_ptr<Resource>;
ResourcePack(QObject* parent = nullptr) : Resource(parent) {}
ResourcePack(QFileInfo file_info) : Resource(file_info) {}
};

View File

@ -35,24 +35,4 @@
#include "ResourcePackFolderModel.h"
ResourcePackFolderModel::ResourcePackFolderModel(const QString &dir) : ModFolderModel(dir) {
}
QVariant ResourcePackFolderModel::headerData(int section, Qt::Orientation orientation, int role) const {
if (role == Qt::ToolTipRole) {
switch (section) {
case ActiveColumn:
return tr("Is the resource pack enabled?");
case NameColumn:
return tr("The name of the resource pack.");
case VersionColumn:
return tr("The version of the resource pack.");
case DateColumn:
return tr("The date and time this resource pack was last changed (or added).");
default:
return QVariant();
}
}
return ModFolderModel::headerData(section, orientation, role);
}
ResourcePackFolderModel::ResourcePackFolderModel(const QString &dir) : ResourceFolderModel(QDir(dir)) {}

View File

@ -1,13 +1,14 @@
#pragma once
#include "ModFolderModel.h"
#include "ResourceFolderModel.h"
class ResourcePackFolderModel : public ModFolderModel
#include "ResourcePack.h"
class ResourcePackFolderModel : public ResourceFolderModel
{
Q_OBJECT
public:
explicit ResourcePackFolderModel(const QString &dir);
QVariant headerData(int section, Qt::Orientation orientation, int role) const override;
RESOURCE_HELPERS(ResourcePack)
};

View File

@ -0,0 +1,10 @@
#pragma once
#include "ResourceFolderModel.h"
class ShaderPackFolderModel : public ResourceFolderModel {
Q_OBJECT
public:
explicit ShaderPackFolderModel(const QString& dir) : ResourceFolderModel(QDir(dir)) {}
};

View File

@ -35,24 +35,4 @@
#include "TexturePackFolderModel.h"
TexturePackFolderModel::TexturePackFolderModel(const QString &dir) : ModFolderModel(dir) {
}
QVariant TexturePackFolderModel::headerData(int section, Qt::Orientation orientation, int role) const {
if (role == Qt::ToolTipRole) {
switch (section) {
case ActiveColumn:
return tr("Is the texture pack enabled?");
case NameColumn:
return tr("The name of the texture pack.");
case VersionColumn:
return tr("The version of the texture pack.");
case DateColumn:
return tr("The date and time this texture pack was last changed (or added).");
default:
return QVariant();
}
}
return ModFolderModel::headerData(section, orientation, role);
}
TexturePackFolderModel::TexturePackFolderModel(const QString &dir) : ResourceFolderModel(QDir(dir)) {}

View File

@ -1,13 +1,11 @@
#pragma once
#include "ModFolderModel.h"
#include "ResourceFolderModel.h"
class TexturePackFolderModel : public ModFolderModel
class TexturePackFolderModel : public ResourceFolderModel
{
Q_OBJECT
public:
explicit TexturePackFolderModel(const QString &dir);
QVariant headerData(int section, Qt::Orientation orientation, int role) const override;
};

View File

@ -0,0 +1,53 @@
#pragma once
#include <QDir>
#include <QMap>
#include <QObject>
#include <memory>
#include "minecraft/mod/Resource.h"
#include "tasks/Task.h"
/** Very simple task that just loads a folder's contents directly.
*/
class BasicFolderLoadTask : public Task
{
Q_OBJECT
public:
struct Result {
QMap<QString, Resource::Ptr> resources;
};
using ResultPtr = std::shared_ptr<Result>;
[[nodiscard]] ResultPtr result() const {
return m_result;
}
public:
BasicFolderLoadTask(QDir dir) : Task(nullptr, false), m_dir(dir), m_result(new Result) {}
[[nodiscard]] bool canAbort() const override { return true; }
bool abort() override { m_aborted = true; return true; }
void executeTask() override
{
m_dir.refresh();
for (auto entry : m_dir.entryInfoList()) {
auto resource = new Resource(entry);
m_result->resources.insert(resource->internal_id(), resource);
}
if (m_aborted)
emitAborted();
else
emitSucceeded();
}
private:
QDir m_dir;
ResultPtr m_result;
bool m_aborted = false;
};

View File

@ -20,22 +20,22 @@ namespace {
// OLD format:
// https://github.com/MinecraftForge/FML/wiki/FML-mod-information-file/5bf6a2d05145ec79387acc0d45c958642fb049fc
std::shared_ptr<ModDetails> ReadMCModInfo(QByteArray contents)
ModDetails ReadMCModInfo(QByteArray contents)
{
auto getInfoFromArray = [&](QJsonArray arr)->std::shared_ptr<ModDetails>
auto getInfoFromArray = [&](QJsonArray arr) -> ModDetails
{
if (!arr.at(0).isObject()) {
return nullptr;
return {};
}
std::shared_ptr<ModDetails> details = std::make_shared<ModDetails>();
ModDetails details;
auto firstObj = arr.at(0).toObject();
details->mod_id = firstObj.value("modid").toString();
details.mod_id = firstObj.value("modid").toString();
auto name = firstObj.value("name").toString();
// NOTE: ignore stupid example mods copies where the author didn't even bother to change the name
if(name != "Example Mod") {
details->name = name;
details.name = name;
}
details->version = firstObj.value("version").toString();
details.version = firstObj.value("version").toString();
auto homeurl = firstObj.value("url").toString().trimmed();
if(!homeurl.isEmpty())
{
@ -45,8 +45,8 @@ std::shared_ptr<ModDetails> ReadMCModInfo(QByteArray contents)
homeurl.prepend("http://");
}
}
details->homeurl = homeurl;
details->description = firstObj.value("description").toString();
details.homeurl = homeurl;
details.description = firstObj.value("description").toString();
QJsonArray authors = firstObj.value("authorList").toArray();
if (authors.size() == 0) {
// FIXME: what is the format of this? is there any?
@ -55,7 +55,7 @@ std::shared_ptr<ModDetails> ReadMCModInfo(QByteArray contents)
for (auto author: authors)
{
details->authors.append(author.toString());
details.authors.append(author.toString());
}
return details;
};
@ -83,7 +83,7 @@ std::shared_ptr<ModDetails> ReadMCModInfo(QByteArray contents)
{
qCritical() << "BAD stuff happened to mod json:";
qCritical() << contents;
return nullptr;
return {};
}
auto arrVal = jsonDoc.object().value("modlist");
if(arrVal.isUndefined()) {
@ -94,13 +94,13 @@ std::shared_ptr<ModDetails> ReadMCModInfo(QByteArray contents)
return getInfoFromArray(arrVal.toArray());
}
}
return nullptr;
return {};
}
// https://github.com/MinecraftForge/Documentation/blob/5ab4ba6cf9abc0ac4c0abd96ad187461aefd72af/docs/gettingstarted/structuring.md
std::shared_ptr<ModDetails> ReadMCModTOML(QByteArray contents)
ModDetails ReadMCModTOML(QByteArray contents)
{
std::shared_ptr<ModDetails> details = std::make_shared<ModDetails>();
ModDetails details;
char errbuf[200];
// top-level table
@ -108,7 +108,7 @@ std::shared_ptr<ModDetails> ReadMCModTOML(QByteArray contents)
if(!tomlData)
{
return nullptr;
return {};
}
// array defined by [[mods]]
@ -116,7 +116,7 @@ std::shared_ptr<ModDetails> ReadMCModTOML(QByteArray contents)
if(!tomlModsArr)
{
qWarning() << "Corrupted mods.toml? Couldn't find [[mods]] array!";
return nullptr;
return {};
}
// we only really care about the first element, since multiple mods in one file is not supported by us at the moment
@ -124,33 +124,33 @@ std::shared_ptr<ModDetails> ReadMCModTOML(QByteArray contents)
if(!tomlModsTable0)
{
qWarning() << "Corrupted mods.toml? [[mods]] didn't have an element at index 0!";
return nullptr;
return {};
}
// mandatory properties - always in [[mods]]
toml_datum_t modIdDatum = toml_string_in(tomlModsTable0, "modId");
if(modIdDatum.ok)
{
details->mod_id = modIdDatum.u.s;
details.mod_id = modIdDatum.u.s;
// library says this is required for strings
free(modIdDatum.u.s);
}
toml_datum_t versionDatum = toml_string_in(tomlModsTable0, "version");
if(versionDatum.ok)
{
details->version = versionDatum.u.s;
details.version = versionDatum.u.s;
free(versionDatum.u.s);
}
toml_datum_t displayNameDatum = toml_string_in(tomlModsTable0, "displayName");
if(displayNameDatum.ok)
{
details->name = displayNameDatum.u.s;
details.name = displayNameDatum.u.s;
free(displayNameDatum.u.s);
}
toml_datum_t descriptionDatum = toml_string_in(tomlModsTable0, "description");
if(descriptionDatum.ok)
{
details->description = descriptionDatum.u.s;
details.description = descriptionDatum.u.s;
free(descriptionDatum.u.s);
}
@ -173,7 +173,7 @@ std::shared_ptr<ModDetails> ReadMCModTOML(QByteArray contents)
}
if(!authors.isEmpty())
{
details->authors.append(authors);
details.authors.append(authors);
}
toml_datum_t homeurlDatum = toml_string_in(tomlData, "displayURL");
@ -200,7 +200,7 @@ std::shared_ptr<ModDetails> ReadMCModTOML(QByteArray contents)
homeurl.prepend("http://");
}
}
details->homeurl = homeurl;
details.homeurl = homeurl;
// this seems to be recursive, so it should free everything
toml_free(tomlData);
@ -209,20 +209,20 @@ std::shared_ptr<ModDetails> ReadMCModTOML(QByteArray contents)
}
// https://fabricmc.net/wiki/documentation:fabric_mod_json
std::shared_ptr<ModDetails> ReadFabricModInfo(QByteArray contents)
ModDetails ReadFabricModInfo(QByteArray contents)
{
QJsonParseError jsonError;
QJsonDocument jsonDoc = QJsonDocument::fromJson(contents, &jsonError);
auto object = jsonDoc.object();
auto schemaVersion = object.contains("schemaVersion") ? object.value("schemaVersion").toInt(0) : 0;
std::shared_ptr<ModDetails> details = std::make_shared<ModDetails>();
ModDetails details;
details->mod_id = object.value("id").toString();
details->version = object.value("version").toString();
details.mod_id = object.value("id").toString();
details.version = object.value("version").toString();
details->name = object.contains("name") ? object.value("name").toString() : details->mod_id;
details->description = object.value("description").toString();
details.name = object.contains("name") ? object.value("name").toString() : details.mod_id;
details.description = object.value("description").toString();
if (schemaVersion >= 1)
{
@ -230,10 +230,10 @@ std::shared_ptr<ModDetails> ReadFabricModInfo(QByteArray contents)
for (auto author: authors)
{
if(author.isObject()) {
details->authors.append(author.toObject().value("name").toString());
details.authors.append(author.toObject().value("name").toString());
}
else {
details->authors.append(author.toString());
details.authors.append(author.toString());
}
}
@ -243,7 +243,7 @@ std::shared_ptr<ModDetails> ReadFabricModInfo(QByteArray contents)
if (contact.contains("homepage"))
{
details->homeurl = contact.value("homepage").toString();
details.homeurl = contact.value("homepage").toString();
}
}
}
@ -251,50 +251,50 @@ std::shared_ptr<ModDetails> ReadFabricModInfo(QByteArray contents)
}
// https://github.com/QuiltMC/rfcs/blob/master/specification/0002-quilt.mod.json.md
std::shared_ptr<ModDetails> ReadQuiltModInfo(QByteArray contents)
ModDetails ReadQuiltModInfo(QByteArray contents)
{
QJsonParseError jsonError;
QJsonDocument jsonDoc = QJsonDocument::fromJson(contents, &jsonError);
auto object = Json::requireObject(jsonDoc, "quilt.mod.json");
auto schemaVersion = Json::ensureInteger(object.value("schema_version"), 0, "Quilt schema_version");
std::shared_ptr<ModDetails> details = std::make_shared<ModDetails>();
ModDetails details;
// https://github.com/QuiltMC/rfcs/blob/be6ba280d785395fefa90a43db48e5bfc1d15eb4/specification/0002-quilt.mod.json.md
if (schemaVersion == 1)
{
auto modInfo = Json::requireObject(object.value("quilt_loader"), "Quilt mod info");
details->mod_id = Json::requireString(modInfo.value("id"), "Mod ID");
details->version = Json::requireString(modInfo.value("version"), "Mod version");
details.mod_id = Json::requireString(modInfo.value("id"), "Mod ID");
details.version = Json::requireString(modInfo.value("version"), "Mod version");
auto modMetadata = Json::ensureObject(modInfo.value("metadata"));
details->name = Json::ensureString(modMetadata.value("name"), details->mod_id);
details->description = Json::ensureString(modMetadata.value("description"));
details.name = Json::ensureString(modMetadata.value("name"), details.mod_id);
details.description = Json::ensureString(modMetadata.value("description"));
auto modContributors = Json::ensureObject(modMetadata.value("contributors"));
// We don't really care about the role of a contributor here
details->authors += modContributors.keys();
details.authors += modContributors.keys();
auto modContact = Json::ensureObject(modMetadata.value("contact"));
if (modContact.contains("homepage"))
{
details->homeurl = Json::requireString(modContact.value("homepage"));
details.homeurl = Json::requireString(modContact.value("homepage"));
}
}
return details;
}
std::shared_ptr<ModDetails> ReadForgeInfo(QByteArray contents)
ModDetails ReadForgeInfo(QByteArray contents)
{
std::shared_ptr<ModDetails> details = std::make_shared<ModDetails>();
ModDetails details;
// Read the data
details->name = "Minecraft Forge";
details->mod_id = "Forge";
details->homeurl = "http://www.minecraftforge.net/forum/";
details.name = "Minecraft Forge";
details.mod_id = "Forge";
details.homeurl = "http://www.minecraftforge.net/forum/";
INIFile ini;
if (!ini.loadFile(contents))
return details;
@ -304,47 +304,47 @@ std::shared_ptr<ModDetails> ReadForgeInfo(QByteArray contents)
QString revision = ini.get("forge.revision.number", "0").toString();
QString build = ini.get("forge.build.number", "0").toString();
details->version = major + "." + minor + "." + revision + "." + build;
details.version = major + "." + minor + "." + revision + "." + build;
return details;
}
std::shared_ptr<ModDetails> ReadLiteModInfo(QByteArray contents)
ModDetails ReadLiteModInfo(QByteArray contents)
{
std::shared_ptr<ModDetails> details = std::make_shared<ModDetails>();
ModDetails details;
QJsonParseError jsonError;
QJsonDocument jsonDoc = QJsonDocument::fromJson(contents, &jsonError);
auto object = jsonDoc.object();
if (object.contains("name"))
{
details->mod_id = details->name = object.value("name").toString();
details.mod_id = details.name = object.value("name").toString();
}
if (object.contains("version"))
{
details->version = object.value("version").toString("");
details.version = object.value("version").toString("");
}
else
{
details->version = object.value("revision").toString("");
details.version = object.value("revision").toString("");
}
details->mcversion = object.value("mcversion").toString();
details.mcversion = object.value("mcversion").toString();
auto author = object.value("author").toString();
if(!author.isEmpty()) {
details->authors.append(author);
details.authors.append(author);
}
details->description = object.value("description").toString();
details->homeurl = object.value("url").toString();
details.description = object.value("description").toString();
details.homeurl = object.value("url").toString();
return details;
}
}
LocalModParseTask::LocalModParseTask(int token, Mod::ModType type, const QFileInfo& modFile):
LocalModParseTask::LocalModParseTask(int token, ResourceType type, const QFileInfo& modFile):
Task(nullptr, false),
m_token(token),
m_type(type),
m_modFile(modFile),
m_result(new Result())
{
}
{}
void LocalModParseTask::processAsZip()
{
@ -366,7 +366,7 @@ void LocalModParseTask::processAsZip()
file.close();
// to replace ${file.jarVersion} with the actual version, as needed
if (m_result->details && m_result->details->version == "${file.jarVersion}")
if (m_result->details.version == "${file.jarVersion}")
{
if (zip.setCurrentFile("META-INF/MANIFEST.MF"))
{
@ -395,7 +395,7 @@ void LocalModParseTask::processAsZip()
manifestVersion = "NONE";
}
m_result->details->version = manifestVersion;
m_result->details.version = manifestVersion;
file.close();
}
@ -497,21 +497,31 @@ void LocalModParseTask::processAsLitemod()
zip.close();
}
void LocalModParseTask::run()
bool LocalModParseTask::abort()
{
m_aborted = true;
return true;
}
void LocalModParseTask::executeTask()
{
switch(m_type)
{
case Mod::MOD_ZIPFILE:
case ResourceType::ZIPFILE:
processAsZip();
break;
case Mod::MOD_FOLDER:
case ResourceType::FOLDER:
processAsFolder();
break;
case Mod::MOD_LITEMOD:
case ResourceType::LITEMOD:
processAsLitemod();
break;
default:
break;
}
emit finished(m_token);
if (m_aborted)
emitAborted();
else
emitSucceeded();
}

View File

@ -2,29 +2,31 @@
#include <QDebug>
#include <QObject>
#include <QRunnable>
#include "minecraft/mod/Mod.h"
#include "minecraft/mod/ModDetails.h"
class LocalModParseTask : public QObject, public QRunnable
#include "tasks/Task.h"
class LocalModParseTask : public Task
{
Q_OBJECT
public:
struct Result {
QString id;
std::shared_ptr<ModDetails> details;
ModDetails details;
};
using ResultPtr = std::shared_ptr<Result>;
ResultPtr result() const {
return m_result;
}
LocalModParseTask(int token, Mod::ModType type, const QFileInfo & modFile);
void run();
[[nodiscard]] bool canAbort() const override { return true; }
bool abort() override;
signals:
void finished(int token);
LocalModParseTask(int token, ResourceType type, const QFileInfo & modFile);
void executeTask() override;
[[nodiscard]] int token() const { return m_token; }
private:
void processAsZip();
@ -33,7 +35,9 @@ private:
private:
int m_token;
Mod::ModType m_type;
ResourceType m_type;
QFileInfo m_modFile;
ResultPtr m_result;
bool m_aborted = false;
};

View File

@ -38,11 +38,11 @@
#include "minecraft/mod/MetadataHandler.h"
ModFolderLoadTask::ModFolderLoadTask(QDir& mods_dir, QDir& index_dir, bool is_indexed, bool clean_orphan)
: m_mods_dir(mods_dir), m_index_dir(index_dir), m_is_indexed(is_indexed), m_clean_orphan(clean_orphan), m_result(new Result())
ModFolderLoadTask::ModFolderLoadTask(QDir mods_dir, QDir index_dir, bool is_indexed, bool clean_orphan, QObject* parent)
: Task(parent, false), m_mods_dir(mods_dir), m_index_dir(index_dir), m_is_indexed(is_indexed), m_clean_orphan(clean_orphan), m_result(new Result())
{}
void ModFolderLoadTask::run()
void ModFolderLoadTask::executeTask()
{
if (m_is_indexed) {
// Read metadata first
@ -52,7 +52,7 @@ void ModFolderLoadTask::run()
// Read JAR files that don't have metadata
m_mods_dir.refresh();
for (auto entry : m_mods_dir.entryInfoList()) {
Mod::Ptr mod(new Mod(entry));
Mod* mod(new Mod(entry));
if (mod->enabled()) {
if (m_result->mods.contains(mod->internal_id())) {
@ -96,7 +96,7 @@ void ModFolderLoadTask::run()
}
}
emit succeeded();
emitSucceeded();
}
void ModFolderLoadTask::getFromMetadata()

View File

@ -42,8 +42,9 @@
#include <QRunnable>
#include <memory>
#include "minecraft/mod/Mod.h"
#include "tasks/Task.h"
class ModFolderLoadTask : public QObject, public QRunnable
class ModFolderLoadTask : public Task
{
Q_OBJECT
public:
@ -56,16 +57,15 @@ public:
}
public:
ModFolderLoadTask(QDir& mods_dir, QDir& index_dir, bool is_indexed, bool clean_orphan = false);
void run();
signals:
void succeeded();
ModFolderLoadTask(QDir mods_dir, QDir index_dir, bool is_indexed, bool clean_orphan = false, QObject* parent = nullptr);
void executeTask() override;
private:
void getFromMetadata();
private:
QDir& m_mods_dir, m_index_dir;
QDir m_mods_dir, m_index_dir;
bool m_is_indexed;
bool m_clean_orphan;
ResultPtr m_result;

View File

@ -0,0 +1 @@
the best mod.

View File

@ -102,7 +102,7 @@ void EnsureMetadataTask::executeTask()
}
// Folders don't have metadata
if (mod->type() == Mod::MOD_FOLDER) {
if (mod->type() == ResourceType::FOLDER) {
emitReady(mod);
}
}

View File

@ -37,8 +37,9 @@
#include <QDebug>
Task::Task(QObject *parent) : QObject(parent)
Task::Task(QObject *parent, bool show_debug) : QObject(parent), m_show_debug(show_debug)
{
setAutoDelete(false);
}
void Task::setStatus(const QString &new_status)
@ -63,27 +64,32 @@ void Task::start()
{
case State::Inactive:
{
qDebug() << "Task" << describe() << "starting for the first time";
if (m_show_debug)
qDebug() << "Task" << describe() << "starting for the first time";
break;
}
case State::AbortedByUser:
{
qDebug() << "Task" << describe() << "restarting for after being aborted by user";
if (m_show_debug)
qDebug() << "Task" << describe() << "restarting for after being aborted by user";
break;
}
case State::Failed:
{
qDebug() << "Task" << describe() << "restarting for after failing at first";
if (m_show_debug)
qDebug() << "Task" << describe() << "restarting for after failing at first";
break;
}
case State::Succeeded:
{
qDebug() << "Task" << describe() << "restarting for after succeeding at first";
if (m_show_debug)
qDebug() << "Task" << describe() << "restarting for after succeeding at first";
break;
}
case State::Running:
{
qWarning() << "The launcher tried to start task" << describe() << "while it was already running!";
if (m_show_debug)
qWarning() << "The launcher tried to start task" << describe() << "while it was already running!";
return;
}
}
@ -118,7 +124,8 @@ void Task::emitAborted()
}
m_state = State::AbortedByUser;
m_failReason = "Aborted.";
qDebug() << "Task" << describe() << "aborted.";
if (m_show_debug)
qDebug() << "Task" << describe() << "aborted.";
emit aborted();
emit finished();
}
@ -132,7 +139,8 @@ void Task::emitSucceeded()
return;
}
m_state = State::Succeeded;
qDebug() << "Task" << describe() << "succeeded";
if (m_show_debug)
qDebug() << "Task" << describe() << "succeeded";
emit succeeded();
emit finished();
}

View File

@ -35,9 +35,11 @@
#pragma once
#include <QRunnable>
#include "QObjectPtr.h"
class Task : public QObject {
class Task : public QObject, public QRunnable {
Q_OBJECT
public:
using Ptr = shared_qobject_ptr<Task>;
@ -45,7 +47,7 @@ class Task : public QObject {
enum class State { Inactive, Running, Succeeded, Failed, AbortedByUser };
public:
explicit Task(QObject* parent = 0);
explicit Task(QObject* parent = 0, bool show_debug_log = true);
virtual ~Task() = default;
bool isRunning() const;
@ -95,6 +97,9 @@ class Task : public QObject {
void stepStatus(QString status);
public slots:
// QRunnable's interface
void run() override { start(); }
virtual void start();
virtual bool abort() { if(canAbort()) emitAborted(); return canAbort(); };
@ -117,4 +122,7 @@ class Task : public QObject {
QString m_status;
int m_progress = 0;
int m_progressTotal = 100;
// TODO: Nuke in favor of QLoggingCategory
bool m_show_debug = true;
};

View File

@ -36,7 +36,7 @@ static ModAPI::ModLoaderTypes mcLoaders(BaseInstance* inst)
ModUpdateDialog::ModUpdateDialog(QWidget* parent,
BaseInstance* instance,
const std::shared_ptr<ModFolderModel> mods,
QList<Mod::Ptr>& search_for)
QList<Mod*>& search_for)
: ReviewMessageBox(parent, tr("Confirm mods to update"), "")
, m_parent(parent)
, m_mod_model(mods)
@ -226,9 +226,8 @@ auto ModUpdateDialog::ensureMetadata() -> bool
};
for (auto candidate : m_candidates) {
auto* candidate_ptr = candidate.get();
if (candidate->status() != ModStatus::NoMetadata) {
onMetadataEnsured(candidate_ptr);
onMetadataEnsured(candidate);
continue;
}
@ -236,7 +235,7 @@ auto ModUpdateDialog::ensureMetadata() -> bool
continue;
if (confirm_rest) {
addToTmp(candidate_ptr, provider_rest);
addToTmp(candidate, provider_rest);
should_try_others.insert(candidate->internal_id(), try_others_rest);
continue;
}
@ -261,7 +260,7 @@ auto ModUpdateDialog::ensureMetadata() -> bool
should_try_others.insert(candidate->internal_id(), response.try_others);
if (confirmed)
addToTmp(candidate_ptr, response.chosen);
addToTmp(candidate, response.chosen);
}
if (!modrinth_tmp.empty()) {

View File

@ -19,7 +19,7 @@ class ModUpdateDialog final : public ReviewMessageBox {
explicit ModUpdateDialog(QWidget* parent,
BaseInstance* instance,
const std::shared_ptr<ModFolderModel> mod_model,
QList<Mod::Ptr>& search_for);
QList<Mod*>& search_for);
void checkCandidates();
@ -46,7 +46,7 @@ class ModUpdateDialog final : public ReviewMessageBox {
const std::shared_ptr<ModFolderModel> m_mod_model;
QList<Mod::Ptr>& m_candidates;
QList<Mod*>& m_candidates;
QList<Mod*> m_modrinth_to_update;
QList<Mod*> m_flame_to_update;

View File

@ -3,100 +3,13 @@
#include "DesktopServices.h"
#include "Version.h"
#include "minecraft/mod/ModFolderModel.h"
#include "minecraft/mod/ResourceFolderModel.h"
#include "ui/GuiUtil.h"
#include <QKeyEvent>
#include <QMenu>
namespace {
// FIXME: wasteful
void RemoveThePrefix(QString& string)
{
QRegularExpression regex(QStringLiteral("^(?:the|teh) +"), QRegularExpression::CaseInsensitiveOption);
string.remove(regex);
string = string.trimmed();
}
} // namespace
class SortProxy : public QSortFilterProxyModel {
public:
explicit SortProxy(QObject* parent = nullptr) : QSortFilterProxyModel(parent) {}
protected:
bool filterAcceptsRow(int source_row, const QModelIndex& source_parent) const override
{
ModFolderModel* model = qobject_cast<ModFolderModel*>(sourceModel());
if (!model)
return false;
const auto& mod = model->at(source_row);
if (filterRegularExpression().match(mod.name()).hasMatch())
return true;
if (filterRegularExpression().match(mod.description()).hasMatch())
return true;
for (auto& author : mod.authors()) {
if (filterRegularExpression().match(author).hasMatch()) {
return true;
}
}
return false;
}
bool lessThan(const QModelIndex& source_left, const QModelIndex& source_right) const override
{
ModFolderModel* model = qobject_cast<ModFolderModel*>(sourceModel());
if (!model || !source_left.isValid() || !source_right.isValid() || source_left.column() != source_right.column()) {
return QSortFilterProxyModel::lessThan(source_left, source_right);
}
// we are now guaranteed to have two valid indexes in the same column... we love the provided invariants unconditionally and
// proceed.
auto column = (ModFolderModel::Columns) source_left.column();
bool invert = false;
switch (column) {
// GH-2550 - sort by enabled/disabled
case ModFolderModel::ActiveColumn: {
auto dataL = source_left.data(Qt::CheckStateRole).toBool();
auto dataR = source_right.data(Qt::CheckStateRole).toBool();
if (dataL != dataR)
return dataL > dataR;
// fallthrough
invert = sortOrder() == Qt::DescendingOrder;
}
// GH-2722 - sort mod names in a way that discards "The" prefixes
case ModFolderModel::NameColumn: {
auto dataL = model->data(model->index(source_left.row(), ModFolderModel::NameColumn)).toString();
RemoveThePrefix(dataL);
auto dataR = model->data(model->index(source_right.row(), ModFolderModel::NameColumn)).toString();
RemoveThePrefix(dataR);
auto less = dataL.compare(dataR, sortCaseSensitivity());
if (less != 0)
return invert ? (less > 0) : (less < 0);
// fallthrough
invert = sortOrder() == Qt::DescendingOrder;
}
// GH-2762 - sort versions by parsing them as versions
case ModFolderModel::VersionColumn: {
auto dataL = Version(model->data(model->index(source_left.row(), ModFolderModel::VersionColumn)).toString());
auto dataR = Version(model->data(model->index(source_right.row(), ModFolderModel::VersionColumn)).toString());
return invert ? (dataL > dataR) : (dataL < dataR);
}
default: {
return QSortFilterProxyModel::lessThan(source_left, source_right);
}
}
}
};
ExternalResourcesPage::ExternalResourcesPage(BaseInstance* instance, std::shared_ptr<ModFolderModel> model, QWidget* parent)
ExternalResourcesPage::ExternalResourcesPage(BaseInstance* instance, std::shared_ptr<ResourceFolderModel> model, QWidget* parent)
: QMainWindow(parent), m_instance(instance), ui(new Ui::ExternalResourcesPage), m_model(model)
{
ui->setupUi(this);
@ -105,7 +18,7 @@ ExternalResourcesPage::ExternalResourcesPage(BaseInstance* instance, std::shared
ui->actionsToolbar->insertSpacer(ui->actionViewConfigs);
m_filterModel = new SortProxy(this);
m_filterModel = model->createFilterProxyModel(this);
m_filterModel->setDynamicSortFilter(true);
m_filterModel->setFilterCaseSensitivity(Qt::CaseInsensitive);
m_filterModel->setSortCaseSensitivity(Qt::CaseInsensitive);
@ -137,19 +50,9 @@ ExternalResourcesPage::ExternalResourcesPage(BaseInstance* instance, std::shared
ExternalResourcesPage::~ExternalResourcesPage()
{
m_model->stopWatching();
delete ui;
}
void ExternalResourcesPage::itemActivated(const QModelIndex&)
{
if (!m_controlsEnabled)
return;
auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection());
m_model->setModStatus(selection.indexes(), ModFolderModel::Toggle);
}
QMenu* ExternalResourcesPage::createPopupMenu()
{
QMenu* filteredMenu = QMainWindow::createPopupMenu();
@ -179,6 +82,15 @@ void ExternalResourcesPage::retranslate()
ui->retranslateUi(this);
}
void ExternalResourcesPage::itemActivated(const QModelIndex&)
{
if (!m_controlsEnabled)
return;
auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection());
m_model->setResourceEnabled(selection.indexes(), EnableAction::TOGGLE);
}
void ExternalResourcesPage::filterTextChanged(const QString& newContents)
{
m_viewFilter = newContents;
@ -241,7 +153,7 @@ void ExternalResourcesPage::addItem()
if (!list.isEmpty()) {
for (auto filename : list) {
m_model->installMod(filename);
m_model->installResource(filename);
}
}
}
@ -252,25 +164,25 @@ void ExternalResourcesPage::removeItem()
return;
auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection());
m_model->deleteMods(selection.indexes());
m_model->deleteResources(selection.indexes());
}
void ExternalResourcesPage::enableItem()
{
if (!m_controlsEnabled)
return;
auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection());
m_model->setModStatus(selection.indexes(), ModFolderModel::Enable);
m_model->setResourceEnabled(selection.indexes(), EnableAction::ENABLE);
}
void ExternalResourcesPage::disableItem()
{
if (!m_controlsEnabled)
return;
auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection());
m_model->setModStatus(selection.indexes(), ModFolderModel::Disable);
m_model->setResourceEnabled(selection.indexes(), EnableAction::DISABLE);
}
void ExternalResourcesPage::viewConfigs()
@ -283,15 +195,23 @@ void ExternalResourcesPage::viewFolder()
DesktopServices::openDirectory(m_model->dir().absolutePath(), true);
}
void ExternalResourcesPage::current(const QModelIndex& current, const QModelIndex& previous)
bool ExternalResourcesPage::current(const QModelIndex& current, const QModelIndex& previous)
{
if (!current.isValid()) {
ui->frame->clear();
return;
return false;
}
return onSelectionChanged(current, previous);
}
bool ExternalResourcesPage::onSelectionChanged(const QModelIndex& current, const QModelIndex& previous)
{
auto sourceCurrent = m_filterModel->mapToSource(current);
int row = sourceCurrent.row();
Mod& m = m_model->operator[](row);
ui->frame->updateWithMod(m);
Resource const& resource = m_model->at(row);
ui->frame->updateWithResource(resource);
return true;
}

View File

@ -7,7 +7,7 @@
#include "minecraft/MinecraftInstance.h"
#include "ui/pages/BasePage.h"
class ModFolderModel;
class ResourceFolderModel;
namespace Ui {
class ExternalResourcesPage;
@ -19,8 +19,7 @@ class ExternalResourcesPage : public QMainWindow, public BasePage {
Q_OBJECT
public:
// FIXME: Switch to different model (or change the name of this one)
explicit ExternalResourcesPage(BaseInstance* instance, std::shared_ptr<ModFolderModel> model, QWidget* parent = nullptr);
explicit ExternalResourcesPage(BaseInstance* instance, std::shared_ptr<ResourceFolderModel> model, QWidget* parent = nullptr);
virtual ~ExternalResourcesPage();
virtual QString displayName() const override = 0;
@ -41,7 +40,9 @@ class ExternalResourcesPage : public QMainWindow, public BasePage {
QMenu* createPopupMenu() override;
public slots:
void current(const QModelIndex& current, const QModelIndex& previous);
bool current(const QModelIndex& current, const QModelIndex& previous);
virtual bool onSelectionChanged(const QModelIndex& current, const QModelIndex& previous);
protected slots:
void itemActivated(const QModelIndex& index);
@ -63,7 +64,7 @@ class ExternalResourcesPage : public QMainWindow, public BasePage {
BaseInstance* m_instance = nullptr;
Ui::ExternalResourcesPage* ui = nullptr;
std::shared_ptr<ModFolderModel> m_model;
std::shared_ptr<ResourceFolderModel> m_model;
QSortFilterProxyModel* m_filterModel = nullptr;
QString m_fileSelectionFilter;

View File

@ -43,7 +43,7 @@
</layout>
</item>
<item row="2" column="1" colspan="3">
<widget class="MCModInfoFrame" name="frame">
<widget class="InfoFrame" name="frame">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Minimum">
<horstretch>0</horstretch>
@ -166,9 +166,9 @@
<header>ui/widgets/ModListView.h</header>
</customwidget>
<customwidget>
<class>MCModInfoFrame</class>
<class>InfoFrame</class>
<extends>QFrame</extends>
<header>ui/widgets/MCModInfoFrame.h</header>
<header>ui/widgets/InfoFrame.h</header>
<container>1</container>
</customwidget>
<customwidget>

View File

@ -65,7 +65,7 @@
#include "ui/dialogs/ProgressDialog.h"
ModFolderPage::ModFolderPage(BaseInstance* inst, std::shared_ptr<ModFolderModel> mods, QWidget* parent)
: ExternalResourcesPage(inst, mods, parent)
: ExternalResourcesPage(inst, mods, parent), m_model(mods)
{
// This is structured like that so that these changes
// do not affect the Resource pack and Shader pack tabs
@ -124,6 +124,17 @@ bool ModFolderPage::shouldDisplay() const
return true;
}
bool ModFolderPage::onSelectionChanged(const QModelIndex& current, const QModelIndex& previous)
{
auto sourceCurrent = m_filterModel->mapToSource(current);
int row = sourceCurrent.row();
Mod const* m = m_model->at(row);
if (m)
ui->frame->updateWithMod(*m);
return true;
}
void ModFolderPage::installMods()
{
if (!m_controlsEnabled)

View File

@ -55,9 +55,15 @@ class ModFolderPage : public ExternalResourcesPage {
virtual bool shouldDisplay() const override;
void runningStateChanged(bool running) override;
public slots:
bool onSelectionChanged(const QModelIndex& current, const QModelIndex& previous) override;
private slots:
void installMods();
void updateMods();
protected:
std::shared_ptr<ModFolderModel> m_model;
};
class CoreModFolderPage : public ModFolderPage {

View File

@ -38,12 +38,14 @@
#include "ExternalResourcesPage.h"
#include "ui_ExternalResourcesPage.h"
#include "minecraft/mod/ResourcePackFolderModel.h"
class ResourcePackPage : public ExternalResourcesPage
{
Q_OBJECT
public:
explicit ResourcePackPage(MinecraftInstance *instance, QWidget *parent = 0)
: ExternalResourcesPage(instance, instance->resourcePackList(), parent)
explicit ResourcePackPage(MinecraftInstance *instance, std::shared_ptr<ResourcePackFolderModel> model, QWidget *parent = 0)
: ExternalResourcesPage(instance, model, parent)
{
ui->actionViewConfigs->setVisible(false);
}

View File

@ -38,12 +38,14 @@
#include "ExternalResourcesPage.h"
#include "ui_ExternalResourcesPage.h"
#include "minecraft/mod/ShaderPackFolderModel.h"
class ShaderPackPage : public ExternalResourcesPage
{
Q_OBJECT
public:
explicit ShaderPackPage(MinecraftInstance *instance, QWidget *parent = 0)
: ExternalResourcesPage(instance, instance->shaderPackList(), parent)
explicit ShaderPackPage(MinecraftInstance *instance, std::shared_ptr<ShaderPackFolderModel> model, QWidget *parent = 0)
: ExternalResourcesPage(instance, model, parent)
{
ui->actionViewConfigs->setVisible(false);
}

View File

@ -38,12 +38,14 @@
#include "ExternalResourcesPage.h"
#include "ui_ExternalResourcesPage.h"
#include "minecraft/mod/TexturePackFolderModel.h"
class TexturePackPage : public ExternalResourcesPage
{
Q_OBJECT
public:
explicit TexturePackPage(MinecraftInstance *instance, QWidget *parent = 0)
: ExternalResourcesPage(instance, instance->texturePackList(), parent)
explicit TexturePackPage(MinecraftInstance *instance, std::shared_ptr<TexturePackFolderModel> model, QWidget *parent = 0)
: ExternalResourcesPage(instance, model, parent)
{
ui->actionViewConfigs->setVisible(false);
}

View File

@ -196,10 +196,10 @@ void VersionPage::packageCurrent(const QModelIndex &current, const QModelIndex &
switch(severity)
{
case ProblemSeverity::Warning:
ui->frame->setModText(tr("%1 possibly has issues.").arg(patch->getName()));
ui->frame->setName(tr("%1 possibly has issues.").arg(patch->getName()));
break;
case ProblemSeverity::Error:
ui->frame->setModText(tr("%1 has issues!").arg(patch->getName()));
ui->frame->setName(tr("%1 has issues!").arg(patch->getName()));
break;
default:
case ProblemSeverity::None:
@ -222,7 +222,7 @@ void VersionPage::packageCurrent(const QModelIndex &current, const QModelIndex &
problemOut += problem.m_description;
problemOut += "\n";
}
ui->frame->setModDescription(problemOut);
ui->frame->setDescription(problemOut);
}
void VersionPage::updateRunningStatus(bool running)

View File

@ -64,7 +64,7 @@
</layout>
</item>
<item>
<widget class="MCModInfoFrame" name="frame">
<widget class="InfoFrame" name="frame">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Minimum">
<horstretch>0</horstretch>
@ -278,9 +278,9 @@
<header>ui/widgets/ModListView.h</header>
</customwidget>
<customwidget>
<class>MCModInfoFrame</class>
<class>InfoFrame</class>
<extends>QFrame</extends>
<header>ui/widgets/MCModInfoFrame.h</header>
<header>ui/widgets/InfoFrame.h</header>
<container>1</container>
</customwidget>
<customwidget>

View File

@ -14,16 +14,30 @@
*/
#include <QMessageBox>
#include <QtGui>
#include "MCModInfoFrame.h"
#include "ui_MCModInfoFrame.h"
#include "InfoFrame.h"
#include "ui_InfoFrame.h"
#include "ui/dialogs/CustomMessageBox.h"
void MCModInfoFrame::updateWithMod(Mod &m)
InfoFrame::InfoFrame(QWidget *parent) :
QFrame(parent),
ui(new Ui::InfoFrame)
{
if (m.type() == m.MOD_FOLDER)
ui->setupUi(this);
ui->descriptionLabel->setHidden(true);
ui->nameLabel->setHidden(true);
updateHiddenState();
}
InfoFrame::~InfoFrame()
{
delete ui;
}
void InfoFrame::updateWithMod(Mod const& m)
{
if (m.type() == ResourceType::FOLDER)
{
clear();
return;
@ -43,42 +57,32 @@ void MCModInfoFrame::updateWithMod(Mod &m)
if (!m.authors().isEmpty())
text += " by " + m.authors().join(", ");
setModText(text);
setName(text);
if (m.description().isEmpty())
{
setModDescription(QString());
setDescription(QString());
}
else
{
setModDescription(m.description());
setDescription(m.description());
}
}
void MCModInfoFrame::clear()
void InfoFrame::updateWithResource(const Resource& resource)
{
setModText(QString());
setModDescription(QString());
setName(resource.name());
}
MCModInfoFrame::MCModInfoFrame(QWidget *parent) :
QFrame(parent),
ui(new Ui::MCModInfoFrame)
void InfoFrame::clear()
{
ui->setupUi(this);
ui->label_ModDescription->setHidden(true);
ui->label_ModText->setHidden(true);
updateHiddenState();
setName();
setDescription();
}
MCModInfoFrame::~MCModInfoFrame()
void InfoFrame::updateHiddenState()
{
delete ui;
}
void MCModInfoFrame::updateHiddenState()
{
if(ui->label_ModDescription->isHidden() && ui->label_ModText->isHidden())
if(ui->descriptionLabel->isHidden() && ui->nameLabel->isHidden())
{
setHidden(true);
}
@ -88,34 +92,34 @@ void MCModInfoFrame::updateHiddenState()
}
}
void MCModInfoFrame::setModText(QString text)
void InfoFrame::setName(QString text)
{
if(text.isEmpty())
{
ui->label_ModText->setHidden(true);
ui->nameLabel->setHidden(true);
}
else
{
ui->label_ModText->setText(text);
ui->label_ModText->setHidden(false);
ui->nameLabel->setText(text);
ui->nameLabel->setHidden(false);
}
updateHiddenState();
}
void MCModInfoFrame::setModDescription(QString text)
void InfoFrame::setDescription(QString text)
{
if(text.isEmpty())
{
ui->label_ModDescription->setHidden(true);
ui->descriptionLabel->setHidden(true);
updateHiddenState();
return;
}
else
{
ui->label_ModDescription->setHidden(false);
ui->descriptionLabel->setHidden(false);
updateHiddenState();
}
ui->label_ModDescription->setToolTip("");
ui->descriptionLabel->setToolTip("");
QString intermediatetext = text.trimmed();
bool prev(false);
QChar rem('\n');
@ -133,36 +137,36 @@ void MCModInfoFrame::setModDescription(QString text)
labeltext.reserve(300);
if(finaltext.length() > 290)
{
ui->label_ModDescription->setOpenExternalLinks(false);
ui->label_ModDescription->setTextFormat(Qt::TextFormat::RichText);
desc = text;
ui->descriptionLabel->setOpenExternalLinks(false);
ui->descriptionLabel->setTextFormat(Qt::TextFormat::RichText);
m_description = text;
// This allows injecting HTML here.
labeltext.append("<html><body>" + finaltext.left(287) + "<a href=\"#mod_desc\">...</a></body></html>");
QObject::connect(ui->label_ModDescription, &QLabel::linkActivated, this, &MCModInfoFrame::modDescEllipsisHandler);
QObject::connect(ui->descriptionLabel, &QLabel::linkActivated, this, &InfoFrame::descriptionEllipsisHandler);
}
else
{
ui->label_ModDescription->setTextFormat(Qt::TextFormat::PlainText);
ui->descriptionLabel->setTextFormat(Qt::TextFormat::PlainText);
labeltext.append(finaltext);
}
ui->label_ModDescription->setText(labeltext);
ui->descriptionLabel->setText(labeltext);
}
void MCModInfoFrame::modDescEllipsisHandler(const QString &link)
void InfoFrame::descriptionEllipsisHandler(QString link)
{
if(!currentBox)
if(!m_current_box)
{
currentBox = CustomMessageBox::selectable(this, QString(), desc);
connect(currentBox, &QMessageBox::finished, this, &MCModInfoFrame::boxClosed);
currentBox->show();
m_current_box = CustomMessageBox::selectable(this, "", m_description);
connect(m_current_box, &QMessageBox::finished, this, &InfoFrame::boxClosed);
m_current_box->show();
}
else
{
currentBox->setText(desc);
m_current_box->setText(m_description);
}
}
void MCModInfoFrame::boxClosed(int result)
void InfoFrame::boxClosed(int result)
{
currentBox = nullptr;
m_current_box = nullptr;
}

View File

@ -16,37 +16,39 @@
#pragma once
#include <QFrame>
#include "minecraft/mod/Mod.h"
#include "minecraft/mod/ResourcePack.h"
namespace Ui
{
class MCModInfoFrame;
class InfoFrame;
}
class MCModInfoFrame : public QFrame
{
class InfoFrame : public QFrame {
Q_OBJECT
public:
explicit MCModInfoFrame(QWidget *parent = 0);
~MCModInfoFrame();
public:
InfoFrame(QWidget* parent = nullptr);
~InfoFrame() override;
void setModText(QString text);
void setModDescription(QString text);
void setName(QString text = {});
void setDescription(QString text = {});
void updateWithMod(Mod &m);
void clear();
public slots:
void modDescEllipsisHandler(const QString& link );
void updateWithMod(Mod const& m);
void updateWithResource(Resource const& resource);
public slots:
void descriptionEllipsisHandler(QString link);
void boxClosed(int result);
private:
private:
void updateHiddenState();
private:
Ui::MCModInfoFrame *ui;
QString desc;
class QMessageBox * currentBox = nullptr;
private:
Ui::InfoFrame* ui;
QString m_description;
class QMessageBox* m_current_box = nullptr;
};

View File

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>MCModInfoFrame</class>
<widget class="QFrame" name="MCModInfoFrame">
<class>InfoFrame</class>
<widget class="QFrame" name="InfoFrame">
<property name="geometry">
<rect>
<x>0</x>
@ -39,7 +39,7 @@
<number>0</number>
</property>
<item>
<widget class="QLabel" name="label_ModText">
<widget class="QLabel" name="nameLabel">
<property name="text">
<string notr="true"/>
</property>
@ -61,7 +61,7 @@
</widget>
</item>
<item>
<widget class="QLabel" name="label_ModDescription">
<widget class="QLabel" name="descriptionLabel">
<property name="toolTip">
<string notr="true"/>
</property>