From 03280cc62e75f8073f8d3d9e9e3952acf21fa77d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Mr=C3=A1zek?= Date: Sun, 15 Jul 2018 14:04:09 +0200 Subject: NOISSUE separate new mods model from the simple one It should list mods in various locations... --- api/logic/CMakeLists.txt | 10 +- api/logic/minecraft/MinecraftInstance.cpp | 34 ++- api/logic/minecraft/MinecraftInstance.h | 22 +- api/logic/minecraft/ModList.cpp | 367 ------------------------- api/logic/minecraft/ModList.h | 121 --------- api/logic/minecraft/ModList_test.cpp | 53 ---- api/logic/minecraft/ModsModel.cpp | 374 ++++++++++++++++++++++++++ api/logic/minecraft/ModsModel.h | 123 +++++++++ api/logic/minecraft/SimpleModList.cpp | 367 +++++++++++++++++++++++++ api/logic/minecraft/SimpleModList.h | 121 +++++++++ api/logic/minecraft/SimpleModList_test.cpp | 53 ++++ api/logic/minecraft/legacy/LegacyInstance.cpp | 2 +- api/logic/minecraft/legacy/LegacyInstance.h | 2 +- 13 files changed, 1084 insertions(+), 565 deletions(-) delete mode 100644 api/logic/minecraft/ModList.cpp delete mode 100644 api/logic/minecraft/ModList.h delete mode 100644 api/logic/minecraft/ModList_test.cpp create mode 100644 api/logic/minecraft/ModsModel.cpp create mode 100644 api/logic/minecraft/ModsModel.h create mode 100644 api/logic/minecraft/SimpleModList.cpp create mode 100644 api/logic/minecraft/SimpleModList.h create mode 100644 api/logic/minecraft/SimpleModList_test.cpp (limited to 'api/logic') diff --git a/api/logic/CMakeLists.txt b/api/logic/CMakeLists.txt index a320ab8b..769f112e 100644 --- a/api/logic/CMakeLists.txt +++ b/api/logic/CMakeLists.txt @@ -278,8 +278,10 @@ set(MINECRAFT_SOURCES minecraft/VersionFilterData.cpp minecraft/Mod.h minecraft/Mod.cpp - minecraft/ModList.h - minecraft/ModList.cpp + minecraft/ModsModel.h + minecraft/ModsModel.cpp + minecraft/SimpleModList.h + minecraft/SimpleModList.cpp minecraft/World.h minecraft/World.cpp minecraft/WorldList.h @@ -315,8 +317,8 @@ add_unit_test(Library ) # FIXME: shares data with FileSystem test -add_unit_test(ModList - SOURCES minecraft/ModList_test.cpp +add_unit_test(SimpleModList + SOURCES minecraft/SimpleModList_test.cpp DATA testdata LIBS MultiMC_logic ) diff --git a/api/logic/minecraft/MinecraftInstance.cpp b/api/logic/minecraft/MinecraftInstance.cpp index 0fffc99f..e9f67b31 100644 --- a/api/logic/minecraft/MinecraftInstance.cpp +++ b/api/logic/minecraft/MinecraftInstance.cpp @@ -25,7 +25,8 @@ #include "meta/Index.h" #include "meta/VersionList.h" -#include "ModList.h" +#include "SimpleModList.h" +#include "ModsModel.h" #include "WorldList.h" #include "icons/IIconList.h" @@ -178,6 +179,11 @@ QString MinecraftInstance::loaderModsDir() const return FS::PathCombine(minecraftRoot(), "mods"); } +QString MinecraftInstance::modsCacheLocation() const +{ + return FS::PathCombine(instanceRoot(), "mods.cache"); +} + QString MinecraftInstance::coreModsDir() const { return FS::PathCombine(minecraftRoot(), "coremods"); @@ -884,41 +890,51 @@ JavaVersion MinecraftInstance::getJavaVersion() const return JavaVersion(settings()->get("JavaVersion").toString()); } -std::shared_ptr MinecraftInstance::loaderModList() const +std::shared_ptr MinecraftInstance::loaderModList() const { if (!m_loader_mod_list) { - m_loader_mod_list.reset(new ModList(loaderModsDir())); + m_loader_mod_list.reset(new SimpleModList(loaderModsDir())); } m_loader_mod_list->update(); return m_loader_mod_list; } -std::shared_ptr MinecraftInstance::coreModList() const +std::shared_ptr MinecraftInstance::modsModel() const +{ + if (!m_mods_model) + { + m_mods_model.reset(new ModsModel(loaderModsDir(), coreModsDir(), modsCacheLocation())); + } + m_mods_model->update(); + return m_mods_model; +} + +std::shared_ptr MinecraftInstance::coreModList() const { if (!m_core_mod_list) { - m_core_mod_list.reset(new ModList(coreModsDir())); + m_core_mod_list.reset(new SimpleModList(coreModsDir())); } m_core_mod_list->update(); return m_core_mod_list; } -std::shared_ptr MinecraftInstance::resourcePackList() const +std::shared_ptr MinecraftInstance::resourcePackList() const { if (!m_resource_pack_list) { - m_resource_pack_list.reset(new ModList(resourcePacksDir())); + m_resource_pack_list.reset(new SimpleModList(resourcePacksDir())); } m_resource_pack_list->update(); return m_resource_pack_list; } -std::shared_ptr MinecraftInstance::texturePackList() const +std::shared_ptr MinecraftInstance::texturePackList() const { if (!m_texture_pack_list) { - m_texture_pack_list.reset(new ModList(texturePacksDir())); + m_texture_pack_list.reset(new SimpleModList(texturePacksDir())); } m_texture_pack_list->update(); return m_texture_pack_list; diff --git a/api/logic/minecraft/MinecraftInstance.h b/api/logic/minecraft/MinecraftInstance.h index 3be15b1c..f1499ef6 100644 --- a/api/logic/minecraft/MinecraftInstance.h +++ b/api/logic/minecraft/MinecraftInstance.h @@ -6,7 +6,8 @@ #include #include "multimc_logic_export.h" -class ModList; +class ModsModel; +class SimpleModList; class WorldList; class LaunchStep; class ComponentList; @@ -41,6 +42,7 @@ public: QString texturePacksDir() const; QString loaderModsDir() const; QString coreModsDir() const; + QString modsCacheLocation() const; QString libDir() const; QString worldDir() const; QDir jarmodsPath() const; @@ -57,10 +59,11 @@ public: std::shared_ptr getComponentList() const; ////// Mod Lists ////// - std::shared_ptr loaderModList() const; - std::shared_ptr coreModList() const; - std::shared_ptr resourcePackList() const; - std::shared_ptr texturePackList() const; + std::shared_ptr modsModel() const; + std::shared_ptr loaderModList() const; + std::shared_ptr coreModList() const; + std::shared_ptr resourcePackList() const; + std::shared_ptr texturePackList() const; std::shared_ptr worldList() const; @@ -114,10 +117,11 @@ private: protected: // data std::shared_ptr m_components; - mutable std::shared_ptr m_loader_mod_list; - mutable std::shared_ptr m_core_mod_list; - mutable std::shared_ptr m_resource_pack_list; - mutable std::shared_ptr m_texture_pack_list; + mutable std::shared_ptr m_mods_model; + mutable std::shared_ptr m_loader_mod_list; + mutable std::shared_ptr m_core_mod_list; + mutable std::shared_ptr m_resource_pack_list; + mutable std::shared_ptr m_texture_pack_list; mutable std::shared_ptr m_world_list; }; diff --git a/api/logic/minecraft/ModList.cpp b/api/logic/minecraft/ModList.cpp deleted file mode 100644 index 6ccf20e2..00000000 --- a/api/logic/minecraft/ModList.cpp +++ /dev/null @@ -1,367 +0,0 @@ -/* Copyright 2013-2018 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 "ModList.h" -#include -#include -#include -#include -#include -#include -#include - -ModList::ModList(const QString &dir) : QAbstractListModel(), m_dir(dir) -{ - FS::ensureFolderPathExists(m_dir.absolutePath()); - m_dir.setFilter(QDir::Readable | QDir::NoDotAndDotDot | QDir::Files | QDir::Dirs | - QDir::NoSymLinks); - 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 ModList::startWatching() -{ - if(is_watching) - return; - - update(); - - 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(); - } -} - -void ModList::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(); - } -} - -bool ModList::update() -{ - if (!isValid()) - return false; - - QList orderedMods; - QList newMods; - m_dir.refresh(); - auto folderContents = m_dir.entryInfoList(); - bool orderOrStateChanged = false; - - // if there are any untracked files... - if (folderContents.size()) - { - // the order surely changed! - for (auto entry : folderContents) - { - newMods.append(Mod(entry)); - } - orderedMods.append(newMods); - orderOrStateChanged = true; - } - // otherwise, if we were already tracking some mods - else if (mods.size()) - { - // if the number doesn't match, order changed. - if (mods.size() != orderedMods.size()) - orderOrStateChanged = true; - // if it does match, compare the mods themselves - else - for (int i = 0; i < mods.size(); i++) - { - if (!mods[i].strongCompare(orderedMods[i])) - { - orderOrStateChanged = true; - break; - } - } - } - beginResetModel(); - mods.swap(orderedMods); - endResetModel(); - if (orderOrStateChanged) - { - emit changed(); - } - return true; -} - -void ModList::directoryChanged(QString path) -{ - update(); -} - -bool ModList::isValid() -{ - return m_dir.exists() && m_dir.isReadable(); -} - -bool ModList::installMod(const QString &filename) -{ - // NOTE: fix for GH-1178: remove trailing slash to avoid issues with using the empty result of QFileInfo::fileName - QFileInfo fileinfo(FS::NormalizePath(filename)); - - qDebug() << "installing: " << fileinfo.absoluteFilePath(); - - if (!fileinfo.exists() || !fileinfo.isReadable()) - { - return false; - } - Mod m(fileinfo); - if (!m.valid()) - return false; - - auto type = m.type(); - if (type == Mod::MOD_UNKNOWN) - return false; - if (type == Mod::MOD_SINGLEFILE || type == Mod::MOD_ZIPFILE || type == Mod::MOD_LITEMOD) - { - QString newpath = FS::PathCombine(m_dir.path(), fileinfo.fileName()); - if (!QFile::copy(fileinfo.filePath(), newpath)) - return false; - FS::updateTimestamp(newpath); - m.repath(newpath); - update(); - return true; - } - else if (type == Mod::MOD_FOLDER) - { - QString from = fileinfo.filePath(); - QString to = FS::PathCombine(m_dir.path(), fileinfo.fileName()); - if (!FS::copy(from, to)()) - return false; - m.repath(to); - update(); - return true; - } - return false; -} - -bool ModList::enableMods(const QModelIndexList& indexes, bool enable) -{ - if(indexes.isEmpty()) - return true; - - for (auto i: indexes) - { - Mod &m = mods[i.row()]; - m.enable(enable); - emit dataChanged(i, i); - } - emit changed(); - return true; -} - -bool ModList::deleteMods(const QModelIndexList& indexes) -{ - if(indexes.isEmpty()) - return true; - - for (auto i: indexes) - { - Mod &m = mods[i.row()]; - m.destroy(); - } - emit changed(); - return true; -} - -int ModList::columnCount(const QModelIndex &parent) const -{ - return NUM_COLUMNS; -} - -QVariant ModList::data(const QModelIndex &index, int role) const -{ - if (!index.isValid()) - return QVariant(); - - 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(); - case VersionColumn: - return mods[row].version(); - case DateColumn: - return mods[row].dateTimeChanged(); - - default: - return QVariant(); - } - - case Qt::ToolTipRole: - return mods[row].mmc_id(); - - case Qt::CheckStateRole: - switch (column) - { - case ActiveColumn: - return mods[row].enabled() ? Qt::Checked : Qt::Unchecked; - default: - return QVariant(); - } - default: - return QVariant(); - } -} - -bool ModList::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) - { - auto &mod = mods[index.row()]; - if (mod.enable(!mod.enabled())) - { - emit dataChanged(index, index); - return true; - } - } - return false; -} - -QVariant ModList::headerData(int section, Qt::Orientation orientation, int role) const -{ - switch (role) - { - case Qt::DisplayRole: - switch (section) - { - case ActiveColumn: - return QString(); - case NameColumn: - return tr("Name"); - case VersionColumn: - return tr("Version"); - case DateColumn: - return tr("Last changed"); - default: - return QVariant(); - } - - case Qt::ToolTipRole: - switch (section) - { - case ActiveColumn: - return tr("Is the mod enabled?"); - case NameColumn: - return tr("The name of the mod."); - case VersionColumn: - return tr("The version of the mod."); - case DateColumn: - return tr("The date and time this mod was last changed (or added)."); - default: - return QVariant(); - } - default: - return QVariant(); - } - return QVariant(); -} - -Qt::ItemFlags ModList::flags(const QModelIndex &index) const -{ - Qt::ItemFlags defaultFlags = QAbstractListModel::flags(index); - if (index.isValid()) - return Qt::ItemIsUserCheckable | Qt::ItemIsDropEnabled | - defaultFlags; - else - return Qt::ItemIsDropEnabled | defaultFlags; -} - -Qt::DropActions ModList::supportedDropActions() const -{ - // copy from outside, move from within and other mod lists - return Qt::CopyAction | Qt::MoveAction; -} - -QStringList ModList::mimeTypes() const -{ - QStringList types; - types << "text/uri-list"; - return types; -} - -bool ModList::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()) - { - bool was_watching = is_watching; - if (was_watching) - { - stopWatching(); - } - 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 (was_watching) - { - startWatching(); - } - return true; - } - return false; -} diff --git a/api/logic/minecraft/ModList.h b/api/logic/minecraft/ModList.h deleted file mode 100644 index 72f50edb..00000000 --- a/api/logic/minecraft/ModList.h +++ /dev/null @@ -1,121 +0,0 @@ -/* Copyright 2013-2018 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. - */ - -#pragma once - -#include -#include -#include -#include - -#include "minecraft/Mod.h" - -#include "multimc_logic_export.h" - -class LegacyInstance; -class BaseInstance; -class QFileSystemWatcher; - -/** - * A legacy mod list. - * Backed by a folder. - */ -class MULTIMC_LOGIC_EXPORT ModList : public QAbstractListModel -{ - Q_OBJECT -public: - enum Columns - { - ActiveColumn = 0, - NameColumn, - DateColumn, - VersionColumn, - NUM_COLUMNS - }; - ModList(const QString &dir); - - 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; - - /// 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; - - 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]; - } - - /// Reloads the mod list and returns true if the list changed. - virtual bool update(); - - /** - * Adds the given mod to the list at the given index - if the list supports custom ordering - */ - bool installMod(const QString& filename); - - /// Deletes all the selected mods - virtual bool deleteMods(const QModelIndexList &indexes); - - /// Enable or disable listed mods - virtual bool enableMods(const QModelIndexList &indexes, bool enable = true); - - void startWatching(); - void stopWatching(); - - virtual bool isValid(); - - QDir dir() - { - return m_dir; - } - - const QList & allMods() - { - return mods; - } - -private -slots: - void directoryChanged(QString path); - -signals: - void changed(); - -protected: - QFileSystemWatcher *m_watcher; - bool is_watching = false; - QDir m_dir; - QList mods; -}; diff --git a/api/logic/minecraft/ModList_test.cpp b/api/logic/minecraft/ModList_test.cpp deleted file mode 100644 index 155c238a..00000000 --- a/api/logic/minecraft/ModList_test.cpp +++ /dev/null @@ -1,53 +0,0 @@ - -#include -#include -#include "TestUtil.h" - -#include "FileSystem.h" -#include "minecraft/ModList.h" - -class ModListTest : 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("data/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; - ModList m(tempDir.path()); - m.installMod(folder); - verify(tempDir.path()); - } - - // 2. test with trailing / - { - QString folder = source + '/'; - QTemporaryDir tempDir; - ModList m(tempDir.path()); - m.installMod(folder); - verify(tempDir.path()); - } - } -}; - -QTEST_GUILESS_MAIN(ModListTest) - -#include "ModList_test.moc" diff --git a/api/logic/minecraft/ModsModel.cpp b/api/logic/minecraft/ModsModel.cpp new file mode 100644 index 00000000..ff99ad4a --- /dev/null +++ b/api/logic/minecraft/ModsModel.cpp @@ -0,0 +1,374 @@ +/* Copyright 2013-2018 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 "ModsModel.h" +#include +#include +#include +#include +#include +#include +#include + +ModsModel::ModsModel(const QString &mainDir, const QString &coreDir, const QString &cacheLocation) + :QAbstractListModel(), m_mainDir(mainDir), m_coreDir(coreDir) +{ + FS::ensureFolderPathExists(m_mainDir.absolutePath()); + m_mainDir.setFilter(QDir::Readable | QDir::NoDotAndDotDot | QDir::Files | QDir::Dirs | + QDir::NoSymLinks); + m_mainDir.setSorting(QDir::Name | QDir::IgnoreCase | QDir::LocaleAware); + m_watcher = new QFileSystemWatcher(this); + connect(m_watcher, SIGNAL(directoryChanged(QString)), this, SLOT(directoryChanged(QString))); +} + +void ModsModel::startWatching() +{ + if(is_watching) + return; + + update(); + + is_watching = m_watcher->addPath(m_mainDir.absolutePath()); + if (is_watching) + { + qDebug() << "Started watching " << m_mainDir.absolutePath(); + } + else + { + qDebug() << "Failed to start watching " << m_mainDir.absolutePath(); + } +} + +void ModsModel::stopWatching() +{ + if(!is_watching) + return; + + is_watching = !m_watcher->removePath(m_mainDir.absolutePath()); + if (!is_watching) + { + qDebug() << "Stopped watching " << m_mainDir.absolutePath(); + } + else + { + qDebug() << "Failed to stop watching " << m_mainDir.absolutePath(); + } +} + +bool ModsModel::update() +{ + if (!isValid()) + return false; + + QList orderedMods; + QList newMods; + m_mainDir.refresh(); + auto folderContents = m_mainDir.entryInfoList(); + bool orderOrStateChanged = false; + + // if there are any untracked files... + if (folderContents.size()) + { + // the order surely changed! + for (auto entry : folderContents) + { + newMods.append(Mod(entry)); + } + orderedMods.append(newMods); + orderOrStateChanged = true; + } + // otherwise, if we were already tracking some mods + else if (mods.size()) + { + // if the number doesn't match, order changed. + if (mods.size() != orderedMods.size()) + orderOrStateChanged = true; + // if it does match, compare the mods themselves + else + for (int i = 0; i < mods.size(); i++) + { + if (!mods[i].strongCompare(orderedMods[i])) + { + orderOrStateChanged = true; + break; + } + } + } + beginResetModel(); + mods.swap(orderedMods); + endResetModel(); + if (orderOrStateChanged) + { + emit changed(); + } + return true; +} + +void ModsModel::directoryChanged(QString path) +{ + update(); +} + +bool ModsModel::isValid() +{ + return m_mainDir.exists() && m_mainDir.isReadable(); +} + +bool ModsModel::installMod(const QString &filename) +{ + // NOTE: fix for GH-1178: remove trailing slash to avoid issues with using the empty result of QFileInfo::fileName + QFileInfo fileinfo(FS::NormalizePath(filename)); + + qDebug() << "installing: " << fileinfo.absoluteFilePath(); + + if (!fileinfo.exists() || !fileinfo.isReadable()) + { + return false; + } + Mod m(fileinfo); + if (!m.valid()) + return false; + + auto type = m.type(); + if (type == Mod::MOD_UNKNOWN) + return false; + if (type == Mod::MOD_SINGLEFILE || type == Mod::MOD_ZIPFILE || type == Mod::MOD_LITEMOD) + { + QString newpath = FS::PathCombine(m_mainDir.path(), fileinfo.fileName()); + if (!QFile::copy(fileinfo.filePath(), newpath)) + return false; + FS::updateTimestamp(newpath); + m.repath(newpath); + update(); + return true; + } + else if (type == Mod::MOD_FOLDER) + { + QString from = fileinfo.filePath(); + QString to = FS::PathCombine(m_mainDir.path(), fileinfo.fileName()); + if (!FS::copy(from, to)()) + return false; + m.repath(to); + update(); + return true; + } + return false; +} + +bool ModsModel::enableMods(const QModelIndexList& indexes, bool enable) +{ + if(indexes.isEmpty()) + return true; + + for (auto i: indexes) + { + Mod &m = mods[i.row()]; + m.enable(enable); + emit dataChanged(i, i); + } + emit changed(); + return true; +} + +bool ModsModel::deleteMods(const QModelIndexList& indexes) +{ + if(indexes.isEmpty()) + return true; + + for (auto i: indexes) + { + Mod &m = mods[i.row()]; + m.destroy(); + } + emit changed(); + return true; +} + +int ModsModel::columnCount(const QModelIndex &parent) const +{ + return NUM_COLUMNS; +} + +QVariant ModsModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid()) + return QVariant(); + + 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(); + case VersionColumn: + return mods[row].version(); + case DateColumn: + return mods[row].dateTimeChanged(); + case LocationColumn: + return "Unknown"; + + default: + return QVariant(); + } + + case Qt::ToolTipRole: + return mods[row].mmc_id(); + + case Qt::CheckStateRole: + switch (column) + { + case ActiveColumn: + return mods[row].enabled() ? Qt::Checked : Qt::Unchecked; + default: + return QVariant(); + } + default: + return QVariant(); + } +} + +bool ModsModel::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) + { + auto &mod = mods[index.row()]; + if (mod.enable(!mod.enabled())) + { + emit dataChanged(index, index); + return true; + } + } + return false; +} + +QVariant ModsModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + switch (role) + { + case Qt::DisplayRole: + switch (section) + { + case ActiveColumn: + return QString(); + case NameColumn: + return tr("Name"); + case VersionColumn: + return tr("Version"); + case DateColumn: + return tr("Last changed"); + case LocationColumn: + return tr("Location"); + default: + return QVariant(); + } + + case Qt::ToolTipRole: + switch (section) + { + case ActiveColumn: + return tr("Is the mod enabled?"); + case NameColumn: + return tr("The name of the mod."); + case VersionColumn: + return tr("The version of the mod."); + case DateColumn: + return tr("The date and time this mod was last changed (or added)."); + case LocationColumn: + return tr("Where the mod is located (inside or outside the instance)."); + default: + return QVariant(); + } + default: + return QVariant(); + } + return QVariant(); +} + +Qt::ItemFlags ModsModel::flags(const QModelIndex &index) const +{ + Qt::ItemFlags defaultFlags = QAbstractListModel::flags(index); + if (index.isValid()) + return Qt::ItemIsUserCheckable | Qt::ItemIsDropEnabled | + defaultFlags; + else + return Qt::ItemIsDropEnabled | defaultFlags; +} + +Qt::DropActions ModsModel::supportedDropActions() const +{ + // copy from outside, move from within and other mod lists + return Qt::CopyAction | Qt::MoveAction; +} + +QStringList ModsModel::mimeTypes() const +{ + QStringList types; + types << "text/uri-list"; + return types; +} + +bool ModsModel::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()) + { + bool was_watching = is_watching; + if (was_watching) + { + stopWatching(); + } + 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 (was_watching) + { + startWatching(); + } + return true; + } + return false; +} diff --git a/api/logic/minecraft/ModsModel.h b/api/logic/minecraft/ModsModel.h new file mode 100644 index 00000000..3c8f66fd --- /dev/null +++ b/api/logic/minecraft/ModsModel.h @@ -0,0 +1,123 @@ +/* Copyright 2013-2018 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. + */ + +#pragma once + +#include +#include +#include +#include + +#include "minecraft/Mod.h" + +#include "multimc_logic_export.h" + +class LegacyInstance; +class BaseInstance; +class QFileSystemWatcher; + +/** + * A legacy mod list. + * Backed by a folder. + */ +class MULTIMC_LOGIC_EXPORT ModsModel : public QAbstractListModel +{ + Q_OBJECT +public: + enum Columns + { + ActiveColumn = 0, + NameColumn, + DateColumn, + VersionColumn, + LocationColumn, + NUM_COLUMNS + }; + ModsModel(const QString &mainDir, const QString &coreDir, const QString &cacheFile); + + 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; + + /// 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; + + 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]; + } + + /// Reloads the mod list and returns true if the list changed. + virtual bool update(); + + /** + * Adds the given mod to the list at the given index - if the list supports custom ordering + */ + bool installMod(const QString& filename); + + /// Deletes all the selected mods + virtual bool deleteMods(const QModelIndexList &indexes); + + /// Enable or disable listed mods + virtual bool enableMods(const QModelIndexList &indexes, bool enable = true); + + void startWatching(); + void stopWatching(); + + virtual bool isValid(); + + QDir dir() + { + return m_mainDir; + } + + const QList & allMods() + { + return mods; + } + +private +slots: + void directoryChanged(QString path); + +signals: + void changed(); + +protected: + QFileSystemWatcher *m_watcher; + bool is_watching = false; + QDir m_mainDir; + QDir m_coreDir; + QList mods; +}; diff --git a/api/logic/minecraft/SimpleModList.cpp b/api/logic/minecraft/SimpleModList.cpp new file mode 100644 index 00000000..ca923226 --- /dev/null +++ b/api/logic/minecraft/SimpleModList.cpp @@ -0,0 +1,367 @@ +/* Copyright 2013-2018 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 "SimpleModList.h" +#include +#include +#include +#include +#include +#include +#include + +SimpleModList::SimpleModList(const QString &dir) : QAbstractListModel(), m_dir(dir) +{ + FS::ensureFolderPathExists(m_dir.absolutePath()); + m_dir.setFilter(QDir::Readable | QDir::NoDotAndDotDot | QDir::Files | QDir::Dirs | + QDir::NoSymLinks); + 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 SimpleModList::startWatching() +{ + if(is_watching) + return; + + update(); + + 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(); + } +} + +void SimpleModList::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(); + } +} + +bool SimpleModList::update() +{ + if (!isValid()) + return false; + + QList orderedMods; + QList newMods; + m_dir.refresh(); + auto folderContents = m_dir.entryInfoList(); + bool orderOrStateChanged = false; + + // if there are any untracked files... + if (folderContents.size()) + { + // the order surely changed! + for (auto entry : folderContents) + { + newMods.append(Mod(entry)); + } + orderedMods.append(newMods); + orderOrStateChanged = true; + } + // otherwise, if we were already tracking some mods + else if (mods.size()) + { + // if the number doesn't match, order changed. + if (mods.size() != orderedMods.size()) + orderOrStateChanged = true; + // if it does match, compare the mods themselves + else + for (int i = 0; i < mods.size(); i++) + { + if (!mods[i].strongCompare(orderedMods[i])) + { + orderOrStateChanged = true; + break; + } + } + } + beginResetModel(); + mods.swap(orderedMods); + endResetModel(); + if (orderOrStateChanged) + { + emit changed(); + } + return true; +} + +void SimpleModList::directoryChanged(QString path) +{ + update(); +} + +bool SimpleModList::isValid() +{ + return m_dir.exists() && m_dir.isReadable(); +} + +bool SimpleModList::installMod(const QString &filename) +{ + // NOTE: fix for GH-1178: remove trailing slash to avoid issues with using the empty result of QFileInfo::fileName + QFileInfo fileinfo(FS::NormalizePath(filename)); + + qDebug() << "installing: " << fileinfo.absoluteFilePath(); + + if (!fileinfo.exists() || !fileinfo.isReadable()) + { + return false; + } + Mod m(fileinfo); + if (!m.valid()) + return false; + + auto type = m.type(); + if (type == Mod::MOD_UNKNOWN) + return false; + if (type == Mod::MOD_SINGLEFILE || type == Mod::MOD_ZIPFILE || type == Mod::MOD_LITEMOD) + { + QString newpath = FS::PathCombine(m_dir.path(), fileinfo.fileName()); + if (!QFile::copy(fileinfo.filePath(), newpath)) + return false; + FS::updateTimestamp(newpath); + m.repath(newpath); + update(); + return true; + } + else if (type == Mod::MOD_FOLDER) + { + QString from = fileinfo.filePath(); + QString to = FS::PathCombine(m_dir.path(), fileinfo.fileName()); + if (!FS::copy(from, to)()) + return false; + m.repath(to); + update(); + return true; + } + return false; +} + +bool SimpleModList::enableMods(const QModelIndexList& indexes, bool enable) +{ + if(indexes.isEmpty()) + return true; + + for (auto i: indexes) + { + Mod &m = mods[i.row()]; + m.enable(enable); + emit dataChanged(i, i); + } + emit changed(); + return true; +} + +bool SimpleModList::deleteMods(const QModelIndexList& indexes) +{ + if(indexes.isEmpty()) + return true; + + for (auto i: indexes) + { + Mod &m = mods[i.row()]; + m.destroy(); + } + emit changed(); + return true; +} + +int SimpleModList::columnCount(const QModelIndex &parent) const +{ + return NUM_COLUMNS; +} + +QVariant SimpleModList::data(const QModelIndex &index, int role) const +{ + if (!index.isValid()) + return QVariant(); + + 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(); + case VersionColumn: + return mods[row].version(); + case DateColumn: + return mods[row].dateTimeChanged(); + + default: + return QVariant(); + } + + case Qt::ToolTipRole: + return mods[row].mmc_id(); + + case Qt::CheckStateRole: + switch (column) + { + case ActiveColumn: + return mods[row].enabled() ? Qt::Checked : Qt::Unchecked; + default: + return QVariant(); + } + default: + return QVariant(); + } +} + +bool SimpleModList::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) + { + auto &mod = mods[index.row()]; + if (mod.enable(!mod.enabled())) + { + emit dataChanged(index, index); + return true; + } + } + return false; +} + +QVariant SimpleModList::headerData(int section, Qt::Orientation orientation, int role) const +{ + switch (role) + { + case Qt::DisplayRole: + switch (section) + { + case ActiveColumn: + return QString(); + case NameColumn: + return tr("Name"); + case VersionColumn: + return tr("Version"); + case DateColumn: + return tr("Last changed"); + default: + return QVariant(); + } + + case Qt::ToolTipRole: + switch (section) + { + case ActiveColumn: + return tr("Is the mod enabled?"); + case NameColumn: + return tr("The name of the mod."); + case VersionColumn: + return tr("The version of the mod."); + case DateColumn: + return tr("The date and time this mod was last changed (or added)."); + default: + return QVariant(); + } + default: + return QVariant(); + } + return QVariant(); +} + +Qt::ItemFlags SimpleModList::flags(const QModelIndex &index) const +{ + Qt::ItemFlags defaultFlags = QAbstractListModel::flags(index); + if (index.isValid()) + return Qt::ItemIsUserCheckable | Qt::ItemIsDropEnabled | + defaultFlags; + else + return Qt::ItemIsDropEnabled | defaultFlags; +} + +Qt::DropActions SimpleModList::supportedDropActions() const +{ + // copy from outside, move from within and other mod lists + return Qt::CopyAction | Qt::MoveAction; +} + +QStringList SimpleModList::mimeTypes() const +{ + QStringList types; + types << "text/uri-list"; + return types; +} + +bool SimpleModList::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()) + { + bool was_watching = is_watching; + if (was_watching) + { + stopWatching(); + } + 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 (was_watching) + { + startWatching(); + } + return true; + } + return false; +} diff --git a/api/logic/minecraft/SimpleModList.h b/api/logic/minecraft/SimpleModList.h new file mode 100644 index 00000000..3257edbe --- /dev/null +++ b/api/logic/minecraft/SimpleModList.h @@ -0,0 +1,121 @@ +/* Copyright 2013-2018 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. + */ + +#pragma once + +#include +#include +#include +#include + +#include "minecraft/Mod.h" + +#include "multimc_logic_export.h" + +class LegacyInstance; +class BaseInstance; +class QFileSystemWatcher; + +/** + * A legacy mod list. + * Backed by a folder. + */ +class MULTIMC_LOGIC_EXPORT SimpleModList : public QAbstractListModel +{ + Q_OBJECT +public: + enum Columns + { + ActiveColumn = 0, + NameColumn, + DateColumn, + VersionColumn, + NUM_COLUMNS + }; + SimpleModList(const QString &dir); + + 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; + + /// 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; + + 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]; + } + + /// Reloads the mod list and returns true if the list changed. + virtual bool update(); + + /** + * Adds the given mod to the list at the given index - if the list supports custom ordering + */ + bool installMod(const QString& filename); + + /// Deletes all the selected mods + virtual bool deleteMods(const QModelIndexList &indexes); + + /// Enable or disable listed mods + virtual bool enableMods(const QModelIndexList &indexes, bool enable = true); + + void startWatching(); + void stopWatching(); + + virtual bool isValid(); + + QDir dir() + { + return m_dir; + } + + const QList & allMods() + { + return mods; + } + +private +slots: + void directoryChanged(QString path); + +signals: + void changed(); + +protected: + QFileSystemWatcher *m_watcher; + bool is_watching = false; + QDir m_dir; + QList mods; +}; diff --git a/api/logic/minecraft/SimpleModList_test.cpp b/api/logic/minecraft/SimpleModList_test.cpp new file mode 100644 index 00000000..28ce8471 --- /dev/null +++ b/api/logic/minecraft/SimpleModList_test.cpp @@ -0,0 +1,53 @@ + +#include +#include +#include "TestUtil.h" + +#include "FileSystem.h" +#include "minecraft/SimpleModList.h" + +class SimpleModListTest : 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("data/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; + SimpleModList m(tempDir.path()); + m.installMod(folder); + verify(tempDir.path()); + } + + // 2. test with trailing / + { + QString folder = source + '/'; + QTemporaryDir tempDir; + SimpleModList m(tempDir.path()); + m.installMod(folder); + verify(tempDir.path()); + } + } +}; + +QTEST_GUILESS_MAIN(SimpleModListTest) + +#include "SimpleModList_test.moc" diff --git a/api/logic/minecraft/legacy/LegacyInstance.cpp b/api/logic/minecraft/legacy/LegacyInstance.cpp index 6e6d9ae6..1282eb4d 100644 --- a/api/logic/minecraft/legacy/LegacyInstance.cpp +++ b/api/logic/minecraft/legacy/LegacyInstance.cpp @@ -21,7 +21,7 @@ #include "LegacyInstance.h" #include "minecraft/legacy/LegacyModList.h" -#include "minecraft/ModList.h" +#include "minecraft/SimpleModList.h" #include "minecraft/WorldList.h" #include #include diff --git a/api/logic/minecraft/legacy/LegacyInstance.h b/api/logic/minecraft/legacy/LegacyInstance.h index 7ab89509..1b4798f6 100644 --- a/api/logic/minecraft/legacy/LegacyInstance.h +++ b/api/logic/minecraft/legacy/LegacyInstance.h @@ -20,7 +20,7 @@ #include "multimc_logic_export.h" -class ModList; +class SimpleModList; class LegacyModList; class WorldList; class Task; -- cgit v1.2.3