1/*
2 SPDX-FileCopyrightText: 2021 Nicolas Fella <nicolas.fella@gmx.de>
3 SPDX-FileCopyrightText: 2021 Alexander Lohnau <alexander.lohnau@gmx.de>
4
5 SPDX-License-Identifier: LGPL-2.0-or-later
6*/
7
8#include "kpluginwidget.h"
9#include "kcmoduleloader.h"
10#include "kpluginproxymodel.h"
11#include "kpluginwidget_p.h"
12
13#include <kcmutils_debug.h>
14
15#include <QApplication>
16#include <QCheckBox>
17#include <QDialog>
18#include <QDialogButtonBox>
19#include <QDir>
20#include <QLineEdit>
21#include <QPainter>
22#include <QPushButton>
23#include <QSortFilterProxyModel>
24#include <QStandardPaths>
25#include <QStyle>
26#include <QStyleOptionViewItem>
27#include <QVBoxLayout>
28
29#include <KAboutPluginDialog>
30#include <KCategorizedSortFilterProxyModel>
31#include <KCategorizedView>
32#include <KCategoryDrawer>
33#include <KLocalizedString>
34#include <KPluginMetaData>
35#include <KStandardGuiItem>
36#include <utility>
37
38static constexpr int s_margin = 5;
39
40int KPluginWidgetPrivate::dependantLayoutValue(int value, int width, int totalWidth) const
41{
42 if (listView->layoutDirection() == Qt::LeftToRight) {
43 return value;
44 }
45
46 return totalWidth - width - value;
47}
48
49KPluginWidget::KPluginWidget(QWidget *parent)
50 : QWidget(parent)
51 , d(new KPluginWidgetPrivate)
52{
53 auto layout = new QVBoxLayout(this);
54 layout->setContentsMargins(0, 0, 0, 0);
55
56 d->lineEdit = new QLineEdit(this);
57 d->lineEdit->setClearButtonEnabled(true);
58 d->lineEdit->setPlaceholderText(i18n("Search..."));
59 d->listView = new KCategorizedView(this);
60 d->categoryDrawer = new KCategoryDrawer(d->listView);
61 d->listView->setVerticalScrollMode(QListView::ScrollPerPixel);
62 d->listView->setAlternatingRowColors(true);
63 d->listView->setCategoryDrawer(d->categoryDrawer);
64
65 d->pluginModel = new KPluginModel(this);
66
67 connect(d->pluginModel, &KPluginModel::defaulted, this, &KPluginWidget::defaulted);
68 connect(d->pluginModel,
69 &QAbstractItemModel::dataChanged,
70 this,
71 [this](const QModelIndex &topLeft, const QModelIndex & /*bottomRight*/, const QList<int> &roles) {
72 if (roles.contains(KPluginModel::EnabledRole)) {
73 Q_EMIT pluginEnabledChanged(topLeft.data(KPluginModel::IdRole).toString(), topLeft.data(KPluginModel::EnabledRole).toBool());
74 Q_EMIT changed(d->pluginModel->isSaveNeeded());
75 }
76 });
77
78 d->proxyModel = new KPluginProxyModel(this);
79 d->proxyModel->setModel(d->pluginModel);
80 d->listView->setModel(d->proxyModel);
81 d->listView->setAlternatingRowColors(true);
82
83 auto pluginDelegate = new PluginDelegate(d.get(), this);
84 d->listView->setItemDelegate(pluginDelegate);
85
86 d->listView->setMouseTracking(true);
87 d->listView->viewport()->setAttribute(Qt::WA_Hover);
88
89 connect(d->lineEdit, &QLineEdit::textChanged, d->proxyModel, [this](const QString &query) {
90 d->proxyModel->setProperty("query", query);
91 d->proxyModel->invalidate();
92 });
93 connect(pluginDelegate, &PluginDelegate::configCommitted, this, &KPluginWidget::pluginConfigSaved);
94 connect(pluginDelegate, &PluginDelegate::changed, this, &KPluginWidget::pluginEnabledChanged);
95
96 layout->addWidget(d->lineEdit);
97 layout->addWidget(d->listView);
98
99 // When a KPluginWidget instance gets focus,
100 // it should pass over the focus to its child searchbar.
101 setFocusProxy(d->lineEdit);
102}
103
104KPluginWidget::~KPluginWidget()
105{
106 delete d->listView->itemDelegate();
107 delete d->listView; // depends on some other things in d, make sure this dies first.
108}
109
110void KPluginWidget::addPlugins(const QList<KPluginMetaData> &plugins, const QString &categoryLabel)
111{
112 d->pluginModel->addPlugins(plugins, categoryLabel);
113 d->proxyModel->sort(0);
114}
115
116void KPluginWidget::setConfig(const KConfigGroup &config)
117{
118 d->pluginModel->setConfig(config);
119}
120
121void KPluginWidget::clear()
122{
123 d->pluginModel->clear();
124}
125
126void KPluginWidget::save()
127{
128 d->pluginModel->save();
129}
130
131void KPluginWidget::load()
132{
133 d->pluginModel->load();
134}
135
136void KPluginWidget::defaults()
137{
138 d->pluginModel->defaults();
139}
140
141bool KPluginWidget::isDefault() const
142{
143 for (int i = 0, count = d->pluginModel->rowCount(); i < count; ++i) {
144 const QModelIndex index = d->pluginModel->index(i, 0);
145 if (d->pluginModel->data(index, Qt::CheckStateRole).toBool() != d->pluginModel->data(index, KPluginModel::EnabledByDefaultRole).toBool()) {
146 return false;
147 }
148 }
149
150 return true;
151}
152
153bool KPluginWidget::isSaveNeeded() const
154{
155 return d->pluginModel->isSaveNeeded();
156}
157
158void KPluginWidget::setConfigurationArguments(const QVariantList &arguments)
159{
160 d->kcmArguments = arguments;
161}
162
163QVariantList KPluginWidget::configurationArguments() const
164{
165 return d->kcmArguments;
166}
167
168void KPluginWidget::showConfiguration(const QString &pluginId)
169{
170 QModelIndex idx;
171 for (int i = 0, c = d->proxyModel->rowCount(); i < c; ++i) {
172 const auto currentIndex = d->proxyModel->index(i, 0);
173 const QString id = currentIndex.data(KPluginModel::IdRole).toString();
174 if (id == pluginId) {
175 idx = currentIndex;
176 break;
177 }
178 }
179
180 if (idx.isValid()) {
181 auto delegate = static_cast<PluginDelegate *>(d->listView->itemDelegate());
182 delegate->configure(idx);
183 } else {
184 qCWarning(KCMUTILS_LOG) << "Could not find plugin" << pluginId;
185 }
186}
187
188void KPluginWidget::setDefaultsIndicatorsVisible(bool isVisible)
189{
190 auto delegate = static_cast<PluginDelegate *>(d->listView->itemDelegate());
191 delegate->resetModel();
192
193 d->showDefaultIndicator = isVisible;
194}
195
196void KPluginWidget::setAdditionalButtonHandler(const std::function<QPushButton *(const KPluginMetaData &)> &handler)
197{
198 auto delegate = static_cast<PluginDelegate *>(d->listView->itemDelegate());
199 delegate->handler = handler;
200}
201
202PluginDelegate::PluginDelegate(KPluginWidgetPrivate *pluginSelector_d_ptr, QObject *parent)
203 : KWidgetItemDelegate(pluginSelector_d_ptr->listView, parent)
204 , checkBox(new QCheckBox)
205 , pushButton(new QPushButton)
206 , pluginSelector_d(pluginSelector_d_ptr)
207{
208 // set the icon to make sure the size can be properly calculated
209 pushButton->setIcon(QIcon::fromTheme(QStringLiteral("configure")));
210}
211
212PluginDelegate::~PluginDelegate()
213{
214 delete checkBox;
215 delete pushButton;
216}
217
218void PluginDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const
219{
220 if (!index.isValid()) {
221 return;
222 }
223
224 const int xOffset = checkBox->sizeHint().width();
225 const bool disabled = !index.model()->data(index, KPluginModel::IsChangeableRole).toBool();
226
227 painter->save();
228
229 QApplication::style()->drawPrimitive(QStyle::PE_PanelItemViewItem, &option, painter, nullptr);
230
231 const int iconSize = option.rect.height() - (s_margin * 2);
232 QIcon icon = QIcon::fromTheme(index.model()->data(index, Qt::DecorationRole).toString());
233 icon.paint(painter,
234 QRect(pluginSelector_d->dependantLayoutValue(value: s_margin + option.rect.left() + xOffset, width: iconSize, totalWidth: option.rect.width()),
235 s_margin + option.rect.top(),
236 iconSize,
237 iconSize));
238
239 QRect contentsRect(pluginSelector_d->dependantLayoutValue(s_margin * 2 + iconSize + option.rect.left() + xOffset,
240 option.rect.width() - (s_margin * 3) - iconSize - xOffset,
241 option.rect.width()),
242 s_margin + option.rect.top(),
243 option.rect.width() - (s_margin * 3) - iconSize - xOffset,
244 option.rect.height() - (s_margin * 2));
245
246 int lessHorizontalSpace = s_margin * 2 + pushButton->sizeHint().width();
247 if (index.model()->data(index, KPluginModel::ConfigRole).value<KPluginMetaData>().isValid()) {
248 lessHorizontalSpace += s_margin + pushButton->sizeHint().width();
249 }
250 // Reserve space for extra button
251 if (handler) {
252 lessHorizontalSpace += s_margin + pushButton->sizeHint().width();
253 }
254
255 contentsRect.setWidth(contentsRect.width() - lessHorizontalSpace);
256
257 if (option.state & QStyle::State_Selected) {
258 painter->setPen(option.palette.highlightedText().color());
259 }
260
261 if (pluginSelector_d->listView->layoutDirection() == Qt::RightToLeft) {
262 contentsRect.translate(lessHorizontalSpace, 0);
263 }
264
265 painter->save();
266 if (disabled) {
267 QPalette pal(option.palette);
268 pal.setCurrentColorGroup(QPalette::Disabled);
269 painter->setPen(pal.text().color());
270 }
271
272 painter->save();
273 QFont font = titleFont(option.font);
274 QFontMetrics fmTitle(font);
275 painter->setFont(font);
276 painter->drawText(contentsRect,
277 Qt::AlignLeft | Qt::AlignTop,
278 fmTitle.elidedText(index.model()->data(index, Qt::DisplayRole).toString(), Qt::ElideRight, contentsRect.width()));
279 painter->restore();
280
281 painter->drawText(
282 contentsRect,
283 Qt::AlignLeft | Qt::AlignBottom,
284 option.fontMetrics.elidedText(index.model()->data(index, KPluginModel::DescriptionRole).toString(), Qt::ElideRight, contentsRect.width()));
285
286 painter->restore();
287 painter->restore();
288}
289
290QSize PluginDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const
291{
292 int i = 5;
293 int j = 1;
294 if (index.model()->data(index, KPluginModel::ConfigRole).value<KPluginMetaData>().isValid()) {
295 i = 6;
296 j = 2;
297 }
298 // Reserve space for extra button
299 if (handler) {
300 ++j;
301 }
302
303 const QFont font = titleFont(option.font);
304 const QFontMetrics fmTitle(font);
305 const QString text = index.model()->data(index, Qt::DisplayRole).toString();
306 const QString comment = index.model()->data(index, KPluginModel::DescriptionRole).toString();
307 const int maxTextWidth = qMax(fmTitle.boundingRect(text).width(), option.fontMetrics.boundingRect(comment).width());
308
309 const auto iconSize = pluginSelector_d->listView->style()->pixelMetric(QStyle::PM_IconViewIconSize);
310 return QSize(maxTextWidth + iconSize + s_margin * i + pushButton->sizeHint().width() * j,
311 qMax(iconSize + s_margin * 2, fmTitle.height() + option.fontMetrics.height() + s_margin * 2));
312}
313
314QList<QWidget *> PluginDelegate::createItemWidgets(const QModelIndex &index) const
315{
316 Q_UNUSED(index);
317 QList<QWidget *> widgetList;
318
319 auto enabledCheckBox = new QCheckBox;
320 connect(enabledCheckBox, &QAbstractButton::clicked, this, &PluginDelegate::slotStateChanged);
321
322 auto aboutPushButton = new QPushButton;
323 aboutPushButton->setIcon(QIcon::fromTheme(QStringLiteral("dialog-information")));
324 aboutPushButton->setToolTip(i18n("About"));
325 connect(aboutPushButton, &QAbstractButton::clicked, this, &PluginDelegate::slotAboutClicked);
326
327 auto configurePushButton = new QPushButton;
328 configurePushButton->setIcon(QIcon::fromTheme(QStringLiteral("configure")));
329 configurePushButton->setToolTip(i18n("Configure"));
330 connect(configurePushButton, &QAbstractButton::clicked, this, &PluginDelegate::slotConfigureClicked);
331
332 const static QList<QEvent::Type> blockedEvents{
333 QEvent::MouseButtonPress,
334 QEvent::MouseButtonRelease,
335 QEvent::MouseButtonDblClick,
336 QEvent::KeyPress,
337 QEvent::KeyRelease,
338 };
339 setBlockedEventTypes(enabledCheckBox, blockedEvents);
340
341 setBlockedEventTypes(aboutPushButton, blockedEvents);
342
343 setBlockedEventTypes(configurePushButton, blockedEvents);
344
345 widgetList << enabledCheckBox << aboutPushButton << configurePushButton;
346 if (handler) {
347 QPushButton *btn = handler(pluginSelector_d->pluginModel->data(index, KPluginModel::MetaDataRole).value<KPluginMetaData>());
348 if (btn) {
349 widgetList << btn;
350 }
351 }
352
353 return widgetList;
354}
355
356void PluginDelegate::updateItemWidgets(const QList<QWidget *> &widgets, const QStyleOptionViewItem &option, const QPersistentModelIndex &index) const
357{
358 int extraButtonWidth = 0;
359 QPushButton *extraButton = nullptr;
360 if (widgets.count() == 4) {
361 extraButton = static_cast<QPushButton *>(widgets[3]);
362 extraButtonWidth = extraButton->sizeHint().width() + s_margin;
363 }
364 auto checkBox = static_cast<QCheckBox *>(widgets[0]);
365 checkBox->resize(checkBox->sizeHint());
366 checkBox->move(pluginSelector_d->dependantLayoutValue(value: s_margin, width: checkBox->sizeHint().width(), totalWidth: option.rect.width()),
367 option.rect.height() / 2 - checkBox->sizeHint().height() / 2);
368
369 auto aboutPushButton = static_cast<QPushButton *>(widgets[1]);
370 const QSize aboutPushButtonSizeHint = aboutPushButton->sizeHint();
371 aboutPushButton->resize(aboutPushButtonSizeHint);
372 aboutPushButton->move(pluginSelector_d->dependantLayoutValue(value: option.rect.width() - s_margin - aboutPushButtonSizeHint.width() - extraButtonWidth,
373 width: aboutPushButtonSizeHint.width(),
374 totalWidth: option.rect.width()),
375 option.rect.height() / 2 - aboutPushButtonSizeHint.height() / 2);
376
377 auto configurePushButton = static_cast<QPushButton *>(widgets[2]);
378 const QSize configurePushButtonSizeHint = configurePushButton->sizeHint();
379 configurePushButton->resize(configurePushButtonSizeHint);
380 configurePushButton->move(pluginSelector_d->dependantLayoutValue(value: option.rect.width() - s_margin * 2 - configurePushButtonSizeHint.width()
381 - aboutPushButtonSizeHint.width() - extraButtonWidth,
382 width: configurePushButtonSizeHint.width(),
383 totalWidth: option.rect.width()),
384 option.rect.height() / 2 - configurePushButtonSizeHint.height() / 2);
385
386 if (extraButton) {
387 const QSize extraPushButtonSizeHint = extraButton->sizeHint();
388 extraButton->resize(extraPushButtonSizeHint);
389 extraButton->move(pluginSelector_d->dependantLayoutValue(value: option.rect.width() - extraButtonWidth, width: extraPushButtonSizeHint.width(), totalWidth: option.rect.width()),
390 option.rect.height() / 2 - extraPushButtonSizeHint.height() / 2);
391 }
392
393 if (!index.isValid() || !index.internalPointer()) {
394 checkBox->setVisible(false);
395 aboutPushButton->setVisible(false);
396 configurePushButton->setVisible(false);
397 if (extraButton) {
398 extraButton->setVisible(false);
399 }
400 } else {
401 const bool enabledByDefault = index.model()->data(index, KPluginModel::EnabledByDefaultRole).toBool();
402 const bool enabled = index.model()->data(index, KPluginModel::EnabledRole).toBool();
403 checkBox->setProperty("_kde_highlight_neutral", pluginSelector_d->showDefaultIndicator && enabledByDefault != enabled);
404 checkBox->setChecked(index.model()->data(index, Qt::CheckStateRole).toBool());
405 checkBox->setEnabled(index.model()->data(index, KPluginModel::IsChangeableRole).toBool());
406 configurePushButton->setVisible(index.model()->data(index, KPluginModel::ConfigRole).value<KPluginMetaData>().isValid());
407 configurePushButton->setEnabled(index.model()->data(index, Qt::CheckStateRole).toBool());
408 }
409}
410
411void PluginDelegate::slotStateChanged(bool state)
412{
413 if (!focusedIndex().isValid()) {
414 return;
415 }
416
417 QModelIndex index = focusedIndex();
418
419 const_cast<QAbstractItemModel *>(index.model())->setData(index, state, Qt::CheckStateRole);
420}
421
422void PluginDelegate::slotAboutClicked()
423{
424 const QModelIndex index = focusedIndex();
425
426 auto pluginMetaData = index.data(KPluginModel::MetaDataRole).value<KPluginMetaData>();
427
428 auto *aboutPlugin = new KAboutPluginDialog(pluginMetaData, itemView());
429 aboutPlugin->setAttribute(Qt::WA_DeleteOnClose);
430 aboutPlugin->show();
431}
432
433void PluginDelegate::slotConfigureClicked()
434{
435 configure(focusedIndex());
436}
437
438void PluginDelegate::configure(const QModelIndex &index)
439{
440 const QAbstractItemModel *model = index.model();
441 const auto kcm = model->data(index, KPluginModel::ConfigRole).value<KPluginMetaData>();
442
443 auto configDialog = new QDialog(itemView());
444 configDialog->setAttribute(Qt::WA_DeleteOnClose);
445 configDialog->setModal(true);
446 configDialog->setWindowTitle(model->data(index, KPluginModel::NameRole).toString());
447
448 QWidget *kcmWrapper = new QWidget;
449 auto kcmInstance = KCModuleLoader::loadModule(kcm, kcmWrapper, pluginSelector_d->kcmArguments);
450
451 auto layout = new QVBoxLayout(configDialog);
452 layout->addWidget(kcmWrapper);
453
454 auto buttonBox = new QDialogButtonBox(configDialog);
455 buttonBox->setStandardButtons(QDialogButtonBox::Ok | QDialogButtonBox::Cancel | QDialogButtonBox::RestoreDefaults);
456 KGuiItem::assign(buttonBox->button(QDialogButtonBox::Ok), KStandardGuiItem::ok());
457 KGuiItem::assign(buttonBox->button(QDialogButtonBox::Cancel), KStandardGuiItem::cancel());
458 KGuiItem::assign(buttonBox->button(QDialogButtonBox::RestoreDefaults), KStandardGuiItem::defaults());
459 connect(buttonBox, &QDialogButtonBox::accepted, configDialog, &QDialog::accept);
460 connect(buttonBox, &QDialogButtonBox::rejected, configDialog, &QDialog::reject);
461 connect(configDialog, &QDialog::accepted, this, [kcmInstance, this, model, index]() {
462 Q_EMIT configCommitted(model->data(index, KPluginModel::IdRole).toString());
463 kcmInstance->save();
464 });
465 connect(configDialog, &QDialog::rejected, this, [kcmInstance]() {
466 kcmInstance->load();
467 });
468
469 connect(buttonBox->button(QDialogButtonBox::RestoreDefaults), &QAbstractButton::clicked, this, [kcmInstance] {
470 kcmInstance->defaults();
471 });
472 layout->addWidget(buttonBox);
473
474 // Load KCM right before showing it
475 kcmInstance->load();
476 configDialog->show();
477}
478
479QFont PluginDelegate::titleFont(const QFont &baseFont) const
480{
481 QFont retFont(baseFont);
482 retFont.setBold(true);
483
484 return retFont;
485}
486
487#include "moc_kpluginwidget.cpp"
488#include "moc_kpluginwidget_p.cpp"
489

source code of kcmutils/src/kpluginwidget.cpp