From 5c32e2f408c939892094776e65b060a1649bb798 Mon Sep 17 00:00:00 2001 From: Warchamp7 Date: Mon, 21 Sep 2026 16:06:50 -0400 Subject: [PATCH] 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. --- frontend/cmake/feature-plugin-manager.cmake | 2 + frontend/data/locale/en-US.ini | 14 +- frontend/forms/PluginManagerWindow.ui | 337 +++++++++--------- .../plugin-manager/InstalledPluginRow.cpp | 164 +++++++++ .../plugin-manager/InstalledPluginRow.hpp | 40 +++ frontend/plugin-manager/PluginManager.cpp | 16 +- .../plugin-manager/PluginManagerWindow.cpp | 261 +++++++++----- .../plugin-manager/PluginManagerWindow.hpp | 25 +- 8 files changed, 586 insertions(+), 273 deletions(-) create mode 100644 frontend/plugin-manager/InstalledPluginRow.cpp create mode 100644 frontend/plugin-manager/InstalledPluginRow.hpp 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