From 74ec078976fbd64433142e6974f30cc113047c00 Mon Sep 17 00:00:00 2001 From: Mikaël Capelle Date: Tue, 10 Nov 2020 21:38:58 +0100 Subject: Replace usage of IPlugin::isActive(). --- src/installationmanager.cpp | 32 +++++++----- src/installationmanager.h | 17 ++++-- src/mainwindow.cpp | 8 +-- src/organizercore.cpp | 3 +- src/organizerproxy.cpp | 10 ++++ src/organizerproxy.h | 8 ++- src/plugincontainer.cpp | 118 +++++++++++++++++++++++++++++++++++++---- src/plugincontainer.h | 125 +++++++++++++++++++++++++++++++++++++++----- src/previewgenerator.cpp | 11 ++-- src/previewgenerator.h | 6 ++- src/problemsdialog.cpp | 15 +++--- src/problemsdialog.h | 5 +- src/proxyutils.h | 2 +- 13 files changed, 296 insertions(+), 64 deletions(-) (limited to 'src') diff --git a/src/installationmanager.cpp b/src/installationmanager.cpp index e9773a95..06909994 100644 --- a/src/installationmanager.cpp +++ b/src/installationmanager.cpp @@ -78,7 +78,8 @@ static T resolveFunction(QLibrary &lib, const char *name) } InstallationManager::InstallationManager() - : m_ParentWidget(nullptr), + : + m_ParentWidget(nullptr), m_SupportedExtensions({"zip", "rar", "7z", "fomod", "001"}), m_IsRunning(false) { m_ArchiveHandler = CreateArchive(); @@ -106,7 +107,7 @@ InstallationManager::InstallationManager() // Connect the query password slot - This is the only way I found to be able to query user // from a separate thread. We use a BlockingQueuedConnection so that calling passwordRequested() // will block until the end of the slot. - connect(this, &InstallationManager::passwordRequested, + connect(this, &InstallationManager::passwordRequested, this, &InstallationManager::queryPassword, Qt::BlockingQueuedConnection); } @@ -122,6 +123,11 @@ void InstallationManager::setParentWidget(QWidget *widget) } } +void InstallationManager::setPluginContainer(const PluginContainer* pluginContainer) +{ + m_PluginContainer = pluginContainer; +} + void InstallationManager::queryPassword() { m_Password = QInputDialog::getText(m_ParentWidget, tr("Password required"), tr("Password"), QLineEdit::Password); @@ -284,7 +290,7 @@ QStringList InstallationManager::extractFiles(std::vector entry) +QString InstallationManager::createFile(std::shared_ptr entry) { // Use QTemporaryFile to create the temporary file with the given template: QTemporaryFile tempFile(QDir::cleanPath(QDir::tempPath() + QDir::separator() + "mo2-install")); @@ -308,11 +314,11 @@ QString InstallationManager::createFile(std::shared_ptr fileTree) +void InstallationManager::cleanCreatedFiles(std::shared_ptr fileTree) { // We simply have to check if all the entries have fileTree as a parent: for (auto it = std::begin(m_CreatedFiles); it != std::end(m_CreatedFiles); ) { - + // Find the parent - Could this be in FileTreeEntry? bool found = false; { @@ -495,12 +501,12 @@ IPluginInstaller::EInstallResult InstallationManager::doInstall(GuessedValue filesTree = + std::shared_ptr filesTree = archiveOpen ? ArchiveFileTree::makeTree(*m_ArchiveHandler) : nullptr; IPluginInstaller::EInstallResult installResult = IPluginInstaller::RESULT_NOTATTEMPTED; @@ -701,7 +707,7 @@ IPluginInstaller::EInstallResult InstallationManager::install(const QString &fil for (IPluginInstaller *installer : m_Installers) { // don't use inactive installers (installer can't be null here but vc static code analysis thinks it could) - if ((installer == nullptr) || !installer->isActive()) { + if ((installer == nullptr) || !m_PluginContainer->isEnabled(installer)) { continue; } @@ -731,7 +737,7 @@ IPluginInstaller::EInstallResult InstallationManager::install(const QString &fil if (p == nullptr) { throw IncompatibilityException(tr("Invalid file tree returned by plugin.")); } - + // Detach the file tree (this ensure the parent is null and call to path() // stops at this root): p->detach(); @@ -786,7 +792,7 @@ IPluginInstaller::EInstallResult InstallationManager::install(const QString &fil case IPluginInstaller::RESULT_SUCCESSCANCEL: { if (filesTree != nullptr) { auto iniTweakEntry = filesTree->find("INI Tweaks", FileTreeEntry::DIRECTORY); - hasIniTweaks = iniTweakEntry != nullptr + hasIniTweaks = iniTweakEntry != nullptr && !iniTweakEntry->astree()->empty(); } return IPluginInstaller::RESULT_SUCCESS; @@ -860,7 +866,7 @@ QStringList InstallationManager::getSupportedExtensions() const void InstallationManager::notifyInstallationStart(QString const& archive, bool reinstallation, ModInfo::Ptr currentMod) { for (auto* installer : m_Installers) { - if (installer->isActive()) { + if (m_PluginContainer->isEnabled(installer)) { installer->onInstallationStart(archive, reinstallation, currentMod.get()); } } @@ -871,7 +877,7 @@ void InstallationManager::notifyInstallationEnd( ModInfo::Ptr newMod) { for (auto* installer : m_Installers) { - if (installer->isActive()) { + if (m_PluginContainer->isEnabled(installer)) { installer->onInstallationEnd(result, newMod.get()); } } diff --git a/src/installationmanager.h b/src/installationmanager.h index 66f11eae..e1db5e1f 100644 --- a/src/installationmanager.h +++ b/src/installationmanager.h @@ -35,6 +35,7 @@ along with Mod Organizer. If not, see . #include #include "modinfo.h" +#include "plugincontainer.h" /** @@ -87,6 +88,11 @@ public: */ void setModsDirectory(const QString &modsDirectory) { m_ModsDirectory = modsDirectory; } + /** + * + */ + void setPluginContainer(const PluginContainer* pluginContainer); + /** * @brief update the directory where downloads are stored * @param downloadDirectory the download directory @@ -126,8 +132,8 @@ public: * @brief register an installer-plugin * @param the installer to register */ - void registerInstaller(MOBase::IPluginInstaller *installer); - + void registerInstaller(MOBase::IPluginInstaller *installer); + /** * @return the extensions of archives supported by this installation manager. */ @@ -167,7 +173,7 @@ public: * * The flatten argument is not present here while it is present in the deprecated QStringList * version for multiple reasons: 1) it was never used, 2) it is kind of fishy because there - * is no way to know if a file is going to be overriden, 3) it is quite easy to flatten a + * is no way to know if a file is going to be overriden, 3) it is quite easy to flatten a * IFileTree and thus to given a list of entries flattened (this was not possible with the * QStringList version since these were based on the name of the file inside the archive). */ @@ -185,7 +191,7 @@ public: * @return the path to the created file. */ virtual QString createFile(std::shared_ptr entry) override; - + /** * @brief Installs the given archive. * @@ -279,6 +285,9 @@ private: private: + // The plugin container, mostly to check if installer are enabled or not: + const PluginContainer *m_PluginContainer; + bool m_IsRunning; QWidget *m_ParentWidget; diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 94bc5800..93ca415d 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -1132,7 +1132,7 @@ void MainWindow::checkForProblemsImpl() size_t numProblems = 0; for (QObject *pluginObj : m_PluginContainer.plugins()) { IPlugin *plugin = qobject_cast(pluginObj); - if (plugin == nullptr || plugin->isActive()) { + if (plugin == nullptr || m_PluginContainer.isEnabled(plugin)) { IPluginDiagnose *diagnose = qobject_cast(pluginObj); if (diagnose != nullptr) numProblems += diagnose->activeProblems().size(); @@ -1663,9 +1663,11 @@ void MainWindow::registerPluginTools(std::vector toolPlugins) } ); + // TODO: I don't know when this method is called? Maybe the check should be perform when + // the context menu is opened? // Remove inactive plugins toolPlugins.erase( - std::remove_if(toolPlugins.begin(), toolPlugins.end(), [](IPluginTool *plugin) -> bool { return !plugin->isActive(); }), + std::remove_if(toolPlugins.begin(), toolPlugins.end(), [this](IPluginTool *plugin) { return !m_PluginContainer.isEnabled(plugin); }), toolPlugins.end() ); @@ -5972,7 +5974,7 @@ void MainWindow::on_actionNotifications_triggered() future.waitForFinished(); - ProblemsDialog problems(m_PluginContainer.plugins(), this); + ProblemsDialog problems(m_PluginContainer, this); problems.exec(); scheduleCheckForProblems(); diff --git a/src/organizercore.cpp b/src/organizercore.cpp index f57903e2..86afe59a 100644 --- a/src/organizercore.cpp +++ b/src/organizercore.cpp @@ -287,6 +287,7 @@ void OrganizerCore::connectPlugins(PluginContainer *container) m_InstallationManager.getSupportedExtensions()); m_PluginContainer = container; m_Updater.setPluginContainer(m_PluginContainer); + m_InstallationManager.setPluginContainer(m_PluginContainer); m_DownloadManager.setPluginContainer(m_PluginContainer); m_ModList.setPluginContainer(m_PluginContainer); @@ -2040,7 +2041,7 @@ std::vector OrganizerCore::fileMapping(const QString &profileName, for (MOBase::IPluginFileMapper *mapper : m_PluginContainer->plugins()) { IPlugin *plugin = dynamic_cast(mapper); - if (plugin->isActive()) { + if (m_PluginContainer->isEnabled(plugin)) { MappingType pluginMap = mapper->mappings(); result.reserve(result.size() + pluginMap.size()); result.insert(result.end(), pluginMap.begin(), pluginMap.end()); diff --git a/src/organizerproxy.cpp b/src/organizerproxy.cpp index a988ba9f..77b7c2d6 100644 --- a/src/organizerproxy.cpp +++ b/src/organizerproxy.cpp @@ -83,6 +83,16 @@ void OrganizerProxy::modDataChanged(IModInterface *mod) m_Proxied->modDataChanged(mod); } +bool OrganizerProxy::isPluginEnabled(QString const& pluginName) const +{ + return m_PluginContainer->isEnabled(pluginName); +} + +bool OrganizerProxy::isPluginEnabled(IPlugin* plugin) const +{ + return m_PluginContainer->isEnabled(plugin); +} + QVariant OrganizerProxy::pluginSetting(const QString &pluginName, const QString &key) const { return m_Proxied->pluginSetting(pluginName, key); diff --git a/src/organizerproxy.h b/src/organizerproxy.h index 6690d612..4329bdda 100644 --- a/src/organizerproxy.h +++ b/src/organizerproxy.h @@ -35,8 +35,6 @@ public: virtual MOBase::IPluginGame *getGame(const QString &gameName) const; virtual MOBase::IModInterface *createMod(MOBase::GuessedValue &name); virtual void modDataChanged(MOBase::IModInterface *mod); - virtual QVariant pluginSetting(const QString &pluginName, const QString &key) const; - virtual void setPluginSetting(const QString &pluginName, const QString &key, const QVariant &value); virtual QVariant persistent(const QString &pluginName, const QString &key, const QVariant &def = QVariant()) const; virtual void setPersistent(const QString &pluginName, const QString &key, const QVariant &value, bool sync = true); virtual QString pluginDataPath() const; @@ -64,6 +62,12 @@ public: virtual bool onProfileRenamed(std::function const& func) override; virtual bool onProfileRemoved(std::function const& func) override; virtual bool onProfileChanged(std::function const& func) override; + + // Plugin related: + virtual bool isPluginEnabled(QString const& pluginName) const override; + virtual bool isPluginEnabled(MOBase::IPlugin* plugin) const override; + virtual QVariant pluginSetting(const QString& pluginName, const QString& key) const override; + virtual void setPluginSetting(const QString& pluginName, const QString& key, const QVariant& value) override; virtual bool onPluginSettingChanged(std::function const& func) override; virtual MOBase::IPluginGame const *managedGame() const; diff --git a/src/plugincontainer.cpp b/src/plugincontainer.cpp index 5992f28d..aa38803f 100644 --- a/src/plugincontainer.cpp +++ b/src/plugincontainer.cpp @@ -51,9 +51,26 @@ QStringList PluginContainer::pluginInterfaces() } +// PluginRequirementProxy + +std::vector PluginRequirementProxy::problems() const +{ + return m_Requirement->problems(m_Proxy); +} +QString PluginRequirementProxy::description(unsigned int id) const +{ + return m_Requirement->description(id); +} +PluginRequirementProxy::PluginRequirementProxy(const MOBase::PluginRequirement* requirement, OrganizerProxy* proxy) : + m_Requirement(requirement), m_Proxy(proxy) { } + + +// PluginContainer + PluginContainer::PluginContainer(OrganizerCore *organizer) : m_Organizer(organizer) , m_UserInterface(nullptr) + , m_PreviewGenerator(this) { } @@ -184,6 +201,7 @@ bool PluginContainer::initPlugin(IPlugin *plugin) if (m_Organizer) { auto* proxy = new OrganizerProxy(m_Organizer, this, plugin); + m_Proxies[plugin] = proxy; if (!plugin->init(proxy)) { log::warn("plugin failed to initialize"); @@ -191,6 +209,10 @@ bool PluginContainer::initPlugin(IPlugin *plugin) } } + for (auto* requirement : plugin->requirements()) { + m_Requirements[plugin].emplace_back(requirement); + } + return true; } @@ -202,9 +224,10 @@ bool PluginContainer::initProxyPlugin(IPlugin *plugin) return false; } - IOrganizer* proxy = nullptr; + OrganizerProxy* proxy = nullptr; if (m_Organizer) { proxy = new OrganizerProxy(m_Organizer, this, plugin); + m_Proxies[plugin] = proxy; } if (!plugin->init(proxy)) { @@ -212,6 +235,10 @@ bool PluginContainer::initProxyPlugin(IPlugin *plugin) return false; } + for (auto* requirement : plugin->requirements()) { + m_Requirements[plugin].emplace_back(requirement); + } + return true; } @@ -227,24 +254,25 @@ bool PluginContainer::registerPlugin(QObject *plugin, const QString &fileName) // way to cast directly between IPlugin* and IPluginDiagnose* bf::at_key(m_Plugins).push_back(plugin); - { // generic treatment for all plugins - IPlugin *pluginObj = qobject_cast(plugin); - if (pluginObj == nullptr) { - log::debug("not an IPlugin"); - return false; - } + // generic treatment for all plugins + IPlugin *pluginObj = qobject_cast(plugin); + if (pluginObj == nullptr) { + log::debug("not an IPlugin"); + return false; + } + bf::at_key(m_AccessPlugins)[pluginObj->name()] = pluginObj; - plugin->setProperty("filename", fileName); + plugin->setProperty("filename", fileName); - if (m_Organizer) { - m_Organizer->settings().plugins().registerPlugin(pluginObj); - } + if (m_Organizer) { + m_Organizer->settings().plugins().registerPlugin(pluginObj); } { // diagnosis plugin IPluginDiagnose *diagnose = qobject_cast(plugin); if (diagnose != nullptr) { bf::at_key(m_Plugins).push_back(diagnose); + bf::at_key(m_AccessPlugins)[diagnose] = pluginObj; m_DiagnosisConnections.push_back( diagnose->onInvalidated([&] () { emit diagnosisUpdate(); }) ); @@ -254,6 +282,7 @@ bool PluginContainer::registerPlugin(QObject *plugin, const QString &fileName) IPluginFileMapper *mapper = qobject_cast(plugin); if (mapper != nullptr) { bf::at_key(m_Plugins).push_back(mapper); + bf::at_key(m_AccessPlugins)[mapper] = pluginObj; } } { // mod page plugin @@ -383,6 +412,73 @@ void PluginContainer::unloadPlugins() } } +bool PluginContainer::isEnabled(IPlugin* plugin) const +{ + // Check if it's a game plugin: + if (implementInterface(plugin)) { + return plugin == m_Organizer->managedGame(); + } + + // Check if the plugin is enabled: + if (!m_Organizer->pluginSetting(plugin->name(), "enabled").toBool()) { + return false; + } + + // Check the requirements: + auto* proxy = m_Proxies.at(plugin); + for (auto& requirement : m_Requirements.at(plugin)) { + if (!requirement->problems(proxy).empty()) { + return false; + } + } + + return true; +} + +MOBase::IPlugin* PluginContainer::plugin(QString const& pluginName) const +{ + auto& map = bf::at_key(m_AccessPlugins); + auto it = map.find(pluginName); + if (it == std::end(map)) { + return nullptr; + } + return it->second; +} + +MOBase::IPlugin* PluginContainer::plugin(MOBase::IPluginDiagnose* diagnose) const +{ + auto& map = bf::at_key(m_AccessPlugins); + auto it = map.find(diagnose); + if (it == std::end(map)) { + return nullptr; + } + return it->second; +} + +MOBase::IPlugin* PluginContainer::plugin(MOBase::IPluginFileMapper* mapper) const +{ + auto& map = bf::at_key(m_AccessPlugins); + auto it = map.find(mapper); + if (it == std::end(map)) { + return nullptr; + } + return it->second; +} + +bool PluginContainer::isEnabled(QString const& pluginName) const { return isEnabled(plugin(pluginName)); } +bool PluginContainer::isEnabled(MOBase::IPluginDiagnose* diagnose) const { return isEnabled(plugin(diagnose)); } +bool PluginContainer::isEnabled(MOBase::IPluginFileMapper* mapper) const { return isEnabled(plugin(mapper)); } + +std::vector PluginContainer::requirements(IPlugin* plugin) const +{ + auto* proxy = m_Proxies.at(plugin); + std::vector proxies; + for (auto& requirement : m_Requirements.at(plugin)) { + proxies.push_back(PluginRequirementProxy(requirement.get(), proxy)); + } + return proxies; +} + IPluginGame *PluginContainer::managedGame(const QString &name) const { auto iter = m_SupportedGames.find(name); diff --git a/src/plugincontainer.h b/src/plugincontainer.h index 2b39726b..1a3173fb 100644 --- a/src/plugincontainer.h +++ b/src/plugincontainer.h @@ -22,6 +22,30 @@ class IUserInterface; #include #endif // Q_MOC_RUN #include +#include + + +class OrganizerProxy; + + +// Small class that allows calling check() for plugin requirements +// without passing the IOrganizer. +class PluginRequirementProxy { +public: + + std::vector problems() const; + QString description(unsigned int id) const; + +private: + + const MOBase::PluginRequirement* m_Requirement; + OrganizerProxy* m_Proxy; + + PluginRequirementProxy(const MOBase::PluginRequirement* requirement, OrganizerProxy* proxy); + + friend class PluginContainer; + +}; class PluginContainer : public QObject, public MOBase::IPluginDiagnose @@ -32,18 +56,24 @@ class PluginContainer : public QObject, public MOBase::IPluginDiagnose private: - typedef boost::fusion::map< - boost::fusion::pair>, - boost::fusion::pair>, - boost::fusion::pair>, - boost::fusion::pair>, - boost::fusion::pair>, - boost::fusion::pair>, - boost::fusion::pair>, - boost::fusion::pair>, - boost::fusion::pair>, - boost::fusion::pair> - > PluginMap; + using PluginMap = boost::fusion::map< + boost::fusion::pair>, + boost::fusion::pair>, + boost::fusion::pair>, + boost::fusion::pair>, + boost::fusion::pair>, + boost::fusion::pair>, + boost::fusion::pair>, + boost::fusion::pair>, + boost::fusion::pair>, + boost::fusion::pair> + >; + + using AccessPluginMap = boost::fusion::map< + boost::fusion::pair>, + boost::fusion::pair>, + boost::fusion::pair> + >; static const unsigned int PROBLEM_PLUGINSNOTLOADED = 1; @@ -112,6 +142,71 @@ public: return temp; } + /** + * @brief Check if a plugin implement a given interface. + * + * @param plugin The plugin to check. + * + * @return true if the plugin implements the interface, false otherwise. + * + * @tparam The interface type. + */ + template + bool implementInterface(MOBase::IPlugin* plugin) const { + // We need a QObject to be able to qobject_cast<> to the plugin types: + QObject* oPlugin = as_qobject(plugin); + + if (!oPlugin) { + return false; + } + + // Find all the names: + bool implement = false; + boost::mp11::mp_for_each([oPlugin, &implement](const auto* p) { + using plugin_type = std::decay_t; + if (qobject_cast(oPlugin)) { + implement = true; + } + }); + + return implement; + } + + /** + * @brief Retrieve a plugin from its name or a corresponding non-IPlugin + * interface. + * + * @param t Name of the plugin to retrieve, or non-IPlugin interface. + * + * @return the corresponding plugin, or a null pointer. + */ + MOBase::IPlugin* plugin(QString const& pluginName) const; + MOBase::IPlugin* plugin(MOBase::IPluginDiagnose* diagnose) const; + MOBase::IPlugin* plugin(MOBase::IPluginFileMapper* mapper) const; + + /** + * @brief Check if the given plugin is enabled. + * + * @param plugin The plugin to check. + * + * @return true if the plugin is enabled, false otherwise. + */ + bool isEnabled(MOBase::IPlugin *plugin) const; + + // These are friendly methods that called isEnabled(plugin(arg)). + bool isEnabled(QString const& pluginName) const; + bool isEnabled(MOBase::IPluginDiagnose* diagnose) const; + bool isEnabled(MOBase::IPluginFileMapper* mapper) const; + + /** + * @brief Retrieve the requirements for the given plugin. + * + * @param plugin The plugin to retrieve the requirements for. + * + * @return the requirements (as proxy) for the given plugin. + */ + std::vector requirements(MOBase::IPlugin* plugin) const; + /** * @brief Retrieved the (localized) names of interfaces implemented by the given * plugin. @@ -179,6 +274,12 @@ private: PluginMap m_Plugins; + // This maps allow access to IPlugin* from name or diagnose/mapper object. + AccessPluginMap m_AccessPlugins; + + std::map m_Proxies; + std::map>> m_Requirements; + std::map m_SupportedGames; std::vector m_DiagnosisConnections; QStringList m_FailedPlugins; diff --git a/src/previewgenerator.cpp b/src/previewgenerator.cpp index b406b7bc..c7df5a25 100644 --- a/src/previewgenerator.cpp +++ b/src/previewgenerator.cpp @@ -18,14 +18,17 @@ along with Mod Organizer. If not, see . */ #include "previewgenerator.h" + #include #include #include #include #include -PreviewGenerator::PreviewGenerator() -{ +#include "plugincontainer.h" + +PreviewGenerator::PreviewGenerator(const PluginContainer* pluginContainer) : + m_PluginContainer(pluginContainer) { m_MaxSize = QGuiApplication::primaryScreen()->size() * 0.8; } @@ -42,13 +45,13 @@ bool PreviewGenerator::previewSupported(const QString &fileExtension) const if (it == m_PreviewPlugins.end()) { return false; } - return it->second->isActive(); + return m_PluginContainer->isEnabled(it->second); } QWidget *PreviewGenerator::genPreview(const QString &fileName) const { auto iter = m_PreviewPlugins.find(QFileInfo(fileName).suffix().toLower()); - if (iter != m_PreviewPlugins.end() && iter->second->isActive()) { + if (iter != m_PreviewPlugins.end() && m_PluginContainer->isEnabled(iter->second)) { return iter->second->genFilePreview(fileName, m_MaxSize); } else { return nullptr; diff --git a/src/previewgenerator.h b/src/previewgenerator.h index e872b06b..0d8f0781 100644 --- a/src/previewgenerator.h +++ b/src/previewgenerator.h @@ -26,10 +26,12 @@ along with Mod Organizer. If not, see . #include #include +class PluginContainer; + class PreviewGenerator { public: - PreviewGenerator(); + PreviewGenerator(const PluginContainer* pluginContainer); void registerPlugin(MOBase::IPluginPreview *plugin); @@ -44,8 +46,8 @@ private: private: + const PluginContainer* m_PluginContainer; std::map m_PreviewPlugins; - QSize m_MaxSize; }; diff --git a/src/problemsdialog.cpp b/src/problemsdialog.cpp index 4b944ed2..d72f8c20 100644 --- a/src/problemsdialog.cpp +++ b/src/problemsdialog.cpp @@ -7,12 +7,13 @@ #include #include +#include "plugincontainer.h" using namespace MOBase; -ProblemsDialog::ProblemsDialog(std::vector pluginObjects, QWidget *parent) : - QDialog(parent), ui(new Ui::ProblemsDialog), m_PluginObjects(pluginObjects), +ProblemsDialog::ProblemsDialog(const PluginContainer& pluginContainer, QWidget *parent) : + QDialog(parent), ui(new Ui::ProblemsDialog), m_PluginContainer(pluginContainer), m_hasProblems(false) { ui->setupUi(this); @@ -40,14 +41,10 @@ void ProblemsDialog::runDiagnosis() m_hasProblems = false; ui->problemsWidget->clear(); - for(QObject *pluginObj : m_PluginObjects) { - IPlugin *plugin = qobject_cast(pluginObj); - if (plugin != nullptr && !plugin->isActive()) - continue; - - IPluginDiagnose *diagnose = qobject_cast(pluginObj); - if (diagnose == nullptr) + for (IPluginDiagnose *diagnose : m_PluginContainer.plugins()) { + if (!m_PluginContainer.isEnabled(diagnose)) { continue; + } std::vector activeProblems = diagnose->activeProblems(); foreach (unsigned int key, activeProblems) { diff --git a/src/problemsdialog.h b/src/problemsdialog.h index a30c8d48..504a0a10 100644 --- a/src/problemsdialog.h +++ b/src/problemsdialog.h @@ -11,13 +11,14 @@ namespace Ui { class ProblemsDialog; } +class PluginContainer; class ProblemsDialog : public QDialog { Q_OBJECT public: - explicit ProblemsDialog(std::vector pluginObjects, QWidget *parent = 0); + explicit ProblemsDialog(PluginContainer const& pluginContainer, QWidget *parent = 0); ~ProblemsDialog(); // also saves and restores geometry @@ -37,7 +38,7 @@ private slots: private: Ui::ProblemsDialog *ui; - std::vector m_PluginObjects; + const PluginContainer& m_PluginContainer; bool m_hasProblems; }; diff --git a/src/proxyutils.h b/src/proxyutils.h index 4c1717d8..4f26c070 100644 --- a/src/proxyutils.h +++ b/src/proxyutils.h @@ -10,7 +10,7 @@ namespace MOShared { template auto callIfPluginActive(OrganizerProxy* proxy, Fn&& callback, T defaultReturn = T{}) { return [fn = std::forward(callback), proxy, defaultReturn](auto&& ...args) { - if (proxy->plugin()->isActive()) { + if (proxy->isPluginEnabled(proxy->plugin())) { return fn(std::forward(args)...); } else { -- cgit v1.3.1 From 6ee74831f7d6e0ac15d42fe0fcb211321615d684 Mon Sep 17 00:00:00 2001 From: Mikaël Capelle Date: Tue, 10 Nov 2020 21:44:38 +0100 Subject: Insert 'enabled' plugin if not present. --- src/settings.cpp | 5 +++++ 1 file changed, 5 insertions(+) (limited to 'src') diff --git a/src/settings.cpp b/src/settings.cpp index 54d32786..0f661bfb 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -1296,6 +1296,11 @@ void PluginSettings::registerPlugin(IPlugin *plugin) .arg(setting.description) .arg(setting.defaultValue.toString()); } + + if (!m_PluginSettings.contains("enabled")) { + m_PluginSettings[plugin->name()]["enabled"] = true; + m_PluginDescriptions[plugin->name()]["enabled"] = QString(); + } } std::vector PluginSettings::plugins() const -- cgit v1.3.1 From eb1d340f2d98e65b1859fd103658f2671569c342 Mon Sep 17 00:00:00 2001 From: Mikaël Capelle Date: Wed, 11 Nov 2020 11:21:30 +0100 Subject: PluginRequirement -> IPluginRequirement and minor fixes. --- src/plugincontainer.cpp | 32 ++++++++++++++++++++++++-------- src/plugincontainer.h | 17 ++++------------- 2 files changed, 28 insertions(+), 21 deletions(-) (limited to 'src') diff --git a/src/plugincontainer.cpp b/src/plugincontainer.cpp index aa38803f..9045e457 100644 --- a/src/plugincontainer.cpp +++ b/src/plugincontainer.cpp @@ -61,7 +61,7 @@ QString PluginRequirementProxy::description(unsigned int id) const { return m_Requirement->description(id); } -PluginRequirementProxy::PluginRequirementProxy(const MOBase::PluginRequirement* requirement, OrganizerProxy* proxy) : +PluginRequirementProxy::PluginRequirementProxy(const MOBase::IPluginRequirement* requirement, OrganizerProxy* proxy) : m_Requirement(requirement), m_Proxy(proxy) { } @@ -203,14 +203,18 @@ bool PluginContainer::initPlugin(IPlugin *plugin) auto* proxy = new OrganizerProxy(m_Organizer, this, plugin); m_Proxies[plugin] = proxy; + // Create requirements before init() in case a plugin use + // IOrganizer::isPluginEnabled on himself during init(). + m_Requirements.emplace(plugin, 0); + if (!plugin->init(proxy)) { log::warn("plugin failed to initialize"); return false; } - } - for (auto* requirement : plugin->requirements()) { - m_Requirements[plugin].emplace_back(requirement); + for (auto* requirement : plugin->requirements()) { + m_Requirements[plugin].emplace_back(requirement); + } } return true; @@ -230,6 +234,9 @@ bool PluginContainer::initProxyPlugin(IPlugin *plugin) m_Proxies[plugin] = proxy; } + // Create requirements before init() in case a plugin use + // IOrganizer::isPluginEnabled on himself during init(). + m_Requirements.emplace(plugin, 0); if (!plugin->init(proxy)) { log::warn("proxy plugin failed to initialize"); return false; @@ -420,7 +427,7 @@ bool PluginContainer::isEnabled(IPlugin* plugin) const } // Check if the plugin is enabled: - if (!m_Organizer->pluginSetting(plugin->name(), "enabled").toBool()) { + if (!m_Organizer->persistent(plugin->name(), "enabled", true).toBool()) { return false; } @@ -465,9 +472,18 @@ MOBase::IPlugin* PluginContainer::plugin(MOBase::IPluginFileMapper* mapper) cons return it->second; } -bool PluginContainer::isEnabled(QString const& pluginName) const { return isEnabled(plugin(pluginName)); } -bool PluginContainer::isEnabled(MOBase::IPluginDiagnose* diagnose) const { return isEnabled(plugin(diagnose)); } -bool PluginContainer::isEnabled(MOBase::IPluginFileMapper* mapper) const { return isEnabled(plugin(mapper)); } +bool PluginContainer::isEnabled(QString const& pluginName) const { + IPlugin* p = plugin(pluginName); + return p ? isEnabled(p) : false; +} +bool PluginContainer::isEnabled(MOBase::IPluginDiagnose* diagnose) const { + IPlugin* p = plugin(diagnose); + return p ? isEnabled(p) : false; +} +bool PluginContainer::isEnabled(MOBase::IPluginFileMapper* mapper) const { + IPlugin* p = plugin(mapper); + return p ? isEnabled(p) : false; +} std::vector PluginContainer::requirements(IPlugin* plugin) const { diff --git a/src/plugincontainer.h b/src/plugincontainer.h index 1a3173fb..26ad2046 100644 --- a/src/plugincontainer.h +++ b/src/plugincontainer.h @@ -38,10 +38,10 @@ public: private: - const MOBase::PluginRequirement* m_Requirement; + const MOBase::IPluginRequirement* m_Requirement; OrganizerProxy* m_Proxy; - PluginRequirementProxy(const MOBase::PluginRequirement* requirement, OrganizerProxy* proxy); + PluginRequirementProxy(const MOBase::IPluginRequirement* requirement, OrganizerProxy* proxy); friend class PluginContainer; @@ -160,16 +160,7 @@ public: return false; } - // Find all the names: - bool implement = false; - boost::mp11::mp_for_each([oPlugin, &implement](const auto* p) { - using plugin_type = std::decay_t; - if (qobject_cast(oPlugin)) { - implement = true; - } - }); - - return implement; + return qobject_cast(oPlugin); } /** @@ -278,7 +269,7 @@ private: AccessPluginMap m_AccessPlugins; std::map m_Proxies; - std::map>> m_Requirements; + std::map>> m_Requirements; std::map m_SupportedGames; std::vector m_DiagnosisConnections; -- cgit v1.3.1 From 7a4f6d5344031db828f5dc2257c1d19f251e7f05 Mon Sep 17 00:00:00 2001 From: Mikaël Capelle Date: Wed, 11 Nov 2020 13:39:53 +0100 Subject: Improve requirements handling. --- src/plugincontainer.cpp | 218 +++++++++++++++++++++++++++--------------- src/plugincontainer.h | 109 +++++++++++++++++---- src/settings.cpp | 17 +++- src/settingsdialog.ui | 10 ++ src/settingsdialogplugins.cpp | 97 ++++++++++++++++++- src/settingsdialogplugins.h | 7 ++ 6 files changed, 361 insertions(+), 97 deletions(-) (limited to 'src') diff --git a/src/plugincontainer.cpp b/src/plugincontainer.cpp index 9045e457..021fe3c8 100644 --- a/src/plugincontainer.cpp +++ b/src/plugincontainer.cpp @@ -53,18 +53,108 @@ QStringList PluginContainer::pluginInterfaces() // PluginRequirementProxy -std::vector PluginRequirementProxy::problems() const +PluginRequirements::PluginRequirements(PluginContainer* pluginContainer, MOBase::IPlugin* plugin, IOrganizer* proxy, MOBase::IPluginProxy* pluginProxy) + : m_PluginContainer(pluginContainer) + , m_Plugin(plugin) + , m_PluginProxy(pluginProxy) + , m_Organizer(proxy) { - return m_Requirement->problems(m_Proxy); + for (auto* requirement : plugin->requirements()) { + m_Requirements.emplace_back(requirement); + } + + // TODO: + if (pluginProxy) { + m_Requirements.emplace_back(PluginRequirementFactory::pluginDependency(pluginProxy->name())); + } +} + +MOBase::IPluginProxy* PluginRequirements::proxy() const +{ + return m_PluginProxy; +} + +std::vector PluginRequirements::problems() const +{ + std::vector result; + for (auto& requirement : m_Requirements) { + for (auto p : requirement->problems(m_Organizer)) { + result.push_back(Problem(requirement.get(), p)); + } + } + return result; +} + +bool PluginRequirements::canEnable() const +{ + return problems().empty(); +} + +QStringList PluginRequirements::requiredGames() const +{ + // We look for a "GameDependencyRequirement" - There can be only one since otherwise + // it'd mean that the plugin requires two games at once. + for (auto& requirement : m_Requirements) { + if (auto* gdep = dynamic_cast(requirement.get())) { + return gdep->gameNames(); + } + } + + return {}; } -QString PluginRequirementProxy::description(unsigned int id) const + +std::vector PluginRequirements::requiredFor() const { - return m_Requirement->description(id); + std::vector required; + std::set visited; + requiredFor(required, visited); + return required; } -PluginRequirementProxy::PluginRequirementProxy(const MOBase::IPluginRequirement* requirement, OrganizerProxy* proxy) : - m_Requirement(requirement), m_Proxy(proxy) { } +void PluginRequirements::requiredFor(std::vector &required, std::set& visited) const +{ + // Handle cyclic dependencies. + if (visited.contains(m_Plugin)) { + return; + } + visited.insert(m_Plugin); + + + for (auto& [plugin, requirements] : m_PluginContainer->m_Requirements) { + + // If the plugin is not enabled, discard: + if (!m_PluginContainer->isEnabled(plugin)) { + continue; + } + + // Check the requirements: + for (auto& requirement : requirements.m_Requirements) { + + // We check for plugin dependency. Game dependency are not checked this way. + if (auto* pdep = dynamic_cast(requirement.get())) { + + // Check if at least one of the plugin in the requirements is enabled (except this + // one): + bool oneEnabled = false; + for (auto& pluginName : pdep->pluginNames()) { + if (pluginName != m_Plugin->name() && m_PluginContainer->isEnabled(pluginName)) { + oneEnabled = true; + break; + } + } + + // No plugin enabled found, so the plugin requires this plugin: + if (!oneEnabled) { + required.push_back(plugin); + requirements.requiredFor(required, visited); + break; + } + } + } + } +} + // PluginContainer PluginContainer::PluginContainer(OrganizerCore *organizer) @@ -186,7 +276,7 @@ QObject* PluginContainer::as_qobject(MOBase::IPlugin* plugin) const return *it; } -bool PluginContainer::initPlugin(IPlugin *plugin) +bool PluginContainer::initPlugin(IPlugin *plugin, IPluginProxy *pluginProxy) { // when MO has no instance loaded, init() is not called on plugins, except // for proxy plugins, where init() is called with a null IOrganizer @@ -199,63 +289,34 @@ bool PluginContainer::initPlugin(IPlugin *plugin) return false; } + OrganizerProxy* proxy = nullptr; if (m_Organizer) { - auto* proxy = new OrganizerProxy(m_Organizer, this, plugin); - m_Proxies[plugin] = proxy; - - // Create requirements before init() in case a plugin use - // IOrganizer::isPluginEnabled on himself during init(). - m_Requirements.emplace(plugin, 0); - - if (!plugin->init(proxy)) { - log::warn("plugin failed to initialize"); - return false; - } - - for (auto* requirement : plugin->requirements()) { - m_Requirements[plugin].emplace_back(requirement); - } + proxy = new OrganizerProxy(m_Organizer, this, plugin); } - return true; -} + // Check if it is a proxy plugin: + bool isProxy = dynamic_cast(plugin); -bool PluginContainer::initProxyPlugin(IPlugin *plugin) -{ - // see initPlugin() above for info - - if (plugin == nullptr) { - return false; + if (!m_Organizer && !isProxy) { + return true; } - OrganizerProxy* proxy = nullptr; - if (m_Organizer) { - proxy = new OrganizerProxy(m_Organizer, this, plugin); - m_Proxies[plugin] = proxy; - } - - // Create requirements before init() in case a plugin use - // IOrganizer::isPluginEnabled on himself during init(). - m_Requirements.emplace(plugin, 0); if (!plugin->init(proxy)) { - log::warn("proxy plugin failed to initialize"); + log::warn("plugin failed to initialize"); return false; } - for (auto* requirement : plugin->requirements()) { - m_Requirements[plugin].emplace_back(requirement); - } + m_Requirements.emplace(plugin, PluginRequirements(this, plugin, proxy, pluginProxy)); return true; } - void PluginContainer::registerGame(IPluginGame *game) { m_SupportedGames.insert({ game->gameName(), game }); } -bool PluginContainer::registerPlugin(QObject *plugin, const QString &fileName) +bool PluginContainer::registerPlugin(QObject *plugin, const QString &fileName, MOBase::IPluginProxy* pluginProxy) { // Storing the original QObject* is a bit of a hack as I couldn't figure out any // way to cast directly between IPlugin* and IPluginDiagnose* @@ -294,34 +355,32 @@ bool PluginContainer::registerPlugin(QObject *plugin, const QString &fileName) } { // mod page plugin IPluginModPage *modPage = qobject_cast(plugin); - if (initPlugin(modPage)) { + if (initPlugin(modPage, pluginProxy)) { bf::at_key(m_Plugins).push_back(modPage); return true; } } { // game plugin IPluginGame *game = qobject_cast(plugin); - if (game) { game->detectGame(); - } - - if (initPlugin(game)) { - bf::at_key(m_Plugins).push_back(game); - registerGame(game); - return true; + if (initPlugin(game, pluginProxy)) { + bf::at_key(m_Plugins).push_back(game); + registerGame(game); + return true; + } } } { // tool plugins IPluginTool *tool = qobject_cast(plugin); - if (initPlugin(tool)) { + if (initPlugin(tool, pluginProxy)) { bf::at_key(m_Plugins).push_back(tool); return true; } } { // installer plugins IPluginInstaller *installer = qobject_cast(plugin); - if (initPlugin(installer)) { + if (initPlugin(installer, pluginProxy)) { bf::at_key(m_Plugins).push_back(installer); if (m_Organizer) { m_Organizer->installationManager()->registerInstaller(installer); @@ -331,7 +390,7 @@ bool PluginContainer::registerPlugin(QObject *plugin, const QString &fileName) } { // preview plugins IPluginPreview *preview = qobject_cast(plugin); - if (initPlugin(preview)) { + if (initPlugin(preview, pluginProxy)) { bf::at_key(m_Plugins).push_back(preview); m_PreviewGenerator.registerPlugin(preview); return true; @@ -339,7 +398,7 @@ bool PluginContainer::registerPlugin(QObject *plugin, const QString &fileName) } { // proxy plugins IPluginProxy *proxy = qobject_cast(plugin); - if (initProxyPlugin(proxy)) { + if (initPlugin(proxy, pluginProxy)) { bf::at_key(m_Plugins).push_back(proxy); QStringList pluginNames = proxy->pluginList( QCoreApplication::applicationDirPath() + "/" + ToQString(AppConfig::pluginPath())); @@ -349,7 +408,7 @@ bool PluginContainer::registerPlugin(QObject *plugin, const QString &fileName) QList matchingPlugins = proxy->instantiate(pluginName); for (QObject *proxiedPlugin : matchingPlugins) { if (proxiedPlugin != nullptr) { - if (registerPlugin(proxiedPlugin, pluginName)) { + if (registerPlugin(proxiedPlugin, pluginName, proxy)) { log::debug("loaded plugin \"{}\"", QFileInfo(pluginName).fileName()); } else { @@ -371,7 +430,7 @@ bool PluginContainer::registerPlugin(QObject *plugin, const QString &fileName) { // dummy plugins // only initialize these, no processing otherwise IPlugin *dummy = qobject_cast(plugin); - if (initPlugin(dummy)) { + if (initPlugin(dummy, pluginProxy)) { bf::at_key(m_Plugins).push_back(dummy); return true; } @@ -419,6 +478,14 @@ void PluginContainer::unloadPlugins() } } +IPlugin* PluginContainer::managedGame() const +{ + // TODO: This const_cast is safe but ugly. Most methods require a IPlugin*, so + // returning a const-version if painful. This should be fixed by making methods accept + // a const IPlugin* instead, but there are a few tricks with qobject_cast and const. + return const_cast(m_Organizer->managedGame()); +} + bool PluginContainer::isEnabled(IPlugin* plugin) const { // Check if it's a game plugin: @@ -431,15 +498,21 @@ bool PluginContainer::isEnabled(IPlugin* plugin) const return false; } - // Check the requirements: - auto* proxy = m_Proxies.at(plugin); - for (auto& requirement : m_Requirements.at(plugin)) { - if (!requirement->problems(proxy).empty()) { - return false; + // Check the requirements (if a plugin checks in init(), the requirements have + // not been computed yet): + auto it = m_Requirements.find(plugin); + + return it == std::end(m_Requirements) ? true : it->second.canEnable(); +} + +void PluginContainer::setEnabled(MOBase::IPlugin* plugin, bool enable, bool dependencies) +{ + if (!enable && dependencies) { + for (auto* p : requirements(plugin).requiredFor()) { + setEnabled(p, false, false); // No need to "recurse" here since requiredFor already does it. } } - - return true; + m_Organizer->setPersistent(plugin->name(), "enabled", enable, true); } MOBase::IPlugin* PluginContainer::plugin(QString const& pluginName) const @@ -485,14 +558,9 @@ bool PluginContainer::isEnabled(MOBase::IPluginFileMapper* mapper) const { return p ? isEnabled(p) : false; } -std::vector PluginContainer::requirements(IPlugin* plugin) const +const PluginRequirements& PluginContainer::requirements(IPlugin* plugin) const { - auto* proxy = m_Proxies.at(plugin); - std::vector proxies; - for (auto& requirement : m_Requirements.at(plugin)) { - proxies.push_back(PluginRequirementProxy(requirement.get(), proxy)); - } - return proxies; + return m_Requirements.at(plugin); } IPluginGame *PluginContainer::managedGame(const QString &name) const @@ -517,7 +585,7 @@ void PluginContainer::loadPlugins() unloadPlugins(); for (QObject *plugin : QPluginLoader::staticInstances()) { - registerPlugin(plugin, ""); + registerPlugin(plugin, "", nullptr); } QFile loadCheck; @@ -613,7 +681,7 @@ void PluginContainer::loadPlugins() "failed to load plugin {}: {}", pluginName, pluginLoader->errorString()); } else { - if (registerPlugin(pluginLoader->instance(), pluginName)) { + if (registerPlugin(pluginLoader->instance(), pluginName, nullptr)) { log::debug("loaded plugin \"{}\"", QFileInfo(pluginName).fileName()); m_PluginLoaders.push_back(pluginLoader.release()); } else { diff --git a/src/plugincontainer.h b/src/plugincontainer.h index 26ad2046..cd1d03ca 100644 --- a/src/plugincontainer.h +++ b/src/plugincontainer.h @@ -27,24 +27,77 @@ class IUserInterface; class OrganizerProxy; +/** + * @brief Class that wrap multiple requirements for a plugin together. THis + * class owns the requirements. + */ +class PluginRequirements { +public: + + // Small intermediate class. + struct Problem { + public: + + QString description() const { return m_Requirement->description(m_Id); } + + + private: + Problem(const MOBase::IPluginRequirement* requirement, unsigned int id) : + m_Requirement(requirement), m_Id(id) { } + + const MOBase::IPluginRequirement* m_Requirement; + unsigned int m_Id; + + friend class PluginRequirements; + }; -// Small class that allows calling check() for plugin requirements -// without passing the IOrganizer. -class PluginRequirementProxy { public: - std::vector problems() const; - QString description(unsigned int id) const; + /** + * @return true if the plugin can be enabled (all requirements are met). + */ + bool canEnable() const; -private: + /** + * @return the proxy that created this plugin, if any. + */ + MOBase::IPluginProxy* proxy() const; - const MOBase::IPluginRequirement* m_Requirement; - OrganizerProxy* m_Proxy; + /** + * @return the list of problems to be resolved before enabling the plugin. + */ + std::vector problems() const; - PluginRequirementProxy(const MOBase::IPluginRequirement* requirement, OrganizerProxy* proxy); + /** + * @return the name of the games (gameName()) this plugin can be used with, or an empty + * list if this plugin does not require particular games. + */ + QStringList requiredGames() const; + + /** + * @return the list of plugins currently enabled that would have to be disabled + * if this plugin was disabled. + */ + std::vector requiredFor() const; + +private: + + // Accumulator version for requiredFor() to avoid infinite recursion. + void requiredFor(std::vector& required, std::set& visited) const; friend class PluginContainer; + PluginContainer* m_PluginContainer; + MOBase::IPlugin* m_Plugin; + MOBase::IPluginProxy* m_PluginProxy; + std::vector> m_Requirements; + MOBase::IOrganizer* m_Organizer; + std::vector m_RequiredFor; + + PluginRequirements( + PluginContainer* pluginContainer, MOBase::IPlugin* plugin, + MOBase::IOrganizer* proxy, MOBase::IPluginProxy* pluginProxy); + }; @@ -175,6 +228,11 @@ public: MOBase::IPlugin* plugin(MOBase::IPluginDiagnose* diagnose) const; MOBase::IPlugin* plugin(MOBase::IPluginFileMapper* mapper) const; + /** + * @return the IPlugin interface to the currently managed game. + */ + MOBase::IPlugin* managedGame() const; + /** * @brief Check if the given plugin is enabled. * @@ -182,13 +240,23 @@ public: * * @return true if the plugin is enabled, false otherwise. */ - bool isEnabled(MOBase::IPlugin *plugin) const; + bool isEnabled(MOBase::IPlugin* plugin) const; // These are friendly methods that called isEnabled(plugin(arg)). bool isEnabled(QString const& pluginName) const; bool isEnabled(MOBase::IPluginDiagnose* diagnose) const; bool isEnabled(MOBase::IPluginFileMapper* mapper) const; + /** + * @brief Enable or disable a plugin. + * + * @param plugin The plugin to enable or disable. + * @param enable true to enable, false to disable. + * @param dependencies If true and enable is false, dependencies will also + * be disabled (see PluginRequirements::requiredFor). + */ + void setEnabled(MOBase::IPlugin* plugin, bool enable, bool dependencies = true); + /** * @brief Retrieve the requirements for the given plugin. * @@ -196,7 +264,7 @@ public: * * @return the requirements (as proxy) for the given plugin. */ - std::vector requirements(MOBase::IPlugin* plugin) const; + const PluginRequirements& requirements(MOBase::IPlugin* plugin) const; /** * @brief Retrieved the (localized) names of interfaces implemented by the given @@ -244,6 +312,8 @@ signals: private: + friend class PluginRequirements; + /** * @brief Find the QObject* corresponding to the given plugin. * @@ -253,11 +323,19 @@ private: */ QObject* as_qobject(MOBase::IPlugin* plugin) const; + /** + * @brief Initialize a plugin. + * + * @param plugin The plugin to initialize. + * @param proxy The proxy that created this plugin (can be null). + * + * @return true if the plugin was initialized correctly, false otherwise. + */ + bool initPlugin(MOBase::IPlugin *plugin, MOBase::IPluginProxy* proxy); - bool initPlugin(MOBase::IPlugin *plugin); - bool initProxyPlugin(MOBase::IPlugin *plugin); void registerGame(MOBase::IPluginGame *game); - bool registerPlugin(QObject *pluginObj, const QString &fileName); + + bool registerPlugin(QObject *pluginObj, const QString &fileName, MOBase::IPluginProxy *proxy); OrganizerCore *m_Organizer; @@ -268,8 +346,7 @@ private: // This maps allow access to IPlugin* from name or diagnose/mapper object. AccessPluginMap m_AccessPlugins; - std::map m_Proxies; - std::map>> m_Requirements; + std::map m_Requirements; std::map m_SupportedGames; std::vector m_DiagnosisConnections; diff --git a/src/settings.cpp b/src/settings.cpp index 0f661bfb..a76d5e76 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -1288,6 +1288,11 @@ void PluginSettings::registerPlugin(IPlugin *plugin) temp.toString(), setting.key, plugin->name()); temp = setting.defaultValue; + + // If there was no previous "enabled" value, skip it: + if (setting.key == "enabled") { + continue; + } } m_PluginSettings[plugin->name()][setting.key] = temp; @@ -1297,9 +1302,15 @@ void PluginSettings::registerPlugin(IPlugin *plugin) .arg(setting.defaultValue.toString()); } - if (!m_PluginSettings.contains("enabled")) { - m_PluginSettings[plugin->name()]["enabled"] = true; - m_PluginDescriptions[plugin->name()]["enabled"] = QString(); + // Handle previous "enabled" settings: + if (m_PluginSettings[plugin->name()].contains("enabled")) { + setPersistent(plugin->name(), "enabled", m_PluginSettings[plugin->name()]["enabled"].toBool(), true); + m_PluginSettings[plugin->name()].remove("enabled"); + m_PluginDescriptions[plugin->name()].remove("enabled"); + + // We need to drop it manually in Settings since it is not possible to remove plugin + // settings: + remove(m_Settings, "Plugins", plugin->name() + "/enabled"); } } diff --git a/src/settingsdialog.ui b/src/settingsdialog.ui index 85c5a4e4..4fd34b21 100644 --- a/src/settingsdialog.ui +++ b/src/settingsdialog.ui @@ -1127,6 +1127,9 @@ + + 6 + @@ -1172,6 +1175,13 @@ + + + + Enabled + + + diff --git a/src/settingsdialogplugins.cpp b/src/settingsdialogplugins.cpp index 5faa0dc9..8a70475c 100644 --- a/src/settingsdialogplugins.cpp +++ b/src/settingsdialogplugins.cpp @@ -3,9 +3,10 @@ #include "noeditdelegate.h" #include +#include "organizercore.h" #include "plugincontainer.h" -using MOBase::IPlugin; +using namespace MOBase; PluginsSettingsTab::PluginsSettingsTab(Settings& s, PluginContainer* pluginContainer, SettingsDialog& d) : SettingsTab(s, d), m_pluginContainer(pluginContainer) @@ -66,15 +67,36 @@ PluginsSettingsTab::PluginsSettingsTab(Settings& s, PluginContainer* pluginConta QObject::connect( ui->pluginsList, &QTreeWidget::currentItemChanged, [&](auto* current, auto* previous) { on_pluginsList_currentItemChanged(current, previous); }); + QObject::connect( + ui->enabledCheckbox, &QCheckBox::clicked, + [&](bool checked) { on_checkboxEnabled_clicked(checked); }); QShortcut *delShortcut = new QShortcut( QKeySequence(Qt::Key_Delete), ui->pluginBlacklist); QObject::connect(delShortcut, &QShortcut::activated, &dialog(), [&] { deleteBlacklistItem(); }); - QObject::connect(&m_filter, &MOBase::FilterWidget::changed, [&] { filterPluginList(); }); + QObject::connect(&m_filter, &FilterWidget::changed, [&] { filterPluginList(); }); + updateListItems(); filterPluginList(); } +void PluginsSettingsTab::updateListItems() +{ + for (auto i = 0; i < ui->pluginsList->topLevelItemCount(); ++i) { + auto* topLevelItem = ui->pluginsList->topLevelItem(i); + for (auto j = 0; j < topLevelItem->childCount(); ++j) { + auto* item = topLevelItem->child(j); + auto* plugin = this->plugin(item); + + if (!m_pluginContainer->implementInterface(plugin) + && !m_pluginContainer->isEnabled(plugin)) { + item->setBackgroundColor(0, Qt::gray); + } + } + } + +} + void PluginsSettingsTab::filterPluginList() { QTreeWidgetItem* firstNotHidden = nullptr; @@ -109,7 +131,7 @@ void PluginsSettingsTab::filterPluginList() auto selectedItems = ui->pluginsList->selectedItems(); if (!selectedItems.isEmpty() && selectedItems[0]->isHidden()) { selectedItems[0]->setSelected(false); - + if (firstNotHidden) { firstNotHidden->setSelected(true); } @@ -150,6 +172,71 @@ void PluginsSettingsTab::closing() storeSettings(ui->pluginsList->currentItem()); } +void PluginsSettingsTab::on_checkboxEnabled_clicked(bool checked) +{ + // Retrieve the plugin: + auto *item = ui->pluginsList->currentItem(); + if (!item || !item->data(0, ROLE_PLUGIN).isValid()) { + return; + } + IPlugin* plugin = this->plugin(item); + const auto& requirements = m_pluginContainer->requirements(plugin); + + // User wants to enable: + if (checked) { + auto problems = requirements.problems(); + if (!problems.empty()) { + QStringList descriptions; + for (auto& problem : problems) { + descriptions.append(problem.description()); + } + QMessageBox::warning( + parentWidget(), QObject::tr("Cannot enable plugin"), + QObject::tr("

This plugin cannot be enabled:

    %1
") + .arg("
  • " + descriptions.join("
  • ") + "
  • "), QMessageBox::Ok); + ui->enabledCheckbox->setChecked(false); + return; + } + + m_pluginContainer->setEnabled(plugin, false, true); + } + else { + // Custom check for proxy + current game: + if (m_pluginContainer->implementInterface(plugin)) { + auto* game = m_pluginContainer->managedGame(); + if (m_pluginContainer->requirements(game).proxy() == plugin) { + QMessageBox::warning( + parentWidget(), QObject::tr("Cannot disable plugin"), + QObject::tr("This plugin is used by the current game plugin and cannot disabled."), QMessageBox::Ok); + ui->enabledCheckbox->setChecked(true); + return; + } + } + + // Check if the plugins is required for other plugins: + auto requiredFor = requirements.requiredFor(); + if (!requiredFor.empty()) { + QStringList pluginNames; + for (auto& p : requiredFor) { + pluginNames.append(p->localizedName()); + } + pluginNames.sort(); + QString message = QObject::tr( + "

    Disabling this plugin will also disable the following plugins:

      %1

    Do you want to continue?

    ") + .arg("
  • " + pluginNames.join("
  • ") + "
  • "); + if (QMessageBox::warning( + parentWidget(), QObject::tr("Really disable plugin?"), message, + QMessageBox::Yes | QMessageBox::No) == QMessageBox::No) { + ui->enabledCheckbox->setChecked(true); + return; + } + } + m_pluginContainer->setEnabled(plugin, false, true); + } + + updateListItems(); +} + void PluginsSettingsTab::on_pluginsList_currentItemChanged(QTreeWidgetItem *current, QTreeWidgetItem *previous) { storeSettings(previous); @@ -164,6 +251,10 @@ void PluginsSettingsTab::on_pluginsList_currentItemChanged(QTreeWidgetItem *curr ui->versionLabel->setText(plugin->version().canonicalString()); ui->descriptionLabel->setText(plugin->description()); + ui->enabledCheckbox->setVisible( + !m_pluginContainer->implementInterface(plugin)); + ui->enabledCheckbox->setChecked(m_pluginContainer->isEnabled(plugin)); + QVariantMap settings = current->data(0, ROLE_SETTINGS).toMap(); QVariantMap descriptions = current->data(0, ROLE_DESCRIPTIONS).toMap(); ui->pluginSettingsList->setEnabled(settings.count() != 0); diff --git a/src/settingsdialogplugins.h b/src/settingsdialogplugins.h index add8fede..3de0d7dc 100644 --- a/src/settingsdialogplugins.h +++ b/src/settingsdialogplugins.h @@ -16,10 +16,17 @@ public: private: void on_pluginsList_currentItemChanged(QTreeWidgetItem *current, QTreeWidgetItem *previous); + void on_checkboxEnabled_clicked(bool checked); void deleteBlacklistItem(); void storeSettings(QTreeWidgetItem *pluginItem); private slots: + + /** + * @brief Update the list item to display inactive plugins. + */ + void updateListItems(); + /** * @brief Filter the plugin list according to the filter widget. * -- cgit v1.3.1 From f53d04a84d148a771dab3de9fe5f2f28e2d0240d Mon Sep 17 00:00:00 2001 From: Mikaël Capelle Date: Wed, 11 Nov 2020 14:08:11 +0100 Subject: Hide Tool action instead of not adding them. --- src/mainwindow.cpp | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) (limited to 'src') diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 93ca415d..9bbe3da9 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -1652,6 +1652,9 @@ void MainWindow::registerPluginTool(IPluginTool *tool, QString name, QMenu *menu connect(action, SIGNAL(triggered()), this, SLOT(toolPluginInvoke()), Qt::QueuedConnection); menu->addAction(action); + if (!m_PluginContainer.isEnabled(tool)) { + action->setVisible(false); + } } void MainWindow::registerPluginTools(std::vector toolPlugins) @@ -1663,14 +1666,6 @@ void MainWindow::registerPluginTools(std::vector toolPlugins) } ); - // TODO: I don't know when this method is called? Maybe the check should be perform when - // the context menu is opened? - // Remove inactive plugins - toolPlugins.erase( - std::remove_if(toolPlugins.begin(), toolPlugins.end(), [this](IPluginTool *plugin) { return !m_PluginContainer.isEnabled(plugin); }), - toolPlugins.end() - ); - // Group the plugins into submenus QMap>> submenuMap; for (auto toolPlugin : toolPlugins) { -- cgit v1.3.1 From 0dddc5eb7b60efd4f124485fd6e3e982c04f8a28 Mon Sep 17 00:00:00 2001 From: Mikaël Capelle Date: Wed, 11 Nov 2020 14:08:23 +0100 Subject: Fix handling of previous 'enabled' settings. --- src/settings.cpp | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) (limited to 'src') diff --git a/src/settings.cpp b/src/settings.cpp index a76d5e76..db48cfd2 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -1280,19 +1280,22 @@ void PluginSettings::registerPlugin(IPlugin *plugin) const QString settingName = plugin->name() + "/" + setting.key; QVariant temp = get( - m_Settings, "Plugins", settingName, setting.defaultValue); + m_Settings, "Plugins", settingName, QVariant()); - if (!temp.convert(setting.defaultValue.type())) { + // No previous enabled? Skip. + if (setting.key == "enabled" && (!temp.isValid() || !temp.canConvert())) { + continue; + } + + if (!temp.isValid()) { + temp = setting.defaultValue; + } + else if (!temp.convert(setting.defaultValue.type())) { log::warn( "failed to interpret \"{}\" as correct type for \"{}\" in plugin \"{}\", using default", temp.toString(), setting.key, plugin->name()); temp = setting.defaultValue; - - // If there was no previous "enabled" value, skip it: - if (setting.key == "enabled") { - continue; - } } m_PluginSettings[plugin->name()][setting.key] = temp; -- cgit v1.3.1 From e528d8b0ad843bf2e728f26c3a1bade145e5ad07 Mon Sep 17 00:00:00 2001 From: Mikaël Capelle Date: Wed, 11 Nov 2020 14:59:11 +0100 Subject: Fix display of tools in the MainWindow menu. --- src/iuserinterface.h | 2 -- src/mainwindow.cpp | 51 ++++++++++++++++++++++--------------------- src/mainwindow.h | 9 ++++---- src/plugincontainer.cpp | 4 ---- src/settingsdialogplugins.cpp | 9 ++++---- 5 files changed, 34 insertions(+), 41 deletions(-) (limited to 'src') diff --git a/src/iuserinterface.h b/src/iuserinterface.h index cce89070..a2a91e62 100644 --- a/src/iuserinterface.h +++ b/src/iuserinterface.h @@ -13,8 +13,6 @@ class IUserInterface { public: - virtual void registerPluginTool(MOBase::IPluginTool *tool, QString name = QString(), QMenu *menu = nullptr) = 0; - virtual void registerPluginTools(std::vector toolPlugins) = 0; virtual void registerModPage(MOBase::IPluginModPage *modPage) = 0; virtual void installTranslator(const QString &name) = 0; diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 9bbe3da9..b5a6382f 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -467,6 +467,7 @@ MainWindow::MainWindow(Settings &settings connect(ui->toolBar, SIGNAL(customContextMenuRequested(QPoint)), this, SLOT(toolBar_customContextMenuRequested(QPoint))); connect(ui->menuToolbars, &QMenu::aboutToShow, [&]{ updateToolbarMenu(); }); connect(ui->menuView, &QMenu::aboutToShow, [&]{ updateViewMenu(); }); + connect(ui->actionTool->menu(), &QMenu::aboutToShow, [&] { updateToolMenu(); }); connect(&m_OrganizerCore, &OrganizerCore::modInstalled, this, &MainWindow::modInstalled); connect(&m_OrganizerCore, &OrganizerCore::close, this, &QMainWindow::close); @@ -508,8 +509,6 @@ MainWindow::MainWindow(Settings &settings installTranslator(QFileInfo(fileName).baseName()); } - registerPluginTools(m_PluginContainer.plugins()); - for (IPluginModPage *modPagePlugin : m_PluginContainer.plugins()) { registerModPage(modPagePlugin); } @@ -1597,22 +1596,6 @@ bool MainWindow::eventFilter(QObject *object, QEvent *event) return false; } - -void MainWindow::toolPluginInvoke() -{ - QAction *triggeredAction = qobject_cast(sender()); - IPluginTool *plugin = qobject_cast(triggeredAction->data().value()); - if (plugin != nullptr) { - try { - plugin->display(); - } catch (const std::exception &e) { - reportError(tr("Plugin \"%1\" failed: %2").arg(plugin->name()).arg(e.what())); - } catch (...) { - reportError(tr("Plugin \"%1\" failed").arg(plugin->name())); - } - } -} - void MainWindow::modPagePluginInvoke() { QAction *triggeredAction = qobject_cast(sender()); @@ -1648,24 +1631,42 @@ void MainWindow::registerPluginTool(IPluginTool *tool, QString name, QMenu *menu QAction *action = new QAction(tool->icon(), name, menu); action->setToolTip(tool->tooltip()); tool->setParentWidget(this); - action->setData(QVariant::fromValue((QObject*)tool)); - connect(action, SIGNAL(triggered()), this, SLOT(toolPluginInvoke()), Qt::QueuedConnection); + connect(action, &QAction::triggered, this, [this, tool]() { + try { + tool->display(); + } + catch (const std::exception& e) { + reportError(tr("Plugin \"%1\" failed: %2").arg(tool->localizedName()).arg(e.what())); + } + catch (...) { + reportError(tr("Plugin \"%1\" failed").arg(tool->localizedName())); + } + }, Qt::QueuedConnection); menu->addAction(action); - if (!m_PluginContainer.isEnabled(tool)) { - action->setVisible(false); - } } -void MainWindow::registerPluginTools(std::vector toolPlugins) +void MainWindow::updateToolMenu() { + // Clear the menu: + ui->actionTool->menu()->clear(); + + std::vector toolPlugins = m_PluginContainer.plugins(); + // Sort the plugins by display name - std::sort(toolPlugins.begin(), toolPlugins.end(), + std::sort(std::begin(toolPlugins), std::end(toolPlugins), [](IPluginTool *left, IPluginTool *right) { return left->displayName().toLower() < right->displayName().toLower(); } ); + // Remove disabled plugins: + toolPlugins.erase( + std::remove_if(std::begin(toolPlugins), std::end(toolPlugins), [&](auto* tool) { + return !m_PluginContainer.isEnabled(tool); + }), + toolPlugins.end()); + // Group the plugins into submenus QMap>> submenuMap; for (auto toolPlugin : toolPlugins) { diff --git a/src/mainwindow.h b/src/mainwindow.h index 8b2188c8..d4aa0bc2 100644 --- a/src/mainwindow.h +++ b/src/mainwindow.h @@ -129,10 +129,6 @@ public: void saveArchiveList(); - void registerPluginTool(MOBase::IPluginTool *tool, QString name = QString(), QMenu *menu = nullptr); - void registerPluginTools(std::vector toolPlugins); - void registerModPage(MOBase::IPluginModPage *modPage); - void addPrimaryCategoryCandidates(QMenu *primaryCategoryMenu, ModInfo::Ptr info); void installTranslator(const QString &name); @@ -160,7 +156,6 @@ public slots: void directory_refreshed(); - void toolPluginInvoke(); void modPagePluginInvoke(); signals: @@ -212,7 +207,11 @@ private: void setToolbarSize(const QSize& s); void setToolbarButtonStyle(Qt::ToolButtonStyle s); + void registerModPage(MOBase::IPluginModPage* modPage); + void registerPluginTool(MOBase::IPluginTool* tool, QString name = QString(), QMenu* menu = nullptr); + void updateToolbarMenu(); + void updateToolMenu(); void updateViewMenu(); QMenu* createPopupMenu() override; diff --git a/src/plugincontainer.cpp b/src/plugincontainer.cpp index 021fe3c8..030fdb31 100644 --- a/src/plugincontainer.cpp +++ b/src/plugincontainer.cpp @@ -180,10 +180,6 @@ void PluginContainer::setUserInterface(IUserInterface *userInterface, QWidget *w for (IPluginModPage *modPage : bf::at_key(m_Plugins)) { userInterface->registerModPage(modPage); } - - for (IPluginTool *tool : bf::at_key(m_Plugins)) { - userInterface->registerPluginTool(tool); - } } m_UserInterface = userInterface; diff --git a/src/settingsdialogplugins.cpp b/src/settingsdialogplugins.cpp index 8a70475c..0c02ffb0 100644 --- a/src/settingsdialogplugins.cpp +++ b/src/settingsdialogplugins.cpp @@ -88,10 +88,9 @@ void PluginsSettingsTab::updateListItems() auto* item = topLevelItem->child(j); auto* plugin = this->plugin(item); - if (!m_pluginContainer->implementInterface(plugin) - && !m_pluginContainer->isEnabled(plugin)) { - item->setBackgroundColor(0, Qt::gray); - } + bool inactive = !m_pluginContainer->implementInterface(plugin) + && !m_pluginContainer->isEnabled(plugin); + // TODO: Better display. } } @@ -198,7 +197,7 @@ void PluginsSettingsTab::on_checkboxEnabled_clicked(bool checked) return; } - m_pluginContainer->setEnabled(plugin, false, true); + m_pluginContainer->setEnabled(plugin, true, false); } else { // Custom check for proxy + current game: -- cgit v1.3.1 From a449098db96527f8ac772cd518657d8e22018468 Mon Sep 17 00:00:00 2001 From: Mikaël Capelle Date: Wed, 11 Nov 2020 16:14:55 +0100 Subject: Cleaner IPluginRequirement. --- src/plugincontainer.cpp | 9 +++++---- src/plugincontainer.h | 21 +-------------------- src/settingsdialogplugins.cpp | 2 +- 3 files changed, 7 insertions(+), 25 deletions(-) (limited to 'src') diff --git a/src/plugincontainer.cpp b/src/plugincontainer.cpp index 030fdb31..a218b1f0 100644 --- a/src/plugincontainer.cpp +++ b/src/plugincontainer.cpp @@ -74,12 +74,13 @@ MOBase::IPluginProxy* PluginRequirements::proxy() const return m_PluginProxy; } -std::vector PluginRequirements::problems() const +std::vector PluginRequirements::problems() const { - std::vector result; + std::vector result; for (auto& requirement : m_Requirements) { - for (auto p : requirement->problems(m_Organizer)) { - result.push_back(Problem(requirement.get(), p)); + auto p = requirement->check(m_Organizer); + if (p) { + result.push_back(*p); } } return result; diff --git a/src/plugincontainer.h b/src/plugincontainer.h index cd1d03ca..520ee83e 100644 --- a/src/plugincontainer.h +++ b/src/plugincontainer.h @@ -32,25 +32,6 @@ class OrganizerProxy; * class owns the requirements. */ class PluginRequirements { -public: - - // Small intermediate class. - struct Problem { - public: - - QString description() const { return m_Requirement->description(m_Id); } - - - private: - Problem(const MOBase::IPluginRequirement* requirement, unsigned int id) : - m_Requirement(requirement), m_Id(id) { } - - const MOBase::IPluginRequirement* m_Requirement; - unsigned int m_Id; - - friend class PluginRequirements; - }; - public: /** @@ -66,7 +47,7 @@ public: /** * @return the list of problems to be resolved before enabling the plugin. */ - std::vector problems() const; + std::vector problems() const; /** * @return the name of the games (gameName()) this plugin can be used with, or an empty diff --git a/src/settingsdialogplugins.cpp b/src/settingsdialogplugins.cpp index 0c02ffb0..1e2117cf 100644 --- a/src/settingsdialogplugins.cpp +++ b/src/settingsdialogplugins.cpp @@ -187,7 +187,7 @@ void PluginsSettingsTab::on_checkboxEnabled_clicked(bool checked) if (!problems.empty()) { QStringList descriptions; for (auto& problem : problems) { - descriptions.append(problem.description()); + descriptions.append(problem.shortDescription()); } QMessageBox::warning( parentWidget(), QObject::tr("Cannot enable plugin"), -- cgit v1.3.1 From d3bd237c226f44004a9917a29e1b98b2038ff271 Mon Sep 17 00:00:00 2001 From: Mikaël Capelle Date: Wed, 11 Nov 2020 20:34:28 +0100 Subject: Fix rebase. --- src/plugincontainer.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) (limited to 'src') diff --git a/src/plugincontainer.cpp b/src/plugincontainer.cpp index a218b1f0..85068d2d 100644 --- a/src/plugincontainer.cpp +++ b/src/plugincontainer.cpp @@ -78,8 +78,7 @@ std::vector PluginRequirements::problems() const { std::vector result; for (auto& requirement : m_Requirements) { - auto p = requirement->check(m_Organizer); - if (p) { + if (auto p = requirement->check(m_Organizer)) { result.push_back(*p); } } -- cgit v1.3.1 From c0ace25a871c4d938fb4be10b0357e263db677cb Mon Sep 17 00:00:00 2001 From: Mikaël Capelle Date: Wed, 11 Nov 2020 21:01:17 +0100 Subject: Use IPlugin::master() when present. --- src/plugincontainer.cpp | 40 ++++++++++++++++++++++++++++++++++++++- src/plugincontainer.h | 15 +++++++++++++++ src/settingsdialogplugins.cpp | 44 +++++++++++++++++++++++++++++++++---------- 3 files changed, 88 insertions(+), 11 deletions(-) (limited to 'src') diff --git a/src/plugincontainer.cpp b/src/plugincontainer.cpp index 85068d2d..ab68cc1d 100644 --- a/src/plugincontainer.cpp +++ b/src/plugincontainer.cpp @@ -69,11 +69,31 @@ PluginRequirements::PluginRequirements(PluginContainer* pluginContainer, MOBase: } } -MOBase::IPluginProxy* PluginRequirements::proxy() const +IPluginProxy* PluginRequirements::proxy() const { return m_PluginProxy; } +IPlugin* PluginRequirements::master() const +{ + if (m_Plugin->master().isEmpty()) { + return nullptr; + } + return m_PluginContainer->plugin(m_Plugin->master()); +} + +std::vector PluginRequirements::children() const +{ + std::vector children; + for (auto* obj : m_PluginContainer->plugins()) { + auto* plugin = qobject_cast(obj); + if (plugin && plugin->master().compare(m_Plugin->name(), Qt::CaseInsensitive) == 0) { + children.push_back(plugin); + } + } + return children; +} + std::vector PluginRequirements::problems() const { std::vector result; @@ -90,6 +110,11 @@ bool PluginRequirements::canEnable() const return problems().empty(); } +bool PluginRequirements::hasRequirements() const +{ + return !m_Requirements.empty(); +} + QStringList PluginRequirements::requiredGames() const { // We look for a "GameDependencyRequirement" - There can be only one since otherwise @@ -304,6 +329,11 @@ bool PluginContainer::initPlugin(IPlugin *plugin, IPluginProxy *pluginProxy) m_Requirements.emplace(plugin, PluginRequirements(this, plugin, proxy, pluginProxy)); + if (!plugin->master().isEmpty() && m_Requirements.at(plugin).hasRequirements()) { + log::warn("a plugin cannot have requirements if it has a master"); + return false; + } + return true; } @@ -503,11 +533,19 @@ bool PluginContainer::isEnabled(IPlugin* plugin) const void PluginContainer::setEnabled(MOBase::IPlugin* plugin, bool enable, bool dependencies) { + // If required, disable dependencies: if (!enable && dependencies) { for (auto* p : requirements(plugin).requiredFor()) { setEnabled(p, false, false); // No need to "recurse" here since requiredFor already does it. } } + + // Always disable/enable child plugins: + for (auto* p : requirements(plugin).children()) { + // "Child" plugin should have no dependencies. + setEnabled(p, enable, false); + } + m_Organizer->setPersistent(plugin->name(), "enabled", enable, true); } diff --git a/src/plugincontainer.h b/src/plugincontainer.h index 520ee83e..e9761cb1 100644 --- a/src/plugincontainer.h +++ b/src/plugincontainer.h @@ -39,11 +39,26 @@ public: */ bool canEnable() const; + /** + * @return true if this plugin has requirements (satisfied or not). + */ + bool hasRequirements() const; + /** * @return the proxy that created this plugin, if any. */ MOBase::IPluginProxy* proxy() const; + /** + * @return the master of this plugin, if any. + */ + MOBase::IPlugin* master() const; + + /** + * @return the plugins this plugin is master of. + */ + std::vector children() const; + /** * @return the list of problems to be resolved before enabling the plugin. */ diff --git a/src/settingsdialogplugins.cpp b/src/settingsdialogplugins.cpp index 1e2117cf..15c0b424 100644 --- a/src/settingsdialogplugins.cpp +++ b/src/settingsdialogplugins.cpp @@ -31,14 +31,10 @@ PluginsSettingsTab::PluginsSettingsTab(Settings& s, PluginContainer* pluginConta // display plugin settings QSet handledNames; for (IPlugin* plugin : settings().plugins().plugins()) { - if (handledNames.contains(plugin->name())) { - continue; - } - if (!m_filter.matches([plugin](const QRegularExpression& regex) { - return regex.match(plugin->localizedName()).hasMatch(); - })) { + if (handledNames.contains(plugin->name()) || !plugin->master().isEmpty()) { continue; } + QTreeWidgetItem* listItem = new QTreeWidgetItem( topItems.at(m_pluginContainer->topImplementedInterface(plugin))); listItem->setData(0, Qt::DisplayRole, plugin->localizedName()); @@ -46,6 +42,18 @@ PluginsSettingsTab::PluginsSettingsTab(Settings& s, PluginContainer* pluginConta listItem->setData(0, ROLE_SETTINGS, settings().plugins().settings(plugin->name())); listItem->setData(0, ROLE_DESCRIPTIONS, settings().plugins().descriptions(plugin->name())); + // Handle child item: + auto children = m_pluginContainer->requirements(plugin).children(); + for (auto* child : children) { + QTreeWidgetItem* childItem = new QTreeWidgetItem(listItem); + childItem->setData(0, Qt::DisplayRole, child->localizedName()); + childItem->setData(0, ROLE_PLUGIN, QVariant::fromValue((void*)child)); + childItem->setData(0, ROLE_SETTINGS, settings().plugins().settings(child->name())); + childItem->setData(0, ROLE_DESCRIPTIONS, settings().plugins().descriptions(child->name())); + + handledNames.insert(child->name()); + } + handledNames.insert(plugin->name()); } @@ -90,7 +98,13 @@ void PluginsSettingsTab::updateListItems() bool inactive = !m_pluginContainer->implementInterface(plugin) && !m_pluginContainer->isEnabled(plugin); - // TODO: Better display. + + auto font = item->font(0); + font.setItalic(inactive); + item->setFont(0, font); + for (auto k = 0; k < item->childCount(); ++k) { + item->child(k)->setFont(0, font); + } } } @@ -108,9 +122,18 @@ void PluginsSettingsTab::filterPluginList() auto* item = topLevelItem->child(j); auto* plugin = this->plugin(item); - if (m_filter.matches([plugin](const QRegularExpression& regex) { + // Check the item or the child - If any match (item or child), the whole + // group is displayed. + bool match = m_filter.matches([plugin](const QRegularExpression& regex) { return regex.match(plugin->localizedName()).hasMatch(); - })) { + }); + for (auto* child : m_pluginContainer->requirements(plugin).children()) { + m_filter.matches([child](const QRegularExpression& regex) { + return regex.match(child->localizedName()).hasMatch(); + }); + } + + if (match) { found = true; item->setHidden(false); @@ -251,7 +274,8 @@ void PluginsSettingsTab::on_pluginsList_currentItemChanged(QTreeWidgetItem *curr ui->descriptionLabel->setText(plugin->description()); ui->enabledCheckbox->setVisible( - !m_pluginContainer->implementInterface(plugin)); + !m_pluginContainer->implementInterface(plugin) + && plugin->master().isEmpty()); ui->enabledCheckbox->setChecked(m_pluginContainer->isEnabled(plugin)); QVariantMap settings = current->data(0, ROLE_SETTINGS).toMap(); -- cgit v1.3.1 From 7ec0128eb59c085cd5961995280238cc22683b8e Mon Sep 17 00:00:00 2001 From: Mikaël Capelle Date: Wed, 11 Nov 2020 21:42:05 +0100 Subject: Remove incorrect warning. --- src/plugincontainer.cpp | 5 ----- 1 file changed, 5 deletions(-) (limited to 'src') diff --git a/src/plugincontainer.cpp b/src/plugincontainer.cpp index ab68cc1d..bcd0db44 100644 --- a/src/plugincontainer.cpp +++ b/src/plugincontainer.cpp @@ -329,11 +329,6 @@ bool PluginContainer::initPlugin(IPlugin *plugin, IPluginProxy *pluginProxy) m_Requirements.emplace(plugin, PluginRequirements(this, plugin, proxy, pluginProxy)); - if (!plugin->master().isEmpty() && m_Requirements.at(plugin).hasRequirements()) { - log::warn("a plugin cannot have requirements if it has a master"); - return false; - } - return true; } -- cgit v1.3.1 From 90beed8c7b356ef58431721ffe8eb4abff3da032 Mon Sep 17 00:00:00 2001 From: Mikaël Capelle Date: Wed, 11 Nov 2020 22:45:33 +0100 Subject: Handle proxy dependencies differently. --- src/plugincontainer.cpp | 37 ++++++++++++++++++++++++------------- src/plugincontainer.h | 10 ++++++++++ src/settingsdialogplugins.cpp | 26 ++++++++++++++++++++++++++ 3 files changed, 60 insertions(+), 13 deletions(-) (limited to 'src') diff --git a/src/plugincontainer.cpp b/src/plugincontainer.cpp index bcd0db44..05c6790d 100644 --- a/src/plugincontainer.cpp +++ b/src/plugincontainer.cpp @@ -58,14 +58,11 @@ PluginRequirements::PluginRequirements(PluginContainer* pluginContainer, MOBase: , m_Plugin(plugin) , m_PluginProxy(pluginProxy) , m_Organizer(proxy) -{ - for (auto* requirement : plugin->requirements()) { - m_Requirements.emplace_back(requirement); - } +{ } - // TODO: - if (pluginProxy) { - m_Requirements.emplace_back(PluginRequirementFactory::pluginDependency(pluginProxy->name())); +void PluginRequirements::fetchRequirements() { + for (auto* requirement : m_Plugin->requirements()) { + m_Requirements.emplace_back(requirement); } } @@ -74,6 +71,20 @@ IPluginProxy* PluginRequirements::proxy() const return m_PluginProxy; } +std::vector PluginRequirements::proxied() const +{ + std::vector children; + if (dynamic_cast(m_Plugin)) { + for (auto* obj : m_PluginContainer->plugins()) { + auto* plugin = qobject_cast(obj); + if (plugin && m_PluginContainer->requirements(plugin).proxy() == m_Plugin) { + children.push_back(plugin); + } + } + } + return children; +} + IPlugin* PluginRequirements::master() const { if (m_Plugin->master().isEmpty()) { @@ -322,12 +333,15 @@ bool PluginContainer::initPlugin(IPlugin *plugin, IPluginProxy *pluginProxy) return true; } + auto [it, bl] = m_Requirements.emplace(plugin, PluginRequirements(this, plugin, proxy, pluginProxy)); + if (!plugin->init(proxy)) { log::warn("plugin failed to initialize"); return false; } - m_Requirements.emplace(plugin, PluginRequirements(this, plugin, proxy, pluginProxy)); + // Update requirements: + it->second.fetchRequirements(); return true; } @@ -519,11 +533,8 @@ bool PluginContainer::isEnabled(IPlugin* plugin) const return false; } - // Check the requirements (if a plugin checks in init(), the requirements have - // not been computed yet): - auto it = m_Requirements.find(plugin); - - return it == std::end(m_Requirements) ? true : it->second.canEnable(); + // Check the requirements: + return m_Requirements.at(plugin).canEnable(); } void PluginContainer::setEnabled(MOBase::IPlugin* plugin, bool enable, bool dependencies) diff --git a/src/plugincontainer.h b/src/plugincontainer.h index e9761cb1..b37b48cd 100644 --- a/src/plugincontainer.h +++ b/src/plugincontainer.h @@ -49,6 +49,11 @@ public: */ MOBase::IPluginProxy* proxy() const; + /** + * @return the list of plugins this plugin proxies (if it's a proxy plugin). + */ + std::vector proxied() const; + /** * @return the master of this plugin, if any. */ @@ -81,6 +86,11 @@ private: // Accumulator version for requiredFor() to avoid infinite recursion. void requiredFor(std::vector& required, std::set& visited) const; + // Retrieve the requirements from the underlying plugin, take ownership on them + // and store them. We cannot do this in the constructor because we want to have a + // constructed object before calling init(). + void fetchRequirements(); + friend class PluginContainer; PluginContainer* m_PluginContainer; diff --git a/src/settingsdialogplugins.cpp b/src/settingsdialogplugins.cpp index 15c0b424..e9d36c57 100644 --- a/src/settingsdialogplugins.cpp +++ b/src/settingsdialogplugins.cpp @@ -225,6 +225,8 @@ void PluginsSettingsTab::on_checkboxEnabled_clicked(bool checked) else { // Custom check for proxy + current game: if (m_pluginContainer->implementInterface(plugin)) { + + // Current game: auto* game = m_pluginContainer->managedGame(); if (m_pluginContainer->requirements(game).proxy() == plugin) { QMessageBox::warning( @@ -233,6 +235,25 @@ void PluginsSettingsTab::on_checkboxEnabled_clicked(bool checked) ui->enabledCheckbox->setChecked(true); return; } + + // Check the proxied plugins: + auto proxied = requirements.proxied(); + QStringList pluginNames; + for (auto& p : proxied) { + pluginNames.append(p->localizedName()); + } + pluginNames.sort(); + QString message = QObject::tr( + "

    Disabling this plugin will prevent the following plugins from working correctly:

      %1
    " + "

    Do you want to continue? You will need to restart ModOrganizer2 for the change to take effect.

    ") + .arg("
  • " + pluginNames.join("
  • ") + "
  • "); + if (QMessageBox::warning( + parentWidget(), QObject::tr("Really disable plugin?"), message, + QMessageBox::Yes | QMessageBox::No) == QMessageBox::No) { + ui->enabledCheckbox->setChecked(true); + return; + } + } // Check if the plugins is required for other plugins: @@ -256,6 +277,11 @@ void PluginsSettingsTab::on_checkboxEnabled_clicked(bool checked) m_pluginContainer->setEnabled(plugin, false, true); } + // Proxy was disabled / enabled, need restart: + if (m_pluginContainer->implementInterface(plugin)) { + dialog().setExitNeeded(Exit::Restart); + } + updateListItems(); } -- cgit v1.3.1 From 4fa44fe37e9b2f9837f7fcbaf9361f9675669782 Mon Sep 17 00:00:00 2001 From: Mikaël Capelle Date: Wed, 11 Nov 2020 22:54:45 +0100 Subject: Remove duplicate dependencies. --- src/settingsdialogplugins.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/settingsdialogplugins.cpp b/src/settingsdialogplugins.cpp index e9d36c57..556cadcb 100644 --- a/src/settingsdialogplugins.cpp +++ b/src/settingsdialogplugins.cpp @@ -242,12 +242,13 @@ void PluginsSettingsTab::on_checkboxEnabled_clicked(bool checked) for (auto& p : proxied) { pluginNames.append(p->localizedName()); } + pluginNames.removeDuplicates(); pluginNames.sort(); QString message = QObject::tr( - "

    Disabling this plugin will prevent the following plugins from working correctly:

      %1
    " + "

    Disabling this plugin will prevent the following plugins from working:

      %1
    " "

    Do you want to continue? You will need to restart ModOrganizer2 for the change to take effect.

    ") .arg("
  • " + pluginNames.join("
  • ") + "
  • "); - if (QMessageBox::warning( + if (QMessageBox::critical( parentWidget(), QObject::tr("Really disable plugin?"), message, QMessageBox::Yes | QMessageBox::No) == QMessageBox::No) { ui->enabledCheckbox->setChecked(true); @@ -263,6 +264,7 @@ void PluginsSettingsTab::on_checkboxEnabled_clicked(bool checked) for (auto& p : requiredFor) { pluginNames.append(p->localizedName()); } + pluginNames.removeDuplicates(); pluginNames.sort(); QString message = QObject::tr( "

    Disabling this plugin will also disable the following plugins:

      %1

    Do you want to continue?

    ") -- cgit v1.3.1 From 15059907c9bd0061f502c64457fce20c7e60a07f Mon Sep 17 00:00:00 2001 From: Mikaël Capelle Date: Thu, 12 Nov 2020 20:47:12 +0100 Subject: Proper handling of proxied plugins that implement multiple interfaces. --- src/plugincontainer.cpp | 149 ++++++++++++++++++++++++++++++++++-------- src/plugincontainer.h | 24 ++++++- src/settingsdialogplugins.cpp | 2 - 3 files changed, 144 insertions(+), 31 deletions(-) (limited to 'src') diff --git a/src/plugincontainer.cpp b/src/plugincontainer.cpp index 05c6790d..94e4760c 100644 --- a/src/plugincontainer.cpp +++ b/src/plugincontainer.cpp @@ -53,12 +53,18 @@ QStringList PluginContainer::pluginInterfaces() // PluginRequirementProxy -PluginRequirements::PluginRequirements(PluginContainer* pluginContainer, MOBase::IPlugin* plugin, IOrganizer* proxy, MOBase::IPluginProxy* pluginProxy) +PluginRequirements::PluginRequirements( + PluginContainer* pluginContainer, MOBase::IPlugin* plugin, IOrganizer* proxy, + MOBase::IPluginProxy* pluginProxy) : m_PluginContainer(pluginContainer) , m_Plugin(plugin) , m_PluginProxy(pluginProxy) + , m_Master(nullptr) , m_Organizer(proxy) -{ } +{ + // There are a lots of things we cannot set here (e.g. m_Master) because we do not + // know the order plugins are loaded. +} void PluginRequirements::fetchRequirements() { for (auto* requirement : m_Plugin->requirements()) { @@ -87,18 +93,36 @@ std::vector PluginRequirements::proxied() const IPlugin* PluginRequirements::master() const { + // If we have a m_Master, it was forced and thus override the default master(). + if (m_Master) { + return m_Master; + } + if (m_Plugin->master().isEmpty()) { return nullptr; } + return m_PluginContainer->plugin(m_Plugin->master()); } +void PluginRequirements::setMaster(IPlugin* master) +{ + m_Master = master; +} + std::vector PluginRequirements::children() const { std::vector children; for (auto* obj : m_PluginContainer->plugins()) { auto* plugin = qobject_cast(obj); - if (plugin && plugin->master().compare(m_Plugin->name(), Qt::CaseInsensitive) == 0) { + + // Not checking master() but requirements().master() due to "hidden" + // masters. + // If the master has the same name as the plugin, this is a "hidden" + // master, we do not had it here. + if (plugin + && m_PluginContainer->requirements(plugin).master() == m_Plugin + && plugin->name() != m_Plugin->name()) { children.push_back(plugin); } } @@ -205,7 +229,6 @@ PluginContainer::~PluginContainer() { unloadPlugins(); } - void PluginContainer::setUserInterface(IUserInterface *userInterface, QWidget *widget) { for (IPluginProxy *proxy : bf::at_key(m_Plugins)) { @@ -221,7 +244,6 @@ void PluginContainer::setUserInterface(IUserInterface *userInterface, QWidget *w m_UserInterface = userInterface; } - QStringList PluginContainer::implementedInterfaces(IPlugin* plugin) const { // We need a QObject to be able to qobject_cast<> to the plugin types: @@ -251,7 +273,6 @@ QStringList PluginContainer::implementedInterfaces(IPlugin* plugin) const return names; } - QString PluginContainer::topImplementedInterface(IPlugin* plugin) const { // We need a QObject to be able to qobject_cast<> to the plugin types: @@ -276,6 +297,21 @@ QString PluginContainer::topImplementedInterface(IPlugin* plugin) const return name; } +bool PluginContainer::isBetterInterface(QObject* lhs, QObject* rhs) const +{ + int count = 0, lhsIdx = -1, rhsIdx = -1; + boost::mp11::mp_for_each([&](const auto* p) { + using plugin_type = std::decay_t; + if (lhsIdx < 0 && qobject_cast(lhs)) { + lhsIdx = count; + } + if (rhsIdx < 0 && qobject_cast(rhs)) { + rhsIdx = count; + } + ++count; + }); + return lhsIdx < rhsIdx; +} QStringList PluginContainer::pluginFileNames() const { @@ -292,14 +328,13 @@ QStringList PluginContainer::pluginFileNames() const return result; } - QObject* PluginContainer::as_qobject(MOBase::IPlugin* plugin) const { // Find the correspond QObject - Can this be done safely with a cast? auto& objects = bf::at_key(m_Plugins); auto it = std::find_if(std::begin(objects), std::end(objects), [plugin](QObject* obj) { return qobject_cast(obj) == plugin; - }); + }); if (it == std::end(objects)) { return nullptr; @@ -351,19 +386,44 @@ void PluginContainer::registerGame(IPluginGame *game) m_SupportedGames.insert({ game->gameName(), game }); } -bool PluginContainer::registerPlugin(QObject *plugin, const QString &fileName, MOBase::IPluginProxy* pluginProxy) +IPlugin* PluginContainer::registerPlugin(QObject *plugin, const QString &fileName, MOBase::IPluginProxy* pluginProxy) { - // Storing the original QObject* is a bit of a hack as I couldn't figure out any - // way to cast directly between IPlugin* and IPluginDiagnose* - bf::at_key(m_Plugins).push_back(plugin); // generic treatment for all plugins IPlugin *pluginObj = qobject_cast(plugin); if (pluginObj == nullptr) { - log::debug("not an IPlugin"); - return false; + log::debug("PluginContainer::registerPlugin() called with a non IPlugin QObject."); + return nullptr; } - bf::at_key(m_AccessPlugins)[pluginObj->name()] = pluginObj; + + // If we already a plugin with this name: + auto& mapNames = bf::at_key(m_AccessPlugins); + if (mapNames.contains(pluginObj->name())) { + + IPlugin* other = mapNames[pluginObj->name()]; + + // If both plugins are from the same proxy and the same file, this is usually + // ok (in theory some one could write two different classes from the same Python file/module): + if (pluginProxy && m_Requirements.at(other).proxy() == pluginProxy + && as_qobject(other)->property("filename") == fileName) { + if (isBetterInterface(plugin, as_qobject(other))) { + log::debug("replacing plugin '{}' with interfaces [{}] by one with interfaces [{}]", + pluginObj->name(), implementedInterfaces(other).join(", "), implementedInterfaces(pluginObj).join(", ")); + bf::at_key(m_AccessPlugins)[pluginObj->name()] = pluginObj; + } + } + else { + log::warn("Trying to register two plugins with the name '{}', the second one will not be registered.", + pluginObj->name()); + } + } + else { + bf::at_key(m_AccessPlugins)[pluginObj->name()] = pluginObj; + } + + // Storing the original QObject* is a bit of a hack as I couldn't figure out any + // way to cast directly between IPlugin* and IPluginDiagnose* + bf::at_key(m_Plugins).push_back(plugin); plugin->setProperty("filename", fileName); @@ -392,7 +452,7 @@ bool PluginContainer::registerPlugin(QObject *plugin, const QString &fileName, M IPluginModPage *modPage = qobject_cast(plugin); if (initPlugin(modPage, pluginProxy)) { bf::at_key(m_Plugins).push_back(modPage); - return true; + return modPage; } } { // game plugin @@ -402,7 +462,7 @@ bool PluginContainer::registerPlugin(QObject *plugin, const QString &fileName, M if (initPlugin(game, pluginProxy)) { bf::at_key(m_Plugins).push_back(game); registerGame(game); - return true; + return game; } } } @@ -410,7 +470,7 @@ bool PluginContainer::registerPlugin(QObject *plugin, const QString &fileName, M IPluginTool *tool = qobject_cast(plugin); if (initPlugin(tool, pluginProxy)) { bf::at_key(m_Plugins).push_back(tool); - return true; + return tool; } } { // installer plugins @@ -420,7 +480,7 @@ bool PluginContainer::registerPlugin(QObject *plugin, const QString &fileName, M if (m_Organizer) { m_Organizer->installationManager()->registerInstaller(installer); } - return true; + return installer; } } { // preview plugins @@ -428,7 +488,7 @@ bool PluginContainer::registerPlugin(QObject *plugin, const QString &fileName, M if (initPlugin(preview, pluginProxy)) { bf::at_key(m_Plugins).push_back(preview); m_PreviewGenerator.registerPlugin(preview); - return true; + return preview; } } { // proxy plugins @@ -439,12 +499,21 @@ bool PluginContainer::registerPlugin(QObject *plugin, const QString &fileName, M QCoreApplication::applicationDirPath() + "/" + ToQString(AppConfig::pluginPath())); for (const QString &pluginName : pluginNames) { try { - // we get a list of matching plugins as proxies don't necessarily have a good way of supporting multiple inheritance + // We get a list of matching plugins as proxies can return multiple plugins + // per file and do not have a good way of supporting multiple inheritance. QList matchingPlugins = proxy->instantiate(pluginName); + + // We are going to group plugin by names and "fix" them later: + std::map> proxiedByNames; + for (QObject *proxiedPlugin : matchingPlugins) { if (proxiedPlugin != nullptr) { - if (registerPlugin(proxiedPlugin, pluginName, proxy)) { - log::debug("loaded plugin \"{}\"", QFileInfo(pluginName).fileName()); + if (IPlugin* proxied = registerPlugin(proxiedPlugin, pluginName, proxy); proxied) { + log::debug("loaded plugin '{}' from '{}' - [{}]", + proxied->name(), QFileInfo(pluginName).fileName(), implementedInterfaces(proxied).join(", ")); + + // Store the plugin for later: + proxiedByNames[proxied->name()].push_back(proxied); } else { log::warn( @@ -454,11 +523,28 @@ bool PluginContainer::registerPlugin(QObject *plugin, const QString &fileName, M } } } + + // Fake masters: + for (auto& [name, proxiedPlugins] : proxiedByNames) { + if (proxiedPlugins.size() > 1) { + auto it = std::min_element(std::begin(proxiedPlugins), std::end(proxiedPlugins), + [&](auto const& lhs, auto const& rhs) { + return isBetterInterface(as_qobject(lhs), as_qobject(rhs)); + }); + + for (auto& proxiedPlugin : proxiedPlugins) { + if (proxiedPlugin != *it) { + m_Requirements.at(proxiedPlugin).setMaster(*it); + } + } + } + } + } catch (const std::exception &e) { reportError(QObject::tr("failed to initialize plugin %1: %2").arg(pluginName).arg(e.what())); } } - return true; + return proxy; } } @@ -467,13 +553,13 @@ bool PluginContainer::registerPlugin(QObject *plugin, const QString &fileName, M IPlugin *dummy = qobject_cast(plugin); if (initPlugin(dummy, pluginProxy)) { bf::at_key(m_Plugins).push_back(dummy); - return true; + return dummy; } } log::debug("no matching plugin interface"); - return false; + return nullptr; } struct clearPlugins @@ -533,6 +619,12 @@ bool PluginContainer::isEnabled(IPlugin* plugin) const return false; } + auto& requirements = m_Requirements.at(plugin); + + if (requirements.master()) { + return isEnabled(requirements.master()); + } + // Check the requirements: return m_Requirements.at(plugin).canEnable(); } @@ -721,8 +813,9 @@ void PluginContainer::loadPlugins() "failed to load plugin {}: {}", pluginName, pluginLoader->errorString()); } else { - if (registerPlugin(pluginLoader->instance(), pluginName, nullptr)) { - log::debug("loaded plugin \"{}\"", QFileInfo(pluginName).fileName()); + if (IPlugin* plugin = registerPlugin(pluginLoader->instance(), pluginName, nullptr); plugin) { + log::debug("loaded plugin '{}' from '{}' - [{}]", + plugin->name(), QFileInfo(pluginName).fileName(), implementedInterfaces(plugin).join(", ")); m_PluginLoaders.push_back(pluginLoader.release()); } else { m_FailedPlugins.push_back(pluginName); diff --git a/src/plugincontainer.h b/src/plugincontainer.h index b37b48cd..89ab5528 100644 --- a/src/plugincontainer.h +++ b/src/plugincontainer.h @@ -91,11 +91,15 @@ private: // constructed object before calling init(). void fetchRequirements(); + // Set the master for this plugin. This is required to "fake" masters for proxied plugins. + void setMaster(MOBase::IPlugin* master); + friend class PluginContainer; PluginContainer* m_PluginContainer; MOBase::IPlugin* m_Plugin; MOBase::IPluginProxy* m_PluginProxy; + MOBase::IPlugin* m_Master; std::vector> m_Requirements; MOBase::IOrganizer* m_Organizer; std::vector m_RequiredFor; @@ -107,6 +111,9 @@ private: }; +/** + * + */ class PluginContainer : public QObject, public MOBase::IPluginDiagnose { @@ -229,6 +236,10 @@ public: * @param t Name of the plugin to retrieve, or non-IPlugin interface. * * @return the corresponding plugin, or a null pointer. + * + * @note It is possible to have multiple plugins for the same name when + * dealing with proxied plugins (e.g. Python), in which case the + * most important one will be returned, as specified in PluginTypeOrder. */ MOBase::IPlugin* plugin(QString const& pluginName) const; MOBase::IPlugin* plugin(MOBase::IPluginDiagnose* diagnose) const; @@ -320,6 +331,17 @@ private: friend class PluginRequirements; + /** + * @brief Check if a plugin implements a "better" interface than another + * one, as specified by PluginTypeOrder. + * + * @param lhs, rhs The plugin to compare. + * + * @return true if the left plugin implements a better interface than the right + * one, false otherwise (or if both implements the same interface). + */ + bool isBetterInterface(QObject* lhs, QObject* rhs) const; + /** * @brief Find the QObject* corresponding to the given plugin. * @@ -341,7 +363,7 @@ private: void registerGame(MOBase::IPluginGame *game); - bool registerPlugin(QObject *pluginObj, const QString &fileName, MOBase::IPluginProxy *proxy); + MOBase::IPlugin* registerPlugin(QObject *pluginObj, const QString &fileName, MOBase::IPluginProxy *proxy); OrganizerCore *m_Organizer; diff --git a/src/settingsdialogplugins.cpp b/src/settingsdialogplugins.cpp index 556cadcb..ba690d41 100644 --- a/src/settingsdialogplugins.cpp +++ b/src/settingsdialogplugins.cpp @@ -242,7 +242,6 @@ void PluginsSettingsTab::on_checkboxEnabled_clicked(bool checked) for (auto& p : proxied) { pluginNames.append(p->localizedName()); } - pluginNames.removeDuplicates(); pluginNames.sort(); QString message = QObject::tr( "

    Disabling this plugin will prevent the following plugins from working:

      %1
    " @@ -264,7 +263,6 @@ void PluginsSettingsTab::on_checkboxEnabled_clicked(bool checked) for (auto& p : requiredFor) { pluginNames.append(p->localizedName()); } - pluginNames.removeDuplicates(); pluginNames.sort(); QString message = QObject::tr( "

    Disabling this plugin will also disable the following plugins:

      %1

    Do you want to continue?

    ") -- cgit v1.3.1 From 56dcdd33a2110a8c5ad13cc9401ce87fb3d7bf80 Mon Sep 17 00:00:00 2001 From: Mikaël Capelle Date: Thu, 12 Nov 2020 21:23:35 +0100 Subject: It's 2020. --- src/plugincontainer.cpp | 51 +++++++++++++------------------------------ src/plugincontainer.h | 14 ++++++++++++ src/settingsdialogplugins.cpp | 2 +- 3 files changed, 30 insertions(+), 37 deletions(-) (limited to 'src') diff --git a/src/plugincontainer.cpp b/src/plugincontainer.cpp index 94e4760c..d2f42fb4 100644 --- a/src/plugincontainer.cpp +++ b/src/plugincontainer.cpp @@ -253,6 +253,11 @@ QStringList PluginContainer::implementedInterfaces(IPlugin* plugin) const return {}; } + return implementedInterfaces(oPlugin); +} + +QStringList PluginContainer::implementedInterfaces(QObject * oPlugin) const +{ // Find all the names: QStringList names; boost::mp11::mp_for_each([oPlugin, &names](const auto* p) { @@ -275,26 +280,8 @@ QStringList PluginContainer::implementedInterfaces(IPlugin* plugin) const QString PluginContainer::topImplementedInterface(IPlugin* plugin) const { - // We need a QObject to be able to qobject_cast<> to the plugin types: - QObject* oPlugin = as_qobject(plugin); - - if (!oPlugin) { - return {}; - } - - // Find all the names: - QString name; - boost::mp11::mp_for_each([oPlugin, &name](auto* p) { - using plugin_type = std::decay_t; - if (name.isEmpty() && qobject_cast(oPlugin)) { - auto tname = PluginTypeName::value(); - if (!tname.isEmpty()) { - name = tname; - } - } - }); - - return name; + auto interfaces = implementedInterfaces(plugin); + return interfaces.isEmpty() ? "" : interfaces[0]; } bool PluginContainer::isBetterInterface(QObject* lhs, QObject* rhs) const @@ -408,7 +395,7 @@ IPlugin* PluginContainer::registerPlugin(QObject *plugin, const QString &fileNam && as_qobject(other)->property("filename") == fileName) { if (isBetterInterface(plugin, as_qobject(other))) { log::debug("replacing plugin '{}' with interfaces [{}] by one with interfaces [{}]", - pluginObj->name(), implementedInterfaces(other).join(", "), implementedInterfaces(pluginObj).join(", ")); + pluginObj->name(), implementedInterfaces(other).join(", "), implementedInterfaces(plugin).join(", ")); bf::at_key(m_AccessPlugins)[pluginObj->name()] = pluginObj; } } @@ -562,15 +549,6 @@ IPlugin* PluginContainer::registerPlugin(QObject *plugin, const QString &fileNam return nullptr; } -struct clearPlugins -{ - template - void operator()(T& t) const - { - t.second.clear(); - } -}; - void PluginContainer::unloadPlugins() { if (m_UserInterface != nullptr) { @@ -582,7 +560,7 @@ void PluginContainer::unloadPlugins() m_Organizer->disconnectPlugins(); } - bf::for_each(m_Plugins, clearPlugins()); + bf::for_each(m_Plugins, [](auto& t) { t.second.clear(); }); for (const boost::signals2::connection &connection : m_DiagnosisConnections) { connection.disconnect(); @@ -614,17 +592,18 @@ bool PluginContainer::isEnabled(IPlugin* plugin) const return plugin == m_Organizer->managedGame(); } - // Check if the plugin is enabled: - if (!m_Organizer->persistent(plugin->name(), "enabled", true).toBool()) { - return false; - } - + // Check the master, if any: auto& requirements = m_Requirements.at(plugin); if (requirements.master()) { return isEnabled(requirements.master()); } + // Check if the plugin is enabled: + if (!m_Organizer->persistent(plugin->name(), "enabled", true).toBool()) { + return false; + } + // Check the requirements: return m_Requirements.at(plugin).canEnable(); } diff --git a/src/plugincontainer.h b/src/plugincontainer.h index 89ab5528..ac40e413 100644 --- a/src/plugincontainer.h +++ b/src/plugincontainer.h @@ -331,6 +331,20 @@ private: friend class PluginRequirements; + + /** + * @brief Retrieved the (localized) names of interfaces implemented by the given + * plugin. + * + * @param plugin The plugin to retrieve interface for. + * + * @return the (localized) names of interfaces implemented by this plugin. + * + * @note This function can be used to get implemented interfaces before registering + * a plugin. + */ + QStringList implementedInterfaces(QObject* plugin) const; + /** * @brief Check if a plugin implements a "better" interface than another * one, as specified by PluginTypeOrder. diff --git a/src/settingsdialogplugins.cpp b/src/settingsdialogplugins.cpp index ba690d41..f019cd2f 100644 --- a/src/settingsdialogplugins.cpp +++ b/src/settingsdialogplugins.cpp @@ -31,7 +31,7 @@ PluginsSettingsTab::PluginsSettingsTab(Settings& s, PluginContainer* pluginConta // display plugin settings QSet handledNames; for (IPlugin* plugin : settings().plugins().plugins()) { - if (handledNames.contains(plugin->name()) || !plugin->master().isEmpty()) { + if (handledNames.contains(plugin->name()) || m_pluginContainer->requirements(plugin).master()) { continue; } -- cgit v1.3.1 From a4acfc1f992eb5135c9ae6542ae5ffa8b09a2c2d Mon Sep 17 00:00:00 2001 From: Mikaël Capelle Date: Thu, 12 Nov 2020 21:49:57 +0100 Subject: Add callbacks for plugin enabled/disabled. --- src/organizercore.cpp | 15 +++++++++++++++ src/organizercore.h | 5 +++++ src/organizerproxy.cpp | 30 +++++++++++++++++++++++++++++- src/organizerproxy.h | 4 ++++ src/plugincontainer.cpp | 7 +++++++ src/plugincontainer.h | 6 ++++++ 6 files changed, 66 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/organizercore.cpp b/src/organizercore.cpp index 86afe59a..e004ba78 100644 --- a/src/organizercore.cpp +++ b/src/organizercore.cpp @@ -295,6 +295,11 @@ void OrganizerCore::connectPlugins(PluginContainer *container) m_GamePlugin = m_PluginContainer->managedGame(m_GameName); emit managedGameChanged(m_GamePlugin); } + + connect(m_PluginContainer, &PluginContainer::pluginEnabled, + [&](IPlugin* plugin) { m_PluginEnabled(plugin); }); + connect(m_PluginContainer, &PluginContainer::pluginDisabled, + [&](IPlugin* plugin) { m_PluginDisabled(plugin); }); } void OrganizerCore::disconnectPlugins() @@ -1225,6 +1230,16 @@ bool OrganizerCore::onPluginSettingChanged(std::function const& func) +{ + return m_PluginEnabled.connect(func).connected(); +} + +bool OrganizerCore::onPluginDisabled(std::function const& func) +{ + return m_PluginDisabled.connect(func).connected(); +} + void OrganizerCore::refresh(bool saveChanges) { // don't lose changes! diff --git a/src/organizercore.h b/src/organizercore.h index b566e626..23b624e8 100644 --- a/src/organizercore.h +++ b/src/organizercore.h @@ -87,6 +87,7 @@ private: using SignalProfileRemoved = boost::signals2::signal; using SignalProfileChanged = boost::signals2::signal; using SignalPluginSettingChanged = boost::signals2::signal; + using SignalPluginEnabled = boost::signals2::signal; public: @@ -335,6 +336,8 @@ public: bool onProfileRemoved(std::function const& func); bool onProfileChanged(std::function const& func); bool onPluginSettingChanged(std::function const& func); + bool onPluginEnabled(std::function const& func); + bool onPluginDisabled(std::function const& func); bool getArchiveParsing() const { @@ -457,6 +460,8 @@ private: SignalProfileRemoved m_ProfileRemoved; SignalProfileChanged m_ProfileChanged; SignalPluginSettingChanged m_PluginSettingChanged; + SignalPluginEnabled m_PluginEnabled; + SignalPluginEnabled m_PluginDisabled; ModList m_ModList; PluginList m_PluginList; diff --git a/src/organizerproxy.cpp b/src/organizerproxy.cpp index 77b7c2d6..c2283acc 100644 --- a/src/organizerproxy.cpp +++ b/src/organizerproxy.cpp @@ -293,8 +293,36 @@ bool OrganizerProxy::onProfileChanged(std::functiononProfileChanged(MOShared::callIfPluginActive(this, func)); } +// Always call these one, otherwise plugin cannot detect they are being enabled / disabled: bool OrganizerProxy::onPluginSettingChanged(std::function const& func) { - // Always call this one, otherwise plugin cannot detect they are being enabled / disabled: return m_Proxied->onPluginSettingChanged(func); } + +bool OrganizerProxy::onPluginEnabled(std::function const& func) +{ + return m_Proxied->onPluginEnabled(func); +} + +bool OrganizerProxy::onPluginEnabled(const QString& pluginName, std::function const& func) +{ + return m_Proxied->onPluginEnabled([=](const IPlugin* plugin) { + if (plugin->name().compare(pluginName, Qt::CaseInsensitive) == 0) { + func(); + } + }); +} + +bool OrganizerProxy::onPluginDisabled(std::function const& func) +{ + return m_Proxied->onPluginDisabled(func); +} + +bool OrganizerProxy::onPluginDisabled(const QString& pluginName, std::function const& func) +{ + return m_Proxied->onPluginDisabled([=](const IPlugin* plugin) { + if (plugin->name().compare(pluginName, Qt::CaseInsensitive) == 0) { + func(); + } + }); +} diff --git a/src/organizerproxy.h b/src/organizerproxy.h index 4329bdda..8664c64f 100644 --- a/src/organizerproxy.h +++ b/src/organizerproxy.h @@ -69,6 +69,10 @@ public: virtual QVariant pluginSetting(const QString& pluginName, const QString& key) const override; virtual void setPluginSetting(const QString& pluginName, const QString& key, const QVariant& value) override; virtual bool onPluginSettingChanged(std::function const& func) override; + virtual bool onPluginEnabled(std::function const& func) override; + virtual bool onPluginEnabled(const QString& pluginName, std::function const& func) override; + virtual bool onPluginDisabled(std::function const& func) override; + virtual bool onPluginDisabled(const QString& pluginName, std::function const& func) override; virtual MOBase::IPluginGame const *managedGame() const; diff --git a/src/plugincontainer.cpp b/src/plugincontainer.cpp index d2f42fb4..1c2a1a1a 100644 --- a/src/plugincontainer.cpp +++ b/src/plugincontainer.cpp @@ -624,6 +624,13 @@ void PluginContainer::setEnabled(MOBase::IPlugin* plugin, bool enable, bool depe } m_Organizer->setPersistent(plugin->name(), "enabled", enable, true); + + if (enable) { + emit pluginEnabled(plugin); + } + else { + emit pluginDisabled(plugin); + } } MOBase::IPlugin* PluginContainer::plugin(QString const& pluginName) const diff --git a/src/plugincontainer.h b/src/plugincontainer.h index ac40e413..54da37d0 100644 --- a/src/plugincontainer.h +++ b/src/plugincontainer.h @@ -325,6 +325,12 @@ public: // IPluginDiagnose interface signals: + /** + * @brief Emitted plugins are enabled or disabled. + */ + void pluginEnabled(MOBase::IPlugin*); + void pluginDisabled(MOBase::IPlugin*); + void diagnosisUpdate(); private: -- cgit v1.3.1 From 0d6bdf5721f1c968444cd55fbd8a89beb8de7980 Mon Sep 17 00:00:00 2001 From: Mikaël Capelle Date: Thu, 12 Nov 2020 22:08:55 +0100 Subject: Do not initialize proxied plugins twice. --- src/plugincontainer.cpp | 26 +++++++++++++++++--------- src/plugincontainer.h | 4 +++- 2 files changed, 20 insertions(+), 10 deletions(-) (limited to 'src') diff --git a/src/plugincontainer.cpp b/src/plugincontainer.cpp index 1c2a1a1a..91655b2c 100644 --- a/src/plugincontainer.cpp +++ b/src/plugincontainer.cpp @@ -330,7 +330,7 @@ QObject* PluginContainer::as_qobject(MOBase::IPlugin* plugin) const return *it; } -bool PluginContainer::initPlugin(IPlugin *plugin, IPluginProxy *pluginProxy) +bool PluginContainer::initPlugin(IPlugin *plugin, IPluginProxy *pluginProxy, bool skipInit) { // when MO has no instance loaded, init() is not called on plugins, except // for proxy plugins, where init() is called with a null IOrganizer @@ -357,7 +357,7 @@ bool PluginContainer::initPlugin(IPlugin *plugin, IPluginProxy *pluginProxy) auto [it, bl] = m_Requirements.emplace(plugin, PluginRequirements(this, plugin, proxy, pluginProxy)); - if (!plugin->init(proxy)) { + if (!skipInit && !plugin->init(proxy)) { log::warn("plugin failed to initialize"); return false; } @@ -384,6 +384,7 @@ IPlugin* PluginContainer::registerPlugin(QObject *plugin, const QString &fileNam } // If we already a plugin with this name: + bool skipInit = false; auto& mapNames = bf::at_key(m_AccessPlugins); if (mapNames.contains(pluginObj->name())) { @@ -393,6 +394,10 @@ IPlugin* PluginContainer::registerPlugin(QObject *plugin, const QString &fileNam // ok (in theory some one could write two different classes from the same Python file/module): if (pluginProxy && m_Requirements.at(other).proxy() == pluginProxy && as_qobject(other)->property("filename") == fileName) { + + // Plugin has already been initialized: + skipInit = true; + if (isBetterInterface(plugin, as_qobject(other))) { log::debug("replacing plugin '{}' with interfaces [{}] by one with interfaces [{}]", pluginObj->name(), implementedInterfaces(other).join(", "), implementedInterfaces(plugin).join(", ")); @@ -402,6 +407,7 @@ IPlugin* PluginContainer::registerPlugin(QObject *plugin, const QString &fileNam else { log::warn("Trying to register two plugins with the name '{}', the second one will not be registered.", pluginObj->name()); + return nullptr; } } else { @@ -437,7 +443,7 @@ IPlugin* PluginContainer::registerPlugin(QObject *plugin, const QString &fileNam } { // mod page plugin IPluginModPage *modPage = qobject_cast(plugin); - if (initPlugin(modPage, pluginProxy)) { + if (initPlugin(modPage, pluginProxy, skipInit)) { bf::at_key(m_Plugins).push_back(modPage); return modPage; } @@ -446,7 +452,7 @@ IPlugin* PluginContainer::registerPlugin(QObject *plugin, const QString &fileNam IPluginGame *game = qobject_cast(plugin); if (game) { game->detectGame(); - if (initPlugin(game, pluginProxy)) { + if (initPlugin(game, pluginProxy, skipInit)) { bf::at_key(m_Plugins).push_back(game); registerGame(game); return game; @@ -455,14 +461,14 @@ IPlugin* PluginContainer::registerPlugin(QObject *plugin, const QString &fileNam } { // tool plugins IPluginTool *tool = qobject_cast(plugin); - if (initPlugin(tool, pluginProxy)) { + if (initPlugin(tool, pluginProxy, skipInit)) { bf::at_key(m_Plugins).push_back(tool); return tool; } } { // installer plugins IPluginInstaller *installer = qobject_cast(plugin); - if (initPlugin(installer, pluginProxy)) { + if (initPlugin(installer, pluginProxy, skipInit)) { bf::at_key(m_Plugins).push_back(installer); if (m_Organizer) { m_Organizer->installationManager()->registerInstaller(installer); @@ -472,7 +478,7 @@ IPlugin* PluginContainer::registerPlugin(QObject *plugin, const QString &fileNam } { // preview plugins IPluginPreview *preview = qobject_cast(plugin); - if (initPlugin(preview, pluginProxy)) { + if (initPlugin(preview, pluginProxy, skipInit)) { bf::at_key(m_Plugins).push_back(preview); m_PreviewGenerator.registerPlugin(preview); return preview; @@ -480,7 +486,7 @@ IPlugin* PluginContainer::registerPlugin(QObject *plugin, const QString &fileNam } { // proxy plugins IPluginProxy *proxy = qobject_cast(plugin); - if (initPlugin(proxy, pluginProxy)) { + if (initPlugin(proxy, pluginProxy, skipInit)) { bf::at_key(m_Plugins).push_back(proxy); QStringList pluginNames = proxy->pluginList( QCoreApplication::applicationDirPath() + "/" + ToQString(AppConfig::pluginPath())); @@ -538,7 +544,7 @@ IPlugin* PluginContainer::registerPlugin(QObject *plugin, const QString &fileNam { // dummy plugins // only initialize these, no processing otherwise IPlugin *dummy = qobject_cast(plugin); - if (initPlugin(dummy, pluginProxy)) { + if (initPlugin(dummy, pluginProxy, skipInit)) { bf::at_key(m_Plugins).push_back(dummy); return dummy; } @@ -561,6 +567,8 @@ void PluginContainer::unloadPlugins() } bf::for_each(m_Plugins, [](auto& t) { t.second.clear(); }); + bf::for_each(m_AccessPlugins, [](auto& t) { t.second.clear(); }); + m_Requirements.clear(); for (const boost::signals2::connection &connection : m_DiagnosisConnections) { connection.disconnect(); diff --git a/src/plugincontainer.h b/src/plugincontainer.h index 54da37d0..e9f0f453 100644 --- a/src/plugincontainer.h +++ b/src/plugincontainer.h @@ -376,10 +376,12 @@ private: * * @param plugin The plugin to initialize. * @param proxy The proxy that created this plugin (can be null). + * @param skipInit If true, IPlugin::init() will not be called, regardless + * of the state of the container. * * @return true if the plugin was initialized correctly, false otherwise. */ - bool initPlugin(MOBase::IPlugin *plugin, MOBase::IPluginProxy* proxy); + bool initPlugin(MOBase::IPlugin *plugin, MOBase::IPluginProxy* proxy, bool skipInit); void registerGame(MOBase::IPluginGame *game); -- cgit v1.3.1 From 5384fb1cf6c2b1377a35dbef17166d6907b74652 Mon Sep 17 00:00:00 2001 From: Mikaël Capelle Date: Thu, 12 Nov 2020 22:30:22 +0100 Subject: Feeling like Shakespeare. --- src/plugincontainer.cpp | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) (limited to 'src') diff --git a/src/plugincontainer.cpp b/src/plugincontainer.cpp index 91655b2c..03950413 100644 --- a/src/plugincontainer.cpp +++ b/src/plugincontainer.cpp @@ -21,6 +21,54 @@ using namespace MOShared; namespace bf = boost::fusion; +// Welcome to the wonderful world of MO2 plugin management! +// +// We'll start by the C++ side. +// +// There are 9 types of MO2 plugins, two of which cannot be standalone: IPluginDiagnose +// and IPluginFileMapper. This means that you can have a class implementing IPluginGame, +// IPluginDiagnose and IPluginFileMapper. It is not possible for a class to implement +// two full plugin types (e.g. IPluginPreview and IPluginTool). +// +// Plugins are fetch as QObject initially and must be "qobject-casted" to the right type. +// +// Plugins are stored in the PluginContainer class in various C++ containers: there is a vector +// that stores all the plugin as QObject, multiple vectors that stores the plugin of each types, +// a map to find IPlugin object from their names or from IPluginDiagnose or IFileMapper (since +// these do not inherit IPlugin, they cannot be downcasted). +// +// Requirements for plugins are stored in m_Requirements: +// - IPluginGame cannot be enabled by user. A game plugin is considered enable only if it is +// the one corresponding to the currently managed games. +// - If a plugin has a master plugin (IPlugin::master()), it cannot be enabled/disabled by users, +// and will follow the enabled/disabled state of its parent. +// - Each plugin has an "enabled" setting stored in persistence. A plugin is considered disabled +// if the setting is false. +// - If the setting is true or does not exist, a plugin is considered disabled if one of its +// requirements is not met. +// - Users cannot enable a plugin if one of its requirements is not met. +// +// Now let's move to the Proxy side... Or the as of now, the Python side. +// +// Proxied plugins are much more annoying because they can implement all interfaces, and are +// given to MO2 as separate plugins... A Python class implementing IPluginGame and IPluginDiagnose +// will be seen by MO2 as two separate QObject, and they will all have the same name. +// +// When a proxied plugin is registered, a few things must be taken care of: +// - There can only be one plugin mapped to a name in the PluginContainer class, so we keep the +// plugin corresponding to the most relevant class (see PluginTypeOrder), e.g. if the class +// inherits both IPluginGame and IPluginFileMapper, we map the name to the C++ QObject corresponding +// to the IPluginGame. +// - When a proxied plugin implements multiple interfaces, the IPlugin corresponding to the most +// important interface is set as the parent (hidden) of the other IPlugin through PluginRequirements. +// This way, the plugin are managed together (enabled/disabled state). The "fake" children plugins +// will not be returned by PluginRequirements::children(). +// - Since each interface corresponds to a different QObject, we need to take care not to call +// IPlugin::init() on each QObject, but only on the first one. +// +// All the proxied plugins are linked to the proxy plugin by PluginRequirements. If the proxy plugin +// is disabled, the proxied plugins are not even loaded so not visible in the plugin management tab. + template struct PluginTypeName; -- cgit v1.3.1 From fe6654f89220c6739405ff7d9a54bc49f92a4c5b Mon Sep 17 00:00:00 2001 From: Mikaël Capelle Date: Fri, 13 Nov 2020 13:04:22 +0100 Subject: Better 'Browse Mod Page' menu. --- src/mainwindow.cpp | 56 +++++++++++++++++++++--------------------------------- src/mainwindow.h | 4 ---- src/mainwindow.ui | 22 +++++++++++++++++++++ 3 files changed, 44 insertions(+), 38 deletions(-) (limited to 'src') diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index b5a6382f..d20fd3ba 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -260,7 +260,6 @@ MainWindow::MainWindow(Settings &settings , m_ContextItem(nullptr) , m_ContextAction(nullptr) , m_ContextRow(-1) - , m_browseModPage(nullptr) , m_CurrentSaveView(nullptr) , m_OrganizerCore(organizerCore) , m_PluginContainer(pluginContainer) @@ -811,6 +810,7 @@ static QModelIndex mapToModel(const QAbstractItemModel *targetModel, QModelIndex void MainWindow::setupToolbar() { + setupActionMenu(ui->actionModPage); setupActionMenu(ui->actionTool); setupActionMenu(ui->actionHelp); setupActionMenu(ui->actionEndorseMO); @@ -1596,29 +1596,6 @@ bool MainWindow::eventFilter(QObject *object, QEvent *event) return false; } -void MainWindow::modPagePluginInvoke() -{ - QAction *triggeredAction = qobject_cast(sender()); - IPluginModPage *plugin = qobject_cast(triggeredAction->data().value()); - if (plugin != nullptr) { - if (plugin->useIntegratedBrowser()) { - - if (!m_IntegratedBrowser) { - m_IntegratedBrowser.reset(new BrowserDialog); - - connect( - m_IntegratedBrowser.get(), SIGNAL(requestDownload(QUrl,QNetworkReply*)), - &m_OrganizerCore, SLOT(requestDownload(QUrl,QNetworkReply*))); - } - - m_IntegratedBrowser->setWindowTitle(plugin->displayName()); - m_IntegratedBrowser->openUrl(plugin->pageURL()); - } else { - QDesktopServices::openUrl(QUrl(plugin->pageURL())); - } - } -} - void MainWindow::registerPluginTool(IPluginTool *tool, QString name, QMenu *menu) { if (!menu) { @@ -1694,23 +1671,34 @@ void MainWindow::updateToolMenu() void MainWindow::registerModPage(IPluginModPage *modPage) { // turn the browser action into a drop-down menu if necessary - if (!m_browseModPage) { - m_browseModPage = new QAction(ui->actionNexus->icon(), tr("Browse Mod Page"), this); - setupActionMenu(m_browseModPage); - - m_browseModPage->menu()->addAction(ui->actionNexus); - - ui->toolBar->insertAction(ui->actionNexus, m_browseModPage); + if (!ui->actionModPage->isVisible()) { ui->toolBar->removeAction(ui->actionNexus); + ui->actionModPage->menu()->addAction(ui->actionNexus); + ui->actionModPage->setVisible(true); } QAction *action = new QAction(modPage->icon(), modPage->displayName(), this); modPage->setParentWidget(this); - action->setData(QVariant::fromValue(reinterpret_cast(modPage))); + connect(action, &QAction::triggered, this, [this, modPage]() { + if (modPage->useIntegratedBrowser()) { + + if (!m_IntegratedBrowser) { + m_IntegratedBrowser.reset(new BrowserDialog); + + connect( + m_IntegratedBrowser.get(), SIGNAL(requestDownload(QUrl, QNetworkReply*)), + &m_OrganizerCore, SLOT(requestDownload(QUrl, QNetworkReply*))); + } - connect(action, SIGNAL(triggered()), this, SLOT(modPagePluginInvoke()), Qt::QueuedConnection); + m_IntegratedBrowser->setWindowTitle(modPage->displayName()); + m_IntegratedBrowser->openUrl(modPage->pageURL()); + } + else { + QDesktopServices::openUrl(QUrl(modPage->pageURL())); + } + }, Qt::QueuedConnection); - m_browseModPage->menu()->addAction(action); + ui->actionModPage->menu()->addAction(action); } diff --git a/src/mainwindow.h b/src/mainwindow.h index d4aa0bc2..78ffec47 100644 --- a/src/mainwindow.h +++ b/src/mainwindow.h @@ -156,8 +156,6 @@ public slots: void directory_refreshed(); - void modPagePluginInvoke(); - signals: /** @@ -325,8 +323,6 @@ private: QTreeWidgetItem *m_ContextItem; QAction *m_ContextAction; - QAction* m_browseModPage; - CategoryFactory &m_CategoryFactory; QTimer m_CheckBSATimer; diff --git a/src/mainwindow.ui b/src/mainwindow.ui index ace0dfeb..dbc6013d 100644 --- a/src/mainwindow.ui +++ b/src/mainwindow.ui @@ -1411,6 +1411,7 @@ p, li { white-space: pre-wrap; } + @@ -1684,6 +1685,27 @@ p, li { white-space: pre-wrap; } Ctrl+N + + + + :/MO/gui/resources/internet-web-browser.png:/MO/gui/resources/internet-web-browser.png + + + Browse Mod Page + + + Browse Mod Page + + + Browse Mod Page + + + Browse Mod Page + + + false + + false -- cgit v1.3.1 From 4d94ba8dbba20da6d030a8a3adf73d2dbd6fc56f Mon Sep 17 00:00:00 2001 From: Mikaël Capelle Date: Fri, 13 Nov 2020 13:18:05 +0100 Subject: Proper handling of mod pages. --- src/mainwindow.cpp | 51 +++++++++++++++++++++++++++++++++++++++++---------- src/mainwindow.h | 1 + 2 files changed, 42 insertions(+), 10 deletions(-) (limited to 'src') diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index d20fd3ba..c927a691 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -467,6 +467,11 @@ MainWindow::MainWindow(Settings &settings connect(ui->menuToolbars, &QMenu::aboutToShow, [&]{ updateToolbarMenu(); }); connect(ui->menuView, &QMenu::aboutToShow, [&]{ updateViewMenu(); }); connect(ui->actionTool->menu(), &QMenu::aboutToShow, [&] { updateToolMenu(); }); + connect(&m_PluginContainer, &PluginContainer::pluginEnabled, this, [this](IPlugin* plugin) { + if (m_PluginContainer.implementInterface(plugin)) { updateModPageMenu(); } }); + connect(&m_PluginContainer, &PluginContainer::pluginDisabled, this, [this](IPlugin* plugin) { + if (m_PluginContainer.implementInterface(plugin)) { updateModPageMenu(); } }); + connect(&m_OrganizerCore, &OrganizerCore::modInstalled, this, &MainWindow::modInstalled); connect(&m_OrganizerCore, &OrganizerCore::close, this, &QMainWindow::close); @@ -508,9 +513,7 @@ MainWindow::MainWindow(Settings &settings installTranslator(QFileInfo(fileName).baseName()); } - for (IPluginModPage *modPagePlugin : m_PluginContainer.plugins()) { - registerModPage(modPagePlugin); - } + updateModPageMenu(); // refresh profiles so the current profile can be activated refreshProfiles(false); @@ -1670,13 +1673,6 @@ void MainWindow::updateToolMenu() void MainWindow::registerModPage(IPluginModPage *modPage) { - // turn the browser action into a drop-down menu if necessary - if (!ui->actionModPage->isVisible()) { - ui->toolBar->removeAction(ui->actionNexus); - ui->actionModPage->menu()->addAction(ui->actionNexus); - ui->actionModPage->setVisible(true); - } - QAction *action = new QAction(modPage->icon(), modPage->displayName(), this); modPage->setParentWidget(this); connect(action, &QAction::triggered, this, [this, modPage]() { @@ -1701,6 +1697,41 @@ void MainWindow::registerModPage(IPluginModPage *modPage) ui->actionModPage->menu()->addAction(action); } +void MainWindow::updateModPageMenu() +{ + // Clear the menu: + ui->actionModPage->menu()->clear(); + ui->actionModPage->menu()->addAction(ui->actionNexus); + + std::vector modPagePlugins = m_PluginContainer.plugins(); + + // Sort the plugins by display name + std::sort(std::begin(modPagePlugins), std::end(modPagePlugins), + [](IPluginModPage* left, IPluginModPage* right) { + return left->displayName().toLower() < right->displayName().toLower(); + } + ); + + // Remove disabled plugins: + modPagePlugins.erase( + std::remove_if(std::begin(modPagePlugins), std::end(modPagePlugins), [&](auto* tool) { + return !m_PluginContainer.isEnabled(tool); + }), + modPagePlugins.end()); + + for (auto* modPagePlugin : modPagePlugins) { + registerModPage(modPagePlugin); + } + + // No mod page plugin and the menu was visible: + if (modPagePlugins.empty()) { + ui->toolBar->insertAction(ui->actionAdd_Profile, ui->actionNexus); + } + else { + ui->toolBar->removeAction(ui->actionNexus); + } + ui->actionModPage->setVisible(!modPagePlugins.empty()); +} void MainWindow::startExeAction() { diff --git a/src/mainwindow.h b/src/mainwindow.h index 78ffec47..e2805b25 100644 --- a/src/mainwindow.h +++ b/src/mainwindow.h @@ -210,6 +210,7 @@ private: void updateToolbarMenu(); void updateToolMenu(); + void updateModPageMenu(); void updateViewMenu(); QMenu* createPopupMenu() override; -- cgit v1.3.1 From 34e9fb001567a244eec296f5ab7e747a9f23db2a Mon Sep 17 00:00:00 2001 From: Mikaël Capelle Date: Fri, 13 Nov 2020 18:26:49 +0100 Subject: Check for disabled mod page plugins. --- src/organizercore.cpp | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) (limited to 'src') diff --git a/src/organizercore.cpp b/src/organizercore.cpp index e004ba78..ae031fe9 100644 --- a/src/organizercore.cpp +++ b/src/organizercore.cpp @@ -1465,10 +1465,13 @@ void OrganizerCore::loggedInAction(QWidget* parent, std::function f) void OrganizerCore::requestDownload(const QUrl &url, QNetworkReply *reply) { - if (m_PluginContainer != nullptr) { - for (IPluginModPage *modPage : - m_PluginContainer->plugins()) { - ModRepositoryFileInfo *fileInfo = new ModRepositoryFileInfo(); + if (!m_PluginContainer) { + return; + } + for (IPluginModPage *modPage : + m_PluginContainer->plugins()) { + if (m_PluginContainer->isEnabled(modPage)) { + ModRepositoryFileInfo* fileInfo = new ModRepositoryFileInfo(); if (modPage->handlesDownload(url, reply->url(), *fileInfo)) { fileInfo->repository = modPage->name(); m_DownloadManager.addDownload(reply, fileInfo); -- cgit v1.3.1 From 4c822e6563dc16d8e045ba48e8ca9b4e3fb4021a Mon Sep 17 00:00:00 2001 From: Mikaël Capelle Date: Sun, 15 Nov 2020 20:41:26 +0100 Subject: Allow '.' in findFiles and filter on file name, not full paths. --- src/organizercore.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/organizercore.cpp b/src/organizercore.cpp index f57903e2..ef9c190f 100644 --- a/src/organizercore.cpp +++ b/src/organizercore.cpp @@ -959,13 +959,13 @@ QStringList OrganizerCore::findFiles( { QStringList result; DirectoryEntry *dir = m_DirectoryStructure; - if (!path.isEmpty()) + if (!path.isEmpty() && path != ".") dir = dir->findSubDirectoryRecursive(ToWString(path)); if (dir != nullptr) { std::vector files = dir->getFiles(); for (FileEntryPtr &file: files) { QString fullPath = ToQString(file->getFullPath()); - if (filter(fullPath)) { + if (filter(ToQString(file->getName()))) { result.append(fullPath); } } -- cgit v1.3.1 From 037ca381a4c39490998c7642537dd54cc4f4aeee Mon Sep 17 00:00:00 2001 From: Mikaël Capelle Date: Tue, 17 Nov 2020 10:27:52 +0100 Subject: Fix filter for child plugins. --- src/settingsdialogplugins.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/settingsdialogplugins.cpp b/src/settingsdialogplugins.cpp index f019cd2f..19515426 100644 --- a/src/settingsdialogplugins.cpp +++ b/src/settingsdialogplugins.cpp @@ -128,7 +128,7 @@ void PluginsSettingsTab::filterPluginList() return regex.match(plugin->localizedName()).hasMatch(); }); for (auto* child : m_pluginContainer->requirements(plugin).children()) { - m_filter.matches([child](const QRegularExpression& regex) { + match = match || m_filter.matches([child](const QRegularExpression& regex) { return regex.match(child->localizedName()).hasMatch(); }); } -- cgit v1.3.1 From e0b73a7d7d27d42b87b3061c51d5d22f57646c45 Mon Sep 17 00:00:00 2001 From: Mikaël Capelle Date: Tue, 17 Nov 2020 10:45:29 +0100 Subject: Fix selection in Plugins settings. --- src/settingsdialog.ui | 130 +++++++++++++++++++++++------------------- src/settingsdialogplugins.cpp | 22 +++++-- 2 files changed, 87 insertions(+), 65 deletions(-) (limited to 'src') diff --git a/src/settingsdialog.ui b/src/settingsdialog.ui index 4fd34b21..d05910cb 100644 --- a/src/settingsdialog.ui +++ b/src/settingsdialog.ui @@ -1111,8 +1111,8 @@
    - - + + 0 @@ -1126,63 +1126,65 @@ 0 - - - 6 - - - - - Author: - - - - - - - - - - - - - - Version: - - - - - - - - - - - - - - - - - true - - - - - - - Description: - - - - - - - Enabled - - - - + + + + 6 + + + + + Author: + + + + + + + + + + + + + + Version: + + + + + + + + + + + + + + Description: + + + + + + + + + + true + + + + + + + Enabled + + + + + @@ -1216,6 +1218,16 @@ + + + + No plugin found. + + + Qt::AlignCenter + + + diff --git a/src/settingsdialogplugins.cpp b/src/settingsdialogplugins.cpp index 19515426..a86ba5c9 100644 --- a/src/settingsdialogplugins.cpp +++ b/src/settingsdialogplugins.cpp @@ -25,6 +25,7 @@ PluginsSettingsTab::PluginsSettingsTab(Settings& s, PluginContainer* pluginConta item->setFont(0, font); topItems[interfaceName] = item; item->setExpanded(true); + item->setFlags(item->flags() & ~Qt::ItemIsSelectable); } ui->pluginsList->setHeaderHidden(true); @@ -112,6 +113,7 @@ void PluginsSettingsTab::updateListItems() void PluginsSettingsTab::filterPluginList() { + auto selectedItems = ui->pluginsList->selectedItems(); QTreeWidgetItem* firstNotHidden = nullptr; for (auto i = 0; i < ui->pluginsList->topLevelItemCount(); ++i) { @@ -150,13 +152,21 @@ void PluginsSettingsTab::filterPluginList() } // Unselect item if hidden: - auto selectedItems = ui->pluginsList->selectedItems(); - if (!selectedItems.isEmpty() && selectedItems[0]->isHidden()) { - selectedItems[0]->setSelected(false); - - if (firstNotHidden) { - firstNotHidden->setSelected(true); + if (firstNotHidden) { + ui->pluginDescription->setVisible(true); + ui->pluginSettingsList->setVisible(true); + ui->noPluginLabel->setVisible(false); + if (selectedItems.isEmpty()) { + ui->pluginsList->setCurrentItem(firstNotHidden); } + else if (selectedItems[0]->isHidden()) { + ui->pluginsList->setCurrentItem(firstNotHidden); + } + } + else { + ui->pluginDescription->setVisible(false); + ui->pluginSettingsList->setVisible(false); + ui->noPluginLabel->setVisible(true); } } -- cgit v1.3.1 From c62da01478822daa9837529916b3290ab07fd7a3 Mon Sep 17 00:00:00 2001 From: Mikaël Capelle Date: Tue, 17 Nov 2020 10:49:29 +0100 Subject: Replace : by . in comments. --- src/installationmanager.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/src/installationmanager.h b/src/installationmanager.h index e1db5e1f..145abdb6 100644 --- a/src/installationmanager.h +++ b/src/installationmanager.h @@ -285,7 +285,7 @@ private: private: - // The plugin container, mostly to check if installer are enabled or not: + // The plugin container, mostly to check if installer are enabled or not. const PluginContainer *m_PluginContainer; bool m_IsRunning; @@ -298,13 +298,13 @@ private: std::vector m_Installers; std::set m_SupportedExtensions; - // Archive management: + // Archive management. std::unique_ptr m_ArchiveHandler; QString m_CurrentFile; QString m_Password; // Map from entries in the tree that is used by the installer and absolute - // paths to temporary files: + // paths to temporary files. std::map, QString> m_CreatedFiles; std::set m_TempFilesToDelete; }; -- cgit v1.3.1 From 9fd2a757fa0c25c72c6d8927924d0a8668e948c4 Mon Sep 17 00:00:00 2001 From: Mikaël Capelle Date: Tue, 17 Nov 2020 10:50:27 +0100 Subject: Fix had -> add. --- src/plugincontainer.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/plugincontainer.cpp b/src/plugincontainer.cpp index 03950413..8c2ece76 100644 --- a/src/plugincontainer.cpp +++ b/src/plugincontainer.cpp @@ -167,7 +167,7 @@ std::vector PluginRequirements::children() const // Not checking master() but requirements().master() due to "hidden" // masters. // If the master has the same name as the plugin, this is a "hidden" - // master, we do not had it here. + // master, we do not add it here. if (plugin && m_PluginContainer->requirements(plugin).master() == m_Plugin && plugin->name() != m_Plugin->name()) { -- cgit v1.3.1 From 1e67c3e68dbe4141c9cd3bfd7b29c747fca03753 Mon Sep 17 00:00:00 2001 From: Mikaël Capelle Date: Tue, 17 Nov 2020 12:30:59 +0100 Subject: Add core plugins and better messages. --- src/plugincontainer.cpp | 20 +++++++++++++++++++- src/plugincontainer.h | 9 +++++++++ src/settingsdialogplugins.cpp | 13 +++++++------ 3 files changed, 35 insertions(+), 7 deletions(-) (limited to 'src') diff --git a/src/plugincontainer.cpp b/src/plugincontainer.cpp index 8c2ece76..7884498c 100644 --- a/src/plugincontainer.cpp +++ b/src/plugincontainer.cpp @@ -101,6 +101,10 @@ QStringList PluginContainer::pluginInterfaces() // PluginRequirementProxy +const std::set PluginRequirements::s_CorePlugins{ + "INI Bakery" +}; + PluginRequirements::PluginRequirements( PluginContainer* pluginContainer, MOBase::IPlugin* plugin, IOrganizer* proxy, MOBase::IPluginProxy* pluginProxy) @@ -193,6 +197,16 @@ bool PluginRequirements::canEnable() const return problems().empty(); } +bool PluginRequirements::isCorePlugin() const +{ + // Let's consider game plugins as "core": + if (m_PluginContainer->implementInterface(m_Plugin)) { + return true; + } + + return s_CorePlugins.contains(m_Plugin->name()); +} + bool PluginRequirements::hasRequirements() const { return !m_Requirements.empty(); @@ -405,7 +419,11 @@ bool PluginContainer::initPlugin(IPlugin *plugin, IPluginProxy *pluginProxy, boo auto [it, bl] = m_Requirements.emplace(plugin, PluginRequirements(this, plugin, proxy, pluginProxy)); - if (!skipInit && !plugin->init(proxy)) { + if (skipInit) { + return true; + } + + if (!plugin->init(proxy)) { log::warn("plugin failed to initialize"); return false; } diff --git a/src/plugincontainer.h b/src/plugincontainer.h index e9f0f453..36edfad9 100644 --- a/src/plugincontainer.h +++ b/src/plugincontainer.h @@ -39,6 +39,12 @@ public: */ bool canEnable() const; + /** + * @return true if this is a core plugin, i.e. a plugin that should not be + * manually enabled or disabled by the user. + */ + bool isCorePlugin() const; + /** * @return true if this plugin has requirements (satisfied or not). */ @@ -83,6 +89,9 @@ public: private: + // The list of "Core" plugins. + static const std::set s_CorePlugins; + // Accumulator version for requiredFor() to avoid infinite recursion. void requiredFor(std::vector& required, std::set& visited) const; diff --git a/src/settingsdialogplugins.cpp b/src/settingsdialogplugins.cpp index a86ba5c9..0c860fa0 100644 --- a/src/settingsdialogplugins.cpp +++ b/src/settingsdialogplugins.cpp @@ -241,7 +241,8 @@ void PluginsSettingsTab::on_checkboxEnabled_clicked(bool checked) if (m_pluginContainer->requirements(game).proxy() == plugin) { QMessageBox::warning( parentWidget(), QObject::tr("Cannot disable plugin"), - QObject::tr("This plugin is used by the current game plugin and cannot disabled."), QMessageBox::Ok); + QObject::tr("The '%1' plugin is used by the current game plugin and cannot disabled.") + .arg(plugin->localizedName()), QMessageBox::Ok); ui->enabledCheckbox->setChecked(true); return; } @@ -254,9 +255,9 @@ void PluginsSettingsTab::on_checkboxEnabled_clicked(bool checked) } pluginNames.sort(); QString message = QObject::tr( - "

    Disabling this plugin will prevent the following plugins from working:

      %1
    " + "

    Disabling the '%1' plugin will prevent the following plugins from working:

      %1
    " "

    Do you want to continue? You will need to restart ModOrganizer2 for the change to take effect.

    ") - .arg("
  • " + pluginNames.join("
  • ") + "
  • "); + .arg(plugin->localizedName()).arg("
  • " + pluginNames.join("
  • ") + "
  • "); if (QMessageBox::critical( parentWidget(), QObject::tr("Really disable plugin?"), message, QMessageBox::Yes | QMessageBox::No) == QMessageBox::No) { @@ -275,8 +276,8 @@ void PluginsSettingsTab::on_checkboxEnabled_clicked(bool checked) } pluginNames.sort(); QString message = QObject::tr( - "

    Disabling this plugin will also disable the following plugins:

      %1

    Do you want to continue?

    ") - .arg("
  • " + pluginNames.join("
  • ") + "
  • "); + "

    Disabling the '%1' plugin will also disable the following plugins:

      %1

    Do you want to continue?

    ") + .arg(plugin->localizedName()).arg("
  • " + pluginNames.join("
  • ") + "
  • "); if (QMessageBox::warning( parentWidget(), QObject::tr("Really disable plugin?"), message, QMessageBox::Yes | QMessageBox::No) == QMessageBox::No) { @@ -310,7 +311,7 @@ void PluginsSettingsTab::on_pluginsList_currentItemChanged(QTreeWidgetItem *curr ui->descriptionLabel->setText(plugin->description()); ui->enabledCheckbox->setVisible( - !m_pluginContainer->implementInterface(plugin) + !m_pluginContainer->requirements(plugin).isCorePlugin() && plugin->master().isEmpty()); ui->enabledCheckbox->setChecked(m_pluginContainer->isEnabled(plugin)); -- cgit v1.3.1 From e9a4f91fac08087ef83ce5b7716abeda90b6bde3 Mon Sep 17 00:00:00 2001 From: Mikaël Capelle Date: Tue, 17 Nov 2020 13:24:57 +0100 Subject: Display inactive checkbox for core plugins (not for games). --- src/settingsdialogplugins.cpp | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/settingsdialogplugins.cpp b/src/settingsdialogplugins.cpp index 0c860fa0..eb0035b2 100644 --- a/src/settingsdialogplugins.cpp +++ b/src/settingsdialogplugins.cpp @@ -310,9 +310,17 @@ void PluginsSettingsTab::on_pluginsList_currentItemChanged(QTreeWidgetItem *curr ui->versionLabel->setText(plugin->version().canonicalString()); ui->descriptionLabel->setText(plugin->description()); + ui->enabledCheckbox->setDisabled(false); + ui->enabledCheckbox->setToolTip(""); ui->enabledCheckbox->setVisible( - !m_pluginContainer->requirements(plugin).isCorePlugin() + !m_pluginContainer->implementInterface(plugin) && plugin->master().isEmpty()); + if (m_pluginContainer->requirements(plugin).isCorePlugin()) { + ui->enabledCheckbox->setDisabled(true); + ui->enabledCheckbox->setToolTip( + QObject::tr("This plugin is required for Mod Organizer to work properly and cannot be disabled.")); + } + ui->enabledCheckbox->setChecked(m_pluginContainer->isEnabled(plugin)); QVariantMap settings = current->data(0, ROLE_SETTINGS).toMap(); -- cgit v1.3.1 From bed3475362f68a660d56dd31546b13e49586e36e Mon Sep 17 00:00:00 2001 From: Mikaël Capelle Date: Tue, 17 Nov 2020 14:28:00 +0100 Subject: Fix crash for proxied plugins on first startup. --- src/plugincontainer.cpp | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) (limited to 'src') diff --git a/src/plugincontainer.cpp b/src/plugincontainer.cpp index 7884498c..f7a556e0 100644 --- a/src/plugincontainer.cpp +++ b/src/plugincontainer.cpp @@ -413,12 +413,12 @@ bool PluginContainer::initPlugin(IPlugin *plugin, IPluginProxy *pluginProxy, boo // Check if it is a proxy plugin: bool isProxy = dynamic_cast(plugin); + auto [it, bl] = m_Requirements.emplace(plugin, PluginRequirements(this, plugin, proxy, pluginProxy)); + if (!m_Organizer && !isProxy) { return true; } - auto [it, bl] = m_Requirements.emplace(plugin, PluginRequirements(this, plugin, proxy, pluginProxy)); - if (skipInit) { return true; } @@ -616,8 +616,6 @@ IPlugin* PluginContainer::registerPlugin(QObject *plugin, const QString &fileNam } } - log::debug("no matching plugin interface"); - return nullptr; } -- cgit v1.3.1 From 8886674379c507fe46bfc6cb2ef013537567441b Mon Sep 17 00:00:00 2001 From: Mikaël Capelle Date: Tue, 17 Nov 2020 14:28:43 +0100 Subject: Better dialog for warning when disabling proxy plugin. --- src/CMakeLists.txt | 1 + src/disableproxyplugindialog.cpp | 30 +++++++ src/disableproxyplugindialog.h | 43 ++++++++++ src/disableproxyplugindialog.ui | 174 +++++++++++++++++++++++++++++++++++++++ src/settingsdialogplugins.cpp | 28 +++---- 5 files changed, 258 insertions(+), 18 deletions(-) create mode 100644 src/disableproxyplugindialog.cpp create mode 100644 src/disableproxyplugindialog.h create mode 100644 src/disableproxyplugindialog.ui (limited to 'src') diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index bb4b151f..a53d845d 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -186,6 +186,7 @@ add_filter(NAME src/settingsdialog GROUPS settingsdialogplugins settingsdialogsteam settingsdialogworkarounds + disableproxyplugindialog ) add_filter(NAME src/utilities GROUPS diff --git a/src/disableproxyplugindialog.cpp b/src/disableproxyplugindialog.cpp new file mode 100644 index 00000000..f392113b --- /dev/null +++ b/src/disableproxyplugindialog.cpp @@ -0,0 +1,30 @@ +#include "disableproxyplugindialog.h" + +#include "ui_proxyplugindialog.h" + +using namespace MOBase; + +DisableProxyPluginDialog::DisableProxyPluginDialog( + MOBase::IPlugin* proxyPlugin, std::vector const& required, QWidget* parent) + : QDialog(parent), ui(new Ui::DisableProxyPluginDialog) +{ + ui->setupUi(this); + + ui->topLabel->setText(QObject::tr( + "Disabling the '%1' plugin will prevent the following %2 plugin(s) from working:", "", required.size()) + .arg(proxyPlugin->localizedName()) + .arg(required.size())); + + connect(ui->noBtn, &QPushButton::clicked, this, &QDialog::reject); + connect(ui->yesBtn, &QPushButton::clicked, this, &QDialog::accept); + + ui->requiredPlugins->setSelectionMode(QAbstractItemView::NoSelection); + ui->requiredPlugins->setRowCount(required.size()); + for (int i = 0; i < required.size(); ++i) { + ui->requiredPlugins->setItem(i, 0, new QTableWidgetItem(required[i]->localizedName())); + ui->requiredPlugins->setItem(i, 1, new QTableWidgetItem(required[i]->description())); + ui->requiredPlugins->setRowHeight(i, 9); + } + ui->requiredPlugins->verticalHeader()->setVisible(false); + ui->requiredPlugins->sortByColumn(0, Qt::AscendingOrder); +} diff --git a/src/disableproxyplugindialog.h b/src/disableproxyplugindialog.h new file mode 100644 index 00000000..52f552e1 --- /dev/null +++ b/src/disableproxyplugindialog.h @@ -0,0 +1,43 @@ +/* +Copyright (C) 2020 Mikaël Capelle. All rights reserved. + +This file is part of Mod Organizer. + +Mod Organizer 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, either version 3 of the License, or +(at your option) any later version. + +Mod Organizer 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 Mod Organizer. If not, see . +*/ + +#ifndef DISABLEPROXYPLUGINDIALOG_H +#define DISABLEPROXYPLUGINDIALOG_H + +#include + +#include "ipluginproxy.h" + +namespace Ui { class DisableProxyPluginDialog; } + +class DisableProxyPluginDialog : public QDialog { +public: + + DisableProxyPluginDialog( + MOBase::IPlugin* proxyPlugin, + std::vector const& required, + QWidget* parent = nullptr); + +private slots: + + Ui::DisableProxyPluginDialog* ui; + +}; + +#endif diff --git a/src/disableproxyplugindialog.ui b/src/disableproxyplugindialog.ui new file mode 100644 index 00000000..9f068787 --- /dev/null +++ b/src/disableproxyplugindialog.ui @@ -0,0 +1,174 @@ + + + DisableProxyPluginDialog + + + + 0 + 0 + 522 + 417 + + + + Really disable plugin? + + + + + + + 0 + 0 + + + + + + + + 0 + 0 + + + + + + + Qt::PlainText + + + :/MO/gui/remove + + + Qt::AlignCenter + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 10 + 20 + + + + + + + + Disabling the '%1' plugin will prevent the following plugins from working: + + + + + + + + + + 2 + + + true + + + + Plugin + + + + + Description + + + + + + + + Do you want to continue? You will need to restart Mod Organizer for the change to take effect. + + + + + + + + 0 + 0 + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 80 + 0 + + + + Yes + + + + :/MO/gui/remove:/MO/gui/remove + + + + + + + + 0 + 0 + + + + + 80 + 0 + + + + No + + + true + + + false + + + + + + + + + + + + + diff --git a/src/settingsdialogplugins.cpp b/src/settingsdialogplugins.cpp index eb0035b2..4313b85e 100644 --- a/src/settingsdialogplugins.cpp +++ b/src/settingsdialogplugins.cpp @@ -3,6 +3,7 @@ #include "noeditdelegate.h" #include +#include "disableproxyplugindialog.h" #include "organizercore.h" #include "plugincontainer.h" @@ -242,29 +243,20 @@ void PluginsSettingsTab::on_checkboxEnabled_clicked(bool checked) QMessageBox::warning( parentWidget(), QObject::tr("Cannot disable plugin"), QObject::tr("The '%1' plugin is used by the current game plugin and cannot disabled.") - .arg(plugin->localizedName()), QMessageBox::Ok); - ui->enabledCheckbox->setChecked(true); - return; + .arg(plugin->localizedName()), QMessageBox::Ok); + ui->enabledCheckbox->setChecked(true); + return; } // Check the proxied plugins: auto proxied = requirements.proxied(); - QStringList pluginNames; - for (auto& p : proxied) { - pluginNames.append(p->localizedName()); - } - pluginNames.sort(); - QString message = QObject::tr( - "

    Disabling the '%1' plugin will prevent the following plugins from working:

      %1
    " - "

    Do you want to continue? You will need to restart ModOrganizer2 for the change to take effect.

    ") - .arg(plugin->localizedName()).arg("
  • " + pluginNames.join("
  • ") + "
  • "); - if (QMessageBox::critical( - parentWidget(), QObject::tr("Really disable plugin?"), message, - QMessageBox::Yes | QMessageBox::No) == QMessageBox::No) { - ui->enabledCheckbox->setChecked(true); - return; + if (!proxied.empty()) { + DisableProxyPluginDialog dialog(plugin, proxied, parentWidget()); + if (dialog.exec() != QDialog::Accepted) { + ui->enabledCheckbox->setChecked(true); + return; + } } - } // Check if the plugins is required for other plugins: -- cgit v1.3.1 From 1d6d01dec887126b70d1de4bbf562d3803072f68 Mon Sep 17 00:00:00 2001 From: Mikaël Capelle Date: Tue, 17 Nov 2020 18:24:10 +0100 Subject: Disable checkbox when plugin cannot be enabled. --- src/disableproxyplugindialog.cpp | 2 +- src/settingsdialogplugins.cpp | 46 ++++++++++++++++++++++++---------------- 2 files changed, 29 insertions(+), 19 deletions(-) (limited to 'src') diff --git a/src/disableproxyplugindialog.cpp b/src/disableproxyplugindialog.cpp index f392113b..c05e30e3 100644 --- a/src/disableproxyplugindialog.cpp +++ b/src/disableproxyplugindialog.cpp @@ -1,6 +1,6 @@ #include "disableproxyplugindialog.h" -#include "ui_proxyplugindialog.h" +#include "ui_disableproxyplugindialog.h" using namespace MOBase; diff --git a/src/settingsdialogplugins.cpp b/src/settingsdialogplugins.cpp index 4313b85e..16f6fb0a 100644 --- a/src/settingsdialogplugins.cpp +++ b/src/settingsdialogplugins.cpp @@ -217,20 +217,6 @@ void PluginsSettingsTab::on_checkboxEnabled_clicked(bool checked) // User wants to enable: if (checked) { - auto problems = requirements.problems(); - if (!problems.empty()) { - QStringList descriptions; - for (auto& problem : problems) { - descriptions.append(problem.shortDescription()); - } - QMessageBox::warning( - parentWidget(), QObject::tr("Cannot enable plugin"), - QObject::tr("

    This plugin cannot be enabled:

      %1
    ") - .arg("
  • " + descriptions.join("
  • ") + "
  • "), QMessageBox::Ok); - ui->enabledCheckbox->setChecked(false); - return; - } - m_pluginContainer->setEnabled(plugin, true, false); } else { @@ -302,18 +288,42 @@ void PluginsSettingsTab::on_pluginsList_currentItemChanged(QTreeWidgetItem *curr ui->versionLabel->setText(plugin->version().canonicalString()); ui->descriptionLabel->setText(plugin->description()); - ui->enabledCheckbox->setDisabled(false); - ui->enabledCheckbox->setToolTip(""); + // Checkbox, do not show for children or game plugins, disable + // if the plugin cannot be enabled. ui->enabledCheckbox->setVisible( !m_pluginContainer->implementInterface(plugin) && plugin->master().isEmpty()); + + bool enabled = m_pluginContainer->isEnabled(plugin); + auto& requirements = m_pluginContainer->requirements(plugin); + auto problems = requirements.problems(); + if (m_pluginContainer->requirements(plugin).isCorePlugin()) { ui->enabledCheckbox->setDisabled(true); ui->enabledCheckbox->setToolTip( QObject::tr("This plugin is required for Mod Organizer to work properly and cannot be disabled.")); } - - ui->enabledCheckbox->setChecked(m_pluginContainer->isEnabled(plugin)); + // Plugin is enable or can be enabled. + else if (enabled || problems.empty()) { + ui->enabledCheckbox->setDisabled(false); + ui->enabledCheckbox->setToolTip(""); + ui->enabledCheckbox->setChecked(enabled); + } + // Plugin is disable and cannot be enabled. + else { + ui->enabledCheckbox->setDisabled(true); + ui->enabledCheckbox->setChecked(false); + if (problems.size() == 1) { + ui->enabledCheckbox->setToolTip(problems[0].shortDescription()); + } + else { + QStringList descriptions; + for (auto& problem : problems) { + descriptions.append(problem.shortDescription()); + } + ui->enabledCheckbox->setToolTip("
    • " + descriptions.join("
    • ") + "
    "); + } + } QVariantMap settings = current->data(0, ROLE_SETTINGS).toMap(); QVariantMap descriptions = current->data(0, ROLE_DESCRIPTIONS).toMap(); -- cgit v1.3.1 From 61554d3df7be909acf6609b4b7465b03d7f16081 Mon Sep 17 00:00:00 2001 From: Mikaël Capelle Date: Tue, 17 Nov 2020 18:27:21 +0100 Subject: Better vertical alignment for plugin descriptions. --- src/settingsdialog.ui | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) (limited to 'src') diff --git a/src/settingsdialog.ui b/src/settingsdialog.ui index d05910cb..2d4ca993 100644 --- a/src/settingsdialog.ui +++ b/src/settingsdialog.ui @@ -1128,6 +1128,9 @@ + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + 6 @@ -1136,6 +1139,9 @@ Author: + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + @@ -1143,6 +1149,9 @@ + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + @@ -1150,6 +1159,9 @@ Version: + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + @@ -1157,6 +1169,9 @@ + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + @@ -1164,6 +1179,9 @@ Description: + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + @@ -1171,6 +1189,9 @@ + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + true -- cgit v1.3.1 From e39de4dd2ab6c19c6b9557f99117f7ffc9ed1cc1 Mon Sep 17 00:00:00 2001 From: Mikaël Capelle Date: Wed, 18 Nov 2020 19:22:05 +0100 Subject: Update following addition of IPluginGame::listSaves(). --- src/mainwindow.cpp | 53 +++++++--------------- src/mainwindow.h | 1 + src/transfersavesdialog.cpp | 105 ++++---------------------------------------- src/transfersavesdialog.h | 7 +-- 4 files changed, 30 insertions(+), 136 deletions(-) (limited to 'src') diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index c2aefdd2..ecb4319b 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -1206,7 +1206,7 @@ void MainWindow::createHelpMenu() ActionList tutorials; - QString tutorialPath = QApplication::applicationDirPath() + QString tutorialPath = QApplication::applicationDirPath() + "/" + QString::fromStdWString(AppConfig::tutorialsPath()) + "/"; QDirIterator dirIter(tutorialPath, QStringList("*.js"), QDir::Files); while (dirIter.hasNext()) { @@ -1532,7 +1532,6 @@ void MainWindow::displaySaveGameInfo(QListWidgetItem *newItem) return; } - QString const &save = newItem->data(Qt::UserRole).toString(); if (m_CurrentSaveView == nullptr) { IPluginGame const *game = m_OrganizerCore.managedGame(); SaveGameInfo const *info = game->feature(); @@ -1543,7 +1542,7 @@ void MainWindow::displaySaveGameInfo(QListWidgetItem *newItem) return; } } - m_CurrentSaveView->setSave(save); + m_CurrentSaveView->setSave(*m_SaveGames[ui->savegameList->row(newItem)]); QWindow *window = m_CurrentSaveView->window()->windowHandle(); QRect screenRect; @@ -1964,36 +1963,23 @@ void MainWindow::stopMonitorSaves() void MainWindow::refreshSaveList() { - ui->savegameList->clear(); + TimeThis tt("MainWindow::refreshSaveList()"); startMonitorSaves(); // re-starts monitoring - QStringList filters; - filters << QString("*.") + m_OrganizerCore.managedGame()->savegameExtension(); - QDir savesDir = currentSavesDir(); - savesDir.setNameFilters(filters); - savesDir.setFilter(QDir::Files); - QDirIterator it(savesDir, QDirIterator::Subdirectories); - log::debug("reading save games from {}", savesDir.absolutePath()); - - QFileInfoList files; - while (it.hasNext()) { - it.next(); - files.append(it.fileInfo()); - } - std::sort(files.begin(), files.end(), [](auto const& lhs, auto const& rhs) { - return lhs.fileTime(QFileDevice::FileModificationTime) > rhs.fileTime(QFileDevice::FileModificationTime); + MOBase::log::debug("reading save games from {}", savesDir.absolutePath()); + m_SaveGames = m_OrganizerCore.managedGame()->listSaves(savesDir); + std::sort(m_SaveGames.begin(), m_SaveGames.end(), [](auto const& lhs, auto const& rhs) { + return lhs->getCreationTime() > rhs->getCreationTime(); }); - for (const QFileInfo &file : files) { - QListWidgetItem *item = new QListWidgetItem(savesDir.relativeFilePath(file.absoluteFilePath())); - item->setData(Qt::UserRole, file.absoluteFilePath()); - ui->savegameList->addItem(item); + ui->savegameList->clear(); + for (auto& save: m_SaveGames) { + ui->savegameList->addItem(savesDir.relativeFilePath(save->getFilepath())); } } - static bool BySortValue(const std::pair &LHS, const std::pair &RHS) { return LHS.first < RHS.first; @@ -4960,20 +4946,15 @@ void MainWindow::deleteSavegame_clicked() int count = 0; for (const QModelIndex &idx : ui->savegameList->selectionModel()->selectedIndexes()) { - QString name = idx.data(Qt::UserRole).toString(); + + auto& saveGame = m_SaveGames[idx.row()]; if (count < 10) { - savesMsgLabel += "
  • " + QFileInfo(name).completeBaseName() + "
  • "; + savesMsgLabel += "
  • " + QFileInfo(saveGame->getFilepath()).completeBaseName() + "
  • "; } ++count; - if (info == nullptr) { - deleteFiles.push_back(name); - } else { - ISaveGame const *save = info->getSaveGameInfo(name); - deleteFiles += save->allFiles(); - delete save; - } + deleteFiles += saveGame->allFiles(); } if (count > 10) { @@ -5032,8 +5013,8 @@ void MainWindow::on_savegameList_customContextMenuRequested(const QPoint& pos) QAction* action = menu.addAction(tr("Enable Mods...")); action->setEnabled(false); if (selection->selectedIndexes().count() == 1) { - QString save = ui->savegameList->currentItem()->data(Qt::UserRole).toString(); - SaveGameInfo::MissingAssets missing = info->getMissingAssets(save); + auto& save = m_SaveGames[selection->selectedIndexes()[0].row()]; + SaveGameInfo::MissingAssets missing = info->getMissingAssets(*save); if (missing.size() != 0) { connect(action, &QAction::triggered, this, [this, missing] { fixMods_clicked(missing); }); action->setEnabled(true); @@ -5229,7 +5210,7 @@ void MainWindow::installTranslator(const QString &name) { QTranslator *translator = new QTranslator(this); QString fileName = name + "_" + m_CurrentLanguage; - QString translationsPath = qApp->applicationDirPath() + QString translationsPath = qApp->applicationDirPath() + "/" + QString::fromStdWString(AppConfig::translationsPath()); if (!translator->load(fileName, translationsPath)) { if (m_CurrentLanguage.contains(QRegularExpression("^.*_(EN|en)(-.*)?$"))) { diff --git a/src/mainwindow.h b/src/mainwindow.h index e2805b25..da916d2a 100644 --- a/src/mainwindow.h +++ b/src/mainwindow.h @@ -334,6 +334,7 @@ private: QTime m_StartTime; //SaveGameInfoWidget *m_CurrentSaveView; + std::vector> m_SaveGames; MOBase::ISaveGameInfoWidget *m_CurrentSaveView; OrganizerCore &m_OrganizerCore; diff --git a/src/transfersavesdialog.cpp b/src/transfersavesdialog.cpp index f8ebbb9d..5288d408 100644 --- a/src/transfersavesdialog.cpp +++ b/src/transfersavesdialog.cpp @@ -42,74 +42,6 @@ along with Mod Organizer. If not, see . using namespace MOBase; using namespace MOShared; -//These two classes give the save-transfer box a smidgin of useful info even -//if save game isn't supported yet. -namespace { - -class DummySave : public ISaveGame -{ -public: - DummySave(QString const &filename) : - m_File(filename) - {} - - ~DummySave() {} - - virtual QString getFilename() const override - { - return m_File; - } - - virtual QDateTime getCreationTime() const override - { - return QFileInfo(m_File).birthTime(); - } - - virtual QString getSaveGroupIdentifier() const override - { - return m_File; - } - - virtual QStringList allFiles() const override - { - return { m_File }; - } - - virtual bool hasScriptExtenderFile() const override - { - return false; - } - -private: - QString m_File; -}; - -class DummyInfo : public SaveGameInfo -{ -public: - virtual MOBase::ISaveGame const *getSaveGameInfo(QString const &file) const override - { - return new DummySave(file); - } - - virtual MissingAssets getMissingAssets(QString const &) const override - { - return {}; - } - - MOBase::ISaveGameInfoWidget *getSaveGameWidget(QWidget *) const override - { - return nullptr; - } - - virtual bool hasScriptExtenderSave(QString const &file) const override - { - return false; - } -}; - -} //end anonymous namespace - TransferSavesDialog::TransferSavesDialog(const Profile &profile, IPluginGame const *gamePlugin, QWidget *parent) : TutorableDialog("TransferSaves", parent) , ui(new Ui::TransferSavesDialog) @@ -263,7 +195,7 @@ void TransferSavesDialog::on_globalCharacterList_currentTextChanged(const QStrin SaveCollection::const_iterator saveList = m_GlobalSaves.find(currentText); if (saveList != m_GlobalSaves.end()) { for (SaveListItem const &save : saveList->second) { - ui->globalSavesList->addItem(QFileInfo(save->getFilename()).fileName()); + ui->globalSavesList->addItem(QFileInfo(save->getFilepath()).fileName()); } } } @@ -276,7 +208,7 @@ void TransferSavesDialog::on_localCharacterList_currentTextChanged(const QString SaveCollection::const_iterator saveList = m_LocalSaves.find(currentText); if (saveList != m_LocalSaves.end()) { for (SaveListItem const &save : saveList->second) { - ui->localSavesList->addItem(QFileInfo(save->getFilename()).fileName()); + ui->localSavesList->addItem(QFileInfo(save->getFilepath()).fileName()); } } } @@ -285,34 +217,13 @@ void TransferSavesDialog::refreshSaves(SaveCollection &saveCollection, QString c { saveCollection.clear(); - SaveGameInfo const *info = m_GamePlugin->feature(); - if (info == nullptr) { - static DummyInfo dummyInfo; - info = &dummyInfo; - } - - QStringList filters; - filters << QString("*.") + m_GamePlugin->savegameExtension(); - - QDir savesDir(savedir); - savesDir.setNameFilters(QStringList() << QString("*.") + m_GamePlugin->savegameExtension()); - savesDir.setFilter(QDir::Files); - QDirIterator it(savesDir, QDirIterator::Subdirectories); - log::debug("reading save games from {}", savesDir.absolutePath()); - - QFileInfoList files; - while (it.hasNext()) { - it.next(); - files.append(it.fileInfo()); - } - std::sort(files.begin(), files.end(), [](auto const& lhs, auto const& rhs) { - return lhs.fileTime(QFileDevice::FileModificationTime) < rhs.fileTime(QFileDevice::FileModificationTime); + auto saves = m_GamePlugin->listSaves(savedir); + std::sort(saves.begin(), saves.end(), [](auto const& lhs, auto const& rhs) { + return lhs->getCreationTime() > rhs->getCreationTime(); }); - for (const QFileInfo &file: files) { - MOBase::ISaveGame const *save = info->getSaveGameInfo(file.absoluteFilePath()); - saveCollection[save->getSaveGroupIdentifier()].push_back( - std::unique_ptr(save)); + for (auto& save: saves) { + saveCollection[save->getSaveGroupIdentifier()].push_back(save); } } @@ -334,7 +245,7 @@ void TransferSavesDialog::refreshCharacters(const SaveCollection &saveCollection } bool TransferSavesDialog::transferCharacters( - QString const &character, char const *message, + QString const &character, char const *message, QDir const& sourceDirectory, SaveList &saves, QDir const& destination, diff --git a/src/transfersavesdialog.h b/src/transfersavesdialog.h index b983ad9e..fe78aa83 100644 --- a/src/transfersavesdialog.h +++ b/src/transfersavesdialog.h @@ -80,9 +80,10 @@ private: MOBase::IPluginGame const *m_GamePlugin; - typedef std::unique_ptr SaveListItem; - typedef std::vector SaveList; - typedef std::map SaveCollection; + using SaveListItem = std::shared_ptr; + using SaveList = std::vector; + using SaveCollection = std::map; + SaveCollection m_GlobalSaves; SaveCollection m_LocalSaves; -- cgit v1.3.1 From e76e7e034d2b42d5819b7b184cd7034ff51ebb26 Mon Sep 17 00:00:00 2001 From: Mikaël Capelle Date: Wed, 18 Nov 2020 19:22:13 +0100 Subject: Move MemoizedLock to uibase. --- src/modinfowithconflictinfo.h | 8 ++++---- src/thread_utils.h | 45 ------------------------------------------- 2 files changed, 4 insertions(+), 49 deletions(-) (limited to 'src') diff --git a/src/modinfowithconflictinfo.h b/src/modinfowithconflictinfo.h index feded99b..e312a89a 100644 --- a/src/modinfowithconflictinfo.h +++ b/src/modinfowithconflictinfo.h @@ -3,7 +3,7 @@ #include -#include "thread_utils.h" +#include "memoizedlock.h" #include "modinfo.h" #include @@ -145,9 +145,9 @@ protected: private: - MOShared::MemoizedLocked> m_FileTree; - MOShared::MemoizedLocked m_Valid; - MOShared::MemoizedLocked> m_Contents; + MOBase::MemoizedLocked> m_FileTree; + MOBase::MemoizedLocked m_Valid; + MOBase::MemoizedLocked> m_Contents; MOShared::DirectoryEntry **m_DirectoryStructure; diff --git a/src/thread_utils.h b/src/thread_utils.h index f64dd601..e816a4e2 100644 --- a/src/thread_utils.h +++ b/src/thread_utils.h @@ -24,51 +24,6 @@ std::thread startSafeThread(F&& f) }); } - -/** - * Class that can be used to perform thread-safe memoization. - * - * Each instance hold a flag indicating if the current value is up-to-date - * or not. This flag can be reset using `invalidate()`. When the value is queried, - * the flag is checked, and if it is not up-to-date, the given callback is used - * to compute the value. - * - * The computation and update of the value is locked to avoid concurrent modifications. - * - * @tparam T Type of value ot memoized. - * @tparam Fn Type of the callback. - */ -template > -struct MemoizedLocked { - - template - MemoizedLocked(Callable &&callable, T value = {}) : - m_Fn{ std::forward(callable) }, m_Value{ std::move(value) } { } - - template - T& value(Args&&... args) const { - if (m_NeedUpdating) { - std::scoped_lock lock(m_Mutex); - if (m_NeedUpdating) { - m_Value = std::invoke(m_Fn, std::forward(args)... ); - m_NeedUpdating = false; - } - } - return m_Value; - } - - void invalidate() { - m_NeedUpdating = true; - } - -private: - mutable std::mutex m_Mutex; - mutable std::atomic m_NeedUpdating{ true }; - - Fn m_Fn; - mutable T m_Value; -}; - /** * @brief Apply the given callable to each element between the two given iterators * in a parallel way. -- cgit v1.3.1 From 951ccdc8234cc0b71d0eb3c939590159779c6abe Mon Sep 17 00:00:00 2001 From: Mikaël Capelle Date: Wed, 18 Nov 2020 19:26:59 +0100 Subject: Update following removal of IPlugin::isActive(). --- src/organizer_en.ts | 3591 ++++++++++++++++++++++++++------------------------- 1 file changed, 1845 insertions(+), 1746 deletions(-) (limited to 'src') diff --git a/src/organizer_en.ts b/src/organizer_en.ts index c65c946c..646b3310 100644 --- a/src/organizer_en.ts +++ b/src/organizer_en.ts @@ -110,22 +110,22 @@ - + ...and all other contributors! - + Other Supporters && Contributors - + Tannin (Original Creator) - + Close @@ -178,17 +178,17 @@ p, li { white-space: pre-wrap; } AdvancedConflictListModel - + Overwrites - + File - + Overwritten By @@ -339,6 +339,295 @@ p, li { white-space: pre-wrap; } + + CreateInstanceDialog + + + Creating an instance + + + + + Creating a new instance + + + + + <h3>What is an instance?</h3> +<p>An instance is a full set of mods, downloads, profiles and configuration for a game. Each game must be managed in its own instance. Mod Organizer can freely switch between instances.</p> + + + + + <html><head/><body><p><a href="https://github.com/ModOrganizer2/modorganizer/wiki/Instances">More information</a></p></body></html> + + + + + Never show this page again + + + + + <h3>Select the type of instance to create.</h3> + + + + + Create a global instance + + + + + Global instances are stored in %1, but some paths can be changed to be on a different drive if necessary. A single installation of Mod Organizer can manage multiple global instances. + + + + + Create a portable instance + + + + + A portable instance stores everything in Mod Organizer's installation folder, currently %1. There can only be one portable instance per installation of Mod Organizer. + + + + + A portable instance already exists. + + + + + <h3>Select the game to manage.</h3> + + + + + Show all supported games + + + + + Filter + + + + + <h3>Select the game edition.</h3> + + + + + This game has multiple variants. The correct one must be selected or Mod Organizer will not be able to launch the game properly. + + + + + <h3>Customize the name for this <span style="white-space: nowrap;">%1</span> instance.</h3> + + + + + Instance name + + + + + There is already an instance with this name. + + + + + The name contains invalid characters. It must be a valid folder name. + + + + + <h3>Select a folder where the data should be stored.</h3> + + + + + This includes downloads, mods, profiles and overwrite for your <b>%1</b> instance. If there is enough space on this drive, you should use the default folder. + + + + + + + + + + ... + + + + + Location + + + + + Warning: This folder already exists. + + + + + + + + + + Folder + + + + + Warning: The folder contains invalid characters. + + + + + Mods + + + + + Overwrite + + + + + Base directory + + + + + Warning: The folder %1 already exists. + + + + + Downloads + + + + + Profiles + + + + + Use %BASE_DIR% to refer to the Base Directory. + + + + + Warning: The folder %1 contains invalid characters. + + + + + Show advanced options + + + + + <h3>Link Mod Organizer with your Nexus account</h3> + + + + + Linking with Nexus allows you to download mods directly from Mod Organizer and automatically check for updates. This is optional. + + + + + Connect to Nexus + + + + + Enter API Key Manually + + + + + <h3>Confirmation</h3> + + + + + The instance is about to be created. Review the information below and press 'Finish'. + + + + + Instance creation log + + + + + Launch the new instance + + + + + < Back + + + + + Next > + + + + + Cancel + + + + + Setting up instance %1 + + + + + Setting up an instance %1 + + + + + Creating instance... + + + + + Writing %1... + + + + + Format error. + + + + + Error %1. + + + + + Done. + + + + + Finish + + + CredentialsDialog @@ -380,6 +669,44 @@ p, li { white-space: pre-wrap; } + + DisableProxyPluginDialog + + + Really disable plugin? + + + + + Disabling the '%1' plugin will prevent the following plugins from working: + + + + + Plugin + + + + + Description + + + + + Do you want to continue? You will need to restart Mod Organizer for the change to take effect. + + + + + Yes + + + + + No + + + DownloadList @@ -524,132 +851,137 @@ p, li { white-space: pre-wrap; } - - + Open Meta File + + + + + + Reveal in Explorer - - + + Delete - + Un-Hide - + Hide - + Cancel - + Pause - + Resume - + Delete Installed Downloads... - + Delete Uninstalled Downloads... - + Delete All Downloads... - + Hide Installed... - + Hide Uninstalled... - + Hide All... - + Un-Hide All... - - - - + + + + Delete Files? - + This will permanently delete the selected download. Are you absolutely sure you want to proceed? - + This will remove all finished downloads from this list and from disk. Are you absolutely sure you want to proceed? - + This will remove all installed downloads from this list and from disk. Are you absolutely sure you want to proceed? - + This will remove all uninstalled downloads from this list and from disk. Are you absolutely sure you want to proceed? - - - + + + Hide Files? - + This will remove all finished downloads from this list (but NOT from disk). - + This will remove all installed downloads from this list (but NOT from disk). - + This will remove all uninstalled downloads from this list (but NOT from disk). @@ -719,7 +1051,7 @@ File %3: %4 - + remove: invalid download index %1 @@ -734,256 +1066,256 @@ File %3: %4 - + restore: invalid download index: %1 - + cancel: invalid download index %1 - + pause: invalid download index %1 - + resume: invalid download index %1 - + resume (int): invalid download index %1 - + No known download urls. Sorry, this download can't be resumed. - - + + query: invalid download index %1 - + Please enter the nexus mod id - + Mod ID: - + Please select the source game code for %1 - + Hashing download file '%1' - + Cancel - + VisitNexus: invalid download index %1 - + Nexus ID for this Mod is unknown - + OpenFile: invalid download index %1 - + OpenFileInDownloadsFolder: invalid download index %1 - + get pending: invalid download index %1 - + get path: invalid download index %1 - + Main - + Update - + Optional - + Old - + Miscellaneous - + Deleted - + Unknown - + display name: invalid download index %1 - + file name: invalid download index %1 - + file time: invalid download index %1 - + file size: invalid download index %1 - + progress: invalid download index %1 - + state: invalid download index %1 - + infocomplete: invalid download index %1 - - - + + + mod id: invalid download index %1 - + ishidden: invalid download index %1 - + file info: invalid download index %1 - + mark installed: invalid download index %1 - + mark uninstalled: invalid download index %1 - + Memory allocation error (in processing progress event). - + Memory allocation error (in processing downloaded data). - + Information updated - - + + No matching file found on Nexus! Maybe this file is no longer available or it was renamed? - + No file on Nexus matches the selected file by name. Please manually choose the correct one. - + No download server available. Please try again later. - + Failed to request file info from nexus: %1 - + Warning: Content type is: %1 - + Download header content length: %1 downloaded file size: %2 - + Download failed: %1 (%2) - + We were unable to download the file due to errors after four retries. There may be an issue with the Nexus servers. - + failed to re-open %1 - + Unable to write download to drive (return %1). Check the drive's available storage. @@ -1462,54 +1794,54 @@ Right now the only case I know of where this needs to be overwritten is for the FileTreeModel - + Name - + Mod - + Type - + Size - + Date modified - + Directory - - + + Virtual path - + Real path - + From - - + + Also in @@ -1748,122 +2080,145 @@ Right now the only case I know of where this needs to be overwritten is for the + + GeneralConflictsTab + + + <table cellspacing="5"><tr><th>Type</th><th>%1</th><th>Total</th><th>Percent</th></tr><tr><td>Loose files:&emsp;</td><td align=right>%2</td><td align=right>%3</td><td align=right>%4%</td></tr><tr><td>Archive files:&emsp;</td><td align=right>%5</td><td align=right>%6</td><td align=right>%7%</td></tr><tr><td>Combined:&emsp;</td><td align=right>%8</td><td align=right>%9</td><td align=right>%10%</td></tr></table> + + + + + Winning + + + + + Losing + + + + + Non conflicting + + + InstallationManager - + Password required - + Password - + Extracting files - - + + Extraction failed: %1 - + Failed to create backup - + Mod Name - + Name - + Invalid name - + The name you entered is invalid, please enter a different one. - + File format "%1" not supported - + Invalid file tree returned by plugin. - + Installation failed - + Something went wrong while installing this mod. - + None of the available installer plugins were able to handle that archive. This is likely due to a corrupted or incompatible download or unrecognized archive format. - + no error - + 7z.dll not found - + 7z.dll isn't valid - + archive not found - + failed to open archive - + unsupported archive type - + internal library error - + archive invalid - + unknown archive error @@ -1881,71 +2236,176 @@ This is likely due to a corrupted or incompatible download or unrecognized archi - - Create portable instance + + Rename - - <html><head/><body><p><a href="https://github.com/ModOrganizer2/modorganizer/wiki/Instance"><span style=" text-decoration: underline; color:#0000ff;">What is an instance?</span></a></p></body></html> + + <html><head/><body><p><a href="https://github.com/ModOrganizer2/modorganizer/wiki/Instances">What is an instance?</a></p></body></html> - - Game: + + Filter - - Name: + + Name - - Location: + + + + Explore - - Rename + + Game - - Base folder: + + Location - - - Explore + + Game location + + + + + Base folder - + Convert to portable - + Convert to global - - Delete instance + + Open INI - + + Delete instance... + + + + + Close + + + + Switch to this instance - + + Switching instances + + + + + Mod Organizer must restart to manage the instance '%1'. + + + + + This confirmation can be disabled in the settings. + + + + + Restart Mod Organizer + + + + + Cancel + + + + Rename instance + + + + + The active instance cannot be renamed. + + + + + Instance name + + + + + Error + + + + + Failed to rename "%1" to "%2": %3 + + + + + + + Deleting instance + + + + + The active instance cannot be deleted. + + + + + These files and folders will be deleted + + + + + All checked items will be deleted. + + + + + Move to the recycle bin + + + + + Delete permanently + + + + + Nothing to delete. + + + + + A portable instance already exists. + + ListDialog @@ -2078,12 +2538,12 @@ This is likely due to a corrupted or incompatible download or unrecognized archi MOApplication - + an error occurred: %1 - + an error occurred @@ -2091,49 +2551,49 @@ This is likely due to a corrupted or incompatible download or unrecognized archi MOBase::FilterWidget - + Filter options - + Use regular expressions - + Use regular expressions in filters - + Case sensitive - + Make regular expressions case sensitive (/i) leave "(/i)" verbatim - + Extended - + Ignores unescaped whitespace in regular expressions (/x) leave "(/x)" verbatim - + Keep selection in view - + Scroll to keep the current selection in view after filtering @@ -2151,17 +2611,17 @@ This is likely due to a corrupted or incompatible download or unrecognized archi - + failed to write to %1 - + file not found: %1 - + Save @@ -2182,6 +2642,29 @@ This is likely due to a corrupted or incompatible download or unrecognized archi + + MOMultiProcess + + + SHM error: %1 + + + + + failed to connect to running process: %1 + + + + + failed to communicate with running process: %1 + + + + + failed to receive data from secondary process: %1 + + + MainWindow @@ -2278,7 +2761,7 @@ p, li { white-space: pre-wrap; } - + Create Backup @@ -2432,7 +2915,7 @@ p, li { white-space: pre-wrap; } - + Refresh @@ -2554,1113 +3037,1100 @@ p, li { white-space: pre-wrap; } - + &File - - + + &Tools - - - + + + &Help - + &View - + &Toolbars - + &Run - - + + Log - + Install &Mod... - + Install &Mod - - + + Install a new mod from an archive - + Ctrl+M - + &Profiles... - + &Profiles - - + + Configure profiles - + Ctrl+P - + &Executables... - + &Executables - - + + Configure the executables that can be started through Mod Organizer - + Ctrl+E - + &Tool Plugins - + Tools - + Ctrl+I - + &Settings... - + &Settings - - + + Configure settings and workarounds - + Ctrl+S - - + + Visit &Nexus - - + + Visit the Nexus website in your browser for more mods - + Ctrl+N - - + + &Update Mod Organizer - - + + Mod Organizer is up-to-date - + &Notifications... - - + + Open the notifications dialog - + This button will be highlighted on the toolbar if MO discovered potential problems in your setup and provide tips on how to fix them. - - + + Show help options - + Ctrl+H - - + + &Endorse ModOrganizer - - - + + + Endorse Mod Organizer - - &Change Game... + + + Manage Instan&ces... - - &Change Game - - - - - - Open the Instance selection dialog to manage a different Game + + + Open the instance manager window to manage a different instance - - + + E&xit - - + + Exits Mod Organizer - + M&ain Toolbar - + &Small Icons - + Lar&ge Icons - + &Icons Only - + &Text Only - + I&cons and Text - + M&edium Icons - + &Menu - + Status &bar - + Toolbar and Menu - + Desktop - + Start Menu - + There is no supported sort mechanism for this game. You will probably have to use a third-party tool. - + Crash on exit - + MO crashed while exiting. Some settings may not be saved. Error: %1 - + There are notifications to read - + There are no notifications - - - + + + Endorse - + Won't Endorse - + Help on UI - + Documentation - + Chat on Discord - + Report Issue - + Tutorials - + About - + About Qt - + Name - + Please enter a name for the new profile - + failed to create profile: %1 - + Show tutorial? - + You are starting Mod Organizer for the first time. Do you want to show a tutorial of its basic features? If you choose no you can always start the tutorial from the "Help"-menu. - + + Never ask to show tutorials + + + + Downloads in progress - + There are still downloads in progress, do you really want to quit? - + Plugin "%1" failed: %2 - + Plugin "%1" failed - - Browse Mod Page - - - - - <Edit...> - - - - - (no executables) - - - - + This bsa is enabled in the ini file so it may be required! - + Activating Network Proxy - + Notice: Your current MO version (%1) is lower than the previously used one (%2). The GUI may not downgrade gracefully, so you may experience oddities. However, there should be no serious issues. - + Choose Mod - + Mod Archive - + Start Tutorial? - + You're about to start a tutorial. For technical reasons it's not possible to end the tutorial early. Continue? - + failed to change origin name: %1 - + failed to move "%1" from mod "%2" to "%3": %4 - + failed to rename mod: %1 - + Overwrite? - + This will replace the existing mod "%1". Continue? - + failed to remove mod "%1" - + failed to rename "%1" to "%2" - - - - - + + + + + Confirm - + Remove the following mods?<br><ul>%1</ul> - + failed to remove mod: %1 - - - + + + Failed - + Installation file no longer exists - + Mods installed with old versions of MO can't be reinstalled in this way. - + Failed to create backup. - + Endorsing multiple mods will take a while. Please wait... - + Unendorsing multiple mods will take a while. Please wait... - + Failed to display overwrite dialog: %1 - + Restore all hidden files in the following mods?<br><ul>%1</ul> - - + + Are you sure? - + About to restore all hidden files in: - + Opening Nexus Links - + You are trying to open %1 links to Nexus Mods. Are you sure you want to do this? - + Nexus ID for this mod is unknown - - + + Opening Web Pages - - + + You are trying to open %1 Web Pages. Are you sure you want to do this? - - No valid Web Page for this mod - - - - + <table cellspacing="5"><tr><th>Type</th><th>All</th><th>Visible</th><tr><td>Enabled mods:&emsp;</td><td align=right>%1 / %2</td><td align=right>%3 / %4</td></tr><tr><td>Unmanaged/DLCs:&emsp;</td><td align=right>%5</td><td align=right>%6</td></tr><tr><td>Mod backups:&emsp;</td><td align=right>%7</td><td align=right>%8</td></tr><tr><td>Separators:&emsp;</td><td align=right>%9</td><td align=right>%10</td></tr></table> - + <table cellspacing="6"><tr><th>Type</th><th>Active </th><th>Total</th></tr><tr><td>All plugins:</td><td align=right>%1 </td><td align=right>%2</td></tr><tr><td>ESMs:</td><td align=right>%3 </td><td align=right>%4</td></tr><tr><td>ESPs:</td><td align=right>%7 </td><td align=right>%8</td></tr><tr><td>ESMs+ESPs:</td><td align=right>%9 </td><td align=right>%10</td></tr><tr><td>ESLs:</td><td align=right>%5 </td><td align=right>%6</td></tr></table> - - - + + + Create Mod... - + This will create an empty mod. Please enter a name: - - + + A mod with this name already exists - + Create Separator... - + This will create a new separator. Please enter a name: - + A separator with this name already exists - + This will move all files from overwrite into a new, regular mod. Please enter a name: - + Move successful. - + About to recursively delete: - + + + You are not currently authenticated with Nexus. Please do so under Settings -> Nexus. + + + + Continue? - + The versioning scheme decides which version is considered newer than another. This function will guess the versioning scheme under the assumption that the installed version is outdated. - + Sorry - + I don't know a versioning scheme where %1 is newer than %2. - + Really enable all visible mods? - + Really disable all visible mods? - + Export to csv - + CSV (Comma Separated Values) is a format that can be imported in programs like Excel to create a spreadsheet. You can also use online editors and converters instead. - + Select what mods you want export: - + All installed mods - + Only active (checked) mods from your current profile - + All currently visible mods in the mod list - + Choose what Columns to export: - + Mod_Priority - + Mod_Name - + Notes_column - + Mod_Status - + Primary_Category - + Nexus_ID - + Mod_Nexus_URL - + Mod_Version - + Install_Date - + Download_File_Name - + export failed: %1 - + Open Game folder - + Open MyGames folder - + Open INIs folder - + Open Instance folder - + Open Mods folder - + Open Profile folder - + Open Downloads folder - + Open MO2 Install folder - + Open MO2 Plugins folder - + Open MO2 Stylesheets folder - + Open MO2 Logs folder - + Install Mod... - + Create empty mod - + Create Separator - + Enable all visible - + Disable all visible - + Check for updates - + Export to csv... - - + + Send to - - + + Top - - + + Bottom - - + + Priority... - + Separator... - + All Mods - + Sync to Mods... - + Move content to Mod... - + Clear Overwrite... - - - + + + Open in Explorer - + Restore Backup - + Remove Backup... - - + + Ignore missing data - - + + Mark as converted/working - - + + Visit on Nexus - - + + Visit on %1 - - + + Change Categories - - + + Primary Category - + Rename Separator... - + Remove Separator... - - + + Select Color... - - + + Reset Color - + Change versioning scheme - + Force-check updates - + Un-ignore update - + Ignore update - - + + Enable selected - - + + Disable selected - + Rename Mod... - + Reinstall Mod - + Remove Mod... - + Restore hidden files - + Un-Endorse - + Won't endorse - + Endorsement state unknown - + Start tracking - + Stop tracking - + Tracked state unknown - + Information... - - + + Exception: - - + + Unknown exception - + %1 more - + Are you sure you want to remove the following %n save(s)?<br><ul>%1</ul><br>Removed saves will be sent to the Recycle Bin. @@ -3668,12 +4138,12 @@ You can also use online editors and converters instead. - + Enable Mods... - + Delete %n save(s) @@ -3681,230 +4151,268 @@ You can also use online editors and converters instead. - + Restart Mod Organizer - + Mod Organizer must restart to finish configuration changes - + Restart - + Continue - + Some things might be weird. - + Can't change download directory while downloads are in progress! - - + + Set Priority - + Set the priority of the selected plugins - + Update available - + Do you want to endorse Mod Organizer on %1 now? - + Abstain from Endorsing Mod Organizer - + Are you sure you want to abstain from endorsing Mod Organizer 2? You will have to visit the mod page on the %1 Nexus site to change your mind. - + Thank you for endorsing MO2! :) - + Please reconsider endorsing MO2 on Nexus! - + + None of your %1 mods appear to have had recent file updates. + + + + + All of your mods have been checked recently. We restrict update checks to help preserve your available API requests. + + + + Thank you! - + Thank you for your endorsement! - + Mod ID %1 no longer seems to be available on Nexus. - + Request to Nexus failed: %1 - - + + failed to read %1: %2 - + Error - + failed to extract %1 (errorcode %2) - + Extract BSA - + This archive contains invalid hashes. Some files may be broken. - + Extract... - + <Multiple> - + Remove '%1' from the toolbar - + Enable all - + Disable all - + Unlock load order - + Lock load order - + Open Origin in Explorer - + Open Origin Info... - + + Sorting plugins + + + + + Are you sure you want to sort your plugins list? + + + + Backup of load order created - + Choose backup to restore - + No Backups - + There are no backups to restore - - + + Restore failed - - + + Failed to restore the backup. Errorcode: %1 - + Backup of mod list created - + A file with the same name has already been downloaded. What would you like to do? - + Overwrite - + Rename new file - + Ignore file - + Set the priority of the selected mods + + + + + + Browse Mod Page + + + + + <Edit...> + + + + + (no executables) + + MessageDialog @@ -3928,12 +4436,17 @@ You will have to visit the mod page on the %1 Nexus site to change your mind. - + + The update check has found a mod with a Nexus ID and source game of %1, but this game is not a valid Nexus source. + + + + All of your mods have been checked recently. We restrict update checks to help preserve your available API requests. - + You have mods that haven't been checked within the last month using the new API. These mods must be checked before we can use the bulk update API. This will consume significantly more API requests than usual. You will need to rerun the update check once complete in order to parse the remaining mods. @@ -4079,87 +4592,105 @@ Most mods do not have optional esps, so chances are good you are looking at an e - - The following conflicted files are provided by this mod + + + Files that exist in other mods but are overwritten by this mod + + + + + Winning file conflicts: - - The following conflicted files are provided by other mods + + + Files that are unused because they are overwritten by other mods - + + Losing file conflicts: + + + + + + Files that have no conflicts + + + + The following files have no conflicts - + Advanced - + Whether files that have no conflicts should be visible in the list - + Show files that have no conflicts - + Shows all mods overwriting or being overwritten by this mod - + Show all conflicting mods - + Shows only the nearest conflicting mods, in order of priority - + Show nearest conflicting mod - + Filter - + Categories - + Primary Category - + Nexus Info - + Mod ID - + Mod ID for this mod on Nexus. - + <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> <html><head><meta name="qrichtext" content="1" /><style type="text/css"> p, li { white-space: pre-wrap; } @@ -4168,22 +4699,22 @@ p, li { white-space: pre-wrap; } - + Source Game - + Source game for this mod. - + <html><head/><body><p>Source game for this mod. This determines where the mod was downloaded from and decides where to fetch info, version updates, and send endorsements. Changing this will likely require you to enter a new Mod ID.</p></body></html> - + <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> <html><head><meta name="qrichtext" content="1" /><style type="text/css"> p, li { white-space: pre-wrap; } @@ -4192,93 +4723,93 @@ p, li { white-space: pre-wrap; } - + Version - - + + Refresh - + Refresh all information from Nexus. - - + + Open in Browser - + Endorse - + Track - + about:blank - + Use Custom URL - + Notes - - - + + + Enter comments about the mod here. These are displayed in the notes column of the mod list. - + Set Color - + Reset Color - - - + + + Enter notes about the mod here. These can be viewed in the mod list by hovering over the notes column or the flags column. - + Filetree - + Open Mod in Explorer - + A directory view of this mod - + <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> <html><head><meta name="qrichtext" content="1" /><style type="text/css"> p, li { white-space: pre-wrap; } @@ -4288,17 +4819,17 @@ p, li { white-space: pre-wrap; } - + Previous - + Next - + Close @@ -4337,12 +4868,12 @@ p, li { white-space: pre-wrap; } ModInfoRegular - + %1 contains no esp/esm/esl and no asset (textures, meshes, interface, ...) directory - + Categories: <br> @@ -4358,273 +4889,273 @@ p, li { white-space: pre-wrap; } ModList - + This entry contains files that have been created inside the virtual data tree (i.e. by the construction kit) - + Backup - + Separator - + No valid game data - + Not endorsed yet - + This mod is for a different<br> game, make sure it's compatible or it could cause crashes. - + Mod is being tracked on the website - + Contains hidden files - + Overwrites loose files - + Overwritten loose files - + Loose files Overwrites & Overwritten - + Redundant - + Overwrites an archive with loose files - + Archive is overwritten by loose files - + Overwrites another archive file - + Overwritten by another archive file - + Archive files overwrites & overwritten - + Non-MO - + invalid - + installed version: "%1", newest version: "%2" - + The newest version on Nexus seems to be older than the one you have installed. This could either mean the version you have has been withdrawn (i.e. due to a bug) or the author uses a non-standard versioning scheme and that newest version is actually newer. Either way you may want to "upgrade". - + This file has been marked as "Old". There is most likely an updated version of this file available. - + This file has been marked as "Deleted"! You may want to check for an update or remove the nexus ID from this mod! - + %1 minute(s) and %2 second(s) - + This mod will be available to check in %2. - + Categories: <br> - + Invalid name - + Name is already in use by another mod - + drag&drop failed: %1 - + Confirm - + Are you sure you want to remove "%1"? - + Conflicts - + Flags - + Content - + Mod Name - + Version - + Priority - + Category - + Source Game - + Nexus ID - + Installation - + Notes - - + + unknown - + Name of your mods - + Version of the mod (if available) - + Installation priority of your mod. The higher, the more "important" it is and thus overwrites files from mods with lower priority. - + Primary category of the mod. - + The source game which was the origin of this mod. - + Id of the mod as used on Nexus. - + Indicators of file conflicts between mods. - + Emblems to highlight things that might require attention. - + Depicts the content of the mod: - + Time this mod was installed - + User notes about the mod @@ -4659,39 +5190,82 @@ p, li { white-space: pre-wrap; } - NexusInterface + NexusConnectionUI - - Please pick the mod ID for "%1" + + Connected. - - You must authorize MO2 in Settings -> Nexus to use the Nexus API. + + Not connected. - - You've exceeded the Nexus API rate limit and requests are now being throttled. Your next batch of requests will be available in approximately %1 minutes and %2 seconds. + + Disconnected. - - Aborting download: Either you clicked on a premium-only link and your account is not premium, or the download link was generated by a different account than the one stored in Mod Organizer. + + Checking API key... - - empty response + + Received API key. - - invalid response + + Received user account information - - + + + Linked with Nexus successfully. + + + + + Failed to set API key + + + + + NexusInterface + + + Please pick the mod ID for "%1" + + + + + You must authorize MO2 in Settings -> Nexus to use the Nexus API. + + + + + You've exceeded the Nexus API rate limit and requests are now being throttled. Your next batch of requests will be available in approximately %1 minutes and %2 seconds. + + + + + Aborting download: Either you clicked on a premium-only link and your account is not premium, or the download link was generated by a different account than the one stored in Mod Organizer. + + + + + empty response + + + + + invalid response + + + + NexusManualKeyDialog @@ -4737,27 +5311,27 @@ p, li { white-space: pre-wrap; } NexusTab - + Current Version: %1 - + No update available - + Tracked - + Untracked - + <div style="text-align: center;"> <p>This mod does not have a valid Nexus ID. You can add a custom web @@ -4769,7 +5343,7 @@ p, li { white-space: pre-wrap; } NoConflictListModel - + File @@ -4777,211 +5351,211 @@ p, li { white-space: pre-wrap; } OrganizerCore - + File is write protected - + Invalid file format (probably a bug) - + Unknown error %1 - + Failed to write settings - + An error occurred trying to write back MO settings to %1: %2 - - + + Download started - + Download failed - - + + Installation cancelled - - + + Another installation is currently in progress. - - + + Installation successful - - + + Configure Mod - - + + This mod contains ini tweaks. Do you want to configure them now? - - + + mod not found: %1 - - + + Extraction cancelled - - + + The installation was cancelled while extracting files. If this was prior to a FOMOD setup, this warning may be ignored. However, if this was during installation, the mod will likely be missing files. - + file not found: %1 - + failed to generate preview for %1 - + Sorry - + Sorry, can't preview anything. This function currently does not support extracting from bsas. - + File '%1' not found. - + Failed to generate preview for %1 - + Failed to refresh list of esps: %1 - + Multiple esps/esls activated, please check that they don't conflict. - + You need to be logged in with Nexus - + Download? - + A download has been started but no installed page plugin recognizes it. If you download anyway no information (i.e. version) will be associated with the download. Continue? - - + + failed to update mod list: %1 - - + + login successful - + Login failed - + Login failed, try again? - + login failed: %1. Download will not be associated with an account - + login failed: %1 - + login failed: %1. You need to log-in with Nexus to update MO. - + MO1 "Script Extender" load mechanism has left hook.dll in your game folder - - + + Description missing - + <a href="%1">hook.dll</a> has been found in your game folder (right click to copy the full path). This is most likely a leftover of setting the ModOrganizer 1 load mechanism to "Script Extender", in which case you must remove this file either by changing the load mechanism in ModOrganizer 1 or manually removing the file, otherwise the game is likely to crash and burn. - + failed to save load order: %1 - + Error - + The designated write target "%1" is not enabled. @@ -4989,12 +5563,12 @@ Continue? OverwriteConflictListModel - + File - + Overwritten Mods @@ -5094,12 +5668,12 @@ Continue? OverwrittenConflictListModel - + File - + Providing Mod @@ -5107,18 +5681,48 @@ Continue? PluginContainer - + + Plugin error + + + + + Mod Organizer failed to load the plugin '%1' last time it was started. + + + + + The plugin can be skipped for this session, blacklisted, or loaded normally, in which case it might fail again. Blacklisted plugins can be re-enabled later in the settings. + + + + + Skip this plugin + + + + + Blacklist this plugin + + + + + Load this plugin + + + + Some plugins could not be loaded - - + + Description missing - + The following plugins could not be loaded. The reason may be missing dependencies (i.e. python) or an outdated version: @@ -5177,108 +5781,108 @@ Continue? - + Plugin not found: %1 - - + + Confirm - + Really enable all plugins? - + Really disable all plugins? - + The file containing locked plugin indices is broken - + Origin - + This plugin can't be disabled (enforced by the game). - + Author - + Description - + Missing Masters - + Enabled Masters - + Loads Archives - + There are Archives connected to this plugin. Their assets will be added to your game, overwriting in case of conflicts following the plugin order. Loose files will always overwrite assets from Archives. (This flag only checks for Archives from the same mod as the plugin) - + Loads INI settings - + There is an ini file connected to this plugin. Its settings will be added to your game settings, overwriting in case of conflicts. - + This ESP is flagged as an ESL. It will adhere to the ESP load order but the records will be loaded in ESL space. - + Incompatible with %1 - + Depends on missing %1 - + Warning - + Error - + failed to restore load order for %1 @@ -5291,6 +5895,49 @@ Continue? + + PluginTypeName + + + Diagnose + + + + + Game + + + + + Installer + + + + + Mod Page + + + + + Preview + + + + + Tool + + + + + Proxy + + + + + File Mapper + + + PreviewDialog @@ -5326,18 +5973,18 @@ p, li { white-space: pre-wrap; } - - + + Fix - + No guided fix - + (There are no notifications) @@ -5404,12 +6051,12 @@ p, li { white-space: pre-wrap; } - + Missing profile-specific game INI files! - + Some of your profile-specific game INI files were missing. They will now be copied from the vanilla game folder. You might want to double-check your settings. Missing files: @@ -5417,12 +6064,12 @@ Missing files: - + Delete profile-specific game INI files? - + Do you want to delete the profile-specific game INI files? (If you select "No", the INI files will be used again if you re-enable profile-specific game INI files.) @@ -5587,88 +6234,93 @@ p, li { white-space: pre-wrap; } - + Archive invalidation isn't required for this game. - - + + This game does not support profile-specific game saves. + + + + + failed to create profile: %1 - + Name - + Please enter a name for the new profile - + failed to copy profile: %1 - + Invalid name - + Invalid profile name - + Deleting active profile - + Unable to delete active profile. Please change to a different profile first. - + Confirm - + Are you sure you want to remove this profile (including profile-specific save games, if any)? - + Profile broken - + This profile you're about to delete seems to be broken or the path is invalid. I'm about to delete the following folder: "%1". Proceed? - + Rename Profile - + New Name - + failed to change archive invalidation state: %1 - + failed to determine if invalidation is active: %1 @@ -5676,79 +6328,78 @@ p, li { white-space: pre-wrap; } QObject - + Filter - - + + INI file is read-only - + Mod Organizer is attempting to write to "%1" which is currently set to read-only. - + Clear the read-only flag - + Allow the write once - + The file will be set to read-only again. - + Skip this file - - - - + - + + + Error - + You can reset these choices by clicking "Reset Dialog Choices" in the General tab of the Settings - + Always ask - - + + Remember my choice - + Remember my choice for %1 @@ -5758,84 +6409,84 @@ p, li { white-space: pre-wrap; } - + removal of "%1" failed: %2 - + removal of "%1" failed - + "%1" doesn't exist (remove) - + Error %1 - - + + failed to create directory "%1" - - + + failed to copy "%1" to "%2" - + %1 B - + %1 MB - + %1 GB - + %1 TB - + %1 GB/s - + %1 TB/s - + %1 KB - + %1 B/s - + %1 KB/s - + %1 MB/s @@ -5850,8 +6501,8 @@ p, li { white-space: pre-wrap; } - + None @@ -6041,137 +6692,36 @@ Destination: - - Deleting instance folder - - - - - This will delete the instance folder. - The folder will be deleted: "%1" - - - - - Move the folder to the recycle bin - - - - - Delete the folder permanently - - - - - Could not delete instance folder "%1". - -%2 - - - - - Select an instance to delete - - - - - Deleting an instance will delete all the mods, downloads, profiles (including profile-specific saves) and anything in the overwrite folder.<br><br>Custom paths outside of the instance folder will not be deleted. - - - - - Enter a Name for the new Instance - - - - - Enter a new name or select one from the suggested list: -(This is just a name for the Instance and can be whatever you wish, - the actual game selection will happen on the next screen regardless of chosen name) - - - - - - Canceled - - - - - Invalid instance name - - - - - The instance name "%1" is invalid. Use the name "%2" instead? - - - - - The instance "%1" already exists. - - - - - Please choose a different instance name, like: "%1 1" . - - - - - Choose Instance - - - - - Each Instance is a full set of MO data files (mods, downloads, profiles, configuration, ...). You can use multiple instances for different games. Instances are stored in Appdata and can be accessed by all MO installations. If your MO folder is writable, you can also store a single instance locally (called a Portable install, and all the MO data files will be inside the installation folder). - - - - - New - - - - - Create a new instance. + + + Portable - - Portable + + Cannot open instance '%1', failed to read INI file %2. - - Use MO folder for data. + + Cannot open instance '%1', the managed game was not found in the INI file %2. Select the game managed by this instance. - - Manage Instances + + Cannot open instance '%1', the game plugin '%2' doesn't exist. It may have been deleted by an antivirus. Select another instance. - - Delete an Instance. + + Cannot open instance '%1', the game directory '%2' doesn't exist or the game plugin '%3' doesn't recognize it. Select the game managed by this instance. - failed to create %1 - - - Data directory created - - - - - New data directory created at %1. If you don't want to store a lot of data there, reconfigure the storage directories via settings. - - General messages @@ -6208,9 +6758,9 @@ Destination: - + Warning @@ -6260,112 +6810,51 @@ Destination: + - Failed to create "%1". Your user account probably lacks permission. - - Plugin to handle %1 no longer installed. An antivirus might have deleted files. - Plugin to handle %1 no longer installed - - - - - - - The configured path to the game directory (%1) appears to be a symbolic (or other) link. This setup is incompatible with MO2's VFS and will not run correctly. - - - - - Could not use configuration settings for game "%1", path "%2". - - - - - - - Please select the installation of %1 to manage - - - - - - - Please select the game to manage - - - - - Canceled finding %1 in "%2". - - - - - Canceled finding game in "%1". - - - - - %1 not identified in "%2". The directory is required to contain the game binary. - - - - - No game identified in "%1". The directory is required to contain the game binary.<br><br><b>These are the games supported by Mod Organizer:</b><ul>%2</ul> - - - - - Please select the game edition you have (MO can't start the game correctly if this is set incorrectly!) - - - - + failed to start shortcut: %1 - + failed to start application: %1 - + + Mod Organizer - + An instance of Mod Organizer is already running - - - Failed to set up instance - - <Unmanaged> - + Please use "Help" from the toolbar to get usage instructions to all elements - - + + <Manage...> - + failed to parse profile %1: %2 @@ -6391,185 +6880,174 @@ Destination: - + Save changes? - + Save changes to "%1"? - + Connecting to Nexus... - + Waiting for Nexus... - + Opened Nexus in browser. - + Switch to your browser and accept the request. - + Finished. - + No answer from Nexus. - - + + A firewall might be blocking Mod Organizer. - + Nexus closed the connection. - + Cancelled. - + Failed to request %1 - - + + Cancelled - + Internal error - + HTTP code %1 - + Invalid JSON - + Bad response - + API key is empty - + SSL error - + Timed out - + One of the configured MO2 directories (profiles, mods, or overwrite) is on a path containing a symbolic (or other) link. This is likely to be incompatible with MO2's virtual filesystem. - + failed to initialize plugin %1: %2 - - Plugin error - - - - - It appears the plugin "%1" failed to load last startup and caused MO to crash. Do you want to disable it? -(Please note: If this is the first time you see this message for this plugin you may want to give it another try. The plugin may be able to recover from the problem) - - - - + failed to access %1 - + failed to set file time %1 - - - + + + No profile set - + Before you can use ModOrganizer, you need to create at least one profile. ATTENTION: Run the game at least once before creating a profile! - + '%1': file is blocked (%2) - + '%1' seems to be missing, an antivirus may have deleted it - + %1 is loaded. This program is known to cause issues with Mod Organizer, such as freezing or blank windows. Consider uninstalling it. - + %1 is loaded. This program is known to cause issues with Mod Organizer and its virtual filesystem, such script extenders refusing to run. Consider uninstalling it. - - - + + + attempt to store setting for unknown plugin "%1" - + Failed - + Failed to start the helper application: %1 @@ -6606,112 +7084,73 @@ This program is known to cause issues with Mod Organizer, such as freezing or bl - + Confirm? - + This will reset all the choices you made to dialogs and make them all visible again. Continue? - - Connected. + + + + + + + + + + + + + + Cancel - - Not connected. + + + + Enter API Key Manually - - Disconnected. + + + + Connect to Nexus - - Checking API key... + + + + + + N/A - - Received API key. + + Failed to create "%1", you may not have the necessary permissions. Path remains unchanged. - - Received user acount information + + Select base directory - - Linked with Nexus successfully. + + Select download directory - - Failed to set API key - - - - - - - - - - - - - - - - Cancel - - - - - - - Enter API Key Manually - - - - - - - Connect to Nexus - - - - - - - - - N/A - - - - - Failed to create "%1", you may not have the necessary permissions. Path remains unchanged. - - - - - Select base directory - - - - - Select download directory - - - - - Select mod directory + + Select mod directory @@ -7047,6 +7486,219 @@ You can restart Mod Organizer as administrator and try launching the program aga Disabled because + + + One of the following plugins must be enabled: %1. + + + + + This plugin can only be enabled if the '%1' plugin is installed and enabled. + + + + + This plugin can only be enabled for the following game(s): %1. + + + + + + + + Cannot disable plugin + + + + + The '%1' plugin is used by the current game plugin and cannot disabled. + + + + + <p>Disabling the '%1' plugin will also disable the following plugins:</p><ul>%1</ul><p>Do you want to continue?</p> + + + + + Really disable plugin? + + + + + This plugin is required for Mod Organizer to work properly and cannot be disabled. + + + + + Instance at '%1' not found. Select another instance. + + + + + Instance at '%1' not found. You must create a new instance + + + + + The instance name must be a valid folder name. + + + + + An instance with this name already exists. + + + + + Disabling the '%1' plugin will prevent the following %2 plugin(s) from working: + + + + + + + + Global + + + + + + Instance type: %1 + + + + + ? + + + + + Find game installation for %1 + + + + + Find game installation + + + + + + + Unrecognized game + + + + + The folder %1 does not seem to contain a game Mod Organizer can manage. + + + + + See details for the list of supported games. + + + + + No installation found + + + + + Browse... + + + + + The folder must contain a valid game installation + + + + + The folder %1 does not seem to contain an installation for <span style="white-space: nowrap; font-weight: bold;">%2</span> or for any other game Mod Organizer can manage. + + + + + + Use this folder for %1 + + + + + + I know what I'm doing + + + + + + Incorrect game + + + + + The folder %1 seems to contain an installation for <span style="white-space: nowrap; font-weight: bold;">%2</span>, not <span style="white-space: nowrap; font-weight: bold;">%3</span>. + + + + + Manage %1 instead + + + + + Instance location: %1 + + + + + Instance name: %1 + + + + + + Base directory: %1 + + + + + Downloads + + + + + Mods + + + + + Profiles + + + + + Overwrite + + + + + Game: %1 + + + + + Game location: %1 + + + + + Creating %1 + + QueryOverwriteDialog @@ -7165,58 +7817,58 @@ p, li { white-space: pre-wrap; } SelfUpdater - + New update available (%1) - + Do you want to install update? All your mods and setup will be left untouched. Select Show Details option to see the full change-log. - + Install - + Download failed - + Failed to find correct download, please try again later. - + Update - + Download in progress - + Download failed: %1 - + Failed to install update: %1 - + Failed to start %1: %2 - + Error @@ -7574,32 +8226,32 @@ Select Show Details option to see the full change-log. - + Disable automatic internet features - + Disable automatic internet features. This does not affect features that are explicitly invoked by the user (like checking mods for updates, endorsing, opening the web browser) - + Offline Mode - + Use a proxy for network connections. - + Use a proxy for network connections. This uses the system-wide settings which can be configured in Internet Explorer. Please note that MO will start up a few seconds slower on some systems when using a proxy. - + Use HTTP Proxy (Uses System Settings) @@ -7620,102 +8272,117 @@ Select Show Details option to see the full change-log. - + + Tracked Integration + + + + Associate with "Download with manager" links - + Remove cache and cookies. - + Clear Cache - + Known Servers (updated on download) - + Preferred Servers (Drag & Drop) - + Steam - + Username - + <html><head/><body><p>If you save your steam user ID and password here, they will be used when logging into steam.</p></body></html> - + Password - + Plugins - + Author: - + Version: - + Description: - + + Enabled + + + + Key - + Value - + + No plugin found. + + + + Blacklisted Plugins (use <del> to remove): - + Workarounds - + Steam App ID - + The Steam AppID for your game - + <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> <html><head><meta name="qrichtext" content="1" /><style type="text/css"> p, li { white-space: pre-wrap; } @@ -7731,17 +8398,17 @@ p, li { white-space: pre-wrap; } - + Load Mechanism - + Select loading mechanism. See help for details. - + Mod Organizer needs a dll to be injected into the game so all mods are visible to it. There are several means to do this: *Mod Organizer* (default) In this mode the Mod Organizer itself injects the dll. The disadvantage is that you always have to start the game through MO or a link created by it. @@ -7752,28 +8419,28 @@ If you use the Steam version of Oblivion the default will NOT work. In this case - + Enforces that inactive ESPs and ESMs are never loaded. - + It seems that the Games occasionally load ESP or ESM files even if they haven't been activated as plugins. I don't yet know what the circumstances are, but user reports imply it is in some cases unwanted. If this is checked, ESPs and ESMs not checked in the List are invisible to the game and can not be loaded. - + Hide inactive ESPs/ESMs - + Disable this to no longer display mods installed outside MO in the mod list (left pane). Assets from those mods will then be treated as having lowest mod priority together with the original game content. - + By default Mod Organizer will display esp+bsa bundles installed with foreign tools as mods (left pane). This allows you to control their priority in relation to other mods. This is particularly useful if you also use Steam Workshop to install mods. However, if you installed loose file mods outside MO which conflict with BSAs also installed outside MO those conflicts can't be resolved correctly. @@ -7781,66 +8448,66 @@ If you disable this feature, MO will only display official DLCs this way. Please - + Display mods installed outside MO - + If checked, files (i.e. esps, esms and bsas) belonging to the core game can not be disabled in the UI. (default: on) - + If checked, files (i.e. esps, esms and bsas) belonging to the core game can not be disabled in the UI. (default: on) Uncheck this if you want to use Mod Organizer with total conversions (like Nehrim) but be aware that the game will crash if required files are not enabled. - + Force-enable game files - - + + Disable this to prevent the GUI from being locked when running an executable. This may result in abnormal behavior. - + Lock GUI when running executable - + Enable parsing of Archives. This is an Experimental Feature. Has negative effects on performance and known incorrectness. - + <html><head/><body><p>By default, MO will parse archive files (BSA, BA2) to calculate conflicts between the contents of the archive files and other loose files. This process has a noticeable cost in performance.</p><p>This feature should not be confused with the archive management feature offered by MO1. MO2 will only show conflicts with archives and will NOT load them into the game or program.</p><p>If you disable this feature, MO will only display conflicts between loose files.</p></body></html> - + Enable parsing of Archives (Experimental Feature) - - + + For Skyrim, this can be used instead of Archive Invalidation. It should make AI redundant for all Profiles. For the other games this is not a sufficient replacement for AI! - + Back-date BSAs - + Add executables to the blacklist to prevent them from accessing the virtual file system. This is useful to prevent unintended programs from being hooked. Hooking unintended @@ -7849,43 +8516,43 @@ programs you are intentionally running. - + Add executables to the blacklist to prevent them from accessing the virtual file system. This is useful to prevent unintended programs from being hooked. Hooking unintended programs may affect the execution of these programs or the programs you are intentionally running. - + Configure Executables Blacklist - - + + Resets the window geometries for all windows. This can be useful if a window becomes too small or too large, if a column becomes too thin or too wide, and in similar situations. - + Reset Window Geometries - + These are workarounds for problems with Mod Organizer. Please make sure you read the help text before changing anything here. - + Diagnostics - + Hint: right click link and copy link location - + Logs and crash dumps are stored under your current instance in the <a href="LOGS_FULL_PATH">LOGS_DIR</a> and <a href="DUMPS_FULL_PATH">DUMPS_DIR</a> folders. @@ -7895,17 +8562,17 @@ programs you are intentionally running. - + Log Level - + Decides the amount of data printed to "ModOrganizer.log" - + Decides the amount of data printed to "ModOrganizer.log". "Debug" produces very useful information for finding problems. There is usually no noteworthy performance impact but the file may become rather large. If this is a problem you may prefer the "Info" level for regular use. On the "Error" level the log file usually remains empty. @@ -7913,17 +8580,17 @@ programs you are intentionally running. - + Crash Dumps - + Decides which type of crash dumps are collected when injected processes crash. - + Decides which type of crash dumps are collected when injected processes crash. "None" Disables the generation of crash dumps by MO. @@ -7934,17 +8601,17 @@ programs you are intentionally running. - + Max Dumps To Keep - + Maximum number of crash dumps to keep on disk. Use 0 for unlimited. - + Maximum number of crash dumps to keep on disk. Use 0 for unlimited. Set "Crash Dumps" above to None to disable crash dump collection. @@ -7952,7 +8619,7 @@ programs you are intentionally running. - + LOOT Log Level @@ -7967,29 +8634,6 @@ programs you are intentionally running. - - SingleInstance - - - SHM error: %1 - - - - - failed to connect to running instance: %1 - - - - - failed to communicate with running instance: %1 - - - - - failed to receive data from secondary instance: %1 - - - StatusBar @@ -7997,11 +8641,6 @@ programs you are intentionally running. Unknown game - - - Portable - - SyncOverwriteDialog @@ -8036,6 +8675,14 @@ programs you are intentionally running. + + T + + + Plugin + + + TaskDialog @@ -8160,22 +8807,22 @@ On Windows XP: - + Characters for profile %1 - + Overwrite - + Overwrite the file "%1" - + Confirm @@ -8183,7 +8830,7 @@ On Windows XP: UsvfsConnector - + Preparing vfs @@ -8197,7 +8844,7 @@ On Windows XP: - + Connecting to Nexus... @@ -8212,557 +8859,9 @@ On Windows XP: - + Trying again... - - tutorial_conflictresolution_main - - - There are multiple types of conflicts you may encounter when dealing with Mods. This tutorial will try to cover and explain how to deal with all of them. - - - - - First up are file conflicts. These occur when two mods contain the same file. Most commonly this happens when several mods replace the same standard asset from the game, like the texture of an armor. - - - - - As an example, say you install "ModA" which contains stylish new iron and leather armor. Then you install "ModB" which contains sexy ebony and leather armor. Obviously there is a conflict now: which leather armor to use? - - - - - If you were to install the mods manually, when installing "ModB" you would be asked if you want to overwrite conflicting files. If you choose yes, you get the leather armor from "ModB" otherwise you keep the one from "ModA". If you later decide you made the wrong choice, you have to reinstall one of the mods. - - - - - With MO, both ModA and ModB get installed completely (no overwrite dialog) and by default "ModB" gets to provide the leather armor because it's automatically assigned the higher priority. - - - - - However, you can change the mod priority at any time using drag&drop on this list. If you assign "ModA" a higher priority, it provides the leather armor, no re-installation required. Since the priorities of mods in this list are treated as if the mods were installed in that order, I tend to talk about "installation order". - - - - - If the "Flags"-column is enabled in the mod list, it will show you which mods are involved in a conflict and how... - - - - - <img src="qrc:///MO/gui/emblem_conflict_overwrite" /> indicates that the mod overwrites files that are also available in another mod. - - - - - <img src="qrc:///MO/gui/emblem_conflict_overwritten" /> indicates that the mod is <b>partially</b> overwritten by another. - - - - - <img src="qrc:///MO/gui/emblem_conflict_mixed" /> indicates that both of the above is true. - - - - - <img src="qrc:///MO/gui/emblem_conflict_redundant" /> indicates that the mod is completely overwritten by another. You could as well disable it. - - - - - There are two ways to see the individual files involved in a conflict: - - - - - Option A: Switch to the "Data"-tab if necessary - - - - - ... here, if you mark the highlighted control, the tree will only display files in conflict. In the right column, it says which mod currently provides the mod (because it has highest priority) and if you hover your mouse over that info, it will list which other mods contains it. - - - - - Option B: Open the <i>Information</i>-Dialog of an <b>enabled</b> mod you're interested in by either double-clicking it or selecting <i>Information...</i> from the right-click menu - - - - - This was everything to know about file conflicts. The second type of conflict we have to deal with are "record conflicts". - - - - - I told you in the "First Steps" tutorial how the esp/esm/esl plugins contain changes to the game world like modifications to the terrain or existing NPCs. Each change like this is stored in a record, hence the name "record conflict". For example when two mods try to change the same location, only one change can become active. - - - - - As with file conflicts you can't really fix these conflicts, you have to choose which change you want. This time around however, if you choose wrong, your game may become unstable because there may be dependencies between the records of a mod. - - - - - Please open the "Plugins"-tab... - - - - - Again you can use drag&drop to change priorities of plugins, thus deciding which plugin takes precedence in regards to conflicts. This is commonly called the "load order". But how do you know how to order the plugins? - - - - - Unlike with file conflicts, MO does not provide help on finding conflicts. The good news is, there already is a perfect tool for that called LOOT. LOOT is available on the Nexus and integrates neatly with MO. Basically, if you don't have LOOT yet, install it once this tutorial is over. - - - - - After you installed LOOT in the default location (follow its instructions), start MO again and LOOT should automatically appear as an Executable... - - - - - When you run LOOT, it will automatically re-organize plugins for best compatibility (overwriting your manual changes). It will also open a report in your browser that warns about incompatibilities. You should read the report, at least for new mods. - - - - - The final type of conflicts are also "record conflicts". Like the previous type. It's confusing, so I'll just call them "lists conflicts" instead. The difference is the types of records in conflict. The ones in question here can be merged so you may be able to get all modifications in your game. - - - - - One common example of such records are leveled lists that contain all the items that may spawn at a specific character level. Traditionally, if multiple mods add items to such a list, only one is in effect... - - - - - ... but there are tools to merge those mods so you can have the effects of all of them. Again, this functionality is not integrated with MO because there are already great tools. For Oblivion and Skyrim look for wrye bash, for fallout 3/nv it's wrye flash. For Skyrim there is also "SkyBash". All of these can create a so-called "bashed patch" which is a plugin that contains the combined mergeable records from all your mods. - - - - - This completes the tutorial. - - - - - tutorial_conflictresolution_modinfo - - - Please switch to the "Conflicts"-Tab. - - - - - Here you can see a list of files in this mod that out-prioritize others in a file conflict and one with files where this mod is overridden. - - - - - Please close the information dialog again. - - - - - tutorial_firststeps_browser - - - This is a fully featured browser that is set up to open the correct nexus page for your game. You can download any mod using the "DOWNLOAD WITH MANAGER"-button or the "manual"-link and it will be downloaded by MO. - - - - - tutorial_firststeps_main - - - Welcome to the ModOrganizer Tutorial! This will guide you through the most common features of MO. -Please go along with the tutorial because some things can't be demonstrated if you skip something. - - - - - Before we continue with the step-by-step tutorial, I'd like to tell you about the other ways you can receive help on ModOrganizer. - - - - - The highlighted button provides hints on solving potential problems MO recognized automatically. - - - - - -There IS a notification now but you may want to hold off on clearing it until after completing the tutorial. - - - - - This button provides multiple sources of information and further tutorials. - - - - - Finally there are tooltips on almost every part of Mod Organizer. If there is a control in MO you don't understand, please try hovering over it to get a short description or use "Help on UI" from the help menu to get a longer explanation - - - - - This list displays all mods installed through MO. It also displays installed DLCs and some mods installed outside MO but you have limited control over those in MO. - - - - - Before we start installing mods, let's have a quick look at the settings. - - - - - Now it's time to install a few mods!Please go along with this because we need a few mods installed to demonstrate other features - - - - - There are a few ways to get mods into ModOrganizer. If you associated MO with NXM links in the settings you can now use your regular browser to send downloads from Nexus to MO. Click on "Nexus" to open nexus, find a mod and click the green download buttons on Nexus saying "Download with Manager". - - - - - You can also install mods from disk using the "Install Mod" button. - - - - - Downloads will appear on the "Downloads"-tab here. You have to download and install at least one mod to proceed. - - - - - Great, you just installed your first mod. Please note that the installation procedure may differ based on how a mod was packaged. - - - - - Now you know all about downloading and installing mods but they are not enabled yet... - - - - - Install a few more mods if you want, then enable mods by checking them in the left pane. Mods that aren't enabled have no effect on the game whatsoever. - - - - - For some mods, enabling it on the left pane is all you have to do... - - - - - ...but most contain plugins. These are plugins for the game and are required to add stuff to the game (new weapons, armors, quests, areas, ...). Please open the "Plugins"-tab to get a list of plugins. - - - - - You will notice some plugins are grayed out. These are part of the main game and can't be disabled. - - - - - A single mod may contain zero, one or multiple esps. Some or all may be optional. If in doubt, please consult the documentation of the individual mod. To do so, right-click the mod and select "Information". - - - - - Now you know how to download, install and enable mods. -It's important you always start the game from inside MO, otherwise the mods you installed here won't work. - - - - - This combobox lets you choose <i>what</i> to start. This way you can start the game, Launcher, Script Extender, the Creation Kit or other tools. If you miss a tool you can also configure this list but that is an advanced topic. - - - - - This completes the basic tutorial. As homework go play a bit! After you have installed more mods you may want to read the tutorial on conflict resolution. - - - - - tutorial_firststeps_modinfo - - - This dialog tries to expose as much information about a mod as possible. Depending on the mod this may include readmes, screenshots, optional plugins and so on. If a certain type of information was not found in a mod, the corresponding tab is grayed out. - - - - - If you installed the mod from Nexus, the corresponding tab should give you direct access to the mod page. - - - - - We may revisit this screen in later tutorials. - - - - - tutorial_firststeps_settings - - - You can use your regular browser to download from Nexus. -Please open the "Nexus"-tab - - - - - Click this button so that "DOWNLOAD WITH MANAGER"-buttons are download with Mod Organizer. - - - - - Use this interface to obtain an API key from NexusMods. This is used for all API connections - downloads, updates etc. MO2 uses the Windows Credential Manager to store this data securely. If the SSO page on Nexus is failing, use the manual entry and copy the API key from your profile. - - - - - tutorial_primer_main - - - This window shows all the mods that are installed. The column headers can be used for sorting. Only checked mods are active in the current profile. - - - - - Each profile is a separate set of enabled mods and ini settings. - - - - - Perform various actions on your mod list, such as refreshing data and checking for mod updates. - - - - - Quick access to various directories, such as your MO2 mods, profiles, saves, and your active game location. - - - - - Restore a mod list backup. - - - - - Create a backup of your current mod list. - - - - - Running counter of your active mods. Hover to see a more detailed breakdown. - - - - - The dropdown allows various ways of grouping the mods shown in the mod list. - - - - - Show/hide the category pane. - - - - - Quickly filter the mod list as you type. - - - - - Switch between information views. - - - - - This shows mod categories and some meta categories (in angle-brackets). Select some to filter the mod list. For example select "<Checked>" to show only active mods. - - - - - Customizable list for choosing the program to run. - - - - - When this button is clicked, Mod Organizer creates a virtual directory structure then runs the program selected to the left. - - - - - Will create a shortcut for quick access. The shortcut can be placed in the toolbar at the top, in the Start Menu or on the Windows Desktop. - - - - - Log messages produced by MO. Please note that messages with a light bulb usually don't require your attention. - - - - - Indicator of your current NexusMods API request limits. - - - - - Change/manage MO2 instances or switch to portable mode. - - - - - Browse to and manually install a mod from an archive on your computer. - - - - - Automatically open NexusMods to browse and install mods via the API. - - - - - Manage your MO2 profiles. - - - - - Open the executable editor to add and modify applications you wish to run with MO2. - - - - - Select from a collection of additional tools, such as an INI editor, integrated FNIS updater, and more. - - - - - Configure Mod Organizer. - - - - - See the status of and/or endorse MO2 on NexusMods. - - - - - Notifications about the current setup. - - - - - Activates if there is an update for MO. Please note that if, for any reason, MO can't communicate with NMM, this will not work either. - - - - - Access more information about MO2, including these tutorials, a link to the development discord, information about the devs and dependencies. - - - - - Plugins (esp/esm/esl files) of the mods in the current profile. They need to be checked to be loaded. - - - - - Automatically sort plugins using the bundled LOOT application. - - - - - Restore a backup of your plugin list order. - - - - - Save a backup of your plugin list order. - - - - - Counter of your total active plugins. Hover to see a breakdown of plugin types. - - - - - Quickly filter plugin list as you type. - - - - - All the asset archives (.bsa files) for all active mods. - - - - - The directory tree and all files that the program will see. - - - - - Save game browser. Shows all the saves for the current profile and whether or not the current mod-load status is correct. - - - - - Shows the mods that have been downloaded and if they’ve been installed. - - - - - Click to quit - - - - - tutorial_window_installer - - - This mod has been packaged in a way that Mod Organizer did not automatically recognize... - - - - - You can use drag&drop on this list to fix the structure of the mod. - - - - - The correct structure replicates the data-directory of the game. That means esps, the "meshes"- or "textures"-directory and so on should be directly below "<data>". - - - - - You can also disable files and directories that you don't want to unpack. - - - - - From the context menu (right-click) you can open textfiles, in case you want to access a readme. - - - - - This text will turn green if MO thinks the structure looks good. - - - -- cgit v1.3.1 From 32072ca19ec1ab520030a1fb411d8f2fca322374 Mon Sep 17 00:00:00 2001 From: Mikaël Capelle Date: Thu, 19 Nov 2020 21:21:38 +0100 Subject: Return shared_ptr in IPlugin::requirements(). --- src/plugincontainer.cpp | 4 +--- src/plugincontainer.h | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) (limited to 'src') diff --git a/src/plugincontainer.cpp b/src/plugincontainer.cpp index f7a556e0..fc7f5cd0 100644 --- a/src/plugincontainer.cpp +++ b/src/plugincontainer.cpp @@ -119,9 +119,7 @@ PluginRequirements::PluginRequirements( } void PluginRequirements::fetchRequirements() { - for (auto* requirement : m_Plugin->requirements()) { - m_Requirements.emplace_back(requirement); - } + m_Requirements = m_Plugin->requirements(); } IPluginProxy* PluginRequirements::proxy() const diff --git a/src/plugincontainer.h b/src/plugincontainer.h index 36edfad9..0c1290b4 100644 --- a/src/plugincontainer.h +++ b/src/plugincontainer.h @@ -109,7 +109,7 @@ private: MOBase::IPlugin* m_Plugin; MOBase::IPluginProxy* m_PluginProxy; MOBase::IPlugin* m_Master; - std::vector> m_Requirements; + std::vector> m_Requirements; MOBase::IOrganizer* m_Organizer; std::vector m_RequiredFor; -- cgit v1.3.1 From 0a912afe72a8fb765f240d59db17f7b50094b014 Mon Sep 17 00:00:00 2001 From: Mikaël Capelle Date: Sat, 21 Nov 2020 12:13:20 +0100 Subject: Add timer to the save list watcher to avoid error during refresh. --- src/mainwindow.cpp | 5 ++++- src/mainwindow.h | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index ecb4319b..04833e6a 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -430,7 +430,10 @@ MainWindow::MainWindow(Settings &settings this, &MainWindow::refresherProgress); connect(m_OrganizerCore.directoryRefresher(), SIGNAL(error(QString)), this, SLOT(showError(QString))); - connect(&m_SavesWatcher, SIGNAL(directoryChanged(QString)), this, SLOT(refreshSavesIfOpen())); + m_SavesWatcherTimer.setSingleShot(true); + m_SavesWatcherTimer.setInterval(500); + connect(&m_SavesWatcher, &QFileSystemWatcher::directoryChanged, [this]() { m_SavesWatcherTimer.start(); }); + connect(&m_SavesWatcherTimer, &QTimer::timeout, this, &MainWindow::refreshSavesIfOpen); connect(&m_OrganizerCore.settings(), SIGNAL(languageChanged(QString)), this, SLOT(languageChange(QString))); connect(&m_OrganizerCore.settings(), SIGNAL(styleChanged(QString)), this, SIGNAL(styleChanged(QString))); diff --git a/src/mainwindow.h b/src/mainwindow.h index da916d2a..da7bb6ee 100644 --- a/src/mainwindow.h +++ b/src/mainwindow.h @@ -345,6 +345,7 @@ private: std::unique_ptr m_IntegratedBrowser; + QTimer m_SavesWatcherTimer; QFileSystemWatcher m_SavesWatcher; QByteArray m_ArchiveListHash; -- cgit v1.3.1 From f3f0036a056b17ed481cf3ec4dea78a59c04b16e Mon Sep 17 00:00:00 2001 From: Mikaël Capelle Date: Mon, 23 Nov 2020 15:30:21 +0100 Subject: Fix PluginContainer::setUserInterface(). --- src/mainwindow.cpp | 4 ++-- src/modinfowithconflictinfo.cpp | 4 ++-- src/modinfowithconflictinfo.h | 4 +++- src/plugincontainer.cpp | 18 ++++++++++-------- src/plugincontainer.h | 2 +- 5 files changed, 18 insertions(+), 14 deletions(-) (limited to 'src') diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 04833e6a..7558076d 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -511,6 +511,7 @@ MainWindow::MainWindow(Settings &settings m_Tutorial.expose("espList", m_OrganizerCore.pluginList()); m_OrganizerCore.setUserInterface(this); + m_PluginContainer.setUserInterface(this); connect(this, &MainWindow::userInterfaceInitialized, &m_OrganizerCore, &OrganizerCore::userInterfaceInitialized); for (const QString &fileName : m_PluginContainer.pluginFileNames()) { installTranslator(QFileInfo(fileName).baseName()); @@ -703,7 +704,7 @@ MainWindow::~MainWindow() try { cleanup(); - m_PluginContainer.setUserInterface(nullptr, nullptr); + m_PluginContainer.setUserInterface(nullptr); m_OrganizerCore.setUserInterface(nullptr); if (m_IntegratedBrowser) { @@ -1679,7 +1680,6 @@ void MainWindow::updateToolMenu() void MainWindow::registerModPage(IPluginModPage *modPage) { QAction *action = new QAction(modPage->icon(), modPage->displayName(), this); - modPage->setParentWidget(this); connect(action, &QAction::triggered, this, [this, modPage]() { if (modPage->useIntegratedBrowser()) { diff --git a/src/modinfowithconflictinfo.cpp b/src/modinfowithconflictinfo.cpp index 6e3f751e..ad7eda3f 100644 --- a/src/modinfowithconflictinfo.cpp +++ b/src/modinfowithconflictinfo.cpp @@ -15,7 +15,7 @@ namespace fs = std::filesystem; ModInfoWithConflictInfo::ModInfoWithConflictInfo( PluginContainer *pluginContainer, const MOBase::IPluginGame* gamePlugin, DirectoryEntry **directoryStructure) - : ModInfo(pluginContainer), m_GamePlugin(gamePlugin), + : ModInfo(pluginContainer), m_GamePlugin(gamePlugin), m_FileTree([this]() { return QDirFileTree::makeTree(absolutePath()); }), m_Valid([this]() { return doIsValid(); }), m_Contents([this]() { return doGetContents(); }), @@ -27,7 +27,7 @@ void ModInfoWithConflictInfo::clearCaches() } std::vector ModInfoWithConflictInfo::getFlags() const -{ +{ std::vector result = std::vector(); if (hasHiddenFiles()) { result.push_back(ModInfo::FLAG_HIDDEN_FILES); diff --git a/src/modinfowithconflictinfo.h b/src/modinfowithconflictinfo.h index e312a89a..0d11fcb1 100644 --- a/src/modinfowithconflictinfo.h +++ b/src/modinfowithconflictinfo.h @@ -34,7 +34,9 @@ public: * * @return true if the content is there, false otherwise. */ - virtual bool hasContent(int content) const override; /** + virtual bool hasContent(int content) const override; + + /** * @brief Retrieve a file tree corresponding to the underlying disk content * of this mod. * diff --git a/src/plugincontainer.cpp b/src/plugincontainer.cpp index fc7f5cd0..44c9a651 100644 --- a/src/plugincontainer.cpp +++ b/src/plugincontainer.cpp @@ -289,15 +289,17 @@ PluginContainer::~PluginContainer() { unloadPlugins(); } -void PluginContainer::setUserInterface(IUserInterface *userInterface, QWidget *widget) +void PluginContainer::setUserInterface(IUserInterface *userInterface) { - for (IPluginProxy *proxy : bf::at_key(m_Plugins)) { - proxy->setParentWidget(widget); - } - - if (userInterface != nullptr) { - for (IPluginModPage *modPage : bf::at_key(m_Plugins)) { - userInterface->registerModPage(modPage); + if (userInterface) { + for (IPluginProxy* proxy : bf::at_key(m_Plugins)) { + proxy->setParentWidget(userInterface->mainWindow()); + } + for (IPluginModPage* modPage : bf::at_key(m_Plugins)) { + modPage->setParentWidget(userInterface->mainWindow()); + } + for (IPluginTool* tool : bf::at_key(m_Plugins)) { + tool->setParentWidget(userInterface->mainWindow()); } } diff --git a/src/plugincontainer.h b/src/plugincontainer.h index 0c1290b4..26e12659 100644 --- a/src/plugincontainer.h +++ b/src/plugincontainer.h @@ -188,7 +188,7 @@ public: PluginContainer(OrganizerCore *organizer); virtual ~PluginContainer(); - void setUserInterface(IUserInterface *userInterface, QWidget *widget); + void setUserInterface(IUserInterface *userInterface); void loadPlugins(); void unloadPlugins(); -- cgit v1.3.1 From d855daf1031497f0ca72e3b64d257e7ce424ee80 Mon Sep 17 00:00:00 2001 From: Mikaël Capelle Date: Wed, 25 Nov 2020 21:18:37 +0100 Subject: Revert translation file. --- src/organizer_en.ts | 3549 +++++++++++++++++++++++++-------------------------- 1 file changed, 1725 insertions(+), 1824 deletions(-) (limited to 'src') diff --git a/src/organizer_en.ts b/src/organizer_en.ts index 646b3310..c65c946c 100644 --- a/src/organizer_en.ts +++ b/src/organizer_en.ts @@ -110,22 +110,22 @@ - + ...and all other contributors! - + Other Supporters && Contributors - + Tannin (Original Creator) - + Close @@ -178,17 +178,17 @@ p, li { white-space: pre-wrap; } AdvancedConflictListModel - + Overwrites - + File - + Overwritten By @@ -339,295 +339,6 @@ p, li { white-space: pre-wrap; } - - CreateInstanceDialog - - - Creating an instance - - - - - Creating a new instance - - - - - <h3>What is an instance?</h3> -<p>An instance is a full set of mods, downloads, profiles and configuration for a game. Each game must be managed in its own instance. Mod Organizer can freely switch between instances.</p> - - - - - <html><head/><body><p><a href="https://github.com/ModOrganizer2/modorganizer/wiki/Instances">More information</a></p></body></html> - - - - - Never show this page again - - - - - <h3>Select the type of instance to create.</h3> - - - - - Create a global instance - - - - - Global instances are stored in %1, but some paths can be changed to be on a different drive if necessary. A single installation of Mod Organizer can manage multiple global instances. - - - - - Create a portable instance - - - - - A portable instance stores everything in Mod Organizer's installation folder, currently %1. There can only be one portable instance per installation of Mod Organizer. - - - - - A portable instance already exists. - - - - - <h3>Select the game to manage.</h3> - - - - - Show all supported games - - - - - Filter - - - - - <h3>Select the game edition.</h3> - - - - - This game has multiple variants. The correct one must be selected or Mod Organizer will not be able to launch the game properly. - - - - - <h3>Customize the name for this <span style="white-space: nowrap;">%1</span> instance.</h3> - - - - - Instance name - - - - - There is already an instance with this name. - - - - - The name contains invalid characters. It must be a valid folder name. - - - - - <h3>Select a folder where the data should be stored.</h3> - - - - - This includes downloads, mods, profiles and overwrite for your <b>%1</b> instance. If there is enough space on this drive, you should use the default folder. - - - - - - - - - - ... - - - - - Location - - - - - Warning: This folder already exists. - - - - - - - - - - Folder - - - - - Warning: The folder contains invalid characters. - - - - - Mods - - - - - Overwrite - - - - - Base directory - - - - - Warning: The folder %1 already exists. - - - - - Downloads - - - - - Profiles - - - - - Use %BASE_DIR% to refer to the Base Directory. - - - - - Warning: The folder %1 contains invalid characters. - - - - - Show advanced options - - - - - <h3>Link Mod Organizer with your Nexus account</h3> - - - - - Linking with Nexus allows you to download mods directly from Mod Organizer and automatically check for updates. This is optional. - - - - - Connect to Nexus - - - - - Enter API Key Manually - - - - - <h3>Confirmation</h3> - - - - - The instance is about to be created. Review the information below and press 'Finish'. - - - - - Instance creation log - - - - - Launch the new instance - - - - - < Back - - - - - Next > - - - - - Cancel - - - - - Setting up instance %1 - - - - - Setting up an instance %1 - - - - - Creating instance... - - - - - Writing %1... - - - - - Format error. - - - - - Error %1. - - - - - Done. - - - - - Finish - - - CredentialsDialog @@ -669,44 +380,6 @@ p, li { white-space: pre-wrap; } - - DisableProxyPluginDialog - - - Really disable plugin? - - - - - Disabling the '%1' plugin will prevent the following plugins from working: - - - - - Plugin - - - - - Description - - - - - Do you want to continue? You will need to restart Mod Organizer for the change to take effect. - - - - - Yes - - - - - No - - - DownloadList @@ -851,137 +524,132 @@ p, li { white-space: pre-wrap; } - Open Meta File - - - - - - + + Reveal in Explorer - - + + Delete - + Un-Hide - + Hide - + Cancel - + Pause - + Resume - + Delete Installed Downloads... - + Delete Uninstalled Downloads... - + Delete All Downloads... - + Hide Installed... - + Hide Uninstalled... - + Hide All... - + Un-Hide All... - - - - + + + + Delete Files? - + This will permanently delete the selected download. Are you absolutely sure you want to proceed? - + This will remove all finished downloads from this list and from disk. Are you absolutely sure you want to proceed? - + This will remove all installed downloads from this list and from disk. Are you absolutely sure you want to proceed? - + This will remove all uninstalled downloads from this list and from disk. Are you absolutely sure you want to proceed? - - - + + + Hide Files? - + This will remove all finished downloads from this list (but NOT from disk). - + This will remove all installed downloads from this list (but NOT from disk). - + This will remove all uninstalled downloads from this list (but NOT from disk). @@ -1051,7 +719,7 @@ File %3: %4 - + remove: invalid download index %1 @@ -1066,256 +734,256 @@ File %3: %4 - + restore: invalid download index: %1 - + cancel: invalid download index %1 - + pause: invalid download index %1 - + resume: invalid download index %1 - + resume (int): invalid download index %1 - + No known download urls. Sorry, this download can't be resumed. - - + + query: invalid download index %1 - + Please enter the nexus mod id - + Mod ID: - + Please select the source game code for %1 - + Hashing download file '%1' - + Cancel - + VisitNexus: invalid download index %1 - + Nexus ID for this Mod is unknown - + OpenFile: invalid download index %1 - + OpenFileInDownloadsFolder: invalid download index %1 - + get pending: invalid download index %1 - + get path: invalid download index %1 - + Main - + Update - + Optional - + Old - + Miscellaneous - + Deleted - + Unknown - + display name: invalid download index %1 - + file name: invalid download index %1 - + file time: invalid download index %1 - + file size: invalid download index %1 - + progress: invalid download index %1 - + state: invalid download index %1 - + infocomplete: invalid download index %1 - - - + + + mod id: invalid download index %1 - + ishidden: invalid download index %1 - + file info: invalid download index %1 - + mark installed: invalid download index %1 - + mark uninstalled: invalid download index %1 - + Memory allocation error (in processing progress event). - + Memory allocation error (in processing downloaded data). - + Information updated - - + + No matching file found on Nexus! Maybe this file is no longer available or it was renamed? - + No file on Nexus matches the selected file by name. Please manually choose the correct one. - + No download server available. Please try again later. - + Failed to request file info from nexus: %1 - + Warning: Content type is: %1 - + Download header content length: %1 downloaded file size: %2 - + Download failed: %1 (%2) - + We were unable to download the file due to errors after four retries. There may be an issue with the Nexus servers. - + failed to re-open %1 - + Unable to write download to drive (return %1). Check the drive's available storage. @@ -1794,54 +1462,54 @@ Right now the only case I know of where this needs to be overwritten is for the FileTreeModel - + Name - + Mod - + Type - + Size - + Date modified - + Directory - - + + Virtual path - + Real path - + From - - + + Also in @@ -2080,145 +1748,122 @@ Right now the only case I know of where this needs to be overwritten is for the - - GeneralConflictsTab - - - <table cellspacing="5"><tr><th>Type</th><th>%1</th><th>Total</th><th>Percent</th></tr><tr><td>Loose files:&emsp;</td><td align=right>%2</td><td align=right>%3</td><td align=right>%4%</td></tr><tr><td>Archive files:&emsp;</td><td align=right>%5</td><td align=right>%6</td><td align=right>%7%</td></tr><tr><td>Combined:&emsp;</td><td align=right>%8</td><td align=right>%9</td><td align=right>%10%</td></tr></table> - - - - - Winning - - - - - Losing - - - - - Non conflicting - - - InstallationManager - + Password required - + Password - + Extracting files - - + + Extraction failed: %1 - + Failed to create backup - + Mod Name - + Name - + Invalid name - + The name you entered is invalid, please enter a different one. - + File format "%1" not supported - + Invalid file tree returned by plugin. - + Installation failed - + Something went wrong while installing this mod. - + None of the available installer plugins were able to handle that archive. This is likely due to a corrupted or incompatible download or unrecognized archive format. - + no error - + 7z.dll not found - + 7z.dll isn't valid - + archive not found - + failed to open archive - + unsupported archive type - + internal library error - + archive invalid - + unknown archive error @@ -2236,176 +1881,71 @@ This is likely due to a corrupted or incompatible download or unrecognized archi - - Rename - - - - - <html><head/><body><p><a href="https://github.com/ModOrganizer2/modorganizer/wiki/Instances">What is an instance?</a></p></body></html> + + Create portable instance - - Filter + + <html><head/><body><p><a href="https://github.com/ModOrganizer2/modorganizer/wiki/Instance"><span style=" text-decoration: underline; color:#0000ff;">What is an instance?</span></a></p></body></html> - - Name + + Game: - - - - Explore + + Name: - - Game + + Location: - - Location + + Rename - - Game location + + Base folder: - - Base folder + + + Explore - + Convert to portable - + Convert to global - - Open INI + + Delete instance - - Delete instance... - - - - - Close - - - - + Switch to this instance - - Switching instances - - - - - Mod Organizer must restart to manage the instance '%1'. - - - - - This confirmation can be disabled in the settings. - - - - - Restart Mod Organizer - - - - - + Cancel - - - - Rename instance - - - - - The active instance cannot be renamed. - - - - - Instance name - - - - - Error - - - - - Failed to rename "%1" to "%2": %3 - - - - - - - Deleting instance - - - - - The active instance cannot be deleted. - - - - - These files and folders will be deleted - - - - - All checked items will be deleted. - - - - - Move to the recycle bin - - - - - Delete permanently - - - - - Nothing to delete. - - - - - A portable instance already exists. - - ListDialog @@ -2538,12 +2078,12 @@ This is likely due to a corrupted or incompatible download or unrecognized archi MOApplication - + an error occurred: %1 - + an error occurred @@ -2551,49 +2091,49 @@ This is likely due to a corrupted or incompatible download or unrecognized archi MOBase::FilterWidget - + Filter options - + Use regular expressions - + Use regular expressions in filters - + Case sensitive - + Make regular expressions case sensitive (/i) leave "(/i)" verbatim - + Extended - + Ignores unescaped whitespace in regular expressions (/x) leave "(/x)" verbatim - + Keep selection in view - + Scroll to keep the current selection in view after filtering @@ -2611,17 +2151,17 @@ This is likely due to a corrupted or incompatible download or unrecognized archi - + failed to write to %1 - + file not found: %1 - + Save @@ -2642,29 +2182,6 @@ This is likely due to a corrupted or incompatible download or unrecognized archi - - MOMultiProcess - - - SHM error: %1 - - - - - failed to connect to running process: %1 - - - - - failed to communicate with running process: %1 - - - - - failed to receive data from secondary process: %1 - - - MainWindow @@ -2761,7 +2278,7 @@ p, li { white-space: pre-wrap; } - + Create Backup @@ -2915,7 +2432,7 @@ p, li { white-space: pre-wrap; } - + Refresh @@ -3037,1100 +2554,1113 @@ p, li { white-space: pre-wrap; } - + &File - - + + &Tools - - - + + + &Help - + &View - + &Toolbars - + &Run - - + + Log - + Install &Mod... - + Install &Mod - - + + Install a new mod from an archive - + Ctrl+M - + &Profiles... - + &Profiles - - + + Configure profiles - + Ctrl+P - + &Executables... - + &Executables - - + + Configure the executables that can be started through Mod Organizer - + Ctrl+E - + &Tool Plugins - + Tools - + Ctrl+I - + &Settings... - + &Settings - - + + Configure settings and workarounds - + Ctrl+S - - + + Visit &Nexus - - + + Visit the Nexus website in your browser for more mods - + Ctrl+N - - + + &Update Mod Organizer - - + + Mod Organizer is up-to-date - + &Notifications... - - + + Open the notifications dialog - + This button will be highlighted on the toolbar if MO discovered potential problems in your setup and provide tips on how to fix them. - - + + Show help options - + Ctrl+H - - + + &Endorse ModOrganizer - - - + + + Endorse Mod Organizer - - - Manage Instan&ces... + + &Change Game... - - - Open the instance manager window to manage a different instance + + &Change Game - - + + + Open the Instance selection dialog to manage a different Game + + + + + E&xit - - + + Exits Mod Organizer - + M&ain Toolbar - + &Small Icons - + Lar&ge Icons - + &Icons Only - + &Text Only - + I&cons and Text - + M&edium Icons - + &Menu - + Status &bar - + Toolbar and Menu - + Desktop - + Start Menu - + There is no supported sort mechanism for this game. You will probably have to use a third-party tool. - + Crash on exit - + MO crashed while exiting. Some settings may not be saved. Error: %1 - + There are notifications to read - + There are no notifications - - - + + + Endorse - + Won't Endorse - + Help on UI - + Documentation - + Chat on Discord - + Report Issue - + Tutorials - + About - + About Qt - + Name - + Please enter a name for the new profile - + failed to create profile: %1 - + Show tutorial? - + You are starting Mod Organizer for the first time. Do you want to show a tutorial of its basic features? If you choose no you can always start the tutorial from the "Help"-menu. - - Never ask to show tutorials - - - - + Downloads in progress - + There are still downloads in progress, do you really want to quit? - + Plugin "%1" failed: %2 - + Plugin "%1" failed - + + Browse Mod Page + + + + + <Edit...> + + + + + (no executables) + + + + This bsa is enabled in the ini file so it may be required! - + Activating Network Proxy - + Notice: Your current MO version (%1) is lower than the previously used one (%2). The GUI may not downgrade gracefully, so you may experience oddities. However, there should be no serious issues. - + Choose Mod - + Mod Archive - + Start Tutorial? - + You're about to start a tutorial. For technical reasons it's not possible to end the tutorial early. Continue? - + failed to change origin name: %1 - + failed to move "%1" from mod "%2" to "%3": %4 - + failed to rename mod: %1 - + Overwrite? - + This will replace the existing mod "%1". Continue? - + failed to remove mod "%1" - + failed to rename "%1" to "%2" - - - - - + + + + + Confirm - + Remove the following mods?<br><ul>%1</ul> - + failed to remove mod: %1 - - - + + + Failed - + Installation file no longer exists - + Mods installed with old versions of MO can't be reinstalled in this way. - + Failed to create backup. - + Endorsing multiple mods will take a while. Please wait... - + Unendorsing multiple mods will take a while. Please wait... - + Failed to display overwrite dialog: %1 - + Restore all hidden files in the following mods?<br><ul>%1</ul> - - + + Are you sure? - + About to restore all hidden files in: - + Opening Nexus Links - + You are trying to open %1 links to Nexus Mods. Are you sure you want to do this? - + Nexus ID for this mod is unknown - - + + Opening Web Pages - - + + You are trying to open %1 Web Pages. Are you sure you want to do this? - + + No valid Web Page for this mod + + + + <table cellspacing="5"><tr><th>Type</th><th>All</th><th>Visible</th><tr><td>Enabled mods:&emsp;</td><td align=right>%1 / %2</td><td align=right>%3 / %4</td></tr><tr><td>Unmanaged/DLCs:&emsp;</td><td align=right>%5</td><td align=right>%6</td></tr><tr><td>Mod backups:&emsp;</td><td align=right>%7</td><td align=right>%8</td></tr><tr><td>Separators:&emsp;</td><td align=right>%9</td><td align=right>%10</td></tr></table> - + <table cellspacing="6"><tr><th>Type</th><th>Active </th><th>Total</th></tr><tr><td>All plugins:</td><td align=right>%1 </td><td align=right>%2</td></tr><tr><td>ESMs:</td><td align=right>%3 </td><td align=right>%4</td></tr><tr><td>ESPs:</td><td align=right>%7 </td><td align=right>%8</td></tr><tr><td>ESMs+ESPs:</td><td align=right>%9 </td><td align=right>%10</td></tr><tr><td>ESLs:</td><td align=right>%5 </td><td align=right>%6</td></tr></table> - - - + + + Create Mod... - + This will create an empty mod. Please enter a name: - - + + A mod with this name already exists - + Create Separator... - + This will create a new separator. Please enter a name: - + A separator with this name already exists - + This will move all files from overwrite into a new, regular mod. Please enter a name: - + Move successful. - + About to recursively delete: - - - You are not currently authenticated with Nexus. Please do so under Settings -> Nexus. - - - - + Continue? - + The versioning scheme decides which version is considered newer than another. This function will guess the versioning scheme under the assumption that the installed version is outdated. - + Sorry - + I don't know a versioning scheme where %1 is newer than %2. - + Really enable all visible mods? - + Really disable all visible mods? - + Export to csv - + CSV (Comma Separated Values) is a format that can be imported in programs like Excel to create a spreadsheet. You can also use online editors and converters instead. - + Select what mods you want export: - + All installed mods - + Only active (checked) mods from your current profile - + All currently visible mods in the mod list - + Choose what Columns to export: - + Mod_Priority - + Mod_Name - + Notes_column - + Mod_Status - + Primary_Category - + Nexus_ID - + Mod_Nexus_URL - + Mod_Version - + Install_Date - + Download_File_Name - + export failed: %1 - + Open Game folder - + Open MyGames folder - + Open INIs folder - + Open Instance folder - + Open Mods folder - + Open Profile folder - + Open Downloads folder - + Open MO2 Install folder - + Open MO2 Plugins folder - + Open MO2 Stylesheets folder - + Open MO2 Logs folder - + Install Mod... - + Create empty mod - + Create Separator - + Enable all visible - + Disable all visible - + Check for updates - + Export to csv... - - + + Send to - - + + Top - - + + Bottom - - + + Priority... - + Separator... - + All Mods - + Sync to Mods... - + Move content to Mod... - + Clear Overwrite... - - - + + + Open in Explorer - + Restore Backup - + Remove Backup... - - + + Ignore missing data - - + + Mark as converted/working - - + + Visit on Nexus - - + + Visit on %1 - - + + Change Categories - - + + Primary Category - + Rename Separator... - + Remove Separator... - - + + Select Color... - - + + Reset Color - + Change versioning scheme - + Force-check updates - + Un-ignore update - + Ignore update - - + + Enable selected - - + + Disable selected - + Rename Mod... - + Reinstall Mod - + Remove Mod... - + Restore hidden files - + Un-Endorse - + Won't endorse - + Endorsement state unknown - + Start tracking - + Stop tracking - + Tracked state unknown - + Information... - - + + Exception: - - + + Unknown exception - + %1 more - + Are you sure you want to remove the following %n save(s)?<br><ul>%1</ul><br>Removed saves will be sent to the Recycle Bin. @@ -4138,12 +3668,12 @@ You can also use online editors and converters instead. - + Enable Mods... - + Delete %n save(s) @@ -4151,268 +3681,230 @@ You can also use online editors and converters instead. - + Restart Mod Organizer - + Mod Organizer must restart to finish configuration changes - + Restart - + Continue - + Some things might be weird. - + Can't change download directory while downloads are in progress! - - + + Set Priority - + Set the priority of the selected plugins - + Update available - + Do you want to endorse Mod Organizer on %1 now? - + Abstain from Endorsing Mod Organizer - + Are you sure you want to abstain from endorsing Mod Organizer 2? You will have to visit the mod page on the %1 Nexus site to change your mind. - + Thank you for endorsing MO2! :) - + Please reconsider endorsing MO2 on Nexus! - - None of your %1 mods appear to have had recent file updates. - - - - - All of your mods have been checked recently. We restrict update checks to help preserve your available API requests. - - - - + Thank you! - + Thank you for your endorsement! - + Mod ID %1 no longer seems to be available on Nexus. - + Request to Nexus failed: %1 - - + + failed to read %1: %2 - + Error - + failed to extract %1 (errorcode %2) - + Extract BSA - + This archive contains invalid hashes. Some files may be broken. - + Extract... - + <Multiple> - + Remove '%1' from the toolbar - + Enable all - + Disable all - + Unlock load order - + Lock load order - + Open Origin in Explorer - + Open Origin Info... - - Sorting plugins - - - - - Are you sure you want to sort your plugins list? - - - - + Backup of load order created - + Choose backup to restore - + No Backups - + There are no backups to restore - - + + Restore failed - - + + Failed to restore the backup. Errorcode: %1 - + Backup of mod list created - + A file with the same name has already been downloaded. What would you like to do? - + Overwrite - + Rename new file - + Ignore file - + Set the priority of the selected mods - - - - - - Browse Mod Page - - - - - <Edit...> - - - - - (no executables) - - MessageDialog @@ -4436,17 +3928,12 @@ You will have to visit the mod page on the %1 Nexus site to change your mind. - - The update check has found a mod with a Nexus ID and source game of %1, but this game is not a valid Nexus source. - - - - + All of your mods have been checked recently. We restrict update checks to help preserve your available API requests. - + You have mods that haven't been checked within the last month using the new API. These mods must be checked before we can use the bulk update API. This will consume significantly more API requests than usual. You will need to rerun the update check once complete in order to parse the remaining mods. @@ -4592,105 +4079,87 @@ Most mods do not have optional esps, so chances are good you are looking at an e - - - Files that exist in other mods but are overwritten by this mod - - - - - Winning file conflicts: + + The following conflicted files are provided by this mod - - - Files that are unused because they are overwritten by other mods + + The following conflicted files are provided by other mods - - Losing file conflicts: - - - - - - Files that have no conflicts - - - - + The following files have no conflicts - + Advanced - + Whether files that have no conflicts should be visible in the list - + Show files that have no conflicts - + Shows all mods overwriting or being overwritten by this mod - + Show all conflicting mods - + Shows only the nearest conflicting mods, in order of priority - + Show nearest conflicting mod - + Filter - + Categories - + Primary Category - + Nexus Info - + Mod ID - + Mod ID for this mod on Nexus. - + <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> <html><head><meta name="qrichtext" content="1" /><style type="text/css"> p, li { white-space: pre-wrap; } @@ -4699,22 +4168,22 @@ p, li { white-space: pre-wrap; } - + Source Game - + Source game for this mod. - + <html><head/><body><p>Source game for this mod. This determines where the mod was downloaded from and decides where to fetch info, version updates, and send endorsements. Changing this will likely require you to enter a new Mod ID.</p></body></html> - + <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> <html><head><meta name="qrichtext" content="1" /><style type="text/css"> p, li { white-space: pre-wrap; } @@ -4723,93 +4192,93 @@ p, li { white-space: pre-wrap; } - + Version - - + + Refresh - + Refresh all information from Nexus. - - + + Open in Browser - + Endorse - + Track - + about:blank - + Use Custom URL - + Notes - - - + + + Enter comments about the mod here. These are displayed in the notes column of the mod list. - + Set Color - + Reset Color - - - + + + Enter notes about the mod here. These can be viewed in the mod list by hovering over the notes column or the flags column. - + Filetree - + Open Mod in Explorer - + A directory view of this mod - + <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> <html><head><meta name="qrichtext" content="1" /><style type="text/css"> p, li { white-space: pre-wrap; } @@ -4819,17 +4288,17 @@ p, li { white-space: pre-wrap; } - + Previous - + Next - + Close @@ -4868,12 +4337,12 @@ p, li { white-space: pre-wrap; } ModInfoRegular - + %1 contains no esp/esm/esl and no asset (textures, meshes, interface, ...) directory - + Categories: <br> @@ -4889,273 +4358,273 @@ p, li { white-space: pre-wrap; } ModList - + This entry contains files that have been created inside the virtual data tree (i.e. by the construction kit) - + Backup - + Separator - + No valid game data - + Not endorsed yet - + This mod is for a different<br> game, make sure it's compatible or it could cause crashes. - + Mod is being tracked on the website - + Contains hidden files - + Overwrites loose files - + Overwritten loose files - + Loose files Overwrites & Overwritten - + Redundant - + Overwrites an archive with loose files - + Archive is overwritten by loose files - + Overwrites another archive file - + Overwritten by another archive file - + Archive files overwrites & overwritten - + Non-MO - + invalid - + installed version: "%1", newest version: "%2" - + The newest version on Nexus seems to be older than the one you have installed. This could either mean the version you have has been withdrawn (i.e. due to a bug) or the author uses a non-standard versioning scheme and that newest version is actually newer. Either way you may want to "upgrade". - + This file has been marked as "Old". There is most likely an updated version of this file available. - + This file has been marked as "Deleted"! You may want to check for an update or remove the nexus ID from this mod! - + %1 minute(s) and %2 second(s) - + This mod will be available to check in %2. - + Categories: <br> - + Invalid name - + Name is already in use by another mod - + drag&drop failed: %1 - + Confirm - + Are you sure you want to remove "%1"? - + Conflicts - + Flags - + Content - + Mod Name - + Version - + Priority - + Category - + Source Game - + Nexus ID - + Installation - + Notes - - + + unknown - + Name of your mods - + Version of the mod (if available) - + Installation priority of your mod. The higher, the more "important" it is and thus overwrites files from mods with lower priority. - + Primary category of the mod. - + The source game which was the origin of this mod. - + Id of the mod as used on Nexus. - + Indicators of file conflicts between mods. - + Emblems to highlight things that might require attention. - + Depicts the content of the mod: - + Time this mod was installed - + User notes about the mod @@ -5190,78 +4659,35 @@ p, li { white-space: pre-wrap; } - NexusConnectionUI + NexusInterface - - Connected. + + Please pick the mod ID for "%1" - - Not connected. + + You must authorize MO2 in Settings -> Nexus to use the Nexus API. - - Disconnected. + + You've exceeded the Nexus API rate limit and requests are now being throttled. Your next batch of requests will be available in approximately %1 minutes and %2 seconds. - - Checking API key... + + Aborting download: Either you clicked on a premium-only link and your account is not premium, or the download link was generated by a different account than the one stored in Mod Organizer. - - Received API key. + + empty response - - Received user account information - - - - - Linked with Nexus successfully. - - - - - Failed to set API key - - - - - NexusInterface - - - Please pick the mod ID for "%1" - - - - - You must authorize MO2 in Settings -> Nexus to use the Nexus API. - - - - - You've exceeded the Nexus API rate limit and requests are now being throttled. Your next batch of requests will be available in approximately %1 minutes and %2 seconds. - - - - - Aborting download: Either you clicked on a premium-only link and your account is not premium, or the download link was generated by a different account than the one stored in Mod Organizer. - - - - - empty response - - - - - invalid response + + invalid response @@ -5311,27 +4737,27 @@ p, li { white-space: pre-wrap; } NexusTab - + Current Version: %1 - + No update available - + Tracked - + Untracked - + <div style="text-align: center;"> <p>This mod does not have a valid Nexus ID. You can add a custom web @@ -5343,7 +4769,7 @@ p, li { white-space: pre-wrap; } NoConflictListModel - + File @@ -5351,211 +4777,211 @@ p, li { white-space: pre-wrap; } OrganizerCore - + File is write protected - + Invalid file format (probably a bug) - + Unknown error %1 - + Failed to write settings - + An error occurred trying to write back MO settings to %1: %2 - - + + Download started - + Download failed - - + + Installation cancelled - - + + Another installation is currently in progress. - - + + Installation successful - - + + Configure Mod - - + + This mod contains ini tweaks. Do you want to configure them now? - - + + mod not found: %1 - - + + Extraction cancelled - - + + The installation was cancelled while extracting files. If this was prior to a FOMOD setup, this warning may be ignored. However, if this was during installation, the mod will likely be missing files. - + file not found: %1 - + failed to generate preview for %1 - + Sorry - + Sorry, can't preview anything. This function currently does not support extracting from bsas. - + File '%1' not found. - + Failed to generate preview for %1 - + Failed to refresh list of esps: %1 - + Multiple esps/esls activated, please check that they don't conflict. - + You need to be logged in with Nexus - + Download? - + A download has been started but no installed page plugin recognizes it. If you download anyway no information (i.e. version) will be associated with the download. Continue? - - + + failed to update mod list: %1 - - + + login successful - + Login failed - + Login failed, try again? - + login failed: %1. Download will not be associated with an account - + login failed: %1 - + login failed: %1. You need to log-in with Nexus to update MO. - + MO1 "Script Extender" load mechanism has left hook.dll in your game folder - - + + Description missing - + <a href="%1">hook.dll</a> has been found in your game folder (right click to copy the full path). This is most likely a leftover of setting the ModOrganizer 1 load mechanism to "Script Extender", in which case you must remove this file either by changing the load mechanism in ModOrganizer 1 or manually removing the file, otherwise the game is likely to crash and burn. - + failed to save load order: %1 - + Error - + The designated write target "%1" is not enabled. @@ -5563,12 +4989,12 @@ Continue? OverwriteConflictListModel - + File - + Overwritten Mods @@ -5668,12 +5094,12 @@ Continue? OverwrittenConflictListModel - + File - + Providing Mod @@ -5681,48 +5107,18 @@ Continue? PluginContainer - - Plugin error - - - - - Mod Organizer failed to load the plugin '%1' last time it was started. - - - - - The plugin can be skipped for this session, blacklisted, or loaded normally, in which case it might fail again. Blacklisted plugins can be re-enabled later in the settings. - - - - - Skip this plugin - - - - - Blacklist this plugin - - - - - Load this plugin - - - - + Some plugins could not be loaded - - + + Description missing - + The following plugins could not be loaded. The reason may be missing dependencies (i.e. python) or an outdated version: @@ -5781,108 +5177,108 @@ Continue? - + Plugin not found: %1 - - + + Confirm - + Really enable all plugins? - + Really disable all plugins? - + The file containing locked plugin indices is broken - + Origin - + This plugin can't be disabled (enforced by the game). - + Author - + Description - + Missing Masters - + Enabled Masters - + Loads Archives - + There are Archives connected to this plugin. Their assets will be added to your game, overwriting in case of conflicts following the plugin order. Loose files will always overwrite assets from Archives. (This flag only checks for Archives from the same mod as the plugin) - + Loads INI settings - + There is an ini file connected to this plugin. Its settings will be added to your game settings, overwriting in case of conflicts. - + This ESP is flagged as an ESL. It will adhere to the ESP load order but the records will be loaded in ESL space. - + Incompatible with %1 - + Depends on missing %1 - + Warning - + Error - + failed to restore load order for %1 @@ -5895,49 +5291,6 @@ Continue? - - PluginTypeName - - - Diagnose - - - - - Game - - - - - Installer - - - - - Mod Page - - - - - Preview - - - - - Tool - - - - - Proxy - - - - - File Mapper - - - PreviewDialog @@ -5973,18 +5326,18 @@ p, li { white-space: pre-wrap; } - - + + Fix - + No guided fix - + (There are no notifications) @@ -6051,12 +5404,12 @@ p, li { white-space: pre-wrap; } - + Missing profile-specific game INI files! - + Some of your profile-specific game INI files were missing. They will now be copied from the vanilla game folder. You might want to double-check your settings. Missing files: @@ -6064,12 +5417,12 @@ Missing files: - + Delete profile-specific game INI files? - + Do you want to delete the profile-specific game INI files? (If you select "No", the INI files will be used again if you re-enable profile-specific game INI files.) @@ -6234,93 +5587,88 @@ p, li { white-space: pre-wrap; } - + Archive invalidation isn't required for this game. - - This game does not support profile-specific game saves. - - - - - + + failed to create profile: %1 - + Name - + Please enter a name for the new profile - + failed to copy profile: %1 - + Invalid name - + Invalid profile name - + Deleting active profile - + Unable to delete active profile. Please change to a different profile first. - + Confirm - + Are you sure you want to remove this profile (including profile-specific save games, if any)? - + Profile broken - + This profile you're about to delete seems to be broken or the path is invalid. I'm about to delete the following folder: "%1". Proceed? - + Rename Profile - + New Name - + failed to change archive invalidation state: %1 - + failed to determine if invalidation is active: %1 @@ -6328,78 +5676,79 @@ p, li { white-space: pre-wrap; } QObject - + Filter - - + + INI file is read-only - + Mod Organizer is attempting to write to "%1" which is currently set to read-only. - + Clear the read-only flag - + Allow the write once - + The file will be set to read-only again. - + Skip this file - + + + + - - - + Error - + You can reset these choices by clicking "Reset Dialog Choices" in the General tab of the Settings - + Always ask - - + + Remember my choice - + Remember my choice for %1 @@ -6409,84 +5758,84 @@ p, li { white-space: pre-wrap; } - + removal of "%1" failed: %2 - + removal of "%1" failed - + "%1" doesn't exist (remove) - + Error %1 - - + + failed to create directory "%1" - - + + failed to copy "%1" to "%2" - + %1 B - + %1 MB - + %1 GB - + %1 TB - + %1 GB/s - + %1 TB/s - + %1 KB - + %1 B/s - + %1 KB/s - + %1 MB/s @@ -6501,8 +5850,8 @@ p, li { white-space: pre-wrap; } - + None @@ -6692,36 +6041,137 @@ Destination: - - - Portable + + Deleting instance folder + + + + + This will delete the instance folder. + The folder will be deleted: "%1" + + + + + Move the folder to the recycle bin + + + + + Delete the folder permanently + + + + + Could not delete instance folder "%1". + +%2 + + + + + Select an instance to delete + + + + + Deleting an instance will delete all the mods, downloads, profiles (including profile-specific saves) and anything in the overwrite folder.<br><br>Custom paths outside of the instance folder will not be deleted. + + + + + Enter a Name for the new Instance + + + + + Enter a new name or select one from the suggested list: +(This is just a name for the Instance and can be whatever you wish, + the actual game selection will happen on the next screen regardless of chosen name) + + + + + + Canceled + + + + + Invalid instance name + + + + + The instance name "%1" is invalid. Use the name "%2" instead? + + + + + The instance "%1" already exists. + + + + + Please choose a different instance name, like: "%1 1" . + + + + + Choose Instance + + + + + Each Instance is a full set of MO data files (mods, downloads, profiles, configuration, ...). You can use multiple instances for different games. Instances are stored in Appdata and can be accessed by all MO installations. If your MO folder is writable, you can also store a single instance locally (called a Portable install, and all the MO data files will be inside the installation folder). + + + + + New - - Cannot open instance '%1', failed to read INI file %2. + + Create a new instance. - - Cannot open instance '%1', the managed game was not found in the INI file %2. Select the game managed by this instance. + + Portable + + + + + Use MO folder for data. - - Cannot open instance '%1', the game plugin '%2' doesn't exist. It may have been deleted by an antivirus. Select another instance. + + Manage Instances - - Cannot open instance '%1', the game directory '%2' doesn't exist or the game plugin '%3' doesn't recognize it. Select the game managed by this instance. + + Delete an Instance. + failed to create %1 + + + Data directory created + + + + + New data directory created at %1. If you don't want to store a lot of data there, reconfigure the storage directories via settings. + + General messages @@ -6758,9 +6208,9 @@ Destination: + - Warning @@ -6810,51 +6260,112 @@ Destination: - + Failed to create "%1". Your user account probably lacks permission. - + + Plugin to handle %1 no longer installed. An antivirus might have deleted files. + Plugin to handle %1 no longer installed + + + + + + + The configured path to the game directory (%1) appears to be a symbolic (or other) link. This setup is incompatible with MO2's VFS and will not run correctly. + + + + + Could not use configuration settings for game "%1", path "%2". + + + + + + + Please select the installation of %1 to manage + + + + + + + Please select the game to manage + + + + + Canceled finding %1 in "%2". + + + + + Canceled finding game in "%1". + + + + + %1 not identified in "%2". The directory is required to contain the game binary. + + + + + No game identified in "%1". The directory is required to contain the game binary.<br><br><b>These are the games supported by Mod Organizer:</b><ul>%2</ul> + + + + + Please select the game edition you have (MO can't start the game correctly if this is set incorrectly!) + + + + failed to start shortcut: %1 - + failed to start application: %1 - + - Mod Organizer - + An instance of Mod Organizer is already running + + + Failed to set up instance + + <Unmanaged> - + Please use "Help" from the toolbar to get usage instructions to all elements - - + + <Manage...> - + failed to parse profile %1: %2 @@ -6880,174 +6391,185 @@ Destination: - + Save changes? - + Save changes to "%1"? - + Connecting to Nexus... - + Waiting for Nexus... - + Opened Nexus in browser. - + Switch to your browser and accept the request. - + Finished. - + No answer from Nexus. - - + + A firewall might be blocking Mod Organizer. - + Nexus closed the connection. - + Cancelled. - + Failed to request %1 - - + + Cancelled - + Internal error - + HTTP code %1 - + Invalid JSON - + Bad response - + API key is empty - + SSL error - + Timed out - + One of the configured MO2 directories (profiles, mods, or overwrite) is on a path containing a symbolic (or other) link. This is likely to be incompatible with MO2's virtual filesystem. - + failed to initialize plugin %1: %2 - + + Plugin error + + + + + It appears the plugin "%1" failed to load last startup and caused MO to crash. Do you want to disable it? +(Please note: If this is the first time you see this message for this plugin you may want to give it another try. The plugin may be able to recover from the problem) + + + + failed to access %1 - + failed to set file time %1 - - - + + + No profile set - + Before you can use ModOrganizer, you need to create at least one profile. ATTENTION: Run the game at least once before creating a profile! - + '%1': file is blocked (%2) - + '%1' seems to be missing, an antivirus may have deleted it - + %1 is loaded. This program is known to cause issues with Mod Organizer, such as freezing or blank windows. Consider uninstalling it. - + %1 is loaded. This program is known to cause issues with Mod Organizer and its virtual filesystem, such script extenders refusing to run. Consider uninstalling it. - - - + + + attempt to store setting for unknown plugin "%1" - + Failed - + Failed to start the helper application: %1 @@ -7084,17 +6606,60 @@ This program is known to cause issues with Mod Organizer, such as freezing or bl - + Confirm? - + This will reset all the choices you made to dialogs and make them all visible again. Continue? - + + Connected. + + + + + Not connected. + + + + + Disconnected. + + + + + Checking API key... + + + + + Received API key. + + + + + Received user acount information + + + + + Linked with Nexus successfully. + + + + + Failed to set API key + + + + + + + @@ -7102,34 +6667,30 @@ This program is known to cause issues with Mod Organizer, such as freezing or bl - - - - - + Cancel - - - + + + Enter API Key Manually - - - + + + Connect to Nexus - - - - - + + + + + N/A @@ -7486,219 +7047,6 @@ You can restart Mod Organizer as administrator and try launching the program aga Disabled because - - - One of the following plugins must be enabled: %1. - - - - - This plugin can only be enabled if the '%1' plugin is installed and enabled. - - - - - This plugin can only be enabled for the following game(s): %1. - - - - - - - - Cannot disable plugin - - - - - The '%1' plugin is used by the current game plugin and cannot disabled. - - - - - <p>Disabling the '%1' plugin will also disable the following plugins:</p><ul>%1</ul><p>Do you want to continue?</p> - - - - - Really disable plugin? - - - - - This plugin is required for Mod Organizer to work properly and cannot be disabled. - - - - - Instance at '%1' not found. Select another instance. - - - - - Instance at '%1' not found. You must create a new instance - - - - - The instance name must be a valid folder name. - - - - - An instance with this name already exists. - - - - - Disabling the '%1' plugin will prevent the following %2 plugin(s) from working: - - - - - - - - Global - - - - - - Instance type: %1 - - - - - ? - - - - - Find game installation for %1 - - - - - Find game installation - - - - - - - Unrecognized game - - - - - The folder %1 does not seem to contain a game Mod Organizer can manage. - - - - - See details for the list of supported games. - - - - - No installation found - - - - - Browse... - - - - - The folder must contain a valid game installation - - - - - The folder %1 does not seem to contain an installation for <span style="white-space: nowrap; font-weight: bold;">%2</span> or for any other game Mod Organizer can manage. - - - - - - Use this folder for %1 - - - - - - I know what I'm doing - - - - - - Incorrect game - - - - - The folder %1 seems to contain an installation for <span style="white-space: nowrap; font-weight: bold;">%2</span>, not <span style="white-space: nowrap; font-weight: bold;">%3</span>. - - - - - Manage %1 instead - - - - - Instance location: %1 - - - - - Instance name: %1 - - - - - - Base directory: %1 - - - - - Downloads - - - - - Mods - - - - - Profiles - - - - - Overwrite - - - - - Game: %1 - - - - - Game location: %1 - - - - - Creating %1 - - QueryOverwriteDialog @@ -7817,58 +7165,58 @@ p, li { white-space: pre-wrap; } SelfUpdater - + New update available (%1) - + Do you want to install update? All your mods and setup will be left untouched. Select Show Details option to see the full change-log. - + Install - + Download failed - + Failed to find correct download, please try again later. - + Update - + Download in progress - + Download failed: %1 - + Failed to install update: %1 - + Failed to start %1: %2 - + Error @@ -8226,32 +7574,32 @@ Select Show Details option to see the full change-log. - + Disable automatic internet features - + Disable automatic internet features. This does not affect features that are explicitly invoked by the user (like checking mods for updates, endorsing, opening the web browser) - + Offline Mode - + Use a proxy for network connections. - + Use a proxy for network connections. This uses the system-wide settings which can be configured in Internet Explorer. Please note that MO will start up a few seconds slower on some systems when using a proxy. - + Use HTTP Proxy (Uses System Settings) @@ -8272,117 +7620,102 @@ Select Show Details option to see the full change-log. - - Tracked Integration - - - - + Associate with "Download with manager" links - + Remove cache and cookies. - + Clear Cache - + Known Servers (updated on download) - + Preferred Servers (Drag & Drop) - + Steam - + Username - + <html><head/><body><p>If you save your steam user ID and password here, they will be used when logging into steam.</p></body></html> - + Password - + Plugins - + Author: - + Version: - + Description: - - Enabled - - - - + Key - + Value - - No plugin found. - - - - + Blacklisted Plugins (use <del> to remove): - + Workarounds - + Steam App ID - + The Steam AppID for your game - + <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> <html><head><meta name="qrichtext" content="1" /><style type="text/css"> p, li { white-space: pre-wrap; } @@ -8398,17 +7731,17 @@ p, li { white-space: pre-wrap; } - + Load Mechanism - + Select loading mechanism. See help for details. - + Mod Organizer needs a dll to be injected into the game so all mods are visible to it. There are several means to do this: *Mod Organizer* (default) In this mode the Mod Organizer itself injects the dll. The disadvantage is that you always have to start the game through MO or a link created by it. @@ -8419,28 +7752,28 @@ If you use the Steam version of Oblivion the default will NOT work. In this case - + Enforces that inactive ESPs and ESMs are never loaded. - + It seems that the Games occasionally load ESP or ESM files even if they haven't been activated as plugins. I don't yet know what the circumstances are, but user reports imply it is in some cases unwanted. If this is checked, ESPs and ESMs not checked in the List are invisible to the game and can not be loaded. - + Hide inactive ESPs/ESMs - + Disable this to no longer display mods installed outside MO in the mod list (left pane). Assets from those mods will then be treated as having lowest mod priority together with the original game content. - + By default Mod Organizer will display esp+bsa bundles installed with foreign tools as mods (left pane). This allows you to control their priority in relation to other mods. This is particularly useful if you also use Steam Workshop to install mods. However, if you installed loose file mods outside MO which conflict with BSAs also installed outside MO those conflicts can't be resolved correctly. @@ -8448,66 +7781,66 @@ If you disable this feature, MO will only display official DLCs this way. Please - + Display mods installed outside MO - + If checked, files (i.e. esps, esms and bsas) belonging to the core game can not be disabled in the UI. (default: on) - + If checked, files (i.e. esps, esms and bsas) belonging to the core game can not be disabled in the UI. (default: on) Uncheck this if you want to use Mod Organizer with total conversions (like Nehrim) but be aware that the game will crash if required files are not enabled. - + Force-enable game files - - + + Disable this to prevent the GUI from being locked when running an executable. This may result in abnormal behavior. - + Lock GUI when running executable - + Enable parsing of Archives. This is an Experimental Feature. Has negative effects on performance and known incorrectness. - + <html><head/><body><p>By default, MO will parse archive files (BSA, BA2) to calculate conflicts between the contents of the archive files and other loose files. This process has a noticeable cost in performance.</p><p>This feature should not be confused with the archive management feature offered by MO1. MO2 will only show conflicts with archives and will NOT load them into the game or program.</p><p>If you disable this feature, MO will only display conflicts between loose files.</p></body></html> - + Enable parsing of Archives (Experimental Feature) - - + + For Skyrim, this can be used instead of Archive Invalidation. It should make AI redundant for all Profiles. For the other games this is not a sufficient replacement for AI! - + Back-date BSAs - + Add executables to the blacklist to prevent them from accessing the virtual file system. This is useful to prevent unintended programs from being hooked. Hooking unintended @@ -8516,43 +7849,43 @@ programs you are intentionally running. - + Add executables to the blacklist to prevent them from accessing the virtual file system. This is useful to prevent unintended programs from being hooked. Hooking unintended programs may affect the execution of these programs or the programs you are intentionally running. - + Configure Executables Blacklist - - + + Resets the window geometries for all windows. This can be useful if a window becomes too small or too large, if a column becomes too thin or too wide, and in similar situations. - + Reset Window Geometries - + These are workarounds for problems with Mod Organizer. Please make sure you read the help text before changing anything here. - + Diagnostics - + Hint: right click link and copy link location - + Logs and crash dumps are stored under your current instance in the <a href="LOGS_FULL_PATH">LOGS_DIR</a> and <a href="DUMPS_FULL_PATH">DUMPS_DIR</a> folders. @@ -8562,17 +7895,17 @@ programs you are intentionally running. - + Log Level - + Decides the amount of data printed to "ModOrganizer.log" - + Decides the amount of data printed to "ModOrganizer.log". "Debug" produces very useful information for finding problems. There is usually no noteworthy performance impact but the file may become rather large. If this is a problem you may prefer the "Info" level for regular use. On the "Error" level the log file usually remains empty. @@ -8580,17 +7913,17 @@ programs you are intentionally running. - + Crash Dumps - + Decides which type of crash dumps are collected when injected processes crash. - + Decides which type of crash dumps are collected when injected processes crash. "None" Disables the generation of crash dumps by MO. @@ -8601,17 +7934,17 @@ programs you are intentionally running. - + Max Dumps To Keep - + Maximum number of crash dumps to keep on disk. Use 0 for unlimited. - + Maximum number of crash dumps to keep on disk. Use 0 for unlimited. Set "Crash Dumps" above to None to disable crash dump collection. @@ -8619,7 +7952,7 @@ programs you are intentionally running. - + LOOT Log Level @@ -8634,6 +7967,29 @@ programs you are intentionally running. + + SingleInstance + + + SHM error: %1 + + + + + failed to connect to running instance: %1 + + + + + failed to communicate with running instance: %1 + + + + + failed to receive data from secondary instance: %1 + + + StatusBar @@ -8641,6 +7997,11 @@ programs you are intentionally running. Unknown game + + + Portable + + SyncOverwriteDialog @@ -8675,14 +8036,6 @@ programs you are intentionally running. - - T - - - Plugin - - - TaskDialog @@ -8807,22 +8160,22 @@ On Windows XP: - + Characters for profile %1 - + Overwrite - + Overwrite the file "%1" - + Confirm @@ -8830,7 +8183,7 @@ On Windows XP: UsvfsConnector - + Preparing vfs @@ -8844,7 +8197,7 @@ On Windows XP: - + Connecting to Nexus... @@ -8859,9 +8212,557 @@ On Windows XP: - + Trying again... + + tutorial_conflictresolution_main + + + There are multiple types of conflicts you may encounter when dealing with Mods. This tutorial will try to cover and explain how to deal with all of them. + + + + + First up are file conflicts. These occur when two mods contain the same file. Most commonly this happens when several mods replace the same standard asset from the game, like the texture of an armor. + + + + + As an example, say you install "ModA" which contains stylish new iron and leather armor. Then you install "ModB" which contains sexy ebony and leather armor. Obviously there is a conflict now: which leather armor to use? + + + + + If you were to install the mods manually, when installing "ModB" you would be asked if you want to overwrite conflicting files. If you choose yes, you get the leather armor from "ModB" otherwise you keep the one from "ModA". If you later decide you made the wrong choice, you have to reinstall one of the mods. + + + + + With MO, both ModA and ModB get installed completely (no overwrite dialog) and by default "ModB" gets to provide the leather armor because it's automatically assigned the higher priority. + + + + + However, you can change the mod priority at any time using drag&drop on this list. If you assign "ModA" a higher priority, it provides the leather armor, no re-installation required. Since the priorities of mods in this list are treated as if the mods were installed in that order, I tend to talk about "installation order". + + + + + If the "Flags"-column is enabled in the mod list, it will show you which mods are involved in a conflict and how... + + + + + <img src="qrc:///MO/gui/emblem_conflict_overwrite" /> indicates that the mod overwrites files that are also available in another mod. + + + + + <img src="qrc:///MO/gui/emblem_conflict_overwritten" /> indicates that the mod is <b>partially</b> overwritten by another. + + + + + <img src="qrc:///MO/gui/emblem_conflict_mixed" /> indicates that both of the above is true. + + + + + <img src="qrc:///MO/gui/emblem_conflict_redundant" /> indicates that the mod is completely overwritten by another. You could as well disable it. + + + + + There are two ways to see the individual files involved in a conflict: + + + + + Option A: Switch to the "Data"-tab if necessary + + + + + ... here, if you mark the highlighted control, the tree will only display files in conflict. In the right column, it says which mod currently provides the mod (because it has highest priority) and if you hover your mouse over that info, it will list which other mods contains it. + + + + + Option B: Open the <i>Information</i>-Dialog of an <b>enabled</b> mod you're interested in by either double-clicking it or selecting <i>Information...</i> from the right-click menu + + + + + This was everything to know about file conflicts. The second type of conflict we have to deal with are "record conflicts". + + + + + I told you in the "First Steps" tutorial how the esp/esm/esl plugins contain changes to the game world like modifications to the terrain or existing NPCs. Each change like this is stored in a record, hence the name "record conflict". For example when two mods try to change the same location, only one change can become active. + + + + + As with file conflicts you can't really fix these conflicts, you have to choose which change you want. This time around however, if you choose wrong, your game may become unstable because there may be dependencies between the records of a mod. + + + + + Please open the "Plugins"-tab... + + + + + Again you can use drag&drop to change priorities of plugins, thus deciding which plugin takes precedence in regards to conflicts. This is commonly called the "load order". But how do you know how to order the plugins? + + + + + Unlike with file conflicts, MO does not provide help on finding conflicts. The good news is, there already is a perfect tool for that called LOOT. LOOT is available on the Nexus and integrates neatly with MO. Basically, if you don't have LOOT yet, install it once this tutorial is over. + + + + + After you installed LOOT in the default location (follow its instructions), start MO again and LOOT should automatically appear as an Executable... + + + + + When you run LOOT, it will automatically re-organize plugins for best compatibility (overwriting your manual changes). It will also open a report in your browser that warns about incompatibilities. You should read the report, at least for new mods. + + + + + The final type of conflicts are also "record conflicts". Like the previous type. It's confusing, so I'll just call them "lists conflicts" instead. The difference is the types of records in conflict. The ones in question here can be merged so you may be able to get all modifications in your game. + + + + + One common example of such records are leveled lists that contain all the items that may spawn at a specific character level. Traditionally, if multiple mods add items to such a list, only one is in effect... + + + + + ... but there are tools to merge those mods so you can have the effects of all of them. Again, this functionality is not integrated with MO because there are already great tools. For Oblivion and Skyrim look for wrye bash, for fallout 3/nv it's wrye flash. For Skyrim there is also "SkyBash". All of these can create a so-called "bashed patch" which is a plugin that contains the combined mergeable records from all your mods. + + + + + This completes the tutorial. + + + + + tutorial_conflictresolution_modinfo + + + Please switch to the "Conflicts"-Tab. + + + + + Here you can see a list of files in this mod that out-prioritize others in a file conflict and one with files where this mod is overridden. + + + + + Please close the information dialog again. + + + + + tutorial_firststeps_browser + + + This is a fully featured browser that is set up to open the correct nexus page for your game. You can download any mod using the "DOWNLOAD WITH MANAGER"-button or the "manual"-link and it will be downloaded by MO. + + + + + tutorial_firststeps_main + + + Welcome to the ModOrganizer Tutorial! This will guide you through the most common features of MO. +Please go along with the tutorial because some things can't be demonstrated if you skip something. + + + + + Before we continue with the step-by-step tutorial, I'd like to tell you about the other ways you can receive help on ModOrganizer. + + + + + The highlighted button provides hints on solving potential problems MO recognized automatically. + + + + + +There IS a notification now but you may want to hold off on clearing it until after completing the tutorial. + + + + + This button provides multiple sources of information and further tutorials. + + + + + Finally there are tooltips on almost every part of Mod Organizer. If there is a control in MO you don't understand, please try hovering over it to get a short description or use "Help on UI" from the help menu to get a longer explanation + + + + + This list displays all mods installed through MO. It also displays installed DLCs and some mods installed outside MO but you have limited control over those in MO. + + + + + Before we start installing mods, let's have a quick look at the settings. + + + + + Now it's time to install a few mods!Please go along with this because we need a few mods installed to demonstrate other features + + + + + There are a few ways to get mods into ModOrganizer. If you associated MO with NXM links in the settings you can now use your regular browser to send downloads from Nexus to MO. Click on "Nexus" to open nexus, find a mod and click the green download buttons on Nexus saying "Download with Manager". + + + + + You can also install mods from disk using the "Install Mod" button. + + + + + Downloads will appear on the "Downloads"-tab here. You have to download and install at least one mod to proceed. + + + + + Great, you just installed your first mod. Please note that the installation procedure may differ based on how a mod was packaged. + + + + + Now you know all about downloading and installing mods but they are not enabled yet... + + + + + Install a few more mods if you want, then enable mods by checking them in the left pane. Mods that aren't enabled have no effect on the game whatsoever. + + + + + For some mods, enabling it on the left pane is all you have to do... + + + + + ...but most contain plugins. These are plugins for the game and are required to add stuff to the game (new weapons, armors, quests, areas, ...). Please open the "Plugins"-tab to get a list of plugins. + + + + + You will notice some plugins are grayed out. These are part of the main game and can't be disabled. + + + + + A single mod may contain zero, one or multiple esps. Some or all may be optional. If in doubt, please consult the documentation of the individual mod. To do so, right-click the mod and select "Information". + + + + + Now you know how to download, install and enable mods. +It's important you always start the game from inside MO, otherwise the mods you installed here won't work. + + + + + This combobox lets you choose <i>what</i> to start. This way you can start the game, Launcher, Script Extender, the Creation Kit or other tools. If you miss a tool you can also configure this list but that is an advanced topic. + + + + + This completes the basic tutorial. As homework go play a bit! After you have installed more mods you may want to read the tutorial on conflict resolution. + + + + + tutorial_firststeps_modinfo + + + This dialog tries to expose as much information about a mod as possible. Depending on the mod this may include readmes, screenshots, optional plugins and so on. If a certain type of information was not found in a mod, the corresponding tab is grayed out. + + + + + If you installed the mod from Nexus, the corresponding tab should give you direct access to the mod page. + + + + + We may revisit this screen in later tutorials. + + + + + tutorial_firststeps_settings + + + You can use your regular browser to download from Nexus. +Please open the "Nexus"-tab + + + + + Click this button so that "DOWNLOAD WITH MANAGER"-buttons are download with Mod Organizer. + + + + + Use this interface to obtain an API key from NexusMods. This is used for all API connections - downloads, updates etc. MO2 uses the Windows Credential Manager to store this data securely. If the SSO page on Nexus is failing, use the manual entry and copy the API key from your profile. + + + + + tutorial_primer_main + + + This window shows all the mods that are installed. The column headers can be used for sorting. Only checked mods are active in the current profile. + + + + + Each profile is a separate set of enabled mods and ini settings. + + + + + Perform various actions on your mod list, such as refreshing data and checking for mod updates. + + + + + Quick access to various directories, such as your MO2 mods, profiles, saves, and your active game location. + + + + + Restore a mod list backup. + + + + + Create a backup of your current mod list. + + + + + Running counter of your active mods. Hover to see a more detailed breakdown. + + + + + The dropdown allows various ways of grouping the mods shown in the mod list. + + + + + Show/hide the category pane. + + + + + Quickly filter the mod list as you type. + + + + + Switch between information views. + + + + + This shows mod categories and some meta categories (in angle-brackets). Select some to filter the mod list. For example select "<Checked>" to show only active mods. + + + + + Customizable list for choosing the program to run. + + + + + When this button is clicked, Mod Organizer creates a virtual directory structure then runs the program selected to the left. + + + + + Will create a shortcut for quick access. The shortcut can be placed in the toolbar at the top, in the Start Menu or on the Windows Desktop. + + + + + Log messages produced by MO. Please note that messages with a light bulb usually don't require your attention. + + + + + Indicator of your current NexusMods API request limits. + + + + + Change/manage MO2 instances or switch to portable mode. + + + + + Browse to and manually install a mod from an archive on your computer. + + + + + Automatically open NexusMods to browse and install mods via the API. + + + + + Manage your MO2 profiles. + + + + + Open the executable editor to add and modify applications you wish to run with MO2. + + + + + Select from a collection of additional tools, such as an INI editor, integrated FNIS updater, and more. + + + + + Configure Mod Organizer. + + + + + See the status of and/or endorse MO2 on NexusMods. + + + + + Notifications about the current setup. + + + + + Activates if there is an update for MO. Please note that if, for any reason, MO can't communicate with NMM, this will not work either. + + + + + Access more information about MO2, including these tutorials, a link to the development discord, information about the devs and dependencies. + + + + + Plugins (esp/esm/esl files) of the mods in the current profile. They need to be checked to be loaded. + + + + + Automatically sort plugins using the bundled LOOT application. + + + + + Restore a backup of your plugin list order. + + + + + Save a backup of your plugin list order. + + + + + Counter of your total active plugins. Hover to see a breakdown of plugin types. + + + + + Quickly filter plugin list as you type. + + + + + All the asset archives (.bsa files) for all active mods. + + + + + The directory tree and all files that the program will see. + + + + + Save game browser. Shows all the saves for the current profile and whether or not the current mod-load status is correct. + + + + + Shows the mods that have been downloaded and if they’ve been installed. + + + + + Click to quit + + + + + tutorial_window_installer + + + This mod has been packaged in a way that Mod Organizer did not automatically recognize... + + + + + You can use drag&drop on this list to fix the structure of the mod. + + + + + The correct structure replicates the data-directory of the game. That means esps, the "meshes"- or "textures"-directory and so on should be directly below "<data>". + + + + + You can also disable files and directories that you don't want to unpack. + + + + + From the context menu (right-click) you can open textfiles, in case you want to access a readme. + + + + + This text will turn green if MO thinks the structure looks good. + + + -- cgit v1.3.1 From a0229380ce08715198a1181b61c2f357ad641ca8 Mon Sep 17 00:00:00 2001 From: Al <26797547+Al12rs@users.noreply.github.com> Date: Thu, 26 Nov 2020 15:33:49 +0100 Subject: Refresh after waiting for application (vfs) --- src/organizerproxy.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/organizerproxy.cpp b/src/organizerproxy.cpp index c2283acc..06e932cb 100644 --- a/src/organizerproxy.cpp +++ b/src/organizerproxy.cpp @@ -155,7 +155,7 @@ bool OrganizerProxy::waitForApplication(HANDLE handle, LPDWORD exitCode) const auto runner = m_Proxied->processRunner(); const auto r = runner - .setWaitForCompletion(ProcessRunner::ForceWait, UILocker::OutputRequired) + .setWaitForCompletion(ProcessRunner::ForceWait | ProcessRunner::Refresh, UILocker::OutputRequired) .attachToProcess(handle); if (exitCode) { -- cgit v1.3.1 From 5dd9bbe33aa962cf796f039dcf77680cb11e2c27 Mon Sep 17 00:00:00 2001 From: isanae <14251494+isanae@users.noreply.github.com> Date: Fri, 27 Nov 2020 07:34:00 -0500 Subject: Revert "Changing the location of several directories" --- src/CMakeLists.txt | 10 ++++------ src/dlls.manifest | 29 ----------------------------- src/dlls.manifest.qt5 | 29 +++++++++++++++++++++++++++++ src/mainwindow.cpp | 11 +++-------- src/qt.conf | 6 +----- src/settingsdialoggeneral.cpp | 6 +++--- src/shared/appconfig.inc | 3 +-- src/tutorials/TutorialOverlay.qml | 4 +--- 8 files changed, 42 insertions(+), 56 deletions(-) delete mode 100644 src/dlls.manifest create mode 100644 src/dlls.manifest.qt5 (limited to 'src') diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index ffaf3c0c..a53d845d 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -232,17 +232,15 @@ target_include_directories(${CMAKE_PROJECT_NAME} PRIVATE ) -install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/dlls.manifest +install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/dlls.manifest.qt5 DESTINATION bin/dlls RENAME dlls.manifest) -install(FILES ${qm_files} DESTINATION bin/resources/translations) +install(FILES ${qm_files} DESTINATION bin/translations) install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/stylesheets - DESTINATION bin) - -install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/tutorials - DESTINATION bin/resources) + ${CMAKE_CURRENT_SOURCE_DIR}/tutorials + DESTINATION bin) deploy_qt(BINARIES ModOrganizer.exe uibase.dll plugins/bsa_packer.dll) diff --git a/src/dlls.manifest b/src/dlls.manifest deleted file mode 100644 index 846a02ae..00000000 --- a/src/dlls.manifest +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/dlls.manifest.qt5 b/src/dlls.manifest.qt5 new file mode 100644 index 00000000..cae74df1 --- /dev/null +++ b/src/dlls.manifest.qt5 @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index c2aefdd2..cdc048fd 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -1206,9 +1206,7 @@ void MainWindow::createHelpMenu() ActionList tutorials; - QString tutorialPath = QApplication::applicationDirPath() - + "/" + QString::fromStdWString(AppConfig::tutorialsPath()) + "/"; - QDirIterator dirIter(tutorialPath, QStringList("*.js"), QDir::Files); + QDirIterator dirIter(QApplication::applicationDirPath() + "/tutorials", QStringList("*.js"), QDir::Files); while (dirIter.hasNext()) { dirIter.next(); QString fileName = dirIter.fileName(); @@ -1320,8 +1318,7 @@ bool MainWindow::addProfile() void MainWindow::hookUpWindowTutorials() { - QString tutorialPath = QApplication::applicationDirPath() + "/" + QString::fromStdWString(AppConfig::tutorialsPath()) + "/"; - QDirIterator dirIter(tutorialPath, QStringList("*.js"), QDir::Files); + QDirIterator dirIter(QApplication::applicationDirPath() + "/tutorials", QStringList("*.js"), QDir::Files); while (dirIter.hasNext()) { dirIter.next(); QString fileName = dirIter.fileName(); @@ -5229,9 +5226,7 @@ void MainWindow::installTranslator(const QString &name) { QTranslator *translator = new QTranslator(this); QString fileName = name + "_" + m_CurrentLanguage; - QString translationsPath = qApp->applicationDirPath() - + "/" + QString::fromStdWString(AppConfig::translationsPath()); - if (!translator->load(fileName, translationsPath)) { + if (!translator->load(fileName, qApp->applicationDirPath() + "/translations")) { if (m_CurrentLanguage.contains(QRegularExpression("^.*_(EN|en)(-.*)?$"))) { log::debug("localization file %s not found", fileName); } // we don't actually expect localization files for English (en, en-us, en-uk, and any variation thereof) diff --git a/src/qt.conf b/src/qt.conf index 5e070fa3..f834a22a 100644 --- a/src/qt.conf +++ b/src/qt.conf @@ -1,7 +1,3 @@ [Paths] -Libraries=dlls -LibraryExecutables=resources +Prefix=. Plugins=dlls -Imports=dlls -Qml2Imports=dlls -Translations=resources/translations \ No newline at end of file diff --git a/src/settingsdialoggeneral.cpp b/src/settingsdialoggeneral.cpp index 9eae2ba1..f29e1d24 100644 --- a/src/settingsdialoggeneral.cpp +++ b/src/settingsdialoggeneral.cpp @@ -81,9 +81,9 @@ void GeneralSettingsTab::addLanguages() const QRegExp exp(pattern); - QString translationsPath = qApp->applicationDirPath() - + "/" + QString::fromStdWString(AppConfig::translationsPath()); - QDirIterator iter(translationsPath, QDir::Files); + QDirIterator iter( + QCoreApplication::applicationDirPath() + "/translations", + QDir::Files); std::vector> languages; diff --git a/src/shared/appconfig.inc b/src/shared/appconfig.inc index 8d4125cf..5839c2f4 100644 --- a/src/shared/appconfig.inc +++ b/src/shared/appconfig.inc @@ -6,8 +6,7 @@ APPPARAM(std::wstring, downloadPath, L"downloads") APPPARAM(std::wstring, overwritePath, L"overwrite") APPPARAM(std::wstring, stylesheetsPath, L"stylesheets") APPPARAM(std::wstring, cachePath, L"webcache") -APPPARAM(std::wstring, tutorialsPath, L"resources/tutorials") -APPPARAM(std::wstring, translationsPath, L"resources/translations") +APPPARAM(std::wstring, tutorialsPath, L"tutorials") APPPARAM(std::wstring, logPath, L"logs") APPPARAM(std::wstring, dumpsDir, L"crashDumps") APPPARAM(std::wstring, defaultProfileName, L"Default") diff --git a/src/tutorials/TutorialOverlay.qml b/src/tutorials/TutorialOverlay.qml index 0a31ef0e..47e5065d 100644 --- a/src/tutorials/TutorialOverlay.qml +++ b/src/tutorials/TutorialOverlay.qml @@ -55,9 +55,7 @@ Rectangle { Connections { target: manager - function onTabChanged() { - tabChanged(index) - } + onTabChanged: tabChanged(index) } Tooltip { -- cgit v1.3.1 From ef1188811633e8c1403f960c9196de92d0a81020 Mon Sep 17 00:00:00 2001 From: Mikaël Capelle Date: Fri, 27 Nov 2020 19:20:41 +0100 Subject: Fix conflicts with master. --- src/mainwindow.cpp | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) (limited to 'src') diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 7558076d..69f90ce4 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -1210,9 +1210,7 @@ void MainWindow::createHelpMenu() ActionList tutorials; - QString tutorialPath = QApplication::applicationDirPath() - + "/" + QString::fromStdWString(AppConfig::tutorialsPath()) + "/"; - QDirIterator dirIter(tutorialPath, QStringList("*.js"), QDir::Files); + QDirIterator dirIter(QApplication::applicationDirPath() + "/tutorials", QStringList("*.js"), QDir::Files); while (dirIter.hasNext()) { dirIter.next(); QString fileName = dirIter.fileName(); @@ -1324,8 +1322,7 @@ bool MainWindow::addProfile() void MainWindow::hookUpWindowTutorials() { - QString tutorialPath = QApplication::applicationDirPath() + "/" + QString::fromStdWString(AppConfig::tutorialsPath()) + "/"; - QDirIterator dirIter(tutorialPath, QStringList("*.js"), QDir::Files); + QDirIterator dirIter(QApplication::applicationDirPath() + "/tutorials", QStringList("*.js"), QDir::Files); while (dirIter.hasNext()) { dirIter.next(); QString fileName = dirIter.fileName(); @@ -5213,9 +5210,7 @@ void MainWindow::installTranslator(const QString &name) { QTranslator *translator = new QTranslator(this); QString fileName = name + "_" + m_CurrentLanguage; - QString translationsPath = qApp->applicationDirPath() - + "/" + QString::fromStdWString(AppConfig::translationsPath()); - if (!translator->load(fileName, translationsPath)) { + if (!translator->load(fileName, qApp->applicationDirPath() + "/translations")) { if (m_CurrentLanguage.contains(QRegularExpression("^.*_(EN|en)(-.*)?$"))) { log::debug("localization file %s not found", fileName); } // we don't actually expect localization files for English (en, en-us, en-uk, and any variation thereof) -- cgit v1.3.1 From 3db08caaf2a5a52310f3e7decb4d603929c36e9c Mon Sep 17 00:00:00 2001 From: AL <26797547+Al12rs@users.noreply.github.com> Date: Sat, 28 Nov 2020 17:28:49 +0100 Subject: Allow optional refresh for waitForApplication() --- src/organizerproxy.cpp | 10 ++++++++-- src/organizerproxy.h | 2 +- 2 files changed, 9 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/src/organizerproxy.cpp b/src/organizerproxy.cpp index 06e932cb..92f250ea 100644 --- a/src/organizerproxy.cpp +++ b/src/organizerproxy.cpp @@ -144,7 +144,7 @@ HANDLE OrganizerProxy::startApplication( return runner.stealProcessHandle().release(); } -bool OrganizerProxy::waitForApplication(HANDLE handle, LPDWORD exitCode) const +bool OrganizerProxy::waitForApplication(HANDLE handle, bool refresh, LPDWORD exitCode) const { const auto pid = ::GetProcessId(handle); @@ -154,8 +154,14 @@ bool OrganizerProxy::waitForApplication(HANDLE handle, LPDWORD exitCode) const auto runner = m_Proxied->processRunner(); + ProcessRunner::WaitFlags waitFlags = ProcessRunner::ForceWait; + + if (refresh) { + waitFlags |= ProcessRunner::Refresh; + } + const auto r = runner - .setWaitForCompletion(ProcessRunner::ForceWait | ProcessRunner::Refresh, UILocker::OutputRequired) + .setWaitForCompletion(waitFlags, UILocker::OutputRequired) .attachToProcess(handle); if (exitCode) { diff --git a/src/organizerproxy.h b/src/organizerproxy.h index 8664c64f..3097adf0 100644 --- a/src/organizerproxy.h +++ b/src/organizerproxy.h @@ -52,7 +52,7 @@ public: virtual MOBase::IProfile *profile() const override; virtual HANDLE startApplication(const QString &executable, const QStringList &args = QStringList(), const QString &cwd = "", const QString &profile = "", const QString &forcedCustomOverwrite = "", bool ignoreCustomOverwrite = false); - virtual bool waitForApplication(HANDLE handle, LPDWORD exitCode = nullptr) const; + virtual bool waitForApplication(HANDLE handle, bool refresh = true, LPDWORD exitCode = nullptr) const; virtual void refresh(bool saveChanges); virtual bool onAboutToRun(const std::function &func) override; -- cgit v1.3.1