frontend: Update plugin manager window

Updates the plugin manager window to use the new failed module info.

Plugins installed in legacy folder locations are marked as Legacy with
a notice that will stop working in the future.

Plugins that failed to load are listed under an Errors section.

Plugins in the modules.json file that are not found are listed under
a Missing section. These entries can be removed from the modules.json
via a delete button.

This also cleans up some other small issues and updates the dialog
buttons to be Save/Discard instead of Ok/Close.
This commit is contained in:
Warchamp7
2026-09-23 18:53:19 -04:00
committed by Ryan Foster
parent 08069bd01a
commit 5c32e2f408
8 changed files with 586 additions and 273 deletions
@@ -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
+13 -1
View File
@@ -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. <a href=\"%1\">Learn More</a>"
# Custom Widget Localization
Accessible.Widget.Name.AlignmentSelector="Alignment Selector"
+175 -162
View File
@@ -6,8 +6,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>861</width>
<height>400</height>
<width>800</width>
<height>560</height>
</rect>
</property>
<property name="windowTitle">
@@ -70,7 +70,7 @@
<property name="lineWidth">
<number>0</number>
</property>
<layout class="QHBoxLayout" name="horizontalLayout" stretch="1,4">
<layout class="QHBoxLayout" name="horizontalLayout">
<property name="spacing">
<number>0</number>
</property>
@@ -88,6 +88,12 @@
</property>
<item>
<widget class="QFrame" name="sectionFrame">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="frameShape">
<enum>QFrame::Shape::NoFrame</enum>
</property>
@@ -118,6 +124,12 @@
</property>
<item>
<widget class="QListWidget" name="sectionList">
<property name="minimumSize">
<size>
<width>180</width>
<height>0</height>
</size>
</property>
<property name="frameShape">
<enum>QFrame::Shape::NoFrame</enum>
</property>
@@ -127,6 +139,9 @@
<property name="lineWidth">
<number>0</number>
</property>
<property name="sizeAdjustPolicy">
<enum>QAbstractScrollArea::SizeAdjustPolicy::AdjustToContentsOnFirstShow</enum>
</property>
<property name="editTriggers">
<set>QAbstractItemView::EditTrigger::NoEditTriggers</set>
</property>
@@ -139,7 +154,13 @@
</widget>
</item>
<item>
<widget class="QFrame" name="modulesFrame">
<widget class="QStackedWidget" name="stackedContents">
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="MinimumExpanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="frameShape">
<enum>QFrame::Shape::NoFrame</enum>
</property>
@@ -150,167 +171,159 @@
<number>0</number>
</property>
<property name="class" stdset="0">
<string>dialog-container dialog-frame</string>
<string>dialog-frame</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_4">
<property name="spacing">
<number>0</number>
<widget class="QWidget" name="browsePage">
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="MinimumExpanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QLabel" name="label">
<property name="frameShape">
<enum>QFrame::Shape::NoFrame</enum>
</property>
<property name="lineWidth">
<number>0</number>
</property>
<property name="text">
<string>PluginManager.Section.Manage.Title</string>
</property>
<property name="indent">
<number>0</number>
</property>
<property name="class" stdset="0">
<string>text-title</string>
</property>
</widget>
</item>
<item>
<widget class="QScrollArea" name="modulesListContainer">
<property name="frameShape">
<enum>QFrame::Shape::NoFrame</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Shadow::Plain</enum>
</property>
<property name="lineWidth">
<number>0</number>
</property>
<property name="horizontalScrollBarPolicy">
<enum>Qt::ScrollBarPolicy::ScrollBarAlwaysOff</enum>
</property>
<property name="sizeAdjustPolicy">
<enum>QAbstractScrollArea::SizeAdjustPolicy::AdjustToContents</enum>
</property>
<property name="widgetResizable">
<bool>false</bool>
</property>
<property name="alignment">
<set>Qt::AlignmentFlag::AlignLeading|Qt::AlignmentFlag::AlignLeft|Qt::AlignmentFlag::AlignTop</set>
</property>
<property name="class" stdset="0">
<string/>
</property>
<widget class="QWidget" name="modulesListContents">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>689</width>
<height>50</height>
</rect>
<layout class="QVBoxLayout" name="verticalLayout_7">
<item>
<widget class="QLabel" name="browseTitle">
<property name="text">
<string>NOT IMPLEMENTED</string>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="MinimumExpanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<layout class="QVBoxLayout" name="verticalLayout_5">
<property name="spacing">
<number>0</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QFrame" name="modulesList">
<property name="minimumSize">
<size>
<width>0</width>
<height>20</height>
</size>
</property>
<property name="frameShape">
<enum>QFrame::Shape::NoFrame</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Shadow::Plain</enum>
</property>
<property name="lineWidth">
<number>0</number>
</property>
<layout class="QVBoxLayout" name="verticalLayout_6">
<property name="spacing">
<number>0</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
</layout>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Orientation::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>10</width>
<height>0</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
</widget>
</item>
<item>
<widget class="QLabel" name="manageRestartLabel">
<property name="text">
<string>PluginManager.Restart</string>
</property>
<property name="alignment">
<set>Qt::AlignmentFlag::AlignCenter</set>
</property>
<property name="class" stdset="0">
<string>frame-notice</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<widget class="QWidget" name="installedPage">
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="MinimumExpanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<layout class="QVBoxLayout" name="verticalLayout_4">
<property name="spacing">
<number>0</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QScrollArea" name="modulesListContainer">
<property name="frameShape">
<enum>QFrame::Shape::NoFrame</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Shadow::Plain</enum>
</property>
<property name="lineWidth">
<number>0</number>
</property>
<property name="widgetResizable">
<bool>true</bool>
</property>
<property name="alignment">
<set>Qt::AlignmentFlag::AlignLeading|Qt::AlignmentFlag::AlignLeft|Qt::AlignmentFlag::AlignTop</set>
</property>
<widget class="QWidget" name="modulesListOuter">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>620</width>
<height>520</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout_5">
<property name="spacing">
<number>0</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QFrame" name="modulesList">
<property name="frameShape">
<enum>QFrame::Shape::NoFrame</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Shadow::Plain</enum>
</property>
<property name="lineWidth">
<number>0</number>
</property>
<property name="class" stdset="0">
<string>dialog-container</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_6">
<property name="spacing">
<number>6</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
</layout>
</widget>
</item>
</layout>
</widget>
</widget>
</item>
<item>
<widget class="QLabel" name="manageRestartLabel">
<property name="text">
<string>PluginManager.Restart</string>
</property>
<property name="alignment">
<set>Qt::AlignmentFlag::AlignCenter</set>
</property>
<property name="class" stdset="0">
<string>frame-notice</string>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="updatesPage">
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="MinimumExpanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<layout class="QVBoxLayout" name="verticalLayout_8">
<item>
<widget class="QLabel" name="browseTitle_2">
<property name="text">
<string>NOT IMPLEMENTED</string>
</property>
</widget>
</item>
</layout>
</widget>
</widget>
</item>
</layout>
@@ -349,7 +362,7 @@
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="standardButtons">
<set>QDialogButtonBox::StandardButton::Cancel|QDialogButtonBox::StandardButton::Ok</set>
<set>QDialogButtonBox::StandardButton::Discard|QDialogButtonBox::StandardButton::Save</set>
</property>
</widget>
</item>
@@ -0,0 +1,164 @@
/******************************************************************************
Copyright (C) 2026 by Warchamp7 <warchamp7@obsproject.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 "InstalledPluginRow.hpp"
#include <OBSApp.hpp>
#include <components/InfoChip.hpp>
#include <Idian/InlineButton.hpp>
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
@@ -0,0 +1,40 @@
/******************************************************************************
Copyright (C) 2026 by Warchamp7 <warchamp7@obsproject.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 <plugin-manager/PluginManagerWindow.hpp>
#include <Idian/Row.hpp>
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
+2 -14
View File
@@ -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"));
+171 -90
View File
@@ -18,21 +18,44 @@
#include "PluginManagerWindow.hpp"
#include <OBSApp.hpp>
#include <plugin-manager/InstalledPluginRow.hpp>
#include <Idian/ListHeader.hpp>
#include <Idian/RowList.hpp>
#include <Idian/Utils.hpp>
#include <QCheckBox>
#include <QDialogButtonBox>
#include <QHBoxLayout>
#include <QLabel>
#include <QPushButton>
#include <QScrollArea>
#include <QString>
#include <QTimer>
#include <QVBoxLayout>
#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<ModuleInfo> const &modules,
std::vector<std::string> const &failedModules, QWidget *parent)
@@ -44,9 +67,6 @@ PluginManagerWindow::PluginManagerWindow(std::vector<ModuleInfo> 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<ModuleInfo> 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<std::string> 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<QVBoxLayout *>(ui->modulesList->layout());
@@ -153,49 +241,46 @@ PluginManagerWindow::PluginManagerWindow(std::vector<ModuleInfo> 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
@@ -17,12 +17,13 @@
#pragma once
#include "ui_PluginManagerWindow.h"
#include "PluginManager.hpp"
#include <QDialog>
#include <QWidget>
#include "ui_PluginManagerWindow.h"
namespace OBS {
class PluginManagerWindow : public QDialog {
@@ -30,22 +31,34 @@ class PluginManagerWindow : public QDialog {
std::unique_ptr<Ui::PluginManagerWindow> 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<ModuleInfo> const &modules,
std::vector<std::string> const &failedModules, QWidget *parent = nullptr);
inline std::vector<ModuleInfo> const result() { return modules_; }
std::vector<ModuleInfo> const getModules() { return modules_; }
bool isEnabledPluginsChanged();
void setPage(Page page);
private:
std::vector<ModuleInfo> modules_;
std::vector<Entry> installedPluginEntries;
void setupInstalledPage(std::vector<std::string> failedModules);
void sectionSelectionChanged();
QPersistentModelIndex activeSectionIndex;
void setSection(QPersistentModelIndex index);
bool isEnabledPluginsChanged();
void setSection(int sidebarRow);
};
}; // namespace OBS