File indexing completed on 2024-04-28 07:41:27

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