summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/CMakeLists.txt11
-rw-r--r--src/disableproxyplugindialog.cpp30
-rw-r--r--src/disableproxyplugindialog.h43
-rw-r--r--src/disableproxyplugindialog.ui174
-rw-r--r--src/dlls.manifest.qt5 (renamed from src/dlls.manifest)58
-rw-r--r--src/installationmanager.cpp32
-rw-r--r--src/installationmanager.h21
-rw-r--r--src/iuserinterface.h2
-rw-r--r--src/mainwindow.cpp222
-rw-r--r--src/mainwindow.h16
-rw-r--r--src/mainwindow.ui22
-rw-r--r--src/modinfowithconflictinfo.cpp4
-rw-r--r--src/modinfowithconflictinfo.h12
-rw-r--r--src/organizercore.cpp33
-rw-r--r--src/organizercore.h5
-rw-r--r--src/organizerproxy.cpp50
-rw-r--r--src/organizerproxy.h14
-rw-r--r--src/plugincontainer.cpp577
-rw-r--r--src/plugincontainer.h260
-rw-r--r--src/previewgenerator.cpp11
-rw-r--r--src/previewgenerator.h6
-rw-r--r--src/problemsdialog.cpp15
-rw-r--r--src/problemsdialog.h5
-rw-r--r--src/proxyutils.h2
-rw-r--r--src/qt.conf6
-rw-r--r--src/settings.cpp23
-rw-r--r--src/settingsdialog.ui141
-rw-r--r--src/settingsdialoggeneral.cpp6
-rw-r--r--src/settingsdialogplugins.cpp193
-rw-r--r--src/settingsdialogplugins.h7
-rw-r--r--src/shared/appconfig.inc3
-rw-r--r--src/thread_utils.h45
-rw-r--r--src/transfersavesdialog.cpp105
-rw-r--r--src/transfersavesdialog.h7
-rw-r--r--src/tutorials/TutorialOverlay.qml4
35 files changed, 1605 insertions, 560 deletions
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 50ce4178..a53d845d 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -186,6 +186,7 @@ add_filter(NAME src/settingsdialog GROUPS
settingsdialogplugins
settingsdialogsteam
settingsdialogworkarounds
+ disableproxyplugindialog
)
add_filter(NAME src/utilities GROUPS
@@ -231,17 +232,15 @@ target_include_directories(${CMAKE_PROJECT_NAME} PRIVATE
)
-install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/dlls.manifest
+install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/dlls.manifest.qt5
DESTINATION bin/dlls
RENAME dlls.manifest)
-install(FILES ${qm_files} DESTINATION bin/resources/translations)
+install(FILES ${qm_files} DESTINATION bin/translations)
install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/stylesheets
- DESTINATION bin)
-
-install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/tutorials
- DESTINATION bin/resources)
+ ${CMAKE_CURRENT_SOURCE_DIR}/tutorials
+ DESTINATION bin)
deploy_qt(BINARIES ModOrganizer.exe uibase.dll plugins/bsa_packer.dll)
diff --git a/src/disableproxyplugindialog.cpp b/src/disableproxyplugindialog.cpp
new file mode 100644
index 00000000..c05e30e3
--- /dev/null
+++ b/src/disableproxyplugindialog.cpp
@@ -0,0 +1,30 @@
+#include "disableproxyplugindialog.h"
+
+#include "ui_disableproxyplugindialog.h"
+
+using namespace MOBase;
+
+DisableProxyPluginDialog::DisableProxyPluginDialog(
+ MOBase::IPlugin* proxyPlugin, std::vector<MOBase::IPlugin*> const& required, QWidget* parent)
+ : QDialog(parent), ui(new Ui::DisableProxyPluginDialog)
+{
+ ui->setupUi(this);
+
+ ui->topLabel->setText(QObject::tr(
+ "Disabling the '%1' plugin will prevent the following %2 plugin(s) from working:", "", required.size())
+ .arg(proxyPlugin->localizedName())
+ .arg(required.size()));
+
+ connect(ui->noBtn, &QPushButton::clicked, this, &QDialog::reject);
+ connect(ui->yesBtn, &QPushButton::clicked, this, &QDialog::accept);
+
+ ui->requiredPlugins->setSelectionMode(QAbstractItemView::NoSelection);
+ ui->requiredPlugins->setRowCount(required.size());
+ for (int i = 0; i < required.size(); ++i) {
+ ui->requiredPlugins->setItem(i, 0, new QTableWidgetItem(required[i]->localizedName()));
+ ui->requiredPlugins->setItem(i, 1, new QTableWidgetItem(required[i]->description()));
+ ui->requiredPlugins->setRowHeight(i, 9);
+ }
+ ui->requiredPlugins->verticalHeader()->setVisible(false);
+ ui->requiredPlugins->sortByColumn(0, Qt::AscendingOrder);
+}
diff --git a/src/disableproxyplugindialog.h b/src/disableproxyplugindialog.h
new file mode 100644
index 00000000..52f552e1
--- /dev/null
+++ b/src/disableproxyplugindialog.h
@@ -0,0 +1,43 @@
+/*
+Copyright (C) 2020 Mikaël Capelle. All rights reserved.
+
+This file is part of Mod Organizer.
+
+Mod Organizer is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+Mod Organizer is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
+*/
+
+#ifndef DISABLEPROXYPLUGINDIALOG_H
+#define DISABLEPROXYPLUGINDIALOG_H
+
+#include <QDialog>
+
+#include "ipluginproxy.h"
+
+namespace Ui { class DisableProxyPluginDialog; }
+
+class DisableProxyPluginDialog : public QDialog {
+public:
+
+ DisableProxyPluginDialog(
+ MOBase::IPlugin* proxyPlugin,
+ std::vector<MOBase::IPlugin*> const& required,
+ QWidget* parent = nullptr);
+
+private slots:
+
+ Ui::DisableProxyPluginDialog* ui;
+
+};
+
+#endif
diff --git a/src/disableproxyplugindialog.ui b/src/disableproxyplugindialog.ui
new file mode 100644
index 00000000..9f068787
--- /dev/null
+++ b/src/disableproxyplugindialog.ui
@@ -0,0 +1,174 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>DisableProxyPluginDialog</class>
+ <widget class="QDialog" name="DisableProxyPluginDialog">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>522</width>
+ <height>417</height>
+ </rect>
+ </property>
+ <property name="windowTitle">
+ <string>Really disable plugin?</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <item>
+ <widget class="QWidget" name="horizontalWidget" native="true">
+ <property name="minimumSize">
+ <size>
+ <width>0</width>
+ <height>0</height>
+ </size>
+ </property>
+ <layout class="QHBoxLayout" name="horizontalLayout">
+ <item>
+ <widget class="QLabel" name="label_2">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string/>
+ </property>
+ <property name="textFormat">
+ <enum>Qt::PlainText</enum>
+ </property>
+ <property name="pixmap">
+ <pixmap resource="resources.qrc">:/MO/gui/remove</pixmap>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignCenter</set>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="horizontalSpacer">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeType">
+ <enum>QSizePolicy::Fixed</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>10</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <widget class="QLabel" name="topLabel">
+ <property name="text">
+ <string>Disabling the '%1' plugin will prevent the following plugins from working:</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <widget class="QTableWidget" name="requiredPlugins">
+ <property name="columnCount">
+ <number>2</number>
+ </property>
+ <attribute name="horizontalHeaderStretchLastSection">
+ <bool>true</bool>
+ </attribute>
+ <column>
+ <property name="text">
+ <string>Plugin</string>
+ </property>
+ </column>
+ <column>
+ <property name="text">
+ <string>Description</string>
+ </property>
+ </column>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLabel" name="label">
+ <property name="text">
+ <string>Do you want to continue? You will need to restart Mod Organizer for the change to take effect.</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QWidget" name="buttons" native="true">
+ <property name="minimumSize">
+ <size>
+ <width>0</width>
+ <height>0</height>
+ </size>
+ </property>
+ <layout class="QHBoxLayout" name="horizontalLayout_2">
+ <item>
+ <spacer name="horizontalSpacer_2">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>40</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <widget class="QPushButton" name="yesBtn">
+ <property name="minimumSize">
+ <size>
+ <width>80</width>
+ <height>0</height>
+ </size>
+ </property>
+ <property name="text">
+ <string>Yes</string>
+ </property>
+ <property name="icon">
+ <iconset resource="resources.qrc">
+ <normaloff>:/MO/gui/remove</normaloff>:/MO/gui/remove</iconset>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="noBtn">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="minimumSize">
+ <size>
+ <width>80</width>
+ <height>0</height>
+ </size>
+ </property>
+ <property name="text">
+ <string>No</string>
+ </property>
+ <property name="default">
+ <bool>true</bool>
+ </property>
+ <property name="flat">
+ <bool>false</bool>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <resources>
+ <include location="resources.qrc"/>
+ </resources>
+ <connections/>
+</ui>
diff --git a/src/dlls.manifest b/src/dlls.manifest.qt5
index 846a02ae..cae74df1 100644
--- a/src/dlls.manifest
+++ b/src/dlls.manifest.qt5
@@ -1,29 +1,29 @@
-<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
-<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
- <assemblyIdentity type="win32" name="dlls" version="1.0.0.0" processorArchitecture="x86"/>
- <file name="7z.dll"/>
- <file name="archive.dll"/>
- <file name="d3dcompiler_47.dll"/>
- <file name="libEGL.dll"/>
- <file name="libGLESV2.dll"/>
- <file name="liblz4.dll"/>
- <file name="opengl32sw.dll"/>
- <file name="Qt5Core.dll"/>
- <file name="Qt5Gui.dll"/>
- <file name="Qt5Network.dll"/>
- <file name="Qt5Positioning.dll"/>
- <file name="Qt5PrintSupport.dll"/>
- <file name="Qt5Qml.dll"/>
- <file name="Qt5QmlModels.dll"/>
- <file name="Qt5QmlWorkerScript.dll"/>
- <file name="Qt5Quick.dll"/>
- <file name="Qt5QuickWidgets.dll"/>
- <file name="Qt5SerialPort.dll"/>
- <file name="Qt5Svg.dll"/>
- <file name="Qt5WebChannel.dll"/>
- <file name="Qt5WebEngineCore.dll"/>
- <file name="Qt5WebEngineWidgets.dll"/>
- <file name="Qt5WebSockets.dll"/>
- <file name="Qt5Widgets.dll"/>
- <file name="Qt5WinExtras.dll"/>
-</assembly>
+<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
+ <assemblyIdentity type="win32" name="dlls" version="1.0.0.0" processorArchitecture="x86"/>
+ <file name="7z.dll"/>
+ <file name="archive.dll"/>
+ <file name="d3dcompiler_47.dll"/>
+ <file name="libEGL.dll"/>
+ <file name="libGLESV2.dll"/>
+ <file name="liblz4.dll"/>
+ <file name="opengl32sw.dll"/>
+ <file name="Qt5Core.dll"/>
+ <file name="Qt5Gui.dll"/>
+ <file name="Qt5Network.dll"/>
+ <file name="Qt5Positioning.dll"/>
+ <file name="Qt5PrintSupport.dll"/>
+ <file name="Qt5Qml.dll"/>
+ <file name="Qt5QmlModels.dll"/>
+ <file name="Qt5QmlWorkerScript.dll"/>
+ <file name="Qt5Quick.dll"/>
+ <file name="Qt5QuickWidgets.dll"/>
+ <file name="Qt5SerialPort.dll"/>
+ <file name="Qt5Svg.dll"/>
+ <file name="Qt5WebChannel.dll"/>
+ <file name="Qt5WebEngineCore.dll"/>
+ <file name="Qt5WebEngineWidgets.dll"/>
+ <file name="Qt5WebSockets.dll"/>
+ <file name="Qt5Widgets.dll"/>
+ <file name="Qt5WinExtras.dll"/>
+</assembly>
diff --git a/src/installationmanager.cpp b/src/installationmanager.cpp
index e9773a95..06909994 100644
--- a/src/installationmanager.cpp
+++ b/src/installationmanager.cpp
@@ -78,7 +78,8 @@ static T resolveFunction(QLibrary &lib, const char *name)
}
InstallationManager::InstallationManager()
- : m_ParentWidget(nullptr),
+ :
+ m_ParentWidget(nullptr),
m_SupportedExtensions({"zip", "rar", "7z", "fomod", "001"}),
m_IsRunning(false) {
m_ArchiveHandler = CreateArchive();
@@ -106,7 +107,7 @@ InstallationManager::InstallationManager()
// Connect the query password slot - This is the only way I found to be able to query user
// from a separate thread. We use a BlockingQueuedConnection so that calling passwordRequested()
// will block until the end of the slot.
- connect(this, &InstallationManager::passwordRequested,
+ connect(this, &InstallationManager::passwordRequested,
this, &InstallationManager::queryPassword, Qt::BlockingQueuedConnection);
}
@@ -122,6 +123,11 @@ void InstallationManager::setParentWidget(QWidget *widget)
}
}
+void InstallationManager::setPluginContainer(const PluginContainer* pluginContainer)
+{
+ m_PluginContainer = pluginContainer;
+}
+
void InstallationManager::queryPassword() {
m_Password = QInputDialog::getText(m_ParentWidget, tr("Password required"),
tr("Password"), QLineEdit::Password);
@@ -284,7 +290,7 @@ QStringList InstallationManager::extractFiles(std::vector<std::shared_ptr<const
}
-QString InstallationManager::createFile(std::shared_ptr<const MOBase::FileTreeEntry> entry)
+QString InstallationManager::createFile(std::shared_ptr<const MOBase::FileTreeEntry> entry)
{
// Use QTemporaryFile to create the temporary file with the given template:
QTemporaryFile tempFile(QDir::cleanPath(QDir::tempPath() + QDir::separator() + "mo2-install"));
@@ -308,11 +314,11 @@ QString InstallationManager::createFile(std::shared_ptr<const MOBase::FileTreeEn
return QDir::toNativeSeparators(absPath);
}
-void InstallationManager::cleanCreatedFiles(std::shared_ptr<const MOBase::IFileTree> fileTree)
+void InstallationManager::cleanCreatedFiles(std::shared_ptr<const MOBase::IFileTree> fileTree)
{
// We simply have to check if all the entries have fileTree as a parent:
for (auto it = std::begin(m_CreatedFiles); it != std::end(m_CreatedFiles); ) {
-
+
// Find the parent - Could this be in FileTreeEntry?
bool found = false;
{
@@ -495,12 +501,12 @@ IPluginInstaller::EInstallResult InstallationManager::doInstall(GuessedValue<QSt
if (QFile::exists(destPath)) {
QFile::remove(destPath);
}
-
+
QDir dir = QFileInfo(destPath).absoluteDir();
if (!dir.exists()) {
dir.mkpath(".");
}
-
+
QFile::copy(p.second, destPath);
}
@@ -691,7 +697,7 @@ IPluginInstaller::EInstallResult InstallationManager::install(const QString &fil
}
ON_BLOCK_EXIT(std::bind(&InstallationManager::postInstallCleanup, this));
- std::shared_ptr<IFileTree> filesTree =
+ std::shared_ptr<IFileTree> filesTree =
archiveOpen ? ArchiveFileTree::makeTree(*m_ArchiveHandler) : nullptr;
IPluginInstaller::EInstallResult installResult = IPluginInstaller::RESULT_NOTATTEMPTED;
@@ -701,7 +707,7 @@ IPluginInstaller::EInstallResult InstallationManager::install(const QString &fil
for (IPluginInstaller *installer : m_Installers) {
// don't use inactive installers (installer can't be null here but vc static code analysis thinks it could)
- if ((installer == nullptr) || !installer->isActive()) {
+ if ((installer == nullptr) || !m_PluginContainer->isEnabled(installer)) {
continue;
}
@@ -731,7 +737,7 @@ IPluginInstaller::EInstallResult InstallationManager::install(const QString &fil
if (p == nullptr) {
throw IncompatibilityException(tr("Invalid file tree returned by plugin."));
}
-
+
// Detach the file tree (this ensure the parent is null and call to path()
// stops at this root):
p->detach();
@@ -786,7 +792,7 @@ IPluginInstaller::EInstallResult InstallationManager::install(const QString &fil
case IPluginInstaller::RESULT_SUCCESSCANCEL: {
if (filesTree != nullptr) {
auto iniTweakEntry = filesTree->find("INI Tweaks", FileTreeEntry::DIRECTORY);
- hasIniTweaks = iniTweakEntry != nullptr
+ hasIniTweaks = iniTweakEntry != nullptr
&& !iniTweakEntry->astree()->empty();
}
return IPluginInstaller::RESULT_SUCCESS;
@@ -860,7 +866,7 @@ QStringList InstallationManager::getSupportedExtensions() const
void InstallationManager::notifyInstallationStart(QString const& archive, bool reinstallation, ModInfo::Ptr currentMod)
{
for (auto* installer : m_Installers) {
- if (installer->isActive()) {
+ if (m_PluginContainer->isEnabled(installer)) {
installer->onInstallationStart(archive, reinstallation, currentMod.get());
}
}
@@ -871,7 +877,7 @@ void InstallationManager::notifyInstallationEnd(
ModInfo::Ptr newMod)
{
for (auto* installer : m_Installers) {
- if (installer->isActive()) {
+ if (m_PluginContainer->isEnabled(installer)) {
installer->onInstallationEnd(result, newMod.get());
}
}
diff --git a/src/installationmanager.h b/src/installationmanager.h
index 66f11eae..145abdb6 100644
--- a/src/installationmanager.h
+++ b/src/installationmanager.h
@@ -35,6 +35,7 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
#include <errorcodes.h>
#include "modinfo.h"
+#include "plugincontainer.h"
/**
@@ -88,6 +89,11 @@ public:
void setModsDirectory(const QString &modsDirectory) { m_ModsDirectory = modsDirectory; }
/**
+ *
+ */
+ void setPluginContainer(const PluginContainer* pluginContainer);
+
+ /**
* @brief update the directory where downloads are stored
* @param downloadDirectory the download directory
*/
@@ -126,8 +132,8 @@ public:
* @brief register an installer-plugin
* @param the installer to register
*/
- void registerInstaller(MOBase::IPluginInstaller *installer);
-
+ void registerInstaller(MOBase::IPluginInstaller *installer);
+
/**
* @return the extensions of archives supported by this installation manager.
*/
@@ -167,7 +173,7 @@ public:
*
* The flatten argument is not present here while it is present in the deprecated QStringList
* version for multiple reasons: 1) it was never used, 2) it is kind of fishy because there
- * is no way to know if a file is going to be overriden, 3) it is quite easy to flatten a
+ * is no way to know if a file is going to be overriden, 3) it is quite easy to flatten a
* IFileTree and thus to given a list of entries flattened (this was not possible with the
* QStringList version since these were based on the name of the file inside the archive).
*/
@@ -185,7 +191,7 @@ public:
* @return the path to the created file.
*/
virtual QString createFile(std::shared_ptr<const MOBase::FileTreeEntry> entry) override;
-
+
/**
* @brief Installs the given archive.
*
@@ -279,6 +285,9 @@ private:
private:
+ // The plugin container, mostly to check if installer are enabled or not.
+ const PluginContainer *m_PluginContainer;
+
bool m_IsRunning;
QWidget *m_ParentWidget;
@@ -289,13 +298,13 @@ private:
std::vector<MOBase::IPluginInstaller*> m_Installers;
std::set<QString, CaseInsensitive> m_SupportedExtensions;
- // Archive management:
+ // Archive management.
std::unique_ptr<Archive> m_ArchiveHandler;
QString m_CurrentFile;
QString m_Password;
// Map from entries in the tree that is used by the installer and absolute
- // paths to temporary files:
+ // paths to temporary files.
std::map<std::shared_ptr<const MOBase::FileTreeEntry>, QString> m_CreatedFiles;
std::set<QString> m_TempFilesToDelete;
};
diff --git a/src/iuserinterface.h b/src/iuserinterface.h
index cce89070..a2a91e62 100644
--- a/src/iuserinterface.h
+++ b/src/iuserinterface.h
@@ -13,8 +13,6 @@
class IUserInterface
{
public:
- virtual void registerPluginTool(MOBase::IPluginTool *tool, QString name = QString(), QMenu *menu = nullptr) = 0;
- virtual void registerPluginTools(std::vector<MOBase::IPluginTool *> toolPlugins) = 0;
virtual void registerModPage(MOBase::IPluginModPage *modPage) = 0;
virtual void installTranslator(const QString &name) = 0;
diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp
index 2bb0a237..3ea55ef8 100644
--- a/src/mainwindow.cpp
+++ b/src/mainwindow.cpp
@@ -260,7 +260,6 @@ MainWindow::MainWindow(Settings &settings
, m_ContextItem(nullptr)
, m_ContextAction(nullptr)
, m_ContextRow(-1)
- , m_browseModPage(nullptr)
, m_CurrentSaveView(nullptr)
, m_OrganizerCore(organizerCore)
, m_PluginContainer(pluginContainer)
@@ -431,7 +430,10 @@ MainWindow::MainWindow(Settings &settings
this, &MainWindow::refresherProgress);
connect(m_OrganizerCore.directoryRefresher(), SIGNAL(error(QString)), this, SLOT(showError(QString)));
- connect(&m_SavesWatcher, SIGNAL(directoryChanged(QString)), this, SLOT(refreshSavesIfOpen()));
+ m_SavesWatcherTimer.setSingleShot(true);
+ m_SavesWatcherTimer.setInterval(500);
+ connect(&m_SavesWatcher, &QFileSystemWatcher::directoryChanged, [this]() { m_SavesWatcherTimer.start(); });
+ connect(&m_SavesWatcherTimer, &QTimer::timeout, this, &MainWindow::refreshSavesIfOpen);
connect(&m_OrganizerCore.settings(), SIGNAL(languageChanged(QString)), this, SLOT(languageChange(QString)));
connect(&m_OrganizerCore.settings(), SIGNAL(styleChanged(QString)), this, SIGNAL(styleChanged(QString)));
@@ -467,6 +469,12 @@ MainWindow::MainWindow(Settings &settings
connect(ui->toolBar, SIGNAL(customContextMenuRequested(QPoint)), this, SLOT(toolBar_customContextMenuRequested(QPoint)));
connect(ui->menuToolbars, &QMenu::aboutToShow, [&]{ updateToolbarMenu(); });
connect(ui->menuView, &QMenu::aboutToShow, [&]{ updateViewMenu(); });
+ connect(ui->actionTool->menu(), &QMenu::aboutToShow, [&] { updateToolMenu(); });
+ connect(&m_PluginContainer, &PluginContainer::pluginEnabled, this, [this](IPlugin* plugin) {
+ if (m_PluginContainer.implementInterface<IPluginModPage>(plugin)) { updateModPageMenu(); } });
+ connect(&m_PluginContainer, &PluginContainer::pluginDisabled, this, [this](IPlugin* plugin) {
+ if (m_PluginContainer.implementInterface<IPluginModPage>(plugin)) { updateModPageMenu(); } });
+
connect(&m_OrganizerCore, &OrganizerCore::modInstalled, this, &MainWindow::modInstalled);
connect(&m_OrganizerCore, &OrganizerCore::close, this, &QMainWindow::close);
@@ -503,16 +511,13 @@ MainWindow::MainWindow(Settings &settings
m_Tutorial.expose("espList", m_OrganizerCore.pluginList());
m_OrganizerCore.setUserInterface(this);
+ m_PluginContainer.setUserInterface(this);
connect(this, &MainWindow::userInterfaceInitialized, &m_OrganizerCore, &OrganizerCore::userInterfaceInitialized);
for (const QString &fileName : m_PluginContainer.pluginFileNames()) {
installTranslator(QFileInfo(fileName).baseName());
}
- registerPluginTools(m_PluginContainer.plugins<IPluginTool>());
-
- for (IPluginModPage *modPagePlugin : m_PluginContainer.plugins<IPluginModPage>()) {
- registerModPage(modPagePlugin);
- }
+ updateModPageMenu();
// refresh profiles so the current profile can be activated
refreshProfiles(false);
@@ -699,7 +704,7 @@ MainWindow::~MainWindow()
try {
cleanup();
- m_PluginContainer.setUserInterface(nullptr, nullptr);
+ m_PluginContainer.setUserInterface(nullptr);
m_OrganizerCore.setUserInterface(nullptr);
if (m_IntegratedBrowser) {
@@ -812,6 +817,7 @@ static QModelIndex mapToModel(const QAbstractItemModel *targetModel, QModelIndex
void MainWindow::setupToolbar()
{
+ setupActionMenu(ui->actionModPage);
setupActionMenu(ui->actionTool);
setupActionMenu(ui->actionHelp);
setupActionMenu(ui->actionEndorseMO);
@@ -1132,7 +1138,7 @@ void MainWindow::checkForProblemsImpl()
size_t numProblems = 0;
for (QObject *pluginObj : m_PluginContainer.plugins<QObject>()) {
IPlugin *plugin = qobject_cast<IPlugin*>(pluginObj);
- if (plugin == nullptr || plugin->isActive()) {
+ if (plugin == nullptr || m_PluginContainer.isEnabled(plugin)) {
IPluginDiagnose *diagnose = qobject_cast<IPluginDiagnose*>(pluginObj);
if (diagnose != nullptr)
numProblems += diagnose->activeProblems().size();
@@ -1204,9 +1210,7 @@ void MainWindow::createHelpMenu()
ActionList tutorials;
- QString tutorialPath = QApplication::applicationDirPath()
- + "/" + QString::fromStdWString(AppConfig::tutorialsPath()) + "/";
- QDirIterator dirIter(tutorialPath, QStringList("*.js"), QDir::Files);
+ QDirIterator dirIter(QApplication::applicationDirPath() + "/tutorials", QStringList("*.js"), QDir::Files);
while (dirIter.hasNext()) {
dirIter.next();
QString fileName = dirIter.fileName();
@@ -1318,8 +1322,7 @@ bool MainWindow::addProfile()
void MainWindow::hookUpWindowTutorials()
{
- QString tutorialPath = QApplication::applicationDirPath() + "/" + QString::fromStdWString(AppConfig::tutorialsPath()) + "/";
- QDirIterator dirIter(tutorialPath, QStringList("*.js"), QDir::Files);
+ QDirIterator dirIter(QApplication::applicationDirPath() + "/tutorials", QStringList("*.js"), QDir::Files);
while (dirIter.hasNext()) {
dirIter.next();
QString fileName = dirIter.fileName();
@@ -1530,7 +1533,6 @@ void MainWindow::displaySaveGameInfo(QListWidgetItem *newItem)
return;
}
- QString const &save = newItem->data(Qt::UserRole).toString();
if (m_CurrentSaveView == nullptr) {
IPluginGame const *game = m_OrganizerCore.managedGame();
SaveGameInfo const *info = game->feature<SaveGameInfo>();
@@ -1541,7 +1543,7 @@ void MainWindow::displaySaveGameInfo(QListWidgetItem *newItem)
return;
}
}
- m_CurrentSaveView->setSave(save);
+ m_CurrentSaveView->setSave(*m_SaveGames[ui->savegameList->row(newItem)]);
QWindow *window = m_CurrentSaveView->window()->windowHandle();
QRect screenRect;
@@ -1600,45 +1602,6 @@ bool MainWindow::eventFilter(QObject *object, QEvent *event)
return false;
}
-
-void MainWindow::toolPluginInvoke()
-{
- QAction *triggeredAction = qobject_cast<QAction*>(sender());
- IPluginTool *plugin = qobject_cast<IPluginTool*>(triggeredAction->data().value<QObject*>());
- if (plugin != nullptr) {
- try {
- plugin->display();
- } catch (const std::exception &e) {
- reportError(tr("Plugin \"%1\" failed: %2").arg(plugin->name()).arg(e.what()));
- } catch (...) {
- reportError(tr("Plugin \"%1\" failed").arg(plugin->name()));
- }
- }
-}
-
-void MainWindow::modPagePluginInvoke()
-{
- QAction *triggeredAction = qobject_cast<QAction*>(sender());
- IPluginModPage *plugin = qobject_cast<IPluginModPage*>(triggeredAction->data().value<QObject*>());
- if (plugin != nullptr) {
- if (plugin->useIntegratedBrowser()) {
-
- if (!m_IntegratedBrowser) {
- m_IntegratedBrowser.reset(new BrowserDialog);
-
- connect(
- m_IntegratedBrowser.get(), SIGNAL(requestDownload(QUrl,QNetworkReply*)),
- &m_OrganizerCore, SLOT(requestDownload(QUrl,QNetworkReply*)));
- }
-
- m_IntegratedBrowser->setWindowTitle(plugin->displayName());
- m_IntegratedBrowser->openUrl(plugin->pageURL());
- } else {
- QDesktopServices::openUrl(QUrl(plugin->pageURL()));
- }
- }
-}
-
void MainWindow::registerPluginTool(IPluginTool *tool, QString name, QMenu *menu)
{
if (!menu) {
@@ -1651,26 +1614,41 @@ void MainWindow::registerPluginTool(IPluginTool *tool, QString name, QMenu *menu
QAction *action = new QAction(tool->icon(), name, menu);
action->setToolTip(tool->tooltip());
tool->setParentWidget(this);
- action->setData(QVariant::fromValue((QObject*)tool));
- connect(action, SIGNAL(triggered()), this, SLOT(toolPluginInvoke()), Qt::QueuedConnection);
+ connect(action, &QAction::triggered, this, [this, tool]() {
+ try {
+ tool->display();
+ }
+ catch (const std::exception& e) {
+ reportError(tr("Plugin \"%1\" failed: %2").arg(tool->localizedName()).arg(e.what()));
+ }
+ catch (...) {
+ reportError(tr("Plugin \"%1\" failed").arg(tool->localizedName()));
+ }
+ }, Qt::QueuedConnection);
menu->addAction(action);
}
-void MainWindow::registerPluginTools(std::vector<IPluginTool *> toolPlugins)
+void MainWindow::updateToolMenu()
{
+ // Clear the menu:
+ ui->actionTool->menu()->clear();
+
+ std::vector<IPluginTool*> toolPlugins = m_PluginContainer.plugins<IPluginTool>();
+
// Sort the plugins by display name
- std::sort(toolPlugins.begin(), toolPlugins.end(),
+ std::sort(std::begin(toolPlugins), std::end(toolPlugins),
[](IPluginTool *left, IPluginTool *right) {
return left->displayName().toLower() < right->displayName().toLower();
}
);
- // Remove inactive plugins
+ // Remove disabled plugins:
toolPlugins.erase(
- std::remove_if(toolPlugins.begin(), toolPlugins.end(), [](IPluginTool *plugin) -> bool { return !plugin->isActive(); }),
- toolPlugins.end()
- );
+ std::remove_if(std::begin(toolPlugins), std::end(toolPlugins), [&](auto* tool) {
+ return !m_PluginContainer.isEnabled(tool);
+ }),
+ toolPlugins.end());
// Group the plugins into submenus
QMap<QString, QList<QPair<QString, IPluginTool *>>> submenuMap;
@@ -1698,26 +1676,64 @@ void MainWindow::registerPluginTools(std::vector<IPluginTool *> toolPlugins)
void MainWindow::registerModPage(IPluginModPage *modPage)
{
- // turn the browser action into a drop-down menu if necessary
- if (!m_browseModPage) {
- m_browseModPage = new QAction(ui->actionNexus->icon(), tr("Browse Mod Page"), this);
- setupActionMenu(m_browseModPage);
-
- m_browseModPage->menu()->addAction(ui->actionNexus);
+ QAction *action = new QAction(modPage->icon(), modPage->displayName(), this);
+ connect(action, &QAction::triggered, this, [this, modPage]() {
+ if (modPage->useIntegratedBrowser()) {
- ui->toolBar->insertAction(ui->actionNexus, m_browseModPage);
- ui->toolBar->removeAction(ui->actionNexus);
- }
+ if (!m_IntegratedBrowser) {
+ m_IntegratedBrowser.reset(new BrowserDialog);
- QAction *action = new QAction(modPage->icon(), modPage->displayName(), this);
- modPage->setParentWidget(this);
- action->setData(QVariant::fromValue(reinterpret_cast<QObject*>(modPage)));
+ connect(
+ m_IntegratedBrowser.get(), SIGNAL(requestDownload(QUrl, QNetworkReply*)),
+ &m_OrganizerCore, SLOT(requestDownload(QUrl, QNetworkReply*)));
+ }
- connect(action, SIGNAL(triggered()), this, SLOT(modPagePluginInvoke()), Qt::QueuedConnection);
+ m_IntegratedBrowser->setWindowTitle(modPage->displayName());
+ m_IntegratedBrowser->openUrl(modPage->pageURL());
+ }
+ else {
+ QDesktopServices::openUrl(QUrl(modPage->pageURL()));
+ }
+ }, Qt::QueuedConnection);
- m_browseModPage->menu()->addAction(action);
+ ui->actionModPage->menu()->addAction(action);
}
+void MainWindow::updateModPageMenu()
+{
+ // Clear the menu:
+ ui->actionModPage->menu()->clear();
+ ui->actionModPage->menu()->addAction(ui->actionNexus);
+
+ std::vector<IPluginModPage*> modPagePlugins = m_PluginContainer.plugins<IPluginModPage>();
+
+ // Sort the plugins by display name
+ std::sort(std::begin(modPagePlugins), std::end(modPagePlugins),
+ [](IPluginModPage* left, IPluginModPage* right) {
+ return left->displayName().toLower() < right->displayName().toLower();
+ }
+ );
+
+ // Remove disabled plugins:
+ modPagePlugins.erase(
+ std::remove_if(std::begin(modPagePlugins), std::end(modPagePlugins), [&](auto* tool) {
+ return !m_PluginContainer.isEnabled(tool);
+ }),
+ modPagePlugins.end());
+
+ for (auto* modPagePlugin : modPagePlugins) {
+ registerModPage(modPagePlugin);
+ }
+
+ // No mod page plugin and the menu was visible:
+ if (modPagePlugins.empty()) {
+ ui->toolBar->insertAction(ui->actionAdd_Profile, ui->actionNexus);
+ }
+ else {
+ ui->toolBar->removeAction(ui->actionNexus);
+ }
+ ui->actionModPage->setVisible(!modPagePlugins.empty());
+}
void MainWindow::startExeAction()
{
@@ -1947,36 +1963,23 @@ void MainWindow::stopMonitorSaves()
void MainWindow::refreshSaveList()
{
- ui->savegameList->clear();
+ TimeThis tt("MainWindow::refreshSaveList()");
startMonitorSaves(); // re-starts monitoring
- QStringList filters;
- filters << QString("*.") + m_OrganizerCore.managedGame()->savegameExtension();
-
QDir savesDir = currentSavesDir();
- savesDir.setNameFilters(filters);
- savesDir.setFilter(QDir::Files);
- QDirIterator it(savesDir, QDirIterator::Subdirectories);
- log::debug("reading save games from {}", savesDir.absolutePath());
-
- QFileInfoList files;
- while (it.hasNext()) {
- it.next();
- files.append(it.fileInfo());
- }
- std::sort(files.begin(), files.end(), [](auto const& lhs, auto const& rhs) {
- return lhs.fileTime(QFileDevice::FileModificationTime) > rhs.fileTime(QFileDevice::FileModificationTime);
+ MOBase::log::debug("reading save games from {}", savesDir.absolutePath());
+ m_SaveGames = m_OrganizerCore.managedGame()->listSaves(savesDir);
+ std::sort(m_SaveGames.begin(), m_SaveGames.end(), [](auto const& lhs, auto const& rhs) {
+ return lhs->getCreationTime() > rhs->getCreationTime();
});
- for (const QFileInfo &file : files) {
- QListWidgetItem *item = new QListWidgetItem(savesDir.relativeFilePath(file.absoluteFilePath()));
- item->setData(Qt::UserRole, file.absoluteFilePath());
- ui->savegameList->addItem(item);
+ ui->savegameList->clear();
+ for (auto& save: m_SaveGames) {
+ ui->savegameList->addItem(savesDir.relativeFilePath(save->getFilepath()));
}
}
-
static bool BySortValue(const std::pair<UINT32, QTreeWidgetItem*> &LHS, const std::pair<UINT32, QTreeWidgetItem*> &RHS)
{
return LHS.first < RHS.first;
@@ -4943,20 +4946,15 @@ void MainWindow::deleteSavegame_clicked()
int count = 0;
for (const QModelIndex &idx : ui->savegameList->selectionModel()->selectedIndexes()) {
- QString name = idx.data(Qt::UserRole).toString();
+
+ auto& saveGame = m_SaveGames[idx.row()];
if (count < 10) {
- savesMsgLabel += "<li>" + QFileInfo(name).completeBaseName() + "</li>";
+ savesMsgLabel += "<li>" + QFileInfo(saveGame->getFilepath()).completeBaseName() + "</li>";
}
++count;
- if (info == nullptr) {
- deleteFiles.push_back(name);
- } else {
- ISaveGame const *save = info->getSaveGameInfo(name);
- deleteFiles += save->allFiles();
- delete save;
- }
+ deleteFiles += saveGame->allFiles();
}
if (count > 10) {
@@ -5015,8 +5013,8 @@ void MainWindow::on_savegameList_customContextMenuRequested(const QPoint& pos)
QAction* action = menu.addAction(tr("Enable Mods..."));
action->setEnabled(false);
if (selection->selectedIndexes().count() == 1) {
- QString save = ui->savegameList->currentItem()->data(Qt::UserRole).toString();
- SaveGameInfo::MissingAssets missing = info->getMissingAssets(save);
+ auto& save = m_SaveGames[selection->selectedIndexes()[0].row()];
+ SaveGameInfo::MissingAssets missing = info->getMissingAssets(*save);
if (missing.size() != 0) {
connect(action, &QAction::triggered, this, [this, missing] { fixMods_clicked(missing); });
action->setEnabled(true);
@@ -5212,9 +5210,7 @@ void MainWindow::installTranslator(const QString &name)
{
QTranslator *translator = new QTranslator(this);
QString fileName = name + "_" + m_CurrentLanguage;
- QString translationsPath = qApp->applicationDirPath()
- + "/" + QString::fromStdWString(AppConfig::translationsPath());
- if (!translator->load(fileName, translationsPath)) {
+ if (!translator->load(fileName, qApp->applicationDirPath() + "/translations")) {
if (m_CurrentLanguage.contains(QRegularExpression("^.*_(EN|en)(-.*)?$"))) {
log::debug("localization file %s not found", fileName);
} // we don't actually expect localization files for English (en, en-us, en-uk, and any variation thereof)
@@ -5977,7 +5973,7 @@ void MainWindow::on_actionNotifications_triggered()
future.waitForFinished();
- ProblemsDialog problems(m_PluginContainer.plugins<QObject>(), this);
+ ProblemsDialog problems(m_PluginContainer, this);
problems.exec();
scheduleCheckForProblems();
diff --git a/src/mainwindow.h b/src/mainwindow.h
index 8b2188c8..da7bb6ee 100644
--- a/src/mainwindow.h
+++ b/src/mainwindow.h
@@ -129,10 +129,6 @@ public:
void saveArchiveList();
- void registerPluginTool(MOBase::IPluginTool *tool, QString name = QString(), QMenu *menu = nullptr);
- void registerPluginTools(std::vector<MOBase::IPluginTool *> toolPlugins);
- void registerModPage(MOBase::IPluginModPage *modPage);
-
void addPrimaryCategoryCandidates(QMenu *primaryCategoryMenu, ModInfo::Ptr info);
void installTranslator(const QString &name);
@@ -160,9 +156,6 @@ public slots:
void directory_refreshed();
- void toolPluginInvoke();
- void modPagePluginInvoke();
-
signals:
/**
@@ -212,7 +205,12 @@ private:
void setToolbarSize(const QSize& s);
void setToolbarButtonStyle(Qt::ToolButtonStyle s);
+ void registerModPage(MOBase::IPluginModPage* modPage);
+ void registerPluginTool(MOBase::IPluginTool* tool, QString name = QString(), QMenu* menu = nullptr);
+
void updateToolbarMenu();
+ void updateToolMenu();
+ void updateModPageMenu();
void updateViewMenu();
QMenu* createPopupMenu() override;
@@ -326,8 +324,6 @@ private:
QTreeWidgetItem *m_ContextItem;
QAction *m_ContextAction;
- QAction* m_browseModPage;
-
CategoryFactory &m_CategoryFactory;
QTimer m_CheckBSATimer;
@@ -338,6 +334,7 @@ private:
QTime m_StartTime;
//SaveGameInfoWidget *m_CurrentSaveView;
+ std::vector<std::shared_ptr<const MOBase::ISaveGame>> m_SaveGames;
MOBase::ISaveGameInfoWidget *m_CurrentSaveView;
OrganizerCore &m_OrganizerCore;
@@ -348,6 +345,7 @@ private:
std::unique_ptr<BrowserDialog> m_IntegratedBrowser;
+ QTimer m_SavesWatcherTimer;
QFileSystemWatcher m_SavesWatcher;
QByteArray m_ArchiveListHash;
diff --git a/src/mainwindow.ui b/src/mainwindow.ui
index ace0dfeb..dbc6013d 100644
--- a/src/mainwindow.ui
+++ b/src/mainwindow.ui
@@ -1411,6 +1411,7 @@ p, li { white-space: pre-wrap; }
<addaction name="actionChange_Game"/>
<addaction name="actionInstallMod"/>
<addaction name="actionNexus"/>
+ <addaction name="actionModPage"/>
<addaction name="actionAdd_Profile"/>
<addaction name="actionModify_Executables"/>
<addaction name="actionTool"/>
@@ -1684,6 +1685,27 @@ p, li { white-space: pre-wrap; }
<string>Ctrl+N</string>
</property>
</action>
+ <action name="actionModPage">
+ <property name="icon">
+ <iconset resource="resources.qrc">
+ <normaloff>:/MO/gui/resources/internet-web-browser.png</normaloff>:/MO/gui/resources/internet-web-browser.png</iconset>
+ </property>
+ <property name="text">
+ <string>Browse Mod Page</string>
+ </property>
+ <property name="iconText">
+ <string>Browse Mod Page</string>
+ </property>
+ <property name="toolTip">
+ <string>Browse Mod Page</string>
+ </property>
+ <property name="statusTip">
+ <string>Browse Mod Page</string>
+ </property>
+ <property name="visible">
+ <bool>false</bool>
+ </property>
+ </action>
<action name="actionUpdate">
<property name="enabled">
<bool>false</bool>
diff --git a/src/modinfowithconflictinfo.cpp b/src/modinfowithconflictinfo.cpp
index 6e3f751e..ad7eda3f 100644
--- a/src/modinfowithconflictinfo.cpp
+++ b/src/modinfowithconflictinfo.cpp
@@ -15,7 +15,7 @@ namespace fs = std::filesystem;
ModInfoWithConflictInfo::ModInfoWithConflictInfo(
PluginContainer *pluginContainer, const MOBase::IPluginGame* gamePlugin, DirectoryEntry **directoryStructure)
- : ModInfo(pluginContainer), m_GamePlugin(gamePlugin),
+ : ModInfo(pluginContainer), m_GamePlugin(gamePlugin),
m_FileTree([this]() { return QDirFileTree::makeTree(absolutePath()); }),
m_Valid([this]() { return doIsValid(); }),
m_Contents([this]() { return doGetContents(); }),
@@ -27,7 +27,7 @@ void ModInfoWithConflictInfo::clearCaches()
}
std::vector<ModInfo::EFlag> ModInfoWithConflictInfo::getFlags() const
-{
+{
std::vector<ModInfo::EFlag> result = std::vector<ModInfo::EFlag>();
if (hasHiddenFiles()) {
result.push_back(ModInfo::FLAG_HIDDEN_FILES);
diff --git a/src/modinfowithconflictinfo.h b/src/modinfowithconflictinfo.h
index feded99b..0d11fcb1 100644
--- a/src/modinfowithconflictinfo.h
+++ b/src/modinfowithconflictinfo.h
@@ -3,7 +3,7 @@
#include <ifiletree.h>
-#include "thread_utils.h"
+#include "memoizedlock.h"
#include "modinfo.h"
#include <set>
@@ -34,7 +34,9 @@ public:
*
* @return true if the content is there, false otherwise.
*/
- virtual bool hasContent(int content) const override; /**
+ virtual bool hasContent(int content) const override;
+
+ /**
* @brief Retrieve a file tree corresponding to the underlying disk content
* of this mod.
*
@@ -145,9 +147,9 @@ protected:
private:
- MOShared::MemoizedLocked<std::shared_ptr<const MOBase::IFileTree>> m_FileTree;
- MOShared::MemoizedLocked<bool> m_Valid;
- MOShared::MemoizedLocked<std::set<int>> m_Contents;
+ MOBase::MemoizedLocked<std::shared_ptr<const MOBase::IFileTree>> m_FileTree;
+ MOBase::MemoizedLocked<bool> m_Valid;
+ MOBase::MemoizedLocked<std::set<int>> m_Contents;
MOShared::DirectoryEntry **m_DirectoryStructure;
diff --git a/src/organizercore.cpp b/src/organizercore.cpp
index f57903e2..a9e62e77 100644
--- a/src/organizercore.cpp
+++ b/src/organizercore.cpp
@@ -287,6 +287,7 @@ void OrganizerCore::connectPlugins(PluginContainer *container)
m_InstallationManager.getSupportedExtensions());
m_PluginContainer = container;
m_Updater.setPluginContainer(m_PluginContainer);
+ m_InstallationManager.setPluginContainer(m_PluginContainer);
m_DownloadManager.setPluginContainer(m_PluginContainer);
m_ModList.setPluginContainer(m_PluginContainer);
@@ -294,6 +295,11 @@ void OrganizerCore::connectPlugins(PluginContainer *container)
m_GamePlugin = m_PluginContainer->managedGame(m_GameName);
emit managedGameChanged(m_GamePlugin);
}
+
+ connect(m_PluginContainer, &PluginContainer::pluginEnabled,
+ [&](IPlugin* plugin) { m_PluginEnabled(plugin); });
+ connect(m_PluginContainer, &PluginContainer::pluginDisabled,
+ [&](IPlugin* plugin) { m_PluginDisabled(plugin); });
}
void OrganizerCore::disconnectPlugins()
@@ -959,13 +965,13 @@ QStringList OrganizerCore::findFiles(
{
QStringList result;
DirectoryEntry *dir = m_DirectoryStructure;
- if (!path.isEmpty())
+ if (!path.isEmpty() && path != ".")
dir = dir->findSubDirectoryRecursive(ToWString(path));
if (dir != nullptr) {
std::vector<FileEntryPtr> files = dir->getFiles();
for (FileEntryPtr &file: files) {
QString fullPath = ToQString(file->getFullPath());
- if (filter(fullPath)) {
+ if (filter(ToQString(file->getName()))) {
result.append(fullPath);
}
}
@@ -1224,6 +1230,16 @@ bool OrganizerCore::onPluginSettingChanged(std::function<void(QString const&, co
return m_PluginSettingChanged.connect(func).connected();
}
+bool OrganizerCore::onPluginEnabled(std::function<void(const IPlugin*)> const& func)
+{
+ return m_PluginEnabled.connect(func).connected();
+}
+
+bool OrganizerCore::onPluginDisabled(std::function<void(const IPlugin*)> const& func)
+{
+ return m_PluginDisabled.connect(func).connected();
+}
+
void OrganizerCore::refresh(bool saveChanges)
{
// don't lose changes!
@@ -1449,10 +1465,13 @@ void OrganizerCore::loggedInAction(QWidget* parent, std::function<void ()> f)
void OrganizerCore::requestDownload(const QUrl &url, QNetworkReply *reply)
{
- if (m_PluginContainer != nullptr) {
- for (IPluginModPage *modPage :
- m_PluginContainer->plugins<MOBase::IPluginModPage>()) {
- ModRepositoryFileInfo *fileInfo = new ModRepositoryFileInfo();
+ if (!m_PluginContainer) {
+ return;
+ }
+ for (IPluginModPage *modPage :
+ m_PluginContainer->plugins<MOBase::IPluginModPage>()) {
+ if (m_PluginContainer->isEnabled(modPage)) {
+ ModRepositoryFileInfo* fileInfo = new ModRepositoryFileInfo();
if (modPage->handlesDownload(url, reply->url(), *fileInfo)) {
fileInfo->repository = modPage->name();
m_DownloadManager.addDownload(reply, fileInfo);
@@ -2040,7 +2059,7 @@ std::vector<Mapping> OrganizerCore::fileMapping(const QString &profileName,
for (MOBase::IPluginFileMapper *mapper :
m_PluginContainer->plugins<MOBase::IPluginFileMapper>()) {
IPlugin *plugin = dynamic_cast<IPlugin *>(mapper);
- if (plugin->isActive()) {
+ if (m_PluginContainer->isEnabled(plugin)) {
MappingType pluginMap = mapper->mappings();
result.reserve(result.size() + pluginMap.size());
result.insert(result.end(), pluginMap.begin(), pluginMap.end());
diff --git a/src/organizercore.h b/src/organizercore.h
index b566e626..23b624e8 100644
--- a/src/organizercore.h
+++ b/src/organizercore.h
@@ -87,6 +87,7 @@ private:
using SignalProfileRemoved = boost::signals2::signal<void(QString const&)>;
using SignalProfileChanged = boost::signals2::signal<void (MOBase::IProfile *, MOBase::IProfile *)>;
using SignalPluginSettingChanged = boost::signals2::signal<void (QString const&, const QString& key, const QVariant&, const QVariant&)>;
+ using SignalPluginEnabled = boost::signals2::signal<void(MOBase::IPlugin*)>;
public:
@@ -335,6 +336,8 @@ public:
bool onProfileRemoved(std::function<void(QString const&)> const& func);
bool onProfileChanged(std::function<void(MOBase::IProfile*, MOBase::IProfile*)> const& func);
bool onPluginSettingChanged(std::function<void(QString const&, const QString& key, const QVariant&, const QVariant&)> const& func);
+ bool onPluginEnabled(std::function<void(const MOBase::IPlugin*)> const& func);
+ bool onPluginDisabled(std::function<void(const MOBase::IPlugin*)> const& func);
bool getArchiveParsing() const
{
@@ -457,6 +460,8 @@ private:
SignalProfileRemoved m_ProfileRemoved;
SignalProfileChanged m_ProfileChanged;
SignalPluginSettingChanged m_PluginSettingChanged;
+ SignalPluginEnabled m_PluginEnabled;
+ SignalPluginEnabled m_PluginDisabled;
ModList m_ModList;
PluginList m_PluginList;
diff --git a/src/organizerproxy.cpp b/src/organizerproxy.cpp
index a988ba9f..92f250ea 100644
--- a/src/organizerproxy.cpp
+++ b/src/organizerproxy.cpp
@@ -83,6 +83,16 @@ void OrganizerProxy::modDataChanged(IModInterface *mod)
m_Proxied->modDataChanged(mod);
}
+bool OrganizerProxy::isPluginEnabled(QString const& pluginName) const
+{
+ return m_PluginContainer->isEnabled(pluginName);
+}
+
+bool OrganizerProxy::isPluginEnabled(IPlugin* plugin) const
+{
+ return m_PluginContainer->isEnabled(plugin);
+}
+
QVariant OrganizerProxy::pluginSetting(const QString &pluginName, const QString &key) const
{
return m_Proxied->pluginSetting(pluginName, key);
@@ -134,7 +144,7 @@ HANDLE OrganizerProxy::startApplication(
return runner.stealProcessHandle().release();
}
-bool OrganizerProxy::waitForApplication(HANDLE handle, LPDWORD exitCode) const
+bool OrganizerProxy::waitForApplication(HANDLE handle, bool refresh, LPDWORD exitCode) const
{
const auto pid = ::GetProcessId(handle);
@@ -144,8 +154,14 @@ bool OrganizerProxy::waitForApplication(HANDLE handle, LPDWORD exitCode) const
auto runner = m_Proxied->processRunner();
+ ProcessRunner::WaitFlags waitFlags = ProcessRunner::ForceWait;
+
+ if (refresh) {
+ waitFlags |= ProcessRunner::Refresh;
+ }
+
const auto r = runner
- .setWaitForCompletion(ProcessRunner::ForceWait, UILocker::OutputRequired)
+ .setWaitForCompletion(waitFlags, UILocker::OutputRequired)
.attachToProcess(handle);
if (exitCode) {
@@ -283,8 +299,36 @@ bool OrganizerProxy::onProfileChanged(std::function<void(MOBase::IProfile*, MOBa
return m_Proxied->onProfileChanged(MOShared::callIfPluginActive(this, func));
}
+// Always call these one, otherwise plugin cannot detect they are being enabled / disabled:
bool OrganizerProxy::onPluginSettingChanged(std::function<void(QString const&, const QString& key, const QVariant&, const QVariant&)> const& func)
{
- // Always call this one, otherwise plugin cannot detect they are being enabled / disabled:
return m_Proxied->onPluginSettingChanged(func);
}
+
+bool OrganizerProxy::onPluginEnabled(std::function<void(const IPlugin*)> const& func)
+{
+ return m_Proxied->onPluginEnabled(func);
+}
+
+bool OrganizerProxy::onPluginEnabled(const QString& pluginName, std::function<void()> const& func)
+{
+ return m_Proxied->onPluginEnabled([=](const IPlugin* plugin) {
+ if (plugin->name().compare(pluginName, Qt::CaseInsensitive) == 0) {
+ func();
+ }
+ });
+}
+
+bool OrganizerProxy::onPluginDisabled(std::function<void(const IPlugin*)> const& func)
+{
+ return m_Proxied->onPluginDisabled(func);
+}
+
+bool OrganizerProxy::onPluginDisabled(const QString& pluginName, std::function<void()> const& func)
+{
+ return m_Proxied->onPluginDisabled([=](const IPlugin* plugin) {
+ if (plugin->name().compare(pluginName, Qt::CaseInsensitive) == 0) {
+ func();
+ }
+ });
+}
diff --git a/src/organizerproxy.h b/src/organizerproxy.h
index 6690d612..3097adf0 100644
--- a/src/organizerproxy.h
+++ b/src/organizerproxy.h
@@ -35,8 +35,6 @@ public:
virtual MOBase::IPluginGame *getGame(const QString &gameName) const;
virtual MOBase::IModInterface *createMod(MOBase::GuessedValue<QString> &name);
virtual void modDataChanged(MOBase::IModInterface *mod);
- virtual QVariant pluginSetting(const QString &pluginName, const QString &key) const;
- virtual void setPluginSetting(const QString &pluginName, const QString &key, const QVariant &value);
virtual QVariant persistent(const QString &pluginName, const QString &key, const QVariant &def = QVariant()) const;
virtual void setPersistent(const QString &pluginName, const QString &key, const QVariant &value, bool sync = true);
virtual QString pluginDataPath() const;
@@ -54,7 +52,7 @@ public:
virtual MOBase::IProfile *profile() const override;
virtual HANDLE startApplication(const QString &executable, const QStringList &args = QStringList(), const QString &cwd = "",
const QString &profile = "", const QString &forcedCustomOverwrite = "", bool ignoreCustomOverwrite = false);
- virtual bool waitForApplication(HANDLE handle, LPDWORD exitCode = nullptr) const;
+ virtual bool waitForApplication(HANDLE handle, bool refresh = true, LPDWORD exitCode = nullptr) const;
virtual void refresh(bool saveChanges);
virtual bool onAboutToRun(const std::function<bool(const QString&)> &func) override;
@@ -64,7 +62,17 @@ public:
virtual bool onProfileRenamed(std::function<void(MOBase::IProfile*, QString const&, QString const&)> const& func) override;
virtual bool onProfileRemoved(std::function<void(QString const&)> const& func) override;
virtual bool onProfileChanged(std::function<void(MOBase::IProfile*, MOBase::IProfile*)> const& func) override;
+
+ // Plugin related:
+ virtual bool isPluginEnabled(QString const& pluginName) const override;
+ virtual bool isPluginEnabled(MOBase::IPlugin* plugin) const override;
+ virtual QVariant pluginSetting(const QString& pluginName, const QString& key) const override;
+ virtual void setPluginSetting(const QString& pluginName, const QString& key, const QVariant& value) override;
virtual bool onPluginSettingChanged(std::function<void(QString const&, const QString& key, const QVariant&, const QVariant&)> const& func) override;
+ virtual bool onPluginEnabled(std::function<void(const MOBase::IPlugin*)> const& func) override;
+ virtual bool onPluginEnabled(const QString& pluginName, std::function<void()> const& func) override;
+ virtual bool onPluginDisabled(std::function<void(const MOBase::IPlugin*)> const& func) override;
+ virtual bool onPluginDisabled(const QString& pluginName, std::function<void()> const& func) override;
virtual MOBase::IPluginGame const *managedGame() const;
diff --git a/src/plugincontainer.cpp b/src/plugincontainer.cpp
index 5992f28d..44c9a651 100644
--- a/src/plugincontainer.cpp
+++ b/src/plugincontainer.cpp
@@ -21,6 +21,54 @@ using namespace MOShared;
namespace bf = boost::fusion;
+// Welcome to the wonderful world of MO2 plugin management!
+//
+// We'll start by the C++ side.
+//
+// There are 9 types of MO2 plugins, two of which cannot be standalone: IPluginDiagnose
+// and IPluginFileMapper. This means that you can have a class implementing IPluginGame,
+// IPluginDiagnose and IPluginFileMapper. It is not possible for a class to implement
+// two full plugin types (e.g. IPluginPreview and IPluginTool).
+//
+// Plugins are fetch as QObject initially and must be "qobject-casted" to the right type.
+//
+// Plugins are stored in the PluginContainer class in various C++ containers: there is a vector
+// that stores all the plugin as QObject, multiple vectors that stores the plugin of each types,
+// a map to find IPlugin object from their names or from IPluginDiagnose or IFileMapper (since
+// these do not inherit IPlugin, they cannot be downcasted).
+//
+// Requirements for plugins are stored in m_Requirements:
+// - IPluginGame cannot be enabled by user. A game plugin is considered enable only if it is
+// the one corresponding to the currently managed games.
+// - If a plugin has a master plugin (IPlugin::master()), it cannot be enabled/disabled by users,
+// and will follow the enabled/disabled state of its parent.
+// - Each plugin has an "enabled" setting stored in persistence. A plugin is considered disabled
+// if the setting is false.
+// - If the setting is true or does not exist, a plugin is considered disabled if one of its
+// requirements is not met.
+// - Users cannot enable a plugin if one of its requirements is not met.
+//
+// Now let's move to the Proxy side... Or the as of now, the Python side.
+//
+// Proxied plugins are much more annoying because they can implement all interfaces, and are
+// given to MO2 as separate plugins... A Python class implementing IPluginGame and IPluginDiagnose
+// will be seen by MO2 as two separate QObject, and they will all have the same name.
+//
+// When a proxied plugin is registered, a few things must be taken care of:
+// - There can only be one plugin mapped to a name in the PluginContainer class, so we keep the
+// plugin corresponding to the most relevant class (see PluginTypeOrder), e.g. if the class
+// inherits both IPluginGame and IPluginFileMapper, we map the name to the C++ QObject corresponding
+// to the IPluginGame.
+// - When a proxied plugin implements multiple interfaces, the IPlugin corresponding to the most
+// important interface is set as the parent (hidden) of the other IPlugin through PluginRequirements.
+// This way, the plugin are managed together (enabled/disabled state). The "fake" children plugins
+// will not be returned by PluginRequirements::children().
+// - Since each interface corresponds to a different QObject, we need to take care not to call
+// IPlugin::init() on each QObject, but only on the first one.
+//
+// All the proxied plugins are linked to the proxy plugin by PluginRequirements. If the proxy plugin
+// is disabled, the proxied plugins are not even loaded so not visible in the plugin management tab.
+
template <class T>
struct PluginTypeName;
@@ -51,9 +99,188 @@ QStringList PluginContainer::pluginInterfaces()
}
+// PluginRequirementProxy
+
+const std::set<QString> PluginRequirements::s_CorePlugins{
+ "INI Bakery"
+};
+
+PluginRequirements::PluginRequirements(
+ PluginContainer* pluginContainer, MOBase::IPlugin* plugin, IOrganizer* proxy,
+ MOBase::IPluginProxy* pluginProxy)
+ : m_PluginContainer(pluginContainer)
+ , m_Plugin(plugin)
+ , m_PluginProxy(pluginProxy)
+ , m_Master(nullptr)
+ , m_Organizer(proxy)
+{
+ // There are a lots of things we cannot set here (e.g. m_Master) because we do not
+ // know the order plugins are loaded.
+}
+
+void PluginRequirements::fetchRequirements() {
+ m_Requirements = m_Plugin->requirements();
+}
+
+IPluginProxy* PluginRequirements::proxy() const
+{
+ return m_PluginProxy;
+}
+
+std::vector<IPlugin*> PluginRequirements::proxied() const
+{
+ std::vector<IPlugin*> children;
+ if (dynamic_cast<IPluginProxy*>(m_Plugin)) {
+ for (auto* obj : m_PluginContainer->plugins<QObject>()) {
+ auto* plugin = qobject_cast<IPlugin*>(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<IPlugin*> PluginRequirements::children() const
+{
+ std::vector<IPlugin*> children;
+ for (auto* obj : m_PluginContainer->plugins<QObject>()) {
+ auto* plugin = qobject_cast<IPlugin*>(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<IPluginRequirement::Problem> PluginRequirements::problems() const
+{
+ std::vector<IPluginRequirement::Problem> 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<IPluginGame>(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<const GameDependencyRequirement*>(requirement.get())) {
+ return gdep->gameNames();
+ }
+ }
+
+ return {};
+}
+
+std::vector<MOBase::IPlugin*> PluginRequirements::requiredFor() const
+{
+ std::vector<MOBase::IPlugin*> required;
+ std::set<MOBase::IPlugin*> visited;
+ requiredFor(required, visited);
+ return required;
+}
+
+
+void PluginRequirements::requiredFor(std::vector<MOBase::IPlugin*> &required, std::set<MOBase::IPlugin*>& 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<const PluginDependencyRequirement*>(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)
{
}
@@ -62,27 +289,23 @@ PluginContainer::~PluginContainer() {
unloadPlugins();
}
-
-void PluginContainer::setUserInterface(IUserInterface *userInterface, QWidget *widget)
+void PluginContainer::setUserInterface(IUserInterface *userInterface)
{
- for (IPluginProxy *proxy : bf::at_key<IPluginProxy>(m_Plugins)) {
- proxy->setParentWidget(widget);
- }
-
- if (userInterface != nullptr) {
- for (IPluginModPage *modPage : bf::at_key<IPluginModPage>(m_Plugins)) {
- userInterface->registerModPage(modPage);
+ if (userInterface) {
+ for (IPluginProxy* proxy : bf::at_key<IPluginProxy>(m_Plugins)) {
+ proxy->setParentWidget(userInterface->mainWindow());
}
-
- for (IPluginTool *tool : bf::at_key<IPluginTool>(m_Plugins)) {
- userInterface->registerPluginTool(tool);
+ for (IPluginModPage* modPage : bf::at_key<IPluginModPage>(m_Plugins)) {
+ modPage->setParentWidget(userInterface->mainWindow());
+ }
+ for (IPluginTool* tool : bf::at_key<IPluginTool>(m_Plugins)) {
+ tool->setParentWidget(userInterface->mainWindow());
}
}
m_UserInterface = userInterface;
}
-
QStringList PluginContainer::implementedInterfaces(IPlugin* plugin) const
{
// We need a QObject to be able to qobject_cast<> to the plugin types:
@@ -92,6 +315,11 @@ QStringList PluginContainer::implementedInterfaces(IPlugin* plugin) const
return {};
}
+ return implementedInterfaces(oPlugin);
+}
+
+QStringList PluginContainer::implementedInterfaces(QObject * oPlugin) const
+{
// Find all the names:
QStringList names;
boost::mp11::mp_for_each<PluginTypeOrder>([oPlugin, &names](const auto* p) {
@@ -112,32 +340,28 @@ QStringList PluginContainer::implementedInterfaces(IPlugin* plugin) const
return names;
}
-
QString PluginContainer::topImplementedInterface(IPlugin* plugin) const
{
- // We need a QObject to be able to qobject_cast<> to the plugin types:
- QObject* oPlugin = as_qobject(plugin);
-
- if (!oPlugin) {
- return {};
- }
+ auto interfaces = implementedInterfaces(plugin);
+ return interfaces.isEmpty() ? "" : interfaces[0];
+}
- // Find all the names:
- QString name;
- boost::mp11::mp_for_each<PluginTypeOrder>([oPlugin, &name](auto* p) {
+bool PluginContainer::isBetterInterface(QObject* lhs, QObject* rhs) const
+{
+ int count = 0, lhsIdx = -1, rhsIdx = -1;
+ boost::mp11::mp_for_each<PluginTypeOrder>([&](const auto* p) {
using plugin_type = std::decay_t<decltype(*p)>;
- if (name.isEmpty() && qobject_cast<plugin_type*>(oPlugin)) {
- auto tname = PluginTypeName<plugin_type>::value();
- if (!tname.isEmpty()) {
- name = tname;
- }
+ if (lhsIdx < 0 && qobject_cast<plugin_type*>(lhs)) {
+ lhsIdx = count;
}
- });
-
- return name;
+ if (rhsIdx < 0 && qobject_cast<plugin_type*>(rhs)) {
+ rhsIdx = count;
+ }
+ ++count;
+ });
+ return lhsIdx < rhsIdx;
}
-
QStringList PluginContainer::pluginFileNames() const
{
QStringList result;
@@ -153,14 +377,13 @@ QStringList PluginContainer::pluginFileNames() const
return result;
}
-
QObject* PluginContainer::as_qobject(MOBase::IPlugin* plugin) const
{
// Find the correspond QObject - Can this be done safely with a cast?
auto& objects = bf::at_key<QObject>(m_Plugins);
auto it = std::find_if(std::begin(objects), std::end(objects), [plugin](QObject* obj) {
return qobject_cast<IPlugin*>(obj) == plugin;
- });
+ });
if (it == std::end(objects)) {
return nullptr;
@@ -169,7 +392,7 @@ QObject* PluginContainer::as_qobject(MOBase::IPlugin* plugin) const
return *it;
}
-bool PluginContainer::initPlugin(IPlugin *plugin)
+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
@@ -182,69 +405,96 @@ bool PluginContainer::initPlugin(IPlugin *plugin)
return false;
}
+ OrganizerProxy* proxy = nullptr;
if (m_Organizer) {
- auto* proxy = new OrganizerProxy(m_Organizer, this, plugin);
-
- if (!plugin->init(proxy)) {
- log::warn("plugin failed to initialize");
- return false;
- }
+ proxy = new OrganizerProxy(m_Organizer, this, plugin);
}
- return true;
-}
+ // Check if it is a proxy plugin:
+ bool isProxy = dynamic_cast<IPluginProxy*>(plugin);
-bool PluginContainer::initProxyPlugin(IPlugin *plugin)
-{
- // see initPlugin() above for info
+ auto [it, bl] = m_Requirements.emplace(plugin, PluginRequirements(this, plugin, proxy, pluginProxy));
- if (plugin == nullptr) {
- return false;
+ if (!m_Organizer && !isProxy) {
+ return true;
}
- IOrganizer* proxy = nullptr;
- if (m_Organizer) {
- proxy = new OrganizerProxy(m_Organizer, this, plugin);
+ if (skipInit) {
+ return true;
}
if (!plugin->init(proxy)) {
- log::warn("proxy plugin failed to initialize");
+ 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 });
}
-bool PluginContainer::registerPlugin(QObject *plugin, const QString &fileName)
+IPlugin* PluginContainer::registerPlugin(QObject *plugin, const QString &fileName, MOBase::IPluginProxy* pluginProxy)
{
+
+ // generic treatment for all plugins
+ IPlugin *pluginObj = qobject_cast<IPlugin*>(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<QString>(m_AccessPlugins);
+ if (mapNames.contains(pluginObj->name())) {
+
+ IPlugin* other = mapNames[pluginObj->name()];
+
+ // If both plugins are from the same proxy and the same file, this is usually
+ // ok (in theory some one could write two different classes from the same Python file/module):
+ if (pluginProxy && m_Requirements.at(other).proxy() == pluginProxy
+ && as_qobject(other)->property("filename") == fileName) {
+
+ // 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<QString>(m_AccessPlugins)[pluginObj->name()] = pluginObj;
+ }
+ }
+ else {
+ log::warn("Trying to register two plugins with the name '{}', the second one will not be registered.",
+ pluginObj->name());
+ return nullptr;
+ }
+ }
+ else {
+ bf::at_key<QString>(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<QObject>(m_Plugins).push_back(plugin);
- { // generic treatment for all plugins
- IPlugin *pluginObj = qobject_cast<IPlugin*>(plugin);
- if (pluginObj == nullptr) {
- log::debug("not an IPlugin");
- return false;
- }
-
- plugin->setProperty("filename", fileName);
+ plugin->setProperty("filename", fileName);
- if (m_Organizer) {
- m_Organizer->settings().plugins().registerPlugin(pluginObj);
- }
+ if (m_Organizer) {
+ m_Organizer->settings().plugins().registerPlugin(pluginObj);
}
{ // diagnosis plugin
IPluginDiagnose *diagnose = qobject_cast<IPluginDiagnose*>(plugin);
if (diagnose != nullptr) {
bf::at_key<IPluginDiagnose>(m_Plugins).push_back(diagnose);
+ bf::at_key<IPluginDiagnose>(m_AccessPlugins)[diagnose] = pluginObj;
m_DiagnosisConnections.push_back(
diagnose->onInvalidated([&] () { emit diagnosisUpdate(); })
);
@@ -254,67 +504,75 @@ bool PluginContainer::registerPlugin(QObject *plugin, const QString &fileName)
IPluginFileMapper *mapper = qobject_cast<IPluginFileMapper*>(plugin);
if (mapper != nullptr) {
bf::at_key<IPluginFileMapper>(m_Plugins).push_back(mapper);
+ bf::at_key<IPluginFileMapper>(m_AccessPlugins)[mapper] = pluginObj;
}
}
{ // mod page plugin
IPluginModPage *modPage = qobject_cast<IPluginModPage*>(plugin);
- if (initPlugin(modPage)) {
+ if (initPlugin(modPage, pluginProxy, skipInit)) {
bf::at_key<IPluginModPage>(m_Plugins).push_back(modPage);
- return true;
+ return modPage;
}
}
{ // game plugin
IPluginGame *game = qobject_cast<IPluginGame*>(plugin);
-
if (game) {
game->detectGame();
- }
-
- if (initPlugin(game)) {
- bf::at_key<IPluginGame>(m_Plugins).push_back(game);
- registerGame(game);
- return true;
+ if (initPlugin(game, pluginProxy, skipInit)) {
+ bf::at_key<IPluginGame>(m_Plugins).push_back(game);
+ registerGame(game);
+ return game;
+ }
}
}
{ // tool plugins
IPluginTool *tool = qobject_cast<IPluginTool*>(plugin);
- if (initPlugin(tool)) {
+ if (initPlugin(tool, pluginProxy, skipInit)) {
bf::at_key<IPluginTool>(m_Plugins).push_back(tool);
- return true;
+ return tool;
}
}
{ // installer plugins
IPluginInstaller *installer = qobject_cast<IPluginInstaller*>(plugin);
- if (initPlugin(installer)) {
+ if (initPlugin(installer, pluginProxy, skipInit)) {
bf::at_key<IPluginInstaller>(m_Plugins).push_back(installer);
if (m_Organizer) {
m_Organizer->installationManager()->registerInstaller(installer);
}
- return true;
+ return installer;
}
}
{ // preview plugins
IPluginPreview *preview = qobject_cast<IPluginPreview*>(plugin);
- if (initPlugin(preview)) {
+ if (initPlugin(preview, pluginProxy, skipInit)) {
bf::at_key<IPluginPreview>(m_Plugins).push_back(preview);
m_PreviewGenerator.registerPlugin(preview);
- return true;
+ return preview;
}
}
{ // proxy plugins
IPluginProxy *proxy = qobject_cast<IPluginProxy*>(plugin);
- if (initProxyPlugin(proxy)) {
+ if (initPlugin(proxy, pluginProxy, skipInit)) {
bf::at_key<IPluginProxy>(m_Plugins).push_back(proxy);
QStringList pluginNames = proxy->pluginList(
QCoreApplication::applicationDirPath() + "/" + ToQString(AppConfig::pluginPath()));
for (const QString &pluginName : pluginNames) {
try {
- // we get a list of matching plugins as proxies don't necessarily have a good way of supporting multiple inheritance
+ // We get a list of matching plugins as proxies can return multiple plugins
+ // per file and do not have a good way of supporting multiple inheritance.
QList<QObject*> matchingPlugins = proxy->instantiate(pluginName);
+
+ // We are going to group plugin by names and "fix" them later:
+ std::map<QString, std::vector<IPlugin*>> proxiedByNames;
+
for (QObject *proxiedPlugin : matchingPlugins) {
if (proxiedPlugin != nullptr) {
- if (registerPlugin(proxiedPlugin, pluginName)) {
- log::debug("loaded plugin \"{}\"", QFileInfo(pluginName).fileName());
+ if (IPlugin* proxied = registerPlugin(proxiedPlugin, pluginName, proxy); proxied) {
+ log::debug("loaded plugin '{}' from '{}' - [{}]",
+ proxied->name(), QFileInfo(pluginName).fileName(), implementedInterfaces(proxied).join(", "));
+
+ // Store the plugin for later:
+ proxiedByNames[proxied->name()].push_back(proxied);
}
else {
log::warn(
@@ -324,37 +582,43 @@ bool PluginContainer::registerPlugin(QObject *plugin, const QString &fileName)
}
}
}
+
+ // Fake masters:
+ for (auto& [name, proxiedPlugins] : proxiedByNames) {
+ if (proxiedPlugins.size() > 1) {
+ auto it = std::min_element(std::begin(proxiedPlugins), std::end(proxiedPlugins),
+ [&](auto const& lhs, auto const& rhs) {
+ return isBetterInterface(as_qobject(lhs), as_qobject(rhs));
+ });
+
+ for (auto& proxiedPlugin : proxiedPlugins) {
+ if (proxiedPlugin != *it) {
+ m_Requirements.at(proxiedPlugin).setMaster(*it);
+ }
+ }
+ }
+ }
+
} catch (const std::exception &e) {
reportError(QObject::tr("failed to initialize plugin %1: %2").arg(pluginName).arg(e.what()));
}
}
- return true;
+ return proxy;
}
}
{ // dummy plugins
// only initialize these, no processing otherwise
IPlugin *dummy = qobject_cast<IPlugin*>(plugin);
- if (initPlugin(dummy)) {
+ if (initPlugin(dummy, pluginProxy, skipInit)) {
bf::at_key<IPlugin>(m_Plugins).push_back(dummy);
- return true;
+ return dummy;
}
}
- log::debug("no matching plugin interface");
-
- return false;
+ return nullptr;
}
-struct clearPlugins
-{
- template<typename T>
- void operator()(T& t) const
- {
- t.second.clear();
- }
-};
-
void PluginContainer::unloadPlugins()
{
if (m_UserInterface != nullptr) {
@@ -366,7 +630,9 @@ void PluginContainer::unloadPlugins()
m_Organizer->disconnectPlugins();
}
- bf::for_each(m_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();
for (const boost::signals2::connection &connection : m_DiagnosisConnections) {
connection.disconnect();
@@ -383,6 +649,110 @@ void PluginContainer::unloadPlugins()
}
}
+IPlugin* PluginContainer::managedGame() const
+{
+ // TODO: This const_cast is safe but ugly. Most methods require a IPlugin*, so
+ // returning a const-version if painful. This should be fixed by making methods accept
+ // a const IPlugin* instead, but there are a few tricks with qobject_cast and const.
+ return const_cast<IPluginGame*>(m_Organizer->managedGame());
+}
+
+bool PluginContainer::isEnabled(IPlugin* plugin) const
+{
+ // Check if it's a game plugin:
+ if (implementInterface<IPluginGame>(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", true).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<QString>(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<IPluginDiagnose>(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<IPluginFileMapper>(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);
+}
+
IPluginGame *PluginContainer::managedGame(const QString &name) const
{
auto iter = m_SupportedGames.find(name);
@@ -405,7 +775,7 @@ void PluginContainer::loadPlugins()
unloadPlugins();
for (QObject *plugin : QPluginLoader::staticInstances()) {
- registerPlugin(plugin, "");
+ registerPlugin(plugin, "", nullptr);
}
QFile loadCheck;
@@ -501,8 +871,9 @@ void PluginContainer::loadPlugins()
"failed to load plugin {}: {}",
pluginName, pluginLoader->errorString());
} else {
- if (registerPlugin(pluginLoader->instance(), pluginName)) {
- log::debug("loaded plugin \"{}\"", QFileInfo(pluginName).fileName());
+ if (IPlugin* plugin = registerPlugin(pluginLoader->instance(), pluginName, nullptr); plugin) {
+ log::debug("loaded plugin '{}' from '{}' - [{}]",
+ plugin->name(), QFileInfo(pluginName).fileName(), implementedInterfaces(plugin).join(", "));
m_PluginLoaders.push_back(pluginLoader.release());
} else {
m_FailedPlugins.push_back(pluginName);
diff --git a/src/plugincontainer.h b/src/plugincontainer.h
index 2b39726b..26e12659 100644
--- a/src/plugincontainer.h
+++ b/src/plugincontainer.h
@@ -22,8 +22,107 @@ class IUserInterface;
#include <boost/mp11.hpp>
#endif // Q_MOC_RUN
#include <vector>
+#include <memory>
+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<MOBase::IPlugin*> proxied() const;
+
+ /**
+ * @return the master of this plugin, if any.
+ */
+ MOBase::IPlugin* master() const;
+
+ /**
+ * @return the plugins this plugin is master of.
+ */
+ std::vector<MOBase::IPlugin*> children() const;
+
+ /**
+ * @return the list of problems to be resolved before enabling the plugin.
+ */
+ std::vector<MOBase::IPluginRequirement::Problem> 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<MOBase::IPlugin*> requiredFor() const;
+
+private:
+
+ // The list of "Core" plugins.
+ static const std::set<QString> s_CorePlugins;
+
+ // Accumulator version for requiredFor() to avoid infinite recursion.
+ void requiredFor(std::vector<MOBase::IPlugin*>& required, std::set<MOBase::IPlugin*>& 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 PluginContainer;
+
+ PluginContainer* m_PluginContainer;
+ MOBase::IPlugin* m_Plugin;
+ MOBase::IPluginProxy* m_PluginProxy;
+ MOBase::IPlugin* m_Master;
+ std::vector<std::shared_ptr<const MOBase::IPluginRequirement>> m_Requirements;
+ MOBase::IOrganizer* m_Organizer;
+ std::vector<MOBase::IPlugin*> m_RequiredFor;
+
+ PluginRequirements(
+ PluginContainer* pluginContainer, MOBase::IPlugin* plugin,
+ MOBase::IOrganizer* proxy, MOBase::IPluginProxy* pluginProxy);
+
+};
+
+
+/**
+ *
+ */
class PluginContainer : public QObject, public MOBase::IPluginDiagnose
{
@@ -32,18 +131,24 @@ class PluginContainer : public QObject, public MOBase::IPluginDiagnose
private:
- typedef boost::fusion::map<
- boost::fusion::pair<QObject, std::vector<QObject*>>,
- boost::fusion::pair<MOBase::IPlugin, std::vector<MOBase::IPlugin*>>,
- boost::fusion::pair<MOBase::IPluginDiagnose, std::vector<MOBase::IPluginDiagnose*>>,
- boost::fusion::pair<MOBase::IPluginGame, std::vector<MOBase::IPluginGame*>>,
- boost::fusion::pair<MOBase::IPluginInstaller, std::vector<MOBase::IPluginInstaller*>>,
- boost::fusion::pair<MOBase::IPluginModPage, std::vector<MOBase::IPluginModPage*>>,
- boost::fusion::pair<MOBase::IPluginPreview, std::vector<MOBase::IPluginPreview*>>,
- boost::fusion::pair<MOBase::IPluginTool, std::vector<MOBase::IPluginTool*>>,
- boost::fusion::pair<MOBase::IPluginProxy, std::vector<MOBase::IPluginProxy*>>,
- boost::fusion::pair<MOBase::IPluginFileMapper, std::vector<MOBase::IPluginFileMapper*>>
- > PluginMap;
+ using PluginMap = boost::fusion::map<
+ boost::fusion::pair<QObject, std::vector<QObject*>>,
+ boost::fusion::pair<MOBase::IPlugin, std::vector<MOBase::IPlugin*>>,
+ boost::fusion::pair<MOBase::IPluginDiagnose, std::vector<MOBase::IPluginDiagnose*>>,
+ boost::fusion::pair<MOBase::IPluginGame, std::vector<MOBase::IPluginGame*>>,
+ boost::fusion::pair<MOBase::IPluginInstaller, std::vector<MOBase::IPluginInstaller*>>,
+ boost::fusion::pair<MOBase::IPluginModPage, std::vector<MOBase::IPluginModPage*>>,
+ boost::fusion::pair<MOBase::IPluginPreview, std::vector<MOBase::IPluginPreview*>>,
+ boost::fusion::pair<MOBase::IPluginTool, std::vector<MOBase::IPluginTool*>>,
+ boost::fusion::pair<MOBase::IPluginProxy, std::vector<MOBase::IPluginProxy*>>,
+ boost::fusion::pair<MOBase::IPluginFileMapper, std::vector<MOBase::IPluginFileMapper*>>
+ >;
+
+ using AccessPluginMap = boost::fusion::map<
+ boost::fusion::pair<MOBase::IPluginDiagnose, std::map<MOBase::IPluginDiagnose*, MOBase::IPlugin*>>,
+ boost::fusion::pair<MOBase::IPluginFileMapper, std::map<MOBase::IPluginFileMapper*, MOBase::IPlugin*>>,
+ boost::fusion::pair<QString, std::map<QString, MOBase::IPlugin*>>
+ >;
static const unsigned int PROBLEM_PLUGINSNOTLOADED = 1;
@@ -83,7 +188,7 @@ public:
PluginContainer(OrganizerCore *organizer);
virtual ~PluginContainer();
- void setUserInterface(IUserInterface *userInterface, QWidget *widget);
+ void setUserInterface(IUserInterface *userInterface);
void loadPlugins();
void unloadPlugins();
@@ -113,6 +218,81 @@ public:
}
/**
+ * @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 <typename T>
+ 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<T*>(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;
+
+ /**
+ * @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.
*
@@ -154,10 +334,43 @@ public: // IPluginDiagnose interface
signals:
+ /**
+ * @brief Emitted plugins are enabled or disabled.
+ */
+ void pluginEnabled(MOBase::IPlugin*);
+ void pluginDisabled(MOBase::IPlugin*);
+
void diagnosisUpdate();
private:
+ friend class PluginRequirements;
+
+
+ /**
+ * @brief Retrieved the (localized) names of interfaces implemented by the given
+ * plugin.
+ *
+ * @param plugin The plugin to retrieve interface for.
+ *
+ * @return the (localized) names of interfaces implemented by this plugin.
+ *
+ * @note This function can be used to get implemented interfaces before registering
+ * a plugin.
+ */
+ QStringList implementedInterfaces(QObject* plugin) const;
+
+ /**
+ * @brief Check if a plugin implements a "better" interface than another
+ * one, as specified by PluginTypeOrder.
+ *
+ * @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.
*
@@ -167,11 +380,21 @@ private:
*/
QObject* as_qobject(MOBase::IPlugin* plugin) const;
+ /**
+ * @brief Initialize a plugin.
+ *
+ * @param plugin The plugin to initialize.
+ * @param proxy The proxy that created this plugin (can be null).
+ * @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);
- bool initPlugin(MOBase::IPlugin *plugin);
- bool initProxyPlugin(MOBase::IPlugin *plugin);
void registerGame(MOBase::IPluginGame *game);
- bool registerPlugin(QObject *pluginObj, const QString &fileName);
+
+ MOBase::IPlugin* registerPlugin(QObject *pluginObj, const QString &fileName, MOBase::IPluginProxy *proxy);
OrganizerCore *m_Organizer;
@@ -179,6 +402,11 @@ private:
PluginMap m_Plugins;
+ // This maps allow access to IPlugin* from name or diagnose/mapper object.
+ AccessPluginMap m_AccessPlugins;
+
+ std::map<MOBase::IPlugin*, PluginRequirements> m_Requirements;
+
std::map<QString, MOBase::IPluginGame*> m_SupportedGames;
std::vector<boost::signals2::connection> m_DiagnosisConnections;
QStringList m_FailedPlugins;
diff --git a/src/previewgenerator.cpp b/src/previewgenerator.cpp
index b406b7bc..c7df5a25 100644
--- a/src/previewgenerator.cpp
+++ b/src/previewgenerator.cpp
@@ -18,14 +18,17 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
*/
#include "previewgenerator.h"
+
#include <QFileInfo>
#include <QLabel>
#include <QImageReader>
#include <QTextEdit>
#include <utility.h>
-PreviewGenerator::PreviewGenerator()
-{
+#include "plugincontainer.h"
+
+PreviewGenerator::PreviewGenerator(const PluginContainer* pluginContainer) :
+ m_PluginContainer(pluginContainer) {
m_MaxSize = QGuiApplication::primaryScreen()->size() * 0.8;
}
@@ -42,13 +45,13 @@ bool PreviewGenerator::previewSupported(const QString &fileExtension) const
if (it == m_PreviewPlugins.end()) {
return false;
}
- return it->second->isActive();
+ return m_PluginContainer->isEnabled(it->second);
}
QWidget *PreviewGenerator::genPreview(const QString &fileName) const
{
auto iter = m_PreviewPlugins.find(QFileInfo(fileName).suffix().toLower());
- if (iter != m_PreviewPlugins.end() && iter->second->isActive()) {
+ if (iter != m_PreviewPlugins.end() && m_PluginContainer->isEnabled(iter->second)) {
return iter->second->genFilePreview(fileName, m_MaxSize);
} else {
return nullptr;
diff --git a/src/previewgenerator.h b/src/previewgenerator.h
index e872b06b..0d8f0781 100644
--- a/src/previewgenerator.h
+++ b/src/previewgenerator.h
@@ -26,10 +26,12 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
#include <functional>
#include <ipluginpreview.h>
+class PluginContainer;
+
class PreviewGenerator
{
public:
- PreviewGenerator();
+ PreviewGenerator(const PluginContainer* pluginContainer);
void registerPlugin(MOBase::IPluginPreview *plugin);
@@ -44,8 +46,8 @@ private:
private:
+ const PluginContainer* m_PluginContainer;
std::map<QString, MOBase::IPluginPreview*> m_PreviewPlugins;
-
QSize m_MaxSize;
};
diff --git a/src/problemsdialog.cpp b/src/problemsdialog.cpp
index 4b944ed2..d72f8c20 100644
--- a/src/problemsdialog.cpp
+++ b/src/problemsdialog.cpp
@@ -7,12 +7,13 @@
#include <QPushButton>
#include <Shellapi.h>
+#include "plugincontainer.h"
using namespace MOBase;
-ProblemsDialog::ProblemsDialog(std::vector<QObject *> pluginObjects, QWidget *parent) :
- QDialog(parent), ui(new Ui::ProblemsDialog), m_PluginObjects(pluginObjects),
+ProblemsDialog::ProblemsDialog(const PluginContainer& pluginContainer, QWidget *parent) :
+ QDialog(parent), ui(new Ui::ProblemsDialog), m_PluginContainer(pluginContainer),
m_hasProblems(false)
{
ui->setupUi(this);
@@ -40,14 +41,10 @@ void ProblemsDialog::runDiagnosis()
m_hasProblems = false;
ui->problemsWidget->clear();
- for(QObject *pluginObj : m_PluginObjects) {
- IPlugin *plugin = qobject_cast<IPlugin*>(pluginObj);
- if (plugin != nullptr && !plugin->isActive())
- continue;
-
- IPluginDiagnose *diagnose = qobject_cast<IPluginDiagnose*>(pluginObj);
- if (diagnose == nullptr)
+ for (IPluginDiagnose *diagnose : m_PluginContainer.plugins<IPluginDiagnose>()) {
+ if (!m_PluginContainer.isEnabled(diagnose)) {
continue;
+ }
std::vector<unsigned int> activeProblems = diagnose->activeProblems();
foreach (unsigned int key, activeProblems) {
diff --git a/src/problemsdialog.h b/src/problemsdialog.h
index a30c8d48..504a0a10 100644
--- a/src/problemsdialog.h
+++ b/src/problemsdialog.h
@@ -11,13 +11,14 @@ namespace Ui {
class ProblemsDialog;
}
+class PluginContainer;
class ProblemsDialog : public QDialog
{
Q_OBJECT
public:
- explicit ProblemsDialog(std::vector<QObject*> pluginObjects, QWidget *parent = 0);
+ explicit ProblemsDialog(PluginContainer const& pluginContainer, QWidget *parent = 0);
~ProblemsDialog();
// also saves and restores geometry
@@ -37,7 +38,7 @@ private slots:
private:
Ui::ProblemsDialog *ui;
- std::vector<QObject *> m_PluginObjects;
+ const PluginContainer& m_PluginContainer;
bool m_hasProblems;
};
diff --git a/src/proxyutils.h b/src/proxyutils.h
index 4c1717d8..4f26c070 100644
--- a/src/proxyutils.h
+++ b/src/proxyutils.h
@@ -10,7 +10,7 @@ namespace MOShared {
template <class Fn, class T = int>
auto callIfPluginActive(OrganizerProxy* proxy, Fn&& callback, T defaultReturn = T{}) {
return [fn = std::forward<Fn>(callback), proxy, defaultReturn](auto&& ...args) {
- if (proxy->plugin()->isActive()) {
+ if (proxy->isPluginEnabled(proxy->plugin())) {
return fn(std::forward<decltype(args)>(args)...);
}
else {
diff --git a/src/qt.conf b/src/qt.conf
index 5e070fa3..f834a22a 100644
--- a/src/qt.conf
+++ b/src/qt.conf
@@ -1,7 +1,3 @@
[Paths]
-Libraries=dlls
-LibraryExecutables=resources
+Prefix=.
Plugins=dlls
-Imports=dlls
-Qml2Imports=dlls
-Translations=resources/translations \ No newline at end of file
diff --git a/src/settings.cpp b/src/settings.cpp
index 75e2fd83..d325c02f 100644
--- a/src/settings.cpp
+++ b/src/settings.cpp
@@ -1280,9 +1280,17 @@ void PluginSettings::registerPlugin(IPlugin *plugin)
const QString settingName = plugin->name() + "/" + setting.key;
QVariant temp = get<QVariant>(
- m_Settings, "Plugins", settingName, setting.defaultValue);
+ m_Settings, "Plugins", settingName, QVariant());
- if (!temp.convert(setting.defaultValue.type())) {
+ // No previous enabled? Skip.
+ if (setting.key == "enabled" && (!temp.isValid() || !temp.canConvert<bool>())) {
+ continue;
+ }
+
+ if (!temp.isValid()) {
+ temp = setting.defaultValue;
+ }
+ else if (!temp.convert(setting.defaultValue.type())) {
log::warn(
"failed to interpret \"{}\" as correct type for \"{}\" in plugin \"{}\", using default",
temp.toString(), setting.key, plugin->name());
@@ -1296,6 +1304,17 @@ void PluginSettings::registerPlugin(IPlugin *plugin)
.arg(setting.description)
.arg(setting.defaultValue.toString());
}
+
+ // Handle previous "enabled" settings:
+ if (m_PluginSettings[plugin->name()].contains("enabled")) {
+ setPersistent(plugin->name(), "enabled", m_PluginSettings[plugin->name()]["enabled"].toBool(), true);
+ m_PluginSettings[plugin->name()].remove("enabled");
+ m_PluginDescriptions[plugin->name()].remove("enabled");
+
+ // We need to drop it manually in Settings since it is not possible to remove plugin
+ // settings:
+ remove(m_Settings, "Plugins", plugin->name() + "/enabled");
+ }
}
std::vector<MOBase::IPlugin*> PluginSettings::plugins() const
diff --git a/src/settingsdialog.ui b/src/settingsdialog.ui
index 754a275f..8670752b 100644
--- a/src/settingsdialog.ui
+++ b/src/settingsdialog.ui
@@ -1121,8 +1121,8 @@
</item>
</layout>
</widget>
- <widget class="QWidget" name="widget" native="true">
- <layout class="QVBoxLayout" name="verticalLayout_7" stretch="0,1">
+ <widget class="QWidget" name="pluginWidget" native="true">
+ <layout class="QVBoxLayout" name="verticalLayout_7" stretch="0,1,0">
<property name="leftMargin">
<number>0</number>
</property>
@@ -1136,53 +1136,86 @@
<number>0</number>
</property>
<item>
- <layout class="QFormLayout" name="formLayout_2">
- <item row="0" column="0">
- <widget class="QLabel" name="label_13">
- <property name="text">
- <string>Author:</string>
- </property>
- </widget>
- </item>
- <item row="0" column="1">
- <widget class="QLabel" name="authorLabel">
- <property name="text">
- <string/>
- </property>
- </widget>
- </item>
- <item row="1" column="0">
- <widget class="QLabel" name="label_15">
- <property name="text">
- <string>Version:</string>
- </property>
- </widget>
- </item>
- <item row="1" column="1">
- <widget class="QLabel" name="versionLabel">
- <property name="text">
- <string/>
- </property>
- </widget>
- </item>
- <item row="2" column="1">
- <widget class="QLabel" name="descriptionLabel">
- <property name="text">
- <string/>
- </property>
- <property name="wordWrap">
- <bool>true</bool>
- </property>
- </widget>
- </item>
- <item row="2" column="0">
- <widget class="QLabel" name="label_14">
- <property name="text">
- <string>Description:</string>
- </property>
- </widget>
- </item>
- </layout>
+ <widget class="QWidget" name="pluginDescription" native="true">
+ <layout class="QFormLayout" name="pluginDescriptionLayout">
+ <property name="labelAlignment">
+ <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
+ </property>
+ <property name="leftMargin">
+ <number>6</number>
+ </property>
+ <item row="0" column="0">
+ <widget class="QLabel" name="label_13">
+ <property name="text">
+ <string>Author:</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="1">
+ <widget class="QLabel" name="authorLabel">
+ <property name="text">
+ <string/>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="0">
+ <widget class="QLabel" name="label_15">
+ <property name="text">
+ <string>Version:</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="1">
+ <widget class="QLabel" name="versionLabel">
+ <property name="text">
+ <string/>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="0">
+ <widget class="QLabel" name="label_14">
+ <property name="text">
+ <string>Description:</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="1">
+ <widget class="QLabel" name="descriptionLabel">
+ <property name="text">
+ <string/>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
+ </property>
+ <property name="wordWrap">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item row="3" column="0">
+ <widget class="QCheckBox" name="enabledCheckbox">
+ <property name="text">
+ <string>Enabled</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
</item>
<item>
<widget class="QTreeWidget" name="pluginSettingsList">
@@ -1216,6 +1249,16 @@
</column>
</widget>
</item>
+ <item>
+ <widget class="QLabel" name="noPluginLabel">
+ <property name="text">
+ <string>No plugin found.</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignCenter</set>
+ </property>
+ </widget>
+ </item>
</layout>
</widget>
</widget>
diff --git a/src/settingsdialoggeneral.cpp b/src/settingsdialoggeneral.cpp
index 9eae2ba1..f29e1d24 100644
--- a/src/settingsdialoggeneral.cpp
+++ b/src/settingsdialoggeneral.cpp
@@ -81,9 +81,9 @@ void GeneralSettingsTab::addLanguages()
const QRegExp exp(pattern);
- QString translationsPath = qApp->applicationDirPath()
- + "/" + QString::fromStdWString(AppConfig::translationsPath());
- QDirIterator iter(translationsPath, QDir::Files);
+ QDirIterator iter(
+ QCoreApplication::applicationDirPath() + "/translations",
+ QDir::Files);
std::vector<std::pair<QString, QString>> languages;
diff --git a/src/settingsdialogplugins.cpp b/src/settingsdialogplugins.cpp
index 5faa0dc9..16f6fb0a 100644
--- a/src/settingsdialogplugins.cpp
+++ b/src/settingsdialogplugins.cpp
@@ -3,9 +3,11 @@
#include "noeditdelegate.h"
#include <iplugin.h>
+#include "disableproxyplugindialog.h"
+#include "organizercore.h"
#include "plugincontainer.h"
-using MOBase::IPlugin;
+using namespace MOBase;
PluginsSettingsTab::PluginsSettingsTab(Settings& s, PluginContainer* pluginContainer, SettingsDialog& d)
: SettingsTab(s, d), m_pluginContainer(pluginContainer)
@@ -24,20 +26,17 @@ PluginsSettingsTab::PluginsSettingsTab(Settings& s, PluginContainer* pluginConta
item->setFont(0, font);
topItems[interfaceName] = item;
item->setExpanded(true);
+ item->setFlags(item->flags() & ~Qt::ItemIsSelectable);
}
ui->pluginsList->setHeaderHidden(true);
// display plugin settings
QSet<QString> handledNames;
for (IPlugin* plugin : settings().plugins().plugins()) {
- if (handledNames.contains(plugin->name())) {
- continue;
- }
- if (!m_filter.matches([plugin](const QRegularExpression& regex) {
- return regex.match(plugin->localizedName()).hasMatch();
- })) {
+ if (handledNames.contains(plugin->name()) || m_pluginContainer->requirements(plugin).master()) {
continue;
}
+
QTreeWidgetItem* listItem = new QTreeWidgetItem(
topItems.at(m_pluginContainer->topImplementedInterface(plugin)));
listItem->setData(0, Qt::DisplayRole, plugin->localizedName());
@@ -45,6 +44,18 @@ PluginsSettingsTab::PluginsSettingsTab(Settings& s, PluginContainer* pluginConta
listItem->setData(0, ROLE_SETTINGS, settings().plugins().settings(plugin->name()));
listItem->setData(0, ROLE_DESCRIPTIONS, settings().plugins().descriptions(plugin->name()));
+ // Handle child item:
+ auto children = m_pluginContainer->requirements(plugin).children();
+ for (auto* child : children) {
+ QTreeWidgetItem* childItem = new QTreeWidgetItem(listItem);
+ childItem->setData(0, Qt::DisplayRole, child->localizedName());
+ childItem->setData(0, ROLE_PLUGIN, QVariant::fromValue((void*)child));
+ childItem->setData(0, ROLE_SETTINGS, settings().plugins().settings(child->name()));
+ childItem->setData(0, ROLE_DESCRIPTIONS, settings().plugins().descriptions(child->name()));
+
+ handledNames.insert(child->name());
+ }
+
handledNames.insert(plugin->name());
}
@@ -66,17 +77,44 @@ PluginsSettingsTab::PluginsSettingsTab(Settings& s, PluginContainer* pluginConta
QObject::connect(
ui->pluginsList, &QTreeWidget::currentItemChanged,
[&](auto* current, auto* previous) { on_pluginsList_currentItemChanged(current, previous); });
+ QObject::connect(
+ ui->enabledCheckbox, &QCheckBox::clicked,
+ [&](bool checked) { on_checkboxEnabled_clicked(checked); });
QShortcut *delShortcut = new QShortcut(
QKeySequence(Qt::Key_Delete), ui->pluginBlacklist);
QObject::connect(delShortcut, &QShortcut::activated, &dialog(), [&] { deleteBlacklistItem(); });
- QObject::connect(&m_filter, &MOBase::FilterWidget::changed, [&] { filterPluginList(); });
+ QObject::connect(&m_filter, &FilterWidget::changed, [&] { filterPluginList(); });
+ updateListItems();
filterPluginList();
}
+void PluginsSettingsTab::updateListItems()
+{
+ for (auto i = 0; i < ui->pluginsList->topLevelItemCount(); ++i) {
+ auto* topLevelItem = ui->pluginsList->topLevelItem(i);
+ for (auto j = 0; j < topLevelItem->childCount(); ++j) {
+ auto* item = topLevelItem->child(j);
+ auto* plugin = this->plugin(item);
+
+ bool inactive = !m_pluginContainer->implementInterface<IPluginGame>(plugin)
+ && !m_pluginContainer->isEnabled(plugin);
+
+ auto font = item->font(0);
+ font.setItalic(inactive);
+ item->setFont(0, font);
+ for (auto k = 0; k < item->childCount(); ++k) {
+ item->child(k)->setFont(0, font);
+ }
+ }
+ }
+
+}
+
void PluginsSettingsTab::filterPluginList()
{
+ auto selectedItems = ui->pluginsList->selectedItems();
QTreeWidgetItem* firstNotHidden = nullptr;
for (auto i = 0; i < ui->pluginsList->topLevelItemCount(); ++i) {
@@ -87,9 +125,18 @@ void PluginsSettingsTab::filterPluginList()
auto* item = topLevelItem->child(j);
auto* plugin = this->plugin(item);
- if (m_filter.matches([plugin](const QRegularExpression& regex) {
+ // Check the item or the child - If any match (item or child), the whole
+ // group is displayed.
+ bool match = m_filter.matches([plugin](const QRegularExpression& regex) {
return regex.match(plugin->localizedName()).hasMatch();
- })) {
+ });
+ for (auto* child : m_pluginContainer->requirements(plugin).children()) {
+ match = match || m_filter.matches([child](const QRegularExpression& regex) {
+ return regex.match(child->localizedName()).hasMatch();
+ });
+ }
+
+ if (match) {
found = true;
item->setHidden(false);
@@ -106,13 +153,21 @@ void PluginsSettingsTab::filterPluginList()
}
// Unselect item if hidden:
- auto selectedItems = ui->pluginsList->selectedItems();
- if (!selectedItems.isEmpty() && selectedItems[0]->isHidden()) {
- selectedItems[0]->setSelected(false);
-
- if (firstNotHidden) {
- firstNotHidden->setSelected(true);
+ if (firstNotHidden) {
+ ui->pluginDescription->setVisible(true);
+ ui->pluginSettingsList->setVisible(true);
+ ui->noPluginLabel->setVisible(false);
+ if (selectedItems.isEmpty()) {
+ ui->pluginsList->setCurrentItem(firstNotHidden);
}
+ else if (selectedItems[0]->isHidden()) {
+ ui->pluginsList->setCurrentItem(firstNotHidden);
+ }
+ }
+ else {
+ ui->pluginDescription->setVisible(false);
+ ui->pluginSettingsList->setVisible(false);
+ ui->noPluginLabel->setVisible(true);
}
}
@@ -150,6 +205,75 @@ void PluginsSettingsTab::closing()
storeSettings(ui->pluginsList->currentItem());
}
+void PluginsSettingsTab::on_checkboxEnabled_clicked(bool checked)
+{
+ // Retrieve the plugin:
+ auto *item = ui->pluginsList->currentItem();
+ if (!item || !item->data(0, ROLE_PLUGIN).isValid()) {
+ return;
+ }
+ IPlugin* plugin = this->plugin(item);
+ const auto& requirements = m_pluginContainer->requirements(plugin);
+
+ // User wants to enable:
+ if (checked) {
+ m_pluginContainer->setEnabled(plugin, true, false);
+ }
+ else {
+ // Custom check for proxy + current game:
+ if (m_pluginContainer->implementInterface<IPluginProxy>(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(
+ "<p>Disabling the '%1' plugin will also disable the following plugins:</p><ul>%1</ul><p>Do you want to continue?</p>")
+ .arg(plugin->localizedName()).arg("<li>" + pluginNames.join("</li><li>") + "</li>");
+ 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<IPluginProxy>(plugin)) {
+ dialog().setExitNeeded(Exit::Restart);
+ }
+
+ updateListItems();
+}
+
void PluginsSettingsTab::on_pluginsList_currentItemChanged(QTreeWidgetItem *current, QTreeWidgetItem *previous)
{
storeSettings(previous);
@@ -164,6 +288,43 @@ void PluginsSettingsTab::on_pluginsList_currentItemChanged(QTreeWidgetItem *curr
ui->versionLabel->setText(plugin->version().canonicalString());
ui->descriptionLabel->setText(plugin->description());
+ // Checkbox, do not show for children or game plugins, disable
+ // if the plugin cannot be enabled.
+ ui->enabledCheckbox->setVisible(
+ !m_pluginContainer->implementInterface<IPluginGame>(plugin)
+ && plugin->master().isEmpty());
+
+ bool enabled = m_pluginContainer->isEnabled(plugin);
+ auto& requirements = m_pluginContainer->requirements(plugin);
+ auto problems = requirements.problems();
+
+ if (m_pluginContainer->requirements(plugin).isCorePlugin()) {
+ ui->enabledCheckbox->setDisabled(true);
+ ui->enabledCheckbox->setToolTip(
+ QObject::tr("This plugin is required for Mod Organizer to work properly and cannot be disabled."));
+ }
+ // Plugin is enable or can be enabled.
+ else if (enabled || problems.empty()) {
+ ui->enabledCheckbox->setDisabled(false);
+ ui->enabledCheckbox->setToolTip("");
+ ui->enabledCheckbox->setChecked(enabled);
+ }
+ // Plugin is disable and cannot be enabled.
+ else {
+ ui->enabledCheckbox->setDisabled(true);
+ ui->enabledCheckbox->setChecked(false);
+ if (problems.size() == 1) {
+ ui->enabledCheckbox->setToolTip(problems[0].shortDescription());
+ }
+ else {
+ QStringList descriptions;
+ for (auto& problem : problems) {
+ descriptions.append(problem.shortDescription());
+ }
+ ui->enabledCheckbox->setToolTip("<ul><li>" + descriptions.join("</li><li>") + "</li></ul>");
+ }
+ }
+
QVariantMap settings = current->data(0, ROLE_SETTINGS).toMap();
QVariantMap descriptions = current->data(0, ROLE_DESCRIPTIONS).toMap();
ui->pluginSettingsList->setEnabled(settings.count() != 0);
diff --git a/src/settingsdialogplugins.h b/src/settingsdialogplugins.h
index add8fede..3de0d7dc 100644
--- a/src/settingsdialogplugins.h
+++ b/src/settingsdialogplugins.h
@@ -16,10 +16,17 @@ public:
private:
void on_pluginsList_currentItemChanged(QTreeWidgetItem *current, QTreeWidgetItem *previous);
+ void on_checkboxEnabled_clicked(bool checked);
void deleteBlacklistItem();
void storeSettings(QTreeWidgetItem *pluginItem);
private slots:
+
+ /**
+ * @brief Update the list item to display inactive plugins.
+ */
+ void updateListItems();
+
/**
* @brief Filter the plugin list according to the filter widget.
*
diff --git a/src/shared/appconfig.inc b/src/shared/appconfig.inc
index 8d4125cf..5839c2f4 100644
--- a/src/shared/appconfig.inc
+++ b/src/shared/appconfig.inc
@@ -6,8 +6,7 @@ APPPARAM(std::wstring, downloadPath, L"downloads")
APPPARAM(std::wstring, overwritePath, L"overwrite")
APPPARAM(std::wstring, stylesheetsPath, L"stylesheets")
APPPARAM(std::wstring, cachePath, L"webcache")
-APPPARAM(std::wstring, tutorialsPath, L"resources/tutorials")
-APPPARAM(std::wstring, translationsPath, L"resources/translations")
+APPPARAM(std::wstring, tutorialsPath, L"tutorials")
APPPARAM(std::wstring, logPath, L"logs")
APPPARAM(std::wstring, dumpsDir, L"crashDumps")
APPPARAM(std::wstring, defaultProfileName, L"Default")
diff --git a/src/thread_utils.h b/src/thread_utils.h
index f64dd601..e816a4e2 100644
--- a/src/thread_utils.h
+++ b/src/thread_utils.h
@@ -24,51 +24,6 @@ std::thread startSafeThread(F&& f)
});
}
-
-/**
- * Class that can be used to perform thread-safe memoization.
- *
- * Each instance hold a flag indicating if the current value is up-to-date
- * or not. This flag can be reset using `invalidate()`. When the value is queried,
- * the flag is checked, and if it is not up-to-date, the given callback is used
- * to compute the value.
- *
- * The computation and update of the value is locked to avoid concurrent modifications.
- *
- * @tparam T Type of value ot memoized.
- * @tparam Fn Type of the callback.
- */
-template <class T, class Fn = std::function<T()>>
-struct MemoizedLocked {
-
- template <class Callable>
- MemoizedLocked(Callable &&callable, T value = {}) :
- m_Fn{ std::forward<Callable>(callable) }, m_Value{ std::move(value) } { }
-
- template <class... Args>
- T& value(Args&&... args) const {
- if (m_NeedUpdating) {
- std::scoped_lock lock(m_Mutex);
- if (m_NeedUpdating) {
- m_Value = std::invoke(m_Fn, std::forward<Args>(args)... );
- m_NeedUpdating = false;
- }
- }
- return m_Value;
- }
-
- void invalidate() {
- m_NeedUpdating = true;
- }
-
-private:
- mutable std::mutex m_Mutex;
- mutable std::atomic<bool> m_NeedUpdating{ true };
-
- Fn m_Fn;
- mutable T m_Value;
-};
-
/**
* @brief Apply the given callable to each element between the two given iterators
* in a parallel way.
diff --git a/src/transfersavesdialog.cpp b/src/transfersavesdialog.cpp
index f8ebbb9d..5288d408 100644
--- a/src/transfersavesdialog.cpp
+++ b/src/transfersavesdialog.cpp
@@ -42,74 +42,6 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
using namespace MOBase;
using namespace MOShared;
-//These two classes give the save-transfer box a smidgin of useful info even
-//if save game isn't supported yet.
-namespace {
-
-class DummySave : public ISaveGame
-{
-public:
- DummySave(QString const &filename) :
- m_File(filename)
- {}
-
- ~DummySave() {}
-
- virtual QString getFilename() const override
- {
- return m_File;
- }
-
- virtual QDateTime getCreationTime() const override
- {
- return QFileInfo(m_File).birthTime();
- }
-
- virtual QString getSaveGroupIdentifier() const override
- {
- return m_File;
- }
-
- virtual QStringList allFiles() const override
- {
- return { m_File };
- }
-
- virtual bool hasScriptExtenderFile() const override
- {
- return false;
- }
-
-private:
- QString m_File;
-};
-
-class DummyInfo : public SaveGameInfo
-{
-public:
- virtual MOBase::ISaveGame const *getSaveGameInfo(QString const &file) const override
- {
- return new DummySave(file);
- }
-
- virtual MissingAssets getMissingAssets(QString const &) const override
- {
- return {};
- }
-
- MOBase::ISaveGameInfoWidget *getSaveGameWidget(QWidget *) const override
- {
- return nullptr;
- }
-
- virtual bool hasScriptExtenderSave(QString const &file) const override
- {
- return false;
- }
-};
-
-} //end anonymous namespace
-
TransferSavesDialog::TransferSavesDialog(const Profile &profile, IPluginGame const *gamePlugin, QWidget *parent)
: TutorableDialog("TransferSaves", parent)
, ui(new Ui::TransferSavesDialog)
@@ -263,7 +195,7 @@ void TransferSavesDialog::on_globalCharacterList_currentTextChanged(const QStrin
SaveCollection::const_iterator saveList = m_GlobalSaves.find(currentText);
if (saveList != m_GlobalSaves.end()) {
for (SaveListItem const &save : saveList->second) {
- ui->globalSavesList->addItem(QFileInfo(save->getFilename()).fileName());
+ ui->globalSavesList->addItem(QFileInfo(save->getFilepath()).fileName());
}
}
}
@@ -276,7 +208,7 @@ void TransferSavesDialog::on_localCharacterList_currentTextChanged(const QString
SaveCollection::const_iterator saveList = m_LocalSaves.find(currentText);
if (saveList != m_LocalSaves.end()) {
for (SaveListItem const &save : saveList->second) {
- ui->localSavesList->addItem(QFileInfo(save->getFilename()).fileName());
+ ui->localSavesList->addItem(QFileInfo(save->getFilepath()).fileName());
}
}
}
@@ -285,34 +217,13 @@ void TransferSavesDialog::refreshSaves(SaveCollection &saveCollection, QString c
{
saveCollection.clear();
- SaveGameInfo const *info = m_GamePlugin->feature<SaveGameInfo>();
- if (info == nullptr) {
- static DummyInfo dummyInfo;
- info = &dummyInfo;
- }
-
- QStringList filters;
- filters << QString("*.") + m_GamePlugin->savegameExtension();
-
- QDir savesDir(savedir);
- savesDir.setNameFilters(QStringList() << QString("*.") + m_GamePlugin->savegameExtension());
- savesDir.setFilter(QDir::Files);
- QDirIterator it(savesDir, QDirIterator::Subdirectories);
- log::debug("reading save games from {}", savesDir.absolutePath());
-
- QFileInfoList files;
- while (it.hasNext()) {
- it.next();
- files.append(it.fileInfo());
- }
- std::sort(files.begin(), files.end(), [](auto const& lhs, auto const& rhs) {
- return lhs.fileTime(QFileDevice::FileModificationTime) < rhs.fileTime(QFileDevice::FileModificationTime);
+ auto saves = m_GamePlugin->listSaves(savedir);
+ std::sort(saves.begin(), saves.end(), [](auto const& lhs, auto const& rhs) {
+ return lhs->getCreationTime() > rhs->getCreationTime();
});
- for (const QFileInfo &file: files) {
- MOBase::ISaveGame const *save = info->getSaveGameInfo(file.absoluteFilePath());
- saveCollection[save->getSaveGroupIdentifier()].push_back(
- std::unique_ptr<MOBase::ISaveGame const>(save));
+ for (auto& save: saves) {
+ saveCollection[save->getSaveGroupIdentifier()].push_back(save);
}
}
@@ -334,7 +245,7 @@ void TransferSavesDialog::refreshCharacters(const SaveCollection &saveCollection
}
bool TransferSavesDialog::transferCharacters(
- QString const &character, char const *message,
+ QString const &character, char const *message,
QDir const& sourceDirectory,
SaveList &saves,
QDir const& destination,
diff --git a/src/transfersavesdialog.h b/src/transfersavesdialog.h
index b983ad9e..fe78aa83 100644
--- a/src/transfersavesdialog.h
+++ b/src/transfersavesdialog.h
@@ -80,9 +80,10 @@ private:
MOBase::IPluginGame const *m_GamePlugin;
- typedef std::unique_ptr<MOBase::ISaveGame const> SaveListItem;
- typedef std::vector<SaveListItem> SaveList;
- typedef std::map<QString, SaveList> SaveCollection;
+ using SaveListItem = std::shared_ptr<const MOBase::ISaveGame>;
+ using SaveList = std::vector<SaveListItem>;
+ using SaveCollection = std::map<QString, SaveList>;
+
SaveCollection m_GlobalSaves;
SaveCollection m_LocalSaves;
diff --git a/src/tutorials/TutorialOverlay.qml b/src/tutorials/TutorialOverlay.qml
index 0a31ef0e..47e5065d 100644
--- a/src/tutorials/TutorialOverlay.qml
+++ b/src/tutorials/TutorialOverlay.qml
@@ -55,9 +55,7 @@ Rectangle {
Connections {
target: manager
- function onTabChanged() {
- tabChanged(index)
- }
+ onTabChanged: tabChanged(index)
}
Tooltip {