File indexing completed on 2024-04-21 16:22:51

0001 /*
0002  *   SPDX-FileCopyrightText: 2009 Ben Cooksley <bcooksley@kde.org>
0003  *   SPDX-FileCopyrightText: 2009 Mathias Soeken <msoeken@informatik.uni-bremen.de>
0004  *   SPDX-FileCopyrightText: 2021 Harald Sitter <sitter@kde.org>
0005  *
0006  *   SPDX-License-Identifier: GPL-2.0-or-later
0007  */
0008 
0009 #include "ModuleView.h"
0010 #include "ExternalAppModule.h"
0011 
0012 #include <QDesktopServices>
0013 #include <QDialogButtonBox>
0014 #include <QKeyEvent>
0015 #include <QList>
0016 #include <QLoggingCategory>
0017 #include <QMap>
0018 #include <QPainter>
0019 #include <QProcess>
0020 #include <QPushButton>
0021 #include <QScrollArea>
0022 #include <QStyle>
0023 #include <QVBoxLayout>
0024 #include <QWhatsThis>
0025 
0026 #include <KAboutData>
0027 #include <kauth_version.h>
0028 #if KAUTH_VERSION >= QT_VERSION_CHECK(5, 92, 0)
0029 #include <KAuth/Action>
0030 #include <KAuth/ObjectDecorator>
0031 #else
0032 #include <KAuthAction>
0033 #include <KAuthObjectDecorator>
0034 #endif
0035 #include <KAuthorized>
0036 #include <KCModuleInfo>
0037 #include <KCModuleProxy>
0038 #include <KColorScheme>
0039 #include <KMessageDialog>
0040 #include <KPageWidget>
0041 #include <KSharedConfig>
0042 #include <KStandardGuiItem>
0043 #include <KTitleWidget>
0044 #include <Kirigami/Units>
0045 
0046 #include <KActivities/ResourceInstance>
0047 
0048 #include <cmath>
0049 #include <kpluginmetadata.h>
0050 
0051 #include "MenuItem.h"
0052 
0053 class CustomTitle : public KTitleWidget
0054 {
0055 public:
0056     explicit CustomTitle(QWidget *parent = nullptr);
0057 
0058 protected:
0059     void paintEvent(QPaintEvent *event) override;
0060     void colorsChanged();
0061 };
0062 
0063 CustomTitle::CustomTitle(QWidget *parent)
0064     : KTitleWidget(parent)
0065 {
0066     // Use the same left margin as QML titles for consistency (Kirigami/AbstractPageHeader.qml)
0067     setContentsMargins(Kirigami::Units().gridUnit(),
0068                        style()->pixelMetric(QStyle::PM_LayoutTopMargin),
0069                        style()->pixelMetric(QStyle::PM_LayoutRightMargin),
0070                        style()->pixelMetric(QStyle::PM_LayoutBottomMargin));
0071 
0072     colorsChanged();
0073     connect(qApp, &QApplication::paletteChanged, this, &CustomTitle::colorsChanged);
0074 }
0075 
0076 void CustomTitle::colorsChanged()
0077 {
0078     auto config = KSharedConfig::openConfig();
0079     auto active = KColorScheme(QPalette::Active, KColorScheme::Header, config);
0080     auto inactive = KColorScheme(QPalette::Inactive, KColorScheme::Header, config);
0081     auto disabled = KColorScheme(QPalette::Disabled, KColorScheme::Header, config);
0082 
0083     QPalette palette = KColorScheme::createApplicationPalette(config);
0084 
0085     palette.setBrush(QPalette::Active, QPalette::Window, active.background());
0086     palette.setBrush(QPalette::Active, QPalette::WindowText, active.foreground());
0087     palette.setBrush(QPalette::Disabled, QPalette::Window, disabled.background());
0088     palette.setBrush(QPalette::Disabled, QPalette::WindowText, disabled.foreground());
0089     palette.setBrush(QPalette::Inactive, QPalette::Window, inactive.background());
0090     palette.setBrush(QPalette::Inactive, QPalette::WindowText, inactive.foreground());
0091 
0092     setPalette(palette);
0093 }
0094 
0095 void CustomTitle::paintEvent(QPaintEvent *event)
0096 {
0097     KTitleWidget::paintEvent(event);
0098 
0099     auto linearlyInterpolateDouble = [](double one, double two, double factor) {
0100         return one + (two - one) * factor;
0101     };
0102 
0103     QPainter p(this);
0104 
0105     const QColor window = palette().color(QPalette::Window);
0106     const QColor text = palette().color(QPalette::Text);
0107     const qreal balance = 0.2;
0108 
0109     const QColor separator = QColor::fromHsv(std::fmod(linearlyInterpolateDouble(window.hue(), text.hue(), balance), 360.0),
0110                                              qBound(0.0, linearlyInterpolateDouble(window.saturation(), text.saturation(), balance), 255.0),
0111                                              qBound(0.0, linearlyInterpolateDouble(window.value(), text.value(), balance), 255.0),
0112                                              qBound(0.0, linearlyInterpolateDouble(window.alpha(), text.alpha(), balance), 255.0));
0113     p.fillRect(event->rect(), window);
0114     p.fillRect(QRect(QPoint(0, height() - 1), QSize(width(), 1)), separator);
0115 }
0116 
0117 class ModuleView::Private
0118 {
0119 public:
0120     Private()
0121     {
0122     }
0123     QMap<KPageWidgetItem *, KCModuleProxy *> mPages;
0124     QMap<KPageWidgetItem *, QString> mPagesPluginIdMap;
0125     KPageWidget *mPageWidget = nullptr;
0126     CustomTitle *mCustomHeader = nullptr;
0127     QVBoxLayout *mLayout = nullptr;
0128     QDialogButtonBox *mButtons = nullptr;
0129     KAuth::ObjectDecorator *mApplyAuthorize = nullptr;
0130     QPushButton *mApply = nullptr;
0131     QPushButton *mReset = nullptr;
0132     QPushButton *mDefault = nullptr;
0133     QPushButton *mHelp = nullptr;
0134     KMessageDialog *mResolvingChangesDialog = nullptr;
0135     bool pageChangeSupressed = false;
0136     bool mSaveStatistics = true;
0137     bool mDefaultsIndicatorsVisible = false;
0138     KCModule::Buttons mButtonMask = ~KCModule::Buttons(KCModule::NoAdditionalButton);
0139 };
0140 
0141 ModuleView::ModuleView(QWidget *parent)
0142     : QWidget(parent)
0143     , d(new Private())
0144 {
0145     QVBoxLayout *rootLayout = new QVBoxLayout(this);
0146     rootLayout->setContentsMargins(0, 0, 0, 0);
0147     rootLayout->setSpacing(0);
0148     // Configure a layout first
0149     d->mLayout = new QVBoxLayout();
0150     // Create the Page Widget
0151     d->mPageWidget = new KPageWidget(this);
0152     d->mCustomHeader = new CustomTitle(this);
0153     d->mCustomHeader->setVisible(false);
0154 
0155     rootLayout->addWidget(d->mCustomHeader);
0156     rootLayout->addItem(d->mLayout);
0157     // d->mPageWidget->setPageHeader(d->mCustomHeader);
0158     d->mPageWidget->layout()->setContentsMargins(0, 0, 0, 0);
0159 
0160     // Zero out only the horizontal spacing (the vertical spacing is fine)
0161     QGridLayout *gridLayout = static_cast<QGridLayout *>(d->mPageWidget->layout());
0162 
0163     gridLayout->setHorizontalSpacing(0);
0164 
0165     d->mLayout->addWidget(d->mPageWidget);
0166     // Create the dialog
0167     d->mButtons = new QDialogButtonBox(Qt::Horizontal, this);
0168     d->mLayout->addWidget(d->mButtons);
0169 
0170     // Create the buttons in it
0171     d->mApply = d->mButtons->addButton(QDialogButtonBox::Apply);
0172     KGuiItem::assign(d->mApply, KStandardGuiItem::apply());
0173     d->mDefault = d->mButtons->addButton(QDialogButtonBox::RestoreDefaults);
0174     KGuiItem::assign(d->mDefault, KStandardGuiItem::defaults());
0175     d->mReset = d->mButtons->addButton(QDialogButtonBox::Reset);
0176     KGuiItem::assign(d->mReset, KStandardGuiItem::reset());
0177     d->mHelp = d->mButtons->addButton(QDialogButtonBox::Help);
0178     KGuiItem::assign(d->mHelp, KStandardGuiItem::help());
0179     // Set some more sensible tooltips
0180     d->mReset->setToolTip(i18n("Reset all current changes to previous values"));
0181     // Set Auto-Default mode ( KDE Bug #211187 )
0182     d->mApply->setAutoDefault(true);
0183     d->mDefault->setAutoDefault(true);
0184     d->mReset->setAutoDefault(true);
0185     d->mHelp->setAutoDefault(true);
0186     // Prevent the buttons from being used
0187     d->mApply->setEnabled(false);
0188     d->mDefault->setEnabled(false);
0189     d->mReset->setEnabled(false);
0190     d->mHelp->setEnabled(false);
0191     // Connect up the buttons
0192     connect(d->mApply, SIGNAL(clicked()), this, SLOT(moduleSave()));
0193     connect(d->mReset, &QAbstractButton::clicked, this, &ModuleView::moduleLoad);
0194     connect(d->mHelp, &QAbstractButton::clicked, this, &ModuleView::moduleHelp);
0195     connect(d->mDefault, &QAbstractButton::clicked, this, &ModuleView::moduleDefaults);
0196     // clang-format off
0197     connect(d->mPageWidget, SIGNAL(currentPageChanged(KPageWidgetItem*,KPageWidgetItem*)),
0198              this, SLOT(activeModuleChanged(KPageWidgetItem*,KPageWidgetItem*)));
0199     // clang-format on
0200     d->mApplyAuthorize = new KAuth::ObjectDecorator(d->mApply);
0201     d->mApplyAuthorize->setAuthAction(KAuth::Action());
0202 }
0203 
0204 ModuleView::~ModuleView()
0205 {
0206     delete d;
0207 }
0208 
0209 QString ModuleView::activeModuleName() const
0210 {
0211     return d->mPageWidget->currentPage() ? d->mPageWidget->currentPage()->name() : QString();
0212 }
0213 
0214 void ModuleView::loadModule(const QModelIndex &menuItem, const QStringList &args)
0215 {
0216     if (!menuItem.isValid()) {
0217         return;
0218     }
0219 
0220     MenuItem *item = menuItem.data(Qt::UserRole).value<MenuItem *>();
0221 
0222     // if module has a main page (like in Appearance > Global Theme) we'll load that
0223     if (item->isLibrary() || item->isExternalAppModule()) {
0224         addModule(item, args);
0225     }
0226     // if module doesn't have a main page, we'll load the first subpage
0227     else if (menuItem.model()->rowCount(menuItem) > 0) {
0228         MenuItem *subpageItem = menuItem.model()->index(0, 0, menuItem).data(Qt::UserRole).value<MenuItem *>();
0229         addModule(subpageItem, args);
0230     }
0231 }
0232 
0233 void ModuleView::addModule(MenuItem *item, const QStringList &args)
0234 {
0235     const KPluginMetaData data = item->metaData();
0236     if (!KAuthorized::authorizeControlModule(data.pluginId())) {
0237         qWarning() << "Not authorised to load module";
0238         return;
0239     }
0240     if (data.isHidden()) {
0241         return;
0242     }
0243 
0244     if (KPageWidgetItem *page = d->mPagesPluginIdMap.key(data.name())) {
0245         activeModuleChanged(page, d->mPageWidget->currentPage());
0246         return;
0247     }
0248 
0249     // Create the scroller
0250     auto *moduleScroll = new QScrollArea(this);
0251     // Prepare the scroll area
0252     moduleScroll->setWidgetResizable(true);
0253     moduleScroll->setFrameStyle(QFrame::NoFrame);
0254     moduleScroll->viewport()->setAutoFillBackground(false);
0255     // Create the page
0256     auto *page = new KPageWidgetItem(moduleScroll, data.name());
0257     // Provide information to the users
0258 
0259     if (item->isExternalAppModule()) {
0260         auto *externalWidget = new ExternalAppModule(this, KService::Ptr(new KService(item->metaData().metaDataFileName())));
0261         moduleScroll->setWidget(externalWidget);
0262         d->mCustomHeader->setText(item->metaData().name()); // We have to set this manually, BUG: 448672
0263         page->setName(QString());
0264     } else { // It must be a normal module then
0265         auto *moduleProxy = new KCModuleProxy(data, moduleScroll, args);
0266         moduleScroll->setWidget(moduleProxy);
0267         moduleProxy->setAutoFillBackground(false);
0268         connect(moduleProxy, &KCModuleProxy::changed, this, &ModuleView::stateChanged);
0269         d->mPages.insert(page, moduleProxy);
0270     }
0271 
0272     d->mPagesPluginIdMap.insert(page, data.name());
0273     updatePageIconHeader(page);
0274     // Add the new page
0275     d->mPageWidget->addPage(page);
0276 }
0277 
0278 void ModuleView::updatePageIconHeader(KPageWidgetItem *page)
0279 {
0280     if (!page) {
0281         // Page is invalid. Probably means we have a race condition during closure of everyone so do nothing
0282         return;
0283     }
0284 
0285     KCModuleProxy *moduleProxy = d->mPages.value(page);
0286     if (!moduleProxy || !moduleProxy->metaData().isValid()) {
0287         // Seems like we have some form of a race condition going on here...
0288         return;
0289     }
0290 
0291     const QString moduleName = moduleProxy->metaData().name();
0292     page->setHeader(moduleName);
0293     page->setIcon(QIcon::fromTheme(moduleProxy->metaData().iconName()));
0294 
0295     const bool isQml = moduleProxy->realModule() && moduleProxy->realModule()->inherits("KCModuleQml");
0296     const bool isSidebar = faceType() == KPageView::Plain;
0297 
0298     // Use the module's header only for QWidgets KCMs on Icons mode
0299     page->setHeaderVisible(!isQml && !isSidebar);
0300 
0301     // Use the custom header only for QWidgets KCMs on Sidebar mode
0302     // Only affect visibility if it's the current page
0303     if (d->mPageWidget->currentPage() == page) {
0304         if (!isQml && isSidebar) {
0305             d->mCustomHeader->setText(moduleName); // also includes show()
0306         } else {
0307             d->mCustomHeader->hide();
0308         }
0309     }
0310 }
0311 
0312 bool ModuleView::resolveChanges()
0313 {
0314     KCModuleProxy *currentProxy = d->mPages.value(d->mPageWidget->currentPage());
0315     return resolveChanges(currentProxy);
0316 }
0317 
0318 bool ModuleView::resolveChanges(KCModuleProxy *currentProxy)
0319 {
0320     if (!currentProxy || !currentProxy->isChanged()) {
0321         return true;
0322     }
0323 
0324     // if we are already resolving changes handle it like a cancel
0325     if (d->mResolvingChangesDialog) {
0326         d->mResolvingChangesDialog->reject();
0327     }
0328 
0329     // Let the user decide
0330     d->mResolvingChangesDialog = new KMessageDialog(KMessageDialog::WarningTwoActionsCancel,
0331                                                     i18n("The settings of the current module have changed.\n"
0332                                                          "Do you want to apply the changes or discard them?"),
0333                                                     this);
0334     d->mResolvingChangesDialog->setAttribute(Qt::WA_DeleteOnClose);
0335     d->mResolvingChangesDialog->setButtons(KStandardGuiItem::apply(), KStandardGuiItem::discard(), KStandardGuiItem::cancel());
0336     d->mResolvingChangesDialog->setCaption(i18n("Apply Settings"));
0337     d->mResolvingChangesDialog->setIcon(QIcon()); // Use default message box warning icon.
0338     int result = d->mResolvingChangesDialog->exec();
0339     d->mResolvingChangesDialog = nullptr;
0340 
0341     switch (result) {
0342     case KMessageDialog::PrimaryAction:
0343         return moduleSave(currentProxy);
0344     case KMessageDialog::SecondaryAction:
0345         currentProxy->load();
0346         return true;
0347     case KMessageDialog::Cancel:
0348         return false;
0349     default:
0350         Q_ASSERT(false);
0351         return false;
0352     }
0353 }
0354 
0355 void ModuleView::closeModules()
0356 {
0357     d->pageChangeSupressed = true;
0358     d->mApplyAuthorize->setAuthAction(KAuth::Action()); // Ensure KAuth knows that authentication is now pointless...
0359     for (auto page = d->mPagesPluginIdMap.cbegin(); page != d->mPagesPluginIdMap.cend(); ++page) {
0360         d->mPageWidget->removePage(page.key());
0361     }
0362 
0363     d->mPages.clear();
0364     d->mPagesPluginIdMap.clear();
0365     d->pageChangeSupressed = false;
0366 }
0367 
0368 bool ModuleView::moduleSave()
0369 {
0370     KCModuleProxy *moduleProxy = d->mPages.value(d->mPageWidget->currentPage());
0371     return moduleSave(moduleProxy);
0372 }
0373 
0374 bool ModuleView::moduleSave(KCModuleProxy *module)
0375 {
0376     if (!module) {
0377         return false;
0378     }
0379 
0380     module->save();
0381     Q_EMIT moduleSaved();
0382     return true;
0383 }
0384 
0385 void ModuleView::moduleLoad()
0386 {
0387     KCModuleProxy *activeModule = d->mPages.value(d->mPageWidget->currentPage());
0388     if (activeModule) {
0389         activeModule->load();
0390     }
0391 }
0392 
0393 void ModuleView::moduleDefaults()
0394 {
0395     KCModuleProxy *activeModule = d->mPages.value(d->mPageWidget->currentPage());
0396     if (activeModule) {
0397         activeModule->defaults();
0398     }
0399 }
0400 
0401 void ModuleView::moduleHelp()
0402 {
0403     KCModuleProxy *activeModule = d->mPages.value(d->mPageWidget->currentPage());
0404     if (!activeModule) {
0405         return;
0406     }
0407 
0408     const QString docPath = activeModule->metaData().value(QStringLiteral("X-DocPath"));
0409     if (docPath.isEmpty()) {
0410         return;
0411     }
0412 
0413     // UrlHandler from KGUIAddons sets a handler for help:/ urls, which opens khelpcenter
0414     // if it's available or falls back to opening the relevant page at docs.kde.org
0415     QDesktopServices::openUrl(QUrl(QStringLiteral("help:/") + docPath));
0416 }
0417 
0418 void ModuleView::activeModuleChanged(KPageWidgetItem *current, KPageWidgetItem *previous)
0419 {
0420     d->mPageWidget->blockSignals(true);
0421     d->mPageWidget->setCurrentPage(previous);
0422     KCModuleProxy *previousModule = d->mPages.value(previous);
0423     if (resolveChanges(previousModule)) {
0424         d->mPageWidget->setCurrentPage(current);
0425     }
0426     d->mPageWidget->blockSignals(false);
0427     if (d->pageChangeSupressed) {
0428         return;
0429     }
0430 
0431     // We need to get the state of the now active module
0432     stateChanged();
0433 
0434     KCModuleProxy *activeModule = d->mPages.value(d->mPageWidget->currentPage());
0435     if (!activeModule) {
0436         return;
0437     }
0438 
0439     // TODO: if we'll ever need statistics for kinfocenter modules, save them with an URL like "kinfo:"
0440     if (d->mSaveStatistics && activeModule->metaData().pluginId() != QStringLiteral("kcm_landingpage")) {
0441         KActivities::ResourceInstance::notifyAccessed(QUrl(QStringLiteral("kcm:") + activeModule->metaData().pluginId()),
0442                                                       QStringLiteral("org.kde.systemsettings"));
0443     }
0444 
0445     d->mLayout->setContentsMargins(0, 0, 0, 0);
0446     d->mLayout->setSpacing(0);
0447     d->mButtons->setContentsMargins(style()->pixelMetric(QStyle::PM_LayoutLeftMargin),
0448                                     0, // Remove extra space between KCM content and bottom buttons
0449                                     style()->pixelMetric(QStyle::PM_LayoutRightMargin),
0450                                     style()->pixelMetric(QStyle::PM_LayoutBottomMargin));
0451     d->mPageWidget->layout()->setSpacing(0);
0452     if (auto titleWidget = qobject_cast<KTitleWidget *>(d->mPageWidget->pageHeader())) {
0453         titleWidget->layout()->setContentsMargins(Kirigami::Units().gridUnit(),
0454                                                   style()->pixelMetric(QStyle::PM_LayoutRightMargin),
0455                                                   style()->pixelMetric(QStyle::PM_LayoutRightMargin),
0456                                                   style()->pixelMetric(QStyle::PM_LayoutBottomMargin));
0457     }
0458 
0459     updatePageIconHeader(current);
0460     moduleShowDefaultsIndicators(d->mDefaultsIndicatorsVisible);
0461 }
0462 
0463 void ModuleView::stateChanged()
0464 {
0465     updatePageIconHeader(d->mPageWidget->currentPage());
0466     updateButtons();
0467 
0468     KCModuleProxy *activeModule = d->mPages.value(d->mPageWidget->currentPage());
0469     Q_EMIT moduleChanged(activeModule && activeModule->isChanged());
0470 }
0471 
0472 void ModuleView::updateButtons()
0473 {
0474     KCModuleProxy *activeModule = d->mPages.value(d->mPageWidget->currentPage());
0475     KAuth::Action moduleAction;
0476     bool change = false;
0477     bool defaulted = false;
0478     KCModule::Buttons buttons = KCModule::NoAdditionalButton;
0479 
0480     if (activeModule) {
0481         buttons = activeModule->buttons() & d->mButtonMask;
0482         change = activeModule->isChanged();
0483         defaulted = activeModule->defaulted();
0484 
0485         // Do not display Help button if there is no docPath available
0486         if (activeModule->metaData().value(QStringLiteral("X-DocPath")).isEmpty()) {
0487             buttons &= ~KCModule::Help;
0488         }
0489 
0490         disconnect(d->mApplyAuthorize, SIGNAL(authorized(KAuth::Action)), this, SLOT(moduleSave()));
0491         disconnect(d->mApply, SIGNAL(clicked()), this, SLOT(moduleSave()));
0492         if (activeModule->realModule()->authAction().isValid()) {
0493             connect(d->mApplyAuthorize, SIGNAL(authorized(KAuth::Action)), this, SLOT(moduleSave()));
0494             moduleAction = activeModule->realModule()->authAction();
0495         } else {
0496             connect(d->mApply, SIGNAL(clicked()), this, SLOT(moduleSave()));
0497         }
0498     }
0499 
0500     d->mApplyAuthorize->setAuthAction(moduleAction);
0501     d->mDefault->setEnabled(!defaulted);
0502     d->mDefault->setVisible(buttons & KCModule::Default);
0503     d->mApply->setEnabled(change);
0504     d->mApply->setVisible(buttons & KCModule::Apply);
0505     d->mReset->setEnabled(change);
0506     d->mReset->setVisible(buttons & KCModule::Apply);
0507     d->mHelp->setEnabled(buttons & KCModule::Help);
0508     d->mHelp->setVisible(buttons & KCModule::Help);
0509 
0510     d->mButtons->setVisible(buttons != KCModule::NoAdditionalButton);
0511 }
0512 
0513 void ModuleView::keyPressEvent(QKeyEvent *event)
0514 {
0515     if (event->key() == Qt::Key_F1 && d->mHelp->isVisible() && d->mHelp->isEnabled()) {
0516         d->mHelp->animateClick();
0517         event->accept();
0518         return;
0519     } else if (event->key() == Qt::Key_Escape) {
0520         event->accept();
0521         Q_EMIT closeRequest();
0522         return;
0523     } else if (event->key() == Qt::Key_F1 && event->modifiers() == Qt::ShiftModifier) {
0524         QWhatsThis::enterWhatsThisMode();
0525         event->accept();
0526         return;
0527     }
0528 
0529     QWidget::keyPressEvent(event);
0530 }
0531 
0532 void ModuleView::setFaceType(KPageView::FaceType type)
0533 {
0534     d->mPageWidget->setFaceType(type);
0535 }
0536 
0537 KPageView::FaceType ModuleView::faceType() const
0538 {
0539     return d->mPageWidget->faceType();
0540 }
0541 
0542 void ModuleView::setSaveStatistics(bool save)
0543 {
0544     d->mSaveStatistics = save;
0545 }
0546 
0547 bool ModuleView::saveStatistics() const
0548 {
0549     return d->mSaveStatistics;
0550 }
0551 
0552 void ModuleView::setApplyVisible(bool visible)
0553 {
0554     d->mButtonMask.setFlag(KCModule::Apply, visible);
0555     updateButtons();
0556 }
0557 
0558 bool ModuleView::isApplyVisible() const
0559 {
0560     return d->mApply->isVisible();
0561 }
0562 
0563 void ModuleView::setDefaultsVisible(bool visible)
0564 {
0565     d->mButtonMask.setFlag(KCModule::Default, visible);
0566     updateButtons();
0567 }
0568 
0569 bool ModuleView::isDefaultsVisible() const
0570 {
0571     return d->mDefault->isVisible();
0572 }
0573 
0574 bool ModuleView::isResetVisible() const
0575 {
0576     return d->mReset->isVisible();
0577 }
0578 
0579 void ModuleView::moduleShowDefaultsIndicators(bool show)
0580 {
0581     d->mDefaultsIndicatorsVisible = show;
0582     KCModuleProxy *activeModule = d->mPages.value(d->mPageWidget->currentPage());
0583     if (activeModule) {
0584         activeModule->setDefaultsIndicatorsVisible(show);
0585     }
0586 }
0587 
0588 void ModuleView::setHeaderHeight(qreal height)
0589 {
0590     if (height == d->mCustomHeader->minimumHeight()) {
0591         return;
0592     }
0593 
0594     d->mCustomHeader->setMinimumHeight(height);
0595 }
0596 
0597 qreal ModuleView::headerHeight() const
0598 {
0599     return d->mCustomHeader->minimumHeight();
0600 }
0601 
0602 void ModuleView::setActiveModule(const QString &moduleName)
0603 {
0604     const auto pageList = d->mPagesPluginIdMap.keys();
0605     for (const auto page : pageList) {
0606         if (d->mPagesPluginIdMap.value(page) == moduleName) {
0607             d->mPageWidget->setCurrentPage(page);
0608             break;
0609         }
0610     }
0611 }
0612 
0613 KPluginMetaData ModuleView::activeModuleMetadata() const
0614 {
0615     KCModuleProxy *activeModule = d->mPages.value(d->mPageWidget->currentPage());
0616     if (!activeModule) {
0617         return {};
0618     }
0619     return activeModule->metaData();
0620 }