From 23b67268e7f9799d146cf7f62813d3b8ede13d3f Mon Sep 17 00:00:00 2001 From: FiniteSingularity Date: Wed, 27 Aug 2025 17:40:17 -0500 Subject: [PATCH] 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. --- frontend/CMakeLists.txt | 1 + frontend/OBSApp.cpp | 41 +++ frontend/OBSApp.hpp | 9 + frontend/cmake/feature-plugin-manager.cmake | 8 + frontend/cmake/ui-qt.cmake | 1 + frontend/components/SourceTreeItem.cpp | 12 + frontend/data/locale/en-US.ini | 8 + frontend/forms/OBSBasic.ui | 6 + frontend/forms/PluginManagerWindow.ui | 48 +++ frontend/plugin-manager/PluginManager.cpp | 303 ++++++++++++++++++ frontend/plugin-manager/PluginManager.hpp | 78 +++++ .../plugin-manager/PluginManagerWindow.cpp | 79 +++++ .../plugin-manager/PluginManagerWindow.hpp | 40 +++ frontend/widgets/OBSBasic.cpp | 43 ++- frontend/widgets/OBSBasic.hpp | 3 + 15 files changed, 670 insertions(+), 10 deletions(-) create mode 100644 frontend/cmake/feature-plugin-manager.cmake create mode 100644 frontend/forms/PluginManagerWindow.ui create mode 100644 frontend/plugin-manager/PluginManager.cpp create mode 100644 frontend/plugin-manager/PluginManager.hpp create mode 100644 frontend/plugin-manager/PluginManagerWindow.cpp create mode 100644 frontend/plugin-manager/PluginManagerWindow.hpp diff --git a/frontend/CMakeLists.txt b/frontend/CMakeLists.txt index 90f09405f..6f677d4fe 100644 --- a/frontend/CMakeLists.txt +++ b/frontend/CMakeLists.txt @@ -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) diff --git a/frontend/OBSApp.cpp b/frontend/OBSApp.cpp index f1f25fdea..2323c2ed6 100644 --- a/frontend/OBSApp.cpp +++ b/frontend/OBSApp.cpp @@ -19,6 +19,7 @@ #include #include +#include #include #include #include @@ -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(); } 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(); +} diff --git a/frontend/OBSApp.hpp b/frontend/OBSApp.hpp index 7eb6b571b..b2524a105 100644 --- a/frontend/OBSApp.hpp +++ b/frontend/OBSApp.hpp @@ -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 translatorHooks; + std::unique_ptr 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(); diff --git a/frontend/cmake/feature-plugin-manager.cmake b/frontend/cmake/feature-plugin-manager.cmake new file mode 100644 index 000000000..b57992518 --- /dev/null +++ b/frontend/cmake/feature-plugin-manager.cmake @@ -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 +) diff --git a/frontend/cmake/ui-qt.cmake b/frontend/cmake/ui-qt.cmake index 72437ad2a..c013dbfa2 100644 --- a/frontend/cmake/ui-qt.cmake +++ b/frontend/cmake/ui-qt.cmake @@ -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 diff --git a/frontend/components/SourceTreeItem.cpp b/frontend/components/SourceTreeItem.cpp index befc52ea4..c063b3d4d 100644 --- a/frontend/components/SourceTreeItem.cpp +++ b/frontend/components/SourceTreeItem.cpp @@ -9,6 +9,8 @@ #include #include +#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); diff --git a/frontend/data/locale/en-US.ini b/frontend/data/locale/en-US.ini index cc5724f6d..c67f20b7d 100644 --- a/frontend/data/locale/en-US.ini +++ b/frontend/data/locale/en-US.ini @@ -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]" diff --git a/frontend/forms/OBSBasic.ui b/frontend/forms/OBSBasic.ui index f37178720..e3c0600e5 100644 --- a/frontend/forms/OBSBasic.ui +++ b/frontend/forms/OBSBasic.ui @@ -938,6 +938,7 @@ + @@ -2181,6 +2182,11 @@ Basic.AutoConfig + + + Basic.OpenPluginManager + + Basic.Stats diff --git a/frontend/forms/PluginManagerWindow.ui b/frontend/forms/PluginManagerWindow.ui new file mode 100644 index 000000000..c8bc997ba --- /dev/null +++ b/frontend/forms/PluginManagerWindow.ui @@ -0,0 +1,48 @@ + + + PluginManagerWindow + + + + 0 + 0 + 850 + 400 + + + + PluginManager + + + true + + + + + + 6 + + + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + PluginManager.HelpText + + + + + + + + + + + diff --git a/frontend/plugin-manager/PluginManager.cpp b/frontend/plugin-manager/PluginManager.cpp new file mode 100644 index 000000000..2fad56d2b --- /dev/null +++ b/frontend/plugin-manager/PluginManager.cpp @@ -0,0 +1,303 @@ +/****************************************************************************** + Copyright (C) 2025 by FiniteSingularity + + 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 . +******************************************************************************/ + +#include "PluginManager.hpp" +#include "PluginManagerWindow.hpp" + +#include +#include +#include + +#include + +#include + +#include +#include + +extern bool restart; + +namespace OBS { + +void addModuleToPluginManagerImpl(void *param, obs_module_t *newModule) +{ + auto &instance = *static_cast(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 diff --git a/frontend/plugin-manager/PluginManager.hpp b/frontend/plugin-manager/PluginManager.hpp new file mode 100644 index 000000000..2f6d5808a --- /dev/null +++ b/frontend/plugin-manager/PluginManager.hpp @@ -0,0 +1,78 @@ +/****************************************************************************** + Copyright (C) 2025 by FiniteSingularity + + 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 . +******************************************************************************/ + +#pragma once + +#include + +#include +#include +#include + +namespace OBS { + +struct ModuleInfo { + std::string display_name; + std::string module_name; + std::string id; + std::string version; + bool enabled; + bool enabledAtLaunch; + std::vector sources; + std::vector outputs; + std::vector encoders; + std::vector services; + std::vector sourcesLoaded; + std::vector outputsLoaded; + std::vector encodersLoaded; + std::vector servicesLoaded; +}; + +class PluginManager { +private: + std::vector modules_ = {}; + std::vector disabledSources_ = {}; + std::vector disabledOutputs_ = {}; + std::vector disabledServices_ = {}; + std::vector 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 diff --git a/frontend/plugin-manager/PluginManagerWindow.cpp b/frontend/plugin-manager/PluginManagerWindow.cpp new file mode 100644 index 000000000..7481f6400 --- /dev/null +++ b/frontend/plugin-manager/PluginManagerWindow.cpp @@ -0,0 +1,79 @@ +/****************************************************************************** + Copyright (C) 2025 by FiniteSingularity + + 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 . +******************************************************************************/ + +#include "PluginManagerWindow.hpp" + +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "moc_PluginManagerWindow.cpp" + +namespace OBS { + +PluginManagerWindow::PluginManagerWindow(std::vector 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 diff --git a/frontend/plugin-manager/PluginManagerWindow.hpp b/frontend/plugin-manager/PluginManagerWindow.hpp new file mode 100644 index 000000000..66b663bfc --- /dev/null +++ b/frontend/plugin-manager/PluginManagerWindow.hpp @@ -0,0 +1,40 @@ +/****************************************************************************** + Copyright (C) 2025 by FiniteSingularity + + 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 . +******************************************************************************/ + +#pragma once + +#include "ui_PluginManagerWindow.h" +#include "PluginManager.hpp" + +#include +#include + +namespace OBS { + +class PluginManagerWindow : public QDialog { + Q_OBJECT + std::unique_ptr ui; + +public: + explicit PluginManagerWindow(std::vector const &modules, QWidget *parent = nullptr); + inline std::vector const result() { return modules_; } + +private: + std::vector modules_; +}; + +}; // namespace OBS diff --git a/frontend/widgets/OBSBasic.cpp b/frontend/widgets/OBSBasic.cpp index 662d8eb08..81c181b50 100644 --- a/frontend/widgets/OBSBasic.cpp +++ b/frontend/widgets/OBSBasic.cpp @@ -22,8 +22,11 @@ #include "ColorSelect.hpp" #include "OBSBasicControls.hpp" #include "OBSBasicStats.hpp" +#include "plugin-manager/PluginManager.hpp" #include "VolControl.hpp" +#include + #ifdef YOUTUBE_ENABLED #include #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 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(); +} diff --git a/frontend/widgets/OBSBasic.hpp b/frontend/widgets/OBSBasic.hpp index 44c0be56e..ca213433e 100644 --- a/frontend/widgets/OBSBasic.hpp +++ b/frontend/widgets/OBSBasic.hpp @@ -762,6 +762,9 @@ public: private slots: void ResizeOutputSizeOfSource(); +private slots: + void on_actionOpenPluginManager_triggered(); + /* ------------------------------------- * MARK: - OBSBasic_Preview * -------------------------------------