diff --git a/frontend/cmake/feature-plugin-manager.cmake b/frontend/cmake/feature-plugin-manager.cmake
index 5be57d9ac..1d617aa5f 100644
--- a/frontend/cmake/feature-plugin-manager.cmake
+++ b/frontend/cmake/feature-plugin-manager.cmake
@@ -11,6 +11,8 @@ set(OBS_PLATFORM_PLUGIN_DATA_PATH "${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_DATAR
target_sources(
obs-studio
PRIVATE
+ plugin-manager/InstalledPluginRow.cpp
+ plugin-manager/InstalledPluginRow.hpp
plugin-manager/PluginManager.cpp
plugin-manager/PluginManager.hpp
plugin-manager/PluginManagerWindow.cpp
diff --git a/frontend/data/locale/en-US.ini b/frontend/data/locale/en-US.ini
index 821c29200..924312918 100644
--- a/frontend/data/locale/en-US.ini
+++ b/frontend/data/locale/en-US.ini
@@ -1684,7 +1684,19 @@ PluginManager.SafeMode="All plugins temporarily disabled by Safe Mode"
PluginManager.Section.Discover="Browse"
PluginManager.Section.Manage="Installed"
PluginManager.Section.Updates="Updates"
-PluginManager.Section.Manage.Title="Manage Enabled Plugins"
+PluginManager.Section.Manage.Title="Installed"
+PluginManager.Section.Manage.Description="Manage your currently installed plugins."
+PluginManager.Section.Manage.NoPlugins="No plugins found."
+PluginManager.Section.Errors.Title="Errors"
+PluginManager.Section.Errors.Description="These plugins failed to load. Make sure they are up to date and compatible with this version of OBS Studio."
+PluginManager.Section.Missing.Title="Missing"
+PluginManager.Section.Missing.Description="These plugins were previously installed but can no longer be found."
+PluginManager.Button.Enable="Enable %1 plugin"
+PluginManager.Status.Legacy="Legacy"
+PluginManager.Status.Disabled="Disabled"
+PluginManager.Status.Error="Error"
+PluginManager.Status.Missing="Missing"
+PluginManager.Status.Legacy.Description="This plugin is installed to a location that will stop working in the future. Learn More"
# Custom Widget Localization
Accessible.Widget.Name.AlignmentSelector="Alignment Selector"
diff --git a/frontend/forms/PluginManagerWindow.ui b/frontend/forms/PluginManagerWindow.ui
index 1294b77b2..a3e1a4fc0 100644
--- a/frontend/forms/PluginManagerWindow.ui
+++ b/frontend/forms/PluginManagerWindow.ui
@@ -6,8 +6,8 @@
0
0
- 861
- 400
+ 800
+ 560
@@ -70,7 +70,7 @@
0
-
+
0
@@ -88,6 +88,12 @@
-
+
+
+ 0
+ 0
+
+
QFrame::Shape::NoFrame
@@ -118,6 +124,12 @@
-
+
+
+ 180
+ 0
+
+
QFrame::Shape::NoFrame
@@ -127,6 +139,9 @@
0
+
+ QAbstractScrollArea::SizeAdjustPolicy::AdjustToContentsOnFirstShow
+
QAbstractItemView::EditTrigger::NoEditTriggers
@@ -139,7 +154,13 @@
-
-
+
+
+
+ 0
+ 0
+
+
QFrame::Shape::NoFrame
@@ -150,167 +171,159 @@
0
- dialog-container dialog-frame
+ dialog-frame
-
-
- 0
+
+
+
+ 0
+ 0
+
-
- 0
-
-
- 0
-
-
- 0
-
-
- 0
-
-
-
-
-
- QFrame::Shape::NoFrame
-
-
- 0
-
-
- PluginManager.Section.Manage.Title
-
-
- 0
-
-
- text-title
-
-
-
- -
-
-
- QFrame::Shape::NoFrame
-
-
- QFrame::Shadow::Plain
-
-
- 0
-
-
- Qt::ScrollBarPolicy::ScrollBarAlwaysOff
-
-
- QAbstractScrollArea::SizeAdjustPolicy::AdjustToContents
-
-
- false
-
-
- Qt::AlignmentFlag::AlignLeading|Qt::AlignmentFlag::AlignLeft|Qt::AlignmentFlag::AlignTop
-
-
-
-
-
-
-
- 0
- 0
- 689
- 50
-
+
+
-
+
+
+ NOT IMPLEMENTED
-
-
- 0
- 0
-
-
-
-
- 0
-
-
- 0
-
-
- 0
-
-
- 0
-
-
- 0
-
-
-
-
-
-
- 0
- 20
-
-
-
- QFrame::Shape::NoFrame
-
-
- QFrame::Shadow::Plain
-
-
- 0
-
-
-
- 0
-
-
- 0
-
-
- 0
-
-
- 0
-
-
- 0
-
-
-
-
- -
-
-
- Qt::Orientation::Vertical
-
-
-
- 10
- 0
-
-
-
-
-
-
-
- -
-
-
- PluginManager.Restart
-
-
- Qt::AlignmentFlag::AlignCenter
-
-
- frame-notice
-
-
-
-
+
+
+
+
+
+
+ 0
+ 0
+
+
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+ -
+
+
+ QFrame::Shape::NoFrame
+
+
+ QFrame::Shadow::Plain
+
+
+ 0
+
+
+ true
+
+
+ Qt::AlignmentFlag::AlignLeading|Qt::AlignmentFlag::AlignLeft|Qt::AlignmentFlag::AlignTop
+
+
+
+
+ 0
+ 0
+ 620
+ 520
+
+
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
-
+
+
+ QFrame::Shape::NoFrame
+
+
+ QFrame::Shadow::Plain
+
+
+ 0
+
+
+ dialog-container
+
+
+
+ 6
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+
+
+
+
+
+
+ -
+
+
+ PluginManager.Restart
+
+
+ Qt::AlignmentFlag::AlignCenter
+
+
+ frame-notice
+
+
+
+
+
+
+
+
+ 0
+ 0
+
+
+
+ -
+
+
+ NOT IMPLEMENTED
+
+
+
+
+
@@ -349,7 +362,7 @@
-
- QDialogButtonBox::StandardButton::Cancel|QDialogButtonBox::StandardButton::Ok
+ QDialogButtonBox::StandardButton::Discard|QDialogButtonBox::StandardButton::Save
diff --git a/frontend/plugin-manager/InstalledPluginRow.cpp b/frontend/plugin-manager/InstalledPluginRow.cpp
new file mode 100644
index 000000000..992cdc692
--- /dev/null
+++ b/frontend/plugin-manager/InstalledPluginRow.cpp
@@ -0,0 +1,164 @@
+/******************************************************************************
+ Copyright (C) 2026 by Warchamp7
+
+ 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 "InstalledPluginRow.hpp"
+
+#include
+#include
+
+#include
+
+constexpr std::string_view kLegacyPluginInfoLink{"https://obsproject.com/go/legacy-plugin-locations"};
+
+namespace OBS {
+InstalledPluginRow::InstalledPluginRow(QWidget *parent, const PluginManagerWindow::Entry &entry) : idian::Row(parent)
+{
+ OBS::ModuleInfo *metadata = entry.module;
+
+ setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred);
+ QString name = entry.name;
+
+ QString version = metadata && !metadata->version.empty() ? metadata->version.c_str() : "";
+
+ auto *moduleText = new QWidget{this};
+ moduleText->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred);
+ auto *infoLayout = new QVBoxLayout{moduleText};
+ infoLayout->setContentsMargins(0, 0, 0, 0);
+ moduleText->setLayout(infoLayout);
+
+ auto *headerLayout = new QHBoxLayout{};
+ headerLayout->setContentsMargins(0, 0, 0, 0);
+ headerLayout->setAlignment(Qt::AlignLeft);
+ infoLayout->addLayout(headerLayout);
+
+ auto nameLabel = new QLabel{name, moduleText};
+ nameLabel->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Minimum);
+ nameLabel->setIndent(0);
+ idian::Utils::addClass(nameLabel, "title");
+ if (metadata && !metadata->enabledAtLaunch) {
+ idian::Utils::addClass(nameLabel, "text-muted");
+ }
+ headerLayout->addWidget(nameLabel);
+
+ if (!version.isEmpty()) {
+ auto versionLabel = new QLabel{version, moduleText};
+ versionLabel->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Minimum);
+ idian::Utils::addClass(versionLabel, "description");
+ headerLayout->addWidget(versionLabel);
+ }
+
+ QVBoxLayout *detailsLayout = new QVBoxLayout{};
+ detailsLayout->setContentsMargins(0, 0, 0, 0);
+ infoLayout->addLayout(detailsLayout);
+
+ if (!entry.isLegacy) {
+ // TODO: Awaiting further implementation of module manifest data.
+ }
+
+ this->addWidget(moduleText);
+
+ InfoChip *statusChip{nullptr};
+ if (entry.status == PluginManagerWindow::Status::Error) {
+ idian::Utils::addClass(nameLabel, "text-muted");
+
+ statusChip = new InfoChip{QTStr("PluginManager.Status.Error"), moduleText};
+ idian::Utils::addClass(statusChip, "bg-warning");
+ idian::Utils::addClass(statusChip, "text-warning");
+ } else if (entry.status == PluginManagerWindow::Status::Missing) {
+ idian::Utils::addClass(nameLabel, "text-muted");
+
+ statusChip = new InfoChip{QTStr("PluginManager.Status.Missing"), moduleText};
+ idian::Utils::addClass(statusChip, "bg-danger");
+ idian::Utils::addClass(statusChip, "text-danger");
+ } else {
+ if (entry.isLegacy) {
+ auto legacyChip = new InfoChip{QTStr("PluginManager.Status.Legacy"), moduleText};
+ idian::Utils::addClass(legacyChip, "bg-primary");
+
+ headerLayout->insertWidget(0, legacyChip);
+
+ auto legacyNotice = new QWidget{};
+ legacyNotice->setLayout(new QHBoxLayout{});
+ legacyNotice->layout()->setContentsMargins(0, 0, 0, 0);
+
+ auto warningIcon = new QLabel{moduleText};
+ warningIcon->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Maximum);
+ warningIcon->setPixmap(getWarningIcon().pixmap(16, 16));
+
+ auto legacyInfo =
+ new QLabel{QTStr("PluginManager.Status.Legacy.Description").arg(kLegacyPluginInfoLink),
+ moduleText};
+ legacyInfo->setTextInteractionFlags(Qt::LinksAccessibleByMouse);
+ legacyInfo->setOpenExternalLinks(true);
+ idian::Utils::addClass(legacyInfo, "description");
+
+ legacyNotice->layout()->addWidget(warningIcon);
+ legacyNotice->layout()->addWidget(legacyInfo);
+
+ detailsLayout->addWidget(legacyNotice);
+ }
+
+ if (metadata && !metadata->enabledAtLaunch) {
+ statusChip = new InfoChip{QTStr("PluginManager.Status.Disabled"), moduleText};
+ idian::Utils::addClass(statusChip, "bg-info");
+ idian::Utils::addClass(statusChip, "text-muted");
+ }
+ }
+
+ if (statusChip) {
+ headerLayout->addWidget(statusChip);
+ }
+
+ if (metadata) {
+ auto toggleSwitch = new idian::ToggleSwitch(this);
+ toggleSwitch->setChecked(metadata->enabled);
+ addWidget(toggleSwitch);
+ toggleSwitch->setAccessibleDescription(QTStr("PluginManager.Button.Enable").arg(name));
+
+ connect(toggleSwitch, &idian::ToggleSwitch::toggled, this, [this, metadata](bool checked) {
+ metadata->enabled = checked;
+
+ emit toggleChanged();
+ });
+ }
+
+ if (entry.status == PluginManagerWindow::Status::Missing) {
+ auto removeButton = new idian::InlineButton(this);
+ removeButton->setAccessibleName(QTStr("Remove"));
+ removeButton->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Maximum);
+ removeButton->setIcon(getTrashIcon());
+ idian::Utils::addClass(removeButton, "icon-trash");
+ addWidget(removeButton);
+
+ connect(removeButton, &QAbstractButton::clicked, this, [this]() {
+ setEnabled(false);
+
+ emit trashClicked();
+ });
+ }
+}
+const QIcon &InstalledPluginRow::getWarningIcon()
+{
+ static const QIcon &icon = *new QIcon(":/res/images/warning.svg");
+ return icon;
+}
+const QIcon &InstalledPluginRow::getTrashIcon()
+{
+ static const QIcon &icon = *new QIcon(":/res/images/trash.svg");
+ return icon;
+}
+} // namespace OBS
diff --git a/frontend/plugin-manager/InstalledPluginRow.hpp b/frontend/plugin-manager/InstalledPluginRow.hpp
new file mode 100644
index 000000000..ac5777780
--- /dev/null
+++ b/frontend/plugin-manager/InstalledPluginRow.hpp
@@ -0,0 +1,40 @@
+/******************************************************************************
+ Copyright (C) 2026 by Warchamp7
+
+ 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
+
+namespace OBS {
+
+class InstalledPluginRow : public idian::Row {
+ Q_OBJECT
+
+public:
+ InstalledPluginRow(QWidget *parent, const PluginManagerWindow::Entry &entry);
+
+signals:
+ void toggleChanged();
+ void trashClicked();
+
+private:
+ static const QIcon &getWarningIcon();
+ static const QIcon &getTrashIcon();
+};
+} // namespace OBS
diff --git a/frontend/plugin-manager/PluginManager.cpp b/frontend/plugin-manager/PluginManager.cpp
index b2fc421c3..8727551b0 100644
--- a/frontend/plugin-manager/PluginManager.cpp
+++ b/frontend/plugin-manager/PluginManager.cpp
@@ -310,24 +310,12 @@ void PluginManager::open()
auto main = OBSBasic::Get();
PluginManagerWindow pluginManagerWindow(modules_, failedModules_, main);
- if (loadState_ == State::PartialFailure) {
- pluginManagerWindow.setPage(PluginManagerWindow::Page::Failure);
- }
-
auto result = pluginManagerWindow.exec();
if (result == QDialog::Accepted) {
- modules_ = pluginManagerWindow.result();
+ modules_ = pluginManagerWindow.getModules();
saveModules_();
- bool changed = false;
-
- for (auto const &moduleInfo : modules_) {
- if (moduleInfo.enabled != moduleInfo.enabledAtLaunch) {
- changed = true;
- break;
- }
- }
-
+ bool changed = pluginManagerWindow.isEnabledPluginsChanged();
if (changed) {
QMessageBox::StandardButton button =
OBSMessageBox::question(main, QTStr("Restart"), QTStr("NeedsRestart"));
diff --git a/frontend/plugin-manager/PluginManagerWindow.cpp b/frontend/plugin-manager/PluginManagerWindow.cpp
index 75411690c..df3123fdd 100644
--- a/frontend/plugin-manager/PluginManagerWindow.cpp
+++ b/frontend/plugin-manager/PluginManagerWindow.cpp
@@ -18,21 +18,44 @@
#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 OBS {
+namespace {
+using Status = OBS::PluginManagerWindow::Status;
+constexpr int getStatusSortOrder(Status s)
+{
+ switch (s) {
+ case Status::Loadable:
+ return 0;
+ case Status::Error:
+ return 1;
+ case Status::Missing:
+ return 2;
+ default:
+ return 3;
+ }
+}
+} // namespace
+namespace OBS {
PluginManagerWindow::PluginManagerWindow(std::vector const &modules,
std::vector const &failedModules, QWidget *parent)
@@ -44,9 +67,6 @@ PluginManagerWindow::PluginManagerWindow(std::vector const &modules,
ui->setupUi(this);
- ui->modulesListContainer->viewport()->setAutoFillBackground(false);
- ui->modulesListContents->setAutoFillBackground(false);
-
// Set up sidebar entries
ui->sectionList->clear();
ui->sectionList->setSelectionMode(QAbstractItemView::SingleSelection);
@@ -63,86 +83,154 @@ PluginManagerWindow::PluginManagerWindow(std::vector const &modules,
QListWidgetItem *installed = new QListWidgetItem(QTStr("PluginManager.Section.Manage"));
ui->sectionList->addItem(installed);
- QListWidgetItem *failed = new QListWidgetItem("Failed");
- ui->sectionList->addItem(failed);
-
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);
- setSection(ui->sectionList->indexFromItem(installed));
+ setupInstalledPage(failedModules);
- 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;
- });
+ setPage(Page::Installed);
- std::sort(modules_.begin(), modules_.end(), [](const ModuleInfo &a, const ModuleInfo &b) {
- bool missingA = !obs_get_module(a.module_name.c_str()) &&
- !obs_get_disabled_module(a.module_name.c_str());
- bool missingB = !obs_get_module(b.module_name.c_str()) &&
- !obs_get_disabled_module(b.module_name.c_str());
+ ui->manageRestartLabel->setVisible(isEnabledPluginsChanged());
- return !missingA && missingB;
- });
+ connect(ui->buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept);
+ connect(ui->buttonBox->button(QDialogButtonBox::Discard), &QPushButton::clicked, this, &QDialog::close);
+}
- int row = 0;
- int missingIndex = -1;
+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);
+
+ 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 id{metadata.module_name};
- // Check if the module is missing:
- obs_module_t *moduleData = obs_get_module(id.data());
+ std::string_view module_name{metadata.module_name};
- if (!moduleData) {
- moduleData = obs_get_disabled_module(id.data());
+ // Check if the module is missing:
+ obs_module_t *moduleData = obs_get_module(module_name.data());
+
+ Status status{Status::Loadable};
+
+ bool isLoaded = moduleData != nullptr;
+ if (!isLoaded) {
+ status = Status::Missing;
+ moduleData = obs_get_disabled_module(module_name.data());
}
- bool isMissingModule = !moduleData;
- bool isLegacyModule = !isMissingModule && obs_is_legacy_module(moduleData);
+ 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.
+ status = Status::Error;
+ failedModules.erase(failedModuleIterator);
+ } else {
+ // Module is disabled but did not fail to load.
+ status = Status::Loadable;
+ }
+ }
+
+ 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() : "";
- if (isMissingModule && missingIndex == -1) {
- missingIndex = row;
- }
-
- if (isLegacyModule) {
- name += " LEGACY";
- }
-
- auto item = new QCheckBox(name);
- item->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Fixed);
- item->setChecked(metadata.enabled);
-
- if (!metadata.enabledAtLaunch || isMissingModule) {
- item->setProperty("class", "text-muted");
- }
-
- ui->modulesList->layout()->addWidget(item);
-
- connect(item, &QCheckBox::toggled, this, [this, row](bool checked) {
- modules_[row].enabled = checked;
- ui->manageRestartLabel->setVisible(isEnabledPluginsChanged());
- });
-
- row++;
+ Entry newEntry{&metadata, name, status, isLegacyModule};
+ installedPluginEntries.push_back(newEntry);
}
- QLabel *item = new QLabel("FAILED ITEMS");
- item->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Fixed);
- ui->modulesList->layout()->addWidget(item);
-
for (const std::string &moduleName : failedModules) {
- QString name = QString::fromStdString(moduleName);
+ // 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());
+ Status status{Status::Error};
+ bool isLegacy{false};
- QLabel *item = new QLabel(name);
- item->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Fixed);
- item->setProperty("class", "text-muted");
- ui->modulesList->layout()->addWidget(item);
+ Entry newEntry{nullptr, name, status, isLegacy};
+ installedPluginEntries.push_back(newEntry);
+ }
+
+ std::sort(installedPluginEntries.begin(), installedPluginEntries.end(), [](const Entry &a, const Entry &b) {
+ if (a.status != b.status) {
+ return getStatusSortOrder(a.status) < getStatusSortOrder(b.status);
+ }
+
+ return a.name.toLower() < b.name.toLower();
+ });
+
+ QWidget *previousRow{nullptr};
+ for (Entry &entry : installedPluginEntries) {
+ auto newRow = new InstalledPluginRow(ui->modulesList, entry);
+
+ if (!previousRow) {
+ setTabOrder(ui->modulesListContainer, newRow);
+ } else {
+ setTabOrder(previousRow, newRow);
+ }
+
+ previousRow = newRow;
+
+ if (entry.status == Status::Loadable) {
+ installedPluginList->addRow(newRow);
+
+ connect(newRow, &InstalledPluginRow::toggleChanged, this,
+ [this]() { ui->manageRestartLabel->setVisible(isEnabledPluginsChanged()); });
+
+ } else if (entry.status == Status::Error) {
+ errorPluginList->addRow(newRow);
+ } else if (entry.status == Status::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);
}
QVBoxLayout *layout = qobject_cast(ui->modulesList->layout());
@@ -153,49 +241,46 @@ PluginManagerWindow::PluginManagerWindow(std::vector const &modules,
safeModeLabel->setIndent(0);
layout->insertWidget(0, safeModeLabel);
- } else if (missingIndex != -1) {
- QLabel *missingLabel = new QLabel(ui->modulesList);
- missingLabel->setText(QTStr("PluginManager.MissingPlugin"));
- missingLabel->setProperty("class", "text-warning text-bold");
- missingLabel->setIndent(0);
-
- layout->insertWidget(missingIndex, new QLabel("", ui->modulesList));
- layout->insertWidget(missingIndex + 1, missingLabel);
}
- ui->modulesList->adjustSize();
- ui->modulesListContents->adjustSize();
-
- ui->manageRestartLabel->setVisible(isEnabledPluginsChanged());
-
- connect(ui->buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept);
- connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
+ // 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);
+ setSection(activeSectionIndex.row());
} else {
auto selectionIndex = ui->sectionList->indexFromItem(selected.first());
- setSection(selectionIndex);
+ setSection(selectionIndex.row());
}
}
-void PluginManagerWindow::setSection(QPersistentModelIndex index)
+void PluginManagerWindow::setSection(int sidebarRow)
{
- if (ui->sectionList->itemFromIndex(index)) {
- activeSectionIndex = index;
- ui->sectionList->setCurrentIndex(index);
+ if (auto item = ui->sectionList->item(sidebarRow)) {
+ activeSectionIndex = ui->sectionList->indexFromItem(item);
+ ui->sectionList->setCurrentIndex(activeSectionIndex);
+
+ ui->stackedContents->setCurrentIndex(sidebarRow);
}
}
bool PluginManagerWindow::isEnabledPluginsChanged()
{
bool result = false;
- for (auto &metadata : modules_) {
- if (metadata.enabledAtLaunch != metadata.enabled) {
+ for (auto &entry : installedPluginEntries) {
+ // Only prompt for restart when a loadable plugin entry changed.
+ if (entry.status == Status::Loadable && entry.module->enabledAtLaunch != entry.module->enabled) {
result = true;
break;
}
@@ -208,14 +293,10 @@ void PluginManagerWindow::setPage(Page page)
{
switch (page) {
case Page::Installed:
- ui->sectionList->setCurrentRow(1);
- break;
- case Page::Failure:
- ui->sectionList->setCurrentRow(2);
+ setSection(1);
break;
default:
break;
}
}
-
}; // namespace OBS
diff --git a/frontend/plugin-manager/PluginManagerWindow.hpp b/frontend/plugin-manager/PluginManagerWindow.hpp
index 54e19e4a9..9817d2258 100644
--- a/frontend/plugin-manager/PluginManagerWindow.hpp
+++ b/frontend/plugin-manager/PluginManagerWindow.hpp
@@ -17,12 +17,13 @@
#pragma once
-#include "ui_PluginManagerWindow.h"
#include "PluginManager.hpp"
#include
#include
+#include "ui_PluginManagerWindow.h"
+
namespace OBS {
class PluginManagerWindow : public QDialog {
@@ -30,22 +31,34 @@ class PluginManagerWindow : public QDialog {
std::unique_ptr ui;
public:
- enum class Page { Installed, Failure };
+ enum class Status { Invalid = 0, Loadable, Error, Missing };
+
+ struct Entry {
+ OBS::ModuleInfo *module{nullptr};
+ QString name{};
+ Status status{Status::Invalid};
+ bool isLegacy{false};
+ };
+
+ enum class Page { Installed };
explicit PluginManagerWindow(std::vector const &modules,
std::vector const &failedModules, QWidget *parent = nullptr);
- inline std::vector const result() { return modules_; }
+
+ std::vector const getModules() { return modules_; }
+ bool isEnabledPluginsChanged();
void setPage(Page page);
private:
std::vector modules_;
+ std::vector installedPluginEntries;
+
+ void setupInstalledPage(std::vector failedModules);
void sectionSelectionChanged();
QPersistentModelIndex activeSectionIndex;
- void setSection(QPersistentModelIndex index);
-
- bool isEnabledPluginsChanged();
+ void setSection(int sidebarRow);
};
}; // namespace OBS