File indexing completed on 2024-04-14 14:18:24

0001 /*
0002     SPDX-FileCopyrightText: 2021 Nicolas Fella <nicolas.fella@gmx.de>
0003     SPDX-FileCopyrightText: 2021 Alexander Lohnau <alexander.lohnau@gmx.de>
0004 
0005     SPDX-License-Identifier: LGPL-2.0-or-later
0006 */
0007 
0008 #include "kpluginwidget.h"
0009 #include "kpluginproxymodel.h"
0010 #include "kpluginwidget_p.h"
0011 
0012 #include <kcmutils_debug.h>
0013 
0014 #include <QApplication>
0015 #include <QCheckBox>
0016 #include <QDialog>
0017 #include <QDialogButtonBox>
0018 #include <QDir>
0019 #include <QLineEdit>
0020 #include <QPainter>
0021 #include <QPushButton>
0022 #include <QSortFilterProxyModel>
0023 #include <QStandardPaths>
0024 #include <QStyle>
0025 #include <QStyleOptionViewItem>
0026 #include <QVBoxLayout>
0027 
0028 #include <KAboutPluginDialog>
0029 #include <KCategorizedSortFilterProxyModel>
0030 #include <KCategorizedView>
0031 #include <KCategoryDrawer>
0032 #include <KLocalizedString>
0033 #include <KPluginMetaData>
0034 #include <KStandardGuiItem>
0035 #include <kcmoduleinfo.h>
0036 #include <kcmoduleproxy.h>
0037 #include <utility>
0038 
0039 static constexpr int s_margin = 5;
0040 
0041 int KPluginWidgetPrivate::dependantLayoutValue(int value, int width, int totalWidth) const
0042 {
0043     if (listView->layoutDirection() == Qt::LeftToRight) {
0044         return value;
0045     }
0046 
0047     return totalWidth - width - value;
0048 }
0049 
0050 KPluginWidget::KPluginWidget(QWidget *parent)
0051     : QWidget(parent)
0052     , d(new KPluginWidgetPrivate)
0053 {
0054     auto layout = new QVBoxLayout(this);
0055     layout->setContentsMargins(0, 0, 0, 0);
0056 
0057     d->lineEdit = new QLineEdit(this);
0058     d->lineEdit->setClearButtonEnabled(true);
0059     d->lineEdit->setPlaceholderText(i18n("Search..."));
0060     d->listView = new KCategorizedView(this);
0061     d->categoryDrawer = new KCategoryDrawer(d->listView);
0062     d->listView->setVerticalScrollMode(QListView::ScrollPerPixel);
0063     d->listView->setAlternatingRowColors(true);
0064     d->listView->setCategoryDrawer(d->categoryDrawer);
0065 
0066     d->pluginModel = new KPluginModel(this);
0067 
0068     connect(d->pluginModel, &KPluginModel::defaulted, this, &KPluginWidget::defaulted);
0069     connect(d->pluginModel,
0070             &QAbstractItemModel::dataChanged,
0071             this,
0072             [this](const QModelIndex &topLeft, const QModelIndex & /*bottomRight*/, const QVector<int> &roles) {
0073                 if (roles.contains(KPluginModel::EnabledRole)) {
0074                     Q_EMIT pluginEnabledChanged(topLeft.data(KPluginModel::IdRole).toString(), topLeft.data(KPluginModel::EnabledRole).toBool());
0075                     Q_EMIT changed(d->pluginModel->isSaveNeeded());
0076                 }
0077             });
0078 
0079     d->proxyModel = new KPluginProxyModel(this);
0080     d->proxyModel->setSourceModel(d->pluginModel);
0081     d->listView->setModel(d->proxyModel);
0082     d->listView->setAlternatingRowColors(true);
0083 
0084     auto pluginDelegate = new PluginDelegate(d.get(), this);
0085     d->listView->setItemDelegate(pluginDelegate);
0086 
0087     d->listView->setMouseTracking(true);
0088     d->listView->viewport()->setAttribute(Qt::WA_Hover);
0089 
0090     connect(d->lineEdit, &QLineEdit::textChanged, d->proxyModel, [this](const QString &query) {
0091         d->proxyModel->setProperty("query", query);
0092         d->proxyModel->invalidate();
0093     });
0094     connect(pluginDelegate, &PluginDelegate::configCommitted, this, &KPluginWidget::pluginConfigSaved);
0095     connect(pluginDelegate, &PluginDelegate::changed, this, &KPluginWidget::pluginEnabledChanged);
0096 
0097     layout->addWidget(d->lineEdit);
0098     layout->addWidget(d->listView);
0099 
0100     // When a KPluginWidget instance gets focus,
0101     // it should pass over the focus to its child searchbar.
0102     setFocusProxy(d->lineEdit);
0103 }
0104 
0105 KPluginWidget::~KPluginWidget()
0106 {
0107     delete d->listView->itemDelegate();
0108     delete d->listView; // depends on some other things in d, make sure this dies first.
0109 }
0110 
0111 void KPluginWidget::addPlugins(const QVector<KPluginMetaData> &plugins, const QString &categoryLabel)
0112 {
0113     d->pluginModel->addPlugins(plugins, categoryLabel);
0114     d->proxyModel->sort(0);
0115 }
0116 
0117 void KPluginWidget::setConfig(const KConfigGroup &config)
0118 {
0119     d->pluginModel->setConfig(config);
0120 }
0121 
0122 void KPluginWidget::clear()
0123 {
0124     d->pluginModel->clear();
0125 }
0126 
0127 void KPluginWidget::save()
0128 {
0129     d->pluginModel->save();
0130 }
0131 
0132 void KPluginWidget::load()
0133 {
0134     d->pluginModel->load();
0135 }
0136 
0137 void KPluginWidget::defaults()
0138 {
0139     d->pluginModel->defaults();
0140 }
0141 
0142 bool KPluginWidget::isDefault() const
0143 {
0144     for (int i = 0, count = d->pluginModel->rowCount(); i < count; ++i) {
0145         const QModelIndex index = d->pluginModel->index(i, 0);
0146         if (d->pluginModel->data(index, Qt::CheckStateRole).toBool() != d->pluginModel->data(index, KPluginModel::EnabledByDefaultRole).toBool()) {
0147             return false;
0148         }
0149     }
0150 
0151     return true;
0152 }
0153 
0154 bool KPluginWidget::isSaveNeeded() const
0155 {
0156     return d->pluginModel->isSaveNeeded();
0157 }
0158 
0159 void KPluginWidget::setConfigurationArguments(const QStringList &arguments)
0160 {
0161     d->kcmArguments = arguments;
0162 }
0163 
0164 QStringList KPluginWidget::configurationArguments() const
0165 {
0166     return d->kcmArguments;
0167 }
0168 
0169 void KPluginWidget::showConfiguration(const QString &pluginId)
0170 {
0171     QModelIndex idx;
0172     for (int i = 0, c = d->proxyModel->rowCount(); i < c; ++i) {
0173         const auto currentIndex = d->proxyModel->index(i, 0);
0174         const QString id = currentIndex.data(KPluginModel::IdRole).toString();
0175         if (id == pluginId) {
0176             idx = currentIndex;
0177             break;
0178         }
0179     }
0180 
0181     if (idx.isValid()) {
0182         auto delegate = static_cast<PluginDelegate *>(d->listView->itemDelegate());
0183         delegate->configure(idx);
0184     } else {
0185         qCWarning(KCMUTILS_LOG) << "Could not find plugin" << pluginId;
0186     }
0187 }
0188 
0189 void KPluginWidget::setDefaultsIndicatorsVisible(bool isVisible)
0190 {
0191     auto delegate = static_cast<PluginDelegate *>(d->listView->itemDelegate());
0192     delegate->resetModel();
0193 
0194     d->showDefaultIndicator = isVisible;
0195 }
0196 
0197 void KPluginWidget::setAdditionalButtonHandler(const std::function<QPushButton *(const KPluginMetaData &)> &handler)
0198 {
0199     auto delegate = static_cast<PluginDelegate *>(d->listView->itemDelegate());
0200     delegate->handler = handler;
0201 }
0202 
0203 PluginDelegate::PluginDelegate(KPluginWidgetPrivate *pluginSelector_d_ptr, QObject *parent)
0204     : KWidgetItemDelegate(pluginSelector_d_ptr->listView, parent)
0205     , checkBox(new QCheckBox)
0206     , pushButton(new QPushButton)
0207     , pluginSelector_d(pluginSelector_d_ptr)
0208 {
0209     // set the icon to make sure the size can be properly calculated
0210     pushButton->setIcon(QIcon::fromTheme(QStringLiteral("configure")));
0211 }
0212 
0213 PluginDelegate::~PluginDelegate()
0214 {
0215     delete checkBox;
0216     delete pushButton;
0217 }
0218 
0219 void PluginDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const
0220 {
0221     if (!index.isValid()) {
0222         return;
0223     }
0224 
0225     const int xOffset = checkBox->sizeHint().width();
0226     const bool disabled = !index.model()->data(index, KPluginModel::IsChangeableRole).toBool();
0227 
0228     painter->save();
0229 
0230     QApplication::style()->drawPrimitive(QStyle::PE_PanelItemViewItem, &option, painter, nullptr);
0231 
0232     const int iconSize = option.rect.height() - (s_margin * 2);
0233     QIcon icon = QIcon::fromTheme(index.model()->data(index, Qt::DecorationRole).toString());
0234     icon.paint(painter,
0235                QRect(pluginSelector_d->dependantLayoutValue(s_margin + option.rect.left() + xOffset, iconSize, option.rect.width()),
0236                      s_margin + option.rect.top(),
0237                      iconSize,
0238                      iconSize));
0239 
0240     QRect contentsRect(pluginSelector_d->dependantLayoutValue(s_margin * 2 + iconSize + option.rect.left() + xOffset,
0241                                                               option.rect.width() - (s_margin * 3) - iconSize - xOffset,
0242                                                               option.rect.width()),
0243                        s_margin + option.rect.top(),
0244                        option.rect.width() - (s_margin * 3) - iconSize - xOffset,
0245                        option.rect.height() - (s_margin * 2));
0246 
0247     int lessHorizontalSpace = s_margin * 2 + pushButton->sizeHint().width();
0248     if (index.model()->data(index, KPluginModel::ConfigRole).value<KPluginMetaData>().isValid()) {
0249         lessHorizontalSpace += s_margin + pushButton->sizeHint().width();
0250     }
0251     // Reserve space for extra button
0252     if (handler) {
0253         lessHorizontalSpace += s_margin + pushButton->sizeHint().width();
0254     }
0255 
0256     contentsRect.setWidth(contentsRect.width() - lessHorizontalSpace);
0257 
0258     if (option.state & QStyle::State_Selected) {
0259         painter->setPen(option.palette.highlightedText().color());
0260     }
0261 
0262     if (pluginSelector_d->listView->layoutDirection() == Qt::RightToLeft) {
0263         contentsRect.translate(lessHorizontalSpace, 0);
0264     }
0265 
0266     painter->save();
0267     if (disabled) {
0268         QPalette pal(option.palette);
0269         pal.setCurrentColorGroup(QPalette::Disabled);
0270         painter->setPen(pal.text().color());
0271     }
0272 
0273     painter->save();
0274     QFont font = titleFont(option.font);
0275     QFontMetrics fmTitle(font);
0276     painter->setFont(font);
0277     painter->drawText(contentsRect,
0278                       Qt::AlignLeft | Qt::AlignTop,
0279                       fmTitle.elidedText(index.model()->data(index, Qt::DisplayRole).toString(), Qt::ElideRight, contentsRect.width()));
0280     painter->restore();
0281 
0282     painter->drawText(
0283         contentsRect,
0284         Qt::AlignLeft | Qt::AlignBottom,
0285         option.fontMetrics.elidedText(index.model()->data(index, KPluginModel::DescriptionRole).toString(), Qt::ElideRight, contentsRect.width()));
0286 
0287     painter->restore();
0288     painter->restore();
0289 }
0290 
0291 QSize PluginDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const
0292 {
0293     int i = 5;
0294     int j = 1;
0295     if (index.model()->data(index, KPluginModel::ConfigRole).value<KPluginMetaData>().isValid()) {
0296         i = 6;
0297         j = 2;
0298     }
0299     // Reserve space for extra button
0300     if (handler) {
0301         ++j;
0302     }
0303 
0304     const QFont font = titleFont(option.font);
0305     const QFontMetrics fmTitle(font);
0306     const QString text = index.model()->data(index, Qt::DisplayRole).toString();
0307     const QString comment = index.model()->data(index, KPluginModel::DescriptionRole).toString();
0308     const int maxTextWidth = qMax(fmTitle.boundingRect(text).width(), option.fontMetrics.boundingRect(comment).width());
0309 
0310     const auto iconSize = pluginSelector_d->listView->style()->pixelMetric(QStyle::PM_IconViewIconSize);
0311     return QSize(maxTextWidth + iconSize + s_margin * i + pushButton->sizeHint().width() * j,
0312                  qMax(iconSize + s_margin * 2, fmTitle.height() + option.fontMetrics.height() + s_margin * 2));
0313 }
0314 
0315 QList<QWidget *> PluginDelegate::createItemWidgets(const QModelIndex &index) const
0316 {
0317     Q_UNUSED(index);
0318     QList<QWidget *> widgetList;
0319 
0320     auto enabledCheckBox = new QCheckBox;
0321     connect(enabledCheckBox, &QAbstractButton::clicked, this, &PluginDelegate::slotStateChanged);
0322 
0323     auto aboutPushButton = new QPushButton;
0324     aboutPushButton->setIcon(QIcon::fromTheme(QStringLiteral("dialog-information")));
0325     aboutPushButton->setToolTip(i18n("About"));
0326     connect(aboutPushButton, &QAbstractButton::clicked, this, &PluginDelegate::slotAboutClicked);
0327 
0328     auto configurePushButton = new QPushButton;
0329     configurePushButton->setIcon(QIcon::fromTheme(QStringLiteral("configure")));
0330     configurePushButton->setToolTip(i18n("Configure"));
0331     connect(configurePushButton, &QAbstractButton::clicked, this, &PluginDelegate::slotConfigureClicked);
0332 
0333     const static QList<QEvent::Type> blockedEvents{
0334         QEvent::MouseButtonPress,
0335         QEvent::MouseButtonRelease,
0336         QEvent::MouseButtonDblClick,
0337         QEvent::KeyPress,
0338         QEvent::KeyRelease,
0339     };
0340     setBlockedEventTypes(enabledCheckBox, blockedEvents);
0341 
0342     setBlockedEventTypes(aboutPushButton, blockedEvents);
0343 
0344     setBlockedEventTypes(configurePushButton, blockedEvents);
0345 
0346     widgetList << enabledCheckBox << aboutPushButton << configurePushButton;
0347     if (handler) {
0348         QPushButton *btn = handler(pluginSelector_d->pluginModel->data(index, KPluginModel::MetaDataRole).value<KPluginMetaData>());
0349         if (btn) {
0350             widgetList << btn;
0351         }
0352     }
0353 
0354     return widgetList;
0355 }
0356 
0357 void PluginDelegate::updateItemWidgets(const QList<QWidget *> widgets, const QStyleOptionViewItem &option, const QPersistentModelIndex &index) const
0358 {
0359     int extraButtonWidth = 0;
0360     QPushButton *extraButton = nullptr;
0361     if (widgets.count() == 4) {
0362         extraButton = static_cast<QPushButton *>(widgets[3]);
0363         extraButtonWidth = extraButton->sizeHint().width() + s_margin;
0364     }
0365     auto checkBox = static_cast<QCheckBox *>(widgets[0]);
0366     checkBox->resize(checkBox->sizeHint());
0367     checkBox->move(pluginSelector_d->dependantLayoutValue(s_margin, checkBox->sizeHint().width(), option.rect.width()),
0368                    option.rect.height() / 2 - checkBox->sizeHint().height() / 2);
0369 
0370     auto aboutPushButton = static_cast<QPushButton *>(widgets[1]);
0371     const QSize aboutPushButtonSizeHint = aboutPushButton->sizeHint();
0372     aboutPushButton->resize(aboutPushButtonSizeHint);
0373     aboutPushButton->move(pluginSelector_d->dependantLayoutValue(option.rect.width() - s_margin - aboutPushButtonSizeHint.width() - extraButtonWidth,
0374                                                                  aboutPushButtonSizeHint.width(),
0375                                                                  option.rect.width()),
0376                           option.rect.height() / 2 - aboutPushButtonSizeHint.height() / 2);
0377 
0378     auto configurePushButton = static_cast<QPushButton *>(widgets[2]);
0379     const QSize configurePushButtonSizeHint = configurePushButton->sizeHint();
0380     configurePushButton->resize(configurePushButtonSizeHint);
0381     configurePushButton->move(pluginSelector_d->dependantLayoutValue(option.rect.width() - s_margin * 2 - configurePushButtonSizeHint.width()
0382                                                                          - aboutPushButtonSizeHint.width() - extraButtonWidth,
0383                                                                      configurePushButtonSizeHint.width(),
0384                                                                      option.rect.width()),
0385                               option.rect.height() / 2 - configurePushButtonSizeHint.height() / 2);
0386 
0387     if (extraButton) {
0388         const QSize extraPushButtonSizeHint = extraButton->sizeHint();
0389         extraButton->resize(extraPushButtonSizeHint);
0390         extraButton->move(pluginSelector_d->dependantLayoutValue(option.rect.width() - extraButtonWidth, extraPushButtonSizeHint.width(), option.rect.width()),
0391                           option.rect.height() / 2 - extraPushButtonSizeHint.height() / 2);
0392     }
0393 
0394     if (!index.isValid() || !index.internalPointer()) {
0395         checkBox->setVisible(false);
0396         aboutPushButton->setVisible(false);
0397         configurePushButton->setVisible(false);
0398         if (extraButton) {
0399             extraButton->setVisible(false);
0400         }
0401     } else {
0402         const bool enabledByDefault = index.model()->data(index, KPluginModel::EnabledByDefaultRole).toBool();
0403         const bool enabled = index.model()->data(index, KPluginModel::EnabledRole).toBool();
0404         checkBox->setProperty("_kde_highlight_neutral", pluginSelector_d->showDefaultIndicator && enabledByDefault != enabled);
0405         checkBox->setChecked(index.model()->data(index, Qt::CheckStateRole).toBool());
0406         checkBox->setEnabled(index.model()->data(index, KPluginModel::IsChangeableRole).toBool());
0407         configurePushButton->setVisible(index.model()->data(index, KPluginModel::ConfigRole).value<KPluginMetaData>().isValid());
0408         configurePushButton->setEnabled(index.model()->data(index, Qt::CheckStateRole).toBool());
0409     }
0410 }
0411 
0412 void PluginDelegate::slotStateChanged(bool state)
0413 {
0414     if (!focusedIndex().isValid()) {
0415         return;
0416     }
0417 
0418     QModelIndex index = focusedIndex();
0419 
0420     const_cast<QAbstractItemModel *>(index.model())->setData(index, state, Qt::CheckStateRole);
0421 }
0422 
0423 void PluginDelegate::slotAboutClicked()
0424 {
0425     const QModelIndex index = focusedIndex();
0426 
0427     auto pluginMetaData = index.data(KPluginModel::MetaDataRole).value<KPluginMetaData>();
0428 
0429     auto *aboutPlugin = new KAboutPluginDialog(pluginMetaData, itemView());
0430     aboutPlugin->setAttribute(Qt::WA_DeleteOnClose);
0431     aboutPlugin->show();
0432 }
0433 
0434 void PluginDelegate::slotConfigureClicked()
0435 {
0436     configure(focusedIndex());
0437 }
0438 
0439 void PluginDelegate::configure(const QModelIndex &index)
0440 {
0441     const QAbstractItemModel *model = index.model();
0442     const auto kcm = model->data(index, KPluginModel::ConfigRole).value<KPluginMetaData>();
0443 
0444     auto configDialog = new QDialog(itemView());
0445     configDialog->setAttribute(Qt::WA_DeleteOnClose);
0446     configDialog->setModal(true);
0447     configDialog->setWindowTitle(model->data(index, KPluginModel::NameRole).toString());
0448 
0449     auto moduleProxy = new KCModuleProxy(kcm, configDialog, pluginSelector_d->kcmArguments);
0450 
0451     if (!moduleProxy->realModule()) {
0452         delete moduleProxy;
0453         return;
0454     }
0455 
0456     auto layout = new QVBoxLayout(configDialog);
0457     layout->addWidget(moduleProxy);
0458 
0459     auto buttonBox = new QDialogButtonBox(configDialog);
0460     buttonBox->setStandardButtons(QDialogButtonBox::Ok | QDialogButtonBox::Cancel | QDialogButtonBox::RestoreDefaults);
0461     KGuiItem::assign(buttonBox->button(QDialogButtonBox::Ok), KStandardGuiItem::ok());
0462     KGuiItem::assign(buttonBox->button(QDialogButtonBox::Cancel), KStandardGuiItem::cancel());
0463     KGuiItem::assign(buttonBox->button(QDialogButtonBox::RestoreDefaults), KStandardGuiItem::defaults());
0464     connect(buttonBox, &QDialogButtonBox::accepted, configDialog, &QDialog::accept);
0465     connect(buttonBox, &QDialogButtonBox::rejected, configDialog, &QDialog::reject);
0466     connect(configDialog, &QDialog::accepted, this, [moduleProxy, this, model, index]() {
0467         Q_EMIT configCommitted(model->data(index, KPluginModel::IdRole).toString());
0468         moduleProxy->save();
0469     });
0470     connect(configDialog, &QDialog::rejected, this, [moduleProxy]() {
0471         moduleProxy->load();
0472     });
0473 
0474     connect(buttonBox->button(QDialogButtonBox::RestoreDefaults), &QAbstractButton::clicked, this, [moduleProxy] {
0475         moduleProxy->defaults();
0476     });
0477     layout->addWidget(buttonBox);
0478 
0479     configDialog->show();
0480 }
0481 
0482 QFont PluginDelegate::titleFont(const QFont &baseFont) const
0483 {
0484     QFont retFont(baseFont);
0485     retFont.setBold(true);
0486 
0487     return retFont;
0488 }
0489 
0490 #include "moc_kpluginwidget.cpp"
0491 #include "moc_kpluginwidget_p.cpp"