diff --git a/CMakeLists.txt b/CMakeLists.txt index c1c1cf9b9..62cf78e57 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -12,5 +12,6 @@ endif() project(organizer) add_subdirectory(src) +add_subdirectory(themes) install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/dump_running_process.bat DESTINATION bin) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 500b0ca5b..ddf8aa5dc 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -4,7 +4,7 @@ add_executable(organizer) set_target_properties(organizer PROPERTIES OUTPUT_NAME "ModOrganizer") mo2_configure_executable(organizer WARNINGS OFF - EXTRA_TRANSLATIONS ${MO2_SUPER_PATH}/game_gamebryo/src ${MO2_UIBASE_PATH}/src + TRANSLATIONS ON PRIVATE_DEPENDS uibase githubpp bsatk esptk archive usvfs lootcli boost::program_options Qt::WebEngineWidgets Qt::WebSockets) @@ -16,7 +16,6 @@ install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/dlls.manifest.qt5" RENAME dlls.manifest) install(DIRECTORY - "${CMAKE_CURRENT_SOURCE_DIR}/stylesheets" "${CMAKE_CURRENT_SOURCE_DIR}/tutorials" DESTINATION bin) @@ -45,11 +44,11 @@ mo2_add_filter(NAME src/browser GROUPS mo2_add_filter(NAME src/core GROUPS categories archivefiletree + inibakery installationmanager nexusinterface nxmaccessmanager organizercore - plugincontainer apiuseraccount processrunner qdirfiletree @@ -57,6 +56,15 @@ mo2_add_filter(NAME src/core GROUPS uilocker ) +mo2_add_filter(NAME src/extensions GROUPS + thememanager + translationmanager + extensionmanager + extensionwatcher + pluginmanager + proxyqt +) + mo2_add_filter(NAME src/dialogs GROUPS aboutdialog activatemodsdialog @@ -192,6 +200,7 @@ mo2_add_filter(NAME src/profiles GROUPS mo2_add_filter(NAME src/proxies GROUPS downloadmanagerproxy modlistproxy + extensionlistproxy organizerproxy pluginlistproxy proxyutils diff --git a/src/commandline.cpp b/src/commandline.cpp index 9082d710a..2c5453bc6 100644 --- a/src/commandline.cpp +++ b/src/commandline.cpp @@ -827,8 +827,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 c5f51e0ec..bb5f1b999 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) @@ -132,13 +132,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(); }); @@ -160,7 +160,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 fe3c5646f..f6fe0d9c2 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, @@ -80,13 +80,12 @@ class CreateInstanceDialog : public QDialog Paths paths; }; - 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 @@ -175,7 +174,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 c8cec45f2..fe260ca22 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 d9751a526..3f898aadb 100644 --- a/src/createinstancedialogpages.h +++ b/src/createinstancedialogpages.h @@ -113,7 +113,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 567fa99d7..2f19405c5 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_core(core), m_pluginManager(pc), m_parent(parent), ui{mwui->tabWidget, mwui->dataTab, mwui->dataTabRefresh, @@ -25,7 +25,7 @@ DataTab::DataTab(OrganizerCore& core, PluginContainer& pc, QWidget* parent, mwui->dataTabShowFromArchives}, 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 3eb916811..31a481b52 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); @@ -57,7 +56,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 6fb88c3b9..576bdac19 100644 --- a/src/downloadmanager.cpp +++ b/src/downloadmanager.cpp @@ -308,9 +308,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 305c10e3e..c2ee11b02 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..a9a58f718 --- /dev/null +++ b/src/extensionlistproxy.cpp @@ -0,0 +1,52 @@ +#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( + fmt::format("extension '{}' not found", identifier.toStdString())); +} + +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..47a394f69 --- /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..e894abe98 --- /dev/null +++ b/src/extensionmanager.cpp @@ -0,0 +1,72 @@ +#include "extensionmanager.h" + +#include + +using namespace MOBase; +namespace fs = std::filesystem; + +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 correctly 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 +{ + // TODO + return 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..5ecc05595 --- /dev/null +++ b/src/extensionmanager.h @@ -0,0 +1,76 @@ +#ifndef EXTENSIONMANAGER_H +#define EXTENSIONMANAGER_H + +#include + +#include +#include +#include + +#include + +#include "extensionwatcher.h" + +class ExtensionManager +{ +public: + // 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: + 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/extensionwatcher.h b/src/extensionwatcher.h new file mode 100644 index 000000000..5dd47259b --- /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 f043448f4..d999f20b2 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/inibakery.cpp b/src/inibakery.cpp new file mode 100644 index 000000000..268692c4b --- /dev/null +++ b/src/inibakery.cpp @@ -0,0 +1,52 @@ +#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 IPluginGame* game = m_core.managedGame(); + + LocalSavegames* savegames = game->feature(); + if (savegames != nullptr) { + savegames->prepareProfile(m_core.currentProfile()); + } + + BSAInvalidation* invalidation = game->feature(); + if (invalidation != nullptr) { + 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..2707085aa --- /dev/null +++ b/src/inibakery.h @@ -0,0 +1,24 @@ +#ifndef INIBAKERY_H +#define INIBAKERY_H + +#include + +#include + +class OrganizerCore; + +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 8c2ce78d3..da1ee1bf8 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() @@ -728,7 +728,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) { @@ -740,7 +740,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; } @@ -887,8 +887,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()); @@ -902,9 +902,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()); } } @@ -913,9 +913,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 e8e975152..cdfbf5b25 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 df4b0078d..f2f04e96d 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 @@ -607,15 +608,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); @@ -699,9 +700,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; + ec.loadExtensions(QDir(QCoreApplication::applicationDirPath() + "/extensions") + .filesystemAbsolutePath()); + + PluginManager pc(ec, nullptr); pc.loadPlugins(); if (m.hasAnyInstances()) { @@ -740,7 +745,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); @@ -772,7 +777,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); @@ -798,7 +803,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..d28eff4ab 100644 --- a/src/instancemanagerdialog.cpp +++ b/src/instancemanagerdialog.cpp @@ -2,7 +2,6 @@ #include "createinstancedialog.h" #include "filesystemutilities.h" #include "instancemanager.h" -#include "plugincontainer.h" #include "selectiondialog.h" #include "settings.h" #include "shared/appconfig.h" @@ -17,7 +16,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 +145,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 +312,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 +340,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 +455,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 ebe7da98d..fe19895d4 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -230,12 +230,14 @@ void setFilterShortcuts(QWidget* widget, QLineEdit* edit) } MainWindow::MainWindow(Settings& settings, OrganizerCore& organizerCore, - PluginContainer& pluginContainer, QWidget* parent) + 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_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 +264,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); { @@ -307,7 +309,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, [&] { @@ -376,8 +378,9 @@ MainWindow::MainWindow(Settings& settings, OrganizerCore& organizerCore, "You will probably have to use a third-party tool.")); } - connect(&m_PluginContainer, SIGNAL(diagnosisUpdate()), this, - SLOT(scheduleCheckForProblems())); + connect(&m_PluginManager, &PluginManager::diagnosePluginInvalidated, [this] { + scheduleCheckForProblems(); + }); connect(&m_OrganizerCore, &OrganizerCore::directoryStructureReady, this, &MainWindow::onDirectoryStructureChanged); @@ -387,10 +390,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, @@ -433,21 +436,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, @@ -501,9 +504,6 @@ MainWindow::MainWindow(Settings& settings, OrganizerCore& organizerCore, [=](auto&& message) { showMessage(message); }); - for (const QString& fileName : m_PluginContainer.pluginFileNames()) { - installTranslator(QFileInfo(fileName).baseName()); - } updateModPageMenu(); @@ -1032,9 +1032,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 +1232,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(); @@ -1276,7 +1276,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 // @@ -1443,7 +1443,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), @@ -1454,7 +1454,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()); @@ -1546,7 +1546,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), @@ -1558,7 +1558,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()); @@ -1864,7 +1864,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()); @@ -2642,7 +2642,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_PluginManager, m_ThemeManager, m_TranslationManager, settings, + this); dialog.exec(); auto e = dialog.exitNeeded(); @@ -2754,36 +2755,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_TranslationManager.load(newLanguage.toStdString()); - m_CurrentLanguage = newLanguage; - - installTranslator("qt"); - installTranslator("qtbase"); - installTranslator(ToQString(AppConfig::translationPrefix())); - for (const QString& fileName : m_PluginContainer.pluginFileNames()) { - installTranslator(QFileInfo(fileName).baseName()); - } ui->retranslateUi(this); log::debug("loaded language {}", newLanguage); @@ -2824,7 +2799,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(); @@ -2993,7 +2968,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; @@ -3049,7 +3024,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; @@ -3192,7 +3167,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; @@ -3292,7 +3267,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(); } @@ -3364,7 +3339,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; @@ -3510,7 +3485,7 @@ void MainWindow::on_actionNotifications_triggered() future.waitForFinished(); - ProblemsDialog problems(m_PluginContainer, this); + ProblemsDialog problems(m_PluginManager, this); problems.exec(); scheduleCheckForProblems(); @@ -3518,18 +3493,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 8cd55c901..58d0869f1 100644 --- a/src/mainwindow.h +++ b/src/mainwindow.h @@ -29,8 +29,9 @@ along with Mod Organizer. If not, see . #include "modinfo.h" #include "modlistbypriorityproxy.h" #include "modlistsortproxy.h" -#include "plugincontainer.h" //class PluginContainer; #include "shared/fileregisterfwd.h" +#include "thememanager.h" +#include "translationmanager.h" #include "tutorialcontrol.h" #include @@ -125,7 +126,8 @@ class MainWindow : public QMainWindow, public IUserInterface public: explicit MainWindow(Settings& settings, OrganizerCore& organizerCore, - PluginContainer& pluginContainer, QWidget* parent = 0); + PluginManager& pluginManager, ThemeManager& themeManager, + TranslationManager& translationManager, QWidget* parent = 0); ~MainWindow(); void processUpdates(); @@ -138,8 +140,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; @@ -165,7 +165,7 @@ public slots: /** * @brief emitted when the selected style changes */ - void styleChanged(const QString& styleFile); + void themeChanged(const QString& themeIdentifier); void checkForProblemsDone(); @@ -294,10 +294,9 @@ private slots: QTime m_StartTime; OrganizerCore& m_OrganizerCore; - PluginContainer& m_PluginContainer; - - QString m_CurrentLanguage; - std::vector m_Translators; + PluginManager& m_PluginManager; + ThemeManager& m_ThemeManager; + TranslationManager& m_TranslationManager; std::unique_ptr m_IntegratedBrowser; @@ -339,7 +338,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 e131d3d96..189c29970 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 @@ -56,55 +58,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: // @@ -148,14 +101,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 = style()->objectName(); - setStyle(new ProxyStyle(style())); addDllsToPath(); } @@ -267,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_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 @@ -329,25 +285,26 @@ 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_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); @@ -464,7 +421,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); @@ -516,31 +473,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 { @@ -558,21 +490,6 @@ bool MOApplication::notify(QObject* receiver, QEvent* event) } } -void MOApplication::updateStyle(const QString& fileName) -{ - if (QStyleFactory::keys().contains(fileName)) { - setStyleSheet(""); - setStyle(new ProxyStyle(QStyleFactory::create(fileName))); - } else { - setStyle(new ProxyStyle(QStyleFactory::create(m_defaultStyle))); - if (QFile::exists(fileName)) { - 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 1d30ac952..fde852fd8 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; } @@ -295,7 +295,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; @@ -314,7 +314,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 9895d44c6..30fa0ec23 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 d0e8d1ad2..35cfa8ab9 100644 --- a/src/modinfodialog.cpp +++ b/src/modinfodialog.cpp @@ -27,7 +27,6 @@ along with Mod Organizer. If not, see . #include "modinfodialogtextfiles.h" #include "modlistview.h" #include "organizercore.h" -#include "plugincontainer.h" #include "shared/directoryentry.h" #include "shared/filesorigin.h" #include "ui_modinfodialog.h" @@ -39,7 +38,7 @@ 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) { if (isArchive) { @@ -47,7 +46,7 @@ bool canPreviewFile(const PluginContainer& pluginContainer, bool isArchive, } const auto ext = QFileInfo(filename).suffix().toLower(); - return pluginContainer.previewGenerator().previewSupported(ext); + return pluginManager.previewGenerator().previewSupported(ext); } bool isExecutableFilename(const QString& filename) @@ -168,11 +167,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); @@ -222,7 +221,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 e48a34d33..dc5f1e410 100644 --- a/src/modinfodialogimages.cpp +++ b/src/modinfodialogimages.cpp @@ -258,7 +258,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())) 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 566361458..42b891465 100644 --- a/src/modinfodialognexus.cpp +++ b/src/modinfodialognexus.cpp @@ -96,7 +96,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()); @@ -339,7 +339,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 4c1004e6b..cc3fd6011 100644 --- a/src/modinforegular.cpp +++ b/src/modinforegular.cpp @@ -4,7 +4,6 @@ #include "messagedialog.h" #include "moddatacontent.h" #include "organizercore.h" -#include "plugincontainer.h" #include "report.h" #include "settings.h" #include @@ -34,8 +33,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 diff --git a/src/modlist.cpp b/src/modlist.cpp index 7a9513695..e93b0d919 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/modlistcontextmenu.cpp b/src/modlistcontextmenu.cpp index e88cadc8a..3066bf1fc 100644 --- a/src/modlistcontextmenu.cpp +++ b/src/modlistcontextmenu.cpp @@ -145,19 +145,19 @@ bool ModListChangeCategoryMenu::populate(QMenu* menu, CategoryFactory& factory, targetMenu = menu->addMenu(factory.getCategoryName(i).replace('&', "&&")); } - int id = factory.getCategoryID(i); - QScopedPointer checkBox(new QCheckBox(targetMenu)); - bool enabled = categories.find(id) != categories.end(); + int id = factory.getCategoryID(i); + QCheckBox* checkBox = new QCheckBox(targetMenu); + bool enabled = categories.find(id) != categories.end(); checkBox->setText(factory.getCategoryName(i).replace('&', "&&")); if (enabled) { childEnabled = true; } checkBox->setChecked(enabled ? Qt::Checked : Qt::Unchecked); - QScopedPointer checkableAction(new QWidgetAction(targetMenu)); - checkableAction->setDefaultWidget(checkBox.take()); + QWidgetAction* checkableAction = new QWidgetAction(targetMenu); + checkableAction->setDefaultWidget(checkBox); checkableAction->setData(id); - targetMenu->addAction(checkableAction.take()); + targetMenu->addAction(checkableAction); if (factory.hasChildren(i)) { if (populate(targetMenu, factory, mod, factory.getCategoryID(i)) || enabled) { diff --git a/src/modlistview.cpp b/src/modlistview.cpp index 73bbd33df..bd4442bd3 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 a6f0f07ee..ad6caee4e 100644 --- a/src/modlistviewactions.cpp +++ b/src/modlistviewactions.cpp @@ -224,7 +224,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()); @@ -527,7 +527,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 2c536b144..8d5780e49 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) {} @@ -252,7 +252,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; @@ -417,7 +417,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())); @@ -438,9 +438,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, @@ -763,7 +763,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 331ce55d4..2a40df5db 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 @@ -522,7 +522,7 @@ class NexusInterface : public QObject */ bool isModURL(int modID, QString const& url) const; - void setPluginContainer(PluginContainer* pluginContainer); + void setPluginManager(PluginManager* pluginManager); signals: @@ -638,7 +638,7 @@ private slots: std::list m_ActiveRequest; QQueue m_RequestQueue; MOBase::VersionInfo m_MOVersion; - PluginContainer* m_PluginContainer; + PluginManager* m_PluginManager; APIUserAccount m_User; }; diff --git a/src/organizercore.cpp b/src/organizercore.cpp index e7b0c748f..99c846156 100644 --- a/src/organizercore.cpp +++ b/src/organizercore.cpp @@ -17,7 +17,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" @@ -70,6 +70,7 @@ #include #include +#include "inibakery.h" #include "organizerproxy.h" using namespace MOShared; @@ -88,9 +89,9 @@ QStringList toStringList(InputIterator current, InputIterator end) } OrganizerCore::OrganizerCore(Settings& settings) - : m_UserInterface(nullptr), m_PluginContainer(nullptr), m_CurrentProfile(nullptr), + : m_UserInterface(nullptr), m_PluginManager(nullptr), m_CurrentProfile(nullptr), m_Settings(settings), m_Updater(&NexusInterface::instance()), - m_ModList(m_PluginContainer, this), m_PluginList(*this), + m_ModList(m_PluginManager, this), m_PluginList(*this), m_DirectoryRefresher(new DirectoryRefresher(settings.refreshThreadCount())), m_DirectoryStructure(new DirectoryEntry(L"data", nullptr, 0)), m_VirtualFileTree([this]() { @@ -100,6 +101,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); @@ -210,7 +214,7 @@ void OrganizerCore::storeSettings() void OrganizerCore::updateExecutablesList() { - if (m_PluginContainer == nullptr) { + if (m_PluginManager == nullptr) { log::error("can't update executables list now"); return; } @@ -253,23 +257,23 @@ 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); }); } @@ -598,7 +602,7 @@ void OrganizerCore::setCurrentProfile(const QString& profileName) MOBase::IModRepositoryBridge* OrganizerCore::createNexusBridge() const { - return new NexusBridge(m_PluginContainer); + return new NexusBridge(); } QString OrganizerCore::profileName() const @@ -646,7 +650,7 @@ MOBase::VersionInfo OrganizerCore::appVersion() 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; @@ -943,7 +947,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())); } @@ -1042,7 +1046,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 { @@ -1110,7 +1114,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; @@ -1403,11 +1407,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(); @@ -1453,9 +1457,9 @@ void OrganizerCore::requestDownload(const QUrl& url, QNetworkReply* reply) } } -PluginContainer& OrganizerCore::pluginContainer() const +PluginManager& OrganizerCore::pluginManager() const { - return *m_PluginContainer; + return *m_PluginManager; } IPluginGame const* OrganizerCore::managedGame() const @@ -1465,7 +1469,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() @@ -2034,10 +2038,17 @@ std::vector OrganizerCore::fileMapping(const QString& profileName, result.insert(result.end(), {QDir::toNativeSeparators(m_Settings.paths().overwrite()), dataPath, 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()); @@ -2046,47 +2057,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 daeb7a7f3..7cf9861d4 100644 --- a/src/organizercore.h +++ b/src/organizercore.h @@ -35,11 +35,12 @@ #include #include +class IniBakery; class ModListSortProxy; class PluginListSortProxy; class Profile; class IUserInterface; -class PluginContainer; +class PluginManager; class DirectoryRefresher; namespace MOBase @@ -217,7 +218,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); @@ -245,9 +246,9 @@ class OrganizerCore : public QObject, public MOBase::IPluginDiagnose MOBase::VersionInfo getVersion() const { return m_Updater.getVersion(); } - // return the plugin container + // return the plugin manager // - PluginContainer& pluginContainer() const; + PluginManager& pluginManager() const; MOBase::IPluginGame const* managedGame() const; @@ -465,11 +466,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 directory_refreshed(); @@ -485,7 +481,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 a47e8798a..63bac637f 100644 --- a/src/organizerproxy.cpp +++ b/src/organizerproxy.cpp @@ -1,11 +1,13 @@ #include "organizerproxy.h" #include "downloadmanagerproxy.h" +#include "extensionlistproxy.h" +#include "extensionmanager.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" @@ -18,11 +20,13 @@ 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())) @@ -78,7 +82,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 @@ -131,14 +135,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 771d09810..0773ab64f 100644 --- a/src/organizerproxy.h +++ b/src/organizerproxy.h @@ -8,17 +8,19 @@ #include "organizercore.h" -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: @@ -87,7 +89,9 @@ class OrganizerProxy : public MOBase::IOrganizer virtual 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, @@ -110,7 +114,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, @@ -127,7 +131,7 @@ class OrganizerProxy : public MOBase::IOrganizer private: OrganizerCore* m_Proxied; - PluginContainer* m_PluginContainer; + PluginManager* m_PluginManager; MOBase::IPlugin* m_Plugin; @@ -145,6 +149,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; }; diff --git a/src/plugincontainer.cpp b/src/plugincontainer.cpp deleted file mode 100644 index 157d33995..000000000 --- a/src/plugincontainer.cpp +++ /dev/null @@ -1,1249 +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_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; -} - -IPlugin* PluginContainer::managedGame() const -{ - // TODO: This const_cast is safe but ugly. Most methods require a IPlugin*, so - // returning a const-version if painful. This should be fixed by making methods accept - // a const IPlugin* instead, but there are a few tricks with qobject_cast and const. - return const_cast(m_Organizer->managedGame()); -} - -bool PluginContainer::isEnabled(IPlugin* plugin) const -{ - // Check if it's a game plugin: - 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()) { - setEnabled( - p, false, - false); // No need to "recurse" here since requiredFor already does it. - } - } - - // Always disable/enable child plugins: - for (auto* p : requirements(plugin).children()) { - // "Child" plugin should have no dependencies. - setEnabled(p, enable, false); - } - - m_Organizer->setPersistent(plugin->name(), "enabled", enable, true); - - 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; - } -} - -const PreviewGenerator& PluginContainer::previewGenerator() const -{ - return m_PreviewGenerator; -} - -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 d5ceca723..000000000 --- a/src/plugincontainer.h +++ /dev/null @@ -1,495 +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 - -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::IPlugin* 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 preview generator. - */ - const PreviewGenerator& previewGenerator() const; - - /** - * @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; - - 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..785aee24d --- /dev/null +++ b/src/pluginmanager.cpp @@ -0,0 +1,739 @@ +#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_loaders = makeLoaders(); +} + +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 const_cast(m_core->managedGame()); +} + +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(); + } + + // check the master of the group + const auto& d = details(plugin); + if (d.master() && d.master() != plugin) { + return isEnabled(d.master()); + } + + 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().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([&, 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; + } + + // find the best interface + auto it = std::min_element(std::begin(objectGroup), std::end(objectGroup), + [&](auto const& lhs, auto const& rhs) { + return isBetterInterface(lhs, rhs); + }); + IPlugin* master = qobject_cast(*it); + + // register plugins in the group + for (auto* object : objectGroup) { + IPlugin* plugin = registerPlugin(extension, object, objectGroup); + + if (plugin) { + m_details.at(plugin).m_master = master; + } + } + } + + // TODO: move this elsewhere, e.g., in core + if (m_core) { + bf::at_key(m_plugins).push_back(m_core); + m_core->connectPlugins(this); + } + 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()); + } + + m_core->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 + // + 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() +{ + if (m_core) { + // 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_core->settings().plugins().clearPlugins(); + } + + 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()); + } + } + } +} + +const PreviewGenerator& PluginManager::previewGenerator() const +{ + return *m_previews; +} + +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..2b0e9836e --- /dev/null +++ b/src/pluginmanager.h @@ -0,0 +1,350 @@ +#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 "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 "master" of the group this plugin belongs to + // + MOBase::IPlugin* master() const { return m_master; } + + // 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; + MOBase::IPlugin* m_master; + 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 preview generator + // + const PreviewGenerator& previewGenerator() const; + +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; + + // 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 ba73c0650..a4a24b41a 100644 --- a/src/previewgenerator.cpp +++ b/src/previewgenerator.cpp @@ -25,19 +25,19 @@ 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; } bool PreviewGenerator::previewSupported(const QString& fileExtension) const { - auto& previews = m_PluginContainer.plugins(); + auto& previews = m_PluginManager.plugins(); for (auto* preview : previews) { if (preview->supportedExtensions().contains(fileExtension)) { return true; @@ -49,9 +49,9 @@ bool PreviewGenerator::previewSupported(const QString& fileExtension) const 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); } diff --git a/src/previewgenerator.h b/src/previewgenerator.h index dd0d2cd4f..8a045857e 100644 --- a/src/previewgenerator.h +++ b/src/previewgenerator.h @@ -26,19 +26,19 @@ 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; QWidget* genPreview(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 f6b23a100..1a7a0d3d0 100644 --- a/src/problemsdialog.cpp +++ b/src/problemsdialog.cpp @@ -7,12 +7,10 @@ #include #include -#include "plugincontainer.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); @@ -41,13 +39,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..80c7fb95b --- /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 7762c9004..22207fec8 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" @@ -81,9 +81,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 cc68ad275..8800e479f 100644 --- a/src/selfupdater.h +++ b/src/selfupdater.h @@ -27,7 +27,7 @@ along with Mod Organizer. If not, see . class Archive; class NexusInterface; -class PluginContainer; +class PluginManager; namespace MOBase { class IPluginGame; @@ -82,7 +82,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 fc9985f47..ecc526568 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -2058,12 +2058,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 6a11d6620..d92cbbdd4 100644 --- a/src/settings.h +++ b/src/settings.h @@ -584,8 +584,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 // @@ -872,7 +872,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; diff --git a/src/settingsdialog.cpp b/src/settingsdialog.cpp index 425a1cb80..1db59117e 100644 --- a/src/settingsdialog.cpp +++ b/src/settingsdialog.cpp @@ -30,16 +30,19 @@ along with Mod Organizer. If not, see . using namespace MOBase; -SettingsDialog::SettingsDialog(PluginContainer* pluginContainer, Settings& settings, - QWidget* parent) +SettingsDialog::SettingsDialog(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), m_pluginManager(&pluginManager) { 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 +50,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 PluginsSettingsTab(settings, *m_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..25a6bf9d9 100644 --- a/src/settingsdialog.h +++ b/src/settingsdialog.h @@ -23,9 +23,12 @@ along with Mod Organizer. If not, see . #include "shared/util.h" #include "tutorabledialog.h" -class PluginContainer; +class PluginManager; class Settings; class SettingsDialog; +class ThemeManager; +class TranslationManager; + namespace Ui { class SettingsDialog; @@ -63,8 +66,10 @@ class SettingsDialog : public MOBase::TutorableDialog friend class SettingsTab; public: - explicit SettingsDialog(PluginContainer* pluginContainer, Settings& settings, - QWidget* parent = 0); + explicit SettingsDialog(PluginManager& pluginManager, + ThemeManager const& themeManager, + TranslationManager const& translationManager, + Settings& settings, QWidget* parent = 0); ~SettingsDialog(); @@ -74,7 +79,6 @@ class SettingsDialog : public MOBase::TutorableDialog */ QString getColoredButtonStyleSheet() const; - PluginContainer* pluginContainer(); QWidget* parentWidgetForDialogs(); void setExitNeeded(ExitFlags e); @@ -90,7 +94,7 @@ public slots: Settings& m_settings; std::vector> m_tabs; ExitFlags m_exit; - PluginContainer* m_pluginContainer; + PluginManager* m_pluginManager; }; #endif // SETTINGSDIALOG_H diff --git a/src/settingsdialoggeneral.cpp b/src/settingsdialoggeneral.cpp index 95655ac86..7afdb145e 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 @@ -73,54 +75,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.nativeCountryName()); - - 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 619381905..ec1744b43 100644 --- a/src/settingsdialoggeneral.h +++ b/src/settingsdialoggeneral.h @@ -3,16 +3,18 @@ #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 index db00ca081..1a6d0c90d 100644 --- a/src/settingsdialogplugins.cpp +++ b/src/settingsdialogplugins.cpp @@ -3,69 +3,49 @@ #include "ui_settingsdialog.h" #include -#include "disableproxyplugindialog.h" #include "organizercore.h" -#include "plugincontainer.h" +#include "pluginmanager.h" using namespace MOBase; -PluginsSettingsTab::PluginsSettingsTab(Settings& s, PluginContainer* pluginContainer, +struct PluginExtensionComparator +{ + bool operator()(const PluginExtension* lhs, const PluginExtension* rhs) const + { + return lhs->metadata().name().compare(rhs->metadata().name(), Qt::CaseInsensitive); + } +}; + +PluginsSettingsTab::PluginsSettingsTab(Settings& s, PluginManager& pluginManager, SettingsDialog& d) - : SettingsTab(s, d), m_pluginContainer(pluginContainer) + : SettingsTab(s, d), m_pluginManager(&pluginManager) { 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()); - } + std::map, PluginExtensionComparator> + pluginsPerExtension; - handledNames.insert(plugin->name()); + for (IPlugin* plugin : pluginManager.plugins()) { + pluginsPerExtension[&pluginManager.details(plugin).extension()].push_back(plugin); } - for (auto& [k, item] : topItems) { - if (item->childCount() == 0) { - item->setHidden(true); + 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()); } } @@ -82,9 +62,6 @@ PluginsSettingsTab::PluginsSettingsTab(Settings& s, PluginContainer* pluginConta [&](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); @@ -95,8 +72,8 @@ PluginsSettingsTab::PluginsSettingsTab(Settings& s, PluginContainer* pluginConta filterPluginList(); }); - updateListItems(); - filterPluginList(); + // updateListItems(); + // filterPluginList(); } void PluginsSettingsTab::updateListItems() @@ -107,8 +84,8 @@ void PluginsSettingsTab::updateListItems() auto* item = topLevelItem->child(j); auto* plugin = this->plugin(item); - bool inactive = !m_pluginContainer->implementInterface(plugin) && - !m_pluginContainer->isEnabled(plugin); + bool inactive = !m_pluginManager->implementInterface(plugin) && + !m_pluginManager->isEnabled(plugin); auto font = item->font(0); font.setItalic(inactive); @@ -138,11 +115,6 @@ void PluginsSettingsTab::filterPluginList() 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; @@ -184,14 +156,14 @@ IPlugin* PluginsSettingsTab::plugin(QTreeWidgetItem* pluginItem) const 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()); - } - } + // 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; @@ -209,77 +181,6 @@ 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) { @@ -298,21 +199,15 @@ void PluginsSettingsTab::on_pluginsList_currentItemChanged(QTreeWidgetItem* curr // Checkbox, do not show for children or game plugins, disable // if the plugin cannot be enabled. ui->enabledCheckbox->setVisible( - !m_pluginContainer->implementInterface(plugin) && + !m_pluginManager->implementInterface(plugin) && plugin->master().isEmpty()); - bool enabled = m_pluginContainer->isEnabled(plugin); - auto& requirements = m_pluginContainer->requirements(plugin); + bool enabled = m_pluginManager->isEnabled(plugin); + auto& requirements = m_pluginManager->details(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()) { + if (enabled || problems.empty()) { ui->enabledCheckbox->setDisabled(false); ui->enabledCheckbox->setToolTip(""); ui->enabledCheckbox->setChecked(enabled); diff --git a/src/settingsdialogplugins.h b/src/settingsdialogplugins.h index 416f457ae..218bd7469 100644 --- a/src/settingsdialogplugins.h +++ b/src/settingsdialogplugins.h @@ -9,7 +9,7 @@ class PluginsSettingsTab : public SettingsTab { public: - PluginsSettingsTab(Settings& settings, PluginContainer* pluginContainer, + PluginsSettingsTab(Settings& settings, PluginManager& pluginManager, SettingsDialog& dialog); void update(); @@ -18,7 +18,6 @@ class PluginsSettingsTab : public SettingsTab private: void on_pluginsList_currentItemChanged(QTreeWidgetItem* current, QTreeWidgetItem* previous); - void on_checkboxEnabled_clicked(bool checked); void deleteBlacklistItem(); void storeSettings(QTreeWidgetItem* pluginItem); @@ -49,7 +48,7 @@ private slots: }; private: - PluginContainer* m_pluginContainer; + PluginManager* m_pluginManager; MOBase::FilterWidget m_filter; }; 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/stylesheets/Transparent-Style/Background-skyrim.png b/src/stylesheets/Transparent-Style/Background-skyrim.png deleted file mode 100644 index 78a82f080..000000000 Binary files a/src/stylesheets/Transparent-Style/Background-skyrim.png and /dev/null differ diff --git a/src/stylesheets/Transparent-Style/Fallout-BOS.png b/src/stylesheets/Transparent-Style/Fallout-BOS.png deleted file mode 100644 index c8585355a..000000000 Binary files a/src/stylesheets/Transparent-Style/Fallout-BOS.png and /dev/null differ diff --git a/src/stylesheets/Transparent-Style/Green/Vault-101-item-end.png b/src/stylesheets/Transparent-Style/Green/Vault-101-item-end.png deleted file mode 100644 index 5b86f7284..000000000 Binary files a/src/stylesheets/Transparent-Style/Green/Vault-101-item-end.png and /dev/null differ diff --git a/src/stylesheets/Transparent-Style/Green/Vault-101-item-line-v.png b/src/stylesheets/Transparent-Style/Green/Vault-101-item-line-v.png deleted file mode 100644 index 17c37a0b2..000000000 Binary files a/src/stylesheets/Transparent-Style/Green/Vault-101-item-line-v.png and /dev/null differ diff --git a/src/stylesheets/Transparent-Style/Green/Vault-101-itemB-close-last.png b/src/stylesheets/Transparent-Style/Green/Vault-101-itemB-close-last.png deleted file mode 100644 index 5582f3118..000000000 Binary files a/src/stylesheets/Transparent-Style/Green/Vault-101-itemB-close-last.png and /dev/null differ diff --git a/src/stylesheets/Transparent-Style/Green/Vault-101-itemB-end.png b/src/stylesheets/Transparent-Style/Green/Vault-101-itemB-end.png deleted file mode 100644 index d2044a867..000000000 Binary files a/src/stylesheets/Transparent-Style/Green/Vault-101-itemB-end.png and /dev/null differ diff --git a/src/stylesheets/Transparent-Style/Green/Vault-101-itemB-line.png b/src/stylesheets/Transparent-Style/Green/Vault-101-itemB-line.png deleted file mode 100644 index f2b49eba6..000000000 Binary files a/src/stylesheets/Transparent-Style/Green/Vault-101-itemB-line.png and /dev/null differ diff --git a/src/stylesheets/Transparent-Style/Green/Vault-101-itemB-open-last.png b/src/stylesheets/Transparent-Style/Green/Vault-101-itemB-open-last.png deleted file mode 100644 index 01b37ebcb..000000000 Binary files a/src/stylesheets/Transparent-Style/Green/Vault-101-itemB-open-last.png and /dev/null differ diff --git a/src/stylesheets/Transparent-Style/Green/Vault-101-itemb-close.png b/src/stylesheets/Transparent-Style/Green/Vault-101-itemb-close.png deleted file mode 100644 index f7011114e..000000000 Binary files a/src/stylesheets/Transparent-Style/Green/Vault-101-itemb-close.png and /dev/null differ diff --git a/src/stylesheets/Transparent-Style/Green/Vault-101-itemb-open.png b/src/stylesheets/Transparent-Style/Green/Vault-101-itemb-open.png deleted file mode 100644 index c8b6c4d48..000000000 Binary files a/src/stylesheets/Transparent-Style/Green/Vault-101-itemb-open.png and /dev/null differ diff --git a/src/stylesheets/Transparent-Style/Green/Vault-101-vault-boy.png b/src/stylesheets/Transparent-Style/Green/Vault-101-vault-boy.png deleted file mode 100644 index dda4af385..000000000 Binary files a/src/stylesheets/Transparent-Style/Green/Vault-101-vault-boy.png and /dev/null differ diff --git a/src/stylesheets/Transparent-Style/Green/arrow-down.png b/src/stylesheets/Transparent-Style/Green/arrow-down.png deleted file mode 100644 index 48be99c2c..000000000 Binary files a/src/stylesheets/Transparent-Style/Green/arrow-down.png and /dev/null differ diff --git a/src/stylesheets/Transparent-Style/Green/arrow-left.png b/src/stylesheets/Transparent-Style/Green/arrow-left.png deleted file mode 100644 index 74a96795e..000000000 Binary files a/src/stylesheets/Transparent-Style/Green/arrow-left.png and /dev/null differ diff --git a/src/stylesheets/Transparent-Style/Green/arrow-right.png b/src/stylesheets/Transparent-Style/Green/arrow-right.png deleted file mode 100644 index 144605c49..000000000 Binary files a/src/stylesheets/Transparent-Style/Green/arrow-right.png and /dev/null differ diff --git a/src/stylesheets/Transparent-Style/Green/arrow-up.png b/src/stylesheets/Transparent-Style/Green/arrow-up.png deleted file mode 100644 index 191c83ce5..000000000 Binary files a/src/stylesheets/Transparent-Style/Green/arrow-up.png and /dev/null differ diff --git a/src/stylesheets/Transparent-Style/Green/border-image.png b/src/stylesheets/Transparent-Style/Green/border-image.png deleted file mode 100644 index a1f98702c..000000000 Binary files a/src/stylesheets/Transparent-Style/Green/border-image.png and /dev/null differ diff --git a/src/stylesheets/Transparent-Style/Green/checkbox-checked-disabled.png b/src/stylesheets/Transparent-Style/Green/checkbox-checked-disabled.png deleted file mode 100644 index 098158d0e..000000000 Binary files a/src/stylesheets/Transparent-Style/Green/checkbox-checked-disabled.png and /dev/null differ diff --git a/src/stylesheets/Transparent-Style/Green/checkbox-checked-hover.png b/src/stylesheets/Transparent-Style/Green/checkbox-checked-hover.png deleted file mode 100644 index 0e1f8be19..000000000 Binary files a/src/stylesheets/Transparent-Style/Green/checkbox-checked-hover.png and /dev/null differ diff --git a/src/stylesheets/Transparent-Style/Green/checkbox-checked-hover1.png b/src/stylesheets/Transparent-Style/Green/checkbox-checked-hover1.png deleted file mode 100644 index 413cb2af6..000000000 Binary files a/src/stylesheets/Transparent-Style/Green/checkbox-checked-hover1.png and /dev/null differ diff --git a/src/stylesheets/Transparent-Style/Green/checkbox-checked-hover2.png b/src/stylesheets/Transparent-Style/Green/checkbox-checked-hover2.png deleted file mode 100644 index bea563f08..000000000 Binary files a/src/stylesheets/Transparent-Style/Green/checkbox-checked-hover2.png and /dev/null differ diff --git a/src/stylesheets/Transparent-Style/Green/checkbox-checked.png b/src/stylesheets/Transparent-Style/Green/checkbox-checked.png deleted file mode 100644 index 7d0e94c98..000000000 Binary files a/src/stylesheets/Transparent-Style/Green/checkbox-checked.png and /dev/null differ diff --git a/src/stylesheets/Transparent-Style/Green/checkbox-hover.png b/src/stylesheets/Transparent-Style/Green/checkbox-hover.png deleted file mode 100644 index 7e226e41d..000000000 Binary files a/src/stylesheets/Transparent-Style/Green/checkbox-hover.png and /dev/null differ diff --git a/src/stylesheets/Transparent-Style/Green/checkbox.png b/src/stylesheets/Transparent-Style/Green/checkbox.png deleted file mode 100644 index 74b446c1b..000000000 Binary files a/src/stylesheets/Transparent-Style/Green/checkbox.png and /dev/null differ diff --git a/src/stylesheets/Transparent-Style/Green/context-menu-separator.png b/src/stylesheets/Transparent-Style/Green/context-menu-separator.png deleted file mode 100644 index 602483134..000000000 Binary files a/src/stylesheets/Transparent-Style/Green/context-menu-separator.png and /dev/null differ diff --git a/src/stylesheets/Transparent-Style/Green/radio-checked.png b/src/stylesheets/Transparent-Style/Green/radio-checked.png deleted file mode 100644 index 5fd9f6f71..000000000 Binary files a/src/stylesheets/Transparent-Style/Green/radio-checked.png and /dev/null differ diff --git a/src/stylesheets/Transparent-Style/Green/radio-hover.png b/src/stylesheets/Transparent-Style/Green/radio-hover.png deleted file mode 100644 index 42b31aee3..000000000 Binary files a/src/stylesheets/Transparent-Style/Green/radio-hover.png and /dev/null differ diff --git a/src/stylesheets/Transparent-Style/Green/radio.png b/src/stylesheets/Transparent-Style/Green/radio.png deleted file mode 100644 index 55d09b4d8..000000000 Binary files a/src/stylesheets/Transparent-Style/Green/radio.png and /dev/null differ diff --git a/src/stylesheets/Transparent-Style/Green/vault-101.png b/src/stylesheets/Transparent-Style/Green/vault-101.png deleted file mode 100644 index 8c3110422..000000000 Binary files a/src/stylesheets/Transparent-Style/Green/vault-101.png and /dev/null differ diff --git a/src/stylesheets/Transparent-Style/Green/vault-tec-executables.png b/src/stylesheets/Transparent-Style/Green/vault-tec-executables.png deleted file mode 100644 index 30f6eba0b..000000000 Binary files a/src/stylesheets/Transparent-Style/Green/vault-tec-executables.png and /dev/null differ diff --git a/src/stylesheets/Transparent-Style/Green/vault-tec-full.png b/src/stylesheets/Transparent-Style/Green/vault-tec-full.png deleted file mode 100644 index f79213c9a..000000000 Binary files a/src/stylesheets/Transparent-Style/Green/vault-tec-full.png and /dev/null differ diff --git a/src/stylesheets/Transparent-Style/Green/vault-tec-overwrite.png b/src/stylesheets/Transparent-Style/Green/vault-tec-overwrite.png deleted file mode 100644 index a61482ffb..000000000 Binary files a/src/stylesheets/Transparent-Style/Green/vault-tec-overwrite.png and /dev/null differ diff --git a/src/stylesheets/Transparent-Style/Green/vault-tec-profiles.png b/src/stylesheets/Transparent-Style/Green/vault-tec-profiles.png deleted file mode 100644 index 5a121b062..000000000 Binary files a/src/stylesheets/Transparent-Style/Green/vault-tec-profiles.png and /dev/null differ diff --git a/src/stylesheets/Transparent-Style/Green/vault-tec-settings.png b/src/stylesheets/Transparent-Style/Green/vault-tec-settings.png deleted file mode 100644 index d6a43c852..000000000 Binary files a/src/stylesheets/Transparent-Style/Green/vault-tec-settings.png and /dev/null differ diff --git a/src/stylesheets/Transparent-Style/Green/vault-tec-small.png b/src/stylesheets/Transparent-Style/Green/vault-tec-small.png deleted file mode 100644 index 999de531a..000000000 Binary files a/src/stylesheets/Transparent-Style/Green/vault-tec-small.png and /dev/null differ diff --git a/src/stylesheets/Transparent-Style/Green/vault-tec.png b/src/stylesheets/Transparent-Style/Green/vault-tec.png deleted file mode 100644 index 809486e6f..000000000 Binary files a/src/stylesheets/Transparent-Style/Green/vault-tec.png and /dev/null differ diff --git a/src/stylesheets/Transparent-Style/Vault-101-item-end.png b/src/stylesheets/Transparent-Style/Vault-101-item-end.png deleted file mode 100644 index 744e88d90..000000000 Binary files a/src/stylesheets/Transparent-Style/Vault-101-item-end.png and /dev/null differ diff --git a/src/stylesheets/Transparent-Style/Vault-101-item-line-v.png b/src/stylesheets/Transparent-Style/Vault-101-item-line-v.png deleted file mode 100644 index 48453ee68..000000000 Binary files a/src/stylesheets/Transparent-Style/Vault-101-item-line-v.png and /dev/null differ diff --git a/src/stylesheets/Transparent-Style/Vault-101-itemB-close-last.png b/src/stylesheets/Transparent-Style/Vault-101-itemB-close-last.png deleted file mode 100644 index 930a3a04f..000000000 Binary files a/src/stylesheets/Transparent-Style/Vault-101-itemB-close-last.png and /dev/null differ diff --git a/src/stylesheets/Transparent-Style/Vault-101-itemB-end.png b/src/stylesheets/Transparent-Style/Vault-101-itemB-end.png deleted file mode 100644 index 6eb0db483..000000000 Binary files a/src/stylesheets/Transparent-Style/Vault-101-itemB-end.png and /dev/null differ diff --git a/src/stylesheets/Transparent-Style/Vault-101-itemB-line.png b/src/stylesheets/Transparent-Style/Vault-101-itemB-line.png deleted file mode 100644 index 86c503b39..000000000 Binary files a/src/stylesheets/Transparent-Style/Vault-101-itemB-line.png and /dev/null differ diff --git a/src/stylesheets/Transparent-Style/Vault-101-itemB-open-last.png b/src/stylesheets/Transparent-Style/Vault-101-itemB-open-last.png deleted file mode 100644 index 3037addd7..000000000 Binary files a/src/stylesheets/Transparent-Style/Vault-101-itemB-open-last.png and /dev/null differ diff --git a/src/stylesheets/Transparent-Style/Vault-101-itemb-close - Copy (2).png b/src/stylesheets/Transparent-Style/Vault-101-itemb-close - Copy (2).png deleted file mode 100644 index fcc44dd09..000000000 Binary files a/src/stylesheets/Transparent-Style/Vault-101-itemb-close - Copy (2).png and /dev/null differ diff --git a/src/stylesheets/Transparent-Style/Vault-101-itemb-close - Copy (3).png b/src/stylesheets/Transparent-Style/Vault-101-itemb-close - Copy (3).png deleted file mode 100644 index f9960221b..000000000 Binary files a/src/stylesheets/Transparent-Style/Vault-101-itemb-close - Copy (3).png and /dev/null differ diff --git a/src/stylesheets/Transparent-Style/Vault-101-itemb-close.png b/src/stylesheets/Transparent-Style/Vault-101-itemb-close.png deleted file mode 100644 index 29a688788..000000000 Binary files a/src/stylesheets/Transparent-Style/Vault-101-itemb-close.png and /dev/null differ diff --git a/src/stylesheets/Transparent-Style/Vault-101-itemb-open.png b/src/stylesheets/Transparent-Style/Vault-101-itemb-open.png deleted file mode 100644 index 3359205e3..000000000 Binary files a/src/stylesheets/Transparent-Style/Vault-101-itemb-open.png and /dev/null differ diff --git a/src/stylesheets/Transparent-Style/Vault-BOS-Main.png b/src/stylesheets/Transparent-Style/Vault-BOS-Main.png deleted file mode 100644 index e30609dfd..000000000 Binary files a/src/stylesheets/Transparent-Style/Vault-BOS-Main.png and /dev/null differ diff --git a/src/stylesheets/Transparent-Style/arrow-down-hover.png b/src/stylesheets/Transparent-Style/arrow-down-hover.png deleted file mode 100644 index 3d53c1a0c..000000000 Binary files a/src/stylesheets/Transparent-Style/arrow-down-hover.png and /dev/null differ diff --git a/src/stylesheets/Transparent-Style/arrow-down-small.png b/src/stylesheets/Transparent-Style/arrow-down-small.png deleted file mode 100644 index a61d70e50..000000000 Binary files a/src/stylesheets/Transparent-Style/arrow-down-small.png and /dev/null differ diff --git a/src/stylesheets/Transparent-Style/arrow-down.png b/src/stylesheets/Transparent-Style/arrow-down.png deleted file mode 100644 index 41232bb47..000000000 Binary files a/src/stylesheets/Transparent-Style/arrow-down.png and /dev/null differ diff --git a/src/stylesheets/Transparent-Style/arrow-left-hover.png b/src/stylesheets/Transparent-Style/arrow-left-hover.png deleted file mode 100644 index 9bd9aa1ef..000000000 Binary files a/src/stylesheets/Transparent-Style/arrow-left-hover.png and /dev/null differ diff --git a/src/stylesheets/Transparent-Style/arrow-left.png b/src/stylesheets/Transparent-Style/arrow-left.png deleted file mode 100644 index ec965b2b4..000000000 Binary files a/src/stylesheets/Transparent-Style/arrow-left.png and /dev/null differ diff --git a/src/stylesheets/Transparent-Style/arrow-right-hover.png b/src/stylesheets/Transparent-Style/arrow-right-hover.png deleted file mode 100644 index 3df981c26..000000000 Binary files a/src/stylesheets/Transparent-Style/arrow-right-hover.png and /dev/null differ diff --git a/src/stylesheets/Transparent-Style/arrow-right-small.png b/src/stylesheets/Transparent-Style/arrow-right-small.png deleted file mode 100644 index 1c4cb2374..000000000 Binary files a/src/stylesheets/Transparent-Style/arrow-right-small.png and /dev/null differ diff --git a/src/stylesheets/Transparent-Style/arrow-right.png b/src/stylesheets/Transparent-Style/arrow-right.png deleted file mode 100644 index 935f1b7a0..000000000 Binary files a/src/stylesheets/Transparent-Style/arrow-right.png and /dev/null differ diff --git a/src/stylesheets/Transparent-Style/arrow-up-hover.png b/src/stylesheets/Transparent-Style/arrow-up-hover.png deleted file mode 100644 index 9fc94dc4d..000000000 Binary files a/src/stylesheets/Transparent-Style/arrow-up-hover.png and /dev/null differ diff --git a/src/stylesheets/Transparent-Style/arrow-up.png b/src/stylesheets/Transparent-Style/arrow-up.png deleted file mode 100644 index 797fb7402..000000000 Binary files a/src/stylesheets/Transparent-Style/arrow-up.png and /dev/null differ diff --git a/src/stylesheets/Transparent-Style/carbonfiber.png b/src/stylesheets/Transparent-Style/carbonfiber.png deleted file mode 100644 index 8f92238ee..000000000 Binary files a/src/stylesheets/Transparent-Style/carbonfiber.png and /dev/null differ diff --git a/src/stylesheets/Transparent-Style/checkbox-checked-disabled.png b/src/stylesheets/Transparent-Style/checkbox-checked-disabled.png deleted file mode 100644 index c781b2a37..000000000 Binary files a/src/stylesheets/Transparent-Style/checkbox-checked-disabled.png and /dev/null differ diff --git a/src/stylesheets/Transparent-Style/checkbox-checked-hover.png b/src/stylesheets/Transparent-Style/checkbox-checked-hover.png deleted file mode 100644 index 6f275c3e6..000000000 Binary files a/src/stylesheets/Transparent-Style/checkbox-checked-hover.png and /dev/null differ diff --git a/src/stylesheets/Transparent-Style/checkbox-checked.png b/src/stylesheets/Transparent-Style/checkbox-checked.png deleted file mode 100644 index 4c2d1d774..000000000 Binary files a/src/stylesheets/Transparent-Style/checkbox-checked.png and /dev/null differ diff --git a/src/stylesheets/Transparent-Style/checkbox-hover.png b/src/stylesheets/Transparent-Style/checkbox-hover.png deleted file mode 100644 index 7fce9dd8f..000000000 Binary files a/src/stylesheets/Transparent-Style/checkbox-hover.png and /dev/null differ diff --git a/src/stylesheets/Transparent-Style/checkbox.png b/src/stylesheets/Transparent-Style/checkbox.png deleted file mode 100644 index 32d6d5b09..000000000 Binary files a/src/stylesheets/Transparent-Style/checkbox.png and /dev/null differ diff --git a/src/stylesheets/Transparent-Style/narrow-arrow-down.png b/src/stylesheets/Transparent-Style/narrow-arrow-down.png deleted file mode 100644 index b1bbdf57a..000000000 Binary files a/src/stylesheets/Transparent-Style/narrow-arrow-down.png and /dev/null differ diff --git a/src/stylesheets/Transparent-Style/narrow-arrow-left.png b/src/stylesheets/Transparent-Style/narrow-arrow-left.png deleted file mode 100644 index 5fad0dc41..000000000 Binary files a/src/stylesheets/Transparent-Style/narrow-arrow-left.png and /dev/null differ diff --git a/src/stylesheets/Transparent-Style/narrow-arrow-right.png b/src/stylesheets/Transparent-Style/narrow-arrow-right.png deleted file mode 100644 index 982a100fa..000000000 Binary files a/src/stylesheets/Transparent-Style/narrow-arrow-right.png and /dev/null differ diff --git a/src/stylesheets/Transparent-Style/narrow-arrow-up.png b/src/stylesheets/Transparent-Style/narrow-arrow-up.png deleted file mode 100644 index 62006d204..000000000 Binary files a/src/stylesheets/Transparent-Style/narrow-arrow-up.png and /dev/null differ diff --git a/src/stylesheets/Transparent-Style/radio-BOS-checked.png b/src/stylesheets/Transparent-Style/radio-BOS-checked.png deleted file mode 100644 index 4c2d1d774..000000000 Binary files a/src/stylesheets/Transparent-Style/radio-BOS-checked.png and /dev/null differ diff --git a/src/stylesheets/Transparent-Style/radio-BOS-hover.png b/src/stylesheets/Transparent-Style/radio-BOS-hover.png deleted file mode 100644 index 7d7ae49c3..000000000 Binary files a/src/stylesheets/Transparent-Style/radio-BOS-hover.png and /dev/null differ diff --git a/src/stylesheets/Transparent-Style/radio-BOS.png b/src/stylesheets/Transparent-Style/radio-BOS.png deleted file mode 100644 index 6f275c3e6..000000000 Binary files a/src/stylesheets/Transparent-Style/radio-BOS.png and /dev/null differ diff --git a/src/thememanager.cpp b/src/thememanager.cpp new file mode 100644 index 000000000..cc9dfa008 --- /dev/null +++ b/src/thememanager.cpp @@ -0,0 +1,353 @@ +#include "thememanager.h" + +#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); + } + } +}; + +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(m_app->style()->objectName().toStdString()); + 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) +{ + // load the default theme + m_app->setStyle( + new ProxyStyle(QStyleFactory::create(ToQString(m_defaultTheme->identifier())))); + + // build the stylesheet and set it + m_app->setStyleSheet(buildStyleSheet(theme)); +} + +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{})); + } +} + +namespace +{ +QString readWholeFile(std::filesystem::path const& path) +{ + QFile file(path); + if (!file.open(QFile::ReadOnly | QFile::Text)) { + return {}; + } + + return QTextStream(&file).readAll(); +} +} // namespace + +QString ThemeManager::buildStyleSheet(std::shared_ptr const& theme) const +{ + // create the base stylesheet + QString stylesheet = patchStyleSheet(readWholeFile(theme->stylesheet()), + 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..a5211ba2c --- /dev/null +++ b/src/thememanager.h @@ -0,0 +1,120 @@ +#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 + // + QString buildStyleSheet(std::shared_ptr const& theme) 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..339209417 --- /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.nativeCountryName()); + + 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..0afab7a82 --- /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..18871c885 --- /dev/null +++ b/themes/CMakeLists.txt @@ -0,0 +1,11 @@ +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}") + install(DIRECTORY ${theme_directory} DESTINATION bin/extensions) + 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..0bd00fa70 --- /dev/null +++ b/themes/dark-theme/metadata.json @@ -0,0 +1,13 @@ +{ + "identifier": "dark-theme", + "type": "theme", + "name": "Dark Theme", + "description": "Dark theme for ModOrganizer2.", + "version": "1.0.0", + "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..4e1cb8543 --- /dev/null +++ b/themes/dracula-theme/metadata.json @@ -0,0 +1,13 @@ +{ + "identifier": "dracula-theme", + "type": "theme", + "name": "Dracula Theme", + "description": "Dracula theme for ModOrganizer2.", + "version": "1.0.0", + "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..4143291c8 --- /dev/null +++ b/themes/make-metadata.ps1 @@ -0,0 +1,25 @@ +$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 = $_.Name + "-theme"; + type = "theme"; + name = "$name Theme"; + description = "$name theme for ModOrganizer2."; + version = "1.0.0"; + 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..f8d8b3261 --- /dev/null +++ b/themes/nighteyes-theme/metadata.json @@ -0,0 +1,13 @@ +{ + "identifier": "nighteyes-theme", + "type": "theme", + "name": "Night Eyes Theme", + "description": "Night Eyes theme for ModOrganizer2.", + "version": "1.0.0", + "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..4e53ba407 --- /dev/null +++ b/themes/parchment-theme/metadata.json @@ -0,0 +1,13 @@ +{ + "identifier": "parchment-theme", + "type": "theme", + "name": "Parchment Theme", + "description": "Parchment theme for ModOrganizer2.", + "version": "1.0.0", + "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..b28358b1b --- /dev/null +++ b/themes/skyrim-theme/metadata.json @@ -0,0 +1,13 @@ +{ + "identifier": "skyrim-theme", + "type": "theme", + "name": "Skyrim Theme", + "description": "Skyrim theme for ModOrganizer2.", + "version": "1.0.0", + "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..0f6ea552d --- /dev/null +++ b/themes/vs15-themes/metadata.json @@ -0,0 +1,37 @@ +{ + "identifier": "vs15-dark-themes", + "type": "theme", + "name": "VS15 Themes", + "description": "Set of dark themes, inspired by Visual Studio, for ModOrganizer2.", + "version": "1.0.0", + "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