mirror of
https://github.com/obsproject/obs-studio.git
synced 2026-09-26 17:30:32 -04:00
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:
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
@@ -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"));
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user