File indexing completed on 2024-04-28 03:53:01

0001 /*
0002     SPDX-FileCopyrightText: 2000 Matthias Elter <elter@kde.org>
0003     SPDX-FileCopyrightText: 2003 Daniel Molkentin <molkentin@kde.org>
0004     SPDX-FileCopyrightText: 2003, 2006 Matthias Kretz <kretz@kde.org>
0005     SPDX-FileCopyrightText: 2004 Frans Englich <frans.englich@telia.com>
0006     SPDX-FileCopyrightText: 2006 Tobias Koenig <tokoe@kde.org>
0007     SPDX-FileCopyrightText: 2021 Alexander Lohnau <alexander.lohnau@gmx.de>
0008 
0009     SPDX-License-Identifier: LGPL-2.0-or-later
0010 */
0011 
0012 #include "kcmultidialog.h"
0013 #include "kcmoduleloader.h"
0014 #include "kcmoduleqml_p.h"
0015 #include "kcmultidialog_p.h"
0016 #include <kcmutils_debug.h>
0017 
0018 #include <QApplication>
0019 #include <QDesktopServices>
0020 #include <QJsonArray>
0021 #include <QLayout>
0022 #include <QProcess>
0023 #include <QPushButton>
0024 #include <QScreen>
0025 #include <QStandardPaths>
0026 #include <QStringList>
0027 #include <QStyle>
0028 #include <QTimer>
0029 #include <QUrl>
0030 
0031 #include <KGuiItem>
0032 #include <KIconUtils>
0033 #include <KLocalizedString>
0034 #include <KMessageBox>
0035 #include <KPageWidgetModel>
0036 
0037 bool KCMultiDialogPrivate::resolveChanges(KCModule *module)
0038 {
0039     if (!module || !module->needsSave()) {
0040         return true;
0041     }
0042 
0043     // Let the user decide
0044     const int queryUser = KMessageBox::warningTwoActionsCancel(q,
0045                                                                i18n("The settings of the current module have changed.\n"
0046                                                                     "Do you want to apply the changes or discard them?"),
0047                                                                i18n("Apply Settings"),
0048                                                                KStandardGuiItem::apply(),
0049                                                                KStandardGuiItem::discard(),
0050                                                                KStandardGuiItem::cancel());
0051 
0052     switch (queryUser) {
0053     case KMessageBox::PrimaryAction:
0054         return moduleSave(module);
0055 
0056     case KMessageBox::SecondaryAction:
0057         module->load();
0058         return true;
0059 
0060     case KMessageBox::Cancel:
0061         return false;
0062 
0063     default:
0064         Q_ASSERT(false);
0065         return false;
0066     }
0067 }
0068 
0069 void KCMultiDialogPrivate::slotCurrentPageChanged(KPageWidgetItem *current, KPageWidgetItem *previous)
0070 {
0071     KCModule *previousModule = nullptr;
0072     for (int i = 0; i < modules.count(); ++i) {
0073         if (modules[i].item == previous) {
0074             previousModule = modules[i].kcm;
0075         }
0076     }
0077 
0078     // Delete global margins and spacing, since we want the contents to
0079     // be able to touch the edges of the window
0080     q->layout()->setContentsMargins(0, 0, 0, 0);
0081 
0082     const KPageWidget *pageWidget = q->pageWidget();
0083     pageWidget->layout()->setSpacing(0);
0084 
0085     // Then, we set the margins for the title header and the buttonBox footer
0086     const QStyle *style = q->style();
0087     const QMargins layoutMargins = QMargins(style->pixelMetric(QStyle::PM_LayoutLeftMargin),
0088                                             style->pixelMetric(QStyle::PM_LayoutTopMargin),
0089                                             style->pixelMetric(QStyle::PM_LayoutRightMargin),
0090                                             style->pixelMetric(QStyle::PM_LayoutBottomMargin));
0091 
0092     if (pageWidget->pageHeader()) {
0093         pageWidget->pageHeader()->setContentsMargins(layoutMargins);
0094     }
0095 
0096     q->buttonBox()->setContentsMargins(layoutMargins.left(), layoutMargins.top(), layoutMargins.right(), layoutMargins.bottom());
0097 
0098     q->blockSignals(true);
0099     q->setCurrentPage(previous);
0100 
0101     if (resolveChanges(previousModule)) {
0102         q->setCurrentPage(current);
0103     }
0104     q->blockSignals(false);
0105 
0106     // We need to get the state of the now active module
0107     clientChanged();
0108 }
0109 
0110 void KCMultiDialogPrivate::clientChanged()
0111 {
0112     // Get the current module
0113     KCModule *activeModule = nullptr;
0114     bool scheduleFirstShow = false;
0115     for (int i = 0; i < modules.count(); ++i) {
0116         if (modules[i].item == q->currentPage()) {
0117             activeModule = modules[i].kcm;
0118             scheduleFirstShow = activeModule && modules[i].firstShow;
0119             break;
0120         }
0121     }
0122 
0123     // When we first show a module, we call the load method
0124     // Just in case we have multiple loadModule calls in a row, the current module could change
0125     // Meaning we wait for the next tick, check the active module and call load if needed
0126     if (scheduleFirstShow) {
0127         QTimer::singleShot(0, q, [this]() {
0128             for (int i = 0; i < modules.count(); ++i) {
0129                 if (modules[i].firstShow && modules[i].kcm && modules[i].item == q->currentPage()) {
0130                     modules[i].kcm->load();
0131                     modules[i].firstShow = false;
0132                 }
0133             }
0134         });
0135     }
0136 
0137     const bool change = activeModule && activeModule->needsSave();
0138     const bool defaulted = activeModule && activeModule->representsDefaults();
0139     const auto buttons = activeModule ? activeModule->buttons() : KCModule::NoAdditionalButton;
0140 
0141     QPushButton *resetButton = q->buttonBox()->button(QDialogButtonBox::Reset);
0142     if (resetButton) {
0143         resetButton->setVisible(buttons & KCModule::Apply);
0144         resetButton->setEnabled(change);
0145     }
0146 
0147     QPushButton *applyButton = q->buttonBox()->button(QDialogButtonBox::Apply);
0148     if (applyButton) {
0149         applyButton->setVisible(buttons & KCModule::Apply);
0150         applyButton->setEnabled(change);
0151     }
0152 
0153     QPushButton *cancelButton = q->buttonBox()->button(QDialogButtonBox::Cancel);
0154     if (cancelButton) {
0155         cancelButton->setVisible(buttons & KCModule::Apply);
0156     }
0157 
0158     QPushButton *okButton = q->buttonBox()->button(QDialogButtonBox::Ok);
0159     if (okButton) {
0160         okButton->setVisible(buttons & KCModule::Apply);
0161     }
0162 
0163     QPushButton *closeButton = q->buttonBox()->button(QDialogButtonBox::Close);
0164     if (closeButton) {
0165         closeButton->setHidden(buttons & KCModule::Apply);
0166     }
0167 
0168     QPushButton *helpButton = q->buttonBox()->button(QDialogButtonBox::Help);
0169     if (helpButton) {
0170         helpButton->setVisible(buttons & KCModule::Help);
0171     }
0172 
0173     QPushButton *defaultButton = q->buttonBox()->button(QDialogButtonBox::RestoreDefaults);
0174     if (defaultButton) {
0175         defaultButton->setVisible(buttons & KCModule::Default);
0176         defaultButton->setEnabled(!defaulted);
0177     }
0178 }
0179 
0180 void KCMultiDialogPrivate::updateHeader(bool use, const QString &message)
0181 {
0182     KPageWidgetItem *item = q->currentPage();
0183     const auto findIt = std::find_if(modules.cbegin(), modules.cend(), [item](const CreatedModule &module) {
0184         return module.item == item;
0185     });
0186     Q_ASSERT(findIt != modules.cend());
0187 
0188     KCModule *kcm = findIt->kcm;
0189     const QString moduleName = kcm->metaData().name();
0190     const QString icon = kcm->metaData().iconName();
0191 
0192     if (use) {
0193         item->setHeader(QStringLiteral("<b>") + moduleName + QStringLiteral("</b><br><i>") + message + QStringLiteral("</i>"));
0194         item->setIcon(KIconUtils::addOverlay(QIcon::fromTheme(icon), QIcon::fromTheme(QStringLiteral("dialog-warning")), Qt::BottomRightCorner));
0195     } else {
0196         item->setHeader(moduleName);
0197         item->setIcon(QIcon::fromTheme(icon));
0198     }
0199 }
0200 
0201 void KCMultiDialogPrivate::init()
0202 {
0203     q->setFaceType(KPageDialog::Auto);
0204     q->setWindowTitle(i18n("Configure"));
0205     q->setModal(false);
0206 
0207     QDialogButtonBox *buttonBox = new QDialogButtonBox(q);
0208     buttonBox->setStandardButtons(QDialogButtonBox::Help | QDialogButtonBox::RestoreDefaults | QDialogButtonBox::Cancel | QDialogButtonBox::Apply
0209                                   | QDialogButtonBox::Close | QDialogButtonBox::Ok | QDialogButtonBox::Reset);
0210     KGuiItem::assign(buttonBox->button(QDialogButtonBox::Ok), KStandardGuiItem::ok());
0211     KGuiItem::assign(buttonBox->button(QDialogButtonBox::Cancel), KStandardGuiItem::cancel());
0212     KGuiItem::assign(buttonBox->button(QDialogButtonBox::RestoreDefaults), KStandardGuiItem::defaults());
0213     KGuiItem::assign(buttonBox->button(QDialogButtonBox::Apply), KStandardGuiItem::apply());
0214     KGuiItem::assign(buttonBox->button(QDialogButtonBox::Close), KStandardGuiItem::close());
0215     KGuiItem::assign(buttonBox->button(QDialogButtonBox::Reset), KStandardGuiItem::reset());
0216     KGuiItem::assign(buttonBox->button(QDialogButtonBox::Help), KStandardGuiItem::help());
0217     buttonBox->button(QDialogButtonBox::Close)->setVisible(false);
0218     buttonBox->button(QDialogButtonBox::Reset)->setEnabled(false);
0219     buttonBox->button(QDialogButtonBox::Apply)->setEnabled(false);
0220 
0221     q->connect(buttonBox->button(QDialogButtonBox::Apply), &QAbstractButton::clicked, q, &KCMultiDialog::slotApplyClicked);
0222     q->connect(buttonBox->button(QDialogButtonBox::Ok), &QAbstractButton::clicked, q, &KCMultiDialog::slotOkClicked);
0223     q->connect(buttonBox->button(QDialogButtonBox::RestoreDefaults), &QAbstractButton::clicked, q, &KCMultiDialog::slotDefaultClicked);
0224     q->connect(buttonBox->button(QDialogButtonBox::Help), &QAbstractButton::clicked, q, &KCMultiDialog::slotHelpClicked);
0225     q->connect(buttonBox->button(QDialogButtonBox::Reset), &QAbstractButton::clicked, q, &KCMultiDialog::slotUser1Clicked);
0226 
0227     q->setButtonBox(buttonBox);
0228     q->connect(q, &KPageDialog::currentPageChanged, q, [this](KPageWidgetItem *current, KPageWidgetItem *before) {
0229         slotCurrentPageChanged(current, before);
0230     });
0231 }
0232 
0233 KCMultiDialog::KCMultiDialog(QWidget *parent)
0234     : KPageDialog(parent)
0235     , d(new KCMultiDialogPrivate(this))
0236 {
0237     d->init();
0238 }
0239 
0240 KCMultiDialog::~KCMultiDialog() = default;
0241 
0242 void KCMultiDialog::showEvent(QShowEvent *ev)
0243 {
0244     KPageDialog::showEvent(ev);
0245     adjustSize();
0246     /**
0247      * adjustSize() relies on sizeHint but is limited to 2/3 of the desktop size
0248      * Workaround for https://bugreports.qt.io/browse/QTBUG-3459
0249      *
0250      * We adjust the size after passing the show event
0251      * because otherwise window pos is set to (0,0)
0252      */
0253 
0254     const QSize maxSize = screen()->availableGeometry().size();
0255     resize(qMin(sizeHint().width(), maxSize.width()), qMin(sizeHint().height(), maxSize.height()));
0256 }
0257 
0258 void KCMultiDialog::slotDefaultClicked()
0259 {
0260     const KPageWidgetItem *item = currentPage();
0261     if (!item) {
0262         return;
0263     }
0264 
0265     for (int i = 0; i < d->modules.count(); ++i) {
0266         if (d->modules[i].item == item) {
0267             d->modules[i].kcm->defaults();
0268             d->clientChanged();
0269             return;
0270         }
0271     }
0272 }
0273 
0274 void KCMultiDialog::slotUser1Clicked()
0275 {
0276     const KPageWidgetItem *item = currentPage();
0277     if (!item) {
0278         return;
0279     }
0280 
0281     for (int i = 0; i < d->modules.count(); ++i) {
0282         if (d->modules[i].item == item) {
0283             d->modules[i].kcm->load();
0284             d->clientChanged();
0285             return;
0286         }
0287     }
0288 }
0289 
0290 bool KCMultiDialogPrivate::moduleSave(KCModule *module)
0291 {
0292     if (!module) {
0293         return false;
0294     }
0295 
0296     module->save();
0297     return true;
0298 }
0299 
0300 void KCMultiDialogPrivate::apply()
0301 {
0302     for (const CreatedModule &module : std::as_const(modules)) {
0303         KCModule *kcm = module.kcm;
0304 
0305         if (kcm->needsSave()) {
0306             kcm->save();
0307         }
0308     }
0309 
0310     Q_EMIT q->configCommitted();
0311 }
0312 
0313 void KCMultiDialog::slotApplyClicked()
0314 {
0315     QPushButton *applyButton = buttonBox()->button(QDialogButtonBox::Apply);
0316     applyButton->setFocus();
0317 
0318     d->apply();
0319 }
0320 
0321 void KCMultiDialog::slotOkClicked()
0322 {
0323     QPushButton *okButton = buttonBox()->button(QDialogButtonBox::Ok);
0324     okButton->setFocus();
0325 
0326     d->apply();
0327     accept();
0328 }
0329 
0330 void KCMultiDialog::slotHelpClicked()
0331 {
0332     const KPageWidgetItem *item = currentPage();
0333     if (!item) {
0334         return;
0335     }
0336 
0337     QString docPath;
0338     for (int i = 0; i < d->modules.count(); ++i) {
0339         if (d->modules[i].item == item) {
0340             if (docPath.isEmpty()) {
0341                 docPath = d->modules[i].kcm->metaData().value(QStringLiteral("X-DocPath"));
0342             }
0343             break;
0344         }
0345     }
0346 
0347     const QUrl docUrl = QUrl(QStringLiteral("help:/")).resolved(QUrl(docPath)); // same code as in KHelpClient::invokeHelp
0348     const QString docUrlScheme = docUrl.scheme();
0349     const QString helpExec = QStandardPaths::findExecutable(QStringLiteral("khelpcenter"));
0350     const bool foundExec = !helpExec.isEmpty();
0351     if (!foundExec) {
0352         qCDebug(KCMUTILS_LOG) << "Couldn't find khelpcenter executable in PATH.";
0353     }
0354     if (foundExec && (docUrlScheme == QLatin1String("man") || docUrlScheme == QLatin1String("info"))) {
0355         QProcess::startDetached(helpExec, QStringList() << docUrl.toString());
0356     } else {
0357         QDesktopServices::openUrl(docUrl);
0358     }
0359 }
0360 
0361 void KCMultiDialog::closeEvent(QCloseEvent *event)
0362 {
0363     KPageDialog::closeEvent(event);
0364 
0365     for (auto &module : d->modules) {
0366         delete module.kcm;
0367         module.kcm = nullptr;
0368     }
0369 }
0370 
0371 KPageWidgetItem *KCMultiDialog::addModule(const KPluginMetaData &metaData, const QVariantList &args)
0372 {
0373     // Create the scroller
0374     auto *moduleScroll = new UnboundScrollArea(this);
0375     // Prepare the scroll area
0376     moduleScroll->setWidgetResizable(true);
0377     moduleScroll->setFrameStyle(QFrame::NoFrame);
0378     moduleScroll->viewport()->setAutoFillBackground(false);
0379 
0380     KCModule *kcm = KCModuleLoader::loadModule(metaData, moduleScroll, args);
0381     moduleScroll->setWidget(kcm->widget());
0382 
0383     KPageWidgetItem *item = new KPageWidgetItem(moduleScroll, metaData.name());
0384 
0385     KCMultiDialogPrivate::CreatedModule createdModule;
0386     createdModule.kcm = kcm;
0387     createdModule.item = item;
0388     d->modules.append(createdModule);
0389 
0390     if (qobject_cast<KCModuleQml *>(kcm)) {
0391         item->setHeaderVisible(false);
0392     }
0393 
0394     item->setHeader(metaData.name());
0395     item->setIcon(QIcon::fromTheme(metaData.iconName()));
0396     const int weight = metaData.rawData().value(QStringLiteral("X-KDE-Weight")).toInt();
0397     item->setProperty("_k_weight", weight);
0398 
0399     bool updateCurrentPage = false;
0400     const KPageWidgetModel *model = qobject_cast<const KPageWidgetModel *>(pageWidget()->model());
0401     Q_ASSERT(model);
0402     const int siblingCount = model->rowCount();
0403     int row = 0;
0404     for (; row < siblingCount; ++row) {
0405         KPageWidgetItem *siblingItem = model->item(model->index(row, 0));
0406         if (siblingItem->property("_k_weight").toInt() > weight) {
0407             // the item we found is heavier than the new module
0408             // qDebug() << "adding KCM " << item->name() << " before " << siblingItem->name();
0409             insertPage(siblingItem, item);
0410             if (siblingItem == currentPage()) {
0411                 updateCurrentPage = true;
0412             }
0413 
0414             break;
0415         }
0416     }
0417     if (row == siblingCount) {
0418         // the new module is either the first or the heaviest item
0419         // qDebug() << "adding KCM " << item->name() << " at the top level";
0420         addPage(item);
0421     }
0422 
0423     connect(kcm, &KCModule::needsSaveChanged, this, [this]() {
0424         d->clientChanged();
0425     });
0426 
0427     if (d->modules.count() == 1 || updateCurrentPage) {
0428         setCurrentPage(item);
0429         d->clientChanged();
0430     }
0431     return item;
0432 }
0433 
0434 void KCMultiDialog::clear()
0435 {
0436     for (int i = 0; i < d->modules.count(); ++i) {
0437         removePage(d->modules[i].item);
0438     }
0439 
0440     d->modules.clear();
0441 
0442     d->clientChanged();
0443 }
0444 
0445 void KCMultiDialog::setDefaultsIndicatorsVisible(bool show)
0446 {
0447     for (const auto &module : std::as_const(d->modules)) {
0448         module.kcm->setDefaultsIndicatorsVisible(show);
0449     }
0450 }
0451 
0452 #include "moc_kcmultidialog.cpp"