diff --git a/.gitignore b/.gitignore index 935c42609..39ca36c12 100644 --- a/.gitignore +++ b/.gitignore @@ -12,5 +12,6 @@ edit /msbuild.log /*std*.log /*build +.vscode /src/version.aps diff --git a/CMakeLists.txt b/CMakeLists.txt index 85ba23f05..fc5b72f77 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -5,17 +5,13 @@ set(MO2_CMAKE_DEPRECATED_UIBASE_INCLUDE ON) project(organizer) -# if MO2_INSTALL_IS_BIN is set, this means that we should install directly into the -# installation prefix, without the bin/ subfolder, typically for a standalone build -# to update an existing install -if (MO2_INSTALL_IS_BIN) - set(_bin ".") -else() - set(_bin bin) -endif() +find_package(mo2-cmake CONFIG REQUIRED) + +set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1) add_subdirectory(src) +add_subdirectory(themes) set_property(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} PROPERTY VS_STARTUP_PROJECT organizer) -install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/dump_running_process.bat DESTINATION ${_bin}) +install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/dump_running_process.bat DESTINATION ${MO2_INSTALL_BIN}) diff --git a/CMakePresets.json b/CMakePresets.json index eeeaaacd0..bda8b9afd 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -29,7 +29,7 @@ "value": "x64" }, "cacheVariables": { - "CMAKE_CXX_FLAGS": "/EHsc /MP /W4", + "CMAKE_CXX_FLAGS": "/EHsc /MP /W3", "VCPKG_TARGET_TRIPLET": { "type": "STRING", "value": "x64-windows-static-md" diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 10e71e6b4..e4f0cc1cb 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -25,16 +25,16 @@ set_target_properties(organizer PROPERTIES # disable translations because we want to be able to install somewhere else if # required -mo2_configure_target(organizer WARNINGS 4 TRANSLATIONS OFF) +mo2_configure_target(organizer WARNINGS 3 TRANSLATIONS OFF) # we add translations "manually" to handle MO2_INSTALL_IS_BIN mo2_add_translations(organizer INSTALL_RELEASE - INSTALL_DIRECTORY "${_bin}/translations" + INSTALL_DIRECTORY "${MO2_INSTALL_BIN}/translations" SOURCES ${CMAKE_CURRENT_SOURCE_DIR}) mo2_set_project_to_run_from_install( - organizer EXECUTABLE ${CMAKE_INSTALL_PREFIX}/${_bin}/ModOrganizer.exe) + organizer EXECUTABLE ${CMAKE_INSTALL_PREFIX}/${MO2_INSTALL_BIN}/ModOrganizer.exe) target_link_libraries(organizer PRIVATE Shlwapi Bcrypt @@ -44,36 +44,30 @@ target_link_libraries(organizer PRIVATE Qt6::WebEngineWidgets Qt6::WebSockets Version Dbghelp) install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/dlls.manifest.qt6" - DESTINATION ${_bin}/dlls + DESTINATION ${MO2_INSTALL_BIN}/dlls CONFIGURATIONS Release RelWithDebInfo RENAME dlls.manifest) install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/dlls.manifest.debug.qt6" - DESTINATION ${_bin}/dlls + DESTINATION ${MO2_INSTALL_BIN}/dlls CONFIGURATIONS Debug RENAME dlls.manifest) -if (NOT MO2_SKIP_STYLESHEETS_INSTALL) - install( - DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/stylesheets" - DESTINATION ${_bin}) -endif() - if (NOT MO2_SKIP_TUTORIALS_INSTALL) install( DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/tutorials" - DESTINATION ${_bin}) + DESTINATION ${MO2_INSTALL_BIN}) endif() install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/resources/markdown.html" - DESTINATION ${_bin}/resources) + DESTINATION ${MO2_INSTALL_BIN}/resources) # install ModOrganizer.exe itself -install(FILES $ DESTINATION ${_bin}) +install(FILES $ DESTINATION ${MO2_INSTALL_BIN}) # install dependencies DLLs -install(FILES $ DESTINATION ${_bin}/dlls) -install(FILES $ DESTINATION ${_bin}/dlls) -install(FILES $ DESTINATION ${_bin}/dlls) +install(FILES $ DESTINATION ${MO2_INSTALL_BIN}/dlls) +install(FILES $ DESTINATION ${MO2_INSTALL_BIN}/dlls) +install(FILES $ DESTINATION ${MO2_INSTALL_BIN}/dlls) # this may copy over the ones from uibase/usvfs # - when building with mob, this should not matter as the files should be identical @@ -89,7 +83,7 @@ install(FILES $ $ $ -DESTINATION ${_bin}) +DESTINATION ${MO2_INSTALL_BIN}) # do not install PDB if CMAKE_INSTALL_PREFIX is "bin" if (NOT MO2_INSTALL_IS_BIN) @@ -97,7 +91,7 @@ if (NOT MO2_INSTALL_IS_BIN) endif() mo2_deploy_qt( - DIRECTORY ${_bin} + DIRECTORY ${MO2_INSTALL_BIN} BINARIES ModOrganizer.exe $) # set source groups for VS @@ -127,13 +121,12 @@ mo2_add_filter(NAME src/categories GROUPS mo2_add_filter(NAME src/core GROUPS archivefiletree - githubpp + github + inibakery installationmanager nexusinterface nxmaccessmanager organizercore - game_features - plugincontainer apiuseraccount processrunner qdirfiletree @@ -141,6 +134,16 @@ mo2_add_filter(NAME src/core GROUPS uilocker ) +mo2_add_filter(NAME src/extensions GROUPS + game_features + thememanager + translationmanager + extensionmanager + extensionwatcher + pluginmanager + proxyqt +) + mo2_add_filter(NAME src/dialogs GROUPS aboutdialog activatemodsdialog @@ -276,6 +279,7 @@ mo2_add_filter(NAME src/proxies GROUPS downloadmanagerproxy gamefeaturesproxy modlistproxy + extensionlistproxy organizerproxy pluginlistproxy proxyutils @@ -292,6 +296,7 @@ mo2_add_filter(NAME src/register GROUPS ) mo2_add_filter(NAME src/settings GROUPS + extensionsettings settings settingsutilities ) @@ -302,7 +307,9 @@ mo2_add_filter(NAME src/settingsdialog GROUPS settingsdialoggeneral settingsdialognexus settingsdialogpaths - settingsdialogplugins + settingsdialogextensions + settingsdialogextensionrow + settingsdialogextensioninfo settingsdialogworkarounds settingsdialogmodlist settingsdialogtheme diff --git a/src/categoriesdialog.h b/src/categoriesdialog.h index 94f390b07..b8465d667 100644 --- a/src/categoriesdialog.h +++ b/src/categoriesdialog.h @@ -21,7 +21,6 @@ along with Mod Organizer. If not, see . #define CATEGORIESDIALOG_H #include "categories.h" -#include "plugincontainer.h" #include "tutorabledialog.h" #include @@ -71,7 +70,6 @@ private slots: private: Ui::CategoriesDialog* ui; - PluginContainer* m_PluginContainer; int m_ContextRow; int m_HighestID; diff --git a/src/commandline.cpp b/src/commandline.cpp index 1d44e0435..0d3061684 100644 --- a/src/commandline.cpp +++ b/src/commandline.cpp @@ -828,8 +828,9 @@ std::optional ReloadPluginCommand::runPostOrganizer(OrganizerCore& core) QDir(qApp->applicationDirPath() + "/" + ToQString(AppConfig::pluginPath())) .absoluteFilePath(name); + // TODO: reload extension, not plugin log::debug("reloading plugin from {}", filepath); - core.pluginContainer().reloadPlugin(filepath); + // core.pluginManager().reloadPlugin(filepath); return {}; } diff --git a/src/createinstancedialog.cpp b/src/createinstancedialog.cpp index d50a2063c..fc9c3ce69 100644 --- a/src/createinstancedialog.cpp +++ b/src/createinstancedialog.cpp @@ -95,7 +95,7 @@ class DirectoryCreator std::vector m_created; }; -CreateInstanceDialog::CreateInstanceDialog(const PluginContainer& pc, Settings* s, +CreateInstanceDialog::CreateInstanceDialog(const PluginManager& pc, Settings* s, QWidget* parent) : QDialog(parent), ui(new Ui::CreateInstanceDialog), m_pc(pc), m_settings(s), m_switching(false), m_singlePage(false) @@ -133,13 +133,13 @@ CreateInstanceDialog::CreateInstanceDialog(const PluginContainer& pc, Settings* addShortcutAction(QKeySequence::Find, Actions::Find); - addShortcut(Qt::ALT + Qt::Key_Left, [&] { + addShortcut(Qt::ALT | Qt::Key_Left, [&] { back(); }); - addShortcut(Qt::ALT + Qt::Key_Right, [&] { + addShortcut(Qt::ALT | Qt::Key_Right, [&] { next(false); }); - addShortcut(Qt::CTRL + Qt::Key_Return, [&] { + addShortcut(Qt::CTRL | Qt::Key_Return, [&] { next(); }); @@ -161,7 +161,7 @@ Ui::CreateInstanceDialog* CreateInstanceDialog::getUI() return ui.get(); } -const PluginContainer& CreateInstanceDialog::pluginContainer() +const PluginManager& CreateInstanceDialog::pluginManager() { return m_pc; } diff --git a/src/createinstancedialog.h b/src/createinstancedialog.h index 4495cc78e..515cd14d2 100644 --- a/src/createinstancedialog.h +++ b/src/createinstancedialog.h @@ -16,7 +16,7 @@ namespace cid class Page; } -class PluginContainer; +class PluginManager; class Settings; // this is a wizard for creating a new instance, it is made out of Page objects, @@ -90,13 +90,12 @@ class CreateInstanceDialog : public QDialog ProfileSettings profileSettings; }; - CreateInstanceDialog(const PluginContainer& pc, Settings* s, - QWidget* parent = nullptr); + CreateInstanceDialog(const PluginManager& pc, Settings* s, QWidget* parent = nullptr); ~CreateInstanceDialog(); Ui::CreateInstanceDialog* getUI(); - const PluginContainer& pluginContainer(); + const PluginManager& pluginManager(); Settings* settings(); // disables all the pages except for the given one, used on startup when some @@ -185,7 +184,7 @@ class CreateInstanceDialog : public QDialog private: std::unique_ptr ui; - const PluginContainer& m_pc; + const PluginManager& m_pc; Settings* m_settings; std::vector> m_pages; QString m_originalNext; diff --git a/src/createinstancedialogpages.cpp b/src/createinstancedialogpages.cpp index 66f4c31cc..147b906b8 100644 --- a/src/createinstancedialogpages.cpp +++ b/src/createinstancedialogpages.cpp @@ -1,7 +1,7 @@ #include "createinstancedialogpages.h" #include "filesystemutilities.h" #include "instancemanager.h" -#include "plugincontainer.h" +#include "pluginmanager.h" #include "settings.h" #include "settingsdialognexus.h" #include "shared/appconfig.h" @@ -55,7 +55,7 @@ void PlaceholderLabel::setVisible(bool b) } Page::Page(CreateInstanceDialog& dlg) - : ui(dlg.getUI()), m_dlg(dlg), m_pc(dlg.pluginContainer()), m_skip(false), + : ui(dlg.getUI()), m_dlg(dlg), m_pc(dlg.pluginManager()), m_skip(false), m_firstActivation(true) {} diff --git a/src/createinstancedialogpages.h b/src/createinstancedialogpages.h index ce5256354..8af91f6a2 100644 --- a/src/createinstancedialogpages.h +++ b/src/createinstancedialogpages.h @@ -117,7 +117,7 @@ class Page protected: Ui::CreateInstanceDialog* ui; CreateInstanceDialog& m_dlg; - const PluginContainer& m_pc; + const PluginManager& m_pc; bool m_skip; bool m_firstActivation; diff --git a/src/datatab.cpp b/src/datatab.cpp index dc8419cf1..cc05e83d4 100644 --- a/src/datatab.cpp +++ b/src/datatab.cpp @@ -14,9 +14,9 @@ using namespace MOBase; // in mainwindow.cpp QString UnmanagedModName(); -DataTab::DataTab(OrganizerCore& core, PluginContainer& pc, QWidget* parent, +DataTab::DataTab(OrganizerCore& core, PluginManager& pc, QWidget* parent, Ui::MainWindow* mwui) - : m_core(core), m_pluginContainer(pc), m_parent(parent), + : m_core(core), m_pluginManager(pc), m_parent(parent), ui{mwui->tabWidget, mwui->dataTab, mwui->dataTabRefresh, @@ -26,7 +26,7 @@ DataTab::DataTab(OrganizerCore& core, PluginContainer& pc, QWidget* parent, mwui->dataTabShowHiddenFiles}, m_needUpdate(true) { - m_filetree.reset(new FileTree(core, m_pluginContainer, ui.tree)); + m_filetree.reset(new FileTree(core, m_pluginManager, ui.tree)); m_filter.setUseSourceSort(true); m_filter.setFilterColumn(FileTreeModel::FileName); m_filter.setEdit(mwui->dataTabFilter); diff --git a/src/datatab.h b/src/datatab.h index 0bc0d83e6..c1d258c9f 100644 --- a/src/datatab.h +++ b/src/datatab.h @@ -14,7 +14,7 @@ class MainWindow; } class OrganizerCore; class Settings; -class PluginContainer; +class PluginManager; class FileTree; namespace MOShared @@ -27,8 +27,7 @@ class DataTab : public QObject Q_OBJECT; public: - DataTab(OrganizerCore& core, PluginContainer& pc, QWidget* parent, - Ui::MainWindow* ui); + DataTab(OrganizerCore& core, PluginManager& pc, QWidget* parent, Ui::MainWindow* ui); void saveState(Settings& s) const; void restoreState(const Settings& s); @@ -58,7 +57,7 @@ class DataTab : public QObject }; OrganizerCore& m_core; - PluginContainer& m_pluginContainer; + PluginManager& m_pluginManager; QWidget* m_parent; DataTabUi ui; std::unique_ptr m_filetree; diff --git a/src/disableproxyplugindialog.cpp b/src/disableproxyplugindialog.cpp deleted file mode 100644 index a99b0a073..000000000 --- a/src/disableproxyplugindialog.cpp +++ /dev/null @@ -1,34 +0,0 @@ -#include "disableproxyplugindialog.h" - -#include "ui_disableproxyplugindialog.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 deleted file mode 100644 index 421697b67..000000000 --- a/src/disableproxyplugindialog.h +++ /dev/null @@ -1,44 +0,0 @@ -/* -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 deleted file mode 100644 index 9f0687879..000000000 --- a/src/disableproxyplugindialog.ui +++ /dev/null @@ -1,174 +0,0 @@ - - - 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/downloadmanager.cpp b/src/downloadmanager.cpp index 730803f6d..9bfc77022 100644 --- a/src/downloadmanager.cpp +++ b/src/downloadmanager.cpp @@ -322,9 +322,9 @@ void DownloadManager::setShowHidden(bool showHidden) refreshList(); } -void DownloadManager::setPluginContainer(PluginContainer* pluginContainer) +void DownloadManager::setPluginManager(PluginManager* pluginManager) { - m_NexusInterface->setPluginContainer(pluginContainer); + m_NexusInterface->setPluginManager(pluginManager); } void DownloadManager::refreshList() diff --git a/src/downloadmanager.h b/src/downloadmanager.h index 1e7a626ee..28af32b73 100644 --- a/src/downloadmanager.h +++ b/src/downloadmanager.h @@ -49,7 +49,7 @@ class IPluginGame; } class NexusInterface; -class PluginContainer; +class PluginManager; class OrganizerCore; /*! @@ -213,7 +213,7 @@ class DownloadManager : public QObject */ void setShowHidden(bool showHidden); - void setPluginContainer(PluginContainer* pluginContainer); + void setPluginManager(PluginManager* pluginManager); /** * @brief download from an already open network connection diff --git a/src/extensionlistproxy.cpp b/src/extensionlistproxy.cpp new file mode 100644 index 000000000..948fb7ee3 --- /dev/null +++ b/src/extensionlistproxy.cpp @@ -0,0 +1,51 @@ +#include "extensionlistproxy.h" + +#include + +using namespace MOBase; + +ExtensionListProxy::ExtensionListProxy(OrganizerProxy* oproxy, + const ExtensionManager& manager) + : m_oproxy(oproxy), m_manager(&manager) +{} + +ExtensionListProxy ::~ExtensionListProxy() {} + +bool ExtensionListProxy::installed(const QString& identifier) const +{ + return m_manager->extension(identifier) != nullptr; +} + +bool ExtensionListProxy::enabled(const QString& extension) const +{ + return m_manager->isEnabled(extension); +} + +bool ExtensionListProxy::enabled(const IExtension& extension) const +{ + return m_manager->isEnabled(extension); +} + +const IExtension& ExtensionListProxy::get(QString const& identifier) const +{ + auto* extension = m_manager->extension(identifier); + if (extension) { + return *extension; + } + throw std::out_of_range(std::format("extension '{}' not found", identifier)); +} + +const IExtension& ExtensionListProxy::at(std::size_t const& index) const +{ + return *m_manager->extensions().at(index); +} + +const IExtension& ExtensionListProxy::operator[](std::size_t const& index) const +{ + return *m_manager->extensions().at(index); +} + +std::size_t ExtensionListProxy::size() const +{ + return m_manager->extensions().size(); +} diff --git a/src/extensionlistproxy.h b/src/extensionlistproxy.h new file mode 100644 index 000000000..84245cdc2 --- /dev/null +++ b/src/extensionlistproxy.h @@ -0,0 +1,29 @@ +#ifndef EXTENSIONLISTPROXY_H +#define EXTENSIONLISTPROXY_H + +#include + +#include "extensionmanager.h" + +class OrganizerProxy; + +class ExtensionListProxy : public MOBase::IExtensionList +{ +public: + ExtensionListProxy(OrganizerProxy* oproxy, const ExtensionManager& manager); + virtual ~ExtensionListProxy(); + + bool installed(const QString& identifier) const override; + bool enabled(const QString& extension) const override; + bool enabled(const MOBase::IExtension& extension) const override; + const MOBase::IExtension& get(QString const& identifier) const override; + const MOBase::IExtension& at(std::size_t const& index) const override; + const MOBase::IExtension& operator[](std::size_t const& index) const override; + std::size_t size() const override; + +private: + OrganizerProxy* m_oproxy; + const ExtensionManager* m_manager; +}; + +#endif diff --git a/src/extensionmanager.cpp b/src/extensionmanager.cpp new file mode 100644 index 000000000..422bd0e6f --- /dev/null +++ b/src/extensionmanager.cpp @@ -0,0 +1,85 @@ +#include "extensionmanager.h" + +#include + +#include "organizercore.h" + +using namespace MOBase; +namespace fs = std::filesystem; + +ExtensionManager::ExtensionManager(OrganizerCore* core) : m_core{core} {} + +void ExtensionManager::loadExtensions(fs::path const& directory) +{ + for (const auto& entry : fs::directory_iterator{directory}) { + if (entry.is_directory()) { + auto extension = ExtensionFactory::loadExtension(entry.path()); + + if (extension) { + // check if we have a duplicate identifier + const auto it = std::find_if( + m_extensions.begin(), m_extensions.end(), [&extension](const auto& value) { + return value->metadata().identifier().compare( + extension->metadata().identifier(), Qt::CaseInsensitive) == 0; + }); + if (it != m_extensions.end()) { + log::error("an extension '{}' already exists", + extension->metadata().identifier()); + continue; + } + + log::debug("extension data loaded from '{}': {}, {}", entry.path().native(), + extension->metadata().identifier(), extension->metadata().type()); + + triggerWatchers(*extension); + m_extensions.push_back(std::move(extension)); + } + } + } +} + +void ExtensionManager::triggerWatchers(const MOBase::IExtension& extension) const +{ + boost::fusion::for_each(m_watchers, [&extension](auto& watchers) { + using KeyType = typename std::decay_t::first_type; + if (auto* p = dynamic_cast(&extension)) { + for (auto& watcher : watchers.second) { + watcher->extensionLoaded(*p); + } + } + }); +} + +const IExtension* ExtensionManager::extension(QString const& identifier) const +{ + // TODO: use a map for faster lookup + auto it = std::find_if(m_extensions.begin(), m_extensions.end(), + [&identifier](const auto& ext) { + return identifier.compare(ext->metadata().identifier(), + Qt::CaseInsensitive) == 0; + }); + + return it == m_extensions.end() ? nullptr : it->get(); +} + +bool ExtensionManager::isEnabled(MOBase::IExtension const& extension) const +{ + if (!m_core) { + return true; + } + + for (auto& requirement : extension.metadata().requirements()) { + // TODO: needs an organizerproxy... + // if (!requirement.check(m_core)) { + // return false; + //} + } + + return m_core->settings().extensions().isEnabled(extension, true); +} + +bool ExtensionManager::isEnabled(QString const& identifier) const +{ + const auto* e = extension(identifier); + return e ? isEnabled(*e) : false; +} diff --git a/src/extensionmanager.h b/src/extensionmanager.h new file mode 100644 index 000000000..a3f7f0711 --- /dev/null +++ b/src/extensionmanager.h @@ -0,0 +1,83 @@ +#ifndef EXTENSIONMANAGER_H +#define EXTENSIONMANAGER_H + +#include + +#include +#include +#include + +#include + +#include "extensionwatcher.h" +#include "organizerproxy.h" + +class OrganizerCore; + +class ExtensionManager +{ +public: + ExtensionManager(OrganizerCore* core); + + // retrieve the list of currently loaded extensions + // + const auto& extensions() const { return m_extensions; } + + // retrieve the extension with the given identifier, or a null pointer if there is + // none + // + // identifier are case insensitive + // + const MOBase::IExtension* extension(QString const& identifier) const; + + // check if the given extension is enabled + // + bool isEnabled(MOBase::IExtension const& extension) const; + bool isEnabled(QString const& extension) const; + +public: + // load all extensions from the given directory + // + // trigger all currently registered watchers + // + void loadExtensions(std::filesystem::path const& directory); + + // register an object implementing one or many watcher classes + // + template + void registerWatcher(Watcher& watcher) + { + using WatcherType = std::decay_t; + boost::fusion::for_each(m_watchers, [&watcher](auto& watchers) { + using KeyType = + ExtensionWatcher::first_type>; + if constexpr (std::is_base_of_v) { + watchers.second.push_back(&watcher); + } + }); + } + +private: + // trigger appropriate watchers for the given extension + // + void triggerWatchers(const MOBase::IExtension& extension) const; + +private: + OrganizerCore* m_core; + std::unique_ptr m_proxy; + std::vector> m_extensions; + + using WatcherMap = boost::fusion::map< + boost::fusion::pair*>>, + boost::fusion::pair*>>, + boost::fusion::pair*>>, + boost::fusion::pair*>>>; + + WatcherMap m_watchers; +}; + +#endif diff --git a/src/extensionsettings.cpp b/src/extensionsettings.cpp new file mode 100644 index 000000000..669a3d5a8 --- /dev/null +++ b/src/extensionsettings.cpp @@ -0,0 +1,193 @@ +#include "extensionsettings.h" + +#include "settingsutilities.h" + +using namespace MOBase; + +static const QString EXTENSIONS_GROUP = "Extensions"; +static const QString EXTENSIONS_ENABLED_GROUP = "ExtensionsEnabled"; +static const QString PLUGINS_GROUP = "Plugins"; +static const QString PLUGINS_PERSISTENT_GROUP = "PluginPersistance"; + +ExtensionSettings::ExtensionSettings(QSettings& settings) : m_Settings(settings) {} + +QString ExtensionSettings::path(const IExtension& extension, const Setting& setting) +{ + QString path = extension.metadata().identifier(); + if (!setting.group().isEmpty()) { + path += "/" + setting.group(); + } + return path + "/" + setting.name(); +} + +bool ExtensionSettings::isEnabled(const MOBase::IExtension& extension, + bool defaultValue) const +{ + return get(m_Settings, EXTENSIONS_ENABLED_GROUP, + extension.metadata().identifier(), defaultValue); +} + +void ExtensionSettings::setEnabled(const MOBase::IExtension& extension, + bool enabled) const +{ + set(m_Settings, EXTENSIONS_ENABLED_GROUP, extension.metadata().identifier(), enabled); +} + +QVariant ExtensionSettings::setting(const IExtension& extension, + const Setting& setting) const +{ + return get(m_Settings, EXTENSIONS_GROUP, path(extension, setting), + setting.defaultValue()); +} + +void ExtensionSettings::setSetting(const IExtension& extension, const Setting& setting, + const QVariant& value) +{ + set(m_Settings, EXTENSIONS_GROUP, path(extension, setting), value); +} + +// commits all the settings to the ini +// +void ExtensionSettings::save() +{ + m_Settings.sync(); +} + +PluginSettings::PluginSettings(QSettings& settings) : m_Settings(settings) {} + +QString PluginSettings::path(const QString& pluginName, const QString& key) +{ + return pluginName + "/" + key; +} + +void PluginSettings::checkPluginSettings(const IPlugin* plugin) const +{ + for (const auto& setting : plugin->settings()) { + const auto settingPath = path(plugin->name(), setting.name()); + + QVariant temp = get(m_Settings, PLUGINS_GROUP, settingPath, QVariant()); + + // No previous enabled? Skip. + if (setting.name() == "enabled" && (!temp.isValid() || !temp.canConvert())) { + continue; + } + + if (!temp.isValid()) { + temp = setting.defaultValue(); + } else if (!temp.convert(setting.defaultValue().metaType())) { + log::warn("failed to interpret \"{}\" as correct type for \"{}\" in plugin " + "\"{}\", using default", + temp.toString(), setting.name(), plugin->name()); + + temp = setting.defaultValue(); + } + } +} + +void PluginSettings::fixPluginEnabledSetting(const IPlugin* plugin) +{ + // handle previous "enabled" settings + // TODO: keep this? + const auto previousEnabledPath = plugin->name() + "/enabled"; + const QVariant previousEnabled = + get(m_Settings, PLUGINS_GROUP, previousEnabledPath, QVariant()); + if (previousEnabled.isValid()) { + setPersistent(plugin->name(), "enabled", previousEnabled.toBool(), true); + + // We need to drop it manually in Settings since it is not possible to remove + // plugin settings: + remove(m_Settings, PLUGINS_GROUP, previousEnabledPath); + } +} + +QVariant PluginSettings::setting(const QString& pluginName, const QString& key, + const QVariant& defaultValue) const +{ + return get(m_Settings, PLUGINS_GROUP, path(pluginName, key), defaultValue); +} + +void PluginSettings::setSetting(const QString& pluginName, const QString& key, + const QVariant& value) +{ + const auto settingPath = path(pluginName, key); + const auto oldValue = + get(m_Settings, PLUGINS_GROUP, settingPath, QVariant()); + set(m_Settings, PLUGINS_GROUP, settingPath, value); + emit pluginSettingChanged(pluginName, key, oldValue, value); +} + +QVariant PluginSettings::persistent(const QString& pluginName, const QString& key, + const QVariant& def) const +{ + return get(m_Settings, "PluginPersistance", pluginName + "/" + key, def); +} + +void PluginSettings::setPersistent(const QString& pluginName, const QString& key, + const QVariant& value, bool sync) +{ + set(m_Settings, PLUGINS_PERSISTENT_GROUP, pluginName + "/" + key, value); + + if (sync) { + m_Settings.sync(); + } +} + +void PluginSettings::addBlacklist(const QString& fileName) +{ + m_PluginBlacklist.insert(fileName); + writeBlacklist(); +} + +bool PluginSettings::blacklisted(const QString& fileName) const +{ + return m_PluginBlacklist.contains(fileName); +} + +void PluginSettings::setBlacklist(const QStringList& pluginNames) +{ + m_PluginBlacklist.clear(); + + for (const auto& name : pluginNames) { + m_PluginBlacklist.insert(name); + } +} + +const QSet& PluginSettings::blacklist() const +{ + return m_PluginBlacklist; +} + +void PluginSettings::save() +{ + m_Settings.sync(); + writeBlacklist(); +} + +void PluginSettings::writeBlacklist() +{ + const auto current = readBlacklist(); + + if (current.size() > m_PluginBlacklist.size()) { + // Qt can't remove array elements, the section must be cleared + removeSection(m_Settings, "pluginBlacklist"); + } + + ScopedWriteArray swa(m_Settings, "pluginBlacklist", m_PluginBlacklist.size()); + + for (const QString& plugin : m_PluginBlacklist) { + swa.next(); + swa.set("name", plugin); + } +} + +QSet PluginSettings::readBlacklist() const +{ + QSet set; + + ScopedReadArray sra(m_Settings, "pluginBlacklist"); + sra.for_each([&] { + set.insert(sra.get("name")); + }); + + return set; +} diff --git a/src/extensionsettings.h b/src/extensionsettings.h new file mode 100644 index 000000000..ca91423b9 --- /dev/null +++ b/src/extensionsettings.h @@ -0,0 +1,126 @@ +#ifndef EXTENSIONSETTINGS_H +#define EXTENSIONSETTINGS_H + +#include +#include + +#include +#include + +// settings about extensions +class ExtensionSettings : public QObject +{ + Q_OBJECT + +public: + ExtensionSettings(QSettings& settings); + + // check if the specified extension is enabled in the settings + // + bool isEnabled(const MOBase::IExtension& extension, bool defaultValue = true) const; + + // set the extension as enabled or disabled in the settings + // + void setEnabled(const MOBase::IExtension& extension, bool enabled) const; + + // returns the plugin setting for the given key + // + QVariant setting(const MOBase::IExtension& extension, + const MOBase::Setting& setting) const; + + // sets the plugin setting for the given key + // + void setSetting(const MOBase::IExtension& extension, const MOBase::Setting& setting, + const QVariant& value); + + // commits all the settings to the ini + // + void save(); + +private: + QSettings& m_Settings; + + // retrieve the path to the given setting + // + static QString path(const MOBase::IExtension& extension, + const MOBase::Setting& setting); +}; + +// settings about plugins +// +class PluginSettings : public QObject +{ + Q_OBJECT + +public: + PluginSettings(QSettings& settings); + + // fix enabled settings from previous MO2 installation + // + void fixPluginEnabledSetting(const MOBase::IPlugin* plugin); + + // check that the settings stored for the given plugin are of the appropriate type, + // warning user if not + // + void checkPluginSettings(const MOBase::IPlugin* plugin) const; + + // returns the plugin setting for the given key + // + QVariant setting(const QString& pluginName, const QString& key, + const QVariant& defaultValue = {}) const; + + // sets the plugin setting for the given key + // + void setSetting(const QString& pluginName, const QString& key, const QVariant& value); + + // get/set persistent settings + QVariant persistent(const QString& pluginName, const QString& key, + const QVariant& def) const; + void setPersistent(const QString& pluginName, const QString& key, + const QVariant& value, bool sync); + + // adds the given plugin to the blacklist + // + void addBlacklist(const QString& fileName); + + // returns whether the given plugin is blacklisted + // + bool blacklisted(const QString& fileName) const; + + // overwrites the whole blacklist + // + void setBlacklist(const QStringList& pluginNames); + + // returns the blacklist + // + const QSet& blacklist() const; + + // commits all the settings to the ini + // + void save(); + +Q_SIGNALS: + + // emitted when a plugin setting changes + // + void pluginSettingChanged(QString const& pluginName, const QString& key, + const QVariant& oldValue, const QVariant& newValue); + +private: + QSettings& m_Settings; + QSet m_PluginBlacklist; + + // retrieve the path to the given setting + // + static QString path(const QString& pluginName, const QString& key); + + // commits the blacklist to the ini + // + void writeBlacklist(); + + // reads the blacklist from the ini + // + QSet readBlacklist() const; +}; + +#endif diff --git a/src/extensionwatcher.h b/src/extensionwatcher.h new file mode 100644 index 000000000..2d476e1b2 --- /dev/null +++ b/src/extensionwatcher.h @@ -0,0 +1,34 @@ +#ifndef EXTENSIONWATCHER_H +#define EXTENSIONWATCHER_H + +#include + +// an extension watcher is a class that watches extensions get loaded/unloaded, +// typically to extract information from theme that are needed by MO2 +// +template +class ExtensionWatcher +{ + static_assert(std::is_base_of_v); + +public: + // called when a new extension is found and loaded + // + virtual void extensionLoaded(ExtensionType const& extension) = 0; + + // called when a new extension is unloaded + // + virtual void extensionUnloaded(ExtensionType const& extension) = 0; + + // called when a new extension is disabled + // + virtual void extensionEnabled(ExtensionType const& extension) = 0; + + // called when a new extension is disabled + // + virtual void extensionDisabled(ExtensionType const& extension) = 0; + + virtual ~ExtensionWatcher() {} +}; + +#endif diff --git a/src/filetree.cpp b/src/filetree.cpp index 6d397f24e..12042fdc7 100644 --- a/src/filetree.cpp +++ b/src/filetree.cpp @@ -3,6 +3,7 @@ #include "filetreeitem.h" #include "filetreemodel.h" #include "organizercore.h" +#include "previewgenerator.h" #include "shared/directoryentry.h" #include "shared/fileentry.h" #include "shared/filesorigin.h" @@ -12,7 +13,7 @@ using namespace MOShared; using namespace MOBase; -bool canPreviewFile(const PluginContainer& pc, const FileEntry& file) +bool canPreviewFile(const PluginManager& pc, const FileEntry& file) { return canPreviewFile(pc, file.isFromArchive(), QString::fromStdWString(file.getName())); @@ -116,7 +117,7 @@ class MenuItem } }; -FileTree::FileTree(OrganizerCore& core, PluginContainer& pc, QTreeView* tree) +FileTree::FileTree(OrganizerCore& core, PluginManager& pc, QTreeView* tree) : m_core(core), m_plugins(pc), m_tree(tree), m_model(new FileTreeModel(core)) { m_tree->sortByColumn(0, Qt::AscendingOrder); diff --git a/src/filetree.h b/src/filetree.h index d9597322a..b91cd5181 100644 --- a/src/filetree.h +++ b/src/filetree.h @@ -10,7 +10,7 @@ class FileEntry; } class OrganizerCore; -class PluginContainer; +class PluginManager; class FileTreeModel; class FileTreeItem; @@ -19,7 +19,7 @@ class FileTree : public QObject Q_OBJECT; public: - FileTree(OrganizerCore& core, PluginContainer& pc, QTreeView* tree); + FileTree(OrganizerCore& core, PluginManager& pc, QTreeView* tree); FileTreeModel* model(); void refresh(); @@ -52,7 +52,7 @@ class FileTree : public QObject private: OrganizerCore& m_core; - PluginContainer& m_plugins; + PluginManager& m_plugins; QTreeView* m_tree; FileTreeModel* m_model; diff --git a/src/filterlist.cpp b/src/filterlist.cpp index 3be67def3..85830306e 100644 --- a/src/filterlist.cpp +++ b/src/filterlist.cpp @@ -2,7 +2,7 @@ #include "categories.h" #include "categoriesdialog.h" #include "organizercore.h" -#include "plugincontainer.h" +#include "pluginmanager.h" #include "settings.h" #include "ui_mainwindow.h" #include diff --git a/src/game_features.cpp b/src/game_features.cpp index 9505c8dcb..99c9d9afb 100644 --- a/src/game_features.cpp +++ b/src/game_features.cpp @@ -12,7 +12,7 @@ #include "gameplugins.h" #include "localsavegames.h" #include "organizercore.h" -#include "plugincontainer.h" +#include "pluginmanager.h" #include "savegameinfo.h" #include "scriptextender.h" #include "unmanagedmods.h" @@ -132,9 +132,8 @@ class GameFeatures::CombinedModDataContent : public ModDataContent } }; -GameFeatures::GameFeatures(OrganizerCore* core, PluginContainer* plugins) - : m_pluginContainer(*plugins), - m_modDataChecker(std::make_unique()), +GameFeatures::GameFeatures(OrganizerCore* core, PluginManager* plugins) + : m_plugins(*plugins), m_modDataChecker(std::make_unique()), m_modDataContent(std::make_unique()) { // can be nullptr since the plugin container can be initialized with a Core (e.g., @@ -151,10 +150,10 @@ GameFeatures::GameFeatures(OrganizerCore* core, PluginContainer* plugins) }); }; - connect(plugins, &PluginContainer::pluginEnabled, updateFeatures); - connect(plugins, &PluginContainer::pluginDisabled, updateFeatures); - connect(plugins, &PluginContainer::pluginRegistered, updateFeatures); - connect(plugins, &PluginContainer::pluginUnregistered, + connect(plugins, &PluginManager::pluginEnabled, updateFeatures); + connect(plugins, &PluginManager::pluginDisabled, updateFeatures); + connect(plugins, &PluginManager::pluginRegistered, updateFeatures); + connect(plugins, &PluginManager::pluginUnregistered, [this, updateFeatures](MOBase::IPlugin* plugin) { // remove features from the current plugin for (auto& [_, features] : m_allFeatures) { @@ -188,20 +187,20 @@ void GameFeatures::updateCurrentFeatures(std::type_index const& index) m_currentFeatures[index].clear(); // this can occur when starting MO2, just wait for the next update - if (!m_pluginContainer.managedGame()) { + if (!m_plugins.managedGame()) { return; } for (const auto& dataFeature : features) { // registering plugin is disabled - if (!m_pluginContainer.isEnabled(dataFeature.plugin())) { + if (!m_plugins.isEnabled(dataFeature.plugin())) { continue; } // games does not match if (!dataFeature.games().isEmpty() && - !dataFeature.games().contains(m_pluginContainer.managedGame()->gameName())) { + !dataFeature.games().contains(m_plugins.managedGame()->gameName())) { continue; } diff --git a/src/game_features.h b/src/game_features.h index 55f35e377..fb42c8987 100644 --- a/src/game_features.h +++ b/src/game_features.h @@ -20,7 +20,7 @@ class IPluginGame; } // namespace MOBase class OrganizerCore; -class PluginContainer; +class PluginManager; /** * Class managing game features, either registered or from the game plugin. @@ -33,7 +33,7 @@ class GameFeatures : public QObject /** * */ - GameFeatures(OrganizerCore* core, PluginContainer* plugins); + GameFeatures(OrganizerCore* core, PluginManager* plugins); ~GameFeatures(); @@ -104,7 +104,7 @@ class GameFeatures : public QObject CombinedModDataChecker& modDataChecker() const; CombinedModDataContent& modDataContent() const; - PluginContainer& m_pluginContainer; + PluginManager& m_plugins; std::unordered_map> m_allFeatures; std::unordered_map>> diff --git a/src/inibakery.cpp b/src/inibakery.cpp new file mode 100644 index 000000000..0b58307f3 --- /dev/null +++ b/src/inibakery.cpp @@ -0,0 +1,50 @@ +#include "inibakery.h" + +#include +#include + +#include "organizercore.h" + +using namespace MOBase; + +IniBakery::IniBakery(OrganizerCore& core) : m_core{core} +{ + m_core.onAboutToRun([this](auto&&...) { + return prepareIni(); + }); +} + +bool IniBakery::prepareIni() const +{ + const auto& features = m_core.pluginManager().gameFeatures(); + + if (auto savegames = features.gameFeature()) { + savegames->prepareProfile(m_core.currentProfile()); + } + + if (auto invalidation = features.gameFeature()) { + invalidation->prepareProfile(m_core.currentProfile()); + } + + return true; +} + +MappingType IniBakery::mappings() const +{ + MappingType result; + + const auto iniFileNames = m_core.managedGame()->iniFiles(); + const IPluginGame* game = m_core.managedGame(); + + IProfile* profile = m_core.currentProfile(); + + if (profile->localSettingsEnabled()) { + for (const QString& iniFile : iniFileNames) { + result.push_back({m_core.profilePath() + "/" + QFileInfo(iniFile).fileName(), + game->documentsDirectory().absoluteFilePath(iniFile), false, + false}); + } + } + + return result; +} diff --git a/src/inibakery.h b/src/inibakery.h new file mode 100644 index 000000000..a32ef55aa --- /dev/null +++ b/src/inibakery.h @@ -0,0 +1,29 @@ +#ifndef INIBAKERY_H +#define INIBAKERY_H + +#include + +#include + +class OrganizerCore; + +// small classes that deal with preparing profiles before runs for local saves, bsa +// invalidation, etc., and providing mapping for local profile files when needed +// +// this class replaces the old INI Bakery plugin +// +class IniBakery +{ +public: + IniBakery(OrganizerCore& core); + + MappingType mappings() const; + +private: + bool prepareIni() const; + +private: + OrganizerCore& m_core; +}; + +#endif diff --git a/src/installationmanager.cpp b/src/installationmanager.cpp index 37c86c7e4..150b677aa 100644 --- a/src/installationmanager.cpp +++ b/src/installationmanager.cpp @@ -116,9 +116,9 @@ void InstallationManager::setParentWidget(QWidget* widget) m_ParentWidget = widget; } -void InstallationManager::setPluginContainer(const PluginContainer* pluginContainer) +void InstallationManager::setPluginManager(const PluginManager* pluginManager) { - m_PluginContainer = pluginContainer; + m_PluginManager = pluginManager; } void InstallationManager::queryPassword() @@ -751,7 +751,7 @@ InstallationResult InstallationManager::install(const QString& fileName, std::shared_ptr filesTree = archiveOpen ? ArchiveFileTree::makeTree(*m_ArchiveHandler) : nullptr; - auto installers = m_PluginContainer->plugins(); + auto installers = m_PluginManager->plugins(); std::sort(installers.begin(), installers.end(), [](IPluginInstaller* lhs, IPluginInstaller* rhs) { @@ -763,7 +763,7 @@ InstallationResult InstallationManager::install(const QString& fileName, for (IPluginInstaller* installer : installers) { // don't use inactive installers (installer can't be null here but vc static code // analysis thinks it could) - if ((installer == nullptr) || !m_PluginContainer->isEnabled(installer)) { + if ((installer == nullptr) || !m_PluginManager->isEnabled(installer)) { continue; } @@ -910,8 +910,8 @@ QStringList InstallationManager::getSupportedExtensions() const { std::set supportedExtensions( {"zip", "rar", "7z", "fomod", "001"}); - for (auto* installer : m_PluginContainer->plugins()) { - if (m_PluginContainer->isEnabled(installer)) { + for (auto* installer : m_PluginManager->plugins()) { + if (m_PluginManager->isEnabled(installer)) { if (auto* installerCustom = dynamic_cast(installer)) { std::set extensions = installerCustom->supportedExtensions(); supportedExtensions.insert(extensions.begin(), extensions.end()); @@ -925,9 +925,9 @@ void InstallationManager::notifyInstallationStart(QString const& archive, bool reinstallation, ModInfo::Ptr currentMod) { - auto& installers = m_PluginContainer->plugins(); + auto& installers = m_PluginManager->plugins(); for (auto* installer : installers) { - if (m_PluginContainer->isEnabled(installer)) { + if (m_PluginManager->isEnabled(installer)) { installer->onInstallationStart(archive, reinstallation, currentMod.get()); } } @@ -936,9 +936,9 @@ void InstallationManager::notifyInstallationStart(QString const& archive, void InstallationManager::notifyInstallationEnd(const InstallationResult& result, ModInfo::Ptr newMod) { - auto& installers = m_PluginContainer->plugins(); + auto& installers = m_PluginManager->plugins(); for (auto* installer : installers) { - if (m_PluginContainer->isEnabled(installer)) { + if (m_PluginManager->isEnabled(installer)) { installer->onInstallationEnd(result.result(), newMod.get()); } } diff --git a/src/installationmanager.h b/src/installationmanager.h index e125bbe94..e24f96b73 100644 --- a/src/installationmanager.h +++ b/src/installationmanager.h @@ -35,7 +35,7 @@ along with Mod Organizer. If not, see . #include #include "modinfo.h" -#include "plugincontainer.h" +#include "pluginmanager.h" // contains installation result from the manager, internal class // for MO2 that is not forwarded to plugin @@ -129,7 +129,7 @@ class InstallationManager : public QObject, public MOBase::IInstallationManager /** * */ - void setPluginContainer(const PluginContainer* pluginContainer); + void setPluginManager(const PluginManager* pluginManager); /** * @brief update the directory where downloads are stored @@ -332,7 +332,7 @@ private slots: private: // The plugin container, mostly to check if installer are enabled or not. - const PluginContainer* m_PluginContainer; + const PluginManager* m_PluginManager; bool m_IsRunning; diff --git a/src/instancemanager.cpp b/src/instancemanager.cpp index 90e9e5190..ebdf939d7 100644 --- a/src/instancemanager.cpp +++ b/src/instancemanager.cpp @@ -20,10 +20,11 @@ along with Mod Organizer. If not, see . #include "instancemanager.h" #include "createinstancedialog.h" #include "createinstancedialogpages.h" +#include "extensionmanager.h" #include "filesystemutilities.h" #include "instancemanagerdialog.h" #include "nexusinterface.h" -#include "plugincontainer.h" +#include "pluginmanager.h" #include "selectiondialog.h" #include "settings.h" #include "shared/appconfig.h" @@ -146,7 +147,7 @@ bool Instance::readFromIni() return true; } -Instance::SetupResults Instance::setup(PluginContainer& plugins) +Instance::SetupResults Instance::setup(PluginManager& plugins) { // read initial values from the ini if (!readFromIni()) { @@ -208,7 +209,7 @@ void Instance::setVariant(const QString& name) m_gameVariant = name; } -Instance::SetupResults Instance::getGamePlugin(PluginContainer& plugins) +Instance::SetupResults Instance::getGamePlugin(PluginManager& plugins) { if (!m_gameName.isEmpty() && !m_gameDir.isEmpty()) { // normal case: both the name and dir are in the ini @@ -609,15 +610,15 @@ bool InstanceManager::allowedToChangeInstance() const MOBase::IPluginGame* InstanceManager::gamePluginForDirectory(const QString& instanceDir, - PluginContainer& plugins) const + PluginManager& plugins) const { return const_cast( - gamePluginForDirectory(instanceDir, const_cast(plugins))); + gamePluginForDirectory(instanceDir, const_cast(plugins))); } const MOBase::IPluginGame* InstanceManager::gamePluginForDirectory(const QString& instanceDir, - const PluginContainer& plugins) const + const PluginManager& plugins) const { const QString ini = iniPath(instanceDir); @@ -701,9 +702,13 @@ std::unique_ptr selectInstance() auto& m = InstanceManager::singleton(); // since there is no instance currently active, load plugins with a null - // OrganizerCore; see PluginContainer::initPlugin() + // OrganizerCore; see PluginManager::initPlugin() NexusInterface ni(nullptr); - PluginContainer pc(nullptr); + ExtensionManager ec(nullptr); + ec.loadExtensions(QDir(QCoreApplication::applicationDirPath() + "/extensions") + .filesystemAbsolutePath()); + + PluginManager pc(ec, nullptr); pc.loadPlugins(); if (m.hasAnyInstances()) { @@ -742,7 +747,7 @@ std::unique_ptr selectInstance() // this is used below in setupInstance() when the game directory is gone or // no plugins can recognize it // -SetupInstanceResults selectGame(Instance& instance, PluginContainer& pc) +SetupInstanceResults selectGame(Instance& instance, PluginManager& pc) { CreateInstanceDialog dlg(pc, nullptr); @@ -774,7 +779,7 @@ SetupInstanceResults selectGame(Instance& instance, PluginContainer& pc) // or when a new variant has become supported by the plugin for a game the // user already has an instance for // -SetupInstanceResults selectVariant(Instance& instance, PluginContainer& pc) +SetupInstanceResults selectVariant(Instance& instance, PluginManager& pc) { CreateInstanceDialog dlg(pc, nullptr); @@ -800,7 +805,7 @@ SetupInstanceResults selectVariant(Instance& instance, PluginContainer& pc) return SetupInstanceResults::TryAgain; } -SetupInstanceResults setupInstance(Instance& instance, PluginContainer& pc) +SetupInstanceResults setupInstance(Instance& instance, PluginManager& pc) { // set up the instance const auto setupResult = instance.setup(pc); diff --git a/src/instancemanager.h b/src/instancemanager.h index 176033e81..efed37c93 100644 --- a/src/instancemanager.h +++ b/src/instancemanager.h @@ -10,7 +10,7 @@ class IPluginGame; } class Settings; -class PluginContainer; +class PluginManager; // represents an instance, either global or portable // @@ -110,7 +110,7 @@ class Instance // setup() tries to recover from some errors, but can fail for a variety of // reasons, see SetupResults // - SetupResults setup(PluginContainer& plugins); + SetupResults setup(PluginManager& plugins); // overrides the game name and directory // @@ -198,7 +198,7 @@ class Instance // figures out the game plugin for this instance // - SetupResults getGamePlugin(PluginContainer& plugins); + SetupResults getGamePlugin(PluginManager& plugins); // figures out the profile name for this instance // @@ -243,11 +243,11 @@ class InstanceManager // // returns null if all of this fails // - const MOBase::IPluginGame* - gamePluginForDirectory(const QString& dir, const PluginContainer& plugins) const; + const MOBase::IPluginGame* gamePluginForDirectory(const QString& dir, + const PluginManager& plugins) const; MOBase::IPluginGame* gamePluginForDirectory(const QString& dir, - PluginContainer& plugins) const; + PluginManager& plugins) const; // clears the instance name from the registry; on restart, this will make MO // either select the portable instance if it exists, or display the instance @@ -359,6 +359,6 @@ std::unique_ptr selectInstance(); // // - if the instance has been set up correctly, returns Okay // -SetupInstanceResults setupInstance(Instance& instance, PluginContainer& pc); +SetupInstanceResults setupInstance(Instance& instance, PluginManager& pc); #endif // MODORGANIZER_INSTANCEMANAGER_INCLUDED diff --git a/src/instancemanagerdialog.cpp b/src/instancemanagerdialog.cpp index b5139e908..5a026f1fb 100644 --- a/src/instancemanagerdialog.cpp +++ b/src/instancemanagerdialog.cpp @@ -2,7 +2,7 @@ #include "createinstancedialog.h" #include "filesystemutilities.h" #include "instancemanager.h" -#include "plugincontainer.h" +#include "pluginmanager.h" #include "selectiondialog.h" #include "settings.h" #include "shared/appconfig.h" @@ -17,7 +17,7 @@ using namespace MOBase; // returns the icon for the given instance or an empty 32x32 icon if the game // plugin couldn't be found // -QIcon instanceIcon(PluginContainer& pc, const Instance& i) +QIcon instanceIcon(PluginManager& pc, const Instance& i) { auto* game = InstanceManager::singleton().gamePluginForDirectory(i.directory(), pc); @@ -146,7 +146,7 @@ QString getInstanceName(QWidget* parent, const QString& title, const QString& mo InstanceManagerDialog::~InstanceManagerDialog() = default; -InstanceManagerDialog::InstanceManagerDialog(PluginContainer& pc, QWidget* parent) +InstanceManagerDialog::InstanceManagerDialog(PluginManager& pc, QWidget* parent) : QDialog(parent), ui(new Ui::InstanceManagerDialog), m_pc(pc), m_model(nullptr), m_restartOnSelect(true) { @@ -313,7 +313,7 @@ void InstanceManagerDialog::select(std::size_t i) fillData(*ii); ui->list->selectionModel()->select( - m_filter.mapFromSource(m_filter.sourceModel()->index(i, 0)), + m_filter.mapFromSource(m_filter.sourceModel()->index(static_cast(i), 0)), QItemSelectionModel::ClearAndSelect); } else { clearData(); @@ -341,7 +341,8 @@ void InstanceManagerDialog::selectActiveInstance() if (m_instances[i]->displayName() == active->displayName()) { select(i); - ui->list->scrollTo(m_filter.mapFromSource(m_filter.sourceModel()->index(i, 0))); + ui->list->scrollTo(m_filter.mapFromSource( + m_filter.sourceModel()->index(static_cast(i), 0))); return; } @@ -455,7 +456,7 @@ void InstanceManagerDialog::rename() auto newInstance = std::make_unique(dest, false); i = newInstance.get(); - m_model->item(selIndex)->setText(newName); + m_model->item(static_cast(selIndex))->setText(newName); m_instances[selIndex] = std::move(newInstance); fillData(*i); diff --git a/src/instancemanagerdialog.h b/src/instancemanagerdialog.h index 884beaa53..3a50d61aa 100644 --- a/src/instancemanagerdialog.h +++ b/src/instancemanagerdialog.h @@ -10,7 +10,7 @@ class InstanceManagerDialog; }; class Instance; -class PluginContainer; +class PluginManager; // a dialog to manage existing instances // @@ -19,7 +19,7 @@ class InstanceManagerDialog : public QDialog Q_OBJECT public: - explicit InstanceManagerDialog(PluginContainer& pc, QWidget* parent = nullptr); + explicit InstanceManagerDialog(PluginManager& pc, QWidget* parent = nullptr); ~InstanceManagerDialog(); @@ -90,7 +90,7 @@ class InstanceManagerDialog : public QDialog static const std::size_t NoSelection = -1; std::unique_ptr ui; - PluginContainer& m_pc; + PluginManager& m_pc; std::vector> m_instances; MOBase::FilterWidget m_filter; QStandardItemModel* m_model; diff --git a/src/iuserinterface.h b/src/iuserinterface.h index 7ddc545d0..01a6e7902 100644 --- a/src/iuserinterface.h +++ b/src/iuserinterface.h @@ -13,8 +13,6 @@ class IUserInterface public: virtual void registerModPage(MOBase::IPluginModPage* modPage) = 0; - virtual void installTranslator(const QString& name) = 0; - virtual bool closeWindow() = 0; virtual void setWindowEnabled(bool enabled) = 0; diff --git a/src/main.cpp b/src/main.cpp index de1578593..02aa2a183 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -6,6 +6,7 @@ #include "multiprocess.h" #include "organizercore.h" #include "shared/util.h" +#include "thememanager.h" #include "thread_utils.h" #include #include @@ -39,7 +40,6 @@ int run(int argc, char* argv[]) // must be after logging TimeThis tt("main() multiprocess"); - QApplication::setAttribute(Qt::AA_EnableHighDpiScaling); MOApplication app(argc, argv); // check if the command line wants to run something right now diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 20a849f49..fc5d161dc 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -230,12 +230,15 @@ void setFilterShortcuts(QWidget* widget, QLineEdit* edit) } MainWindow::MainWindow(Settings& settings, OrganizerCore& organizerCore, - PluginContainer& pluginContainer, QWidget* parent) + ExtensionManager& extensionManager, PluginManager& pluginManager, + ThemeManager& themeManager, + TranslationManager& translationManager, QWidget* parent) : QMainWindow(parent), ui(new Ui::MainWindow), m_WasVisible(false), m_FirstPaint(true), m_linksSeparator(nullptr), m_Tutorial(this, "MainWindow"), m_OldProfileIndex(-1), m_OldExecutableIndex(-1), m_CategoryFactory(CategoryFactory::instance()), m_OrganizerCore(organizerCore), - m_PluginContainer(pluginContainer), + m_ExtensionManager(extensionManager), m_PluginManager(pluginManager), + m_ThemeManager(themeManager), m_TranslationManager(translationManager), m_ArchiveListWriter(std::bind(&MainWindow::saveArchiveList, this)), m_LinkToolbar(nullptr), m_LinkDesktop(nullptr), m_LinkStartMenu(nullptr), m_NumberOfProblems(0), m_ProblemsCheckRequired(false) @@ -262,7 +265,7 @@ MainWindow::MainWindow(Settings& settings, OrganizerCore& organizerCore, MOShared::SetThisThreadName("main"); ui->setupUi(this); - languageChange(settings.interface().language()); + onLanguageChanged(settings.interface().language()); ui->statusBar->setup(ui, settings); { @@ -309,7 +312,7 @@ MainWindow::MainWindow(Settings& settings, OrganizerCore& organizerCore, settings.geometry().restoreState(ui->espList->header()); // data tab - m_DataTab.reset(new DataTab(m_OrganizerCore, m_PluginContainer, this, ui)); + m_DataTab.reset(new DataTab(m_OrganizerCore, m_PluginManager, this, ui)); m_DataTab->restoreState(settings); connect(m_DataTab.get(), &DataTab::executablesChanged, [&] { @@ -373,8 +376,9 @@ MainWindow::MainWindow(Settings& settings, OrganizerCore& organizerCore, updateSortButton(); - connect(&m_PluginContainer, SIGNAL(diagnosisUpdate()), this, - SLOT(scheduleCheckForProblems())); + connect(&m_PluginManager, &PluginManager::diagnosePluginInvalidated, [this] { + scheduleCheckForProblems(); + }); connect(&m_OrganizerCore, &OrganizerCore::directoryStructureReady, this, &MainWindow::onDirectoryStructureChanged); @@ -384,10 +388,10 @@ MainWindow::MainWindow(Settings& settings, OrganizerCore& organizerCore, connect(m_OrganizerCore.directoryRefresher(), SIGNAL(error(QString)), this, SLOT(showError(QString))); - connect(&m_OrganizerCore.settings(), SIGNAL(languageChanged(QString)), this, - SLOT(languageChange(QString))); - connect(&m_OrganizerCore.settings(), SIGNAL(styleChanged(QString)), this, - SIGNAL(styleChanged(QString))); + connect(&m_OrganizerCore.settings(), &Settings::languageChanged, this, + &MainWindow::onLanguageChanged); + connect(&m_OrganizerCore.settings(), &Settings::themeChanged, this, + &MainWindow::themeChanged); connect(m_OrganizerCore.updater(), SIGNAL(restart()), this, SLOT(close())); connect(m_OrganizerCore.updater(), SIGNAL(updateAvailable()), this, @@ -426,21 +430,21 @@ MainWindow::MainWindow(Settings& settings, OrganizerCore& organizerCore, connect(ui->actionTool->menu(), &QMenu::aboutToShow, [&] { updateToolMenu(); }); - connect(&m_PluginContainer, &PluginContainer::pluginEnabled, this, + connect(&m_PluginManager, &PluginManager::pluginEnabled, this, [this](IPlugin* plugin) { - if (m_PluginContainer.implementInterface(plugin)) { + if (m_PluginManager.implementInterface(plugin)) { updateModPageMenu(); } }); - connect(&m_PluginContainer, &PluginContainer::pluginDisabled, this, + connect(&m_PluginManager, &PluginManager::pluginDisabled, this, [this](IPlugin* plugin) { - if (m_PluginContainer.implementInterface(plugin)) { + if (m_PluginManager.implementInterface(plugin)) { updateModPageMenu(); } }); - connect(&m_PluginContainer, &PluginContainer::pluginRegistered, this, + connect(&m_PluginManager, &PluginManager::pluginRegistered, this, &MainWindow::onPluginRegistrationChanged); - connect(&m_PluginContainer, &PluginContainer::pluginUnregistered, this, + connect(&m_PluginManager, &PluginManager::pluginUnregistered, this, &MainWindow::onPluginRegistrationChanged); connect(&m_OrganizerCore, &OrganizerCore::modInstalled, this, @@ -498,9 +502,6 @@ MainWindow::MainWindow(Settings& settings, OrganizerCore& organizerCore, [=](auto&& message) { showMessage(message); }); - for (const QString& fileName : m_PluginContainer.pluginFileNames()) { - installTranslator(QFileInfo(fileName).baseName()); - } updateModPageMenu(); @@ -1025,9 +1026,9 @@ void MainWindow::checkForProblemsImpl() m_ProblemsCheckRequired = false; TimeThis tt("MainWindow::checkForProblemsImpl()"); size_t numProblems = 0; - for (QObject* pluginObj : m_PluginContainer.plugins()) { + for (QObject* pluginObj : m_PluginManager.plugins()) { IPlugin* plugin = qobject_cast(pluginObj); - if (plugin == nullptr || m_PluginContainer.isEnabled(plugin)) { + if (plugin == nullptr || m_PluginManager.isEnabled(plugin)) { IPluginDiagnose* diagnose = qobject_cast(pluginObj); if (diagnose != nullptr) numProblems += diagnose->activeProblems().size(); @@ -1232,7 +1233,7 @@ void MainWindow::showEvent(QShowEvent* event) // by connecting the event here, changing the style setting will first be // handled by MOApplication, and then in updateStyle(), at which point the // stylesheet has already been set correctly - connect(this, SIGNAL(styleChanged(QString)), this, SLOT(updateStyle(QString))); + connect(this, &MainWindow::themeChanged, this, &MainWindow::updateStyle); // only the first time the window becomes visible m_Tutorial.registerControl(); @@ -1347,7 +1348,7 @@ void MainWindow::showEvent(QShowEvent* event) updateProblemsButton(); // notify plugins that the MO2 is ready - m_PluginContainer.startPlugins(this); + m_PluginManager.startPlugins(this); // forces a log list refresh to display startup logs // @@ -1514,7 +1515,7 @@ void MainWindow::updateToolMenu() // Clear the menu: ui->actionTool->menu()->clear(); - std::vector toolPlugins = m_PluginContainer.plugins(); + std::vector toolPlugins = m_PluginManager.plugins(); // Sort the plugins by display name std::sort(std::begin(toolPlugins), std::end(toolPlugins), @@ -1525,7 +1526,7 @@ void MainWindow::updateToolMenu() // Remove disabled plugins: toolPlugins.erase(std::remove_if(std::begin(toolPlugins), std::end(toolPlugins), [&](auto* tool) { - return !m_PluginContainer.isEnabled(tool); + return !m_PluginManager.isEnabled(tool); }), toolPlugins.end()); @@ -1617,7 +1618,7 @@ void MainWindow::updateModPageMenu() // Determine the loaded mod page plugins std::vector modPagePlugins = - m_PluginContainer.plugins(); + m_PluginManager.plugins(); // Sort the plugins by display name std::sort(std::begin(modPagePlugins), std::end(modPagePlugins), @@ -1629,7 +1630,7 @@ void MainWindow::updateModPageMenu() modPagePlugins.erase(std::remove_if(std::begin(modPagePlugins), std::end(modPagePlugins), [&](auto* tool) { - return !m_PluginContainer.isEnabled(tool); + return !m_PluginManager.isEnabled(tool); }), modPagePlugins.end()); @@ -1935,7 +1936,7 @@ void MainWindow::updateBSAList(const QStringList& defaultArchives, }; for (FileEntryPtr current : files) { - QFileInfo fileInfo(ToQString(current->getName().c_str())); + QFileInfo fileInfo(ToQString(current->getName())); if (fileInfo.suffix().toLower() == "bsa" || fileInfo.suffix().toLower() == "ba2") { int index = activeArchives.indexOf(fileInfo.fileName()); @@ -2443,7 +2444,7 @@ void MainWindow::modInstalled(const QString& modName) void MainWindow::importCategories(bool) { NexusInterface& nexus = NexusInterface::instance(); - nexus.setPluginContainer(&m_OrganizerCore.pluginContainer()); + nexus.setPluginManager(&m_OrganizerCore.pluginManager()); nexus.requestGameInfo(Settings::instance().game().plugin()->gameShortName(), this, QVariant(), QString()); } @@ -2719,7 +2720,8 @@ void MainWindow::on_actionSettings_triggered() const bool oldCheckForUpdates = settings.checkForUpdates(); const int oldMaxDumps = settings.diagnostics().maxCoreDumps(); - SettingsDialog dialog(&m_PluginContainer, settings, this); + SettingsDialog dialog(m_ExtensionManager, m_PluginManager, m_ThemeManager, + m_TranslationManager, settings, this); dialog.exec(); auto e = dialog.exitNeeded(); @@ -2829,7 +2831,7 @@ void MainWindow::onPluginRegistrationChanged() void MainWindow::refreshNexusCategories(CategoriesDialog* dialog) { NexusInterface& nexus = NexusInterface::instance(); - nexus.setPluginContainer(&m_PluginContainer); + nexus.setPluginManager(&m_PluginManager); if (!Settings::instance().game().plugin()->primarySources().isEmpty()) { nexus.requestGameInfo( Settings::instance().game().plugin()->primarySources().first(), dialog, @@ -2860,42 +2862,10 @@ void MainWindow::on_actionNexus_triggered() shell::Open(QUrl(NexusInterface::instance().getGameURL(gameName))); } -void MainWindow::installTranslator(const QString& name) -{ - QTranslator* translator = new QTranslator(this); - QString fileName = name + "_" + m_CurrentLanguage; - 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) - } - - qApp->installTranslator(translator); - m_Translators.push_back(translator); -} - -void MainWindow::languageChange(const QString& newLanguage) +void MainWindow::onLanguageChanged(const QString& newLanguage) { - for (QTranslator* trans : m_Translators) { - qApp->removeTranslator(trans); - } - m_Translators.clear(); - - m_CurrentLanguage = newLanguage; - - installTranslator("qt"); - installTranslator("qtbase"); - installTranslator(ToQString(AppConfig::translationPrefix())); - installTranslator("uibase"); + m_TranslationManager.load(newLanguage.toStdString()); - // TODO: this will probably be changed once extension come out - installTranslator("game_gamebryo"); - installTranslator("game_creation"); - - for (const QString& fileName : m_PluginContainer.pluginFileNames()) { - installTranslator(QFileInfo(fileName).baseName()); - } ui->retranslateUi(this); log::debug("loaded language {}", newLanguage); @@ -2936,7 +2906,7 @@ void MainWindow::motdReceived(const QString& motd) // don't show motd after 5 seconds, may be annoying. Hopefully the user's // internet connection is faster next time if (m_StartTime.secsTo(QTime::currentTime()) < 5) { - uint hash = qHash(motd); + unsigned int hash = static_cast(qHash(motd)); if (hash != m_OrganizerCore.settings().motdHash()) { MotDDialog dialog(motd); dialog.exec(); @@ -3116,7 +3086,7 @@ void MainWindow::nxmUpdateInfoAvailable(QString gameName, QVariant userData, QVariant resultData, int) { QString gameNameReal; - for (IPluginGame* game : m_PluginContainer.plugins()) { + for (IPluginGame* game : m_PluginManager.plugins()) { if (game->gameNexusName() == gameName) { gameNameReal = game->gameShortName(); break; @@ -3172,7 +3142,7 @@ void MainWindow::nxmUpdatesAvailable(QString gameName, int modID, QVariant userD QList fileUpdates = resultInfo["file_updates"].toList(); QString gameNameReal; - for (IPluginGame* game : m_PluginContainer.plugins()) { + for (IPluginGame* game : m_PluginManager.plugins()) { if (game->gameNexusName() == gameName) { gameNameReal = game->gameShortName(); break; @@ -3315,7 +3285,7 @@ void MainWindow::nxmModInfoAvailable(QString gameName, int modID, QVariant userD QString gameNameReal; bool foundUpdate = false; - for (IPluginGame* game : m_PluginContainer.plugins()) { + for (IPluginGame* game : m_PluginManager.plugins()) { if (game->gameNexusName() == gameName) { gameNameReal = game->gameShortName(); break; @@ -3417,7 +3387,7 @@ void MainWindow::nxmEndorsementToggled(QString, int, QVariant, QVariant resultDa void MainWindow::nxmTrackedModsAvailable(QVariant userData, QVariant resultData, int) { QMap gameNames; - for (auto game : m_PluginContainer.plugins()) { + for (auto game : m_PluginManager.plugins()) { gameNames[game->gameNexusName()] = game->gameShortName(); } @@ -3505,7 +3475,7 @@ void MainWindow::nxmRequestFailed(QString gameName, int modID, int, QVariant, in // update last checked timestamp on orphaned mods as well to avoid repeating // requests QString gameNameReal; - for (IPluginGame* game : m_PluginContainer.plugins()) { + for (IPluginGame* game : m_PluginManager.plugins()) { if (game->gameNexusName() == gameName) { gameNameReal = game->gameShortName(); break; @@ -3651,7 +3621,7 @@ void MainWindow::on_actionNotifications_triggered() future.waitForFinished(); - ProblemsDialog problems(m_PluginContainer, this); + ProblemsDialog problems(m_PluginManager, this); problems.exec(); scheduleCheckForProblems(); @@ -3659,18 +3629,20 @@ void MainWindow::on_actionNotifications_triggered() void MainWindow::on_actionChange_Game_triggered() { - InstanceManagerDialog dlg(m_PluginContainer, this); + InstanceManagerDialog dlg(m_PluginManager, this); dlg.exec(); } void MainWindow::setCategoryListVisible(bool visible) { + using namespace std::literals; + if (visible) { ui->categoriesGroup->show(); - ui->displayCategoriesBtn->setText(ToQString(L"\u00ab")); + ui->displayCategoriesBtn->setText(ToQString(L"\u00ab"sv)); } else { ui->categoriesGroup->hide(); - ui->displayCategoriesBtn->setText(ToQString(L"\u00bb")); + ui->displayCategoriesBtn->setText(ToQString(L"\u00bb"sv)); } } diff --git a/src/mainwindow.h b/src/mainwindow.h index b3f4233aa..18435bc78 100644 --- a/src/mainwindow.h +++ b/src/mainwindow.h @@ -28,12 +28,15 @@ along with Mod Organizer. If not, see . #include #include "delayedfilewriter.h" +#include "extensionmanager.h" #include "iuserinterface.h" #include "modinfo.h" #include "modlistbypriorityproxy.h" #include "modlistsortproxy.h" -#include "plugincontainer.h" +#include "pluginmanager.h" //class PluginManager; #include "shared/fileregisterfwd.h" +#include "thememanager.h" +#include "translationmanager.h" class Executable; class CategoryFactory; @@ -126,7 +129,9 @@ class MainWindow : public QMainWindow, public IUserInterface public: explicit MainWindow(Settings& settings, OrganizerCore& organizerCore, - PluginContainer& pluginContainer, QWidget* parent = 0); + ExtensionManager& extensionManager, PluginManager& pluginManager, + ThemeManager& themeManager, + TranslationManager& translationManager, QWidget* parent = 0); ~MainWindow(); void processUpdates(); @@ -139,8 +144,6 @@ class MainWindow : public QMainWindow, public IUserInterface void saveArchiveList(); - void installTranslator(const QString& name); - void displayModInformation(ModInfo::Ptr modInfo, unsigned int modIndex, ModInfoTabIDs tabID) override; @@ -166,7 +169,7 @@ public slots: /** * @brief emitted when the selected style changes */ - void styleChanged(const QString& styleFile); + void themeChanged(const QString& themeIdentifier); void checkForProblemsDone(); @@ -297,10 +300,10 @@ private slots: QTime m_StartTime; OrganizerCore& m_OrganizerCore; - PluginContainer& m_PluginContainer; - - QString m_CurrentLanguage; - std::vector m_Translators; + ExtensionManager& m_ExtensionManager; + PluginManager& m_PluginManager; + ThemeManager& m_ThemeManager; + TranslationManager& m_TranslationManager; std::unique_ptr m_IntegratedBrowser; @@ -344,7 +347,7 @@ private slots: void linkDesktop(); void linkMenu(); - void languageChange(const QString& newLanguage); + void onLanguageChanged(const QString& newLanguage); void windowTutorialFinished(const QString& windowName); diff --git a/src/moapplication.cpp b/src/moapplication.cpp index f621d32a9..79994938d 100644 --- a/src/moapplication.cpp +++ b/src/moapplication.cpp @@ -19,6 +19,7 @@ along with Mod Organizer. If not, see . #include "moapplication.h" #include "commandline.h" +#include "extensionmanager.h" #include "instancemanager.h" #include "loglist.h" #include "mainwindow.h" @@ -27,16 +28,17 @@ along with Mod Organizer. If not, see . #include "nexusinterface.h" #include "nxmaccessmanager.h" #include "organizercore.h" +#include "pluginmanager.h" #include "sanitychecks.h" #include "settings.h" #include "shared/appconfig.h" #include "shared/util.h" +#include "thememanager.h" #include "thread_utils.h" #include "tutorialmanager.h" #include #include #include -#include #include #include #include @@ -57,55 +59,6 @@ along with Mod Organizer. If not, see . using namespace MOBase; using namespace MOShared; -// style proxy that changes the appearance of drop indicators -// -class ProxyStyle : public QProxyStyle -{ -public: - ProxyStyle(QStyle* baseStyle = 0) : QProxyStyle(baseStyle) {} - - void drawPrimitive(PrimitiveElement element, const QStyleOption* option, - QPainter* painter, const QWidget* widget) const override - { - if (element == QStyle::PE_IndicatorItemViewItemDrop) { - - // 0. Fix a bug that made the drop indicator sometimes appear on top - // of the mod list when selecting a mod. - if (option->rect.height() == 0 && option->rect.bottomRight() == QPoint(-1, -1)) { - return; - } - - // 1. full-width drop indicator - QRect rect(option->rect); - if (auto* view = qobject_cast(widget)) { - rect.setLeft(view->indentation()); - rect.setRight(widget->width()); - } - - // 2. stylish drop indicator - painter->setRenderHint(QPainter::Antialiasing, true); - - QColor col(option->palette.windowText().color()); - QPen pen(col); - pen.setWidth(2); - col.setAlpha(50); - - painter->setPen(pen); - painter->setBrush(QBrush(col)); - if (rect.height() == 0) { - QPoint tri[3] = {rect.topLeft(), rect.topLeft() + QPoint(-5, 5), - rect.topLeft() + QPoint(-5, -5)}; - painter->drawPolygon(tri, 3); - painter->drawLine(rect.topLeft(), rect.topRight()); - } else { - painter->drawRoundedRect(rect, 5, 5); - } - } else { - QProxyStyle::drawPrimitive(element, option, painter, widget); - } - } -}; - // This adds the `dlls` directory to the path so the dlls can be found. How // MO is able to find dlls in there is a bit convoluted: // @@ -149,15 +102,6 @@ MOApplication::MOApplication(int& argc, char** argv) : QApplication(argc, argv) TimeThis tt("MOApplication()"); qputenv("QML_DISABLE_DISK_CACHE", "true"); - - connect(&m_styleWatcher, &QFileSystemWatcher::fileChanged, [&](auto&& file) { - log::debug("style file '{}' changed, reloading", file); - updateStyle(file); - }); - - m_defaultStyle = "windowsvista"; - updateStyle(m_defaultStyle); - addDllsToPath(); } OrganizerCore& MOApplication::core() @@ -268,7 +212,18 @@ int MOApplication::setup(MOMultiProcess& multiProcess, bool forceSelect) tt.start("MOApplication::doOneRun() plugins"); log::debug("initializing plugins"); - m_plugins = std::make_unique(m_core.get()); + m_themes = std::make_unique(this); + m_translations = std::make_unique(this); + + m_extensions = std::make_unique(m_core.get()); + m_extensions->registerWatcher(*m_themes); + m_extensions->registerWatcher(*m_translations); + + m_extensions->loadExtensions( + QDir(QCoreApplication::applicationDirPath() + "/extensions") + .filesystemAbsolutePath()); + + m_plugins = std::make_unique(*m_extensions, m_core.get()); m_plugins->loadPlugins(); // instance @@ -330,25 +285,27 @@ int MOApplication::run(MOMultiProcess& multiProcess) m_core.get()); // styling - if (!setStyleFile(m_settings->interface().styleName().value_or(""))) { + if (!m_themes->load(m_settings->interface().themeName().value_or("").toStdString())) { // disable invalid stylesheet - m_settings->interface().setStyleName(""); + m_settings->interface().setThemeName(""); } int res = 1; { tt.start("MOApplication::doOneRun() MainWindow setup"); - MainWindow mainWindow(*m_settings, *m_core, *m_plugins); + MainWindow mainWindow(*m_settings, *m_core, *m_extensions, *m_plugins, *m_themes, + *m_translations); // the nexus interface can show dialogs, make sure they're parented to the // main window m_nexus->getAccessManager()->setTopLevelWidget(&mainWindow); + // TODO: connect( - &mainWindow, &MainWindow::styleChanged, this, - [this](auto&& file) { - setStyleFile(file); + &mainWindow, &MainWindow::themeChanged, this, + [this](auto&& themeIdentifier) { + m_themes->load(themeIdentifier.toStdString()); }, Qt::QueuedConnection); @@ -471,7 +428,7 @@ std::unique_ptr MOApplication::getCurrentInstance(bool forceSelect) } std::optional MOApplication::setupInstanceLoop(Instance& currentInstance, - PluginContainer& pc) + PluginManager& pc) { for (;;) { const auto setupResult = setupInstance(currentInstance, pc); @@ -523,31 +480,6 @@ void MOApplication::resetForRestart() m_instance = {}; } -bool MOApplication::setStyleFile(const QString& styleName) -{ - // remove all files from watch - QStringList currentWatch = m_styleWatcher.files(); - if (currentWatch.count() != 0) { - m_styleWatcher.removePaths(currentWatch); - } - // set new stylesheet or clear it - if (styleName.length() != 0) { - QString styleSheetName = applicationDirPath() + "/" + - MOBase::ToQString(AppConfig::stylesheetsPath()) + "/" + - styleName; - if (QFile::exists(styleSheetName)) { - m_styleWatcher.addPath(styleSheetName); - updateStyle(styleSheetName); - } else { - updateStyle(styleName); - } - } else { - setStyle(new ProxyStyle(QStyleFactory::create(m_defaultStyle))); - setStyleSheet(""); - } - return true; -} - bool MOApplication::notify(QObject* receiver, QEvent* event) { try { @@ -565,101 +497,6 @@ bool MOApplication::notify(QObject* receiver, QEvent* event) } } -namespace -{ -QStringList extractTopStyleSheetComments(QFile& stylesheet) -{ - if (!stylesheet.open(QFile::ReadOnly)) { - log::error("failed to open stylesheet file {}", stylesheet.fileName()); - return {}; - } - ON_BLOCK_EXIT([&stylesheet]() { - stylesheet.close(); - }); - - QStringList topComments; - - while (true) { - const auto byteLine = stylesheet.readLine(); - if (byteLine.isNull()) { - break; - } - - const auto line = QString(byteLine).trimmed(); - - // skip empty lines - if (line.isEmpty()) { - continue; - } - - // only handle single line comments - if (!line.startsWith("/*")) { - break; - } - - topComments.push_back(line.mid(2, line.size() - 4).trimmed()); - } - - return topComments; -} - -QString extractBaseStyleFromStyleSheet(QFile& stylesheet, const QString& defaultStyle) -{ - // read the first line of the files that are either empty or comments - // - const auto topLines = extractTopStyleSheetComments(stylesheet); - - const auto factoryStyles = QStyleFactory::keys(); - - QString style = defaultStyle; - - for (const auto& line : topLines) { - if (!line.startsWith("mo2-base-style")) { - continue; - } - - const auto parts = line.split(":"); - if (parts.size() != 2) { - log::warn("found invalid top-comment for mo2 in {}: {}", stylesheet.fileName(), - line); - continue; - } - - const auto tmpStyle = parts[1].trimmed(); - const auto index = factoryStyles.indexOf(tmpStyle, 0, Qt::CaseInsensitive); - if (index == -1) { - log::warn("base style '{}' from style '{}' not found", tmpStyle, - stylesheet.fileName(), line); - continue; - } - - style = factoryStyles[index]; - log::info("found base style '{}' for style '{}'", style, stylesheet.fileName()); - break; - } - - return style; -} - -} // namespace - -void MOApplication::updateStyle(const QString& fileName) -{ - if (QStyleFactory::keys().contains(fileName)) { - setStyleSheet(""); - setStyle(new ProxyStyle(QStyleFactory::create(fileName))); - } else { - QFile stylesheet(fileName); - if (stylesheet.exists()) { - setStyle(new ProxyStyle(QStyleFactory::create( - extractBaseStyleFromStyleSheet(stylesheet, m_defaultStyle)))); - setStyleSheet(QString("file:///%1").arg(fileName)); - } else { - log::warn("invalid stylesheet: {}", fileName); - } - } -} - MOSplash::MOSplash(const Settings& settings, const QString& dataPath, const MOBase::IPluginGame* game) { diff --git a/src/moapplication.h b/src/moapplication.h index 498242f3e..55a8b943f 100644 --- a/src/moapplication.h +++ b/src/moapplication.h @@ -24,12 +24,16 @@ along with Mod Organizer. If not, see . #include #include -class Settings; -class MOMultiProcess; +#include "extensionmanager.h" +#include "thememanager.h" +#include "translationmanager.h" + class Instance; -class PluginContainer; -class OrganizerCore; +class MOMultiProcess; class NexusInterface; +class OrganizerCore; +class PluginManager; +class Settings; namespace MOBase { @@ -70,26 +74,21 @@ class MOApplication : public QApplication // bool notify(QObject* receiver, QEvent* event) override; -public slots: - bool setStyleFile(const QString& style); - -private slots: - void updateStyle(const QString& fileName); - private: - QFileSystemWatcher m_styleWatcher; - QString m_defaultStyle; std::unique_ptr m_modules; std::unique_ptr m_instance; std::unique_ptr m_settings; std::unique_ptr m_nexus; - std::unique_ptr m_plugins; + std::unique_ptr m_extensions; + std::unique_ptr m_plugins; + std::unique_ptr m_themes; + std::unique_ptr m_translations; std::unique_ptr m_core; void externalMessage(const QString& message); std::unique_ptr getCurrentInstance(bool forceSelect); - std::optional setupInstanceLoop(Instance& currentInstance, PluginContainer& pc); + std::optional setupInstanceLoop(Instance& currentInstance, PluginManager& pc); void purgeOldFiles(); }; diff --git a/src/modinfo.cpp b/src/modinfo.cpp index abfd9de6c..3ee958817 100644 --- a/src/modinfo.cpp +++ b/src/modinfo.cpp @@ -94,7 +94,7 @@ ModInfo::Ptr ModInfo::createFrom(const QDir& dir, OrganizerCore& core) } else { result = ModInfo::Ptr(new ModInfoRegular(dir, core)); } - result->m_Index = s_Collection.size(); + result->m_Index = static_cast(s_Collection.size()); s_Collection.push_back(result); return result; } @@ -106,7 +106,7 @@ ModInfo::Ptr ModInfo::createFromPlugin(const QString& modName, const QString& es QMutexLocker locker(&s_Mutex); ModInfo::Ptr result = ModInfo::Ptr(new ModInfoForeign(modName, espName, bsaNames, modType, core)); - result->m_Index = s_Collection.size(); + result->m_Index = static_cast(s_Collection.size()); s_Collection.push_back(result); return result; } @@ -115,7 +115,7 @@ ModInfo::Ptr ModInfo::createFromOverwrite(OrganizerCore& core) { QMutexLocker locker(&s_Mutex); ModInfo::Ptr overwrite = ModInfo::Ptr(new ModInfoOverwrite(core)); - overwrite->m_Index = s_Collection.size(); + overwrite->m_Index = static_cast(s_Collection.size()); s_Collection.push_back(overwrite); return overwrite; } @@ -250,7 +250,7 @@ void ModInfo::updateFromDisc(const QString& modsDirectory, OrganizerCore& core, } auto* game = core.managedGame(); - auto& features = core.pluginContainer().gameFeatures(); + auto& features = core.pluginManager().gameFeatures(); auto unmanaged = features.gameFeature(); if (unmanaged != nullptr) { for (const QString& modName : unmanaged->mods(!displayForeign)) { @@ -296,7 +296,7 @@ void ModInfo::updateIndices() ModInfo::ModInfo(OrganizerCore& core) : m_PrimaryCategory(-1), m_Core(core) {} -bool ModInfo::checkAllForUpdate(PluginContainer* pluginContainer, QObject* receiver) +bool ModInfo::checkAllForUpdate(PluginManager* pluginManager, QObject* receiver) { bool updatesAvailable = true; @@ -315,7 +315,7 @@ bool ModInfo::checkAllForUpdate(PluginContainer* pluginContainer, QObject* recei // Detect invalid source games for (auto itr = games.begin(); itr != games.end();) { - auto gamePlugins = pluginContainer->plugins(); + auto gamePlugins = pluginManager->plugins(); IPluginGame* gamePlugin = qApp->property("managed_game").value(); for (auto plugin : gamePlugins) { if (plugin != nullptr && diff --git a/src/modinfo.h b/src/modinfo.h index bbb1aae3c..c43a40c84 100644 --- a/src/modinfo.h +++ b/src/modinfo.h @@ -25,7 +25,7 @@ along with Mod Organizer. If not, see . #include "versioninfo.h" class OrganizerCore; -class PluginContainer; +class PluginManager; class QDir; class QDateTime; @@ -215,7 +215,7 @@ class ModInfo : public QObject, public MOBase::IModInterface * * @return true if any mods are checked for update. */ - static bool checkAllForUpdate(PluginContainer* pluginContainer, QObject* receiver); + static bool checkAllForUpdate(PluginManager* pluginManager, QObject* receiver); /** * diff --git a/src/modinfodialog.cpp b/src/modinfodialog.cpp index 1798e9870..f9e480e85 100644 --- a/src/modinfodialog.cpp +++ b/src/modinfodialog.cpp @@ -27,7 +27,7 @@ along with Mod Organizer. If not, see . #include "modinfodialogtextfiles.h" #include "modlistview.h" #include "organizercore.h" -#include "plugincontainer.h" +#include "pluginmanager.h" #include "shared/directoryentry.h" #include "shared/filesorigin.h" #include "ui_modinfodialog.h" @@ -39,11 +39,11 @@ namespace fs = std::filesystem; const int max_scan_for_context_menu = 50; -bool canPreviewFile(const PluginContainer& pluginContainer, bool isArchive, +bool canPreviewFile(const PluginManager& pluginManager, bool isArchive, const QString& filename) { const auto ext = QFileInfo(filename).suffix().toLower(); - return pluginContainer.previewGenerator().previewSupported(ext, isArchive); + return pluginManager.previewGenerator().previewSupported(ext, isArchive); } bool isExecutableFilename(const QString& filename) @@ -164,11 +164,11 @@ bool ModInfoDialog::TabInfo::isVisible() const return (realPos != -1); } -ModInfoDialog::ModInfoDialog(OrganizerCore& core, PluginContainer& plugin, +ModInfoDialog::ModInfoDialog(OrganizerCore& core, PluginManager& plugins, ModInfo::Ptr mod, ModListView* modListView, QWidget* parent) : TutorableDialog("ModInfoDialog", parent), ui(new Ui::ModInfoDialog), m_core(core), - m_plugin(plugin), m_modListView(modListView), m_initialTab(ModInfoTabIDs::None), + m_plugins(plugins), m_modListView(modListView), m_initialTab(ModInfoTabIDs::None), m_arrangingTabs(false) { ui->setupUi(this); @@ -218,7 +218,7 @@ template std::unique_ptr createTab(ModInfoDialog& d, ModInfoTabIDs id) { return std::make_unique(ModInfoDialogTabContext( - d.m_core, d.m_plugin, &d, d.ui.get(), id, d.m_mod, d.getOrigin())); + d.m_core, d.m_plugins, &d, d.ui.get(), id, d.m_mod, d.getOrigin())); } void ModInfoDialog::createTabs() diff --git a/src/modinfodialog.h b/src/modinfodialog.h index e894efb0c..cb74c6f02 100644 --- a/src/modinfodialog.h +++ b/src/modinfodialog.h @@ -34,7 +34,7 @@ namespace MOShared class FilesOrigin; } -class PluginContainer; +class PluginManager; class OrganizerCore; class Settings; class ModInfoDialogTab; @@ -56,7 +56,7 @@ class ModInfoDialog : public MOBase::TutorableDialog ModInfoTabIDs index); public: - ModInfoDialog(OrganizerCore& core, PluginContainer& plugin, ModInfo::Ptr mod, + ModInfoDialog(OrganizerCore& core, PluginManager& plugins, ModInfo::Ptr mod, ModListView* view, QWidget* parent = nullptr); ~ModInfoDialog(); @@ -123,7 +123,7 @@ class ModInfoDialog : public MOBase::TutorableDialog std::unique_ptr ui; OrganizerCore& m_core; - PluginContainer& m_plugin; + PluginManager& m_plugins; ModListView* m_modListView; ModInfo::Ptr m_mod; std::vector m_tabs; diff --git a/src/modinfodialogconflicts.cpp b/src/modinfodialogconflicts.cpp index 715214b33..e47f94147 100644 --- a/src/modinfodialogconflicts.cpp +++ b/src/modinfodialogconflicts.cpp @@ -221,7 +221,7 @@ void ConflictsTab::activateItems(QTreeView* tree) forEachInSelection(tree, [&](const ConflictItem* item) { const auto path = item->fileName(); - if (tryPreview && canPreviewFile(plugin(), item->isArchive(), path)) { + if (tryPreview && canPreviewFile(plugins(), item->isArchive(), path)) { previewItem(item); } else { openItem(item, false); @@ -424,7 +424,7 @@ ConflictsTab::Actions ConflictsTab::createMenuActions(QTreeView* tree) enableUnhide = item->canUnhide(); enableRun = item->canRun(); enableOpen = item->canOpen(); - enablePreview = item->canPreview(plugin()); + enablePreview = item->canPreview(plugins()); enableExplore = item->canExplore(); enableGoto = item->hasAlts(); } else { diff --git a/src/modinfodialogconflictsmodels.cpp b/src/modinfodialogconflictsmodels.cpp index a1806e61f..d2a6b18fe 100644 --- a/src/modinfodialogconflictsmodels.cpp +++ b/src/modinfodialogconflictsmodels.cpp @@ -73,9 +73,9 @@ bool ConflictItem::canOpen() const return canOpenFile(isArchive(), fileName()); } -bool ConflictItem::canPreview(PluginContainer& pluginContainer) const +bool ConflictItem::canPreview(PluginManager& pluginManager) const { - return canPreviewFile(pluginContainer, isArchive(), fileName()); + return canPreviewFile(pluginManager, isArchive(), fileName()); } bool ConflictItem::canExplore() const diff --git a/src/modinfodialogconflictsmodels.h b/src/modinfodialogconflictsmodels.h index 263723285..851c440e3 100644 --- a/src/modinfodialogconflictsmodels.h +++ b/src/modinfodialogconflictsmodels.h @@ -1,6 +1,6 @@ #include "shared/fileentry.h" -class PluginContainer; +class PluginManager; class ConflictItem { @@ -25,7 +25,7 @@ class ConflictItem bool canUnhide() const; bool canRun() const; bool canOpen() const; - bool canPreview(PluginContainer& pluginContainer) const; + bool canPreview(PluginManager& pluginManager) const; bool canExplore() const; private: diff --git a/src/modinfodialogfiletree.cpp b/src/modinfodialogfiletree.cpp index 140b813d3..76bb28a22 100644 --- a/src/modinfodialogfiletree.cpp +++ b/src/modinfodialogfiletree.cpp @@ -178,7 +178,7 @@ void FileTreeTab::onActivated() const auto path = m_fs->filePath(selection); const auto tryPreview = core().settings().interface().doubleClicksOpenPreviews(); - if (tryPreview && canPreviewFile(plugin(), false, path)) { + if (tryPreview && canPreviewFile(plugins(), false, path)) { onPreview(); } else { onOpen(); @@ -446,7 +446,7 @@ void FileTreeTab::onContextMenu(const QPoint& pos) } } - enablePreview = canPreviewFile(plugin(), false, fileName); + enablePreview = canPreviewFile(plugins(), false, fileName); enableExplore = canExploreFile(false, fileName); enableHide = canHideFile(false, fileName); enableUnhide = canUnhideFile(false, fileName); diff --git a/src/modinfodialogfwd.h b/src/modinfodialogfwd.h index 086263236..ee3cac219 100644 --- a/src/modinfodialogfwd.h +++ b/src/modinfodialogfwd.h @@ -21,9 +21,9 @@ enum class ModInfoTabIDs Filetree }; -class PluginContainer; +class PluginManager; -bool canPreviewFile(const PluginContainer& pluginContainer, bool isArchive, +bool canPreviewFile(const PluginManager& pluginManager, bool isArchive, const QString& filename); bool canRunFile(bool isArchive, const QString& filename); bool canOpenFile(bool isArchive, const QString& filename); diff --git a/src/modinfodialogimages.cpp b/src/modinfodialogimages.cpp index 7b250e1e8..877e7f30e 100644 --- a/src/modinfodialogimages.cpp +++ b/src/modinfodialogimages.cpp @@ -261,7 +261,7 @@ void ImagesTab::select(std::size_t i, Visibility v) ui->imagesPath->setText(QDir::toNativeSeparators(f->path())); ui->imagesExplore->setEnabled(true); - if (plugin().previewGenerator().previewSupported( + if (plugins().previewGenerator().previewSupported( QFileInfo(f->path()).suffix().toLower(), false)) ui->previewPluginButton->setEnabled(true); else diff --git a/src/modinfodialogimages.h b/src/modinfodialogimages.h index e7311aa0a..e35d1648e 100644 --- a/src/modinfodialogimages.h +++ b/src/modinfodialogimages.h @@ -4,7 +4,6 @@ #include "filterwidget.h" #include "modinfodialogtab.h" #include "organizercore.h" -#include "plugincontainer.h" #include using namespace MOBase; diff --git a/src/modinfodialognexus.cpp b/src/modinfodialognexus.cpp index 485f138a9..a99b79829 100644 --- a/src/modinfodialognexus.cpp +++ b/src/modinfodialognexus.cpp @@ -100,7 +100,7 @@ void NexusTab::update() if (core().managedGame()->validShortNames().size() == 0) { ui->sourceGame->setDisabled(true); } else { - for (auto game : plugin().plugins()) { + for (auto game : plugins().plugins()) { for (QString gameName : core().managedGame()->validShortNames()) { if (game->gameShortName().compare(gameName, Qt::CaseInsensitive) == 0) { ui->sourceGame->addItem(game->gameName(), game->gameShortName()); @@ -345,7 +345,7 @@ void NexusTab::onSourceGameChanged() return; } - for (auto game : plugin().plugins()) { + for (auto game : plugins().plugins()) { if (game->gameName() == ui->sourceGame->currentText()) { mod().setGameName(game->gameShortName()); mod().setLastNexusQuery(QDateTime::fromSecsSinceEpoch(0)); diff --git a/src/modinfodialogtab.cpp b/src/modinfodialogtab.cpp index c443a389b..96e901668 100644 --- a/src/modinfodialogtab.cpp +++ b/src/modinfodialogtab.cpp @@ -5,7 +5,7 @@ #include "ui_modinfodialog.h" ModInfoDialogTab::ModInfoDialogTab(ModInfoDialogTabContext cx) - : ui(cx.ui), m_core(cx.core), m_plugin(cx.plugin), m_parent(cx.parent), + : ui(cx.ui), m_core(cx.core), m_plugins(cx.plugins), m_parent(cx.parent), m_origin(cx.origin), m_tabID(cx.id), m_hasData(false), m_firstActivation(true) {} @@ -112,9 +112,9 @@ OrganizerCore& ModInfoDialogTab::core() return m_core; } -PluginContainer& ModInfoDialogTab::plugin() +PluginManager& ModInfoDialogTab::plugins() { - return m_plugin; + return m_plugins; } QWidget* ModInfoDialogTab::parentWidget() diff --git a/src/modinfodialogtab.h b/src/modinfodialogtab.h index dff1732d0..40706c022 100644 --- a/src/modinfodialogtab.h +++ b/src/modinfodialogtab.h @@ -21,17 +21,17 @@ class OrganizerCore; struct ModInfoDialogTabContext { OrganizerCore& core; - PluginContainer& plugin; + PluginManager& plugins; QWidget* parent; Ui::ModInfoDialog* ui; ModInfoTabIDs id; ModInfoPtr mod; MOShared::FilesOrigin* origin; - ModInfoDialogTabContext(OrganizerCore& core, PluginContainer& plugin, QWidget* parent, + ModInfoDialogTabContext(OrganizerCore& core, PluginManager& plugins, QWidget* parent, Ui::ModInfoDialog* ui, ModInfoTabIDs id, ModInfoPtr mod, MOShared::FilesOrigin* origin) - : core(core), plugin(plugin), parent(parent), ui(ui), id(id), mod(mod), + : core(core), plugins(plugins), parent(parent), ui(ui), id(id), mod(mod), origin(origin) {} }; @@ -227,7 +227,7 @@ class ModInfoDialogTab : public QObject ModInfoDialogTab(ModInfoDialogTabContext cx); OrganizerCore& core(); - PluginContainer& plugin(); + PluginManager& plugins(); QWidget* parentWidget(); // emits originModified @@ -254,7 +254,7 @@ class ModInfoDialogTab : public QObject OrganizerCore& m_core; // plugin - PluginContainer& m_plugin; + PluginManager& m_plugins; // current mod, never null ModInfoPtr m_mod; diff --git a/src/modinforegular.cpp b/src/modinforegular.cpp index d3ff65472..d7239bf36 100644 --- a/src/modinforegular.cpp +++ b/src/modinforegular.cpp @@ -4,7 +4,7 @@ #include "messagedialog.h" #include "moddatacontent.h" #include "organizercore.h" -#include "plugincontainer.h" +#include "pluginmanager.h" #include "report.h" #include "settings.h" #include @@ -34,8 +34,7 @@ ModInfoRegular::ModInfoRegular(const QDir& path, OrganizerCore& core) m_GameName(core.managedGame()->gameShortName()), m_IsAlternate(false), m_Converted(false), m_Validated(false), m_MetaInfoChanged(false), m_EndorsedState(EndorsedState::ENDORSED_UNKNOWN), - m_TrackedState(TrackedState::TRACKED_UNKNOWN), - m_NexusBridge(&core.pluginContainer()) + m_TrackedState(TrackedState::TRACKED_UNKNOWN), m_NexusBridge() { m_CreationTime = QFileInfo(path.absolutePath()).birthTime(); // read out the meta-file for information @@ -704,7 +703,7 @@ std::vector ModInfoRegular::getFlags() const std::set ModInfoRegular::doGetContents() const { auto contentFeature = - m_Core.pluginContainer().gameFeatures().gameFeature(); + m_Core.pluginManager().gameFeatures().gameFeature(); if (contentFeature) { auto result = contentFeature->getContentsFor(fileTree()); diff --git a/src/modlist.cpp b/src/modlist.cpp index 8945dea11..2a85ebebb 100644 --- a/src/modlist.cpp +++ b/src/modlist.cpp @@ -60,10 +60,10 @@ along with Mod Organizer. If not, see . using namespace MOBase; -ModList::ModList(PluginContainer* pluginContainer, OrganizerCore* organizer) +ModList::ModList(PluginManager* pluginManager, OrganizerCore* organizer) : QAbstractItemModel(organizer), m_Organizer(organizer), m_Profile(nullptr), m_NexusInterface(nullptr), m_Modified(false), m_InNotifyChange(false), - m_FontMetrics(QFont()), m_PluginContainer(pluginContainer) + m_FontMetrics(QFont()), m_PluginManager(pluginManager) { m_LastCheck.start(); } @@ -218,8 +218,8 @@ QVariant ModList::data(const QModelIndex& modelIndex, int role) const return QVariant(); } } else if (column == COL_GAME) { - if (m_PluginContainer != nullptr) { - for (auto game : m_PluginContainer->plugins()) { + if (m_PluginManager != nullptr) { + for (auto game : m_PluginManager->plugins()) { if (game->gameShortName().compare(modInfo->gameName(), Qt::CaseInsensitive) == 0) return game->gameName(); @@ -726,9 +726,9 @@ void ModList::changeModPriority(int sourceIndex, int newPriority) emit modPrioritiesChanged({index(sourceIndex, 0)}); } -void ModList::setPluginContainer(PluginContainer* pluginContianer) +void ModList::setPluginManager(PluginManager* pluginContianer) { - m_PluginContainer = pluginContianer; + m_PluginManager = pluginContianer; } bool ModList::modInfoAboutToChange(ModInfo::Ptr info) diff --git a/src/modlist.h b/src/modlist.h index 112070e12..cf05351b6 100644 --- a/src/modlist.h +++ b/src/modlist.h @@ -41,7 +41,7 @@ along with Mod Organizer. If not, see . #include class QSortFilterProxyModel; -class PluginContainer; +class PluginManager; class OrganizerCore; class ModListDropInfo; @@ -106,7 +106,7 @@ class ModList : public QAbstractItemModel * @todo ensure this view works without a profile set, otherwise there are *intransparent dependencies on the initialisation order **/ - ModList(PluginContainer* pluginContainer, OrganizerCore* parent); + ModList(PluginManager* pluginManager, OrganizerCore* parent); ~ModList(); @@ -136,7 +136,7 @@ class ModList : public QAbstractItemModel void changeModPriority(int sourceIndex, int newPriority); void changeModPriority(std::vector sourceIndices, int newPriority); - void setPluginContainer(PluginContainer* pluginContainer); + void setPluginManager(PluginManager* pluginContainer); bool modInfoAboutToChange(ModInfo::Ptr info); void modInfoChanged(ModInfo::Ptr info); @@ -414,7 +414,7 @@ public slots: QElapsedTimer m_LastCheck; - PluginContainer* m_PluginContainer; + PluginManager* m_PluginManager; }; #endif // MODLIST_H diff --git a/src/modlistbypriorityproxy.cpp b/src/modlistbypriorityproxy.cpp index ba53ee749..bc0def559 100644 --- a/src/modlistbypriorityproxy.cpp +++ b/src/modlistbypriorityproxy.cpp @@ -131,8 +131,8 @@ void ModListByPriorityProxy::onModelLayoutChanged(const QList(idx.internalPointer()); - toPersistent.append( - createIndex(item->parent->childIndex(item), idx.column(), item)); + toPersistent.append(createIndex(static_cast(item->parent->childIndex(item)), + idx.column(), item)); } changePersistentIndexList(persistent, toPersistent); @@ -171,7 +171,8 @@ QModelIndex ModListByPriorityProxy::mapFromSource(const QModelIndex& sourceIndex } auto* item = m_IndexToItem.at(sourceIndex.row()).get(); - return createIndex(item->parent->childIndex(item), sourceIndex.column(), item); + return createIndex(static_cast(item->parent->childIndex(item)), + sourceIndex.column(), item); } QModelIndex ModListByPriorityProxy::mapToSource(const QModelIndex& proxyIndex) const @@ -187,13 +188,13 @@ QModelIndex ModListByPriorityProxy::mapToSource(const QModelIndex& proxyIndex) c int ModListByPriorityProxy::rowCount(const QModelIndex& parent) const { if (!parent.isValid()) { - return m_Root.children.size(); + return static_cast(m_Root.children.size()); } auto* item = static_cast(parent.internalPointer()); if (item->mod->isSeparator()) { - return item->children.size(); + return static_cast(item->children.size()); } return 0; @@ -216,7 +217,8 @@ QModelIndex ModListByPriorityProxy::parent(const QModelIndex& child) const return QModelIndex(); } - return createIndex(item->parent->parent->childIndex(item->parent), 0, item->parent); + return createIndex(static_cast(item->parent->parent->childIndex(item->parent)), + 0, item->parent); } bool ModListByPriorityProxy::hasChildren(const QModelIndex& parent) const diff --git a/src/modlistview.cpp b/src/modlistview.cpp index 81e87c5ee..cef6f737c 100644 --- a/src/modlistview.cpp +++ b/src/modlistview.cpp @@ -1136,7 +1136,7 @@ void ModListView::setHighlightedMods(const std::vector& pluginIndi QColor ModListView::markerColor(const QModelIndex& index) const { unsigned int modIndex = index.data(ModList::IndexRole).toInt(); - bool highligth = m_markers.highlight.find(modIndex) != m_markers.highlight.end(); + bool highlight = m_markers.highlight.find(modIndex) != m_markers.highlight.end(); bool overwrite = m_markers.overwrite.find(modIndex) != m_markers.overwrite.end(); bool archiveOverwrite = m_markers.archiveOverwrite.find(modIndex) != m_markers.archiveOverwrite.end(); @@ -1149,7 +1149,7 @@ QColor ModListView::markerColor(const QModelIndex& index) const bool archiveLooseOverwritten = m_markers.archiveLooseOverwritten.find(modIndex) != m_markers.archiveLooseOverwritten.end(); - if (highligth) { + if (highlight) { return Settings::instance().colors().modlistContainsPlugin(); } else if (overwritten || archiveLooseOverwritten) { return Settings::instance().colors().modlistOverwritingLoose(); @@ -1187,8 +1187,8 @@ QColor ModListView::markerColor(const QModelIndex& index) const a += color.alpha(); } - return QColor(r / colors.size(), g / colors.size(), b / colors.size(), - a / colors.size()); + const int ncolors = static_cast(colors.size()); + return QColor(r / ncolors, g / ncolors, b / ncolors, a / ncolors); } return QColor(); @@ -1391,9 +1391,10 @@ void ModListView::dropEvent(QDropEvent* event) { // from Qt source QModelIndex index; - if (viewport()->rect().contains(event->pos())) { - index = indexAt(event->pos()); - if (!index.isValid() || !visualRect(index).contains(event->pos())) + const auto position = event->position().toPoint(); + if (viewport()->rect().contains(position)) { + index = indexAt(position); + if (!index.isValid() || !visualRect(index).contains(position)) index = QModelIndex(); } diff --git a/src/modlistviewactions.cpp b/src/modlistviewactions.cpp index a38e876b1..35b4dfdc1 100644 --- a/src/modlistviewactions.cpp +++ b/src/modlistviewactions.cpp @@ -225,7 +225,7 @@ void ModListViewActions::checkModsForUpdates() const bool checkingModsForUpdate = false; if (NexusInterface::instance().getAccessManager()->validated()) { checkingModsForUpdate = - ModInfo::checkAllForUpdate(&m_core.pluginContainer(), m_receiver); + ModInfo::checkAllForUpdate(&m_core.pluginManager(), m_receiver); NexusInterface::instance().requestEndorsementInfo(m_receiver, QVariant(), QString()); NexusInterface::instance().requestTrackingInfo(m_receiver, QVariant(), QString()); @@ -568,7 +568,7 @@ void ModListViewActions::displayModInformation(ModInfo::Ptr modInfo, } else { modInfo->saveMeta(); - ModInfoDialog dialog(m_core, m_core.pluginContainer(), modInfo, m_view, m_parent); + ModInfoDialog dialog(m_core, m_core.pluginManager(), modInfo, m_view, m_parent); connect(&dialog, &ModInfoDialog::originModified, this, &ModListViewActions::originModified); connect(&dialog, &ModInfoDialog::modChanged, [=](unsigned int index) { diff --git a/src/nexusinterface.cpp b/src/nexusinterface.cpp index ecd33cb67..c6fd59dd3 100644 --- a/src/nexusinterface.cpp +++ b/src/nexusinterface.cpp @@ -46,7 +46,7 @@ void throttledWarning(const APIUserAccount& user) APIUserAccount::ThrottleThreshold, user.remainingRequests()); } -NexusBridge::NexusBridge(PluginContainer* pluginContainer, const QString& subModule) +NexusBridge::NexusBridge(const QString& subModule) : m_Interface(&NexusInterface::instance()), m_SubModule(subModule) {} @@ -268,7 +268,7 @@ NexusInterface::parseLimits(const QList& headers) static NexusInterface* g_instance = nullptr; -NexusInterface::NexusInterface(Settings* s) : m_PluginContainer(nullptr) +NexusInterface::NexusInterface(Settings* s) : m_PluginManager(nullptr) { MO_ASSERT(!g_instance); g_instance = this; @@ -435,7 +435,7 @@ NexusInterface::getGameChoices(const MOBase::IPluginGame* game) choices.push_back( std::pair(game->gameShortName(), game->gameName())); for (QString gameName : game->validShortNames()) { - for (auto gamePlugin : m_PluginContainer->plugins()) { + for (auto gamePlugin : m_PluginManager->plugins()) { if (gamePlugin->gameShortName().compare(gameName, Qt::CaseInsensitive) == 0) { choices.push_back(std::pair(gamePlugin->gameShortName(), gamePlugin->gameName())); @@ -456,9 +456,9 @@ bool NexusInterface::isModURL(int modID, const QString& url) const return QUrl(alt) == QUrl(url); } -void NexusInterface::setPluginContainer(PluginContainer* pluginContainer) +void NexusInterface::setPluginManager(PluginManager* pluginContainer) { - m_PluginContainer = pluginContainer; + m_PluginManager = pluginContainer; } int NexusInterface::requestDescription(QString gameName, int modID, QObject* receiver, @@ -806,7 +806,7 @@ int NexusInterface::requestInfoFromMd5(QString gameName, QByteArray& hash, IPluginGame* NexusInterface::getGame(QString gameName) const { - auto gamePlugins = m_PluginContainer->plugins(); + auto gamePlugins = m_PluginManager->plugins(); IPluginGame* gamePlugin = qApp->property("managed_game").value(); for (auto plugin : gamePlugins) { if (plugin != nullptr && diff --git a/src/nexusinterface.h b/src/nexusinterface.h index f6efef1d9..abe92e6d4 100644 --- a/src/nexusinterface.h +++ b/src/nexusinterface.h @@ -21,7 +21,7 @@ along with Mod Organizer. If not, see . #define NEXUSINTERFACE_H #include "apiuseraccount.h" -#include "plugincontainer.h" +#include "pluginmanager.h" #include #include @@ -58,7 +58,7 @@ class NexusBridge : public MOBase::IModRepositoryBridge Q_OBJECT public: - NexusBridge(PluginContainer* pluginContainer, const QString& subModule = ""); + NexusBridge(const QString& subModule = ""); /** * @brief request description for a mod @@ -561,7 +561,7 @@ class NexusInterface : public QObject */ bool isModURL(int modID, QString const& url) const; - void setPluginContainer(PluginContainer* pluginContainer); + void setPluginManager(PluginManager* pluginManager); signals: @@ -681,7 +681,7 @@ private slots: NXMAccessManager* m_AccessManager; std::list m_ActiveRequest; QQueue m_RequestQueue; - PluginContainer* m_PluginContainer; + PluginManager* m_PluginManager; APIUserAccount m_User; }; diff --git a/src/nxmaccessmanager.cpp b/src/nxmaccessmanager.cpp index 3efdcda01..ade36e3ab 100644 --- a/src/nxmaccessmanager.cpp +++ b/src/nxmaccessmanager.cpp @@ -162,11 +162,9 @@ NexusSSOLogin::NexusSSOLogin() : m_keyReceived(false), m_active(false) onConnected(); }); - QObject::connect(&m_socket, - qOverload(&QWebSocket::error), - [&](auto&& e) { - onError(e); - }); + QObject::connect(&m_socket, &QWebSocket::errorOccurred, [&](auto&& e) { + onError(e); + }); QObject::connect(&m_socket, &QWebSocket::sslErrors, [&](auto&& errors) { onSslErrors(errors); diff --git a/src/organizer_en.ts b/src/organizer_en.ts index 5715889bd..410b22d52 100644 --- a/src/organizer_en.ts +++ b/src/organizer_en.ts @@ -836,44 +836,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 @@ -1796,197 +1758,269 @@ Right now the only case I know of where this needs to be overwritten is for the + + ExtensionListInfoWidget + + + Form + + + + + Author: + + + + + Version: + + + + + Description: + + + + + Enabled + + + + + Key + + + + + Translation and theme extensions cannot be disabled. + + + + + ExtensionListItemWidget + + + Form + + + + + + + + TextLabel + + + + + ExtensionSettingWidget + + + False + + + + + True + + + + + Edit + + + FileTree - + Enter Name - + Enter a name for the executable - + Not an executable - + This is not a recognized executable. - - + + File '%1' does not exist, you may need to refresh. - + (only has %1 file(s)) - + %1 file(s) selected - + &Add as Executable - + Add this file to the executables list - + This file is not executable - + Reveal in E&xplorer - + Opens the file in Explorer - - - - - - - + + + + + + + This file is in an archive - + Open &Mod Info - + Opens the Mod Info Window - + This file is not in a managed mod - + &Un-Hide - + Un-hides the file - + &Hide - + Hides the file - + &Execute - + Launches this program - + Execute with &VFS - + Launches this program hooked to the VFS - + &Open - + Opens this file with its default handler - + Open with &VFS - + Opens this file with its default handler hooked to the VFS - + &Preview - + Previews this file within Mod Organizer - + This file is in an archive or has no preview handler associated with it - + &Save Tree to Text File... - + Writes the list of files to a text file - + &Refresh - + Refreshes the list - + Ex&pand All - + &Collapse All @@ -2501,96 +2535,96 @@ This is likely due to a corrupted or incompatible download or unrecognized archi - + 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. @@ -2731,37 +2765,37 @@ This is likely due to a corrupted or incompatible download or unrecognized archi MOApplication - + Failed to create log folder. - + Failed to set up data paths. - + Download started - + This shortcut or command line is for instance '%1', but the current instance is '%2'. - + This shortcut or command line is for profile '%1', but the current profile is '%2'. - + an error occurred: %1 - + an error occurred @@ -3003,7 +3037,7 @@ This is likely due to a corrupted or incompatible download or unrecognized archi - + Sort the plugins using LOOT. @@ -3137,7 +3171,7 @@ This is likely due to a corrupted or incompatible download or unrecognized archi - + Name @@ -3395,7 +3429,7 @@ This is likely due to a corrupted or incompatible download or unrecognized archi - + Endorse Mod Organizer @@ -3479,169 +3513,169 @@ This is likely due to a corrupted or incompatible download or unrecognized archi - + Toolbar and Menu - + Desktop - + Start Menu - + 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 - + First Steps Translation strings for tutorial names - + Conflict Resolution - + Overview - + Help on UI - + Documentation - - + + Game Support Wiki - + Chat on Discord - + Report Issue - + Tutorials - + About - + About Qt - + 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 - + Do you know how to mod this game? Do you need to learn? There's a game support wiki available! Click OK to open the wiki. In the future, you can access this link from the "Help" menu. - + Category Setup - + Please choose how to handle the default category setup. If you've already connected to Nexus, you can automatically import Nexus categories for this game (if applicable). Otherwise, use the old Mod Organizer default category structure, or leave the categories blank (for manual setup). - - + + &Import Nexus Categories - + Use &Old Category Defaults - + Do &Nothing - + This is your first time running version 2.5 or higher with an old MO2 instance. The category system now relies on an updated system to map Nexus categories. In order to assign Nexus categories automatically, you will need to import the Nexus categories for the currently managed game and map them to your preferred category structure. @@ -3652,321 +3686,321 @@ As a final option, you can disable Nexus category mapping altogether, which can - + &Open Categories Dialog - + &Disable Nexus Mappings - + &Close - + &Don't show this again - + Downloads in progress - + There are still downloads in progress, do you really want to quit? - + Plugin "%1" failed: %2 - + Plugin "%1" failed - + <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. - + failed to change origin name: %1 - + failed to move "%1" from mod "%2" to "%3": %4 - + 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 - + 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! - + 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! - + There is no supported sort mechanism for this game. You will probably have to use a third-party tool. - + 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. - + Error %1: Request to Nexus failed: %2 - - + + 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... - + Remove '%1' from the toolbar - + 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 @@ -4435,12 +4469,12 @@ p, li { white-space: pre-wrap; } ModInfoRegular - + %1 contains no esp/esm/esl and no asset (textures, meshes, interface, ...) directory - + Categories: <br> @@ -5626,208 +5660,208 @@ Please enter a name: 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 - + The selected profile '%1' does not exist. The profile '%2' will be used instead - + 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. @@ -5950,55 +5984,6 @@ 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: - - - PluginList @@ -6270,7 +6255,6 @@ Continue? <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>ESHs:</td><td align=right>%11 </td><td align=right>%12</td></tr><tr><td>ESLs:</td><td align=right>%5 </td><td align=right>%6</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><tr><td>Overlay:</td><td align=right>%11 </td><td align=right>%12</td></tr></table> @@ -6302,44 +6286,44 @@ Continue? - PluginTypeName + PluginManager - + + Plugin + + + + Diagnose - + Game - + Installer - + Mod Page - + Preview - + Tool - - Proxy - - - - + File Mapper @@ -6869,12 +6853,12 @@ p, li { white-space: pre-wrap; } - + Download URL must start with https:// - + Download started @@ -6890,7 +6874,7 @@ p, li { white-space: pre-wrap; } - + Portable @@ -7137,14 +7121,6 @@ p, li { white-space: pre-wrap; } empty field name - - - Disabling the '%1' plugin will prevent the following %2 plugin(s) from working: - - - - - No menu available @@ -7193,7 +7169,7 @@ Destination: - + Disabled because @@ -7203,22 +7179,22 @@ Destination: - + Cannot open instance '%1', failed to read INI file %2. - + Cannot open instance '%1', the managed game was not found in the INI file %2. Select the game managed by this instance. - + Cannot open instance '%1', the game plugin '%2' doesn't exist. It may have been deleted by an antivirus. Select another 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. @@ -7235,7 +7211,7 @@ Destination: - + @@ -7245,7 +7221,7 @@ Destination: - + Failed to create "%1". Your user account probably lacks permission. @@ -7352,33 +7328,33 @@ Destination: - + Please use "Help" from the toolbar to get usage instructions to all elements - + Visit %1 on Nexus - - + + <Manage...> - + failed to parse profile %1: %2 - + Instance at '%1' not found. Select another instance. - + Instance at '%1' not found. You must create a new instance @@ -7414,107 +7390,102 @@ Destination: - + 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 - - failed to access %1 @@ -7574,19 +7545,12 @@ This program is known to cause issues with Mod Organizer, such as freezing or bl - - - - attempt to store setting for unknown plugin "%1" - - - - + Failed - + Failed to start the helper application: %1 @@ -7623,12 +7587,12 @@ 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? @@ -7715,31 +7679,6 @@ This program is known to cause issues with Mod Organizer, such as freezing or bl The given path was not recognized as a valid game installation. The current game plugin requires the executable to be in a "%1" subfolder of the game directory. - - - 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. - - Executables Blacklist @@ -8629,7 +8568,7 @@ If you disable this feature, MO will only display official DLCs this way. Please - + ... @@ -8766,7 +8705,7 @@ If you disable this feature, MO will only display official DLCs this way. Please - + Options @@ -8828,123 +8767,88 @@ If you disable this feature, MO will only display official DLCs this way. Please - Plugins + Extensions - - Author: - - - - - Version: - - - - - Description: - - - - - Enabled - - - - - Key - - - - - Value - - - - - No plugin found. - - - - + Blacklisted Plugins (use <del> to remove): - + Workarounds - + 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 - + 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 archives parsing (experimental) - - + + Disable this to prevent the GUI from being locked when running an executable. This may result in abnormal behavior. - + Lock GUI when running executable - + Steam - + Password - + Username - + 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; } @@ -8960,69 +8864,69 @@ p, li { white-space: pre-wrap; } - + Network - + 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 System HTTP Proxy - - - - - - + + + + + + Use "%1" as a placeholder for the URL. - + Custom browser - - + + 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 - - + + 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! @@ -9030,7 +8934,12 @@ p, li { white-space: pre-wrap; } - + + 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 @@ -9039,69 +8948,64 @@ 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. - + Executables Blacklist - - Back-date BSAs - - - - - + + Files to skip or ignore from the virtual file system. - + Skip File Suffixes - - + + Directories to skip or ignore from the virtual file system. - + Skip Directories - + These are workarounds for problems with Mod Organizer. Please make sure you read the help text before changing anything here. - + Diagnostics - + Logs and Crashes - + 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. @@ -9109,17 +9013,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. @@ -9130,17 +9034,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. @@ -9148,22 +9052,22 @@ programs you are intentionally running. - + Integrated LOOT - + LOOT Log Level - + Click a link to open the 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. @@ -9173,12 +9077,12 @@ programs you are intentionally running. - + Confirm - + Changing the mod directory affects all your profiles! Mods not present (or named differently) in the new location will be disabled in all profiles. There is no way to undo this unless you backed up your profiles manually. Proceed? @@ -9224,14 +9128,6 @@ programs you are intentionally running. - - T - - - Plugin - - - TransferSavesDialog diff --git a/src/organizercore.cpp b/src/organizercore.cpp index 88166c8e6..0cbbf1199 100644 --- a/src/organizercore.cpp +++ b/src/organizercore.cpp @@ -18,7 +18,7 @@ #include "modrepositoryfileinfo.h" #include "nexusinterface.h" #include "nxmaccessmanager.h" -#include "plugincontainer.h" +#include "pluginmanager.h" #include "previewdialog.h" #include "profile.h" #include "shared/appconfig.h" @@ -73,6 +73,7 @@ #include +#include "inibakery.h" #include "organizerproxy.h" using namespace MOShared; @@ -91,9 +92,9 @@ QStringList toStringList(InputIterator current, InputIterator end) } OrganizerCore::OrganizerCore(Settings& settings) - : m_UserInterface(nullptr), m_PluginContainer(nullptr), m_GamePlugin(nullptr), + : m_UserInterface(nullptr), m_PluginManager(nullptr), m_GamePlugin(nullptr), m_CurrentProfile(nullptr), m_Settings(settings), - m_Updater(&NexusInterface::instance()), m_ModList(m_PluginContainer, this), + m_Updater(&NexusInterface::instance()), m_ModList(m_PluginManager, this), m_PluginList(*this), m_DirectoryRefresher(new DirectoryRefresher(this, settings.refreshThreadCount())), m_DirectoryStructure(new DirectoryEntry(L"data", nullptr, 0)), @@ -104,6 +105,9 @@ OrganizerCore::OrganizerCore(Settings& settings) m_ArchivesInit(false), m_PluginListsWriter(std::bind(&OrganizerCore::savePluginList, this)) { + // need to initialize here for aboutToRun() to be callable + m_IniBakery = std::make_unique(*this); + env::setHandleCloserThreadCount(settings.refreshThreadCount()); m_DownloadManager.setOutputDirectory(m_Settings.paths().downloads(), false); @@ -204,7 +208,7 @@ void OrganizerCore::storeSettings() void OrganizerCore::updateExecutablesList() { - if (m_PluginContainer == nullptr) { + if (m_PluginManager == nullptr) { log::error("can't update executables list now"); return; } @@ -247,27 +251,27 @@ void OrganizerCore::checkForUpdates() } } -void OrganizerCore::connectPlugins(PluginContainer* container) +void OrganizerCore::connectPlugins(PluginManager* manager) { - m_PluginContainer = container; - m_Updater.setPluginContainer(m_PluginContainer); - m_InstallationManager.setPluginContainer(m_PluginContainer); - m_DownloadManager.setPluginContainer(m_PluginContainer); - m_ModList.setPluginContainer(m_PluginContainer); + m_PluginManager = manager; + m_Updater.setPluginManager(m_PluginManager); + m_InstallationManager.setPluginManager(m_PluginManager); + m_DownloadManager.setPluginManager(m_PluginManager); + m_ModList.setPluginManager(m_PluginManager); if (!m_GameName.isEmpty()) { - m_GamePlugin = m_PluginContainer->game(m_GameName); + m_GamePlugin = m_PluginManager->game(m_GameName); emit managedGameChanged(m_GamePlugin); } - connect(m_PluginContainer, &PluginContainer::pluginEnabled, [&](IPlugin* plugin) { + connect(m_PluginManager, &PluginManager::pluginEnabled, [&](IPlugin* plugin) { m_PluginEnabled(plugin); }); - connect(m_PluginContainer, &PluginContainer::pluginDisabled, [&](IPlugin* plugin) { + connect(m_PluginManager, &PluginManager::pluginDisabled, [&](IPlugin* plugin) { m_PluginDisabled(plugin); }); - connect(&m_PluginContainer->gameFeatures(), &GameFeatures::modDataContentUpdated, + connect(&m_PluginManager->gameFeatures(), &GameFeatures::modDataContentUpdated, [this](ModDataContent const* contentFeature) { if (contentFeature) { m_Contents = ModDataContentHolder(contentFeature->getAllContents()); @@ -605,7 +609,7 @@ void OrganizerCore::setCurrentProfile(const QString& profileName) MOBase::IModRepositoryBridge* OrganizerCore::createNexusBridge() const { - return new NexusBridge(m_PluginContainer); + return new NexusBridge(); } QString OrganizerCore::profileName() const @@ -653,7 +657,7 @@ MOBase::Version OrganizerCore::version() const MOBase::IPluginGame* OrganizerCore::getGame(const QString& name) const { - for (IPluginGame* game : m_PluginContainer->plugins()) { + for (IPluginGame* game : m_PluginManager->plugins()) { if (game != nullptr && game->gameShortName().compare(name, Qt::CaseInsensitive) == 0) return game; @@ -963,7 +967,7 @@ QStringList OrganizerCore::getFileOrigins(const QString& fileName) const if (file.get() != nullptr) { result.append( ToQString(m_DirectoryStructure->getOriginByID(file->getOrigin()).getName())); - foreach (const auto& i, file->getAlternatives()) { + for (const auto& i : file->getAlternatives()) { result.append( ToQString(m_DirectoryStructure->getOriginByID(i.originID()).getName())); } @@ -1062,7 +1066,7 @@ bool OrganizerCore::previewFileWithAlternatives(QWidget* parent, QString fileNam if (QFile::exists(filePath)) { // it's very possible the file doesn't exist, because it's inside an archive. we // don't support that - QWidget* wid = m_PluginContainer->previewGenerator().genPreview(filePath); + QWidget* wid = m_PluginManager->previewGenerator().genPreview(filePath); if (wid == nullptr) { reportError(tr("failed to generate preview for %1").arg(filePath)); } else { @@ -1077,7 +1081,7 @@ bool OrganizerCore::previewFileWithAlternatives(QWidget* parent, QString fileNam libbsarch::memory_blob fileData = archiveLoader.extract_to_memory(fileName.toStdWString()); QByteArray convertedFileData((char*)(fileData.data), fileData.size); - QWidget* wid = m_PluginContainer->previewGenerator().genArchivePreview( + QWidget* wid = m_PluginManager->previewGenerator().genArchivePreview( convertedFileData, filePath); if (wid == nullptr) { reportError(tr("failed to generate preview for %1").arg(filePath)); @@ -1149,7 +1153,7 @@ bool OrganizerCore::previewFile(QWidget* parent, const QString& originName, PreviewDialog preview(path, parent); - QWidget* wid = m_PluginContainer->previewGenerator().genPreview(path); + QWidget* wid = m_PluginManager->previewGenerator().genPreview(path); if (wid == nullptr) { reportError(tr("Failed to generate preview for %1").arg(path)); return false; @@ -1452,11 +1456,11 @@ void OrganizerCore::loggedInAction(QWidget* parent, std::function f) void OrganizerCore::requestDownload(const QUrl& url, QNetworkReply* reply) { - if (!m_PluginContainer) { + if (!m_PluginManager) { return; } - for (IPluginModPage* modPage : m_PluginContainer->plugins()) { - if (m_PluginContainer->isEnabled(modPage)) { + for (IPluginModPage* modPage : m_PluginManager->plugins()) { + if (m_PluginManager->isEnabled(modPage)) { ModRepositoryFileInfo* fileInfo = new ModRepositoryFileInfo(); if (modPage->handlesDownload(url, reply->url(), *fileInfo)) { fileInfo->repository = modPage->name(); @@ -1502,14 +1506,14 @@ void OrganizerCore::requestDownload(const QUrl& url, QNetworkReply* reply) } } -PluginContainer& OrganizerCore::pluginContainer() const +PluginManager& OrganizerCore::pluginManager() const { - return *m_PluginContainer; + return *m_PluginManager; } GameFeatures& OrganizerCore::gameFeatures() const { - return pluginContainer().gameFeatures(); + return pluginManager().gameFeatures(); } IPluginGame const* OrganizerCore::managedGame() const @@ -1519,7 +1523,7 @@ IPluginGame const* OrganizerCore::managedGame() const IOrganizer const* OrganizerCore::managedGameOrganizer() const { - return m_PluginContainer->requirements(m_GamePlugin).m_Organizer; + return m_PluginManager->details(m_GamePlugin).proxy(); } std::vector OrganizerCore::enabledArchives() @@ -2086,10 +2090,17 @@ std::vector OrganizerCore::fileMapping(const QString& profileName, true, customOverwrite.isEmpty()}); } + // ini bakery + { + const auto iniBakeryMapping = m_IniBakery->mappings(); + result.reserve(result.size() + iniBakeryMapping.size()); + result.insert(result.end(), iniBakeryMapping.begin(), iniBakeryMapping.end()); + } + for (MOBase::IPluginFileMapper* mapper : - m_PluginContainer->plugins()) { + m_PluginManager->plugins()) { IPlugin* plugin = dynamic_cast(mapper); - if (m_PluginContainer->isEnabled(plugin)) { + if (m_PluginManager->isEnabled(plugin)) { MappingType pluginMap = mapper->mappings(); result.reserve(result.size() + pluginMap.size()); result.insert(result.end(), pluginMap.begin(), pluginMap.end()); @@ -2098,47 +2109,3 @@ std::vector OrganizerCore::fileMapping(const QString& profileName, return result; } - -std::vector OrganizerCore::fileMapping(const QString& dataPath, - const QString& relPath, - const DirectoryEntry* base, - const DirectoryEntry* directoryEntry, - int createDestination) -{ - std::vector result; - - for (FileEntryPtr current : directoryEntry->getFiles()) { - bool isArchive = false; - int origin = current->getOrigin(isArchive); - if (isArchive || (origin == 0)) { - continue; - } - - QString originPath = QString::fromStdWString(base->getOriginByID(origin).getPath()); - QString fileName = QString::fromStdWString(current->getName()); - // QString fileName = ToQString(current->getName()); - QString source = originPath + relPath + fileName; - QString target = dataPath + relPath + fileName; - if (source != target) { - result.push_back({source, target, false, false}); - } - } - - // recurse into subdirectories - for (const auto& d : directoryEntry->getSubDirectories()) { - int origin = d->anyOrigin(); - - QString originPath = QString::fromStdWString(base->getOriginByID(origin).getPath()); - QString dirName = QString::fromStdWString(d->getName()); - QString source = originPath + relPath + dirName; - QString target = dataPath + relPath + dirName; - - bool writeDestination = (base == directoryEntry) && (origin == createDestination); - - result.push_back({source, target, true, writeDestination}); - std::vector subRes = - fileMapping(dataPath, relPath + dirName + "\\", base, d, createDestination); - result.insert(result.end(), subRes.begin(), subRes.end()); - } - return result; -} diff --git a/src/organizercore.h b/src/organizercore.h index fe80da240..d56ab4446 100644 --- a/src/organizercore.h +++ b/src/organizercore.h @@ -37,12 +37,13 @@ #include "uilocker.h" #include "usvfsconnector.h" +class IniBakery; class ModListSortProxy; class PluginListSortProxy; class Profile; class IUserInterface; class GameFeatures; -class PluginContainer; +class PluginManager; class DirectoryRefresher; namespace MOBase @@ -246,7 +247,7 @@ class OrganizerCore : public QObject, public MOBase::IPluginDiagnose ~OrganizerCore(); void setUserInterface(IUserInterface* ui); - void connectPlugins(PluginContainer* container); + void connectPlugins(PluginManager* manager); void setManagedGame(MOBase::IPluginGame* game); @@ -274,9 +275,9 @@ class OrganizerCore : public QObject, public MOBase::IPluginDiagnose MOBase::Version getVersion() const { return m_Updater.getVersion(); } - // return the plugin container + // return the plugin manager // - PluginContainer& pluginContainer() const; + PluginManager& pluginManager() const; // return the game features GameFeatures& gameFeatures() const; @@ -508,11 +509,6 @@ public slots: std::vector fileMapping(const QString& profile, const QString& customOverwrite); - std::vector fileMapping(const QString& dataPath, const QString& relPath, - const MOShared::DirectoryEntry* base, - const MOShared::DirectoryEntry* directoryEntry, - int createDestination); - private slots: void onDirectoryRefreshed(); @@ -528,7 +524,8 @@ private slots: private: IUserInterface* m_UserInterface; - PluginContainer* m_PluginContainer; + PluginManager* m_PluginManager; + std::unique_ptr m_IniBakery; QString m_GameName; MOBase::IPluginGame* m_GamePlugin; ModDataContentHolder m_Contents; diff --git a/src/organizerproxy.cpp b/src/organizerproxy.cpp index 22a8a0234..fb98c875b 100644 --- a/src/organizerproxy.cpp +++ b/src/organizerproxy.cpp @@ -1,12 +1,14 @@ #include "organizerproxy.h" #include "downloadmanagerproxy.h" +#include "extensionlistproxy.h" +#include "extensionmanager.h" #include "gamefeaturesproxy.h" #include "glob_matching.h" #include "modlistproxy.h" #include "organizercore.h" -#include "plugincontainer.h" #include "pluginlistproxy.h" +#include "pluginmanager.h" #include "proxyutils.h" #include "settings.h" #include "shared/appconfig.h" @@ -19,16 +21,18 @@ using namespace MOBase; using namespace MOShared; OrganizerProxy::OrganizerProxy(OrganizerCore* organizer, - PluginContainer* pluginContainer, - MOBase::IPlugin* plugin) - : m_Proxied(organizer), m_PluginContainer(pluginContainer), m_Plugin(plugin), + const ExtensionManager& extensionManager, + PluginManager* pluginManager, MOBase::IPlugin* plugin) + : m_Proxied(organizer), m_PluginManager(pluginManager), m_Plugin(plugin), m_DownloadManagerProxy( std::make_unique(this, organizer->downloadManager())), + m_ExtensionListProxy( + std::make_unique(this, extensionManager)), m_ModListProxy(std::make_unique(this, organizer->modList())), m_PluginListProxy( std::make_unique(this, organizer->pluginList())), m_GameFeaturesProxy( - std::make_unique(this, pluginContainer->gameFeatures())) + std::make_unique(this, pluginManager->gameFeatures())) {} OrganizerProxy::~OrganizerProxy() @@ -81,7 +85,7 @@ void OrganizerProxy::disconnectSignals() IModRepositoryBridge* OrganizerProxy::createNexusBridge() const { - return new NexusBridge(m_PluginContainer, m_Plugin->name()); + return new NexusBridge(m_Plugin->name()); } QString OrganizerProxy::profileName() const @@ -178,14 +182,19 @@ void OrganizerProxy::modDataChanged(IModInterface* mod) m_Proxied->modDataChanged(mod); } +MOBase::IExtensionList& OrganizerProxy::extensionList() const +{ + return *m_ExtensionListProxy; +} + bool OrganizerProxy::isPluginEnabled(QString const& pluginName) const { - return m_PluginContainer->isEnabled(pluginName); + return m_PluginManager->isEnabled(pluginName); } bool OrganizerProxy::isPluginEnabled(IPlugin* plugin) const { - return m_PluginContainer->isEnabled(plugin); + return m_PluginManager->isEnabled(plugin); } QVariant OrganizerProxy::pluginSetting(const QString& pluginName, diff --git a/src/organizerproxy.h b/src/organizerproxy.h index 2c70cb2c3..03205b06b 100644 --- a/src/organizerproxy.h +++ b/src/organizerproxy.h @@ -9,17 +9,19 @@ #include "organizercore.h" class GameFeaturesProxy; -class PluginContainer; +class PluginManager; class DownloadManagerProxy; class ModListProxy; +class ExtensionManager; class PluginListProxy; +class ExtensionListProxy; class OrganizerProxy : public MOBase::IOrganizer { public: - OrganizerProxy(OrganizerCore* organizer, PluginContainer* pluginContainer, - MOBase::IPlugin* plugin); + OrganizerProxy(OrganizerCore* organizer, const ExtensionManager& extensionManager, + PluginManager* pluginManager, MOBase::IPlugin* plugin); ~OrganizerProxy(); public: @@ -92,7 +94,9 @@ class OrganizerProxy : public MOBase::IOrganizer bool onProfileChanged( std::function const& func) override; - // Plugin related: + // Plugin/extension related: + virtual MOBase::IExtensionList& extensionList() const override; + virtual bool isPluginEnabled(QString const& pluginName) const override; virtual bool isPluginEnabled(MOBase::IPlugin* plugin) const override; virtual QVariant pluginSetting(const QString& pluginName, @@ -115,7 +119,7 @@ class OrganizerProxy : public MOBase::IOrganizer protected: // The container needs access to some callbacks to simulate startup. - friend class PluginContainer; + friend class PluginManager; /** * @brief Connect the signals from this proxy and all the child proxies (plugin list, @@ -132,7 +136,7 @@ class OrganizerProxy : public MOBase::IOrganizer private: OrganizerCore* m_Proxied; - PluginContainer* m_PluginContainer; + PluginManager* m_PluginManager; MOBase::IPlugin* m_Plugin; @@ -150,6 +154,7 @@ class OrganizerProxy : public MOBase::IOrganizer std::vector m_Connections; std::unique_ptr m_DownloadManagerProxy; + std::unique_ptr m_ExtensionListProxy; std::unique_ptr m_ModListProxy; std::unique_ptr m_PluginListProxy; std::unique_ptr m_GameFeaturesProxy; diff --git a/src/plugincontainer.cpp b/src/plugincontainer.cpp deleted file mode 100644 index c73d45666..000000000 --- a/src/plugincontainer.cpp +++ /dev/null @@ -1,1245 +0,0 @@ -#include "plugincontainer.h" -#include "iuserinterface.h" -#include "organizercore.h" -#include "organizerproxy.h" -#include "report.h" -#include "shared/appconfig.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -using namespace MOBase; -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. If the setting does -// not exist, -// the plugin's enabledByDefault is used instead. -// - A plugin is considered disabled if the setting is false. -// - If the setting is true, 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; - -template <> -struct PluginTypeName -{ - static QString value() { return QT_TR_NOOP("Plugin"); } -}; -template <> -struct PluginTypeName -{ - static QString value() { return QT_TR_NOOP("Diagnose"); } -}; -template <> -struct PluginTypeName -{ - static QString value() { return QT_TR_NOOP("Game"); } -}; -template <> -struct PluginTypeName -{ - static QString value() { return QT_TR_NOOP("Installer"); } -}; -template <> -struct PluginTypeName -{ - static QString value() { return QT_TR_NOOP("Mod Page"); } -}; -template <> -struct PluginTypeName -{ - static QString value() { return QT_TR_NOOP("Preview"); } -}; -template <> -struct PluginTypeName -{ - static QString value() { return QT_TR_NOOP("Tool"); } -}; -template <> -struct PluginTypeName -{ - static QString value() { return QT_TR_NOOP("Proxy"); } -}; -template <> -struct PluginTypeName -{ - static QString value() { return QT_TR_NOOP("File Mapper"); } -}; - -QStringList PluginContainer::pluginInterfaces() -{ - // Find all the names: - QStringList names; - boost::mp11::mp_for_each([&names](const auto* p) { - using plugin_type = std::decay_t; - auto name = PluginTypeName::value(); - if (!name.isEmpty()) { - names.append(name); - } - }); - - return names; -} - -// PluginRequirementProxy - -const std::set PluginRequirements::s_CorePlugins{"INI Bakery"}; - -PluginRequirements::PluginRequirements(PluginContainer* pluginContainer, - MOBase::IPlugin* plugin, OrganizerProxy* 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() -{ - m_Requirements = m_Plugin->requirements(); -} - -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 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); - - // 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 add it here. - if (plugin && m_PluginContainer->requirements(plugin).master() == m_Plugin && - plugin->name() != m_Plugin->name()) { - children.push_back(plugin); - } - } - return children; -} - -std::vector PluginRequirements::problems() const -{ - std::vector result; - for (auto& requirement : m_Requirements) { - if (auto p = requirement->check(m_Organizer)) { - result.push_back(*p); - } - } - return result; -} - -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(); -} - -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 {}; -} - -std::vector PluginRequirements::requiredFor() const -{ - std::vector required; - std::set visited; - requiredFor(required, visited); - return required; -} - -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) - : m_Organizer(organizer), m_UserInterface(nullptr), - m_GameFeatures(std::make_unique(organizer, this)), - m_PreviewGenerator(*this) -{} - -PluginContainer::~PluginContainer() -{ - m_Organizer = nullptr; - unloadPlugins(); -} - -void PluginContainer::startPlugins(IUserInterface* userInterface) -{ - m_UserInterface = userInterface; - startPluginsImpl(plugins()); -} - -QStringList PluginContainer::implementedInterfaces(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 {}; - } - - 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) { - using plugin_type = std::decay_t; - if (qobject_cast(oPlugin)) { - auto name = PluginTypeName::value(); - if (!name.isEmpty()) { - names.append(name); - } - } - }); - - // If the plugin implements at least one interface other than IPlugin, remove IPlugin: - if (names.size() > 1) { - names.removeAll(PluginTypeName::value()); - } - - return names; -} - -QString PluginContainer::topImplementedInterface(IPlugin* plugin) const -{ - auto interfaces = implementedInterfaces(plugin); - return interfaces.isEmpty() ? "" : interfaces[0]; -} - -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 -{ - QStringList result; - for (QPluginLoader* loader : m_PluginLoaders) { - result.append(loader->fileName()); - } - std::vector proxyList = bf::at_key(m_Plugins); - for (IPluginProxy* proxy : proxyList) { - QStringList proxiedPlugins = - proxy->pluginList(QCoreApplication::applicationDirPath() + "/" + - ToQString(AppConfig::pluginPath())); - result.append(proxiedPlugins); - } - 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; - } - - return *it; -} - -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 - // - // after proxies are initialized, instantiate() is called for all the plugins - // they've discovered, but as for regular plugins, init() won't be - // called on them if m_OrganizerCore is null - - if (plugin == nullptr) { - return false; - } - - OrganizerProxy* proxy = nullptr; - if (m_Organizer) { - proxy = new OrganizerProxy(m_Organizer, this, plugin); - proxy->setParent(as_qobject(plugin)); - } - - // 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; - } - - if (skipInit) { - return true; - } - - if (!plugin->init(proxy)) { - log::warn("plugin failed to initialize"); - return false; - } - - // Update requirements: - it->second.fetchRequirements(); - - return true; -} - -void PluginContainer::registerGame(IPluginGame* game) -{ - m_SupportedGames.insert({game->gameName(), game}); -} - -void PluginContainer::unregisterGame(MOBase::IPluginGame* game) -{ - m_SupportedGames.erase(game->gameName()); -} - -IPlugin* PluginContainer::registerPlugin(QObject* plugin, const QString& filepath, - MOBase::IPluginProxy* pluginProxy) -{ - - // generic treatment for all plugins - IPlugin* pluginObj = qobject_cast(plugin); - if (pluginObj == nullptr) { - log::debug("PluginContainer::registerPlugin() called with a non IPlugin QObject."); - return nullptr; - } - - // If we already a plugin with this name: - bool skipInit = false; - 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 && - this->filepath(other) == QDir::cleanPath(filepath)) { - - // 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(", ")); - bf::at_key(m_AccessPlugins)[pluginObj->name()] = pluginObj; - } - } else { - log::warn("Trying to register two plugins with the name '{}' (from {} and {}), " - "the second one will not be registered.", - pluginObj->name(), this->filepath(other), QDir::cleanPath(filepath)); - return nullptr; - } - } 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("filepath", QDir::cleanPath(filepath)); - plugin->setParent(this); - - 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; - diagnose->onInvalidated([&]() { - emit diagnosisUpdate(); - }); - } - } - { // file mapper plugin - 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 - IPluginModPage* modPage = qobject_cast(plugin); - if (initPlugin(modPage, pluginProxy, skipInit)) { - bf::at_key(m_Plugins).push_back(modPage); - emit pluginRegistered(modPage); - return modPage; - } - } - { // game plugin - IPluginGame* game = qobject_cast(plugin); - if (game) { - game->detectGame(); - if (initPlugin(game, pluginProxy, skipInit)) { - bf::at_key(m_Plugins).push_back(game); - registerGame(game); - emit pluginRegistered(game); - return game; - } - } - } - { // tool plugins - IPluginTool* tool = qobject_cast(plugin); - if (initPlugin(tool, pluginProxy, skipInit)) { - bf::at_key(m_Plugins).push_back(tool); - emit pluginRegistered(tool); - return tool; - } - } - { // installer plugins - IPluginInstaller* installer = qobject_cast(plugin); - if (initPlugin(installer, pluginProxy, skipInit)) { - bf::at_key(m_Plugins).push_back(installer); - if (m_Organizer) { - installer->setInstallationManager(m_Organizer->installationManager()); - } - emit pluginRegistered(installer); - return installer; - } - } - { // preview plugins - IPluginPreview* preview = qobject_cast(plugin); - if (initPlugin(preview, pluginProxy, skipInit)) { - bf::at_key(m_Plugins).push_back(preview); - return preview; - } - } - { // proxy plugins - IPluginProxy* proxy = qobject_cast(plugin); - if (initPlugin(proxy, pluginProxy, skipInit)) { - bf::at_key(m_Plugins).push_back(proxy); - emit pluginRegistered(proxy); - - QStringList filepaths = - proxy->pluginList(QCoreApplication::applicationDirPath() + "/" + - ToQString(AppConfig::pluginPath())); - for (const QString& filepath : filepaths) { - loadProxied(filepath, proxy); - } - return proxy; - } - } - - { // dummy plugins - // only initialize these, no processing otherwise - IPlugin* dummy = qobject_cast(plugin); - if (initPlugin(dummy, pluginProxy, skipInit)) { - bf::at_key(m_Plugins).push_back(dummy); - emit pluginRegistered(dummy); - return dummy; - } - } - - return nullptr; -} - -IPluginGame* 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 m_Organizer ? const_cast(m_Organizer->managedGame()) : nullptr; -} - -bool PluginContainer::isEnabled(IPlugin* plugin) const -{ - // Check if it's a game plugin: - if (implementInterface(plugin)) { - return plugin == m_Organizer->managedGame(); - } - - // 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", plugin->enabledByDefault()) - .toBool()) { - return false; - } - - // Check the requirements: - return m_Requirements.at(plugin).canEnable(); -} - -void PluginContainer::setEnabled(MOBase::IPlugin* plugin, bool enable, - bool dependencies) -{ - // If required, disable dependencies: - if (!enable && dependencies) { - for (auto* p : requirements(plugin).requiredFor()) { - // No need to "recurse" here since requiredFor already does it. - setEnabled(p, false, false); - } - } - - // 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); - - if (enable) { - emit pluginEnabled(plugin); - } else { - emit pluginDisabled(plugin); - } -} - -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 -{ - 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; -} - -const PluginRequirements& PluginContainer::requirements(IPlugin* plugin) const -{ - return m_Requirements.at(plugin); -} - -OrganizerProxy* PluginContainer::organizerProxy(MOBase::IPlugin* plugin) const -{ - return requirements(plugin).m_Organizer; -} - -MOBase::IPluginProxy* PluginContainer::pluginProxy(MOBase::IPlugin* plugin) const -{ - return requirements(plugin).proxy(); -} - -QString PluginContainer::filepath(MOBase::IPlugin* plugin) const -{ - return as_qobject(plugin)->property("filepath").toString(); -} - -IPluginGame* PluginContainer::game(const QString& name) const -{ - auto iter = m_SupportedGames.find(name); - if (iter != m_SupportedGames.end()) { - return iter->second; - } else { - return nullptr; - } -} - -void PluginContainer::startPluginsImpl(const std::vector& plugins) const -{ - // setUserInterface() - if (m_UserInterface) { - for (auto* plugin : plugins) { - if (auto* proxy = qobject_cast(plugin)) { - proxy->setParentWidget(m_UserInterface->mainWindow()); - } - if (auto* modPage = qobject_cast(plugin)) { - modPage->setParentWidget(m_UserInterface->mainWindow()); - } - if (auto* tool = qobject_cast(plugin)) { - tool->setParentWidget(m_UserInterface->mainWindow()); - } - if (auto* installer = qobject_cast(plugin)) { - installer->setParentWidget(m_UserInterface->mainWindow()); - } - } - } - - // Trigger initial callbacks, e.g. onUserInterfaceInitialized and onProfileChanged. - if (m_Organizer) { - for (auto* object : plugins) { - auto* plugin = qobject_cast(object); - auto* oproxy = organizerProxy(plugin); - oproxy->connectSignals(); - oproxy->m_ProfileChanged(nullptr, m_Organizer->currentProfile()); - - if (m_UserInterface) { - oproxy->m_UserInterfaceInitialized(m_UserInterface->mainWindow()); - } - } - } -} - -std::vector PluginContainer::loadProxied(const QString& filepath, - IPluginProxy* proxy) -{ - std::vector proxiedPlugins; - - try { - // 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->load(filepath); - - // We are going to group plugin by names and "fix" them later: - std::map> proxiedByNames; - - for (QObject* proxiedPlugin : matchingPlugins) { - if (proxiedPlugin != nullptr) { - - if (IPlugin* proxied = registerPlugin(proxiedPlugin, filepath, proxy); - proxied) { - log::debug("loaded plugin '{}' from '{}' - [{}]", proxied->name(), - QFileInfo(filepath).fileName(), - implementedInterfaces(proxied).join(", ")); - - // Store the plugin for later: - proxiedPlugins.push_back(proxiedPlugin); - proxiedByNames[proxied->name()].push_back(proxied); - } else { - log::warn("plugin \"{}\" failed to load. If this plugin is for an older " - "version of MO " - "you have to update it or delete it if no update exists.", - filepath); - } - } - } - - // 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(filepath).arg(e.what())); - } - - return proxiedPlugins; -} - -QObject* PluginContainer::loadQtPlugin(const QString& filepath) -{ - std::unique_ptr pluginLoader(new QPluginLoader(filepath, this)); - if (pluginLoader->instance() == nullptr) { - m_FailedPlugins.push_back(filepath); - log::error("failed to load plugin {}: {}", filepath, pluginLoader->errorString()); - } else { - QObject* object = pluginLoader->instance(); - if (IPlugin* plugin = registerPlugin(object, filepath, nullptr); plugin) { - log::debug("loaded plugin '{}' from '{}' - [{}]", plugin->name(), - QFileInfo(filepath).fileName(), - implementedInterfaces(plugin).join(", ")); - m_PluginLoaders.push_back(pluginLoader.release()); - return object; - } else { - m_FailedPlugins.push_back(filepath); - log::warn("plugin '{}' failed to load (may be outdated)", filepath); - } - } - return nullptr; -} - -std::optional PluginContainer::isQtPluginFolder(const QString& filepath) const -{ - - if (!QFileInfo(filepath).isDir()) { - return {}; - } - - QDirIterator iter(filepath, QDir::Files | QDir::NoDotAndDotDot); - while (iter.hasNext()) { - iter.next(); - const auto filePath = iter.filePath(); - - // not a library, skip - if (!QLibrary::isLibrary(filePath)) { - continue; - } - - // check if we have proper metadata - this does not load the plugin (metaData() - // should be very lightweight) - const QPluginLoader loader(filePath); - if (!loader.metaData().isEmpty()) { - return filePath; - } - } - - return {}; -} - -void PluginContainer::loadPlugin(QString const& filepath) -{ - std::vector plugins; - if (QFileInfo(filepath).isFile() && QLibrary::isLibrary(filepath)) { - QObject* plugin = loadQtPlugin(filepath); - if (plugin) { - plugins.push_back(plugin); - } - } else if (auto p = isQtPluginFolder(filepath)) { - QObject* plugin = loadQtPlugin(*p); - if (plugin) { - plugins.push_back(plugin); - } - } else { - // We need to check if this can be handled by a proxy. - for (auto* proxy : this->plugins()) { - auto filepaths = proxy->pluginList(QCoreApplication::applicationDirPath() + "/" + - ToQString(AppConfig::pluginPath())); - if (filepaths.contains(filepath)) { - plugins = loadProxied(filepath, proxy); - break; - } - } - } - - for (auto* plugin : plugins) { - emit pluginRegistered(qobject_cast(plugin)); - } - - startPluginsImpl(plugins); -} - -void PluginContainer::unloadPlugin(MOBase::IPlugin* plugin, QObject* object) -{ - if (auto* game = qobject_cast(object)) { - - if (game == managedGame()) { - throw Exception("cannot unload the plugin for the currently managed game"); - } - - unregisterGame(game); - } - - // We need to remove from the m_Plugins maps BEFORE unloading from the proxy - // otherwise the qobject_cast to check the plugin type will not work. - bf::for_each(m_Plugins, [object](auto& t) { - using type = typename std::decay_t::value_type; - - // We do not want to remove from QObject since we are iterating over them. - if constexpr (!std::is_same{}) { - auto itp = - std::find(t.second.begin(), t.second.end(), qobject_cast(object)); - if (itp != t.second.end()) { - t.second.erase(itp); - } - } - }); - - emit pluginUnregistered(plugin); - - // Remove from the members. - if (auto* diagnose = qobject_cast(object)) { - bf::at_key(m_AccessPlugins).erase(diagnose); - } - if (auto* mapper = qobject_cast(object)) { - bf::at_key(m_AccessPlugins).erase(mapper); - } - - auto& mapNames = bf::at_key(m_AccessPlugins); - if (mapNames.contains(plugin->name())) { - mapNames.erase(plugin->name()); - } - - m_Organizer->settings().plugins().unregisterPlugin(plugin); - - // Force disconnection of the signals from the proxies. This is a safety - // operations since those signals should be disconnected when the proxies - // are destroyed anyway. - organizerProxy(plugin)->disconnectSignals(); - - // Is this a proxied plugin? - auto* proxy = pluginProxy(plugin); - - if (proxy) { - proxy->unload(filepath(plugin)); - } else { - // We need to find the loader. - auto it = std::find_if(m_PluginLoaders.begin(), m_PluginLoaders.end(), - [object](auto* loader) { - return loader->instance() == object; - }); - - if (it != m_PluginLoaders.end()) { - if (!(*it)->unload()) { - log::error("failed to unload {}: {}", (*it)->fileName(), (*it)->errorString()); - } - delete *it; - m_PluginLoaders.erase(it); - } else { - log::error("loader for plugin {} does not exist, cannot unload", plugin->name()); - } - } - - object->deleteLater(); - - // Do this at the end. - m_Requirements.erase(plugin); -} - -void PluginContainer::unloadPlugin(QString const& filepath) -{ - // We need to find all the plugins from the given path and - // unload them: - QString cleanPath = QDir::cleanPath(filepath); - auto& objects = bf::at_key(m_Plugins); - for (auto it = objects.begin(); it != objects.end();) { - auto* plugin = qobject_cast(*it); - if (this->filepath(plugin) == filepath) { - unloadPlugin(plugin, *it); - it = objects.erase(it); - } else { - ++it; - } - } -} - -void PluginContainer::reloadPlugin(QString const& filepath) -{ - unloadPlugin(filepath); - loadPlugin(filepath); -} - -void PluginContainer::unloadPlugins() -{ - if (m_Organizer) { - // this will clear several structures that can hold on to pointers to - // plugins, as well as read the plugin blacklist from the ini file, which - // is used in loadPlugins() below to skip plugins - // - // note that the first thing loadPlugins() does is call unloadPlugins(), - // so this makes sure the blacklist is always available - m_Organizer->settings().plugins().clearPlugins(); - } - - bf::for_each(m_Plugins, [](auto& t) { - t.second.clear(); - }); - bf::for_each(m_AccessPlugins, [](auto& t) { - t.second.clear(); - }); - m_Requirements.clear(); - - while (!m_PluginLoaders.empty()) { - QPluginLoader* loader = m_PluginLoaders.back(); - m_PluginLoaders.pop_back(); - if ((loader != nullptr) && !loader->unload()) { - log::debug("failed to unload {}: {}", loader->fileName(), loader->errorString()); - } - delete loader; - } -} - -void PluginContainer::loadPlugins() -{ - TimeThis tt("PluginContainer::loadPlugins()"); - - unloadPlugins(); - - for (QObject* plugin : QPluginLoader::staticInstances()) { - registerPlugin(plugin, "", nullptr); - } - - QFile loadCheck; - QString skipPlugin; - - if (m_Organizer) { - loadCheck.setFileName(qApp->property("dataPath").toString() + - "/plugin_loadcheck.tmp"); - - if (loadCheck.exists() && loadCheck.open(QIODevice::ReadOnly)) { - // oh, there was a failed plugin load last time. Find out which plugin was loaded - // last - QString fileName; - while (!loadCheck.atEnd()) { - fileName = QString::fromUtf8(loadCheck.readLine().constData()).trimmed(); - } - - log::warn("loadcheck file found for plugin '{}'", fileName); - - MOBase::TaskDialog dlg; - - const auto Skip = QMessageBox::Ignore; - const auto Blacklist = QMessageBox::Cancel; - const auto Load = QMessageBox::Ok; - - const auto r = - dlg.title(tr("Plugin error")) - .main(tr("Mod Organizer failed to load the plugin '%1' last time it was " - "started.") - .arg(fileName)) - .content(tr( - "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.")) - .icon(QMessageBox::Warning) - .button({tr("Skip this plugin"), Skip}) - .button({tr("Blacklist this plugin"), Blacklist}) - .button({tr("Load this plugin"), Load}) - .exec(); - - switch (r) { - case Skip: - log::warn("user wants to skip plugin '{}'", fileName); - skipPlugin = fileName; - break; - - case Blacklist: - log::warn("user wants to blacklist plugin '{}'", fileName); - m_Organizer->settings().plugins().addBlacklist(fileName); - break; - - case Load: - log::warn("user wants to load plugin '{}' anyway", fileName); - break; - } - - loadCheck.close(); - } - - loadCheck.open(QIODevice::WriteOnly); - } - - QString pluginPath = - qApp->applicationDirPath() + "/" + ToQString(AppConfig::pluginPath()); - log::debug("looking for plugins in {}", QDir::toNativeSeparators(pluginPath)); - QDirIterator iter(pluginPath, QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot); - - while (iter.hasNext()) { - iter.next(); - - if (skipPlugin == iter.fileName()) { - log::debug("plugin \"{}\" skipped for this session", iter.fileName()); - continue; - } - - if (m_Organizer) { - if (m_Organizer->settings().plugins().blacklisted(iter.fileName())) { - log::debug("plugin \"{}\" blacklisted", iter.fileName()); - continue; - } - } - - if (loadCheck.isOpen()) { - loadCheck.write(iter.fileName().toUtf8()); - loadCheck.write("\n"); - loadCheck.flush(); - } - - QString filepath = iter.filePath(); - if (QLibrary::isLibrary(filepath)) { - loadQtPlugin(filepath); - } else if (auto p = isQtPluginFolder(filepath)) { - loadQtPlugin(*p); - } - } - - if (skipPlugin.isEmpty()) { - // remove the load check file on success - if (loadCheck.isOpen()) { - loadCheck.remove(); - } - } else { - // remember the plugin for next time - if (loadCheck.isOpen()) { - loadCheck.close(); - } - - log::warn("user skipped plugin '{}', remembering in loadcheck", skipPlugin); - loadCheck.open(QIODevice::WriteOnly); - loadCheck.write(skipPlugin.toUtf8()); - loadCheck.write("\n"); - loadCheck.flush(); - } - - bf::at_key(m_Plugins).push_back(this); - - if (m_Organizer) { - bf::at_key(m_Plugins).push_back(m_Organizer); - m_Organizer->connectPlugins(this); - } -} - -std::vector PluginContainer::activeProblems() const -{ - std::vector problems; - if (m_FailedPlugins.size()) { - problems.push_back(PROBLEM_PLUGINSNOTLOADED); - } - return problems; -} - -QString PluginContainer::shortDescription(unsigned int key) const -{ - switch (key) { - case PROBLEM_PLUGINSNOTLOADED: { - return tr("Some plugins could not be loaded"); - } break; - default: { - return tr("Description missing"); - } break; - } -} - -QString PluginContainer::fullDescription(unsigned int key) const -{ - switch (key) { - case PROBLEM_PLUGINSNOTLOADED: { - QString result = - tr("The following plugins could not be loaded. The reason may be missing " - "dependencies (i.e. python) or an outdated version:") + - "
    "; - for (const QString& plugin : m_FailedPlugins) { - result += "
  • " + plugin + "
  • "; - } - result += "
      "; - return result; - } break; - default: { - return tr("Description missing"); - } break; - } -} - -bool PluginContainer::hasGuidedFix(unsigned int) const -{ - return false; -} - -void PluginContainer::startGuidedFix(unsigned int) const {} diff --git a/src/plugincontainer.h b/src/plugincontainer.h deleted file mode 100644 index 4854954d3..000000000 --- a/src/plugincontainer.h +++ /dev/null @@ -1,505 +0,0 @@ -#ifndef PLUGINCONTAINER_H -#define PLUGINCONTAINER_H - -#include "previewgenerator.h" - -class OrganizerCore; -class IUserInterface; - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#ifndef Q_MOC_RUN -#include -#include -#include -#endif // Q_MOC_RUN -#include -#include - -#include "game_features.h" - -class OrganizerProxy; - -/** - * @brief Class that wrap multiple requirements for a plugin together. THis - * class owns the requirements. - */ -class PluginRequirements -{ -public: - /** - * @return true if the plugin can be enabled (all requirements are met). - */ - 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). - */ - bool hasRequirements() const; - - /** - * @return the proxy that created this plugin, if any. - */ - 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. - */ - 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. - */ - std::vector problems() const; - - /** - * @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: - // 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; - - // 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(); - - // Set the master for this plugin. This is required to "fake" masters for proxied - // plugins. - void setMaster(MOBase::IPlugin* master); - - friend class OrganizerCore; - friend class PluginContainer; - - PluginContainer* m_PluginContainer; - MOBase::IPlugin* m_Plugin; - MOBase::IPluginProxy* m_PluginProxy; - MOBase::IPlugin* m_Master; - std::vector> m_Requirements; - OrganizerProxy* m_Organizer; - std::vector m_RequiredFor; - - PluginRequirements(PluginContainer* pluginContainer, MOBase::IPlugin* plugin, - OrganizerProxy* proxy, MOBase::IPluginProxy* pluginProxy); -}; - -/** - * - */ -class PluginContainer : public QObject, public MOBase::IPluginDiagnose -{ - - Q_OBJECT - Q_INTERFACES(MOBase::IPluginDiagnose) - -private: - 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; - - /** - * This typedefs defines the order of plugin interface. This is increasing order of - * importance". - * - * @note IPlugin is the less important interface, followed by IPluginDiagnose and - * IPluginFileMapper as those are usually implemented together with another - * interface. Other interfaces are in a alphabetical order since it is unlikely a - * plugin will implement multiple ones. - */ - using PluginTypeOrder = boost::mp11::mp_transform< - std::add_pointer_t, - boost::mp11::mp_list< - MOBase::IPluginGame, MOBase::IPluginInstaller, MOBase::IPluginModPage, - MOBase::IPluginPreview, MOBase::IPluginProxy, MOBase::IPluginTool, - MOBase::IPluginDiagnose, MOBase::IPluginFileMapper, MOBase::IPlugin>>; - - static_assert(boost::mp11::mp_size::value == - boost::mp11::mp_size::value - 1); - -public: - /** - * @brief Retrieved the (localized) names of the various plugin interfaces. - * - * @return the (localized) names of the various plugin interfaces. - */ - static QStringList pluginInterfaces(); - -public: - PluginContainer(OrganizerCore* organizer); - virtual ~PluginContainer(); - - /** - * @brief Start the plugins. - * - * This function should not be called before MO2 is ready and plugins can be - * started, and will do the following: - * - connect the callbacks of the plugins, - * - set the parent widget for plugins that can have one, - * - notify plugins that MO2 has been started, including: - * - triggering a call to the "profile changed" callback for the initial profile, - * - triggering a call to the "user interface initialized" callback. - * - * @param userInterface The main user interface to use for the plugins. - */ - void startPlugins(IUserInterface* userInterface); - - /** - * @brief Load, unload or reload the plugin at the given path. - * - */ - void loadPlugin(QString const& filepath); - void unloadPlugin(QString const& filepath); - void reloadPlugin(QString const& filepath); - - /** - * @brief Load all plugins. - * - */ - void loadPlugins(); - - /** - * @brief Retrieve the list of plugins of the given type. - * - * @return the list of plugins of the specified type. - * - * @tparam T The type of plugin to retrieve. - */ - template - const std::vector& plugins() const - { - typename boost::fusion::result_of::at_key::type temp = - boost::fusion::at_key(m_Plugins); - 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; - } - - return qobject_cast(oPlugin); - } - - /** - * @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. - * - * @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; - MOBase::IPlugin* plugin(MOBase::IPluginFileMapper* mapper) const; - - /** - * @brief Find the game plugin corresponding to the given name. - * - * @param name The name of the game to find a plugin for (as returned by - * IPluginGame::gameName()). - * - * @return the game plugin for the given name, or a null pointer if no - * plugin exists for this game. - */ - MOBase::IPluginGame* game(const QString& name) const; - - /** - * @return the IPlugin interface to the currently managed game. - */ - MOBase::IPluginGame* managedGame() 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 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. - * - * @param plugin The plugin to retrieve the requirements for. - * - * @return the requirements (as proxy) for the given plugin. - */ - const PluginRequirements& requirements(MOBase::IPlugin* plugin) const; - - /** - * @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. - */ - QStringList implementedInterfaces(MOBase::IPlugin* plugin) const; - - /** - * @brief Return the (localized) name of the most important interface implemented by - * the given plugin. - * - * The order of interfaces is defined in X. - * - * @param plugin The plugin to retrieve the interface for. - * - * @return the (localized) name of the most important interface implemented by this - * plugin. - */ - QString topImplementedInterface(MOBase::IPlugin* plugin) const; - - /** - * @return the game features. - */ - GameFeatures& gameFeatures() const { return *m_GameFeatures; } - - /** - * @return the preview generator. - */ - const PreviewGenerator& previewGenerator() const { return m_PreviewGenerator; } - - /** - * @return the list of plugin file names, including proxied plugins. - */ - QStringList pluginFileNames() const; - -public: // IPluginDiagnose interface - virtual std::vector activeProblems() const; - virtual QString shortDescription(unsigned int key) const; - virtual QString fullDescription(unsigned int key) const; - virtual bool hasGuidedFix(unsigned int key) const; - virtual void startGuidedFix(unsigned int key) const; - -signals: - - /** - * @brief Emitted when plugins are enabled or disabled. - */ - void pluginEnabled(MOBase::IPlugin*); - void pluginDisabled(MOBase::IPlugin*); - - /** - * @brief Emitted when plugins are registered or unregistered. - */ - void pluginRegistered(MOBase::IPlugin*); - void pluginUnregistered(MOBase::IPlugin*); - - void diagnosisUpdate(); - -private: - friend class PluginRequirements; - - // Unload all the plugins. - void unloadPlugins(); - - // Retrieve the organizer proxy for the given plugin. - OrganizerProxy* organizerProxy(MOBase::IPlugin* plugin) const; - - // Retrieve the proxy plugin that instantiated the given plugin, or a null pointer - // if the plugin was not instantiated by a proxy. - MOBase::IPluginProxy* pluginProxy(MOBase::IPlugin* plugin) const; - - // Retrieve the path to the file or folder corresponding to the plugin. - QString filepath(MOBase::IPlugin* plugin) const; - - // Load plugins from the given filepath using the given proxy. - std::vector loadProxied(const QString& filepath, - MOBase::IPluginProxy* proxy); - - // Load the Qt plugin from the given file. - QObject* loadQtPlugin(const QString& filepath); - - // check if a plugin is folder containing a Qt plugin, it is, return the path to the - // DLL containing the plugin in the folder, otherwise return an empty optional - // - // a Qt plugin folder is a folder with a DLL containing a library (not in a - // subdirectory), if multiple plugins are present, only the first one is returned - // - // extra DLLs are ignored by Qt so can be present in the folder - // - std::optional isQtPluginFolder(const QString& filepath) const; - - // See startPlugins for more details. This is simply an intermediate function - // that can be used when loading plugins after initialization. This uses the - // user interface in m_UserInterface. - void startPluginsImpl(const std::vector& plugins) const; - - /** - * @brief Unload the given plugin. - * - * This function is not public because it's kind of dangerous trying to unload - * plugin directly since some plugins are linked together. - * - * @param plugin The plugin to unload/unregister. - * @param object The QObject corresponding to the plugin. - */ - void unloadPlugin(MOBase::IPlugin* plugin, QObject* object); - - /** - * @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. - * - * @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. - * - * @param plugin The plugin to find the QObject* for. - * - * @return a QObject* for the given plugin. - */ - 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). - * @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 skipInit); - - void registerGame(MOBase::IPluginGame* game); - void unregisterGame(MOBase::IPluginGame* game); - - MOBase::IPlugin* registerPlugin(QObject* pluginObj, const QString& fileName, - MOBase::IPluginProxy* proxy); - - // Core organizer, can be null (e.g. on first MO2 startup). - OrganizerCore* m_Organizer; - - // Main user interface, can be null until MW has been initialized. - IUserInterface* m_UserInterface; - - // Game features - std::unique_ptr m_GameFeatures; - - PluginMap m_Plugins; - - // This maps allow access to IPlugin* from name or diagnose/mapper object. - AccessPluginMap m_AccessPlugins; - - std::map m_Requirements; - - std::map m_SupportedGames; - QStringList m_FailedPlugins; - std::vector m_PluginLoaders; - - PreviewGenerator m_PreviewGenerator; - - QFile m_PluginsCheck; -}; - -#endif // PLUGINCONTAINER_H diff --git a/src/pluginmanager.cpp b/src/pluginmanager.cpp new file mode 100644 index 000000000..48a6e09fe --- /dev/null +++ b/src/pluginmanager.cpp @@ -0,0 +1,708 @@ +#include "pluginmanager.h" + +#include +#include + +#include +#include +#include +#include + +#include "extensionmanager.h" +#include "iuserinterface.h" +#include "organizercore.h" +#include "organizerproxy.h" +#include "previewgenerator.h" +#include "proxyqt.h" + +using namespace MOBase; +namespace bf = boost::fusion; + +// localized names + +template +struct PluginTypeName; + +template <> +struct PluginTypeName +{ + static QString value() { return PluginManager::tr("Plugin"); } +}; +template <> +struct PluginTypeName +{ + static QString value() { return PluginManager::tr("Diagnose"); } +}; +template <> +struct PluginTypeName +{ + static QString value() { return PluginManager::tr("Game"); } +}; +template <> +struct PluginTypeName +{ + static QString value() { return PluginManager::tr("Installer"); } +}; +template <> +struct PluginTypeName +{ + static QString value() { return PluginManager::tr("Mod Page"); } +}; +template <> +struct PluginTypeName +{ + static QString value() { return PluginManager::tr("Preview"); } +}; +template <> +struct PluginTypeName +{ + static QString value() { return PluginManager::tr("Tool"); } +}; +template <> +struct PluginTypeName +{ + static QString value() { return PluginManager::tr("File Mapper"); } +}; + +// PluginDetails + +PluginDetails::PluginDetails(PluginManager* manager, PluginExtension const& extension, + IPlugin* plugin, OrganizerProxy* proxy) + : m_manager(manager), m_extension(&extension), m_plugin(plugin), m_organizer(proxy) +{} + +void PluginDetails::fetchRequirements() +{ + m_requirements = m_plugin->requirements(); +} + +std::vector PluginDetails::problems() const +{ + std::vector result; + for (auto& requirement : m_requirements) { + if (auto p = requirement->check(m_organizer)) { + result.push_back(*p); + } + } + return result; +} + +bool PluginDetails::canEnable() const +{ + return problems().empty(); +} + +bool PluginDetails::hasRequirements() const +{ + return !m_requirements.empty(); +} + +QStringList PluginDetails::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 {}; +} + +// PluginManager + +QStringList PluginManager::pluginInterfaces() +{ + // Find all the names: + QStringList names; + boost::mp11::mp_for_each([&names](const auto* p) { + using plugin_type = std::decay_t; + auto name = PluginTypeName::value(); + if (!name.isEmpty()) { + names.append(name); + } + }); + + return names; +} + +PluginManager::PluginManager(ExtensionManager const& manager, OrganizerCore* core) + : m_extensions{manager}, m_core{core}, + m_gameFeatures(std::make_unique(core, this)) +{ + m_loaders = makeLoaders(); + + if (m_core) { + bf::at_key(m_plugins).push_back(m_core); + m_core->connectPlugins(this); + } +} + +QStringList PluginManager::implementedInterfaces(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 {}; + } + + return implementedInterfaces(oPlugin); +} + +QStringList PluginManager::implementedInterfaces(QObject* oPlugin) const +{ + // Find all the names: + QStringList names; + boost::mp11::mp_for_each([oPlugin, &names](const auto* p) { + using plugin_type = std::decay_t; + if (qobject_cast(oPlugin)) { + auto name = PluginTypeName::value(); + if (!name.isEmpty()) { + names.append(name); + } + } + }); + + // If the plugin implements at least one interface other than IPlugin, remove IPlugin: + if (names.size() > 1) { + names.removeAll(PluginTypeName::value()); + } + + return names; +} + +QString PluginManager::topImplementedInterface(IPlugin* plugin) const +{ + auto interfaces = implementedInterfaces(plugin); + return interfaces.isEmpty() ? "" : interfaces[0]; +} + +bool PluginManager::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; +} + +MOBase::IPluginGame* PluginManager::game(const QString& name) const +{ + auto iter = m_supportedGames.find(name); + if (iter != m_supportedGames.end()) { + return iter->second; + } else { + return nullptr; + } +} + +MOBase::IPluginGame* PluginManager::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 m_core ? const_cast(m_core->managedGame()) : nullptr; +} + +MOBase::IPlugin* PluginManager::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* PluginManager::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* PluginManager::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 PluginManager::isEnabled(MOBase::IPlugin* plugin) const +{ + // check if it is a game plugin + if (implementInterface(plugin)) { + return plugin == m_core->managedGame(); + } + + // TODO: allow disabling/enabling plugins alone? + return m_extensions.isEnabled(details(plugin).extension()); +} + +bool PluginManager::isEnabled(QString const& pluginName) const +{ + IPlugin* p = plugin(pluginName); + return p ? isEnabled(p) : false; +} + +bool PluginManager::isEnabled(MOBase::IPluginDiagnose* diagnose) const +{ + IPlugin* p = plugin(diagnose); + return p ? isEnabled(p) : false; +} + +bool PluginManager::isEnabled(MOBase::IPluginFileMapper* mapper) const +{ + IPlugin* p = plugin(mapper); + return p ? isEnabled(p) : false; +} + +QObject* PluginManager::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; + } + + return *it; +} + +bool PluginManager::initPlugin(PluginExtension const& extension, IPlugin* plugin, + 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 + // + // after proxies are initialized, instantiate() is called for all the plugins + // they've discovered, but as for regular plugins, init() won't be + // called on them if m_OrganizerCore is null + // + + if (plugin == nullptr) { + return false; + } + + OrganizerProxy* proxy = nullptr; + if (m_core) { + proxy = new OrganizerProxy(m_core, m_extensions, this, plugin); + proxy->setParent(as_qobject(plugin)); + } + + auto [it, bl] = + m_details.emplace(plugin, PluginDetails(this, extension, plugin, proxy)); + + if (!m_core || skipInit) { + return true; + } + + if (!plugin->init(proxy)) { + log::warn("plugin failed to initialize"); + return false; + } + + // Update requirements: + it->second.fetchRequirements(); + + return true; +} + +IPlugin* PluginManager::registerPlugin(const PluginExtension& extension, + QObject* plugin, + QList const& pluginGroup) +{ + // generic treatment for all plugins + IPlugin* pluginObj = qobject_cast(plugin); + if (pluginObj == nullptr) { + log::debug("PluginContainer::registerPlugin() called with a non IPlugin QObject."); + return nullptr; + } + + // we check if there is already a plugin with this name, if there is one, it must be + // from the same group + bool skipInit = false; + auto& mapNames = bf::at_key(m_accessPlugins); + if (mapNames.contains(pluginObj->name())) { + + IPlugin* other = mapNames[pluginObj->name()]; + + // if both plugins are from the same group that's ok, we just need to skip + // initialization + if (pluginGroup.contains(as_qobject(other))) { + + // 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(", ")); + bf::at_key(m_accessPlugins)[pluginObj->name()] = pluginObj; + } + } else { + log::warn("trying to register two plugins with the name '{}' (from {} and {}), " + "the second one will not be registered", + pluginObj->name(), details(other).extension().metadata().name(), + extension.metadata().name()); + return nullptr; + } + } 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); + m_allPlugins.push_back(pluginObj); + + plugin->setParent(this); + + if (m_core) { + m_core->settings().plugins().fixPluginEnabledSetting(pluginObj); + m_core->settings().plugins().checkPluginSettings(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; + diagnose->onInvalidated([&, diagnose]() { + emit diagnosePluginInvalidated(diagnose); + }); + } + } + + { // file mapper plugin + 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 + IPluginModPage* modPage = qobject_cast(plugin); + if (initPlugin(extension, modPage, skipInit)) { + bf::at_key(m_plugins).push_back(modPage); + emit pluginRegistered(modPage); + return modPage; + } + } + + { // game plugin + IPluginGame* game = qobject_cast(plugin); + if (game) { + game->detectGame(); + if (initPlugin(extension, game, skipInit)) { + bf::at_key(m_plugins).push_back(game); + registerGame(game); + emit pluginRegistered(game); + return game; + } + } + } + + { // tool plugins + IPluginTool* tool = qobject_cast(plugin); + if (initPlugin(extension, tool, skipInit)) { + bf::at_key(m_plugins).push_back(tool); + emit pluginRegistered(tool); + return tool; + } + } + + { // installer plugins + IPluginInstaller* installer = qobject_cast(plugin); + if (initPlugin(extension, installer, skipInit)) { + bf::at_key(m_plugins).push_back(installer); + if (m_core) { + installer->setInstallationManager(m_core->installationManager()); + } + emit pluginRegistered(installer); + return installer; + } + } + + { // preview plugins + IPluginPreview* preview = qobject_cast(plugin); + if (initPlugin(extension, preview, skipInit)) { + bf::at_key(m_plugins).push_back(preview); + return preview; + } + } + + { // dummy plugins + // only initialize these, no processing otherwise + IPlugin* dummy = qobject_cast(plugin); + if (initPlugin(extension, dummy, skipInit)) { + bf::at_key(m_plugins).push_back(dummy); + emit pluginRegistered(dummy); + return dummy; + } + } + + return nullptr; +} + +void PluginManager::loadPlugins() +{ + unloadPlugins(); + + // TODO: order based on dependencies + for (auto& extension : m_extensions.extensions()) { + if (auto* pluginExtension = dynamic_cast(extension.get())) { + loadPlugins(*pluginExtension); + } + } +} + +bool PluginManager::loadPlugins(const MOBase::PluginExtension& extension) +{ + unloadPlugins(extension); + + // load plugins + QList> objects; + for (auto& loader : m_loaders) { + objects.append(loader->load(extension)); + } + + for (auto& objectGroup : objects) { + + // safety for min_element + if (objectGroup.isEmpty()) { + continue; + } + + // register plugins in the group + for (auto* object : objectGroup) { + registerPlugin(extension, object, objectGroup); + } + } + + return true; +} + +void PluginManager::unloadPlugin(MOBase::IPlugin* plugin, QObject* object) +{ + if (auto* game = qobject_cast(object)) { + + if (game == managedGame()) { + throw Exception("cannot unload the plugin for the currently managed game"); + } + + unregisterGame(game); + } + + // we need to remove from the m_plugins maps BEFORE unloading from the proxy + // otherwise the qobject_cast to check the plugin type will not work + bf::for_each(m_plugins, [object](auto& t) { + using type = typename std::decay_t::value_type; + + // we do not want to remove from QObject since we are iterating over them + if constexpr (!std::is_same{}) { + auto itp = + std::find(t.second.begin(), t.second.end(), qobject_cast(object)); + if (itp != t.second.end()) { + t.second.erase(itp); + } + } + }); + + emit pluginUnregistered(plugin); + + // remove from the members + if (auto* diagnose = qobject_cast(object)) { + bf::at_key(m_accessPlugins).erase(diagnose); + } + if (auto* mapper = qobject_cast(object)) { + bf::at_key(m_accessPlugins).erase(mapper); + } + + auto& mapNames = bf::at_key(m_accessPlugins); + if (mapNames.contains(plugin->name())) { + mapNames.erase(plugin->name()); + } + + // force disconnection of the signals from the proxies + // + // this is a safety operations since those signals should be disconnected when the + // proxies are destroyed anyway + // + details(plugin).m_organizer->disconnectSignals(); + + // do this at the end + m_details.erase(plugin); +} + +bool PluginManager::unloadPlugins(const MOBase::PluginExtension& extension) +{ + std::vector objectsToDelete; + + // first we clear the internal structures, disconnect signales, etc. + { + auto& objects = bf::at_key(m_plugins); + for (auto it = objects.begin(); it != objects.end();) { + auto* plugin = qobject_cast(*it); + if (&details(plugin).extension() == &extension) { + unloadPlugin(plugin, *it); + objectsToDelete.push_back(*it); + it = objects.erase(it); + } else { + ++it; + } + } + } + + // then we let the loader unload the plugin + for (auto& loader : m_loaders) { + loader->unload(extension); + } + + // manual delete (for safety) + for (auto* object : objectsToDelete) { + object->deleteLater(); + } + + return true; +} + +void PluginManager::unloadPlugins() +{ + bf::for_each(m_plugins, [](auto& t) { + t.second.clear(); + }); + bf::for_each(m_accessPlugins, [](auto& t) { + t.second.clear(); + }); + + m_details.clear(); + m_supportedGames.clear(); + + for (auto& loader : m_loaders) { + loader->unloadAll(); + } +} + +bool PluginManager::reloadPlugins(const MOBase::PluginExtension& extension) +{ + // load plugin already unload(), so no need to manually do it here + return loadPlugins(extension); +} + +void PluginManager::registerGame(MOBase::IPluginGame* game) +{ + m_supportedGames.insert({game->gameName(), game}); +} + +void PluginManager::unregisterGame(MOBase::IPluginGame* game) +{ + m_supportedGames.erase(game->gameName()); +} + +void PluginManager::startPlugins(IUserInterface* userInterface) +{ + m_userInterface = userInterface; + startPluginsImpl(plugins()); +} + +void PluginManager::startPluginsImpl(const std::vector& plugins) const +{ + if (m_userInterface) { + for (auto* plugin : plugins) { + if (auto* modPage = qobject_cast(plugin)) { + modPage->setParentWidget(m_userInterface->mainWindow()); + } + if (auto* tool = qobject_cast(plugin)) { + tool->setParentWidget(m_userInterface->mainWindow()); + } + if (auto* installer = qobject_cast(plugin)) { + installer->setParentWidget(m_userInterface->mainWindow()); + } + } + } + + // Trigger initial callbacks, e.g. onUserInterfaceInitialized and onProfileChanged. + if (m_core) { + for (auto* object : plugins) { + auto* plugin = qobject_cast(object); + auto* oproxy = details(plugin).m_organizer; + oproxy->connectSignals(); + oproxy->m_ProfileChanged(nullptr, m_core->currentProfile()); + + if (m_userInterface) { + oproxy->m_UserInterfaceInitialized(m_userInterface->mainWindow()); + } + } + } +} + +PluginManager::PluginLoaderDeleter::PluginLoaderDeleter(QPluginLoader* qPluginLoader) + : m_qPluginLoader(qPluginLoader) +{} + +void PluginManager::PluginLoaderDeleter::operator()(MOBase::IPluginLoader* loader) const +{ + // if there is a QPluginLoader, the loader is responsible for unloading the plugin + if (m_qPluginLoader) { + m_qPluginLoader->unload(); + delete m_qPluginLoader; + } else { + delete loader; + } +} + +std::vector PluginManager::makeLoaders() +{ + std::vector loaders; + + // create the Qt loader + loaders.push_back(PluginLoaderPtr(new ProxyQtLoader(), PluginLoaderDeleter{})); + + // load the python proxy + { + const QString proxyPath = + QCoreApplication::applicationDirPath() + "/proxies/python"; + auto pluginLoader = + std::make_unique(proxyPath + "/python_proxy.dll", this); + + if (auto* object = pluginLoader->instance(); object) { + auto loader = qobject_cast(object); + QString errorMessage; + + if (loader->initialize(errorMessage)) { + loaders.push_back( + PluginLoaderPtr(loader, PluginLoaderDeleter{pluginLoader.release()})); + } else { + log::error("failed to initialize proxy from '{}': {}", proxyPath, errorMessage); + } + } + } + + return loaders; +} diff --git a/src/pluginmanager.h b/src/pluginmanager.h new file mode 100644 index 000000000..b51b6d229 --- /dev/null +++ b/src/pluginmanager.h @@ -0,0 +1,353 @@ +#ifndef PLUGINMANAGER_H +#define PLUGINMANAGER_H + +#include + +#ifndef Q_MOC_RUN +#include +#include +#include +#endif // Q_MOC_RUN + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "game_features.h" +#include "previewgenerator.h" + +class ExtensionManager; +class IUserInterface; +class OrganizerCore; +class OrganizerProxy; +class PluginManager; + +// class containing extra useful information for plugins +// +class PluginDetails +{ +public: + // check if the plugin can be enabled (all requirements are met) + // + bool canEnable() const; + + // check if this plugin has requirements (satisfied or not) + // + bool hasRequirements() const; + + // the organizer proxy for this plugin + // + const auto* proxy() const { return m_organizer; } + + // the extension containing this plugin + // + const MOBase::PluginExtension& extension() const { return *m_extension; } + + // retrieve the list of problems to be resolved before enabling the plugin + // + std::vector problems() const; + + // retrieve 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; + +private: + // 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 PluginManager; + + PluginManager* m_manager; + MOBase::IPlugin* m_plugin; + const MOBase::PluginExtension* m_extension; + std::vector> m_requirements; + OrganizerProxy* m_organizer; + + PluginDetails(PluginManager* manager, MOBase::PluginExtension const& extension, + MOBase::IPlugin* plugin, OrganizerProxy* proxy); +}; + +// manager for plugins +// +class PluginManager : public QObject +{ + Q_OBJECT +public: + // retrieve the (localized) names of the various plugin interfaces + // + static QStringList pluginInterfaces(); + +public: + PluginManager(ExtensionManager const& manager, OrganizerCore* core); + +public: // access + // retrieve the list of plugins of a given type + // + // - if no type is specified, return the list of all plugins as IPlugin + // - if IPlugin is specified, returns only plugins that only extends IPlugin + // + // + template + const auto& plugins() const + { + if constexpr (std::is_void_v) { + + return m_allPlugins; + } else { + return boost::fusion::at_key(m_plugins); + } + } + + // retrieve the details for the given plugin + // + const auto& details(MOBase::IPlugin* plugin) const { return m_details.at(plugin); } + + // retrieve the (localized) names of interfaces implemented by the given plugin + // + QStringList implementedInterfaces(MOBase::IPlugin* plugin) const; + + // retrieve the (localized) name of the most important interface implemented by the + // given plugin + // + QString topImplementedInterface(MOBase::IPlugin* plugin) const; + + // retrieve a plugin from its name or a corresponding non-IPlugin interface + // + MOBase::IPlugin* plugin(QString const& pluginName) const; + MOBase::IPlugin* plugin(MOBase::IPluginDiagnose* diagnose) const; + MOBase::IPlugin* plugin(MOBase::IPluginFileMapper* mapper) const; + + // find the game plugin corresponding to the given name, returns a null pointer if no + // game exists + // + MOBase::IPluginGame* game(const QString& name) const; + + // retrieve the IPlugin interface to the currently managed game. + // + MOBase::IPluginGame* managedGame() const; + + // retrieve the game features + // + GameFeatures& gameFeatures() const { return *m_gameFeatures; } + + // retrieve the preview generator + // + const PreviewGenerator& previewGenerator() const { return *m_previews; } + +public: // checks + // check if a plugin implement a given plugin interface + // + 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; + } + + return qobject_cast(oPlugin); + } + + // check if a plugin is enabled + // + bool isEnabled(MOBase::IPlugin* plugin) const; + bool isEnabled(QString const& pluginName) const; + bool isEnabled(MOBase::IPluginDiagnose* diagnose) const; + bool isEnabled(MOBase::IPluginFileMapper* mapper) const; + +public: // load + // load all plugins from the extension manager + // + void loadPlugins(); + + // load plugins from the given extension + // + bool loadPlugins(const MOBase::PluginExtension& extension); + bool unloadPlugins(const MOBase::PluginExtension& extension); + bool reloadPlugins(const MOBase::PluginExtension& extension); + + // start the plugins + // + // this function should not be called before MO2 is ready and plugins can be started, + // and will do the following: + // - connect the callbacks of the plugins, + // - set the parent widget for plugins that can have one, + // - notify plugins that MO2 has been started, including: + // - triggering a call to the "profile changed" callback for the initial profile, + // - triggering a call to the "user interface initialized" callback. + // + void startPlugins(IUserInterface* userInterface); + +signals: + + // emitted when plugins are enabled or disabled + // + void pluginEnabled(MOBase::IPlugin*); + void pluginDisabled(MOBase::IPlugin*); + + // emitted when plugins are registered or unregistered + // + void pluginRegistered(MOBase::IPlugin*); + void pluginUnregistered(MOBase::IPlugin*); + + // enmitted when a diagnose plugin invalidates() itself + // + void diagnosePluginInvalidated(MOBase::IPluginDiagnose*); + +private: + friend class PluginDetails; + +private: + 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>>; + + using AccessPluginMap = boost::fusion::map< + boost::fusion::pair>, + boost::fusion::pair>, + boost::fusion::pair>>; + + // type defining the order of plugin interface, in increasing order of importance + // + // IPlugin is the less important interface, followed by IPluginDiagnose and + // IPluginFileMapper as those are usually implemented together with another interface + // + // other interfaces are in a alphabetical order since it is unlikely a plugin will + // implement multiple ones + // + using PluginTypeOrder = boost::mp11::mp_transform< + std::add_pointer_t, + boost::mp11::mp_list>; + static_assert(boost::mp11::mp_size::value == + boost::mp11::mp_size::value - 1); + +private: + // retrieve the (localized) names of interfaces implemented by the given plugin + // + // this function can be used to get implemented interfaces before registering a plugin + // + QStringList implementedInterfaces(QObject* plugin) const; + + // check if the left plugin implements a "better" interface than the right one, as + // specified by PluginTypeOrder + // + bool isBetterInterface(QObject* lhs, QObject* rhs) const; + + // find the QObject* corresponding to the given plugin + // + QObject* as_qobject(MOBase::IPlugin* plugin) const; + + // see startPlugins for more details + // + // this is simply an intermediate function that can be used when loading plugins after + // initialization which uses the user interface in m_userInterface + // + void startPluginsImpl(const std::vector& plugins) const; + + // unload the given plugin + // + // this function is not public because it's kind of dangerous trying to unload plugin + // directly since some plugins are linked together + // + void unloadPlugin(MOBase::IPlugin* plugin, QObject* object); + + // unload all plugins + // + void unloadPlugins(); + + // register/unregister a game plugin + // + void registerGame(MOBase::IPluginGame* game); + void unregisterGame(MOBase::IPluginGame* game); + + // initialize a plugin and creates approriate PluginDetails for it + // + bool initPlugin(MOBase::PluginExtension const& extension, MOBase::IPlugin* plugin, + bool skipInit); + + // register a plugin for the given extension + // + MOBase::IPlugin* registerPlugin(const MOBase::PluginExtension& extension, + QObject* pluginObj, + QList const& pluginGroup); + +private: + struct PluginLoaderDeleter + { + PluginLoaderDeleter(QPluginLoader* qPluginLoader = nullptr); + + void operator()(MOBase::IPluginLoader* loader) const; + + private: + QPluginLoader* m_qPluginLoader; + }; + + using PluginLoaderPtr = std::unique_ptr; + + // create the loaders + // + std::vector makeLoaders(); + +private: + const ExtensionManager& m_extensions; + + // core organizer, can be null (e.g. on first MO2 startup). + OrganizerCore* m_core; + + // main user interface, can be null until MW has been initialized. + IUserInterface* m_userInterface; + + // Game features + std::unique_ptr m_gameFeatures; + + // plugin loaders + std::vector m_loaders; + + PluginMap m_plugins; + std::vector m_allPlugins; + + // this maps allow access to IPlugin* from name or diagnose/mapper object, and from + // game + AccessPluginMap m_accessPlugins; + std::map m_supportedGames; + + // details for plugins + std::map m_details; + + // the preview generator + PreviewGenerator* m_previews; +}; + +#endif diff --git a/src/previewgenerator.cpp b/src/previewgenerator.cpp index dad41c2fa..cd5376cfe 100644 --- a/src/previewgenerator.cpp +++ b/src/previewgenerator.cpp @@ -25,12 +25,12 @@ along with Mod Organizer. If not, see . #include #include -#include "plugincontainer.h" +#include "pluginmanager.h" using namespace MOBase; -PreviewGenerator::PreviewGenerator(const PluginContainer& pluginContainer) - : m_PluginContainer(pluginContainer) +PreviewGenerator::PreviewGenerator(const PluginManager& pluginManager) + : m_PluginManager(pluginManager) { m_MaxSize = QGuiApplication::primaryScreen()->size() * 0.8; } @@ -38,7 +38,7 @@ PreviewGenerator::PreviewGenerator(const PluginContainer& pluginContainer) bool PreviewGenerator::previewSupported(const QString& fileExtension, const bool& isArchive) const { - auto& previews = m_PluginContainer.plugins(); + auto& previews = m_PluginManager.plugins(); for (auto* preview : previews) { if (preview->supportedExtensions().contains(fileExtension)) { if (!isArchive) @@ -53,9 +53,9 @@ bool PreviewGenerator::previewSupported(const QString& fileExtension, QWidget* PreviewGenerator::genPreview(const QString& fileName) const { const QString ext = QFileInfo(fileName).suffix().toLower(); - auto& previews = m_PluginContainer.plugins(); + auto& previews = m_PluginManager.plugins(); for (auto* preview : previews) { - if (m_PluginContainer.isEnabled(preview) && + if (m_PluginManager.isEnabled(preview) && preview->supportedExtensions().contains(ext)) { return preview->genFilePreview(fileName, m_MaxSize); } @@ -67,9 +67,9 @@ QWidget* PreviewGenerator::genArchivePreview(const QByteArray& fileData, const QString& fileName) const { const QString ext = QFileInfo(fileName).suffix().toLower(); - auto& previews = m_PluginContainer.plugins(); + auto& previews = m_PluginManager.plugins(); for (auto* preview : previews) { - if (m_PluginContainer.isEnabled(preview) && + if (m_PluginManager.isEnabled(preview) && preview->supportedExtensions().contains(ext) && preview->supportsArchives()) { return preview->genDataPreview(fileData, fileName, m_MaxSize); } diff --git a/src/previewgenerator.h b/src/previewgenerator.h index 15d652f62..cb82824d7 100644 --- a/src/previewgenerator.h +++ b/src/previewgenerator.h @@ -26,12 +26,12 @@ along with Mod Organizer. If not, see . #include #include -class PluginContainer; +class PluginManager; class PreviewGenerator { public: - PreviewGenerator(const PluginContainer& pluginContainer); + PreviewGenerator(const PluginManager& pluginManager); bool previewSupported(const QString& fileExtension, const bool& isArchive) const; @@ -40,7 +40,7 @@ class PreviewGenerator QWidget* genArchivePreview(const QByteArray& fileData, const QString& fileName) const; private: - const PluginContainer& m_PluginContainer; + const PluginManager& m_PluginManager; QSize m_MaxSize; }; diff --git a/src/problemsdialog.cpp b/src/problemsdialog.cpp index 1b049fcdf..5a7bb7360 100644 --- a/src/problemsdialog.cpp +++ b/src/problemsdialog.cpp @@ -7,12 +7,12 @@ #include #include -#include "plugincontainer.h" +#include "pluginmanager.h" using namespace MOBase; -ProblemsDialog::ProblemsDialog(const PluginContainer& pluginContainer, QWidget* parent) - : QDialog(parent), ui(new Ui::ProblemsDialog), m_PluginContainer(pluginContainer), +ProblemsDialog::ProblemsDialog(const PluginManager& pluginManager, QWidget* parent) + : QDialog(parent), ui(new Ui::ProblemsDialog), m_PluginManager(pluginManager), m_hasProblems(false) { ui->setupUi(this); @@ -42,13 +42,13 @@ void ProblemsDialog::runDiagnosis() m_hasProblems = false; ui->problemsWidget->clear(); - for (IPluginDiagnose* diagnose : m_PluginContainer.plugins()) { - if (!m_PluginContainer.isEnabled(diagnose)) { + for (IPluginDiagnose* diagnose : m_PluginManager.plugins()) { + if (!m_PluginManager.isEnabled(diagnose)) { continue; } std::vector activeProblems = diagnose->activeProblems(); - foreach (unsigned int key, activeProblems) { + for (const auto key : activeProblems) { QTreeWidgetItem* newItem = new QTreeWidgetItem(); newItem->setText(0, diagnose->shortDescription(key)); newItem->setData(0, Qt::UserRole, diagnose->fullDescription(key)); diff --git a/src/problemsdialog.h b/src/problemsdialog.h index 288f50491..25ab83763 100644 --- a/src/problemsdialog.h +++ b/src/problemsdialog.h @@ -10,14 +10,14 @@ namespace Ui class ProblemsDialog; } -class PluginContainer; +class PluginManager; class ProblemsDialog : public QDialog { Q_OBJECT public: - explicit ProblemsDialog(PluginContainer const& pluginContainer, QWidget* parent = 0); + explicit ProblemsDialog(PluginManager const& pluginContainer, QWidget* parent = 0); ~ProblemsDialog(); // also saves and restores geometry @@ -37,7 +37,7 @@ private slots: private: Ui::ProblemsDialog* ui; - const PluginContainer& m_PluginContainer; + const PluginManager& m_PluginManager; bool m_hasProblems; }; diff --git a/src/proxyqt.cpp b/src/proxyqt.cpp new file mode 100644 index 000000000..42e9567ce --- /dev/null +++ b/src/proxyqt.cpp @@ -0,0 +1,71 @@ +#include "proxyqt.h" + +#include + +using namespace MOBase; + +void ProxyQtLoader::QPluginLoaderDeleter::operator()(QPluginLoader* loader) const +{ + if (loader) { + loader->unload(); + delete loader; + } +} + +ProxyQtLoader::ProxyQtLoader() {} + +bool ProxyQtLoader::initialize(QString& errorMessage) +{ + return true; +} + +QList> ProxyQtLoader::load(const MOBase::PluginExtension& extension) +{ + QList> plugins; + + // TODO - retrieve plugins from extension instead of listing them + + QDirIterator iter( + QDir(extension.directory(), {}, QDir::NoSort, QDir::Files | QDir::NoDotAndDotDot), + QDirIterator::Subdirectories); + while (iter.hasNext()) { + iter.next(); + const auto filePath = iter.filePath(); + + // not a library, skip + if (!QLibrary::isLibrary(filePath)) { + continue; + } + + // check if we have proper metadata - this does not load the plugin (metaData() + // should be very lightweight) + auto loader = QPluginLoaderPtr(new QPluginLoader(filePath)); + if (loader->metaData().isEmpty()) { + log::debug("no metadata found in '{}', skipping", filePath); + continue; + } + + QObject* instance = loader->instance(); + if (!instance) { + log::warn("failed to load plugin from '{}', skipping", filePath); + continue; + } + + m_loaders[&extension].push_back(std::move(loader)); + plugins.push_back({instance}); + } + + return plugins; +} + +void ProxyQtLoader::unload(const MOBase::PluginExtension& extension) +{ + if (auto it = m_loaders.find(&extension); it != m_loaders.end()) { + m_loaders.erase(it); + } +} + +void ProxyQtLoader::unloadAll() +{ + m_loaders.clear(); +} diff --git a/src/proxyqt.h b/src/proxyqt.h new file mode 100644 index 000000000..a234fab1e --- /dev/null +++ b/src/proxyqt.h @@ -0,0 +1,32 @@ +#ifndef PROXYQTLOADER_H +#define PROXYQTLOADER_H + +#include + +#include + +class ProxyQtLoader : public MOBase::IPluginLoader +{ + Q_OBJECT + Q_INTERFACES(MOBase::IPluginLoader) + Q_PLUGIN_METADATA(IID "org.mo2.ProxyQt") + +public: + ProxyQtLoader(); + + bool initialize(QString& errorMessage) override; + QList> load(const MOBase::PluginExtension& extension) override; + void unload(const MOBase::PluginExtension& extension) override; + void unloadAll() override; + +private: + struct QPluginLoaderDeleter + { + void operator()(QPluginLoader*) const; + }; + using QPluginLoaderPtr = std::unique_ptr; + + std::map> m_loaders; +}; + +#endif diff --git a/src/selfupdater.cpp b/src/selfupdater.cpp index 43992e707..d41b43ed0 100644 --- a/src/selfupdater.cpp +++ b/src/selfupdater.cpp @@ -26,7 +26,7 @@ along with Mod Organizer. If not, see . #include "nexusinterface.h" #include "nxmaccessmanager.h" #include "organizercore.h" -#include "plugincontainer.h" +#include "pluginmanager.h" #include "settings.h" #include "shared/util.h" #include "updatedialog.h" @@ -80,9 +80,9 @@ void SelfUpdater::setUserInterface(QWidget* widget) m_Parent = widget; } -void SelfUpdater::setPluginContainer(PluginContainer* pluginContainer) +void SelfUpdater::setPluginManager(PluginManager* pluginManager) { - m_Interface->setPluginContainer(pluginContainer); + m_Interface->setPluginManager(pluginManager); } void SelfUpdater::testForUpdate(const Settings& settings) diff --git a/src/selfupdater.h b/src/selfupdater.h index b39340849..0db43cb98 100644 --- a/src/selfupdater.h +++ b/src/selfupdater.h @@ -24,7 +24,7 @@ along with Mod Organizer. If not, see . class Archive; class NexusInterface; -class PluginContainer; +class PluginManager; namespace MOBase { class IPluginGame; @@ -83,7 +83,7 @@ class SelfUpdater : public QObject void setUserInterface(QWidget* widget); - void setPluginContainer(PluginContainer* pluginContainer); + void setPluginManager(PluginManager* pluginManager); /** * @brief request information about the current version diff --git a/src/settings.cpp b/src/settings.cpp index d88311ebe..53b5bc96c 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -64,9 +64,10 @@ Settings* Settings::s_Instance = nullptr; Settings::Settings(const QString& path, bool globalInstance) : m_Settings(path, QSettings::IniFormat), m_Game(m_Settings), m_Geometry(m_Settings), m_Widgets(m_Settings, globalInstance), - m_Colors(m_Settings), m_Plugins(m_Settings), m_Paths(m_Settings), - m_Network(m_Settings, globalInstance), m_Nexus(*this, m_Settings), - m_Steam(*this, m_Settings), m_Interface(m_Settings), m_Diagnostics(m_Settings) + m_Colors(m_Settings), m_Extensions(m_Settings), m_Plugins(m_Settings), + m_Paths(m_Settings), m_Network(m_Settings, globalInstance), + m_Nexus(*this, m_Settings), m_Steam(*this, m_Settings), m_Interface(m_Settings), + m_Diagnostics(m_Settings) { if (globalInstance) { if (s_Instance != nullptr) { @@ -457,6 +458,16 @@ const ColorSettings& Settings::colors() const return m_Colors; } +ExtensionSettings& Settings::extensions() +{ + return m_Extensions; +} + +const ExtensionSettings& Settings::extensions() const +{ + return m_Extensions; +} + PluginSettings& Settings::plugins() { return m_Plugins; @@ -1338,251 +1349,6 @@ QColor ColorSettings::idealTextColor(const QColor& rBackgroundColor) return QColor(iLuminance >= 128 ? Qt::black : Qt::white); } -PluginSettings::PluginSettings(QSettings& settings) : m_Settings(settings) {} - -void PluginSettings::clearPlugins() -{ - m_Plugins.clear(); - m_PluginSettings.clear(); - m_PluginBlacklist.clear(); - - m_PluginBlacklist = readBlacklist(); -} - -void PluginSettings::registerPlugin(IPlugin* plugin) -{ - m_Plugins.push_back(plugin); - m_PluginSettings.insert(plugin->name(), QVariantMap()); - m_PluginDescriptions.insert(plugin->name(), QVariantMap()); - - for (const PluginSetting& setting : plugin->settings()) { - const QString settingName = plugin->name() + "/" + setting.key; - - QVariant temp = get(m_Settings, "Plugins", settingName, QVariant()); - - // 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; - } - - m_PluginSettings[plugin->name()][setting.key] = temp; - - m_PluginDescriptions[plugin->name()][setting.key] = - QString("%1 (default: %2)") - .arg(setting.description) - .arg(setting.defaultValue.toString()); - } - - // 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"); - } -} - -void PluginSettings::unregisterPlugin(IPlugin* plugin) -{ - auto it = std::find(m_Plugins.begin(), m_Plugins.end(), plugin); - if (it != m_Plugins.end()) { - m_Plugins.erase(it); - } - m_PluginSettings.remove(plugin->name()); - m_PluginDescriptions.remove(plugin->name()); -} - -std::vector PluginSettings::plugins() const -{ - return m_Plugins; -} - -QVariant PluginSettings::setting(const QString& pluginName, const QString& key) const -{ - auto iterPlugin = m_PluginSettings.find(pluginName); - if (iterPlugin == m_PluginSettings.end()) { - return QVariant(); - } - - auto iterSetting = iterPlugin->find(key); - if (iterSetting == iterPlugin->end()) { - return QVariant(); - } - - return *iterSetting; -} - -void PluginSettings::setSetting(const QString& pluginName, const QString& key, - const QVariant& value) -{ - auto iterPlugin = m_PluginSettings.find(pluginName); - - if (iterPlugin == m_PluginSettings.end()) { - throw MyException(QObject::tr("attempt to store setting for unknown plugin \"%1\"") - .arg(pluginName)); - } - - QVariant oldValue = m_PluginSettings[pluginName][key]; - - // store the new setting both in memory and in the ini - m_PluginSettings[pluginName][key] = value; - set(m_Settings, "Plugins", pluginName + "/" + key, value); - - // emit signal: - emit pluginSettingChanged(pluginName, key, oldValue, value); -} - -QVariantMap PluginSettings::settings(const QString& pluginName) const -{ - return m_PluginSettings[pluginName]; -} - -void PluginSettings::setSettings(const QString& pluginName, const QVariantMap& map) -{ - auto iterPlugin = m_PluginSettings.find(pluginName); - - if (iterPlugin == m_PluginSettings.end()) { - throw MyException(QObject::tr("attempt to store setting for unknown plugin \"%1\"") - .arg(pluginName)); - } - - QVariantMap oldSettings = m_PluginSettings[pluginName]; - m_PluginSettings[pluginName] = map; - - // Emit signals for settings that have been changed or added: - for (auto& k : map.keys()) { - // .value() return a default-constructed QVariant if k is not in oldSettings: - QVariant oldValue = oldSettings.value(k); - if (oldValue != map[k]) { - emit pluginSettingChanged(pluginName, k, oldSettings.value(k), map[k]); - } - } - - // Emit signals for settings that have been removed: - for (auto& k : oldSettings.keys()) { - if (!map.contains(k)) { - emit pluginSettingChanged(pluginName, k, oldSettings[k], QVariant()); - } - } -} - -QVariantMap PluginSettings::descriptions(const QString& pluginName) const -{ - return m_PluginDescriptions[pluginName]; -} - -void PluginSettings::setDescriptions(const QString& pluginName, const QVariantMap& map) -{ - m_PluginDescriptions[pluginName] = map; -} - -QVariant PluginSettings::persistent(const QString& pluginName, const QString& key, - const QVariant& def) const -{ - if (!m_PluginSettings.contains(pluginName)) { - return def; - } - - return get(m_Settings, "PluginPersistance", pluginName + "/" + key, def); -} - -void PluginSettings::setPersistent(const QString& pluginName, const QString& key, - const QVariant& value, bool sync) -{ - if (!m_PluginSettings.contains(pluginName)) { - throw MyException(QObject::tr("attempt to store setting for unknown plugin \"%1\"") - .arg(pluginName)); - } - - set(m_Settings, "PluginPersistance", pluginName + "/" + key, value); - - if (sync) { - m_Settings.sync(); - } -} - -void PluginSettings::addBlacklist(const QString& fileName) -{ - m_PluginBlacklist.insert(fileName); - writeBlacklist(); -} - -bool PluginSettings::blacklisted(const QString& fileName) const -{ - return m_PluginBlacklist.contains(fileName); -} - -void PluginSettings::setBlacklist(const QStringList& pluginNames) -{ - m_PluginBlacklist.clear(); - - for (const auto& name : pluginNames) { - m_PluginBlacklist.insert(name); - } -} - -const QSet& PluginSettings::blacklist() const -{ - return m_PluginBlacklist; -} - -void PluginSettings::save() -{ - for (auto iterPlugins = m_PluginSettings.begin(); - iterPlugins != m_PluginSettings.end(); ++iterPlugins) { - for (auto iterSettings = iterPlugins->begin(); iterSettings != iterPlugins->end(); - ++iterSettings) { - const auto key = iterPlugins.key() + "/" + iterSettings.key(); - set(m_Settings, "Plugins", key, iterSettings.value()); - } - } - - writeBlacklist(); -} - -void PluginSettings::writeBlacklist() -{ - const auto current = readBlacklist(); - - if (current.size() > m_PluginBlacklist.size()) { - // Qt can't remove array elements, the section must be cleared - removeSection(m_Settings, "pluginBlacklist"); - } - - ScopedWriteArray swa(m_Settings, "pluginBlacklist", m_PluginBlacklist.size()); - - for (const QString& plugin : m_PluginBlacklist) { - swa.next(); - swa.set("name", plugin); - } -} - -QSet PluginSettings::readBlacklist() const -{ - QSet set; - - ScopedReadArray sra(m_Settings, "pluginBlacklist"); - sra.for_each([&] { - set.insert(sra.get("name")); - }); - - return set; -} - const QString PathSettings::BaseDirVariable = "%BASE_DIR%"; PathSettings::PathSettings(QSettings& settings) : m_Settings(settings) {} @@ -2136,12 +1902,12 @@ void InterfaceSettings::setLockGUI(bool b) set(m_Settings, "Settings", "lock_gui", b); } -std::optional InterfaceSettings::styleName() const +std::optional InterfaceSettings::themeName() const { return getOptional(m_Settings, "Settings", "style"); } -void InterfaceSettings::setStyleName(const QString& name) +void InterfaceSettings::setThemeName(const QString& name) { set(m_Settings, "Settings", "style", name); } diff --git a/src/settings.h b/src/settings.h index bd58e10d4..b0a01db1f 100644 --- a/src/settings.h +++ b/src/settings.h @@ -20,13 +20,16 @@ along with Mod Organizer. If not, see . #ifndef SETTINGS_H #define SETTINGS_H -#include "envdump.h" -#include -#include #include #include +#include #include +#include + +#include "envdump.h" +#include "extensionsettings.h" + #ifdef interface #undef interface #endif @@ -278,102 +281,6 @@ class ColorSettings QSettings& m_Settings; }; -// settings about plugins -// -class PluginSettings : public QObject -{ - Q_OBJECT - -public: - PluginSettings(QSettings& settings); - - // forgets all the plugins - // - void clearPlugins(); - - // adds/removes the given plugin to the list and loads all of its settings - // - void registerPlugin(MOBase::IPlugin* plugin); - void unregisterPlugin(MOBase::IPlugin* plugin); - - // returns all the registered plugins - // - std::vector plugins() const; - - // returns the plugin setting for the given key - // - QVariant setting(const QString& pluginName, const QString& key) const; - - // sets the plugin setting for the given key - // - void setSetting(const QString& pluginName, const QString& key, const QVariant& value); - - // returns all settings - // - QVariantMap settings(const QString& pluginName) const; - - // overwrites all settings - // - void setSettings(const QString& pluginName, const QVariantMap& map); - - // returns all descriptions - // - QVariantMap descriptions(const QString& pluginName) const; - - // overwrites all descriptions - // - void setDescriptions(const QString& pluginName, const QVariantMap& map); - - // ? - QVariant persistent(const QString& pluginName, const QString& key, - const QVariant& def) const; - void setPersistent(const QString& pluginName, const QString& key, - const QVariant& value, bool sync); - - // adds the given plugin to the blacklist - // - void addBlacklist(const QString& fileName); - - // returns whether the given plugin is blacklisted - // - bool blacklisted(const QString& fileName) const; - - // overwrites the whole blacklist - // - void setBlacklist(const QStringList& pluginNames); - - // returns the blacklist - // - const QSet& blacklist() const; - - // commits all the settings to the ini - // - void save(); - -Q_SIGNALS: - - /** - * Emitted when a plugin setting changes. - */ - void pluginSettingChanged(QString const& pluginName, const QString& key, - const QVariant& oldValue, const QVariant& newValue); - -private: - QSettings& m_Settings; - std::vector m_Plugins; - QMap m_PluginSettings; - QMap m_PluginDescriptions; - QSet m_PluginBlacklist; - - // commits the blacklist to the ini - // - void writeBlacklist(); - - // reads the blacklist from the ini - // - QSet readBlacklist() const; -}; - // paths for the game and various components // // if the 'resolve' parameter is true, %BASE_DIR% is expanded; it's set to @@ -592,8 +499,8 @@ class InterfaceSettings // filename of the theme // - std::optional styleName() const; - void setStyleName(const QString& name); + std::optional themeName() const; + void setThemeName(const QString& name); // whether to use collapsible separators when possible // @@ -861,6 +768,9 @@ class Settings : public QObject ColorSettings& colors(); const ColorSettings& colors() const; + ExtensionSettings& extensions(); + const ExtensionSettings& extensions() const; + PluginSettings& plugins(); const PluginSettings& plugins() const; @@ -901,7 +811,7 @@ public slots: // these are fired from outside the settings, mostly by the settings dialog // void languageChanged(const QString& newLanguage); - void styleChanged(const QString& newStyle); + void themeChanged(const QString& themeIdentifier); private: static Settings* s_Instance; @@ -911,6 +821,7 @@ public slots: GeometrySettings m_Geometry; WidgetSettings m_Widgets; ColorSettings m_Colors; + ExtensionSettings m_Extensions; PluginSettings m_Plugins; PathSettings m_Paths; NetworkSettings m_Network; diff --git a/src/settingsdialog.cpp b/src/settingsdialog.cpp index 425a1cb80..d352489ba 100644 --- a/src/settingsdialog.cpp +++ b/src/settingsdialog.cpp @@ -19,27 +19,31 @@ along with Mod Organizer. If not, see . #include "settingsdialog.h" #include "settingsdialogdiagnostics.h" +#include "settingsdialogextensions.h" #include "settingsdialoggeneral.h" #include "settingsdialogmodlist.h" #include "settingsdialognexus.h" #include "settingsdialogpaths.h" -#include "settingsdialogplugins.h" #include "settingsdialogtheme.h" #include "settingsdialogworkarounds.h" #include "ui_settingsdialog.h" using namespace MOBase; -SettingsDialog::SettingsDialog(PluginContainer* pluginContainer, Settings& settings, - QWidget* parent) +SettingsDialog::SettingsDialog(ExtensionManager& extensionManager, + PluginManager& pluginManager, + ThemeManager const& themeManager, + TranslationManager const& translationManager, + Settings& settings, QWidget* parent) : TutorableDialog("SettingsDialog", parent), ui(new Ui::SettingsDialog), - m_settings(settings), m_exit(Exit::None), m_pluginContainer(pluginContainer) + m_settings(settings), m_exit(Exit::None) { ui->setupUi(this); - m_tabs.push_back( - std::unique_ptr(new GeneralSettingsTab(settings, *this))); - m_tabs.push_back(std::unique_ptr(new ThemeSettingsTab(settings, *this))); + m_tabs.push_back(std::unique_ptr( + new GeneralSettingsTab(settings, translationManager, *this))); + m_tabs.push_back(std::unique_ptr( + new ThemeSettingsTab(settings, themeManager, *this))); m_tabs.push_back( std::unique_ptr(new ModListSettingsTab(settings, *this))); m_tabs.push_back(std::unique_ptr(new PathsSettingsTab(settings, *this))); @@ -47,16 +51,11 @@ SettingsDialog::SettingsDialog(PluginContainer* pluginContainer, Settings& setti std::unique_ptr(new DiagnosticsSettingsTab(settings, *this))); m_tabs.push_back(std::unique_ptr(new NexusSettingsTab(settings, *this))); m_tabs.push_back(std::unique_ptr( - new PluginsSettingsTab(settings, m_pluginContainer, *this))); + new ExtensionsSettingsTab(settings, extensionManager, pluginManager, *this))); m_tabs.push_back( std::unique_ptr(new WorkaroundsSettingsTab(settings, *this))); } -PluginContainer* SettingsDialog::pluginContainer() -{ - return m_pluginContainer; -} - QWidget* SettingsDialog::parentWidgetForDialogs() { if (isVisible()) { diff --git a/src/settingsdialog.h b/src/settingsdialog.h index eca0b2662..da3555f3d 100644 --- a/src/settingsdialog.h +++ b/src/settingsdialog.h @@ -23,9 +23,13 @@ along with Mod Organizer. If not, see . #include "shared/util.h" #include "tutorabledialog.h" -class PluginContainer; +class PluginManager; +class ExtensionManager; class Settings; class SettingsDialog; +class ThemeManager; +class TranslationManager; + namespace Ui { class SettingsDialog; @@ -63,8 +67,11 @@ class SettingsDialog : public MOBase::TutorableDialog friend class SettingsTab; public: - explicit SettingsDialog(PluginContainer* pluginContainer, Settings& settings, - QWidget* parent = 0); + explicit SettingsDialog(ExtensionManager& extensionManager, + PluginManager& pluginManager, + ThemeManager const& themeManager, + TranslationManager const& translationManager, + Settings& settings, QWidget* parent = 0); ~SettingsDialog(); @@ -74,7 +81,6 @@ class SettingsDialog : public MOBase::TutorableDialog */ QString getColoredButtonStyleSheet() const; - PluginContainer* pluginContainer(); QWidget* parentWidgetForDialogs(); void setExitNeeded(ExitFlags e); @@ -90,7 +96,6 @@ public slots: Settings& m_settings; std::vector> m_tabs; ExitFlags m_exit; - PluginContainer* m_pluginContainer; }; #endif // SETTINGSDIALOG_H diff --git a/src/settingsdialog.ui b/src/settingsdialog.ui index 6011b1588..80c4fffee 100644 --- a/src/settingsdialog.ui +++ b/src/settingsdialog.ui @@ -7,7 +7,7 @@ 0 0 820 - 592 + 607 @@ -17,7 +17,7 @@ - 0 + 5 @@ -44,8 +44,8 @@ 0 0 - 761 - 550 + 766 + 611 @@ -1052,8 +1052,8 @@ If you disable this feature, MO will only display official DLCs this way. Please 0 0 - 761 - 515 + 766 + 548 @@ -1476,9 +1476,9 @@ If you disable this feature, MO will only display official DLCs this way. Please - + - Plugins + Extensions @@ -1532,18 +1532,12 @@ If you disable this feature, MO will only display official DLCs this way. Please 0 - - - - 1 - - - + - + @@ -1553,8 +1547,8 @@ If you disable this feature, MO will only display official DLCs this way. Please - - + + 0 @@ -1567,130 +1561,6 @@ If you disable this feature, MO will only display official DLCs this way. Please 0 - - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop - - - 6 - - - - - Author: - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop - - - - - - - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop - - - - - - - Version: - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop - - - - - - - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop - - - - - - - Description: - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop - - - - - - - - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop - - - true - - - - - - - Enabled - - - - - - - - - - 0 - - - false - - - false - - - false - - - false - - - 170 - - - - Key - - - - - Value - - - - - - - - No plugin found. - - - Qt::AlignCenter - - - @@ -1742,8 +1612,8 @@ If you disable this feature, MO will only display official DLCs this way. Please 0 0 - 778 - 475 + 780 + 489 @@ -2041,9 +1911,6 @@ p, li { white-space: pre-wrap; } - - Qt::Orientation::Horizontal - 40 @@ -2336,6 +2203,12 @@ programs you are intentionally running. QTableWidget
      colortable.h
      + + ExtensionListInfoWidget + QWidget +
      settingsdialogextensioninfo.h
      + 1 +
      languageBox @@ -2354,7 +2227,6 @@ programs you are intentionally running. browseOverwriteDirBtn managedGameDirEdit browseGameDirBtn - pluginSettingsList lockGUIBox diff --git a/src/settingsdialogextensioninfo.cpp b/src/settingsdialogextensioninfo.cpp new file mode 100644 index 000000000..96dd33d4a --- /dev/null +++ b/src/settingsdialogextensioninfo.cpp @@ -0,0 +1,189 @@ +#include "settingsdialogextensioninfo.h" + +#include "ui_settingsdialogextensioninfo.h" + +#include + +#include + +#include + +#include "extensionmanager.h" +#include "pluginmanager.h" +#include "settings.h" + +using namespace MOBase; + +ExtensionSettingWidget::ExtensionSettingWidget(Setting const& setting, + QVariant const& value, QWidget* parent) + : QWidget(parent), m_value(value) +{ + setLayout(new QVBoxLayout()); + + { + auto* titleLabel = new QLabel(setting.title()); + auto font = titleLabel->font(); + font.setBold(true); + titleLabel->setFont(font); + layout()->addWidget(titleLabel); + } + + if (!setting.description().isEmpty()) { + auto* descriptionLabel = new QLabel(setting.description()); + auto font = descriptionLabel->font(); + font.setItalic(true); + font.setPointSize(static_cast(font.pointSize() * 0.85)); + descriptionLabel->setFont(font); + layout()->addWidget(descriptionLabel); + } + + { + QWidget* valueWidget = nullptr; + switch (setting.defaultValue().typeId()) { + case QMetaType::Bool: { + auto* comboBox = new QComboBox(); + comboBox->addItems({tr("False"), tr("True")}); + comboBox->setCurrentIndex(value.toBool()); + valueWidget = comboBox; + } break; + case QMetaType::QString: { + auto* lineEdit = new QLineEdit(value.toString()); + valueWidget = lineEdit; + } break; + case QMetaType::Float: + case QMetaType::Double: { + auto* lineEdit = new QLineEdit(QString::number(value.toInt())); + lineEdit->setValidator(new QDoubleValidator(lineEdit)); + valueWidget = lineEdit; + } break; + case QMetaType::Int: + case QMetaType::LongLong: + case QMetaType::UInt: + case QMetaType::ULongLong: { + auto* lineEdit = new QLineEdit(QString::number(value.toInt())); + lineEdit->setValidator(new QIntValidator(lineEdit)); + valueWidget = lineEdit; + } break; + case QMetaType::QColor: { + valueWidget = new QWidget(this); + auto* layout = new QHBoxLayout(); + valueWidget->setLayout(layout); + + const auto color = m_value.value(); + auto* textWidget = new QLabel(color.name()); + auto* colorWidget = new QLabel(""); + colorWidget->setStyleSheet( + QString("QLabel { background-color: %1; }").arg(color.name())); + auto* button = new QPushButton(tr("Edit")); + connect(button, &QPushButton::clicked, [textWidget, colorWidget, this]() { + const auto newColor = QColorDialog::getColor(m_value.value()); + if (newColor.isValid()) { + m_value = newColor; + textWidget->setText(newColor.name()); + colorWidget->setStyleSheet( + QString("QLabel { background-color: %1; }").arg(newColor.name())); + } + }); + + layout->addWidget(textWidget); + layout->addWidget(colorWidget, 1); + layout->addWidget(button); + } break; + } + + if (valueWidget) { + layout()->addWidget(valueWidget); + } + } +} + +ExtensionListInfoWidget::ExtensionListInfoWidget(QWidget* parent) + : QWidget(parent), ui{new Ui::ExtensionListInfoWidget()} +{ + ui->setupUi(this); + + ui->authorLabel->setTextInteractionFlags(Qt::TextBrowserInteraction); + ui->authorLabel->setOpenExternalLinks(true); + + ui->pluginSettingsList->setRootIsDecorated(true); + ui->pluginSettingsList->setSelectionMode(QAbstractItemView::NoSelection); + ui->pluginSettingsList->setItemsExpandable(true); + ui->pluginSettingsList->setColumnCount(1); + ui->pluginSettingsList->header()->setSectionResizeMode( + 0, QHeaderView::ResizeMode::Stretch); +} + +void ExtensionListInfoWidget::setup(Settings& settings, + ExtensionManager& extensionManager, + PluginManager& pluginManager) +{ + m_settings = &settings; + m_extensionManager = &extensionManager; + m_pluginManager = &pluginManager; +} + +void ExtensionListInfoWidget::setExtension(const IExtension& extension) +{ + m_extension = &extension; + + // update the header for the extension + + const auto& metadata = m_extension->metadata(); + const auto& author = metadata.author(); + + if (author.homepage().isEmpty()) { + ui->authorLabel->setText(metadata.author().name()); + } else { + ui->authorLabel->setText(QString::fromStdString( + std::format("{}", author.homepage(), author.name()))); + } + ui->descriptionLabel->setText(metadata.description()); + ui->versionLabel->setText(metadata.version().string(Version::FormatCondensed)); + + if (metadata.type() == ExtensionType::THEME || + metadata.type() == ExtensionType::TRANSLATION) { + ui->enabledCheckbox->setChecked(true); + ui->enabledCheckbox->setEnabled(false); + ui->enabledCheckbox->setToolTip( + tr("Translation and theme extensions cannot be disabled.")); + } else { + ui->enabledCheckbox->setChecked(m_extensionManager->isEnabled(extension)); + ui->enabledCheckbox->setEnabled(true); + ui->enabledCheckbox->setToolTip(QString()); + } + + ui->pluginSettingsList->clear(); + + // update the list of settings + if (const auto* pluginExtension = dynamic_cast(m_extension)) { + + // TODO: refactor code somewhere to have direct access of the plugins for a given + // extension + for (auto& plugin : m_pluginManager->plugins()) { + if (&m_pluginManager->details(plugin).extension() != m_extension) { + continue; + } + + const auto settings = plugin->settings(); + if (settings.isEmpty()) { + continue; + } + + QTreeWidgetItem* pluginItem = new QTreeWidgetItem({plugin->localizedName()}); + ui->pluginSettingsList->addTopLevelItem(pluginItem); + + for (auto& setting : settings) { + auto* settingItem = new QTreeWidgetItem(); + auto* settingWidget = new ExtensionSettingWidget( + setting, m_settings->plugins().setting(plugin->name(), setting.name(), + setting.defaultValue())); + pluginItem->addChild(settingItem); + ui->pluginSettingsList->setItemWidget(settingItem, 0, settingWidget); + settingItem->setSizeHint(0, settingWidget->sizeHint()); + } + + pluginItem->setChildIndicatorPolicy(QTreeWidgetItem::ShowIndicator); + pluginItem->setExpanded(true); + } + } +} diff --git a/src/settingsdialogextensioninfo.h b/src/settingsdialogextensioninfo.h new file mode 100644 index 000000000..0a134a4fb --- /dev/null +++ b/src/settingsdialogextensioninfo.h @@ -0,0 +1,53 @@ +#ifndef SETTINGSDIALOGEXTENSIONINFO_H +#define SETTINGSDIALOGEXTENSIONINFO_H + +#include + +#include + +namespace Ui +{ +class ExtensionListInfoWidget; +} + +class ExtensionManager; +class PluginManager; +class Settings; + +class ExtensionSettingWidget : public QWidget +{ + Q_OBJECT +public: + ExtensionSettingWidget(MOBase::Setting const& setting, QVariant const& value, + QWidget* parent = nullptr); + +private: + QVariant m_value; +}; + +class ExtensionListInfoWidget : public QWidget +{ +public: + ExtensionListInfoWidget(QWidget* parent = nullptr); + + // setup the widget, should be called before any other functions + // + void setup(Settings& settings, ExtensionManager& extensionManager, + PluginManager& pluginManager); + + // set the extension to display + // + void setExtension(const MOBase::IExtension& extension); + +private: + Ui::ExtensionListInfoWidget* ui; + + Settings* m_settings; + ExtensionManager* m_extensionManager; + PluginManager* m_pluginManager; + + // currently displayed extension (default to nullptr) + const MOBase::IExtension* m_extension{nullptr}; +}; + +#endif diff --git a/src/settingsdialogextensioninfo.ui b/src/settingsdialogextensioninfo.ui new file mode 100644 index 000000000..ab479aa7e --- /dev/null +++ b/src/settingsdialogextensioninfo.ui @@ -0,0 +1,124 @@ + + + ExtensionListInfoWidget + + + + 0 + 0 + 400 + 410 + + + + Form + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + 6 + + + + + Author: + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + + + + + Version: + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + + + + + Description: + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + true + + + + + + + Enabled + + + + + + + + + + 0 + + + true + + + 1 + + + false + + + + Key + + + + + + + + + diff --git a/src/settingsdialogextensionrow.cpp b/src/settingsdialogextensionrow.cpp new file mode 100644 index 000000000..5a5b4e8e4 --- /dev/null +++ b/src/settingsdialogextensionrow.cpp @@ -0,0 +1,49 @@ +#include "settingsdialogextensionrow.h" + +#include "ui_settingsdialogextensionrow.h" + +using namespace MOBase; + +namespace +{ + +const auto& defaultIcon() +{ + static QIcon icon; + + if (icon.isNull()) { + const QImage baseIcon(":/MO/gui/app_icon"); + QImage grayIcon = baseIcon.convertToFormat(QImage::Format_ARGB32); + { + for (int y = 0; y < grayIcon.height(); ++y) { + QRgb* scanLine = (QRgb*)grayIcon.scanLine(y); + for (int x = 0; x < grayIcon.width(); ++x) { + QRgb pixel = *scanLine; + uint ci = uint(qGray(pixel)); + *scanLine = qRgba(ci, ci, ci, qAlpha(pixel) / 3); + ++scanLine; + } + } + } + icon = QIcon(QPixmap::fromImage(grayIcon)); + } + + return icon; +} + +} // namespace + +ExtensionListItemWidget::ExtensionListItemWidget(const IExtension& extension) + : ui{new Ui::ExtensionListItemWidget()}, m_extension{&extension} +{ + ui->setupUi(this); + + const auto& metadata = extension.metadata(); + const auto& icon = metadata.icon().isNull() ? defaultIcon() : metadata.icon(); + + ui->extensionIcon->setPixmap(icon.pixmap(QSize(48, 48))); + ui->extensionName->setText(extension.metadata().name()); + + ui->extensionDescription->setText(extension.metadata().description()); + ui->extensionAuthor->setText(extension.metadata().author().name()); +} diff --git a/src/settingsdialogextensionrow.h b/src/settingsdialogextensionrow.h new file mode 100644 index 000000000..ce65278d1 --- /dev/null +++ b/src/settingsdialogextensionrow.h @@ -0,0 +1,27 @@ +#ifndef SETTINGSDIALOGEXTENSIONROW_H +#define SETTINGSDIALOGEXTENSIONROW_H + +#include + +#include + +namespace Ui +{ +class ExtensionListItemWidget; +} + +class ExtensionListItemWidget : public QWidget +{ +public: + ExtensionListItemWidget(MOBase::IExtension const& extension); + + // retrieve the extension associated with this widget + // + const auto& extension() const { return *m_extension; } + +private: + Ui::ExtensionListItemWidget* ui; + const MOBase::IExtension* m_extension; +}; + +#endif diff --git a/src/settingsdialogextensionrow.ui b/src/settingsdialogextensionrow.ui new file mode 100644 index 000000000..bb9986a17 --- /dev/null +++ b/src/settingsdialogextensionrow.ui @@ -0,0 +1,123 @@ + + + ExtensionListItemWidget + + + + 0 + 0 + 250 + 60 + + + + Form + + + + 2 + + + 2 + + + 2 + + + 2 + + + + + + 0 + 0 + + + + + 48 + 48 + + + + + 45 + 48 + + + + TextLabel + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 5 + 20 + + + + + + + + 2 + + + + + + 10 + true + + + + TextLabel + + + + + + + + 8 + true + + + + TextLabel + + + + + + + + + + 8 + + + + TextLabel + + + + + + + + + + + + diff --git a/src/settingsdialogextensions.cpp b/src/settingsdialogextensions.cpp new file mode 100644 index 000000000..813f39917 --- /dev/null +++ b/src/settingsdialogextensions.cpp @@ -0,0 +1,308 @@ +#include "settingsdialogextensions.h" +#include "noeditdelegate.h" +#include "ui_settingsdialog.h" +#include + +#include "organizercore.h" +#include "pluginmanager.h" + +#include "settingsdialogextensionrow.h" + +using namespace MOBase; + +ExtensionsSettingsTab::ExtensionsSettingsTab(Settings& s, + ExtensionManager& extensionManager, + PluginManager& pluginManager, + SettingsDialog& d) + : SettingsTab(s, d), m_extensionManager(&extensionManager), + m_pluginManager(&pluginManager) +{ + ui->infoWidget->setup(s, extensionManager, pluginManager); + + // TODO: use Qt system to sort extensions instead of sorting beforehand + std::vector extensions; + for (auto& extension : m_extensionManager->extensions()) { + extensions.push_back(extension.get()); + } + std::sort(extensions.begin(), extensions.end(), [](auto* lhs, auto* rhs) { + return lhs->metadata().name().compare(rhs->metadata().name(), Qt::CaseInsensitive) < + 0; + }); + + ui->extensionsList->setSortingEnabled(false); + + for (const auto* extension : extensions) { + auto* item = new QListWidgetItem(); + auto* widget = new ExtensionListItemWidget(*extension); + item->setSizeHint(widget->sizeHint()); + ui->extensionsList->addItem(item); + ui->extensionsList->setItemWidget(item, widget); + } + + QObject::connect(ui->extensionsList, &QListWidget::currentItemChanged, + [this](QListWidgetItem* current, QListWidgetItem*) { + if (auto* widget = dynamic_cast( + ui->extensionsList->itemWidget(current))) { + extensionSelected(widget->extension()); + } + }); + + // ui->pluginSettingsList->setStyleSheet("QTreeWidget::item {padding-right: 10px;}"); + // ui->pluginsList->setHeaderHidden(true); + + //// display plugin settings + // std::map, PluginExtensionComparator> + // pluginsPerExtension; + + // for (IPlugin* plugin : pluginManager.plugins()) { + // pluginsPerExtension[&pluginManager.details(plugin).extension()].push_back(plugin); + // } + + // for (auto& [extension, plugins] : pluginsPerExtension) { + + // QTreeWidgetItem* extensionItem = new QTreeWidgetItem(); + // extensionItem->setData(0, Qt::DisplayRole, extension->metadata().name()); + // ui->pluginsList->addTopLevelItem(extensionItem); + + // for (auto* plugin : plugins) { + + // // only show master + // if (pluginManager.details(plugin).master() != plugin) { + // continue; + // } + + // QTreeWidgetItem* pluginItem = new QTreeWidgetItem(extensionItem); + // pluginItem->setData(0, Qt::DisplayRole, plugin->localizedName()); + // } + //} + + // ui->pluginsList->sortByColumn(0, Qt::AscendingOrder); + + //// display plugin blacklist + // for (const QString& pluginName : settings().plugins().blacklist()) { + // ui->pluginBlacklist->addItem(pluginName); + // } + + // m_filter.setEdit(ui->pluginFilterEdit); + + // QShortcut* delShortcut = + // new QShortcut(QKeySequence(Qt::Key_Delete), ui->pluginBlacklist); + // QObject::connect(delShortcut, &QShortcut::activated, &dialog(), [&] { + // deleteBlacklistItem(); + // }); + // 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); +// +// bool inactive = !m_pluginManager->implementInterface(plugin) && +// !m_pluginManager->isEnabled(plugin); +// +// 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); +// } +// } +// } +//} +// +// void PluginsSettingsTab::filterPluginList() +//{ +// auto selectedItems = ui->pluginsList->selectedItems(); +// QTreeWidgetItem* firstNotHidden = nullptr; +// +// for (auto i = 0; i < ui->pluginsList->topLevelItemCount(); ++i) { +// auto* topLevelItem = ui->pluginsList->topLevelItem(i); +// +// bool found = false; +// for (auto j = 0; j < topLevelItem->childCount(); ++j) { +// auto* item = topLevelItem->child(j); +// auto* plugin = this->plugin(item); +// +// // 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(); +// }); +// +// if (match) { +// found = true; +// item->setHidden(false); +// +// if (firstNotHidden == nullptr) { +// firstNotHidden = item; +// } +// } else { +// item->setHidden(true); +// } +// } +// +// topLevelItem->setHidden(!found); +// } +// +// // Unselect item if hidden: +// 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); +// } +//} +// +// IPlugin* PluginsSettingsTab::plugin(QTreeWidgetItem* pluginItem) const +//{ +// return static_cast(qvariant_cast(pluginItem->data(0, PluginRole))); +//} + +void ExtensionsSettingsTab::update() +{ + // transfer plugin settings to in-memory structure + // for (int i = 0; i < ui->pluginsList->topLevelItemCount(); ++i) { + // auto* topLevelItem = ui->pluginsList->topLevelItem(i); + // for (int j = 0; j < topLevelItem->childCount(); ++j) { + // auto* item = topLevelItem->child(j); + // settings().plugins().setSettings(plugin(item)->name(), + // item->data(0, SettingsRole).toMap()); + // } + //} + + // set plugin blacklist + QStringList names; + for (QListWidgetItem* item : ui->pluginBlacklist->findItems("*", Qt::MatchWildcard)) { + names.push_back(item->text()); + } + + settings().plugins().setBlacklist(names); + + settings().plugins().save(); +} + +void ExtensionsSettingsTab::closing() +{ + // storeSettings(ui->pluginsList->currentItem()); +} + +void ExtensionsSettingsTab::extensionSelected(IExtension const& extension) +{ + // TODO: store current settings in-memory for save later OR save live when modifying? + if (m_currentExtension) { + } + + m_currentExtension = &extension; + ui->infoWidget->setExtension(extension); +} + +// +// void PluginsSettingsTab::on_pluginsList_currentItemChanged(QTreeWidgetItem* current, +// QTreeWidgetItem* previous) +//{ +// storeSettings(previous); +// +// if (!current->data(0, PluginRole).isValid()) { +// return; +// } +// +// ui->pluginSettingsList->clear(); +// IPlugin* plugin = this->plugin(current); +// // ui->authorLabel->setText(plugin->author()); +// // ui->versionLabel->setText(plugin->version().canonicalString()); +// // ui->descriptionLabel->setText(plugin->description()); +// +// //// Checkbox, do not show for children or game plugins, disable +// //// if the plugin cannot be enabled. +// // ui->enabledCheckbox->setVisible( +// // !m_pluginManager->implementInterface(plugin) && +// // plugin->master().isEmpty()); +// +// bool enabled = m_pluginManager->isEnabled(plugin); +// auto& requirements = m_pluginManager->details(plugin); +// auto problems = requirements.problems(); +// +// // Plugin is enable or can be enabled. +// 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, SettingsRole).toMap(); +// QVariantMap descriptions = current->data(0, DescriptionsRole).toMap(); +// ui->pluginSettingsList->setEnabled(settings.count() != 0); +// for (auto iter = settings.begin(); iter != settings.end(); ++iter) { +// QTreeWidgetItem* newItem = new QTreeWidgetItem(QStringList(iter.key())); +// QVariant value = *iter; +// QString description; +// { +// auto descriptionIter = descriptions.find(iter.key()); +// if (descriptionIter != descriptions.end()) { +// description = descriptionIter->toString(); +// } +// } +// +// ui->pluginSettingsList->setItemDelegateForColumn(0, new NoEditDelegate()); +// newItem->setData(1, Qt::DisplayRole, value); +// newItem->setData(1, Qt::EditRole, value); +// newItem->setToolTip(1, description); +// +// newItem->setFlags(newItem->flags() | Qt::ItemIsEditable); +// ui->pluginSettingsList->addTopLevelItem(newItem); +// } +// +// ui->pluginSettingsList->resizeColumnToContents(0); +// ui->pluginSettingsList->resizeColumnToContents(1); +//} +// +// void PluginsSettingsTab::deleteBlacklistItem() +//{ +// ui->pluginBlacklist->takeItem(ui->pluginBlacklist->currentIndex().row()); +//} +// +// void PluginsSettingsTab::storeSettings(QTreeWidgetItem* pluginItem) +//{ +// if (pluginItem != nullptr && pluginItem->data(0, PluginRole).isValid()) { +// QVariantMap settings = pluginItem->data(0, SettingsRole).toMap(); +// +// for (int i = 0; i < ui->pluginSettingsList->topLevelItemCount(); ++i) { +// const QTreeWidgetItem* item = ui->pluginSettingsList->topLevelItem(i); +// settings[item->text(0)] = item->data(1, Qt::DisplayRole); +// } +// +// pluginItem->setData(0, SettingsRole, settings); +// } +//} diff --git a/src/settingsdialogextensions.h b/src/settingsdialogextensions.h new file mode 100644 index 000000000..70760d0b2 --- /dev/null +++ b/src/settingsdialogextensions.h @@ -0,0 +1,65 @@ +#ifndef SETTINGSDIALOGEXTENSIONS_H +#define SETTINGSDIALOGEXTENSIONS_H + +#include "filterwidget.h" + +#include "settings.h" +#include "settingsdialog.h" + +#include "extensionmanager.h" +#include "pluginmanager.h" + +class ExtensionsSettingsTab : public SettingsTab +{ +public: + ExtensionsSettingsTab(Settings& settings, ExtensionManager& extensionManager, + PluginManager& pluginManager, SettingsDialog& dialog); + + void update(); + void closing() override; + + // private: + // void on_pluginsList_currentItemChanged(QListWidgetItem* current, + // QListWidgetItem* previous); + // void deleteBlacklistItem(); + // void storeSettings(QListWidgetItem* pluginItem); + +private slots: + + ///** + // * @brief Update the list item to display inactive plugins. + // */ + // void updateListItems(); + + ///** + // * @brief Filter the plugin list according to the filter widget. + // * + // */ + // void filterPluginList(); + + ///** + // * @brief Retrieve the plugin associated to the given item in the list. + // * + // */ + // MOBase::IPlugin* plugin(QListWidgetItem* pluginItem) const; + + void extensionSelected(MOBase::IExtension const& extension); + + enum + { + PluginRole = Qt::UserRole, + SettingsRole = Qt::UserRole + 1, + DescriptionsRole = Qt::UserRole + 2 + }; + +private: + ExtensionManager* m_extensionManager; + PluginManager* m_pluginManager; + + // the currently selected extension + const MOBase::IExtension* m_currentExtension{nullptr}; + + MOBase::FilterWidget m_filter; +}; + +#endif // SETTINGSDIALOGPLUGINS_H diff --git a/src/settingsdialoggeneral.cpp b/src/settingsdialoggeneral.cpp index 265c9db90..108c4792a 100644 --- a/src/settingsdialoggeneral.cpp +++ b/src/settingsdialoggeneral.cpp @@ -8,11 +8,13 @@ using namespace MOBase; -GeneralSettingsTab::GeneralSettingsTab(Settings& s, SettingsDialog& d) +GeneralSettingsTab::GeneralSettingsTab(Settings& s, + TranslationManager const& translationManager, + SettingsDialog& d) : SettingsTab(s, d) { // language - addLanguages(); + addLanguages(translationManager); selectLanguage(); // download list @@ -84,54 +86,18 @@ void GeneralSettingsTab::update() ui->doubleClickPreviews->isChecked()); } -void GeneralSettingsTab::addLanguages() +void GeneralSettingsTab::addLanguages(TranslationManager const& manager) { - // matches the end of filenames for something like "_en.qm" or "_zh_CN.qm" - const QString pattern = QString::fromStdWString(AppConfig::translationPrefix()) + - "_([a-z]{2,3}(_[A-Z]{2,2})?).qm"; + auto translations = manager.translations(); - const QRegularExpression exp(QRegularExpression::anchoredPattern(pattern)); - - QDirIterator iter(QCoreApplication::applicationDirPath() + "/translations", - QDir::Files); - - std::vector> languages; - - while (iter.hasNext()) { - iter.next(); - - const QString file = iter.fileName(); - auto match = exp.match(file); - if (!match.hasMatch()) { - continue; - } - - const QString languageCode = match.captured(1); - const QLocale locale(languageCode); - - QString languageString = QString("%1 (%2)") - .arg(locale.nativeLanguageName()) - .arg(locale.nativeTerritoryName()); - - if (locale.language() == QLocale::Chinese) { - if (languageCode == "zh_TW") { - languageString = "Chinese (Traditional)"; - } else { - languageString = "Chinese (Simplified)"; - } - } - - languages.push_back({languageString, match.captured(1)}); - } - - if (!ui->languageBox->findText("English")) { - languages.push_back({QString("English"), QString("en_US")}); - } - - std::sort(languages.begin(), languages.end()); + std::sort(translations.begin(), translations.end(), [](auto&& lhs, auto&& rhs) { + return std::forward_as_tuple(lhs->language(), lhs->identifier()) < + std::forward_as_tuple(rhs->language(), rhs->identifier()); + }); - for (const auto& lang : languages) { - ui->languageBox->addItem(lang.first, lang.second); + for (const auto& translation : translations) { + ui->languageBox->addItem(ToQString(translation->language()), + ToQString(translation->identifier())); } } diff --git a/src/settingsdialoggeneral.h b/src/settingsdialoggeneral.h index 6c1ee8670..ec1744b43 100644 --- a/src/settingsdialoggeneral.h +++ b/src/settingsdialoggeneral.h @@ -1,19 +1,20 @@ #ifndef SETTINGSDIALOGGENERAL_H #define SETTINGSDIALOGGENERAL_H -#include "plugincontainer.h" #include "settings.h" #include "settingsdialog.h" +#include "translationmanager.h" class GeneralSettingsTab : public SettingsTab { public: - GeneralSettingsTab(Settings& settings, SettingsDialog& dialog); + GeneralSettingsTab(Settings& settings, TranslationManager const& translationManager, + SettingsDialog& dialog); void update(); private: - void addLanguages(); + void addLanguages(TranslationManager const& translationManager); void selectLanguage(); void resetDialogs(); diff --git a/src/settingsdialogplugins.cpp b/src/settingsdialogplugins.cpp deleted file mode 100644 index db00ca081..000000000 --- a/src/settingsdialogplugins.cpp +++ /dev/null @@ -1,380 +0,0 @@ -#include "settingsdialogplugins.h" -#include "noeditdelegate.h" -#include "ui_settingsdialog.h" -#include - -#include "disableproxyplugindialog.h" -#include "organizercore.h" -#include "plugincontainer.h" - -using namespace MOBase; - -PluginsSettingsTab::PluginsSettingsTab(Settings& s, PluginContainer* pluginContainer, - SettingsDialog& d) - : SettingsTab(s, d), m_pluginContainer(pluginContainer) -{ - ui->pluginSettingsList->setStyleSheet("QTreeWidget::item {padding-right: 10px;}"); - - // Create top-level tree widget: - QStringList pluginInterfaces = m_pluginContainer->pluginInterfaces(); - pluginInterfaces.sort(Qt::CaseInsensitive); - std::map topItems; - for (QString interfaceName : pluginInterfaces) { - auto* item = new QTreeWidgetItem(ui->pluginsList, {interfaceName}); - item->setFlags(item->flags() & ~Qt::ItemIsSelectable); - auto font = item->font(0); - font.setBold(true); - item->setFont(0, font); - topItems[interfaceName] = item; - item->setExpanded(true); - item->setFlags(item->flags() & ~Qt::ItemIsSelectable); - } - ui->pluginsList->setHeaderHidden(true); - - // display plugin settings - QSet handledNames; - for (IPlugin* plugin : settings().plugins().plugins()) { - if (handledNames.contains(plugin->name()) || - m_pluginContainer->requirements(plugin).master()) { - continue; - } - - QTreeWidgetItem* listItem = new QTreeWidgetItem( - topItems.at(m_pluginContainer->topImplementedInterface(plugin))); - listItem->setData(0, Qt::DisplayRole, plugin->localizedName()); - listItem->setData(0, PluginRole, QVariant::fromValue((void*)plugin)); - listItem->setData(0, SettingsRole, settings().plugins().settings(plugin->name())); - listItem->setData(0, DescriptionsRole, - 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, PluginRole, QVariant::fromValue((void*)child)); - childItem->setData(0, SettingsRole, settings().plugins().settings(child->name())); - childItem->setData(0, DescriptionsRole, - settings().plugins().descriptions(child->name())); - - handledNames.insert(child->name()); - } - - handledNames.insert(plugin->name()); - } - - for (auto& [k, item] : topItems) { - if (item->childCount() == 0) { - item->setHidden(true); - } - } - - ui->pluginsList->sortByColumn(0, Qt::AscendingOrder); - - // display plugin blacklist - for (const QString& pluginName : settings().plugins().blacklist()) { - ui->pluginBlacklist->addItem(pluginName); - } - - m_filter.setEdit(ui->pluginFilterEdit); - - 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, &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); - - bool inactive = !m_pluginContainer->implementInterface(plugin) && - !m_pluginContainer->isEnabled(plugin); - - 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); - } - } - } -} - -void PluginsSettingsTab::filterPluginList() -{ - auto selectedItems = ui->pluginsList->selectedItems(); - QTreeWidgetItem* firstNotHidden = nullptr; - - for (auto i = 0; i < ui->pluginsList->topLevelItemCount(); ++i) { - auto* topLevelItem = ui->pluginsList->topLevelItem(i); - - bool found = false; - for (auto j = 0; j < topLevelItem->childCount(); ++j) { - auto* item = topLevelItem->child(j); - auto* plugin = this->plugin(item); - - // 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()) { - match = match || m_filter.matches([child](const QRegularExpression& regex) { - return regex.match(child->localizedName()).hasMatch(); - }); - } - - if (match) { - found = true; - item->setHidden(false); - - if (firstNotHidden == nullptr) { - firstNotHidden = item; - } - } else { - item->setHidden(true); - } - } - - topLevelItem->setHidden(!found); - } - - // Unselect item if hidden: - 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); - } -} - -IPlugin* PluginsSettingsTab::plugin(QTreeWidgetItem* pluginItem) const -{ - return static_cast(qvariant_cast(pluginItem->data(0, PluginRole))); -} - -void PluginsSettingsTab::update() -{ - // transfer plugin settings to in-memory structure - for (int i = 0; i < ui->pluginsList->topLevelItemCount(); ++i) { - auto* topLevelItem = ui->pluginsList->topLevelItem(i); - for (int j = 0; j < topLevelItem->childCount(); ++j) { - auto* item = topLevelItem->child(j); - settings().plugins().setSettings(plugin(item)->name(), - item->data(0, SettingsRole).toMap()); - } - } - - // set plugin blacklist - QStringList names; - for (QListWidgetItem* item : ui->pluginBlacklist->findItems("*", Qt::MatchWildcard)) { - names.push_back(item->text()); - } - - settings().plugins().setBlacklist(names); - - settings().plugins().save(); -} - -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, PluginRole).isValid()) { - return; - } - IPlugin* plugin = this->plugin(item); - const auto& requirements = m_pluginContainer->requirements(plugin); - - // User wants to enable: - if (checked) { - m_pluginContainer->setEnabled(plugin, true, false); - } 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(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; - } - - // Check the proxied plugins: - auto proxied = requirements.proxied(); - 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: - auto requiredFor = requirements.requiredFor(); - if (!requiredFor.empty()) { - QStringList pluginNames; - for (auto& p : requiredFor) { - pluginNames.append(p->localizedName()); - } - pluginNames.sort(); - QString message = - QObject::tr("

      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) { - ui->enabledCheckbox->setChecked(true); - return; - } - } - m_pluginContainer->setEnabled(plugin, false, true); - } - - // Proxy was disabled / enabled, need restart: - if (m_pluginContainer->implementInterface(plugin)) { - dialog().setExitNeeded(Exit::Restart); - } - - updateListItems(); -} - -void PluginsSettingsTab::on_pluginsList_currentItemChanged(QTreeWidgetItem* current, - QTreeWidgetItem* previous) -{ - storeSettings(previous); - - if (!current->data(0, PluginRole).isValid()) { - return; - } - - ui->pluginSettingsList->clear(); - IPlugin* plugin = this->plugin(current); - ui->authorLabel->setText(plugin->author()); - ui->versionLabel->setText(plugin->version().canonicalString()); - ui->descriptionLabel->setText(plugin->description()); - - // 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.")); - } - // 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, SettingsRole).toMap(); - QVariantMap descriptions = current->data(0, DescriptionsRole).toMap(); - ui->pluginSettingsList->setEnabled(settings.count() != 0); - for (auto iter = settings.begin(); iter != settings.end(); ++iter) { - QTreeWidgetItem* newItem = new QTreeWidgetItem(QStringList(iter.key())); - QVariant value = *iter; - QString description; - { - auto descriptionIter = descriptions.find(iter.key()); - if (descriptionIter != descriptions.end()) { - description = descriptionIter->toString(); - } - } - - ui->pluginSettingsList->setItemDelegateForColumn(0, new NoEditDelegate()); - newItem->setData(1, Qt::DisplayRole, value); - newItem->setData(1, Qt::EditRole, value); - newItem->setToolTip(1, description); - - newItem->setFlags(newItem->flags() | Qt::ItemIsEditable); - ui->pluginSettingsList->addTopLevelItem(newItem); - } - - ui->pluginSettingsList->resizeColumnToContents(0); - ui->pluginSettingsList->resizeColumnToContents(1); -} - -void PluginsSettingsTab::deleteBlacklistItem() -{ - ui->pluginBlacklist->takeItem(ui->pluginBlacklist->currentIndex().row()); -} - -void PluginsSettingsTab::storeSettings(QTreeWidgetItem* pluginItem) -{ - if (pluginItem != nullptr && pluginItem->data(0, PluginRole).isValid()) { - QVariantMap settings = pluginItem->data(0, SettingsRole).toMap(); - - for (int i = 0; i < ui->pluginSettingsList->topLevelItemCount(); ++i) { - const QTreeWidgetItem* item = ui->pluginSettingsList->topLevelItem(i); - settings[item->text(0)] = item->data(1, Qt::DisplayRole); - } - - pluginItem->setData(0, SettingsRole, settings); - } -} diff --git a/src/settingsdialogplugins.h b/src/settingsdialogplugins.h deleted file mode 100644 index 416f457ae..000000000 --- a/src/settingsdialogplugins.h +++ /dev/null @@ -1,57 +0,0 @@ -#ifndef SETTINGSDIALOGPLUGINS_H -#define SETTINGSDIALOGPLUGINS_H - -#include "filterwidget.h" - -#include "settings.h" -#include "settingsdialog.h" - -class PluginsSettingsTab : public SettingsTab -{ -public: - PluginsSettingsTab(Settings& settings, PluginContainer* pluginContainer, - SettingsDialog& dialog); - - void update(); - void closing() override; - -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. - * - */ - void filterPluginList(); - - /** - * @brief Retrieve the plugin associated to the given item in the list. - * - */ - MOBase::IPlugin* plugin(QTreeWidgetItem* pluginItem) const; - - enum - { - PluginRole = Qt::UserRole, - SettingsRole = Qt::UserRole + 1, - DescriptionsRole = Qt::UserRole + 2 - }; - -private: - PluginContainer* m_pluginContainer; - - MOBase::FilterWidget m_filter; -}; - -#endif // SETTINGSDIALOGPLUGINS_H diff --git a/src/settingsdialogtheme.cpp b/src/settingsdialogtheme.cpp index 75bcdb598..49dd72668 100644 --- a/src/settingsdialogtheme.cpp +++ b/src/settingsdialogtheme.cpp @@ -9,10 +9,12 @@ using namespace MOBase; -ThemeSettingsTab::ThemeSettingsTab(Settings& s, SettingsDialog& d) : SettingsTab(s, d) +ThemeSettingsTab::ThemeSettingsTab(Settings& s, ThemeManager const& manager, + SettingsDialog& d) + : SettingsTab(s, d) { // style - addStyles(); + addStyles(manager); selectStyle(); // colors @@ -21,61 +23,56 @@ ThemeSettingsTab::ThemeSettingsTab(Settings& s, SettingsDialog& d) : SettingsTab QObject::connect(ui->resetColorsBtn, &QPushButton::clicked, [&] { ui->colorTable->resetColors(); }); - - QObject::connect(ui->exploreStyles, &QPushButton::clicked, [&] { - onExploreStyles(); - }); } void ThemeSettingsTab::update() { // style - const QString oldStyle = settings().interface().styleName().value_or(""); + const QString oldStyle = settings().interface().themeName().value_or(""); const QString newStyle = ui->styleBox->itemData(ui->styleBox->currentIndex()).toString(); if (oldStyle != newStyle) { - settings().interface().setStyleName(newStyle); - emit settings().styleChanged(newStyle); + settings().interface().setThemeName(newStyle); + emit settings().themeChanged(newStyle); } // colors ui->colorTable->commitColors(); } -void ThemeSettingsTab::addStyles() +void ThemeSettingsTab::addStyles(ThemeManager const& manager) { ui->styleBox->addItem("None", ""); - for (auto&& key : QStyleFactory::keys()) { - ui->styleBox->addItem(key, key); - } - ui->styleBox->insertSeparator(ui->styleBox->count()); + auto themes = manager.themes(); - QDirIterator iter(QCoreApplication::applicationDirPath() + "/" + - QString::fromStdWString(AppConfig::stylesheetsPath()), - QStringList("*.qss"), QDir::Files); + std::sort(themes.begin(), themes.end(), [&manager](auto&& lhs, auto&& rhs) { + if (manager.isBuiltIn(lhs) == manager.isBuiltIn(rhs)) { + return lhs->name() < rhs->name(); + } else { + // put built-in before others + return manager.isBuiltIn(rhs) < manager.isBuiltIn(lhs); + } + }); - while (iter.hasNext()) { - iter.next(); + bool separator = true; + for (auto&& theme : themes) { + if (separator && !manager.isBuiltIn(theme)) { + ui->styleBox->insertSeparator(ui->styleBox->count()); + separator = false; + } - ui->styleBox->addItem(iter.fileInfo().completeBaseName(), iter.fileName()); + ui->styleBox->addItem(ToQString(theme->name()), ToQString(theme->identifier())); } } void ThemeSettingsTab::selectStyle() { const int currentID = - ui->styleBox->findData(settings().interface().styleName().value_or("")); + ui->styleBox->findData(settings().interface().themeName().value_or("")); if (currentID != -1) { ui->styleBox->setCurrentIndex(currentID); } } - -void ThemeSettingsTab::onExploreStyles() -{ - QString ssPath = QCoreApplication::applicationDirPath() + "/" + - ToQString(AppConfig::stylesheetsPath()); - shell::Explore(ssPath); -} diff --git a/src/settingsdialogtheme.h b/src/settingsdialogtheme.h index 2a53dc38c..2ca5c9458 100644 --- a/src/settingsdialogtheme.h +++ b/src/settingsdialogtheme.h @@ -5,18 +5,19 @@ #include "settings.h" #include "settingsdialog.h" +#include "thememanager.h" class ThemeSettingsTab : public SettingsTab { public: - ThemeSettingsTab(Settings& settings, SettingsDialog& dialog); + ThemeSettingsTab(Settings& settings, ThemeManager const& manager, + SettingsDialog& dialog); void update() override; private: - void addStyles(); + void addStyles(ThemeManager const& manager); void selectStyle(); - void onExploreStyles(); }; #endif // SETTINGSDIALOGGENERAL_H diff --git a/src/settingsutilities.h b/src/settingsutilities.h index cd55464e5..a5bd9c068 100644 --- a/src/settingsutilities.h +++ b/src/settingsutilities.h @@ -1,43 +1,16 @@ #ifndef SETTINGSUTILITIES_H #define SETTINGSUTILITIES_H -#include +#include + +#include +#include namespace MOBase { class ExpanderWidget; } -template -struct ValueConverter -{ - static const T& convert(const T& t) { return t; } -}; - -template -struct ValueConverter>> -{ - static QString convert(const T& t) - { - return QString("%1").arg(static_cast>(t)); - } -}; - -template <> -struct ValueConverter -{ - static QString convert(const QVariantList& t) - { - return QString("%1").arg(QVariant(t).toStringList().join(",")); - } -}; - -template <> -struct ValueConverter -{ - static QString convert(const QStringList& t) { return t.join(", "); } -}; - bool shouldLogSetting(const QString& displayName); template @@ -47,13 +20,11 @@ void logChange(const QString& displayName, std::optional oldValue, const T& n return; } - using VC = ValueConverter; - if (oldValue) { - MOBase::log::debug("setting '{}' changed from '{}' to '{}'", displayName, - VC::convert(*oldValue), VC::convert(newValue)); + MOBase::log::debug("setting '{}' changed from '{}' to '{}'", displayName, *oldValue, + newValue); } else { - MOBase::log::debug("setting '{}' set to '{}'", displayName, VC::convert(newValue)); + MOBase::log::debug("setting '{}' set to '{}'", displayName, newValue); } } diff --git a/src/texteditor.cpp b/src/texteditor.cpp index 281a545d7..b618d8c26 100644 --- a/src/texteditor.cpp +++ b/src/texteditor.cpp @@ -464,7 +464,7 @@ TextEditorToolbar::TextEditorToolbar(TextEditor& editor) m_save = new QAction(QIcon(":/MO/gui/save"), QObject::tr("&Save"), &editor); m_save->setShortcutContext(Qt::WidgetWithChildrenShortcut); - m_save->setShortcut(Qt::CTRL + Qt::Key_S); + m_save->setShortcut(Qt::CTRL | Qt::Key_S); m_editor.addAction(m_save); m_wordWrap = diff --git a/src/thememanager.cpp b/src/thememanager.cpp new file mode 100644 index 000000000..765f736ae --- /dev/null +++ b/src/thememanager.cpp @@ -0,0 +1,431 @@ +#include "thememanager.h" + +#include +#include +#include +#include + +#include +#include + +#include "shared/appconfig.h" + +using namespace MOBase; + +// style proxy that changes the appearance of drop indicators +// +class ProxyStyle : public QProxyStyle +{ +public: + ProxyStyle(QStyle* baseStyle = 0) : QProxyStyle(baseStyle) {} + + void drawPrimitive(PrimitiveElement element, const QStyleOption* option, + QPainter* painter, const QWidget* widget) const override + { + if (element == QStyle::PE_IndicatorItemViewItemDrop) { + + // 0. Fix a bug that made the drop indicator sometimes appear on top + // of the mod list when selecting a mod. + if (option->rect.height() == 0 && option->rect.bottomRight() == QPoint(-1, -1)) { + return; + } + + // 1. full-width drop indicator + QRect rect(option->rect); + if (auto* view = qobject_cast(widget)) { + rect.setLeft(view->indentation()); + rect.setRight(widget->width()); + } + + // 2. stylish drop indicator + painter->setRenderHint(QPainter::Antialiasing, true); + + QColor col(option->palette.windowText().color()); + QPen pen(col); + pen.setWidth(2); + col.setAlpha(50); + + painter->setPen(pen); + painter->setBrush(QBrush(col)); + if (rect.height() == 0) { + QPoint tri[3] = {rect.topLeft(), rect.topLeft() + QPoint(-5, 5), + rect.topLeft() + QPoint(-5, -5)}; + painter->drawPolygon(tri, 3); + painter->drawLine(rect.topLeft(), rect.topRight()); + } else { + painter->drawRoundedRect(rect, 5, 5); + } + } else { + QProxyStyle::drawPrimitive(element, option, painter, widget); + } + } +}; + +namespace +{ +QString readWholeFile(std::filesystem::path const& path) +{ + QFile file(path); + if (!file.open(QFile::ReadOnly | QFile::Text)) { + return {}; + } + + return QTextStream(&file).readAll(); +} + +QStringList extractTopStyleSheetComments(QString const& stylesheet) +{ + QTextStream stream(stylesheet.toUtf8()); + QStringList topComments; + + while (true) { + const auto byteLine = stream.readLine(); + if (byteLine.isNull()) { + break; + } + + const auto line = QString(byteLine).trimmed(); + + // skip empty lines + if (line.isEmpty()) { + continue; + } + + // only handle single line comments + if (!line.startsWith("/*")) { + break; + } + + topComments.push_back(line.mid(2, line.size() - 4).trimmed()); + } + + return topComments; +} + +QString extractBaseStyleFromStyleSheet(std::filesystem::path const& path, + QString const& stylesheet, + const QString& defaultStyle) +{ + // read the first line of the files that are either empty or comments + // + const auto topLines = extractTopStyleSheetComments(stylesheet); + + const auto factoryStyles = QStyleFactory::keys(); + + QString style = defaultStyle; + + for (const auto& line : topLines) { + if (!line.startsWith("mo2-base-style")) { + continue; + } + + const auto parts = line.split(":"); + if (parts.size() != 2) { + log::warn("found invalid top-comment for mo2 in {}: {}", path, line); + continue; + } + + const auto tmpStyle = parts[1].trimmed(); + const auto index = factoryStyles.indexOf(tmpStyle, 0, Qt::CaseInsensitive); + if (index == -1) { + log::warn("base style '{}' from style '{}' not found", tmpStyle, path, line); + continue; + } + + style = factoryStyles[index]; + log::info("found base style '{}' for style '{}'", style, path); + break; + } + + return style; +} + +} // namespace + +ThemeManager::ThemeManager(QApplication* application) : m_app{application} +{ + // add built-in themes + addQtThemes(); + + // find the default theme - this might be a built-in Qt theme, or null, in which case + // we just create a default theme + if (auto it = m_baseThemesByIdentifier.find("windowsvista"); + it != m_baseThemesByIdentifier.end()) { + m_defaultTheme = it->second; + } else { + m_defaultTheme = std::make_shared("", "", std::filesystem::path{}); + } + + // for ease, we set the empty identifier to the default theme + m_baseThemesByIdentifier[""] = m_defaultTheme; + + // load the default theme + load(m_defaultTheme); + + // connect the style watcher + m_app->connect(&m_watcher, &QFileSystemWatcher::fileChanged, [this](auto&&) { + reload(); + }); +} + +// TODO: remove this +void ThemeManager::addOldFormatThemes() +{ + QDirIterator iter(QCoreApplication::applicationDirPath() + "/" + + QString::fromStdWString(AppConfig::stylesheetsPath()), + QStringList("*.qss"), QDir::Files); + + while (iter.hasNext()) { + iter.next(); + registerTheme( + std::make_shared(iter.fileInfo().completeBaseName().toStdString(), + iter.fileInfo().baseName().toStdString(), + iter.fileInfo().filesystemFilePath())); + } +} + +bool ThemeManager::load(std::shared_ptr theme) +{ + // no theme -> default + if (!theme) { + theme = m_defaultTheme; + } + + // do not reload the current theme + if (theme == m_currentTheme) { + return true; + } + + // set the current theme + m_currentTheme = theme; + + if (isBuiltIn(theme)) { + loadQtTheme(theme->identifier()); + watchThemeFiles(nullptr); + } else { + loadExtensionTheme(theme); + watchThemeFiles(theme); + } + + return true; +} + +bool ThemeManager::load(std::string_view themeIdentifier) +{ + auto it = m_baseThemesByIdentifier.find(themeIdentifier); + if (it == m_baseThemesByIdentifier.end()) { + log::error("theme '{}' not found", themeIdentifier); + return false; + } + + return load(it->second); +} + +void ThemeManager::loadQtTheme(std::string_view themeIdentifier) +{ + m_app->setStyleSheet(""); + m_app->setStyle(new ProxyStyle(QStyleFactory::create(ToQString(themeIdentifier)))); +} + +void ThemeManager::loadExtensionTheme(std::shared_ptr const& theme) +{ + auto baseTheme = ToQString(m_defaultTheme->identifier()); + const auto stylesheet = buildStyleSheet(theme, baseTheme); + + // load the default theme + m_app->setStyle(new ProxyStyle(QStyleFactory::create(baseTheme))); + + // build the stylesheet and set it + m_app->setStyleSheet(stylesheet); +} + +void ThemeManager::unload() +{ + // load the default style + load(m_defaultTheme); +} + +void ThemeManager::reload() +{ + // cannot reload if there is no theme or builtin themes + if (!m_currentTheme || m_currentTheme->stylesheet().empty()) { + return; + } + + if (isBuiltIn(m_currentTheme)) { + loadQtTheme(m_currentTheme->identifier()); + } else { + loadExtensionTheme(m_currentTheme); + } +} + +void ThemeManager::registerTheme(std::shared_ptr const& theme) +{ + // two themes with same identifier, skip (+ warn) + auto it = m_baseThemesByIdentifier.find(theme->identifier()); + if (it != m_baseThemesByIdentifier.end()) { + log::warn("found existing theme with identifier '{}', skipping", + theme->identifier()); + return; + } + + m_baseThemes.push_back(theme); + m_baseThemesByIdentifier[theme->identifier()] = theme; +} + +void ThemeManager::addQtThemes() +{ + for (const auto& key : QStyleFactory::keys()) { + registerTheme(std::make_shared(key.toStdString(), key.toStdString(), + std::filesystem::path{})); + } +} + +QString ThemeManager::buildStyleSheet(std::shared_ptr const& theme, + QString& baseTheme) const +{ + // read the file + const auto stylesheetContent = readWholeFile(theme->stylesheet()); + + // check for base theme override + baseTheme = + extractBaseStyleFromStyleSheet(theme->stylesheet(), stylesheetContent, baseTheme); + + // patch the file + QString stylesheet = + patchStyleSheet(stylesheetContent, theme->stylesheet().parent_path()); + + for (auto&& themeAddition : m_additions) { + if (themeAddition->isAdditionFor(*theme)) { + stylesheet += "\n" + patchStyleSheet(readWholeFile(themeAddition->stylesheet()), + themeAddition->stylesheet().parent_path()); + } + } + + return stylesheet; +} + +QString ThemeManager::patchStyleSheet(QString stylesheet, + std::filesystem::path const& folder) const +{ + // we try to extract url() from the stylesheet and replace them + QRegularExpression urlRegex(R"re((:|\s+)url\("?([^")]+)"?\))re"); + + QString newStyleSheet = ""; + while (!stylesheet.isEmpty()) { + auto match = urlRegex.match(stylesheet); + + if (match.hasMatch()) { + QFileInfo path(match.captured(2)); + if (path.isRelative()) { + path = QFileInfo(QDir(folder), match.captured(2)); + } + newStyleSheet += stylesheet.left(match.capturedStart()) + match.captured(1) + + "url(\"" + path.absoluteFilePath() + "\")"; + stylesheet = stylesheet.mid(match.capturedEnd()); + } else { + newStyleSheet += stylesheet; + stylesheet = ""; + } + } + + return newStyleSheet; +} + +void ThemeManager::watchThemeFiles(std::shared_ptr const& theme) +{ + // clear previous files + QStringList currentWatch = m_watcher.files(); + if (currentWatch.count() != 0) { + m_watcher.removePaths(currentWatch); + } + + if (!theme) { + return; + } + + // find theme files + QStringList themeFiles; + + themeFiles.append(ToQString(absolute(theme->stylesheet()).native())); + + for (auto&& themeAddition : m_additions) { + if (themeAddition->isAdditionFor(*theme)) { + themeFiles.append(ToQString(absolute(themeAddition->stylesheet()).native())); + } + } + + // add all files + m_watcher.addPaths(themeFiles); +} + +void ThemeManager::extensionLoaded(ThemeExtension const& extension) +{ + for (const auto& theme : extension.themes()) { + registerTheme(theme); + } +} + +void ThemeManager::extensionUnloaded(ThemeExtension const& extension) +{ + // remove theme, unload if needed + for (const auto& theme : extension.themes()) { + if (m_currentTheme == theme) { + unload(); + } + + if (std::erase(m_baseThemes, theme) > 0) { + m_baseThemesByIdentifier.erase(theme->identifier()); + } + } +} + +void ThemeManager::extensionEnabled(ThemeExtension const& extension) +{ + extensionLoaded(extension); +} + +void ThemeManager::extensionDisabled(ThemeExtension const& extension) +{ + extensionUnloaded(extension); +} + +void ThemeManager::extensionLoaded(PluginExtension const& extension) +{ + bool needReload = false; + for (const auto& themeAddition : extension.themeAdditions()) { + m_additions.push_back(themeAddition); + + // check if the addition is for the current theme + needReload = needReload || themeAddition->isAdditionFor(*m_currentTheme); + } + + if (needReload) { + reload(); + } +} + +void ThemeManager::extensionUnloaded(PluginExtension const& extension) +{ + bool needReload = false; + for (const auto& themeAddition : extension.themeAdditions()) { + std::erase(m_additions, themeAddition); + + // check if the addition is for the current theme + needReload = needReload || themeAddition->isAdditionFor(*m_currentTheme); + } + + if (needReload) { + reload(); + } +} + +void ThemeManager::extensionEnabled(PluginExtension const& extension) +{ + extensionLoaded(extension); +} + +void ThemeManager::extensionDisabled(PluginExtension const& extension) +{ + extensionUnloaded(extension); +} diff --git a/src/thememanager.h b/src/thememanager.h new file mode 100644 index 000000000..099755cc2 --- /dev/null +++ b/src/thememanager.h @@ -0,0 +1,122 @@ +#ifndef THEMEMANAGER_H +#define THEMEMANAGER_H + +#include +#include + +#include + +#include "extensionwatcher.h" + +class ThemeManager : public ExtensionWatcher, + public ExtensionWatcher +{ +public: + ThemeManager(QApplication* application); + + // retrieve the list of available themes + // + const auto& themes() const { return m_baseThemes; } + + // load the given theme + // + bool load(std::shared_ptr theme); + bool load(std::string_view themeIdentifier); + + // unload the current theme + // + void unload(); + + // retrieve the current theme, if there is one + // + auto currentTheme() const { return m_currentTheme; } + + // check if the given theme is a built-in Qt theme (not from an extension) + // + bool isBuiltIn(std::shared_ptr const& theme) const + { + return theme->stylesheet().empty(); + } + +public: // ExtensionWatcher + void extensionLoaded(MOBase::ThemeExtension const& extension) override; + void extensionUnloaded(MOBase::ThemeExtension const& extension) override; + void extensionEnabled(MOBase::ThemeExtension const& extension) override; + void extensionDisabled(MOBase::ThemeExtension const& extension) override; + + void extensionLoaded(MOBase::PluginExtension const& extension) override; + void extensionUnloaded(MOBase::PluginExtension const& extension) override; + void extensionEnabled(MOBase::PluginExtension const& extension) override; + void extensionDisabled(MOBase::PluginExtension const& extension) override; + +private: + // reload the current style + // + void reload(); + + // register a theme + // + void registerTheme(std::shared_ptr const& theme); + + // add built-in themes + // + void addQtThemes(); + + // load a Qt theme + // + void loadQtTheme(std::string_view identifier); + + // load an extension theme + // + void loadExtensionTheme(std::shared_ptr const& theme); + + // build a stylesheet for a theme, extracting the base theme if needed (if no base + // theme is found, the baseTheme variable is kept untouched) + // + QString buildStyleSheet(std::shared_ptr const& theme, + QString& baseTheme) const; + + // patch the given stylesheet by replacing url() to be relative to the given folder + // + QString patchStyleSheet(QString stylesheet, + std::filesystem::path const& folder) const; + + // watch files for the given theme (can be nullptr to stop watching) + // + void watchThemeFiles(std::shared_ptr const& theme); + + // [deprecated] add themes for the stylesheets folder + // + [[deprecated]] void addOldFormatThemes(); + +private: + // TODO: move these two elsewhere + struct string_equal : std::equal_to + { + using is_transparent = std::true_type; + }; + + struct string_hash : std::hash + { + using is_transparent = std::true_type; + }; + + // application and file system watcher + QApplication* m_app; + QFileSystemWatcher m_watcher; + + // the default current theme + std::shared_ptr m_defaultTheme; + std::shared_ptr m_currentTheme; + + // the list of base themes + std::vector> m_baseThemes; + std::unordered_map, string_hash, + string_equal> + m_baseThemesByIdentifier; + + // theme extensions for all themes + std::vector> m_additions; +}; + +#endif diff --git a/src/translationmanager.cpp b/src/translationmanager.cpp new file mode 100644 index 000000000..1eeafe167 --- /dev/null +++ b/src/translationmanager.cpp @@ -0,0 +1,251 @@ +#include "translationmanager.h" + +#include +#include +#include + +#include +#include + +#include "shared/appconfig.h" + +using namespace MOBase; + +TranslationManager::TranslationManager(QApplication* application) : m_app{application} +{ + // TODO: remove this + // addOldFormatTranslations(); + + registerTranslation(std::make_shared( + "en_US", "English", std::vector{})); + + // specific translation to allow load("") to actually unload + m_translationByLanguage[""] = nullptr; +} + +// TODO: remove this +void TranslationManager::addOldFormatTranslations() +{ + const QRegularExpression mainTranslationPattern(QRegularExpression::anchoredPattern( + QString::fromStdWString(AppConfig::translationPrefix()) + + "_([a-z]{2,3}(_[A-Z]{2,2})?).qm")); + + // extract the main translations + QDirIterator iter(QCoreApplication::applicationDirPath() + "/translations", + QDir::Files); + while (iter.hasNext()) { + iter.next(); + + const QString file = iter.fileName(); + auto match = mainTranslationPattern.match(file); + if (!match.hasMatch()) { + continue; + } + + const QString languageCode = match.captured(1); + const QLocale locale(languageCode); + + QString languageString = QString("%1 (%2)") + .arg(locale.nativeLanguageName()) + .arg(locale.nativeTerritoryName()); + + if (locale.language() == QLocale::Chinese) { + if (languageCode == "zh_TW") { + languageString = "Chinese (Traditional)"; + } else { + languageString = "Chinese (Simplified)"; + } + } + + std::vector qm_files{iter.fileInfo().filesystemFilePath()}; + + if (auto qt_path = qm_files[0].parent_path() / ("qt_" + languageCode.toStdString()); + exists(qt_path)) { + qm_files.push_back(qt_path); + } + + if (auto qtbase_path = + qm_files[0].parent_path() / ("qtbase_" + languageCode.toStdString()); + exists(qtbase_path)) { + qm_files.push_back(qtbase_path); + } + + registerTranslation(std::make_shared( + languageCode.toStdString(), languageString.toStdString(), qm_files)); + } + + // lookup each file except for main and Qt and add an extension for them + for (auto& [code, translation] : m_translationByLanguage) { + QDirIterator iter(QCoreApplication::applicationDirPath() + "/translations", + {ToQString("*_" + code + ".qm")}, QDir::Files); + + while (iter.hasNext()) { + iter.next(); + const auto filename = iter.fileName(); + + // skip main files + if (filename.startsWith(AppConfig::translationPrefix()) || + filename.startsWith("qt")) { + continue; + } + + m_translationExtensions[code].push_back(std::make_shared( + code, std::vector{iter.fileInfo().filesystemFilePath()})); + } + } +} + +bool TranslationManager::load(std::shared_ptr translation) +{ + // no translation -> abort + if (!translation) { + unload(); + return true; + } + + // do not reload the current translation + if (translation == m_currentTranslation) { + return true; + } + + // unload previous translations + unload(); + + // set the current translation + m_currentTranslation = translation; + + // retrieve all files + std::vector qm_files = translation->files(); + { + auto it = m_translationExtensions.find(m_currentTranslation->identifier()); + if (it != m_translationExtensions.end()) { + for (auto&& translationExtension : it->second) { + const auto& ext_files = translationExtension->files(); + qm_files.insert(qm_files.end(), ext_files.begin(), ext_files.end()); + } + } + } + + // add translators + for (const auto& qm_file : qm_files) { + auto translator = std::make_unique(); + if (translator->load(ToQString(absolute(qm_file).native()))) { + m_app->installTranslator(translator.get()); + m_translators.push_back(std::move(translator)); + } else { + log::warn("failed to load translation from '{}'", qm_file.native()); + } + } + + return true; +} + +bool TranslationManager::load(std::string_view language) +{ + auto it = m_translationByLanguage.find(language); + if (it == m_translationByLanguage.end()) { + log::error("translation for '{}' not found", language); + return false; + } + + return load(it->second); +} + +void TranslationManager::unload() +{ + // remove translators from application + for (auto&& translator : m_translators) { + m_app->removeTranslator(translator.get()); + } + m_translators.clear(); + + // unset current translation + m_currentTranslation = nullptr; +} + +void TranslationManager::registerTranslation( + std::shared_ptr const& translation) +{ + // two translations with same identifier, skip (+ warn) + auto it = m_translationByLanguage.find(translation->identifier()); + if (it != m_translationByLanguage.end()) { + log::warn("found existing translation with identifier '{}', skipping", + translation->identifier()); + return; + } + + m_translations.push_back(translation); + m_translationByLanguage[translation->identifier()] = translation; +} + +void TranslationManager::extensionLoaded(TranslationExtension const& extension) +{ + for (const auto& translation : extension.translations()) { + registerTranslation(translation); + } +} + +void TranslationManager::extensionUnloaded(TranslationExtension const& extension) +{ + // remove translation, unload if needed + for (const auto& translation : extension.translations()) { + if (m_currentTranslation == translation) { + unload(); + } + + if (std::erase(m_translations, translation) > 0) { + m_translationByLanguage.erase(translation->identifier()); + } + } +} + +void TranslationManager::extensionEnabled(TranslationExtension const& extension) +{ + extensionLoaded(extension); +} + +void TranslationManager::extensionDisabled(TranslationExtension const& extension) +{ + extensionUnloaded(extension); +} + +void TranslationManager::extensionLoaded(PluginExtension const& extension) +{ + for (const auto& translationExtension : extension.translationAdditions()) { + const auto identifier = translationExtension->baseIdentifier(); + m_translationExtensions[identifier].push_back(translationExtension); + } +} + +void TranslationManager::extensionUnloaded(PluginExtension const& extension) +{ + for (const auto& translationAddition : extension.translationAdditions()) { + if (m_translationExtensions.contains(translationAddition->baseIdentifier())) { + std::erase(m_translationExtensions[translationAddition->baseIdentifier()], + translationAddition); + } + + // unload translator if there is one + const auto& files = translationAddition->files(); + const auto it = std::find_if( + m_translators.begin(), m_translators.end(), [&files](const auto& translator) { + const auto path = QFileInfo(translator->filePath()).filesystemFilePath(); + return std::find(files.begin(), files.end(), path) != files.end(); + }); + + if (it != m_translators.end()) { + m_app->removeTranslator(it->get()); + m_translators.erase(it); + } + } +} + +void TranslationManager::extensionEnabled(PluginExtension const& extension) +{ + extensionLoaded(extension); +} + +void TranslationManager::extensionDisabled(PluginExtension const& extension) +{ + extensionUnloaded(extension); +} diff --git a/src/translationmanager.h b/src/translationmanager.h new file mode 100644 index 000000000..691a2af0f --- /dev/null +++ b/src/translationmanager.h @@ -0,0 +1,89 @@ +#ifndef TRANSLATIONMANAGER_H +#define TRANSLATIONMANAGER_H + +#include +#include + +#include + +#include "extensionwatcher.h" + +class TranslationManager : public ExtensionWatcher, + public ExtensionWatcher +{ +public: + TranslationManager(QApplication* application); + + // retrieve the list of available translations + // + const auto& translations() const { return m_translations; } + + // load the given translation + // + bool load(std::shared_ptr translation); + bool load(std::string_view language); + + // unload the current translation + // + void unload(); + + // retrieve the current language, if there is one + // + auto currentTranslation() const { return m_currentTranslation; } + +public: // ExtensionWatcher + void extensionLoaded(MOBase::TranslationExtension const& extension) override; + void extensionUnloaded(MOBase::TranslationExtension const& extension) override; + void extensionEnabled(MOBase::TranslationExtension const& extension) override; + void extensionDisabled(MOBase::TranslationExtension const& extension) override; + + void extensionLoaded(MOBase::PluginExtension const& extension) override; + void extensionUnloaded(MOBase::PluginExtension const& extension) override; + void extensionEnabled(MOBase::PluginExtension const& extension) override; + void extensionDisabled(MOBase::PluginExtension const& extension) override; + +private: + // register a translation + // + void + registerTranslation(std::shared_ptr const& translation); + + // [deprecated] add themes for the translations folder + // + [[deprecated]] void addOldFormatTranslations(); + +private: + // TODO: move these two elsewhere + struct string_equal : std::equal_to + { + using is_transparent = std::true_type; + }; + + struct string_hash : std::hash + { + using is_transparent = std::true_type; + }; + + // application + QApplication* m_app; + + // installed translators + std::vector> m_translators; + + // the current translation + std::shared_ptr m_currentTranslation; + + // the list of base translations + std::vector> m_translations; + std::unordered_map, + string_hash, string_equal> + m_translationByLanguage; + + // the list of translations extensions + std::unordered_map>, + string_hash, string_equal> + m_translationExtensions; +}; + +#endif diff --git a/themes/CMakeLists.txt b/themes/CMakeLists.txt new file mode 100644 index 000000000..edc48ad52 --- /dev/null +++ b/themes/CMakeLists.txt @@ -0,0 +1,13 @@ +cmake_minimum_required(VERSION 3.16) + +file(GLOB theme_directories + RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} + LIST_DIRECTORIES TRUE "*") + +foreach(theme_directory ${theme_directories}) + if (IS_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/${theme_directory}") + file(READ ${theme_directory}/metadata.json JSON_METADATA) + string(JSON theme_identifier GET ${JSON_METADATA} id) + install(DIRECTORY ${theme_directory}/ DESTINATION ${MO2_INSTALL_BIN}/extensions/${theme_identifier}) + endif() +endforeach() diff --git a/src/stylesheets/dark.qss b/themes/dark-theme/dark.qss similarity index 95% rename from src/stylesheets/dark.qss rename to themes/dark-theme/dark.qss index b7a185e55..e37e44ba4 100644 --- a/src/stylesheets/dark.qss +++ b/themes/dark-theme/dark.qss @@ -1,395 +1,395 @@ -QToolTip -{ - border: 1px solid black; - color: #D9E6EA; - background-color: #2F3031; - padding: 1px; - border-radius: 3px; - opacity: 255; -} - -QWidget -{ - color: #E9E6E4; - background-color: #2F3031; -} - -QWidget:disabled -{ - color: #757676; - background-color: #292A2B; -} - -QAbstractItemView -{ - background-color: QLinearGradient( x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #484F53, stop: 0.7 #656666, stop: 1 #484F53); -} - -QLineEdit -{ - background-color: QLinearGradient( x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #2D3330, stop: 0.9 #484F53, stop: 1 #2D3330); - padding: 1px; - border-style: solid; - border: 1px solid #1e1e1e; - border-radius: 5; -} - -QPushButton -{ - color: #D9E6EA; - background-color: QLinearGradient( x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #697670, stop: 1 #484F53); - border-width: 2px; - border-color: #1F2021; - border-style: solid; - border-radius: 6; - padding: 3px; - padding-left: 15px; - padding-right: 15px; -} - -QPushButton:pressed -{ - background-color: QLinearGradient( x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #484F53, stop: 1 #697670); -} - -QPushButton:checked -{ - border-width: 1px; - border-color: #3EA0CA; -} - -QComboBox -{ - selection-background-color: #D9E6EA; - background-color: QLinearGradient( x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #9AA6A4, stop: 1 #484F53); - border: 2px solid #1D2320; - height: 20px; - border-radius: 5px; -} - -QComboBox:hover,QPushButton:hover -{ - border: 2px solid #3EA0CA; -} - -QComboBox:on -{ - padding-top: 3px; - padding-left: 4px; - background-color: QLinearGradient( x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #697670, stop: 1 #484F53); - selection-background-color: #80B5C3; -} - -QComboBox::drop-down -{ - subcontrol-origin: padding; - subcontrol-position: top right; - width: 15px; - - border-left-width: 0px; - border-left-color: darkgray; - border-left-style: solid; - border-top-right-radius: 3px; - border-bottom-right-radius: 3px; -} - -QComboBox::down-arrow -{ - image: url(:/stylesheet/combobox-down.png); -} - -QScrollBar:horizontal -{ - border: 1px solid #1F2021; - background: QLinearGradient( x1: 0, y1: 0, x2: 0, y2: 1, stop: 0.0 #1D2320, stop: 0.2 #1F2021, stop: 1 #484F53); - height: 14px; - margin: 1px 16px 1px 16px; -} - -QScrollBar::handle:horizontal -{ - background: QLinearGradient( x1: 0, y1: 0, x2: 1, y2: 0, stop: 0 #3EA0CA, stop: 0.5 #427683, stop: 1 #3EA0CA); - min-height: 20px; - border-radius: 4px; -} - -QScrollBar::add-line:horizontal -{ - border: 1px solid #1b1b19; - border-radius: 2px; - background: QLinearGradient( x1: 0, y1: 0, x2: 1, y2: 0, stop: 0 #3EA0CA, stop: 1 #427683); - width: 14px; - subcontrol-position: right; - subcontrol-origin: margin; -} - -QScrollBar::sub-line:horizontal -{ - border: 1px solid #1b1b19; - border-radius: 2px; - background: QLinearGradient( x1: 0, y1: 0, x2: 1, y2: 0, stop: 0 #3EA0CA, stop: 1 #427683); - width: 14px; - subcontrol-position: left; - subcontrol-origin: margin; -} - -QScrollBar::right-arrow:horizontal, QScrollBar::left-arrow:horizontal -{ - border: 1px solid black; - width: 1px; - height: 1px; - background: white; -} - -QScrollBar::add-page:horizontal, QScrollBar::sub-page:horizontal -{ - background: none; -} - -QScrollBar:vertical -{ - border: 1px solid #1F2021; - background: QLinearGradient( x1: 0, y1: 0, x2: 1, y2: 0, stop: 0.0 #1D2320, stop: 0.2 #1F2021, stop: 1 #484F53); - width: 14px; - margin: 16px 1px 16px 1px; -} - -QScrollBar::handle:vertical -{ - background: QLinearGradient( x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #3EA0CA, stop: 0.5 #427683, stop: 1 #3EA0CA); - min-height: 20px; - border-radius: 4px; -} - -QScrollBar::add-line:vertical -{ - border: 1px solid #1b1b19; - border-radius: 2px; - background: QLinearGradient( x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #3EA0CA, stop: 1 #427683); - height: 14px; - subcontrol-position: bottom; - subcontrol-origin: margin; -} - -QScrollBar::sub-line:vertical -{ - border: 1px solid #1b1b19; - border-radius: 2px; - background: QLinearGradient( x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #3EA0CA, stop: 1 #427683); - height: 14px; - subcontrol-position: top; - subcontrol-origin: margin; -} - -QScrollBar::up-arrow:vertical, QScrollBar::down-arrow:vertical -{ - border: 1px solid black; - width: 1px; - height: 1px; - background: white; -} - -QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical -{ - background: none; -} - -QTextEdit -{ - background-color: #484F53; -} - -QPlainTextEdit -{ - background-color: #484F53; -} - -QWebView -{ - background-color: #484F53; -} - -QHeaderView::section -{ - background-color: QLinearGradient(x1:0, y1:0, x2:0, y2:1, stop:0 #484F53, stop:0.5 #757676, stop:1 #484F53); - color: white; - padding-left: 4px; - border: 1px solid #2D3330; - border-radius: 2px; - border-bottom-right-radius: 4px; - border-bottom-left-radius: 4px; -} - -QCheckBox:disabled -{ - color: #414141; -} - -QMenu::separator -{ - height: 2px; - background-color: #484F53; - color: white; - padding-left: 4px; - margin-left: 10px; - margin-right: 5px; -} - -QMenu::item -{ - padding: 2px 25px 2px 20px; - border: 1px solid transparent; -} - -QMenu::item:selected -{ - background-color: #3c4b54; - border-color: #3EA0CA; -} - -QMenuBar::item:selected { - background-color: #3c4b54; - border-color: #3EA0CA; -} - -QStatusBar::item {border: None;} - -QProgressBar -{ - border: 2px solid grey; - border-radius: 5px; - text-align: center; -} - -QProgressBar::chunk -{ - background-color: #427683; -} - -QTabBar::tab -{ - color: #E9E6E4; - border: 1px solid #444; - border-bottom-style: none; - background-color: QLinearGradient(x1:0, y1:0, x2:0, y2:1, stop:0.8 #1D2320, stop:0.2 #757676); - padding-left: 10px; - padding-right: 10px; - padding-top: 3px; - padding-bottom: 2px; - margin-right: -1px; - border-top-left-radius: 4px; - border-top-right-radius: 4px; -} - -QTabWidget::pane -{ - border: 1px solid #444; - top: 1px; -} - -QTabBar::tab:last -{ - margin-right: 0px; -} - -QTabBar::tab:first -{ - margin-left: 0px; -} - -QTabBar::tab:!selected -{ - color: #E9E6E4; - border-bottom-style: solid; - margin-top: 3px; - background-color: QLinearGradient(x1:0, y1:0, x2:0, y2:1, stop:0.8 #1D2320, stop:0.4 #484F53); -} - -QTabBar::tab:disabled -{ - color: #757676; - border-bottom-style: solid; - margin-top: 3px; - background-color: #484F53; -} -QTabBar::tab:selected -{ - border-top-left-radius: 3px; - border-top-right-radius: 3px; - margin-bottom: 0px; -} - -QTabBar::tab:!selected:hover -{ - border-top-left-radius: 6px; - border-top-right-radius: 6px; - background-color: QLinearGradient(x1:0, y1:0, x2:0, y2:1, stop:1 #212121, stop:0.4 #343434, stop:0.2 #343434, stop:0.1 #3EA0CA); -} - -QToolButton -{ - border:2px ridge #757676; - border-radius: 6px; - margin: 3px; - padding-left: 8px; - padding-right: 8px; - padding-top: 0px; - padding-bottom: 2px; -} - -QToolButton:hover -{ - border: 2px ridge #757676; - background-color: #484F53; - border-radius: 6px; - margin: 3px; - padding-left: 8px; - padding-right: 8px; - padding-top: 0px; - padding-bottom: 2px; -} - -QTreeView, QListView - { - color: #E9E6E4; - background-color: #3F4041; - alternate-background-color: #2F3031; -} - -QAbstractItemView[filtered=true] { - border: 2px solid #f00 !important; -} - -QTreeView::branch:has-children:!has-siblings:closed, -QTreeView::branch:closed:has-children:has-siblings -{ - border-image: none; - image: url(:/stylesheet/branch-closed.png); -} - -QTreeView::branch:open:has-children:!has-siblings, -QTreeView::branch:open:has-children:has-siblings -{ - border-image: none; - image: url(:/stylesheet/branch-open.png); -} - -DownloadListView QLabel#installLabel { - color: none; -} - -DownloadListView[downloadView=standard]::item { - padding: 16px; -} - -DownloadListView[downloadView=compact]::item { - padding: 4px; -} - -LinkLabel { - qproperty-linkColor: #3399FF; -} - -QLineEdit[valid-filter=false] { - background-color: #661111 !important; -} +QToolTip +{ + border: 1px solid black; + color: #D9E6EA; + background-color: #2F3031; + padding: 1px; + border-radius: 3px; + opacity: 255; +} + +QWidget +{ + color: #E9E6E4; + background-color: #2F3031; +} + +QWidget:disabled +{ + color: #757676; + background-color: #292A2B; +} + +QAbstractItemView +{ + background-color: QLinearGradient( x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #484F53, stop: 0.7 #656666, stop: 1 #484F53); +} + +QLineEdit +{ + background-color: QLinearGradient( x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #2D3330, stop: 0.9 #484F53, stop: 1 #2D3330); + padding: 1px; + border-style: solid; + border: 1px solid #1e1e1e; + border-radius: 5; +} + +QPushButton +{ + color: #D9E6EA; + background-color: QLinearGradient( x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #697670, stop: 1 #484F53); + border-width: 2px; + border-color: #1F2021; + border-style: solid; + border-radius: 6; + padding: 3px; + padding-left: 15px; + padding-right: 15px; +} + +QPushButton:pressed +{ + background-color: QLinearGradient( x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #484F53, stop: 1 #697670); +} + +QPushButton:checked +{ + border-width: 1px; + border-color: #3EA0CA; +} + +QComboBox +{ + selection-background-color: #D9E6EA; + background-color: QLinearGradient( x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #9AA6A4, stop: 1 #484F53); + border: 2px solid #1D2320; + height: 20px; + border-radius: 5px; +} + +QComboBox:hover,QPushButton:hover +{ + border: 2px solid #3EA0CA; +} + +QComboBox:on +{ + padding-top: 3px; + padding-left: 4px; + background-color: QLinearGradient( x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #697670, stop: 1 #484F53); + selection-background-color: #80B5C3; +} + +QComboBox::drop-down +{ + subcontrol-origin: padding; + subcontrol-position: top right; + width: 15px; + + border-left-width: 0px; + border-left-color: darkgray; + border-left-style: solid; + border-top-right-radius: 3px; + border-bottom-right-radius: 3px; +} + +QComboBox::down-arrow +{ + image: url(:/stylesheet/combobox-down.png); +} + +QScrollBar:horizontal +{ + border: 1px solid #1F2021; + background: QLinearGradient( x1: 0, y1: 0, x2: 0, y2: 1, stop: 0.0 #1D2320, stop: 0.2 #1F2021, stop: 1 #484F53); + height: 14px; + margin: 1px 16px 1px 16px; +} + +QScrollBar::handle:horizontal +{ + background: QLinearGradient( x1: 0, y1: 0, x2: 1, y2: 0, stop: 0 #3EA0CA, stop: 0.5 #427683, stop: 1 #3EA0CA); + min-height: 20px; + border-radius: 4px; +} + +QScrollBar::add-line:horizontal +{ + border: 1px solid #1b1b19; + border-radius: 2px; + background: QLinearGradient( x1: 0, y1: 0, x2: 1, y2: 0, stop: 0 #3EA0CA, stop: 1 #427683); + width: 14px; + subcontrol-position: right; + subcontrol-origin: margin; +} + +QScrollBar::sub-line:horizontal +{ + border: 1px solid #1b1b19; + border-radius: 2px; + background: QLinearGradient( x1: 0, y1: 0, x2: 1, y2: 0, stop: 0 #3EA0CA, stop: 1 #427683); + width: 14px; + subcontrol-position: left; + subcontrol-origin: margin; +} + +QScrollBar::right-arrow:horizontal, QScrollBar::left-arrow:horizontal +{ + border: 1px solid black; + width: 1px; + height: 1px; + background: white; +} + +QScrollBar::add-page:horizontal, QScrollBar::sub-page:horizontal +{ + background: none; +} + +QScrollBar:vertical +{ + border: 1px solid #1F2021; + background: QLinearGradient( x1: 0, y1: 0, x2: 1, y2: 0, stop: 0.0 #1D2320, stop: 0.2 #1F2021, stop: 1 #484F53); + width: 14px; + margin: 16px 1px 16px 1px; +} + +QScrollBar::handle:vertical +{ + background: QLinearGradient( x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #3EA0CA, stop: 0.5 #427683, stop: 1 #3EA0CA); + min-height: 20px; + border-radius: 4px; +} + +QScrollBar::add-line:vertical +{ + border: 1px solid #1b1b19; + border-radius: 2px; + background: QLinearGradient( x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #3EA0CA, stop: 1 #427683); + height: 14px; + subcontrol-position: bottom; + subcontrol-origin: margin; +} + +QScrollBar::sub-line:vertical +{ + border: 1px solid #1b1b19; + border-radius: 2px; + background: QLinearGradient( x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #3EA0CA, stop: 1 #427683); + height: 14px; + subcontrol-position: top; + subcontrol-origin: margin; +} + +QScrollBar::up-arrow:vertical, QScrollBar::down-arrow:vertical +{ + border: 1px solid black; + width: 1px; + height: 1px; + background: white; +} + +QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical +{ + background: none; +} + +QTextEdit +{ + background-color: #484F53; +} + +QPlainTextEdit +{ + background-color: #484F53; +} + +QWebView +{ + background-color: #484F53; +} + +QHeaderView::section +{ + background-color: QLinearGradient(x1:0, y1:0, x2:0, y2:1, stop:0 #484F53, stop:0.5 #757676, stop:1 #484F53); + color: white; + padding-left: 4px; + border: 1px solid #2D3330; + border-radius: 2px; + border-bottom-right-radius: 4px; + border-bottom-left-radius: 4px; +} + +QCheckBox:disabled +{ + color: #414141; +} + +QMenu::separator +{ + height: 2px; + background-color: #484F53; + color: white; + padding-left: 4px; + margin-left: 10px; + margin-right: 5px; +} + +QMenu::item +{ + padding: 2px 25px 2px 20px; + border: 1px solid transparent; +} + +QMenu::item:selected +{ + background-color: #3c4b54; + border-color: #3EA0CA; +} + +QMenuBar::item:selected { + background-color: #3c4b54; + border-color: #3EA0CA; +} + +QStatusBar::item {border: None;} + +QProgressBar +{ + border: 2px solid grey; + border-radius: 5px; + text-align: center; +} + +QProgressBar::chunk +{ + background-color: #427683; +} + +QTabBar::tab +{ + color: #E9E6E4; + border: 1px solid #444; + border-bottom-style: none; + background-color: QLinearGradient(x1:0, y1:0, x2:0, y2:1, stop:0.8 #1D2320, stop:0.2 #757676); + padding-left: 10px; + padding-right: 10px; + padding-top: 3px; + padding-bottom: 2px; + margin-right: -1px; + border-top-left-radius: 4px; + border-top-right-radius: 4px; +} + +QTabWidget::pane +{ + border: 1px solid #444; + top: 1px; +} + +QTabBar::tab:last +{ + margin-right: 0px; +} + +QTabBar::tab:first +{ + margin-left: 0px; +} + +QTabBar::tab:!selected +{ + color: #E9E6E4; + border-bottom-style: solid; + margin-top: 3px; + background-color: QLinearGradient(x1:0, y1:0, x2:0, y2:1, stop:0.8 #1D2320, stop:0.4 #484F53); +} + +QTabBar::tab:disabled +{ + color: #757676; + border-bottom-style: solid; + margin-top: 3px; + background-color: #484F53; +} +QTabBar::tab:selected +{ + border-top-left-radius: 3px; + border-top-right-radius: 3px; + margin-bottom: 0px; +} + +QTabBar::tab:!selected:hover +{ + border-top-left-radius: 6px; + border-top-right-radius: 6px; + background-color: QLinearGradient(x1:0, y1:0, x2:0, y2:1, stop:1 #212121, stop:0.4 #343434, stop:0.2 #343434, stop:0.1 #3EA0CA); +} + +QToolButton +{ + border:2px ridge #757676; + border-radius: 6px; + margin: 3px; + padding-left: 8px; + padding-right: 8px; + padding-top: 0px; + padding-bottom: 2px; +} + +QToolButton:hover +{ + border: 2px ridge #757676; + background-color: #484F53; + border-radius: 6px; + margin: 3px; + padding-left: 8px; + padding-right: 8px; + padding-top: 0px; + padding-bottom: 2px; +} + +QTreeView, QListView + { + color: #E9E6E4; + background-color: #3F4041; + alternate-background-color: #2F3031; +} + +QAbstractItemView[filtered=true] { + border: 2px solid #f00 !important; +} + +QTreeView::branch:has-children:!has-siblings:closed, +QTreeView::branch:closed:has-children:has-siblings +{ + border-image: none; + image: url(:/stylesheet/branch-closed.png); +} + +QTreeView::branch:open:has-children:!has-siblings, +QTreeView::branch:open:has-children:has-siblings +{ + border-image: none; + image: url(:/stylesheet/branch-open.png); +} + +DownloadListView QLabel#installLabel { + color: none; +} + +DownloadListView[downloadView=standard]::item { + padding: 16px; +} + +DownloadListView[downloadView=compact]::item { + padding: 4px; +} + +LinkLabel { + qproperty-linkColor: #3399FF; +} + +QLineEdit[valid-filter=false] { + background-color: #661111 !important; +} diff --git a/themes/dark-theme/metadata.json b/themes/dark-theme/metadata.json new file mode 100644 index 000000000..9075b2c5f --- /dev/null +++ b/themes/dark-theme/metadata.json @@ -0,0 +1,15 @@ +{ + "id": "mo2-theme-dark", + "name": "Dark Theme", + "description": "Dark theme for ModOrganizer2.", + "version": "1.0.0", + "type": "theme", + "content": { + "themes": { + "dark": { + "name": "Dark", + "path": "dark.qss" + } + } + } +} diff --git a/src/stylesheets/dracula.qss b/themes/dracula-theme/dracula.qss similarity index 100% rename from src/stylesheets/dracula.qss rename to themes/dracula-theme/dracula.qss diff --git a/themes/dracula-theme/metadata.json b/themes/dracula-theme/metadata.json new file mode 100644 index 000000000..b811bf853 --- /dev/null +++ b/themes/dracula-theme/metadata.json @@ -0,0 +1,15 @@ +{ + "id": "mo2-theme-dracula", + "type": "theme", + "name": "Dracula Theme", + "description": "Dracula theme for ModOrganizer2.", + "version": "1.0.0", + "content": { + "themes": { + "dracula": { + "name": "Dracula", + "path": "dracula.qss" + } + } + } +} diff --git a/themes/make-metadata.ps1 b/themes/make-metadata.ps1 new file mode 100644 index 000000000..1625f3377 --- /dev/null +++ b/themes/make-metadata.ps1 @@ -0,0 +1,46 @@ +Get-ChildItem -Directory | ForEach-Object { + $metadata = Get-Content "$_/metadata.json" | ConvertFrom-Json + $metadata + $name = $metadata.identifier -replace "-themes?" + $data = [ordered]@{ + id = "mo2-theme-" + $name; + type = "theme"; + name = $metadata.name; + description = $metadata.description; + version = "1.0.0"; + content = @{ + themes = $metadata.themes; + }; + } + $data + # ConvertTo-Json -InputObject $data # | Set-Content (Join-Path $_ "metadata.json") +} + +# $fixNames = @{ +# dark = "Dark"; +# dracula = "Dracula"; +# nighteyes = "Night Eyes"; +# parchment = "Parchment"; +# skyrim = "Skyrim"; +# } + +# Get-ChildItem -Directory -Exclude "vs15" | ForEach-Object { +# $name = $fixNames[$_.Name]; +# $data = [ordered]@{ +# identifier = "mo2-theme-" + $_.Name; +# type = "theme"; +# name = "$name Theme"; +# description = "$name theme for ModOrganizer2."; +# version = "1.0.0"; +# content = @{ +# themes = @{ +# $_.Name = [ordered]@{ +# name = $name; +# path = (Get-ChildItem $_ -Filter "*.qss")[0].Name; +# } +# }; +# }; +# ; +# } +# ConvertTo-Json -InputObject $data | Set-Content (Join-Path $_ "metadata.json") +# } diff --git a/themes/nighteyes-theme/metadata.json b/themes/nighteyes-theme/metadata.json new file mode 100644 index 000000000..d8d7b6a3f --- /dev/null +++ b/themes/nighteyes-theme/metadata.json @@ -0,0 +1,19 @@ +{ + "id": "mo2-theme-nighteyes", + "type": "theme", + "name": "Night Eyes Theme", + "author": { + "name": "ciathyza", + "homepage": "https://github.com/ciathyza" + }, + "description": "Night Eyes theme for ModOrganizer2.", + "version": "1.2.0", + "content": { + "themes": { + "nighteyes": { + "name": "Night Eyes", + "path": "nigheyes.qss" + } + } + } +} diff --git a/src/stylesheets/Night Eyes.qss b/themes/nighteyes-theme/nigheyes.qss similarity index 100% rename from src/stylesheets/Night Eyes.qss rename to themes/nighteyes-theme/nigheyes.qss diff --git a/src/stylesheets/Parchment/checkbox-alt-checked.png b/themes/parchment-theme/Parchment/checkbox-alt-checked.png similarity index 100% rename from src/stylesheets/Parchment/checkbox-alt-checked.png rename to themes/parchment-theme/Parchment/checkbox-alt-checked.png diff --git a/src/stylesheets/Parchment/checkbox-alt-unchecked-hover.png b/themes/parchment-theme/Parchment/checkbox-alt-unchecked-hover.png similarity index 100% rename from src/stylesheets/Parchment/checkbox-alt-unchecked-hover.png rename to themes/parchment-theme/Parchment/checkbox-alt-unchecked-hover.png diff --git a/src/stylesheets/Parchment/checkbox-alt-unchecked.png b/themes/parchment-theme/Parchment/checkbox-alt-unchecked.png similarity index 100% rename from src/stylesheets/Parchment/checkbox-alt-unchecked.png rename to themes/parchment-theme/Parchment/checkbox-alt-unchecked.png diff --git a/src/stylesheets/Parchment/checkbox-checked-disabled.png b/themes/parchment-theme/Parchment/checkbox-checked-disabled.png similarity index 100% rename from src/stylesheets/Parchment/checkbox-checked-disabled.png rename to themes/parchment-theme/Parchment/checkbox-checked-disabled.png diff --git a/src/stylesheets/Parchment/checkbox-checked-hover.png b/themes/parchment-theme/Parchment/checkbox-checked-hover.png similarity index 100% rename from src/stylesheets/Parchment/checkbox-checked-hover.png rename to themes/parchment-theme/Parchment/checkbox-checked-hover.png diff --git a/src/stylesheets/Parchment/checkbox-checked.png b/themes/parchment-theme/Parchment/checkbox-checked.png similarity index 100% rename from src/stylesheets/Parchment/checkbox-checked.png rename to themes/parchment-theme/Parchment/checkbox-checked.png diff --git a/src/stylesheets/Parchment/checkbox-disabled.png b/themes/parchment-theme/Parchment/checkbox-disabled.png similarity index 100% rename from src/stylesheets/Parchment/checkbox-disabled.png rename to themes/parchment-theme/Parchment/checkbox-disabled.png diff --git a/src/stylesheets/Parchment/checkbox-hover.png b/themes/parchment-theme/Parchment/checkbox-hover.png similarity index 100% rename from src/stylesheets/Parchment/checkbox-hover.png rename to themes/parchment-theme/Parchment/checkbox-hover.png diff --git a/src/stylesheets/Parchment/checkbox.png b/themes/parchment-theme/Parchment/checkbox.png similarity index 100% rename from src/stylesheets/Parchment/checkbox.png rename to themes/parchment-theme/Parchment/checkbox.png diff --git a/themes/parchment-theme/metadata.json b/themes/parchment-theme/metadata.json new file mode 100644 index 000000000..09448ec76 --- /dev/null +++ b/themes/parchment-theme/metadata.json @@ -0,0 +1,15 @@ +{ + "id": "mo2-theme-parchment", + "type": "theme", + "name": "Parchment Theme", + "description": "Parchment theme for ModOrganizer2.", + "version": "1.0.0", + "content": { + "themes": { + "parchment": { + "name": "Parchment", + "path": "parchment.qss" + } + } + } +} diff --git a/src/stylesheets/Parchment v1.1 by Bob.qss b/themes/parchment-theme/parchment.qss similarity index 100% rename from src/stylesheets/Parchment v1.1 by Bob.qss rename to themes/parchment-theme/parchment.qss diff --git a/themes/skyrim-theme/metadata.json b/themes/skyrim-theme/metadata.json new file mode 100644 index 000000000..f2f78ec09 --- /dev/null +++ b/themes/skyrim-theme/metadata.json @@ -0,0 +1,15 @@ +{ + "id": "mo2-theme-skyrim", + "type": "theme", + "name": "Skyrim Theme", + "description": "Skyrim theme for ModOrganizer2.", + "version": "1.0.0", + "content": { + "themes": { + "skyrim": { + "name": "Skyrim", + "path": "skyrim.qss" + } + } + } +} diff --git a/src/stylesheets/skyrim.qss b/themes/skyrim-theme/skyrim.qss similarity index 100% rename from src/stylesheets/skyrim.qss rename to themes/skyrim-theme/skyrim.qss diff --git a/src/stylesheets/skyrim/arrow-down.png b/themes/skyrim-theme/skyrim/arrow-down.png similarity index 100% rename from src/stylesheets/skyrim/arrow-down.png rename to themes/skyrim-theme/skyrim/arrow-down.png diff --git a/src/stylesheets/skyrim/arrow-left.png b/themes/skyrim-theme/skyrim/arrow-left.png similarity index 100% rename from src/stylesheets/skyrim/arrow-left.png rename to themes/skyrim-theme/skyrim/arrow-left.png diff --git a/src/stylesheets/skyrim/arrow-right.png b/themes/skyrim-theme/skyrim/arrow-right.png similarity index 100% rename from src/stylesheets/skyrim/arrow-right.png rename to themes/skyrim-theme/skyrim/arrow-right.png diff --git a/src/stylesheets/skyrim/arrow-up.png b/themes/skyrim-theme/skyrim/arrow-up.png similarity index 100% rename from src/stylesheets/skyrim/arrow-up.png rename to themes/skyrim-theme/skyrim/arrow-up.png diff --git a/src/stylesheets/skyrim/border-image.png b/themes/skyrim-theme/skyrim/border-image.png similarity index 100% rename from src/stylesheets/skyrim/border-image.png rename to themes/skyrim-theme/skyrim/border-image.png diff --git a/src/stylesheets/skyrim/border-image1.png b/themes/skyrim-theme/skyrim/border-image1.png similarity index 100% rename from src/stylesheets/skyrim/border-image1.png rename to themes/skyrim-theme/skyrim/border-image1.png diff --git a/src/stylesheets/skyrim/border-image2.png b/themes/skyrim-theme/skyrim/border-image2.png similarity index 100% rename from src/stylesheets/skyrim/border-image2.png rename to themes/skyrim-theme/skyrim/border-image2.png diff --git a/src/stylesheets/skyrim/branch-opened.png b/themes/skyrim-theme/skyrim/branch-opened.png similarity index 100% rename from src/stylesheets/skyrim/branch-opened.png rename to themes/skyrim-theme/skyrim/branch-opened.png diff --git a/src/stylesheets/skyrim/button-big-border.png b/themes/skyrim-theme/skyrim/button-big-border.png similarity index 100% rename from src/stylesheets/skyrim/button-big-border.png rename to themes/skyrim-theme/skyrim/button-big-border.png diff --git a/src/stylesheets/skyrim/button-border.png b/themes/skyrim-theme/skyrim/button-border.png similarity index 100% rename from src/stylesheets/skyrim/button-border.png rename to themes/skyrim-theme/skyrim/button-border.png diff --git a/src/stylesheets/skyrim/button-checked-border.png b/themes/skyrim-theme/skyrim/button-checked-border.png similarity index 100% rename from src/stylesheets/skyrim/button-checked-border.png rename to themes/skyrim-theme/skyrim/button-checked-border.png diff --git a/src/stylesheets/skyrim/checkbox-alt-checked.png b/themes/skyrim-theme/skyrim/checkbox-alt-checked.png similarity index 100% rename from src/stylesheets/skyrim/checkbox-alt-checked.png rename to themes/skyrim-theme/skyrim/checkbox-alt-checked.png diff --git a/src/stylesheets/skyrim/checkbox-alt-unchecked-hover.png b/themes/skyrim-theme/skyrim/checkbox-alt-unchecked-hover.png similarity index 100% rename from src/stylesheets/skyrim/checkbox-alt-unchecked-hover.png rename to themes/skyrim-theme/skyrim/checkbox-alt-unchecked-hover.png diff --git a/src/stylesheets/skyrim/checkbox-alt-unchecked.png b/themes/skyrim-theme/skyrim/checkbox-alt-unchecked.png similarity index 100% rename from src/stylesheets/skyrim/checkbox-alt-unchecked.png rename to themes/skyrim-theme/skyrim/checkbox-alt-unchecked.png diff --git a/src/stylesheets/skyrim/checkbox-checked-disabled.png b/themes/skyrim-theme/skyrim/checkbox-checked-disabled.png similarity index 100% rename from src/stylesheets/skyrim/checkbox-checked-disabled.png rename to themes/skyrim-theme/skyrim/checkbox-checked-disabled.png diff --git a/src/stylesheets/skyrim/checkbox-checked-hover.png b/themes/skyrim-theme/skyrim/checkbox-checked-hover.png similarity index 100% rename from src/stylesheets/skyrim/checkbox-checked-hover.png rename to themes/skyrim-theme/skyrim/checkbox-checked-hover.png diff --git a/src/stylesheets/skyrim/checkbox-checked.png b/themes/skyrim-theme/skyrim/checkbox-checked.png similarity index 100% rename from src/stylesheets/skyrim/checkbox-checked.png rename to themes/skyrim-theme/skyrim/checkbox-checked.png diff --git a/src/stylesheets/skyrim/checkbox-disabled.png b/themes/skyrim-theme/skyrim/checkbox-disabled.png similarity index 100% rename from src/stylesheets/skyrim/checkbox-disabled.png rename to themes/skyrim-theme/skyrim/checkbox-disabled.png diff --git a/src/stylesheets/skyrim/checkbox-hover.png b/themes/skyrim-theme/skyrim/checkbox-hover.png similarity index 100% rename from src/stylesheets/skyrim/checkbox-hover.png rename to themes/skyrim-theme/skyrim/checkbox-hover.png diff --git a/src/stylesheets/skyrim/checkbox.png b/themes/skyrim-theme/skyrim/checkbox.png similarity index 100% rename from src/stylesheets/skyrim/checkbox.png rename to themes/skyrim-theme/skyrim/checkbox.png diff --git a/src/stylesheets/skyrim/context-menu-separator.png b/themes/skyrim-theme/skyrim/context-menu-separator.png similarity index 100% rename from src/stylesheets/skyrim/context-menu-separator.png rename to themes/skyrim-theme/skyrim/context-menu-separator.png diff --git a/src/stylesheets/skyrim/progress-bar-border.png b/themes/skyrim-theme/skyrim/progress-bar-border.png similarity index 100% rename from src/stylesheets/skyrim/progress-bar-border.png rename to themes/skyrim-theme/skyrim/progress-bar-border.png diff --git a/src/stylesheets/skyrim/progress-bar-chunk.png b/themes/skyrim-theme/skyrim/progress-bar-chunk.png similarity index 100% rename from src/stylesheets/skyrim/progress-bar-chunk.png rename to themes/skyrim-theme/skyrim/progress-bar-chunk.png diff --git a/src/stylesheets/skyrim/radio-checked.png b/themes/skyrim-theme/skyrim/radio-checked.png similarity index 100% rename from src/stylesheets/skyrim/radio-checked.png rename to themes/skyrim-theme/skyrim/radio-checked.png diff --git a/src/stylesheets/skyrim/radio-hover.png b/themes/skyrim-theme/skyrim/radio-hover.png similarity index 100% rename from src/stylesheets/skyrim/radio-hover.png rename to themes/skyrim-theme/skyrim/radio-hover.png diff --git a/src/stylesheets/skyrim/radio.png b/themes/skyrim-theme/skyrim/radio.png similarity index 100% rename from src/stylesheets/skyrim/radio.png rename to themes/skyrim-theme/skyrim/radio.png diff --git a/src/stylesheets/skyrim/scrollbar-down.png b/themes/skyrim-theme/skyrim/scrollbar-down.png similarity index 100% rename from src/stylesheets/skyrim/scrollbar-down.png rename to themes/skyrim-theme/skyrim/scrollbar-down.png diff --git a/src/stylesheets/skyrim/scrollbar-horizontal.png b/themes/skyrim-theme/skyrim/scrollbar-horizontal.png similarity index 100% rename from src/stylesheets/skyrim/scrollbar-horizontal.png rename to themes/skyrim-theme/skyrim/scrollbar-horizontal.png diff --git a/src/stylesheets/skyrim/scrollbar-left.png b/themes/skyrim-theme/skyrim/scrollbar-left.png similarity index 100% rename from src/stylesheets/skyrim/scrollbar-left.png rename to themes/skyrim-theme/skyrim/scrollbar-left.png diff --git a/src/stylesheets/skyrim/scrollbar-right.png b/themes/skyrim-theme/skyrim/scrollbar-right.png similarity index 100% rename from src/stylesheets/skyrim/scrollbar-right.png rename to themes/skyrim-theme/skyrim/scrollbar-right.png diff --git a/src/stylesheets/skyrim/scrollbar-up.png b/themes/skyrim-theme/skyrim/scrollbar-up.png similarity index 100% rename from src/stylesheets/skyrim/scrollbar-up.png rename to themes/skyrim-theme/skyrim/scrollbar-up.png diff --git a/src/stylesheets/skyrim/scrollbar-vertical.png b/themes/skyrim-theme/skyrim/scrollbar-vertical.png similarity index 100% rename from src/stylesheets/skyrim/scrollbar-vertical.png rename to themes/skyrim-theme/skyrim/scrollbar-vertical.png diff --git a/src/stylesheets/skyrim/separator.png b/themes/skyrim-theme/skyrim/separator.png similarity index 100% rename from src/stylesheets/skyrim/separator.png rename to themes/skyrim-theme/skyrim/separator.png diff --git a/src/stylesheets/skyrim/slider-border.png b/themes/skyrim-theme/skyrim/slider-border.png similarity index 100% rename from src/stylesheets/skyrim/slider-border.png rename to themes/skyrim-theme/skyrim/slider-border.png diff --git a/src/stylesheets/skyrim/slider-handle.png b/themes/skyrim-theme/skyrim/slider-handle.png similarity index 100% rename from src/stylesheets/skyrim/slider-handle.png rename to themes/skyrim-theme/skyrim/slider-handle.png diff --git a/themes/vs15-themes/metadata.json b/themes/vs15-themes/metadata.json new file mode 100644 index 000000000..c6404d462 --- /dev/null +++ b/themes/vs15-themes/metadata.json @@ -0,0 +1,39 @@ +{ + "id": "mo2-theme-vs15-dark", + "type": "theme", + "name": "VS15 Themes", + "description": "Set of dark themes, inspired by Visual Studio, for ModOrganizer2.", + "version": "1.0.0", + "content": { + "themes": { + "vs15-dark-blue": { + "name": "VS15 - Dark Blue", + "path": "vs15 Dark-Blue.qss" + }, + "vs15-dark-green": { + "name": "VS15 - Dark Green", + "path": "vs15 Dark-Green.qss" + }, + "vs15-dark-orange": { + "name": "VS15 - Dark Orange", + "path": "vs15 Dark-Orange.qss" + }, + "vs15-dark-pink": { + "name": "VS15 - Dark Pink", + "path": "vs15 Dark-Pink.qss" + }, + "vs15-dark-purple": { + "name": "VS15 - Dark Purple", + "path": "vs15 Dark-Purple.qss" + }, + "vs15-dark-red": { + "name": "VS15 - Dark Red", + "path": "vs15 Dark-Red.qss" + }, + "vs15-dark-yellow": { + "name": "VS15 - Dark Yellow", + "path": "vs15 Dark-Yellow.qss" + } + } + } +} diff --git a/src/stylesheets/vs15 Dark.qss b/themes/vs15-themes/vs15 Dark-Blue.qss similarity index 100% rename from src/stylesheets/vs15 Dark.qss rename to themes/vs15-themes/vs15 Dark-Blue.qss diff --git a/src/stylesheets/vs15 Dark-Green.qss b/themes/vs15-themes/vs15 Dark-Green.qss similarity index 100% rename from src/stylesheets/vs15 Dark-Green.qss rename to themes/vs15-themes/vs15 Dark-Green.qss diff --git a/src/stylesheets/vs15 Dark-Orange.qss b/themes/vs15-themes/vs15 Dark-Orange.qss similarity index 100% rename from src/stylesheets/vs15 Dark-Orange.qss rename to themes/vs15-themes/vs15 Dark-Orange.qss diff --git a/src/stylesheets/vs15 Dark-Pink.qss b/themes/vs15-themes/vs15 Dark-Pink.qss similarity index 100% rename from src/stylesheets/vs15 Dark-Pink.qss rename to themes/vs15-themes/vs15 Dark-Pink.qss diff --git a/src/stylesheets/vs15 Dark-Purple.qss b/themes/vs15-themes/vs15 Dark-Purple.qss similarity index 100% rename from src/stylesheets/vs15 Dark-Purple.qss rename to themes/vs15-themes/vs15 Dark-Purple.qss diff --git a/src/stylesheets/vs15 Dark-Red.qss b/themes/vs15-themes/vs15 Dark-Red.qss similarity index 100% rename from src/stylesheets/vs15 Dark-Red.qss rename to themes/vs15-themes/vs15 Dark-Red.qss diff --git a/src/stylesheets/vs15 Dark-Yellow.qss b/themes/vs15-themes/vs15 Dark-Yellow.qss similarity index 100% rename from src/stylesheets/vs15 Dark-Yellow.qss rename to themes/vs15-themes/vs15 Dark-Yellow.qss diff --git a/src/stylesheets/vs15/branch-closed.png b/themes/vs15-themes/vs15/branch-closed.png similarity index 100% rename from src/stylesheets/vs15/branch-closed.png rename to themes/vs15-themes/vs15/branch-closed.png diff --git a/src/stylesheets/vs15/branch-open.png b/themes/vs15-themes/vs15/branch-open.png similarity index 100% rename from src/stylesheets/vs15/branch-open.png rename to themes/vs15-themes/vs15/branch-open.png diff --git a/src/stylesheets/vs15/checkbox-check-disabled.png b/themes/vs15-themes/vs15/checkbox-check-disabled.png similarity index 100% rename from src/stylesheets/vs15/checkbox-check-disabled.png rename to themes/vs15-themes/vs15/checkbox-check-disabled.png diff --git a/src/stylesheets/vs15/checkbox-check.png b/themes/vs15-themes/vs15/checkbox-check.png similarity index 100% rename from src/stylesheets/vs15/checkbox-check.png rename to themes/vs15-themes/vs15/checkbox-check.png diff --git a/src/stylesheets/vs15/combobox-down.png b/themes/vs15-themes/vs15/combobox-down.png similarity index 100% rename from src/stylesheets/vs15/combobox-down.png rename to themes/vs15-themes/vs15/combobox-down.png diff --git a/src/stylesheets/vs15/scrollbar-down-disabled.png b/themes/vs15-themes/vs15/scrollbar-down-disabled.png similarity index 100% rename from src/stylesheets/vs15/scrollbar-down-disabled.png rename to themes/vs15-themes/vs15/scrollbar-down-disabled.png diff --git a/src/stylesheets/vs15/scrollbar-down-hover.png b/themes/vs15-themes/vs15/scrollbar-down-hover.png similarity index 100% rename from src/stylesheets/vs15/scrollbar-down-hover.png rename to themes/vs15-themes/vs15/scrollbar-down-hover.png diff --git a/src/stylesheets/vs15/scrollbar-down.png b/themes/vs15-themes/vs15/scrollbar-down.png similarity index 100% rename from src/stylesheets/vs15/scrollbar-down.png rename to themes/vs15-themes/vs15/scrollbar-down.png diff --git a/src/stylesheets/vs15/scrollbar-left-disabled.png b/themes/vs15-themes/vs15/scrollbar-left-disabled.png similarity index 100% rename from src/stylesheets/vs15/scrollbar-left-disabled.png rename to themes/vs15-themes/vs15/scrollbar-left-disabled.png diff --git a/src/stylesheets/vs15/scrollbar-left-hover.png b/themes/vs15-themes/vs15/scrollbar-left-hover.png similarity index 100% rename from src/stylesheets/vs15/scrollbar-left-hover.png rename to themes/vs15-themes/vs15/scrollbar-left-hover.png diff --git a/src/stylesheets/vs15/scrollbar-left.png b/themes/vs15-themes/vs15/scrollbar-left.png similarity index 100% rename from src/stylesheets/vs15/scrollbar-left.png rename to themes/vs15-themes/vs15/scrollbar-left.png diff --git a/src/stylesheets/vs15/scrollbar-right-disabled.png b/themes/vs15-themes/vs15/scrollbar-right-disabled.png similarity index 100% rename from src/stylesheets/vs15/scrollbar-right-disabled.png rename to themes/vs15-themes/vs15/scrollbar-right-disabled.png diff --git a/src/stylesheets/vs15/scrollbar-right-hover.png b/themes/vs15-themes/vs15/scrollbar-right-hover.png similarity index 100% rename from src/stylesheets/vs15/scrollbar-right-hover.png rename to themes/vs15-themes/vs15/scrollbar-right-hover.png diff --git a/src/stylesheets/vs15/scrollbar-right.png b/themes/vs15-themes/vs15/scrollbar-right.png similarity index 100% rename from src/stylesheets/vs15/scrollbar-right.png rename to themes/vs15-themes/vs15/scrollbar-right.png diff --git a/src/stylesheets/vs15/scrollbar-up-disabled.png b/themes/vs15-themes/vs15/scrollbar-up-disabled.png similarity index 100% rename from src/stylesheets/vs15/scrollbar-up-disabled.png rename to themes/vs15-themes/vs15/scrollbar-up-disabled.png diff --git a/src/stylesheets/vs15/scrollbar-up-hover.png b/themes/vs15-themes/vs15/scrollbar-up-hover.png similarity index 100% rename from src/stylesheets/vs15/scrollbar-up-hover.png rename to themes/vs15-themes/vs15/scrollbar-up-hover.png diff --git a/src/stylesheets/vs15/scrollbar-up.png b/themes/vs15-themes/vs15/scrollbar-up.png similarity index 100% rename from src/stylesheets/vs15/scrollbar-up.png rename to themes/vs15-themes/vs15/scrollbar-up.png diff --git a/src/stylesheets/vs15/sort-asc.png b/themes/vs15-themes/vs15/sort-asc.png similarity index 100% rename from src/stylesheets/vs15/sort-asc.png rename to themes/vs15-themes/vs15/sort-asc.png diff --git a/src/stylesheets/vs15/sort-desc.png b/themes/vs15-themes/vs15/sort-desc.png similarity index 100% rename from src/stylesheets/vs15/sort-desc.png rename to themes/vs15-themes/vs15/sort-desc.png diff --git a/src/stylesheets/vs15/spinner-down.png b/themes/vs15-themes/vs15/spinner-down.png similarity index 100% rename from src/stylesheets/vs15/spinner-down.png rename to themes/vs15-themes/vs15/spinner-down.png diff --git a/src/stylesheets/vs15/spinner-up.png b/themes/vs15-themes/vs15/spinner-up.png similarity index 100% rename from src/stylesheets/vs15/spinner-up.png rename to themes/vs15-themes/vs15/spinner-up.png diff --git a/src/stylesheets/vs15/sub-menu-arrow-hover.png b/themes/vs15-themes/vs15/sub-menu-arrow-hover.png similarity index 100% rename from src/stylesheets/vs15/sub-menu-arrow-hover.png rename to themes/vs15-themes/vs15/sub-menu-arrow-hover.png diff --git a/src/stylesheets/vs15/sub-menu-arrow.png b/themes/vs15-themes/vs15/sub-menu-arrow.png similarity index 100% rename from src/stylesheets/vs15/sub-menu-arrow.png rename to themes/vs15-themes/vs15/sub-menu-arrow.png diff --git a/vcpkg-configuration.json b/vcpkg-configuration.json deleted file mode 100644 index 6a56decd9..000000000 --- a/vcpkg-configuration.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "default-registry": { - "kind": "git", - "repository": "https://github.com/Microsoft/vcpkg", - "baseline": "f61a294e765b257926ae9e9d85f96468a0af74e7" - }, - "registries": [ - { - "kind": "git", - "repository": "https://github.com/Microsoft/vcpkg", - "baseline": "f61a294e765b257926ae9e9d85f96468a0af74e7", - "packages": ["boost*", "boost-*"] - }, - { - "kind": "git", - "repository": "https://github.com/ModOrganizer2/vcpkg-registry", - "baseline": "a1cd2ddbf1afb836419a5f1a9f70d6378fc41df2", - "packages": ["mo2-*", "7zip", "usvfs"] - } - ] -} diff --git a/vcpkg.json b/vcpkg.json index 013a13873..4cbcfa353 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -28,5 +28,26 @@ "usvfs" ] } + }, + "vcpkg-configuration": { + "default-registry": { + "kind": "git", + "repository": "https://github.com/Microsoft/vcpkg", + "baseline": "8ae59b5b1329a51875abc71d528da93d9c3e8972" + }, + "registries": [ + { + "kind": "git", + "repository": "https://github.com/Microsoft/vcpkg", + "baseline": "8ae59b5b1329a51875abc71d528da93d9c3e8972", + "packages": ["boost*", "boost-*"] + }, + { + "kind": "git", + "repository": "https://github.com/ModOrganizer2/vcpkg-registry", + "baseline": "84ff92223433d101738a3c6cef96fa6ae6a6f302", + "packages": ["mo2-*", "7zip", "usvfs"] + } + ] } }