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