frontend: Provide UI for phase 1 of plugin manager

For phase 1 of the plugin manager, the ability to toggle off/on plugins
to be loaded at launch is provided.

This commit adds a new Plugin Manager dialog which can be accessed from
the Tools menu, which shows a list of all installed 3rd party plugins
with a checkbox to toggle them off or on.  If a change is made, the user
is prompted to restart OBS. To allow this, the plugin manager uses a
json based config file stored in the OBS config directory. Additionally
for sources in the source tree, a sample UI has been provided that
indicates any sources whose parent module is disabled, by turning its
title red.
This commit is contained in:
FiniteSingularity 2025-08-27 17:40:17 -05:00 committed by Ryan Foster
parent 14004cec96
commit 23b67268e7
15 changed files with 670 additions and 10 deletions

View File

@ -46,6 +46,7 @@ include(cmake/ui-components.cmake)
include(cmake/ui-dialogs.cmake)
include(cmake/ui-docks.cmake)
include(cmake/feature-importers.cmake)
include(cmake/feature-plugin-manager.cmake)
include(cmake/ui-models.cmake)
include(cmake/ui-oauth.cmake)
include(cmake/feature-browserpanels.cmake)

View File

@ -19,6 +19,7 @@
#include <components/Multiview.hpp>
#include <dialogs/LogUploadDialog.hpp>
#include <plugin-manager/PluginManager.hpp>
#include <utility/CrashHandler.hpp>
#include <utility/OBSEventFilter.hpp>
#include <utility/OBSProxyStyle.hpp>
@ -323,6 +324,7 @@ bool OBSApp::InitGlobalLocationDefaults()
config_set_default_string(appConfig, "Locations", "Configuration", path);
config_set_default_string(appConfig, "Locations", "SceneCollections", path);
config_set_default_string(appConfig, "Locations", "Profiles", path);
config_set_default_string(appConfig, "Locations", "PluginManagerSettings", path);
return true;
}
@ -422,6 +424,7 @@ static bool MakeUserDirs()
constexpr std::string_view OBSProfileSubDirectory = "obs-studio/basic/profiles";
constexpr std::string_view OBSScenesSubDirectory = "obs-studio/basic/scenes";
constexpr std::string_view OBSPluginManagerSubDirectory = "obs-studio/plugin_manager";
static bool MakeUserProfileDirs()
{
@ -429,6 +432,8 @@ static bool MakeUserProfileDirs()
App()->userProfilesLocation / std::filesystem::u8path(OBSProfileSubDirectory);
const std::filesystem::path userScenesPath =
App()->userScenesLocation / std::filesystem::u8path(OBSScenesSubDirectory);
const std::filesystem::path userPluginManagerPath =
App()->userPluginManagerSettingsLocation / std::filesystem::u8path(OBSPluginManagerSubDirectory);
if (!std::filesystem::exists(userProfilePath)) {
try {
@ -450,6 +455,16 @@ static bool MakeUserProfileDirs()
}
}
if (!std::filesystem::exists(userPluginManagerPath)) {
try {
std::filesystem::create_directories(userPluginManagerPath);
} catch (const std::filesystem::filesystem_error &error) {
blog(LOG_ERROR, "Failed to create user plugin manager directory '%s'\n%s",
userPluginManagerPath.u8string().c_str(), error.what());
return false;
}
}
return true;
}
@ -522,11 +537,14 @@ bool OBSApp::InitGlobalConfig()
std::filesystem::u8path(config_get_default_string(appConfig, "Locations", "SceneCollections"));
std::filesystem::path defaultUserProfilesLocation =
std::filesystem::u8path(config_get_default_string(appConfig, "Locations", "Profiles"));
std::filesystem::path defaultPluginManagerLocation =
std::filesystem::u8path(config_get_default_string(appConfig, "Locations", "PluginManagerSettings"));
if (IsPortableMode()) {
userConfigLocation = std::move(defaultUserConfigLocation);
userScenesLocation = std::move(defaultUserScenesLocation);
userProfilesLocation = std::move(defaultUserProfilesLocation);
userPluginManagerSettingsLocation = std::move(defaultPluginManagerLocation);
} else {
std::filesystem::path currentUserConfigLocation =
std::filesystem::u8path(config_get_string(appConfig, "Locations", "Configuration"));
@ -534,6 +552,8 @@ bool OBSApp::InitGlobalConfig()
std::filesystem::u8path(config_get_string(appConfig, "Locations", "SceneCollections"));
std::filesystem::path currentUserProfilesLocation =
std::filesystem::u8path(config_get_string(appConfig, "Locations", "Profiles"));
std::filesystem::path currentUserPluginManagerLocation =
std::filesystem::u8path(config_get_string(appConfig, "Locations", "PluginManagerSettings"));
userConfigLocation = (std::filesystem::exists(currentUserConfigLocation))
? std::move(currentUserConfigLocation)
@ -544,6 +564,9 @@ bool OBSApp::InitGlobalConfig()
userProfilesLocation = (std::filesystem::exists(currentUserProfilesLocation))
? std::move(currentUserProfilesLocation)
: std::move(defaultUserProfilesLocation);
userPluginManagerSettingsLocation = (std::filesystem::exists(currentUserPluginManagerLocation))
? std::move(currentUserPluginManagerLocation)
: std::move(defaultPluginManagerLocation);
}
bool userConfigResult = InitUserConfig(userConfigLocation, lastVersion);
@ -867,6 +890,7 @@ OBSApp::OBSApp(int &argc, char **argv, profiler_name_store_t *store)
#endif
setDesktopFileName("com.obsproject.Studio");
pluginManager_ = std::make_unique<OBS::PluginManager>();
}
OBSApp::~OBSApp()
@ -1769,3 +1793,20 @@ void OBSApp::addLogLine(int logLevel, const QString &message)
{
emit logLineAdded(logLevel, message);
}
void OBSApp::loadAppModules(struct obs_module_failure_info &mfi)
{
pluginManager_->preLoad();
blog(LOG_INFO, "---------------------------------");
obs_load_all_modules2(&mfi);
blog(LOG_INFO, "---------------------------------");
obs_log_loaded_modules();
blog(LOG_INFO, "---------------------------------");
obs_post_load_modules();
pluginManager_->postLoad();
}
void OBSApp::pluginManagerOpenDialog()
{
pluginManager_->open();
}

View File

@ -47,6 +47,7 @@ class CrashHandler;
enum class LogFileType { NoType, CurrentAppLog, LastAppLog, CrashLog };
enum class LogFileState { NoState, New, Uploaded };
class PluginManager;
} // namespace OBS
struct UpdateBranch {
@ -84,6 +85,8 @@ private:
std::deque<obs_frontend_translate_ui_cb> translatorHooks;
std::unique_ptr<OBS::PluginManager> pluginManager_;
bool UpdatePre22MultiviewLayout(const char *layout);
bool InitGlobalConfig();
@ -144,6 +147,7 @@ public:
std::filesystem::path userConfigLocation;
std::filesystem::path userScenesLocation;
std::filesystem::path userProfilesLocation;
std::filesystem::path userPluginManagerSettingsLocation;
inline const char *GetLocale() const { return locale.c_str(); }
@ -210,6 +214,11 @@ public:
static void SigIntSignalHandler(int);
#endif
void loadAppModules(struct obs_module_failure_info &mfi);
// Plugin Manager Accessors
void pluginManagerOpenDialog();
public slots:
void Exec(VoidFunc func);
void ProcessSigInt();

View File

@ -0,0 +1,8 @@
target_sources(
obs-studio
PRIVATE
plugin-manager/PluginManager.cpp
plugin-manager/PluginManager.hpp
plugin-manager/PluginManagerWindow.cpp
plugin-manager/PluginManagerWindow.hpp
)

View File

@ -47,6 +47,7 @@ target_sources(
forms/OBSRemux.ui
forms/StatusBarWidget.ui
forms/obs.qrc
forms/PluginManagerWindow.ui
forms/source-toolbar/browser-source-toolbar.ui
forms/source-toolbar/color-source-toolbar.ui
forms/source-toolbar/device-select-toolbar.ui

View File

@ -9,6 +9,8 @@
#include <QLineEdit>
#include <QPainter>
#include "plugin-manager/PluginManager.hpp"
#include "moc_SourceTreeItem.cpp"
static inline OBSScene GetCurrentScene()
@ -84,6 +86,16 @@ SourceTreeItem::SourceTreeItem(SourceTree *tree_, OBSSceneItem sceneitem_) : tre
label->setAttribute(Qt::WA_TranslucentBackground);
label->setEnabled(sourceVisible);
const char *sourceId = obs_source_get_unversioned_id(source);
switch (obs_source_load_state(sourceId)) {
case OBS_MODULE_DISABLED:
case OBS_MODULE_MISSING:
label->setStyleSheet("QLabel {color: #CC0000;}");
break;
default:
break;
}
#ifdef __APPLE__
vis->setAttribute(Qt::WA_LayoutUsesWidgetRect);
lock->setAttribute(Qt::WA_LayoutUsesWidgetRect);

View File

@ -1621,3 +1621,11 @@ MultitrackVideo.IncompatibleSettings.UpdateAndStartStreaming="Update Settings an
MultitrackVideo.IncompatibleSettings.AudioChannels="%1 is not currently compatible with [Audio → General → Channels] set to '%2', %3"
MultitrackVideo.IncompatibleSettings.AudioChannelsSingle="[Audio → General → Channels] needs to be set to '%1'"
MultitrackVideo.IncompatibleSettings.AudioChannelsMultiple="%1 requires multiple different settings for [Audio → General → Channels]"
#Plugin ManagerW
Basic.OpenPluginManager="Plugin Manager"
PluginManager="Plugin Manager"
PluginManager.HelpText="Plugin Manager"
PluginManager.Restart="Restart OBS?"
PluginManager.NeedsRestart="To apply these changes, OBS needs to restart. Do you want to restart now?"
PluginManager.MissingPlugin="[PLUGIN NOT FOUND]"

View File

@ -938,6 +938,7 @@
</property>
<addaction name="autoConfigure"/>
<addaction name="idianPlayground"/>
<addaction name="actionOpenPluginManager"/>
<addaction name="separator"/>
</widget>
<widget class="QMenu" name="menuDocks">
@ -2181,6 +2182,11 @@
<string>Basic.AutoConfig</string>
</property>
</action>
<action name="actionOpenPluginManager">
<property name="text">
<string>Basic.OpenPluginManager</string>
</property>
</action>
<action name="stats">
<property name="text">
<string>Basic.Stats</string>

View File

@ -0,0 +1,48 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>PluginManagerWindow</class>
<widget class="QDialog" name="PluginManagerWindow">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>850</width>
<height>400</height>
</rect>
</property>
<property name="windowTitle">
<string>PluginManager</string>
</property>
<property name="sizeGripEnabled">
<bool>true</bool>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="2" column="0">
<layout class="QHBoxLayout" name="horizontalLayout_4">
<property name="spacing">
<number>6</number>
</property>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</item>
<item row="0" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>PluginManager.HelpText</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QListWidget" name="modulesList"/>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@ -0,0 +1,303 @@
/******************************************************************************
Copyright (C) 2025 by FiniteSingularity <finitesingularityttv@gmail.com>
This program 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 2 of the License, or
(at your option) any later version.
This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
******************************************************************************/
#include "PluginManager.hpp"
#include "PluginManagerWindow.hpp"
#include <OBSApp.hpp>
#include <qt-wrappers.hpp>
#include <widgets/OBSBasic.hpp>
#include <QMessageBox>
#include <nlohmann/json.hpp>
#include <algorithm>
#include <fstream>
extern bool restart;
namespace OBS {
void addModuleToPluginManagerImpl(void *param, obs_module_t *newModule)
{
auto &instance = *static_cast<OBS::PluginManager *>(param);
std::string moduleName = obs_get_module_file_name(newModule);
moduleName = moduleName.substr(0, moduleName.rfind("."));
if (!obs_get_module_allow_disable(moduleName.c_str()))
return;
const char *display_name = obs_get_module_name(newModule);
std::string module_name = moduleName;
const char *id = obs_get_module_id(newModule);
const char *version = obs_get_module_version(newModule);
auto it = std::find_if(instance.modules_.begin(), instance.modules_.end(),
[&](OBS::ModuleInfo module) { return module.module_name == moduleName; });
if (it == instance.modules_.end()) {
instance.modules_.push_back({display_name ? display_name : "", module_name, id ? id : "",
version ? version : "", true, true});
} else {
it->display_name = display_name ? display_name : "";
it->module_name = module_name;
it->id = id ? id : "";
it->version = version ? version : "";
}
}
constexpr std::string_view OBSPluginManagerPath = "obs-studio/plugin_manager";
constexpr std::string_view OBSPluginManagerModulesFile = "modules.json";
void PluginManager::preLoad()
{
loadModules_();
disableModules_();
}
void PluginManager::postLoad()
{
// Find any new modules and add to Plugin Manager.
obs_enum_modules(addModuleToPluginManager, this);
// Get list of valid module types.
addModuleTypes_();
saveModules_();
// Add provided features from any unloaded modules
linkUnloadedModules_();
}
std::filesystem::path PluginManager::getConfigFilePath_()
{
std::filesystem::path path = App()->userPluginManagerSettingsLocation /
std::filesystem::u8path(OBSPluginManagerPath) /
std::filesystem::u8path(OBSPluginManagerModulesFile);
return path;
}
void PluginManager::loadModules_()
{
auto modulesFile = getConfigFilePath_();
if (std::filesystem::exists(modulesFile)) {
std::ifstream jsonFile(modulesFile);
nlohmann::json data = nlohmann::json::parse(jsonFile);
modules_.clear();
for (auto it : data) {
ModuleInfo obsModule;
try {
obsModule = {it.at("display_name"),
it.at("module_name"),
it.at("id"),
it.at("version"),
it.at("enabled"),
it.at("enabled"),
it.at("sources"),
it.at("outputs"),
it.at("encoders"),
it.at("services"),
{},
{},
{},
{}};
} catch (const nlohmann::json::out_of_range &error) {
blog(LOG_WARNING, "Error loading module info: %s", error.what());
continue;
}
modules_.push_back(obsModule);
}
}
}
void PluginManager::linkUnloadedModules_()
{
for (const auto &moduleInfo : modules_) {
if (!moduleInfo.enabled) {
auto obsModule = obs_get_disabled_module(moduleInfo.module_name.c_str());
if (!obsModule) {
continue;
}
for (const auto &source : moduleInfo.sources) {
obs_module_add_source(obsModule, source.c_str());
}
for (const auto &output : moduleInfo.outputs) {
obs_module_add_output(obsModule, output.c_str());
}
for (const auto &encoder : moduleInfo.encoders) {
obs_module_add_encoder(obsModule, encoder.c_str());
}
for (const auto &service : moduleInfo.services) {
obs_module_add_service(obsModule, service.c_str());
}
}
}
}
void PluginManager::saveModules_()
{
auto modulesFile = getConfigFilePath_();
std::ofstream outFile(modulesFile);
nlohmann::json data = nlohmann::json::array();
for (auto const &moduleInfo : modules_) {
nlohmann::json modData;
modData["display_name"] = moduleInfo.display_name;
modData["module_name"] = moduleInfo.module_name;
modData["id"] = moduleInfo.id;
modData["version"] = moduleInfo.version;
modData["enabled"] = moduleInfo.enabled;
modData["sources"] = moduleInfo.sources;
modData["outputs"] = moduleInfo.outputs;
modData["encoders"] = moduleInfo.encoders;
modData["services"] = moduleInfo.services;
data.push_back(modData);
}
outFile << std::setw(4) << data << std::endl;
}
void PluginManager::addModuleTypes_()
{
const char *source_id;
int i = 0;
while (obs_enum_source_types(i, &source_id)) {
i += 1;
obs_module_t *obsModule = obs_source_get_module(source_id);
if (!obsModule) {
continue;
}
std::string moduleName = obs_get_module_file_name(obsModule);
moduleName = moduleName.substr(0, moduleName.rfind("."));
auto it = std::find_if(modules_.begin(), modules_.end(),
[moduleName](ModuleInfo const &m) { return m.module_name == moduleName; });
if (it != modules_.end()) {
it->sourcesLoaded.push_back(source_id);
}
}
const char *output_id;
i = 0;
while (obs_enum_output_types(i, &output_id)) {
i += 1;
obs_module_t *obsModule = obs_source_get_module(output_id);
if (!obsModule) {
continue;
}
std::string moduleName = obs_get_module_file_name(obsModule);
moduleName = moduleName.substr(0, moduleName.rfind("."));
auto it = std::find_if(modules_.begin(), modules_.end(),
[moduleName](ModuleInfo const &m) { return m.module_name == moduleName; });
if (it != modules_.end()) {
it->outputsLoaded.push_back(output_id);
}
}
const char *encoder_id;
i = 0;
while (obs_enum_encoder_types(i, &encoder_id)) {
i += 1;
obs_module_t *obsModule = obs_source_get_module(encoder_id);
if (!obsModule) {
continue;
}
std::string moduleName = obs_get_module_file_name(obsModule);
moduleName = moduleName.substr(0, moduleName.rfind("."));
auto it = std::find_if(modules_.begin(), modules_.end(),
[moduleName](ModuleInfo const &m) { return m.module_name == moduleName; });
if (it != modules_.end()) {
it->encodersLoaded.push_back(encoder_id);
}
}
const char *service_id;
i = 0;
while (obs_enum_service_types(i, &service_id)) {
i += 1;
obs_module_t *obsModule = obs_source_get_module(service_id);
if (!obsModule) {
continue;
}
std::string moduleName = obs_get_module_file_name(obsModule);
moduleName = moduleName.substr(0, moduleName.rfind("."));
auto it = std::find_if(modules_.begin(), modules_.end(),
[moduleName](ModuleInfo const &m) { return m.module_name == moduleName; });
if (it != modules_.end()) {
it->servicesLoaded.push_back(service_id);
}
}
for (auto &moduleInfo : modules_) {
if (moduleInfo.enabledAtLaunch) {
moduleInfo.sources = moduleInfo.sourcesLoaded;
moduleInfo.encoders = moduleInfo.encodersLoaded;
moduleInfo.outputs = moduleInfo.outputsLoaded;
moduleInfo.services = moduleInfo.servicesLoaded;
} else {
for (auto const &source : moduleInfo.sources) {
disabledSources_.push_back(source);
}
for (auto const &output : moduleInfo.outputs) {
disabledOutputs_.push_back(output);
}
for (auto const &encoder : moduleInfo.encoders) {
disabledEncoders_.push_back(encoder);
}
for (auto const &service : moduleInfo.services) {
disabledServices_.push_back(service);
}
}
}
}
void PluginManager::disableModules_()
{
for (const auto &moduleInfo : modules_) {
if (!moduleInfo.enabled) {
obs_add_disabled_module(moduleInfo.module_name.c_str());
}
}
}
void PluginManager::open()
{
auto main = OBSBasic::Get();
PluginManagerWindow pluginManagerWindow(modules_, main);
auto result = pluginManagerWindow.exec();
if (result == QDialog::Accepted) {
modules_ = pluginManagerWindow.result();
saveModules_();
bool changed = false;
for (auto const &moduleInfo : modules_) {
if (moduleInfo.enabled != moduleInfo.enabledAtLaunch) {
changed = true;
break;
}
}
if (changed) {
QMessageBox::StandardButton button = OBSMessageBox::question(
main, QTStr("PluginManager.Restart"), QTStr("PluginManager.NeedsRestart"));
if (button == QMessageBox::Yes) {
restart = true;
main->close();
}
}
}
}
}; // namespace OBS

View File

@ -0,0 +1,78 @@
/******************************************************************************
Copyright (C) 2025 by FiniteSingularity <finitesingularityttv@gmail.com>
This program 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 2 of the License, or
(at your option) any later version.
This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
******************************************************************************/
#pragma once
#include <obs-module.h>
#include <filesystem>
#include <string>
#include <vector>
namespace OBS {
struct ModuleInfo {
std::string display_name;
std::string module_name;
std::string id;
std::string version;
bool enabled;
bool enabledAtLaunch;
std::vector<std::string> sources;
std::vector<std::string> outputs;
std::vector<std::string> encoders;
std::vector<std::string> services;
std::vector<std::string> sourcesLoaded;
std::vector<std::string> outputsLoaded;
std::vector<std::string> encodersLoaded;
std::vector<std::string> servicesLoaded;
};
class PluginManager {
private:
std::vector<ModuleInfo> modules_ = {};
std::vector<std::string> disabledSources_ = {};
std::vector<std::string> disabledOutputs_ = {};
std::vector<std::string> disabledServices_ = {};
std::vector<std::string> disabledEncoders_ = {};
std::filesystem::path getConfigFilePath_();
void loadModules_();
void saveModules_();
void disableModules_();
void addModuleTypes_();
void linkUnloadedModules_();
public:
void preLoad();
void postLoad();
void open();
friend void addModuleToPluginManagerImpl(void *param, obs_module_t *newModule);
};
void addModuleToPluginManagerImpl(void *param, obs_module_t *newModule);
}; // namespace OBS
// Anonymous namespace function to add module to plugin manager
// via libobs's module enumeration.
namespace {
inline void addModuleToPluginManager(void *param, obs_module_t *newModule)
{
OBS::addModuleToPluginManagerImpl(param, newModule);
}
} // namespace

View File

@ -0,0 +1,79 @@
/******************************************************************************
Copyright (C) 2025 by FiniteSingularity <finitesingularityttv@gmail.com>
This program 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 2 of the License, or
(at your option) any later version.
This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
******************************************************************************/
#include "PluginManagerWindow.hpp"
#include <OBSApp.hpp>
#include <QCheckBox>
#include <QDialogButtonBox>
#include <QHBoxLayout>
#include <QLabel>
#include <QScrollArea>
#include <QString>
#include <QVBoxLayout>
#include "moc_PluginManagerWindow.cpp"
namespace OBS {
PluginManagerWindow::PluginManagerWindow(std::vector<ModuleInfo> const &modules, QWidget *parent)
: QDialog(parent),
modules_(modules),
ui(new Ui::PluginManagerWindow)
{
setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
ui->setupUi(this);
std::sort(modules_.begin(), modules_.end(), [](const ModuleInfo &a, const ModuleInfo &b) {
std::string aName = !a.display_name.empty() ? a.display_name : a.module_name;
std::string bName = !b.display_name.empty() ? b.display_name : b.module_name;
return aName < bName;
});
for (auto &metadata : modules_) {
std::string id = metadata.module_name;
// Check if the module is missing:
bool missing = !obs_get_module(id.c_str()) && !obs_get_disabled_module(id.c_str());
QString name = !metadata.display_name.empty() ? metadata.display_name.c_str()
: metadata.module_name.c_str();
if (missing) {
name += " " + QTStr("PluginManager.MissingPlugin");
}
auto item = new QListWidgetItem(name);
item->setFlags(item->flags() | Qt::ItemIsUserCheckable);
item->setCheckState(metadata.enabled ? Qt::Checked : Qt::Unchecked);
if (missing) {
item->setFlags(item->flags() & ~Qt::ItemIsEnabled);
}
ui->modulesList->addItem(item);
}
connect(ui->modulesList, &QListWidget::itemChanged, this, [this](QListWidgetItem *item) {
auto row = ui->modulesList->row(item);
bool checked = item->checkState() == Qt::Checked;
modules_[row].enabled = checked;
});
connect(ui->buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept);
connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
}
}; // namespace OBS

View File

@ -0,0 +1,40 @@
/******************************************************************************
Copyright (C) 2025 by FiniteSingularity <finitesingularityttv@gmail.com>
This program 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 2 of the License, or
(at your option) any later version.
This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
******************************************************************************/
#pragma once
#include "ui_PluginManagerWindow.h"
#include "PluginManager.hpp"
#include <QDialog>
#include <QWidget>
namespace OBS {
class PluginManagerWindow : public QDialog {
Q_OBJECT
std::unique_ptr<Ui::PluginManagerWindow> ui;
public:
explicit PluginManagerWindow(std::vector<ModuleInfo> const &modules, QWidget *parent = nullptr);
inline std::vector<ModuleInfo> const result() { return modules_; }
private:
std::vector<ModuleInfo> modules_;
};
}; // namespace OBS

View File

@ -22,8 +22,11 @@
#include "ColorSelect.hpp"
#include "OBSBasicControls.hpp"
#include "OBSBasicStats.hpp"
#include "plugin-manager/PluginManager.hpp"
#include "VolControl.hpp"
#include <obs-module.h>
#ifdef YOUTUBE_ENABLED
#include <docks/YouTubeAppDock.hpp>
#endif
@ -184,9 +187,9 @@ static void SetSafeModuleNames()
return;
#else
string module;
stringstream modules(SAFE_MODULES);
stringstream modules_(SAFE_MODULES);
while (getline(modules, module, '|')) {
while (getline(modules_, module, '|')) {
/* When only disallowing third-party plugins, still add
* "unsafe" bundled modules to the safe list. */
if (disable_3p_plugins || !unsafe_modules.count(module))
@ -195,6 +198,24 @@ static void SetSafeModuleNames()
#endif
}
static void SetCoreModuleNames()
{
#ifndef SAFE_MODULES
throw "SAFE_MODULES not defined";
#else
std::string safeModules = SAFE_MODULES;
if (safeModules.empty()) {
throw "SAFE_MODULES is empty";
}
string module;
stringstream modules_(SAFE_MODULES);
while (getline(modules_, module, '|')) {
obs_add_core_module(module.c_str());
}
#endif
}
extern void setupDockAction(QDockWidget *dock);
OBSBasic::OBSBasic(QWidget *parent) : OBSMainWindow(parent), undo_s(ui), ui(new Ui::OBSBasic)
@ -996,26 +1017,23 @@ void OBSBasic::OBSInit()
#endif
struct obs_module_failure_info mfi;
/* Safe Mode disables third-party plugins so we don't need to add earch
* paths outside the OBS bundle/installation. */
// Safe Mode disables third-party plugins so we don't need to add each path outside the OBS bundle/installation.
if (safe_mode || disable_3p_plugins) {
SetSafeModuleNames();
} else {
AddExtraModulePaths();
}
// Core modules are not allowed to be disabled by the user via plugin manager.
SetCoreModuleNames();
/* Modules can access frontend information (i.e. profile and scene collection data) during their initialization, and some modules (e.g. obs-websockets) are known to use the filesystem location of the current profile in their own code.
Thus the profile and scene collection discovery needs to happen before any access to that information (but after intializing global settings) to ensure legacy code gets valid path information.
*/
RefreshSceneCollections(true);
blog(LOG_INFO, "---------------------------------");
obs_load_all_modules2(&mfi);
blog(LOG_INFO, "---------------------------------");
obs_log_loaded_modules();
blog(LOG_INFO, "---------------------------------");
obs_post_load_modules();
App()->loadAppModules(mfi);
BPtr<char *> failed_modules = mfi.failed_modules;
@ -2070,3 +2088,8 @@ OBSPromptResult OBSBasic::PromptForName(const OBSPromptRequest &request, const O
return result;
}
void OBSBasic::on_actionOpenPluginManager_triggered()
{
App()->pluginManagerOpenDialog();
}

View File

@ -762,6 +762,9 @@ public:
private slots:
void ResizeOutputSizeOfSource();
private slots:
void on_actionOpenPluginManager_triggered();
/* -------------------------------------
* MARK: - OBSBasic_Preview
* -------------------------------------