/****************************************************************************** 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 #include #include #include #include #include #include "moc_PluginManagerWindow.cpp" extern bool safe_mode; namespace { using Category = OBS::PluginManagerWindow::Category; constexpr int getCategorySortOrder(Category c) { switch (c) { case Category::Installed: return 0; case Category::Error: return 1; case Category::Missing: return 2; default: return 3; } } } // namespace namespace OBS { PluginManagerWindow::PluginManagerWindow(std::vector const &modules, std::vector const &failedModules, QWidget *parent) : QDialog(parent), modules_(modules), ui(new Ui::PluginManagerWindow) { setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); ui->setupUi(this); // Set up sidebar entries ui->sectionList->clear(); ui->sectionList->setSelectionMode(QAbstractItemView::SingleSelection); connect(ui->sectionList, &QListWidget::itemSelectionChanged, this, &PluginManagerWindow::sectionSelectionChanged); QListWidgetItem *browse = new QListWidgetItem(QTStr("PluginManager.Section.Discover")); browse->setFlags(browse->flags() & ~Qt::ItemIsEnabled); browse->setFlags(browse->flags() & ~Qt::ItemIsSelectable); browse->setToolTip(QTStr("ComingSoon")); ui->sectionList->addItem(browse); QListWidgetItem *installed = new QListWidgetItem(QTStr("PluginManager.Section.Manage")); ui->sectionList->addItem(installed); QListWidgetItem *updates = new QListWidgetItem(QTStr("PluginManager.Section.Updates")); updates->setFlags(updates->flags() & ~Qt::ItemIsEnabled); updates->setFlags(updates->flags() & ~Qt::ItemIsSelectable); updates->setToolTip(QTStr("ComingSoon")); ui->sectionList->addItem(updates); setupInstalledPage(failedModules); setPage(Page::Installed); ui->manageRestartLabel->setVisible(isEnabledPluginsChanged()); connect(ui->buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept); connect(ui->buttonBox->button(QDialogButtonBox::Discard), &QPushButton::clicked, this, &QDialog::close); } void PluginManagerWindow::setupInstalledPage(std::vector failedModules) { ui->modulesListContainer->viewport()->setAutoFillBackground(false); ui->modulesListOuter->layout()->setAlignment(Qt::AlignTop | Qt::AlignHCenter); ui->modulesListOuter->setAutoFillBackground(false); // Set up Idian sections auto installedPluginList = new idian::RowList(ui->modulesList); auto installedHeader = new idian::ListHeader(installedPluginList, QTStr("PluginManager.Section.Manage.Title"), QTStr("PluginManager.Section.Manage.Description")); installedPluginList->addHeader(installedHeader); auto errorPluginList = new idian::RowList(ui->modulesList); auto errorHeader = new idian::ListHeader(errorPluginList, QTStr("PluginManager.Section.Errors.Title"), QTStr("PluginManager.Section.Errors.Description")); errorPluginList->addHeader(errorHeader); auto missingPluginList = new idian::RowList(ui->modulesList); auto missingHeader = new idian::ListHeader(missingPluginList, QTStr("PluginManager.Section.Missing.Title"), QTStr("PluginManager.Section.Missing.Description")); missingPluginList->addHeader(missingHeader); bool isSafeMode = obs_frontend_is_safe_mode_enabled(); if (isSafeMode) { auto *safeModeFrame = new QFrame{}; safeModeFrame->setLayout(new QVBoxLayout); safeModeFrame->layout()->setContentsMargins(0, 0, 0, 0); safeModeFrame->setProperty("class", "frame-notice"); auto *safeModeLabel = new QLabel(ui->modulesList); safeModeLabel->setText(QTStr("PluginManager.SafeMode")); safeModeLabel->setAlignment(Qt::AlignCenter); safeModeLabel->setIndent(0); safeModeFrame->layout()->addWidget(safeModeLabel); ui->installedPage->layout()->addWidget(safeModeFrame); } ui->modulesList->layout()->addWidget(installedPluginList); ui->modulesList->layout()->addWidget(errorPluginList); ui->modulesList->layout()->addWidget(missingPluginList); installedPluginEntries.reserve(modules_.size() + failedModules.size()); for (auto &metadata : modules_) { std::string_view module_name{metadata.module_name}; // Check if the module is missing: obs_module_t *moduleData = obs_get_module(module_name.data()); Category category{Category::Installed}; bool isLoaded = moduleData != nullptr; if (!isLoaded) { category = Category::Missing; moduleData = obs_get_disabled_module(module_name.data()); } bool isDisabled = moduleData != nullptr; if (isDisabled) { // This module is disabled but check if it also failed to load. auto failedModuleIterator = std::find(failedModules.begin(), failedModules.end(), module_name); if (failedModuleIterator != failedModules.end()) { // This module is in the plugin manager cache so it has loaded properly before but now failed. // Remove entry from the failedModules list so we don't create a dummy entry for it. category = Category::Error; failedModules.erase(failedModuleIterator); } else { // Module is disabled but did not fail to load. category = Category::Installed; } } bool isLegacyModule = obs_is_legacy_module(moduleData); QString name = !metadata.display_name.empty() ? metadata.display_name.c_str() : metadata.module_name.c_str(); QString version = !metadata.version.empty() ? metadata.version.c_str() : ""; Entry newEntry{&metadata, name, category, isLegacyModule}; installedPluginEntries.push_back(newEntry); } for (const std::string &moduleName : failedModules) { // This failed module is not in the plugin manager cache which means it has never been loaded successfully. // Create a dummy visual entry for it. QString name = QString::fromStdString(moduleName.data()); Category category{Category::Error}; bool isLegacy{false}; Entry newEntry{nullptr, name, category, isLegacy}; installedPluginEntries.push_back(newEntry); } std::sort(installedPluginEntries.begin(), installedPluginEntries.end(), [](const Entry &a, const Entry &b) { if (a.category != b.category) { return getCategorySortOrder(a.category) < getCategorySortOrder(b.category); } return a.name.toLower() < b.name.toLower(); }); QWidget *previousRow{nullptr}; for (Entry &entry : installedPluginEntries) { if (isSafeMode) { entry.category = Category::Installed; } auto newRow = new InstalledPluginRow(ui->modulesList, entry); if (!previousRow) { setTabOrder(ui->modulesListContainer, newRow); } else { setTabOrder(previousRow, newRow); } previousRow = newRow; if (entry.category == Category::Installed) { installedPluginList->addRow(newRow); connect(newRow, &InstalledPluginRow::toggleChanged, this, [this]() { ui->manageRestartLabel->setVisible(isEnabledPluginsChanged()); }); } else if (entry.category == Category::Error) { errorPluginList->addRow(newRow); } else if (entry.category == Category::Missing) { missingPluginList->addRow(newRow); connect(newRow, &InstalledPluginRow::trashClicked, this, [this, newRow, entry]() { auto it = std::find_if(modules_.begin(), modules_.end(), [&entry](const auto &module) { return &module == entry.module; }); if (it != modules_.end()) { modules_.erase(it); } }); } } setTabOrder(previousRow, ui->buttonBox); if (installedPluginList->count() == 0) { installedHeader->setDescription(QTStr("PluginManager.Section.Manage.NoPlugins")); } if (errorPluginList->count() == 0) { errorPluginList->setVisible(false); } if (missingPluginList->count() == 0) { missingPluginList->setVisible(false); } // Qt is weird about how styling from dynamic properties affects certain widgets such as scroll areas. // This forces a recalculation after the items have all been added. // TODO: https://github.com/obsproject/obs-studio/issues/13920 // Rip this out after #13920 has been done. { ui->modulesList->style()->polish(ui->modulesList); QEvent event(QEvent::StyleChange); QApplication::sendEvent(ui->modulesList, &event); } } void PluginManagerWindow::sectionSelectionChanged() { auto selected = ui->sectionList->selectedItems(); if (selected.count() != 1) { setSection(activeSectionIndex.row()); } else { auto selectionIndex = ui->sectionList->indexFromItem(selected.first()); setSection(selectionIndex.row()); } } void PluginManagerWindow::setSection(int sidebarRow) { if (auto item = ui->sectionList->item(sidebarRow)) { activeSectionIndex = ui->sectionList->indexFromItem(item); ui->sectionList->setCurrentIndex(activeSectionIndex); ui->stackedContents->setCurrentIndex(sidebarRow); } } bool PluginManagerWindow::isEnabledPluginsChanged() { if (obs_frontend_is_safe_mode_enabled()) { return false; } bool result = false; for (auto &entry : installedPluginEntries) { // Only prompt for restart when a loadable plugin entry changed. if (entry.category == Category::Installed && entry.module->enabledAtLaunch != entry.module->enabled) { result = true; break; } } return result; } void PluginManagerWindow::setPage(Page page) { switch (page) { case Page::Installed: setSection(1); break; default: break; } } }; // namespace OBS